create-pmos 0.497.0 → 0.499.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/package.json +57 -26
  2. package/payload/AGENTS.md +1 -0
  3. package/payload/KIT-VERSION +1 -1
  4. package/payload/planning/evals/eval-calibration.md +1 -1
  5. package/payload/planning/okrs/health-budgets.md +1 -1
  6. package/payload/scripts/workflow-state.js +28 -0
  7. package/payload/state/discoveries.md +1 -1
  8. package/workspace/briefing-premium-test.js +2 -2
  9. package/workspace/changelog.json +56 -0
  10. package/workspace/guide/briefing-premium.md +4 -2
  11. package/workspace/guide/claude-proposes.md +1 -1
  12. package/workspace/guide/home.md +4 -4
  13. package/workspace/guide/launcher.md +7 -1
  14. package/workspace/lib/briefing-premium.js +38 -12
  15. package/workspace/lib/claude-readiness.js +29 -6
  16. package/workspace/lib/context-use.js +57 -15
  17. package/workspace/lib/engine/workflow-state.js +28 -0
  18. package/workspace/lib/git-identity-cli.js +67 -0
  19. package/workspace/lib/git-identity.js +93 -0
  20. package/workspace/lib/initiative-cli.js +16 -2
  21. package/workspace/lib/pmos-verbs.js +3 -0
  22. package/workspace/lib/routes-boards.js +8 -3
  23. package/workspace/lib/routes-onboard.js +1 -1
  24. package/workspace/lib/transcript-read.js +1 -1
  25. package/workspace/lib/vendor/scripts/workflow-state.js +28 -0
  26. package/workspace/package-lock.json +2 -2
  27. package/workspace/package.json +1 -1
  28. package/workspace/public/pm/bd-ini-panel.js +1 -1
  29. package/workspace/public/pm/cozy-onboard.js +15 -8
  30. package/workspace/public/pm/home-app.js +4 -2
  31. package/workspace/public/pm/home-chat.js +2 -2
  32. package/workspace/public/pm/home-ini.js +1 -1
  33. package/workspace/public/pm/home-need.js +1 -1
  34. package/workspace/public/pm/ini-edit.js +82 -19
  35. package/workspace/public/pm/ini-tasks.js +6 -1
  36. package/workspace/public/pm/ini-view-model.js +3 -1
  37. package/workspace/public/pm/ini-view.css +31 -7
  38. package/workspace/public/pm/premium-board.js +2 -2
  39. package/workspace/stage-card-tasks-test.js +4 -3
  40. package/workspace/test-lists/git-identity.json +5 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-pmos",
3
- "version": "0.497.0",
3
+ "version": "0.499.0",
4
4
  "description": "Scaffold the PMOS portable kit — run the PMOS product-management workflow (initiative → lane-fit spec → contract → build → three gates, with work-type lanes for features, research, maintenance, incidents, and content) entirely inside your own repo, with all state as git-versioned files. Includes `workflow-sweep`, which audits the state every board derives from and separates real defects from work you simply have not done. `create-pmos workspace` also installs + launches the local Terminal Workspace GUI. No database, no network, no telemetry.",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -32,6 +32,62 @@
32
32
  "audit"
33
33
  ],
34
34
  "pmosChanges": [
35
+ {
36
+ "v": "0.499.0",
37
+ "at": "2026-11-03T03:25:28.533Z",
38
+ "f": 3,
39
+ "prs": [
40
+ {
41
+ "n": 1180,
42
+ "t": "The context meter knows the default model is 1M",
43
+ "s": "The context meter knows the default model is 1M",
44
+ "k": "fix"
45
+ },
46
+ {
47
+ "n": 1181,
48
+ "t": "`pmos initiative list` names records on a document lane",
49
+ "s": "Pmos initiative list names records on a document lane",
50
+ "k": "fix"
51
+ },
52
+ {
53
+ "n": 1182,
54
+ "t": "Claude login test passes for a signed-in new user; picker shows each login once",
55
+ "s": "Claude login test passes for a signed-in new user; picker shows each login once",
56
+ "k": "fix"
57
+ }
58
+ ]
59
+ },
60
+ {
61
+ "v": "0.498.0",
62
+ "at": "2026-11-03T02:50:09.805Z",
63
+ "f": 3,
64
+ "prs": [
65
+ {
66
+ "n": 1176,
67
+ "t": "Commit as the repo's own git identity; ask the PM once when it has none",
68
+ "s": "Commit as the repo's own git identity; ask the PM once when it has none",
69
+ "k": "fix"
70
+ },
71
+ {
72
+ "n": 1178,
73
+ "t": "Full task names wrap; add-task chips for stage, type, dates, owner",
74
+ "s": "Full task names wrap; add-task chips for stage, type, dates, owner",
75
+ "k": "new"
76
+ },
77
+ {
78
+ "n": 1179,
79
+ "t": "Each product repo pushes with its own GitHub login",
80
+ "s": "Each product repo pushes with its own GitHub login",
81
+ "k": "new"
82
+ },
83
+ {
84
+ "n": 1177,
85
+ "t": "A workflow routes the initiative; blocked steps ask only as ✦ proposals",
86
+ "s": "A workflow routes the initiative; blocked steps ask only as ✦ proposals",
87
+ "k": "fix"
88
+ }
89
+ ]
90
+ },
35
91
  {
36
92
  "v": "0.497.0",
37
93
  "at": "2026-11-03T02:04:11.405Z",
@@ -366,31 +422,6 @@
366
422
  "k": "fix"
367
423
  }
368
424
  ]
369
- },
370
- {
371
- "v": "0.479.0",
372
- "at": "2026-11-01T10:19:06.142Z",
373
- "f": 3,
374
- "prs": [
375
- {
376
- "n": 1137,
377
- "t": "Batch 21: feedback fixes (held for one merge)",
378
- "s": "Batch 21: feedback fixes (held for one merge)"
379
- }
380
- ]
381
- },
382
- {
383
- "v": "0.478.0",
384
- "at": "2026-11-01T08:32:21.933Z",
385
- "f": 3,
386
- "prs": [
387
- {
388
- "n": 1138,
389
- "t": "Shared-board milestone keeps its place; CI runs only the suites a PR reaches",
390
- "s": "Shared-board milestone keeps its place; CI runs only the suites a PR reaches",
391
- "k": "fix"
392
- }
393
- ]
394
425
  }
