Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Versions follow [SemVer](https://semver.org/) (`0.1.0-alpha.x` while the public

- Dedicated **Desktop Installers** and **Release** GitHub Actions workflows
- Public-release checklist (`docs/PUBLIC_RELEASE.md`) and clearer vulnerability reporting guidance
- Default **PE Engineering Career Framework** (Graduate → CTO) seeded on empty and demo workspaces

### Changed

Expand Down
39 changes: 39 additions & 0 deletions apps/api/src/defaultRoleFramework.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import { DEFAULT_ROLE_FRAMEWORK } from "../src/defaultRoleFramework.js";

describe("default PE role framework pack", () => {
it("includes IC and management ladder from the PE chart", () => {
const keys = DEFAULT_ROLE_FRAMEWORK.roles.map((r) => r.key);
assert.deepEqual(keys, [
"eng_ic1",
"eng_ic2",
"eng_ic3",
"eng_ic4",
"eng_ic5",
"eng_ic6",
"eng_l1",
"eng_m1",
"eng_m2",
"eng_m3",
]);
assert.equal(DEFAULT_ROLE_FRAMEWORK.roles.find((r) => r.key === "eng_ic1")?.title, "Graduate Developer");
assert.equal(DEFAULT_ROLE_FRAMEWORK.roles.find((r) => r.key === "eng_m3")?.title, "Chief Technical Officer (CTO)");
});

it("wires progression next keys and fills responsibilities + expectations", () => {
const byKey = Object.fromEntries(DEFAULT_ROLE_FRAMEWORK.roles.map((r) => [r.key, r]));
assert.deepEqual(byKey.eng_ic1.next, ["eng_ic2"]);
assert.deepEqual(byKey.eng_ic4.next, ["eng_ic5", "eng_l1"]);
assert.deepEqual(byKey.eng_l1.next, ["eng_m1"]);
assert.deepEqual(byKey.eng_m2.next, ["eng_m3"]);
for (const role of DEFAULT_ROLE_FRAMEWORK.roles) {
assert.ok(role.summary.length > 40, role.key);
assert.ok(role.expectations.some((e) => e.kind === "responsibility"), role.key);
assert.ok(role.expectations.some((e) => e.kind === "expectation"), role.key);
for (const next of role.next) {
assert.ok(byKey[next], `${role.key} → missing ${next}`);
}
}
});
});
324 changes: 324 additions & 0 deletions apps/api/src/defaultRoleFramework.ts

Large diffs are not rendered by default.

29 changes: 24 additions & 5 deletions apps/api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import type {
} from "@prm/shared";
import { COMPETENCY_CATALOG, MIDDLE_BAND, competencyLabel } from "@prm/shared";
import { seedWorkspace } from "./seed.js";
import { seedDefaultRoleFramework } from "./seedDefaultRoleFramework.js";
import { buildTeamPerformance } from "./teamPerformance.js";
import type { TeamPerformancePersonInput } from "./teamPerformance.js";
import { loadCycleCompare, loadPersonTimeline, loadRatingTrends, loadSprintPulse } from "./analyticsGather.js";
Expand Down Expand Up @@ -373,24 +374,42 @@ app.post("/api/workspace/init", async (c) => {
})
.run();
db.insert(aiSettings).values({ id: "default", enabled: 0, provider: "anthropic" }).run();
const emName = (body.emName ?? "").trim();
const { frameworkVersionId, roleIds } = seedDefaultRoleFramework(db, {
ownerName: (body.emName ?? "").trim() || "Workspace EM",
});
const emNameTrimmed = (body.emName ?? "").trim();
let emPersonId: string | null = null;
if (emName) {
if (emNameTrimmed) {
emPersonId = id("person");
const createdAt = nowIso();
db.insert(people)
.values({
id: emPersonId,
name: emName,
name: emNameTrimmed,
email: null,
title: "Engineering Manager",
levelKey: "M1",
managerId: null,
hireDate: null,
status: "active",
createdAt: nowIso(),
createdAt,
})
.run();
logActivity("person.create", "person", emPersonId, { name: emName, role: "em" });
const emRoleId = roleIds.eng_m1;
if (emRoleId) {
db.insert(roleAssignments)
.values({
id: id("ra"),
personId: emPersonId,
roleDefinitionId: emRoleId,
frameworkVersionId,
targetNextRoleDefinitionId: roleIds.eng_m2 ?? null,
effectiveFrom: createdAt.slice(0, 10),
assignedByAccountId: null,
})
.run();
}
logActivity("person.create", "person", emPersonId, { name: emNameTrimmed, role: "em" });
}
logActivity("workspace.init", "workspace", wsId);
return c.json({ workspaceId: wsId, token: getSessionToken(), emPersonId });
Expand Down
115 changes: 6 additions & 109 deletions apps/api/src/seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,6 @@ import {
people,
reviews,
roleAssignments,
roleDefinitions,
roleExpectations,
roleFrameworkVersions,
roleFrameworks,
workspace,
} from "@prm/db";
import {
Expand All @@ -25,75 +21,16 @@ import {
nowIso,
paths,
} from "./store.js";

const FRAMEWORK = {
name: "Engineering Career Framework 2026",
roles: [
{
key: "eng_ic3",
title: "Software Engineer (IC3)",
track: "ic" as const,
levelKey: "IC3",
summary: "Delivers well-scoped features with guidance; growing ownership.",
next: ["eng_ic4"],
expectations: [
{ kind: "responsibility" as const, text: "Implements features within a squad with code review support." },
{ kind: "responsibility" as const, text: "Writes tests and documents non-obvious decisions." },
{ kind: "expectation" as const, text: "Unblocks self with timely escalation and options." },
{ kind: "expectation" as const, text: "Collaborates clearly in design and standup forums." },
],
},
{
key: "eng_ic4",
title: "Senior Software Engineer (IC4)",
track: "ic" as const,
levelKey: "IC4",
summary: "Owns multi-sprint outcomes; creates clarity for others.",
next: ["eng_ic5", "eng_m1"],
expectations: [
{ kind: "responsibility" as const, text: "Owns ambiguous projects end-to-end across dependencies." },
{ kind: "responsibility" as const, text: "Raises engineering quality through design and review." },
{ kind: "expectation" as const, text: "Mentors IC3s; multiplies team output." },
{ kind: "expectation" as const, text: "Connects delivery to user/business outcomes." },
],
},
{
key: "eng_ic5",
title: "Staff Software Engineer (IC5)",
track: "ic" as const,
levelKey: "IC5",
summary: "Drives multi-squad technical outcomes and leverage.",
next: [],
expectations: [
{ kind: "responsibility" as const, text: "Sets technical direction for a problem domain." },
{ kind: "expectation" as const, text: "Creates systems/process leverage beyond individual contribution." },
{ kind: "expectation" as const, text: "Influences partner teams without formal authority." },
],
},
{
key: "eng_m1",
title: "Engineering Manager (M1)",
track: "manager" as const,
levelKey: "M1",
summary: "Delivers through a team; coaches and sets clear expectations.",
next: [],
expectations: [
{ kind: "responsibility" as const, text: "Runs effective 1:1s, feedback, and career conversations." },
{ kind: "expectation" as const, text: "Team delivers predictably with healthy culture." },
{ kind: "expectation" as const, text: "Develops engineers toward next-level scope." },
],
},
],
};
import { seedDefaultRoleFramework } from "./seedDefaultRoleFramework.js";

const TEAM = [
{ name: "Alex Rivera", title: "Engineering Manager", levelKey: "M1", roleKey: "eng_m1", isEm: true },
{ name: "Maya Chen", title: "Senior Software Engineer", levelKey: "IC4", roleKey: "eng_ic4", nextKey: "eng_ic5" },
{ name: "Jordan Lee", title: "Software Engineer", levelKey: "IC3", roleKey: "eng_ic3", nextKey: "eng_ic4" },
{ name: "Sam Okonkwo", title: "Senior Software Engineer", levelKey: "IC4", roleKey: "eng_ic4", nextKey: "eng_ic5" },
{ name: "Riley Nguyen", title: "Software Engineer", levelKey: "IC3", roleKey: "eng_ic3", nextKey: "eng_ic4" },
{ name: "Casey Brooks", title: "Software Engineer", levelKey: "IC3", roleKey: "eng_ic3", nextKey: "eng_ic4" },
{ name: "Priya Shah", title: "Senior Software Engineer", levelKey: "IC4", roleKey: "eng_ic4", nextKey: "eng_m1" },
{ name: "Casey Brooks", title: "Junior Software Engineer", levelKey: "IC2", roleKey: "eng_ic2", nextKey: "eng_ic3" },
{ name: "Priya Shah", title: "Senior Software Engineer", levelKey: "IC4", roleKey: "eng_ic4", nextKey: "eng_l1" },
{ name: "Devon Park", title: "Software Engineer", levelKey: "IC3", roleKey: "eng_ic3", nextKey: "eng_ic4" },
];

Expand Down Expand Up @@ -164,50 +101,10 @@ export async function seedWorkspace(opts: {
localOnly: 0,
}).run();

const fwId = id("fw");
const fwvId = id("fwv");
db.insert(roleFrameworks).values({ id: fwId, name: FRAMEWORK.name, createdAt }).run();
db.insert(roleFrameworkVersions).values({
id: fwvId,
frameworkId: fwId,
versionLabel: "2026.1",
status: "published",
publishedAt: createdAt,
sourceFilename: "seeded",
const { frameworkVersionId: fwvId, roleIds } = seedDefaultRoleFramework(db, {
ownerName: "Seed EM",
effectiveFrom: createdAt.slice(0, 10),
effectiveTo: null,
}).run();

const roleIds: Record<string, string> = {};
for (const role of FRAMEWORK.roles) {
const rid = id("role");
roleIds[role.key] = rid;
db.insert(roleDefinitions).values({
id: rid,
frameworkVersionId: fwvId,
key: role.key,
title: role.title,
track: role.track,
levelKey: role.levelKey,
summary: role.summary,
nextRoleKeysJson: JSON.stringify(role.next),
competencyKeysJson: JSON.stringify(
role.track === "manager"
? ["execution", "ownership", "collaboration", "impact", "people_leadership", "org_influence"]
: ["execution", "technical_depth", "ownership", "collaboration", "impact", "growth_leadership"],
),
}).run();
role.expectations.forEach((exp, idx) => {
db.insert(roleExpectations).values({
id: id("rexp"),
roleDefinitionId: rid,
kind: exp.kind,
text: exp.text,
sortOrder: idx,
}).run();
});
}
createdAt,
});

const personIds: Record<string, string> = {};
let emPersonId = "";
Expand Down
68 changes: 68 additions & 0 deletions apps/api/src/seedDefaultRoleFramework.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import {
roleDefinitions,
roleExpectations,
roleFrameworkVersions,
roleFrameworks,
} from "@prm/db";
import { competencyKeysForTrack, DEFAULT_ROLE_FRAMEWORK } from "./defaultRoleFramework.js";
import { id, nowIso } from "./store.js";

type Db = ReturnType<typeof import("./store.js").getDb>;

/** Insert published PE default framework; returns version id + role key → definition id map. */
export function seedDefaultRoleFramework(
db: Db,
opts?: { ownerName?: string; createdAt?: string },
): { frameworkId: string; frameworkVersionId: string; roleIds: Record<string, string> } {
const createdAt = opts?.createdAt ?? nowIso();
const fwId = id("fw");
const fwvId = id("fwv");
db.insert(roleFrameworks)
.values({ id: fwId, name: DEFAULT_ROLE_FRAMEWORK.name, createdAt })
.run();
db.insert(roleFrameworkVersions)
.values({
id: fwvId,
frameworkId: fwId,
versionLabel: DEFAULT_ROLE_FRAMEWORK.versionLabel,
status: "published",
publishedAt: createdAt,
sourceFilename: DEFAULT_ROLE_FRAMEWORK.sourceFilename,
ownerName: opts?.ownerName ?? "Default",
effectiveFrom: createdAt.slice(0, 10),
effectiveTo: null,
})
.run();

const roleIds: Record<string, string> = {};
for (const role of DEFAULT_ROLE_FRAMEWORK.roles) {
const rid = id("role");
roleIds[role.key] = rid;
db.insert(roleDefinitions)
.values({
id: rid,
frameworkVersionId: fwvId,
key: role.key,
title: role.title,
track: role.track,
levelKey: role.levelKey,
summary: role.summary,
nextRoleKeysJson: JSON.stringify(role.next),
competencyKeysJson: JSON.stringify(competencyKeysForTrack(role.track)),
})
.run();
role.expectations.forEach((exp, idx) => {
db.insert(roleExpectations)
.values({
id: id("rexp"),
roleDefinitionId: rid,
kind: exp.kind,
text: exp.text,
sortOrder: idx,
})
.run();
});
}

return { frameworkId: fwId, frameworkVersionId: fwvId, roleIds };
}
2 changes: 1 addition & 1 deletion apps/ui/src/components/OnboardingWizard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export function OnboardingWizard({ onActiveChange }: { onActiveChange?: (active:
<NextStep
tone="brand"
label={data.personCount === 0 ? "Add yourself and your directs" : "Add your directs"}
detail="On Team: you are the EM (no manager). Then add each direct, or paste a CSV. Roles can wait until after the first cycle."
detail="On Team: add directs (you may already be EM). Roles from the PE Engineering Career Framework are already loaded — assign each person on Roles, then open a cycle."
action={
<>
<Link className="btn" to="/team">
Expand Down
2 changes: 1 addition & 1 deletion apps/ui/src/pages/HomePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ export function HomePage() {
<Link to="/team">Team</Link> — add directs (you’re already EM if you entered your name at create)
</li>
<li>
<Link to="/roles">Roles</Link> — assign current (+ optional next) role definitions
<Link to="/roles">Roles</Link> — PE career ladder is preloaded (Graduate → CTO); assign current (+ optional next)
</li>
<li>
<Link to="/cycles">Cycles</Link> — open a review window and export self/peer bundles
Expand Down
3 changes: 2 additions & 1 deletion apps/ui/src/pages/UnlockPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,8 @@ export function UnlockPage({
autoComplete="name"
/>
<p className="muted" style={{ margin: "0.35rem 0 0" }}>
Creates you as the first person on the roster (no manager). You can add directs next.
Creates you as the first person on the roster (Engineering Manager). The PE career framework
(Graduate → CTO) is preloaded so you can assign roles immediately.
</p>
</div>
)}
Expand Down
2 changes: 1 addition & 1 deletion docs/CORE_LOOP_EVAL.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ upload role R&R → tag people → year-round dossier
| Encrypted workspace unlock/lock | Done | AES vault; demo seed password `workbench` (demo only — never a silent API default); `.session` is token-only (re-unlock after API restart) |
| Encrypted backup / restore | Done | Settings → one-click snapshot under `data/backups`; restore replaces live DB while unlocked |
| People list + CSV / manual add | Done | `POST /api/people/import` + Team paste/file; template download |
| Role catalog + assign + author | Done | Seeded ladder; Roles page add/edit/delete; CSV/markdown or AI extract from R&R doc |
| Role catalog + assign + author | Done | Seeded PE Engineering Career Framework (Graduate → CTO, IC + management); Roles page add/edit/delete; CSV/markdown or AI extract from R&R doc |
| Continuous dossier | Done | Achievements, goals, feedback, documents + visibility |
| Concern / underperformance log | Done | Per-person dated concerns + plan; `ai_allowed` default off; opted-in concerns feed digests/drafts |
| Cycle create + phase wizard | Done | Soft statuses (`self_open` → `closed`); editable window; waive-with-reason on missing self/peer |
Expand Down