395
426
  ]
396
427
  }
package/payload/AGENTS.md CHANGED
@@ -89,6 +89,7 @@ Every initiative carries a `type` — the lane picks what the gate requires **be
89
89
  - **Existing surface → the PM needs the understanding too.** That retrieval warms *you*. When the work touches something that already exists — the test is *would specifying this honestly require reading code that already exists?* — run `skills/explain-surface.skill` on that region **before** the brief/PRD is authored, in a fresh context and **not** by the agent that will write it. Standard for existing surfaces, skipped for greenfield. Without it, the agent acquires the understanding and the **PM approves a spec built on facts they never saw**.
90
90
  4. Anchor: the initiative file must exist at stage `intake`/`spec` with its **lane's** spec/eval artifacts authored **before** build (eval-first, D13, generalized per lane — see the lane table above; `feature` = PRD + rubric; the incident lane alone records post-hoc). Contract per lane: `feature` = the rubric's sprint-contract section with the PM's explicit `PM_approved: true`; `discovery` = the PM-approved spike brief (question + timebox); `chore` = the stated intent; `content` = the PM-approved content brief; `prototype` = the PM-approved quick brief (the judgement exercised + the specimen format) — and **no rubric**, which is the decision, not an omission. No work before the lane's approval. When you question the PM to reach any of these, follow `skills/requirements-elicitation.skill`. Feature-lane `test_method`s SHOULD be executable checks written before build (D58) — wire `templates/product-ci-starter.yml` so tier 1 is never silently empty.
91
91
  5. Branch `initiative/<id>`; execute at control altitude; one commit per artifact.
92
+ - **Commit as the repository's own git identity, and push with its own GitHub login.** Never pass an author on the command line (`-c user.name=…`, `-c user.email=…`, `--author`, `GIT_AUTHOR_*` or `GIT_COMMITTER_*`), and never use a name, email or login from your account, the session or another repository. The PM uses different identities and GitHub logins for different products. Run `pmos git-identity` (or `node "$PMOS_BIN" git-identity`) before the first commit or push in a repository, and again if `git commit` fails with "Author identity unknown" or `git push` fails with "Repository not found". It says what this repository uses, or lists what the PM uses in the other repositories. If something is missing, ask the PM once which identity and GitHub login this product uses, offering those. Save the answer with `pmos git-identity set --name "…" --email "…" --login <login>` (this repository only, never `--global`), then commit and push. After that, `git push` uses that login by itself; run `gh` in this repository as `GH_TOKEN="$(gh auth token --user <login>)" gh …`. Never run `gh auth switch`. Do not leave the PM's files staged.
92
93
  6. Self-evaluate against the contract — self-eval is unreliable; it is an input to the gates, never a verdict.
93
94
  7. Close the gates: **Quality** = this repo's CI plus the kit gate (`pmos/scripts/gate-initiative.sh`
94
95
  + `pmos/planning/evals/check_rubrics.py`); **Review** = score against the rubric with the evaluator skill (adversarial, outcome-only, default-to-refute) and append to `evals.jsonl`; **Acceptance** = the human PM merges and disposes — final and non-delegable. Never merge your own PR.
@@ -1,3 +1,3 @@
1
- kernel-source: pmos@4a32332c
1
+ kernel-source: pmos@02cdbcfc
2
2
  assembled: 2026-11-03
3
3
  mode: vendor
@@ -2,7 +2,7 @@
2
2
  type: Reference
3
3
  title: "Eval calibration log"
4
4
  description: "Append-only record of every PM override of an evaluator verdict."
5
- generated: {by: process:create-pmos, at: "2026-11-03T02:04:07Z"}
5
+ generated: {by: process:create-pmos, at: "2026-11-03T03:25:24Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -2,7 +2,7 @@
2
2
  type: Reference
3
3
  title: "Health budgets"
4
4
  description: "Closed registry of health-budget anchors (D59), edited only by the PM."
5
- generated: {by: process:create-pmos, at: "2026-11-03T02:04:07Z"}
5
+ generated: {by: process:create-pmos, at: "2026-11-03T03:25:24Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -556,6 +556,29 @@ function profileOf(root) {
556
556
  } catch { return 'product-code'; }
557
557
  }
558
558
 
559
+ /*
560
+ * workflowOf(root, id) — the workflow an initiative runs, when its repository runs initiatives on workflows (Boards, Settings ›
561
+ * Workflows). The workspace gives every repository it keeps state for a default board, so every initiative there runs a workflow:
562
+ * an `ini-wf` line in the boards ledger (<state dir>/boards.jsonl, the dirs workspace/server.js stateDirFor reads) names its
563
+ * own, else it runs its board's (Product feature by default). A repository runs on workflows when it holds that ledger or a
564
+ * pmos/state/ dir. PMOS itself holds neither (its state is the workspace's own), so it still routes by lane and R0 still binds it.
565
+ * → the workflow's id, '' (its board's), or null (no workflows). The ledger is read again when its size or mtime changes.
566
+ */
567
+ const wfMemo = new Map();
568
+ function workflowOf(root, id) {
569
+ const f = [['pmos', 'state'], ['state'], ['.pmos']].map((rel) => path.join(root, ...rel, 'boards.jsonl')).find((x) => fs.existsSync(x));
570
+ const on = !!f || fs.existsSync(path.join(root, 'pmos', 'state'));
571
+ if (!on) return null;
572
+ let key = '';
573
+ try { const st = f ? fs.statSync(f) : null; key = st ? st.size + ':' + st.mtimeMs : '-'; } catch { key = '?'; }
574
+ let m = wfMemo.get(root);
575
+ if (!m || m.key !== key) { // a held module sees a new line on the next read (the in-process freshness rule)
576
+ m = { key, wf: new Map() };
577
+ for (const l of f ? jsonl.readJsonl(f).rows : []) if (l && l.kind === 'ini-wf' && typeof l.id === 'string' && typeof l.wf === 'string' && l.wf) m.wf.set(l.id, l.wf);
578
+ wfMemo.set(root, m);
579
+ }
580
+ return m.wf.get(id) || '';
581
+ }
559
582
  /** The lanes a repository's initiatives may declare — what the lane picker offers and the lane write accepts. */
560
583
  function laneNames(profile) { return laneDefs.laneNames(profile === 'pm-artifacts' ? 'pm-artifacts' : 'product-code'); }
561
584
  function lanesFor(root) {
@@ -735,6 +758,7 @@ function computeWorkflowDerived(id, opts) {
735
758
  // ── route ─ the lane is chosen and the two intake questions are answered ─────────────────────
736
759
  const sRoute = specimenRoute(fm.specimen_test);
737
760
  const fRoute = surfaceRoute(fm.surface_check);
761
+ const onWf = workflowOf(root, id);
738
762
  {
739
763
  const missing = [];
740
764
  if (sRoute === 'undeclared' || sRoute === 'malformed') missing.push('specimen_test');
@@ -742,6 +766,10 @@ function computeWorkflowDerived(id, opts) {
742
766
  if (!missing.length) {
743
767
  steps.push(step('route', 'route', 'done',
744
768
  `${lane} lane · specimen test ${sRoute} · surface ${fRoute}`));
769
+ } else if (onWf !== null) {
770
+ // Batch 25 item 6: an initiative on a workflow is routed. The workflow's stages say what comes first, and PMOS proposes a
771
+ // switch when the BRD says another workflow fits, so the two intake questions are not asked of it again.
772
+ steps.push(step('route', 'route', 'done', `${onWf ? `runs the ${onWf} workflow` : 'runs its board\'s workflow'} — the workflow routes it`));
745
773
  } else if (effLane !== 'feature') {
746
774
  // The routing questions are asked of feature work only.
747
775
  steps.push(step('route', 'route', 'done', `${lane} lane — routing questions are for features only (${CUT_NOTE})`));
@@ -2,7 +2,7 @@
2
2
  type: Reference
3
3
  title: "Discoveries and friction log"
4
4
  description: "Append-only record of what runs discovered and where the process rubbed."
5
- generated: {by: process:create-pmos, at: "2026-11-03T02:04:07Z"}
5
+ generated: {by: process:create-pmos, at: "2026-11-03T03:25:24Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -172,11 +172,11 @@ const vw = () => { const v = document.querySelector('#czViewer:not([hidden])');
172
172
  await p.click(`.hm-r[data-id="n:${k4}"] .hm-rm`);
173
173
  await until(() => p.evaluate(() => !!document.querySelector('#czViewer:not([hidden]) [data-czs-act="start"]')));
174
174
  const s0 = await p.evaluate(() => { const v = document.querySelector('#czViewer'); return { line: (v.querySelector('.czs-line') || {}).textContent || '', offer: (v.querySelector('.nv-offer') || {}).textContent || '', cost: (v.querySelector('.nv-cost') || {}).textContent || '', pri: (v.querySelector('.czs-pri') || {}).textContent || '', own: (v.querySelector('.nv-own') || {}).textContent || '' }; });
175
- ok('W8 a blocked row says what is missing, offers ✦ what Claude drafts, says what waiting costs, and holds ONE verb', s0.line === 'Blocked: the spec is missing' && /^I can do the spec/.test(s0.offer) && /^If it waits: /.test(s0.cost) && s0.pri === 'Do the spec' && /Yours to unblock · New feature/.test(s0.own), JSON.stringify(s0));
175
+ ok('W8 a blocked row says what is missing, offers ✦ what Claude drafts (batch 25 item 6: a concrete proposal), says what waiting costs, and holds ONE verb', s0.line === 'Blocked: the spec is missing' && /^I can draft the PRD/.test(s0.offer) && /^If it waits: /.test(s0.cost) && s0.pri === 'Draft the PRD' && /Yours to unblock · New feature/.test(s0.own), JSON.stringify(s0));
176
176
  await p.click('[data-czs-act="start"]');
177
177
  const sp = await until(() => { const l = A.posts.filter((x) => x.path === '/api/agent/start'); return l.length ? l : null; });
178
178
  ok('W8 Start posts one agent start for that row', !!sp && sp.length === 1, JSON.stringify(A.posts.map((x) => [x.path, x.body])));
179
- const s1 = await until(() => p.evaluate(() => { const v = document.querySelector('#czViewer:not([hidden])'); const l = v && v.querySelector('.czs-line'); return l && /^Doing the spec/.test(l.textContent) ? { stop: !!v.querySelector('[data-czs-act="stop"][aria-label="Stop the agent"]'), chat: !!v.querySelector('[data-czs-act="talk"]'), pri: !!v.querySelector('.czs-pri') } : null; }));
179
+ const s1 = await until(() => p.evaluate(() => { const v = document.querySelector('#czViewer:not([hidden])'); const l = v && v.querySelector('.czs-line'); return l && /^Drafting the PRD/.test(l.textContent) ? { stop: !!v.querySelector('[data-czs-act="stop"][aria-label="Stop the agent"]'), chat: !!v.querySelector('[data-czs-act="talk"]'), pri: !!v.querySelector('.czs-pri') } : null; }));
180
180
  ok('W8 after Start the same card says what the agent does now, with Stop and its chat and no other verb (OvA1After)', !!s1 && s1.stop && s1.chat && !s1.pri, JSON.stringify(s1));
181
181
  const mint = await T.nonce(BASE);
182
182
  const writes = A.posts.filter((x) => /briefing-premium\/(decide|put-off)|\/api\/agent\/start/.test(x.path));
@@ -1,4 +1,60 @@
1
1
  [
2
+ {
3
+ "v": "0.499.0",
4
+ "at": "2026-11-03T03:25:28.533Z",
5
+ "f": 3,
6
+ "prs": [
7
+ {
8
+ "n": 1180,
9
+ "t": "The context meter knows the default model is 1M",
10
+ "s": "The context meter knows the default model is 1M",
11
+ "k": "fix"
12
+ },
13
+ {
14
+ "n": 1181,
15
+ "t": "`pmos initiative list` names records on a document lane",
16
+ "s": "Pmos initiative list names records on a document lane",
17
+ "k": "fix"
18
+ },
19
+ {
20
+ "n": 1182,
21
+ "t": "Claude login test passes for a signed-in new user; picker shows each login once",
22
+ "s": "Claude login test passes for a signed-in new user; picker shows each login once",
23
+ "k": "fix"
24
+ }
25
+ ]
26
+ },
27
+ {
28
+ "v": "0.498.0",
29
+ "at": "2026-11-03T02:50:09.805Z",
30
+ "f": 3,
31
+ "prs": [
32
+ {
33
+ "n": 1176,
34
+ "t": "Commit as the repo's own git identity; ask the PM once when it has none",
35
+ "s": "Commit as the repo's own git identity; ask the PM once when it has none",
36
+ "k": "fix"
37
+ },
38
+ {
39
+ "n": 1178,
40
+ "t": "Full task names wrap; add-task chips for stage, type, dates, owner",
41
+ "s": "Full task names wrap; add-task chips for stage, type, dates, owner",
42
+ "k": "new"
43
+ },
44
+ {
45
+ "n": 1179,
46
+ "t": "Each product repo pushes with its own GitHub login",
47
+ "s": "Each product repo pushes with its own GitHub login",
48
+ "k": "new"
49
+ },
50
+ {
51
+ "n": 1177,
52
+ "t": "A workflow routes the initiative; blocked steps ask only as ✦ proposals",
53
+ "s": "A workflow routes the initiative; blocked steps ask only as ✦ proposals",
54
+ "k": "fix"
55
+ }
56
+ ]
57
+ },
2
58
  {
3
59
  "v": "0.497.0",
4
60
  "at": "2026-11-03T02:04:11.405Z",
@@ -2,7 +2,7 @@
2
2
  type: Guide
3
3
  title: "The premium Briefing (/premium.html)"
4
4
  description: "The Briefing page at /premium.html: the hero sentence, What you need to do, Today, the dock, and what each one reads from the repository."
5
- timestamp: 2026-11-02
5
+ timestamp: 2026-11-03
6
6
  ---
7
7
 
8
8
  # The premium Briefing
@@ -133,7 +133,9 @@ The list is the console's **Needs you**: the same rows, in the same order, from
133
133
 
134
134
  Each row is a thing that waits for you. The order is fixed: blocked work first, then decisions only you can make, then what has waited longest. The first row is tagged "Do this first".
135
135
 
136
- A row has two lines: a verb-first title, then the subject and what the repository says is next, joined by a dot. When the line is too long it ends in "…"; the full text is in the row's dialog. "If you skip", which says what stays blocked, is in the dialog too. Press a row to open it. Detail opens first, and the conversation sits beside it. On a phone, the Detail tab is first.
136
+ A row has two lines: a verb-first title, then the subject and what the repository says is next, joined by a dot.
137
+
138
+ **Blocked work is Claude's ✦ proposal.** A blocked initiative shows as a row only when Claude can propose a way through. The title names the proposal (for example, **Draft the PRD**, **Anchor it** or **Start a prototype**), and the second line says what Claude will do: "✦ I'll draft the PRD with its done-criteria…". The initiative page and the chat show the same proposal ("Also waiting on you: ✦ Draft the PRD · Review"), not a question. Press the proposal to approve it, and Claude starts an agent session that does it. Two blocks are never a row: routing, because the initiative's workflow routes it, and an order violation in git history, which no draft can fix. When the line is too long it ends in "…"; the full text is in the row's dialog. "If you skip", which says what stays blocked, is in the dialog too. Press a row to open it. Detail opens first, and the conversation sits beside it. On a phone, the Detail tab is first.
137
139
 
138
140
  The header counts "N of M done". A row counts as done when the record behind it changes, so a row clears by itself. Nothing on this page presses Done for you. The count starts again each day and is kept on this device.
139
141
 
@@ -53,7 +53,7 @@ Press the ✦ to open "Claude proposes" in the Viewer. The groups come in this o
53
53
 
54
54
  In the pill menu on an initiative's row, a session that is idle and safe to close is light blue. Its line starts with ✦ and says why, and its ✓ closes it. An idle session that is not safe has no ✦ and no ✓. Its line says what holds it, such as "3 files not committed".
55
55
 
56
- Each session shows how full its context window is, as a small pie in the pill list, the chat header, the phone top bar and the Live pill. The pie shows from 50% (grey). It turns amber from 70% and red from 90%. Hover it to see the tokens used, such as "156k of 200k used · last turn 10:12". The window is 1M when the session runs a 1M model, else 200k. From 70%, when it is your turn, Claude proposes Compact: the row's ✦ line says so, and its ✓ types /compact into the session. The chat shows the same offer as a chip. A session that is safe to close shows that ✦ instead. ✕ hides the Compact offer until the context is 10 points fuller.
56
+ Each session shows how full its context window is, as a small pie in the pill list, the chat header, the phone top bar and the Live pill. The pie shows from 50% (grey). It turns amber from 70% and red from 90%. Hover it to see the tokens used, such as "156k of 200k used · last turn 10:12". The window is 1M when the session runs a 1M model, else 200k. Claude Code's default model, Opus 5.5, is a 1M model, so a session you start without choosing a model has a 1M window. If CLAUDE_CODE_DISABLE_1M_CONTEXT is set, the window is 200k. From 70%, when it is your turn, Claude proposes Compact: the row's ✦ line says so, and its ✓ types /compact into the session. The chat shows the same offer as a chip. A session that is safe to close shows that ✦ instead. ✕ hides the Compact offer until the context is 10 points fuller.
57
57
 
58
58
  ✓ in the header keeps everything shown. Each proposal has its own ✓ and ✕, and ⌄ shows its rows. ✕ on a row leaves it out of that group for good, with Undo on the row. One toast says what was placed, with Undo. History lists each approval with its own Undo. An initiative that was started stays when you undo: close it on the Board.
59
59
 
@@ -2,7 +2,7 @@
2
2
  type: Guide
3
3
  title: "Home (/premium.html) and the designed features"
4
4
  description: "Home in the Agreed design: the list grouped by product, the Viewer, the place menu, Your day and its New for you fold, Search, and the initiative, meeting, report, Board and Insights features designed after it."
5
- timestamp: 2026-11-01
5
+ timestamp: 2026-11-03
6
6
  ---
7
7
 
8
8
  # Home
@@ -103,8 +103,8 @@ Press an initiative's name in the list, or open `?ini=<id>`. The list stays as i
103
103
  - **Set a Jira ticket's dates.** Press the dates of a Jira ticket in the List, or drag its bar on the Timeline, as you do for a task. PMOS keeps the start and due days and shows them in place of the sprint end. Jira does not change. The status and the name stay Jira's.
104
104
  - **Tasks** show one row (one group) for each stage of the workflow, in order. Finished stages and later stages are never merged into one row; they start folded, and the current stage and any stage with a late task start open.
105
105
  - **Change a task where it is.** In the List, press a task's name. The row opens in place: change the name (Enter saves), the status, and the dates. Only one row is open at a time. Esc closes it. The ⋯ after the dates has **Milestone** (or **Make it a plain task**), **Move to a stage**, **Open the task card** and **Delete task**. A milestone has one date: its due day. Each change shows **Undo**.
106
- - In the List, a row shows the dates at the right. On a phone, it shows only the due day. A task with no dates shows no date; open the row to set them.
107
- - **Add a task to a stage.** Press **+** on an open stage. The field opens at the top of that stage. Enter adds the task and keeps the field open for the next one. **Dates** is optional. Esc closes the field.
106
+ - In the List, a task's full name always shows: a long name wraps onto more lines. A short prefix such as "Bus shelter:" stays in the name, in grey. The dates show at the right. In a narrow panel or on a phone, the dates show under the name. A task with no dates shows no date; open the row to set them.
107
+ - **Add a task to a stage.** Press **+** on an open stage. The field opens at the top of that stage, with one row of chips under it. **Stage** is the stage you pressed + on; press it to pick another stage of the workflow. **Task** can change to **Milestone**: the box becomes ◇ and **Date** takes one day. **Dates** and **Owner** are optional; with no owner, the task is yours. Enter adds the task and keeps the field open for the next one. Esc closes the field.
108
108
  - **Move a task between stages.** On a computer, drag the grip at the left of a row. On a phone, press and hold the task's name, then drag. Drop it on a stage name to put it last in that stage, or between two rows to put it there. The status and the dates do not change. The message shows **Undo**.
109
109
  - A linked Jira ticket opens in place too. Its name and status stay Jira's ("In Progress in Jira · read 4 min ago"). Its dates can change, and it can be a milestone. It has no **Delete**.
110
110
  - **Delete a task.** Press the caret under a task's box (on a phone, press and hold the box) and press the trash at the end of the status picker. The task goes at once and the message shows **Undo**. In the Task Viewer, the trash is the last icon of the Status row, and **Delete** is the last entry of its ⋯ menu. You can delete every PMOS task, also a task that Claude planned in the breakdown and a task that a stage's To finish list made. A deleted To finish task does not come back when the stage is read again or when the initiative moves back to that stage. A Jira ticket has no Delete: change it in Jira. A task deleted in a draft leaves only the draft.
@@ -182,7 +182,7 @@ There is no My work page. The list on Home is your list:
182
182
  - **One chat box.** Every conversation has the same box: **+** (attach a file, notes, the terminal, and **Link to an initiative** for a loose chat), **/** for commands, **@** inside an initiative, the To or In chip, the mic, and Send. A chat opens at its newest message.
183
183
  - **A session's terminal.** **Watch its terminal** in **+**, or a session row such as **Claude sign-in**, opens the session's terminal in the Viewer. While the session runs, it is a real terminal: type in it to answer Claude or paste a code. When the session has ended, the Viewer shows it read only and says so.
184
184
  - **Setting up your first project opens by itself** until a registered folder holds an initiative or setup has finished. The `PMOS` folder that the one-line installer makes holds no initiative yet, so a new user who opens PMOS from the Start menu or the Dock still gets the setup conversation, not an empty Home.
185
- - **Signing in to Claude when you set up your first project.** If the login you choose is signed in already, PMOS does not open a terminal. It tests the login at once. Otherwise PMOS runs `claude auth login`: Claude opens your browser to sign in, and the terminal under the question shows only Claude's request for a code, if it needs one. Claude's theme and trust-this-folder screens do not appear. When the sign-in ends, PMOS tests the login by itself. **The Claude login on this computer** is the login your usual terminal uses (`~/.claude.json`); PMOS no longer points it at a different file.
185
+ - **Signing in to Claude when you set up your first project.** If the login you choose is signed in already, PMOS does not open a terminal. It tests the login at once. Otherwise PMOS runs `claude auth login`: Claude opens your browser to sign in, and the terminal under the question shows only Claude's request for a code, if it needs one. Claude's theme and trust-this-folder screens do not appear. When the sign-in ends, PMOS tests the login by itself. **The Claude login on this computer** is the login your usual terminal uses (`~/.claude.json`); PMOS no longer points it at a different file. You choose between two logins only: **The Claude login on this computer** and **A separate login just for PMOS**. The test passes for a login that works in your terminal. Claude updates its own files when it runs, and the test does not count that as a changed setup.
186
186
  - **Documents not written yet.** A document that does not exist yet shows **Draft it**, in its stage or ahead of it. The agent writes a draft. Nothing is issued until you read it.
187
187
  - **Meeting notes.** A decision, to-do or question that the notes caught is not a fact until you confirm it in the meeting's Replay. Home, Your day and the evening recap do not count unconfirmed points.
188
188
 
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  type: Guide
3
3
  title: "Start the workspace without a terminal"
4
- timestamp: 2026-10-30
4
+ timestamp: 2026-11-03
5
5
  ---
6
6
 
7
7
  # Start the workspace without a terminal
@@ -31,3 +31,9 @@ Type `pmos launcher off`. This removes only launcher files that PMOS owns. It le
31
31
  ## Limits
32
32
 
33
33
  The login item starts the server once at login. Open the app if the server stops. Login does not enable remote access. Mac and Linux launcher output goes to `launcher.log` in the runtime state folder. Windows launchers run without a command window and use the server's normal logs.
34
+
35
+ ## Choose which git identity and GitHub login a repository uses
36
+
37
+ Some computers have no global git name and email, so each repository keeps its own. A computer can also have more than one GitHub login, and a private repository of one login is not found by the other. PMOS does not choose for you, because you can use a different identity and login for each product. When a repository has none, Claude asks you once and offers the ones from your other repositories.
38
+
39
+ Type `pmos git-identity` in the repository to see the identity it commits as and the GitHub login it pushes with. When one is missing, the result lists the ones your other repositories use. To save your choice for this repository only, type `pmos git-identity set --name "Your Name" --email you@example.com --login your-github-login`. To save only the login, type `pmos git-identity set --login your-github-login`. After that, `git push` in the repository uses that login, whichever login `gh` has active. Your global git settings do not change.
@@ -54,8 +54,32 @@ const STEP = {
54
54
  spec: { title: 'Put the spec ahead of the build', skip: 'the build stays ahead of the spec, so the order check keeps failing.' },
55
55
  contract: { title: 'Agree the contract', skip: 'the contract stays unagreed, so the run has no done-criteria to check.' },
56
56
  anchor: { title: 'Anchor the initiative', skip: 'the initiative stays unanchored, and unanchored work is rejected.' },
57
- route: { title: 'Route the initiative', skip: 'the initiative has no lane, so no lane rule can run.' },
57
+ route: { title: 'Route the initiative', skip: 'the initiative has no workflow, so its stages cannot run.' },
58
58
  };
59
+
60
+ /* What Claude proposes for a blocked step (batch 25 item 6). A blocked step reaches the PM only as a ✦ proposal: what Claude will do, in
61
+ * one sentence, that the PM approves (the Start write opens the agent session that does it). A step Claude cannot move by itself is not
62
+ * an ask: route (the initiative's workflow routes it, scripts/workflow-state.js), and an order violation (git history; no draft fixes it).
63
+ * `what` is the agent's task and starts with the verb the press names (pm/home-need.js plainStep makes the label from it).
64
+ * → { label, say, what } | null */
65
+ const PROPOSE = {
66
+ route: () => null,
67
+ anchor: (st) => (/only the PM adds/.test(st.next || '')
68
+ ? { label: 'Anchor it again', say: 'I’ll anchor it again to a goal or health budget you already have, and say which and why.', what: 'anchor it again to a KR or health budget the OKR documents already declare, set parent_kr in the record, and say which and why' }
69
+ : { label: 'Anchor it', say: 'I’ll pick the goal it moves from your goals and add it as its anchor, and say which and why.', what: 'anchor it to the KR or health budget it moves, from the OKR documents: set parent_kr in the record and say which and why' }),
70
+ prototype: (st) => (/cannot follow it/.test(st.next || '') ? null
71
+ : /prototype lane first/.test(st.next || '')
72
+ ? { label: 'Start a prototype', say: 'I’ll start a prototype: a quick brief and two or three specimens you can look at.', what: 'start a prototype with a quick brief and two or three divergent specimens (skills/core/prototype.skill), then set predecessor in the record' }
73
+ : { label: 'Link its prototype', say: 'I’ll link it to its prototype, or start one if there is none.', what: 'link its prototype by setting predecessor in the record to the prototype initiative, or start a prototype if there is none' }),
74
+ spec: (st) => (/^O[12]$/.test(st.code || st.rule || '') || /earlier commit than the build/.test(st.next || '') ? null
75
+ : /intent/.test(st.next || '')
76
+ ? { label: 'Draft the intent', say: 'I’ll write the one-paragraph intent from what the initiative already says.', what: 'draft the one-paragraph intent from what the initiative already says and put it in the record\'s status' }
77
+ : /rubric/.test(st.next || '')
78
+ ? { label: 'Draft the rubric', say: 'I’ll draft the done-criteria rubric from the PRD, for you to review.', what: 'draft the rubric from the PRD (skills/core/eval-rubric.skill), then set rubric_path' }
79
+ : { label: 'Draft the PRD', say: 'I’ll draft the PRD with its done-criteria from what the initiative already says, for you to review.', what: 'draft the PRD with its done-criteria (skills/core/prd.skill) from what the initiative already says, then set prd_path' }),
80
+ contract: () => ({ label: 'Draft the contract', say: 'I’ll draft the done-criteria and how we test them, for you to approve.', what: 'draft the contract with its done-criteria and test method, for the PM to approve, and record it in the runs ledger' }),
81
+ };
82
+ const proposalOf = (st) => { try { return PROPOSE[st.key] ? PROPOSE[st.key](st) : null; } catch { return null; } };
59
83
  const stepOf = (key) => STEP[key] || { title: 'Unblock the ' + key + ' step', skip: 'the ' + key + ' step stays blocked.' };
60
84
 
61
85
  /* The options a decision offers. They name the choices the repository's own rules give (D41 for a tripwire, D60 for a prototype); none is marked
@@ -99,26 +123,28 @@ function blockedRows(f) {
99
123
  for (const i of f.initiatives) {
100
124
  try {
101
125
  if (!live(i)) continue;
102
- const st = (i.steps || []).find((s) => s.status === 'blocked' && s.blocking !== false) || (i.steps || []).find((s) => s.status === 'blocked');
126
+ // the first blocked step Claude can propose a way through, the blocking ones first; a step with no proposal is not an ask
127
+ const blocked = (i.steps || []).filter((s) => s.status === 'blocked');
128
+ const st = [...blocked.filter((s) => s.blocking !== false), ...blocked.filter((s) => s.blocking === false)].find((s) => proposalOf(s));
103
129
  if (!st) continue;
104
- const m = stepOf(st.key);
130
+ const m = stepOf(st.key), pr = proposalOf(st);
105
131
  out.push({
106
- kind: 'blocked', key: 'blocked:' + i.id, title: m.title, subject: clip(i.title, 90),
107
- prepared: st.next ? 'Next step: ' + clip(st.next, 140) : 'No next step is on record.',
132
+ kind: 'blocked', key: 'blocked:' + i.id, title: pr.label, subject: clip(i.title, 90),
133
+ prepared: '✦ ' + pr.say, propose: { label: pr.label, say: pr.say },
108
134
  skip: 'If you skip: ' + m.skip,
109
135
  from: [i.id], stepKey: st.key, order: [-(i.blockers || []).length, i.id],
110
- detail: { id: i.id, step: st.key, rule: st.rule || null, next: st.next == null ? null : String(st.next), record: i.record || null, ...whoOf(i) },
136
+ detail: { id: i.id, step: st.key, rule: st.rule || null, next: pr.what, record: i.record || null, ...whoOf(i) },
111
137
  dialog: tryD(() => ({
112
138
  eye: 'Unblock', logTitle: 'Read from the repository',
113
139
  log: ['Read the initiative record' + (i.record ? ' (' + i.record + ')' : ''), 'Ran the workflow order rules on it', 'Found the step that blocks it: ' + st.key],
114
140
  say: i.id + ' is blocked at the ' + st.key + ' step.' + (st.rule ? ' The rule it does not meet: ' + clip(st.rule, 200) : ''),
115
141
  hold: START_NOTE, head: 'The way through',
116
142
  facts: [['Blocked at', st.key], ['Initiative', i.id], ['The check', st.rule && clip(st.rule, 200)], ['Record', i.record]].filter((x) => x[1]),
117
- opts: st.next ? [{ k: 'next', t: clip(st.next[0].toUpperCase() + st.next.slice(1), 140), d: 'The next step on record.' }] : [],
118
- sel: st.next ? 'next' : null,
119
- nodef: st.next ? '' : 'No next step is on record.',
120
- write: st.next ? { kind: 'start', initiative: i.id, what: clip(st.next, 200), ...(i.ws ? { ws: i.ws } : {}) } : undefined,
121
- prim: { next: 'Start: ' + clip(st.next || '', 40) },
143
+ opts: [{ k: 'next', t: pr.label, d: '✦ ' + pr.say }],
144
+ sel: 'next',
145
+ nodef: '',
146
+ write: { kind: 'start', initiative: i.id, what: clip(pr.what, 200), ...(i.ws ? { ws: i.ws } : {}) },
147
+ prim: { next: '✦ ' + pr.label },
122
148
  pane: { label: 'Initiative record', file: i.record || i.id, h: clip(i.title, 90), rows: fmRows(i.fm) },
123
149
  })),
124
150
  });
@@ -528,7 +554,7 @@ function goalOf(A, raw) {
528
554
  async function readInitiatives({ root, engine }) {
529
555
  const wf = engine ? await run(process.execPath, [engine, '--json', '--root', root], { cwd: root, nice: 10 }) : null;
530
556
  let list;
531
- try { list = JSON.parse(wf).map((x) => ({ id: x.id, title: x.title, lane: x.lane, stage: x.stage, accepted: !!x.accepted, stopped: !!x.stopped, legacy: !!x.legacy, anchor: x.anchor, last_activity: x.last_activity, blockers: x.blockers || [], next: x.next, record: x.record, steps: (x.steps || []).map((s) => ({ key: s.key, status: s.status, next: s.next, rule: (s.basis && s.basis.rule) || s.rule, blocking: s.blocking })) })); } catch { return null; }
557
+ try { list = JSON.parse(wf).map((x) => ({ id: x.id, title: x.title, lane: x.lane, stage: x.stage, accepted: !!x.accepted, stopped: !!x.stopped, legacy: !!x.legacy, anchor: x.anchor, last_activity: x.last_activity, blockers: x.blockers || [], next: x.next, record: x.record, steps: (x.steps || []).map((s) => ({ key: s.key, status: s.status, next: s.next, rule: (s.basis && s.basis.rule) || s.rule, code: s.rule || null, blocking: s.blocking })) })); } catch { return null; }
532
558
  let A = null;
533
559
  try { A = require('./initiative-rows.js').anchorsOf(root); } catch { A = null; }
534
560
  for (const i of list) {
@@ -8,6 +8,7 @@ const cp = require('child_process');
8
8
  const win = require('./win-command');
9
9
  const claudeHome = require('./claude-home');
10
10
  const DEFAULT = '__default__';
11
+ const OWN_ID = require('./own-profile').ID; // PMOS's separate login (lib/own-profile.js)
11
12
  const PROMPT = 'Reply with exactly PMOS_READY. Do not use tools.';
12
13
  const FLAGS = ['--safe-mode', '--tools', '--strict-mcp-config', '--mcp-config', '--disable-slash-commands', '--no-session-persistence', '--output-format'];
13
14
  const MESSAGES = {
@@ -38,7 +39,15 @@ function executable(env) {
38
39
  const file = String(env.PATH || '').split(path.delimiter).map(d => path.join(d, 'claude')).find(f => { try { fs.accessSync(f, fs.constants.X_OK); return fs.statSync(f).isFile(); } catch { return false; } });
39
40
  return { file: file || 'claude', prefix: [] };
40
41
  }
41
- function stamp(f) { try { const s = fs.statSync(f); return [s.mtimeMs, s.size]; } catch { return null; } }
42
+ // The parts of settings.json that decide which account Claude uses: only these are fingerprinted, as a hash, never returned.
43
+ const AUTH_ENV = ['ANTHROPIC_API_KEY', 'ANTHROPIC_AUTH_TOKEN', 'ANTHROPIC_BASE_URL', 'CLAUDE_CODE_USE_BEDROCK', 'CLAUDE_CODE_USE_VERTEX', 'CLAUDE_CODE_USE_FOUNDRY'];
44
+ function settingsAuth(f) {
45
+ let j; try { j = JSON.parse(fs.readFileSync(f, 'utf8')); } catch (e) { return e && e.code === 'ENOENT' ? null : 'unreadable'; }
46
+ if (!j || typeof j !== 'object') return 'unreadable';
47
+ const env = j.env && typeof j.env === 'object' ? j.env : {};
48
+ const picked = { apiKeyHelper: j.apiKeyHelper || '', forceLoginMethod: j.forceLoginMethod || '', forceLoginOrgUUID: j.forceLoginOrgUUID || '', env: AUTH_ENV.map(k => [k, env[k] || '']) };
49
+ return crypto.createHash('sha256').update(JSON.stringify(picked)).digest('hex');
50
+ }
42
51
  function atomic(file, value) {
43
52
  fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
44
53
  if (fs.existsSync(file) && fs.lstatSync(file).isSymbolicLink()) throw new Error('symlink');
@@ -56,18 +65,20 @@ function create({ stateDir, home = os.homedir(), readProfiles, readPrefs, savePr
56
65
  if (typeof id !== 'string' || !/^[a-zA-Z0-9_.-]{1,80}$/.test(id)) return null;
57
66
  // The usual login is Claude with no CLAUDE_CONFIG_DIR at all, unless the PM's own environment sets one. Naming ~/.claude
58
67
  // here made Claude read ~/.claude/.claude.json and miss the login in ~/.claude.json (lib/claude-home.js).
59
- if (id === DEFAULT) { const inherited = envFor(null).CLAUDE_CONFIG_DIR; return { id, label: 'My usual Claude login', env: inherited ? { CLAUDE_CONFIG_DIR: path.resolve(inherited) } : {} }; }
68
+ if (id === DEFAULT) { const inherited = envFor(null).CLAUDE_CONFIG_DIR; return { id, label: 'The Claude login on this computer', env: inherited ? { CLAUDE_CONFIG_DIR: path.resolve(inherited) } : {} }; }
60
69
  return (readProfiles().profiles || []).find(p => p.id === id) || null;
61
70
  }
62
71
  function binding(p) {
63
72
  const env = envFor(p);
64
73
  const command = resolveExecutable(env);
65
74
  const dir = path.resolve(env.CLAUDE_CONFIG_DIR || path.join(home, '.claude'));
66
- const authNames = ['ANTHROPIC_API_KEY', 'ANTHROPIC_AUTH_TOKEN', 'ANTHROPIC_BASE_URL', 'CLAUDE_CODE_USE_BEDROCK', 'CLAUDE_CODE_USE_VERTEX', 'CLAUDE_CODE_USE_FOUNDRY'];
67
75
  // the env as it is applied (a CLAUDE_CONFIG_DIR that names only ~/.claude sets nothing), so an old descriptor and a new one match
68
76
  const applied = claudeHome.overlay(p.env, { home, inherited: envFor(null).CLAUDE_CONFIG_DIR || '' });
69
- const input = { command, executable: stamp(command.file), env: Object.entries(applied).sort(), dir,
70
- auth: authNames.map(k => [k, env[k] || '']), config: [path.join(dir, 'settings.json'), claudeHome.configFile(env, home), path.join(dir, '.credentials.json')].map(stamp) };
77
+ // Only what decides the login: the command, the env, the folder and the auth settings. NOT the timestamps of ~/.claude.json,
78
+ // .credentials.json or the executable: every `claude -p` rewrites ~/.claude.json and may refresh its token, and Claude Code
79
+ // updates itself, so those made every check end "The Claude setup changed during the check" (bug 2026-11-03, new user).
80
+ const input = { command: { file: command.file, prefix: command.prefix }, env: Object.entries(applied).sort(), dir,
81
+ auth: AUTH_ENV.map(k => [k, env[k] || '']), settings: settingsAuth(path.join(dir, 'settings.json')) };
71
82
  return { env, command, fingerprint: crypto.createHash('sha256').update(JSON.stringify(input)).digest('hex') };
72
83
  }
73
84
  // The sentence for a state. A signed-out or unverified login names the login it checked and its folder, because signing in to another
@@ -88,9 +99,21 @@ function create({ stateDir, home = os.homedir(), readProfiles, readPrefs, savePr
88
99
  const state = valid ? r.state : 'unchecked';
89
100
  return { state, message: say(state, p), profile: id, label: p.label || id, checked_at: r && r.checked_at || null, ready: state === 'ready' && selected() === id };
90
101
  }
102
+ /** The logins to choose from, one row per login that really differs (bug 2026-11-03: "My usual Claude login" and "PMOS"
103
+ * showed beside the two options they repeat). A profile with the same binding as the login on this computer is that login,
104
+ * so it folds into it; PMOS's own profile is the "separate login" option, so it is named `own` and not listed again.
105
+ * `alias` maps a folded id to the row that shows it. */
91
106
  function list() {
92
107
  const read = readProfiles();
93
- return { ok: true, selected: selected(), status: status(), profiles: [candidate(DEFAULT), ...(read.profiles || [])].map(p => ({ id: p.id, label: p.label || p.id })), broken: (read.broken || []).map(b => ({ file: b.file })), discovery_failed: read.ok === false };
108
+ const usual = candidate(DEFAULT), usualPrint = binding(usual).fingerprint;
109
+ const alias = {}, rows = [{ id: DEFAULT, label: usual.label }];
110
+ let own = null;
111
+ for (const p of read.profiles || []) {
112
+ if (p.id === OWN_ID) { own = p.id; continue; }
113
+ let same = false; try { same = binding(p).fingerprint === usualPrint; } catch {}
114
+ if (same) alias[p.id] = DEFAULT; else rows.push({ id: p.id, label: p.label || p.id });
115
+ }
116
+ return { ok: true, selected: selected(), status: status(), profiles: rows, alias, own, broken: (read.broken || []).map(b => ({ file: b.file })), discovery_failed: read.ok === false };
94
117
  }
95
118
  function pinDefault(p) {
96
119
  if (p.id !== DEFAULT) return p.id;