create-pmos 0.52.0 → 0.53.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 (212) hide show
  1. package/package.json +1 -1
  2. package/payload/KIT-VERSION +2 -2
  3. package/payload/mods/pmos-cockpit/README.md +13 -0
  4. package/payload/mods/pmos-cockpit/hooks/lib/commands.mjs +2 -1
  5. package/payload/mods/pmos-cockpit/hooks/lib/grants.mjs +41 -11
  6. package/payload/mods/pmos-cockpit/hooks/lib/guard.mjs +228 -8
  7. package/payload/mods/pmos-cockpit/hooks/register.tsx +11 -3
  8. package/payload/planning/evals/eval-calibration.md +1 -1
  9. package/payload/planning/okrs/health-budgets.md +1 -1
  10. package/payload/scripts/plan-items-stage.js +271 -0
  11. package/payload/scripts/pmos-log.sh +18 -7
  12. package/payload/scripts/workflow-state.js +28 -2
  13. package/payload/state/discoveries.md +1 -1
  14. package/payload/state/log.md +1 -1
  15. package/workspace/bin/pmos.js +15 -1
  16. package/workspace/cut-stub.js +40 -0
  17. package/workspace/first-paint-audit.js +145 -0
  18. package/workspace/fixtures/capture/requirements/capture-2026-10-04-1.md +10 -0
  19. package/workspace/fixtures/initiatives-api/list.json +176 -0
  20. package/workspace/fixtures/initiatives-api/post-403.json +5 -0
  21. package/workspace/fixtures/initiatives-api/post-ok.json +10 -0
  22. package/workspace/fixtures/initiatives-api/roadmap.json +208 -0
  23. package/workspace/fixtures/initiatives-api/sheet.json +112 -0
  24. package/workspace/fixtures/report-api/draft.json +177 -0
  25. package/workspace/fixtures/reports-page/draft-biweekly-2026-10-16.md +97 -0
  26. package/workspace/fixtures/today-api/provider-rows.json +204 -0
  27. package/workspace/fixtures/today-api/seam-probe-kinds.json +5 -0
  28. package/workspace/guide/board.md +81 -0
  29. package/workspace/guide/breakdown.md +154 -0
  30. package/workspace/guide/command-palette.md +75 -0
  31. package/workspace/guide/index.md +27 -17
  32. package/workspace/guide/initiatives-roadmap.md +130 -0
  33. package/workspace/guide/initiatives-screen.md +89 -0
  34. package/workspace/guide/initiatives-timeline.md +57 -0
  35. package/workspace/guide/integrated-work.md +94 -0
  36. package/workspace/guide/knowledge-graph.md +3 -68
  37. package/workspace/guide/layout.md +98 -32
  38. package/workspace/guide/live-notes.md +3 -3
  39. package/workspace/guide/pm-only-acts.md +89 -0
  40. package/workspace/guide/pm-only-lines.md +61 -0
  41. package/workspace/guide/reports-page.md +80 -0
  42. package/workspace/guide/reports.md +73 -0
  43. package/workspace/guide/timeline.md +70 -0
  44. package/workspace/guide/today.md +85 -0
  45. package/workspace/guide/validate-with-typesafe.md +1 -1
  46. package/workspace/lib/actor.js +91 -0
  47. package/workspace/lib/board-cache.js +32 -2
  48. package/workspace/lib/board-cli.js +108 -0
  49. package/workspace/lib/board-fold.js +232 -0
  50. package/workspace/lib/breakdown-export.js +80 -0
  51. package/workspace/lib/capture.js +98 -0
  52. package/workspace/lib/engine/plan-items-stage.js +271 -0
  53. package/workspace/lib/engine/workflow-state.js +28 -2
  54. package/workspace/lib/factory-join.js +158 -0
  55. package/workspace/lib/grant-mint.js +113 -0
  56. package/workspace/lib/health-rules-factory.js +92 -0
  57. package/workspace/lib/health-rules-plan.js +164 -0
  58. package/workspace/lib/health.js +79 -0
  59. package/workspace/lib/initiative-cli.js +222 -0
  60. package/workspace/lib/initiative-display.js +183 -0
  61. package/workspace/lib/initiative-needs.js +127 -0
  62. package/workspace/lib/initiative-rows.js +206 -0
  63. package/workspace/lib/initiative-writer.js +141 -0
  64. package/workspace/lib/issues.js +206 -0
  65. package/workspace/lib/item-cli.js +195 -0
  66. package/workspace/lib/item-spans.js +137 -0
  67. package/workspace/lib/measure-cli.js +100 -0
  68. package/workspace/lib/measurements.js +189 -0
  69. package/workspace/lib/mobile-nav.js +57 -53
  70. package/workspace/lib/needs-providers.js +120 -0
  71. package/workspace/lib/placements.js +440 -0
  72. package/workspace/lib/plan-items.js +922 -0
  73. package/workspace/lib/plan-ledgers.js +85 -0
  74. package/workspace/lib/plan-rules.js +72 -0
  75. package/workspace/lib/plan-tree.js +367 -0
  76. package/workspace/lib/pm-guard.js +174 -0
  77. package/workspace/lib/pm-lines-needs.js +331 -0
  78. package/workspace/lib/pmos-verbs.js +66 -0
  79. package/workspace/lib/proactive.js +7 -4
  80. package/workspace/lib/report-cli.js +152 -0
  81. package/workspace/lib/report-draft.js +579 -0
  82. package/workspace/lib/report-needs.js +213 -0
  83. package/workspace/lib/route-modules.js +128 -0
  84. package/workspace/lib/routes-board.js +91 -0
  85. package/workspace/lib/routes-capture.js +52 -0
  86. package/workspace/lib/routes-initiatives.js +371 -0
  87. package/workspace/lib/routes-reports.js +163 -0
  88. package/workspace/lib/routes-timeline.js +197 -0
  89. package/workspace/lib/routes-today.js +32 -0
  90. package/workspace/lib/today-cli.js +99 -0
  91. package/workspace/lib/today.js +333 -0
  92. package/workspace/lib/triage.js +163 -0
  93. package/workspace/lib/verify-work-cli.js +81 -0
  94. package/workspace/lib/work-check-needs.js +99 -0
  95. package/workspace/package-lock.json +1 -1
  96. package/workspace/package.json +4 -5
  97. package/workspace/public/index.html +29 -19
  98. package/workspace/public/pm/app.js +299 -30
  99. package/workspace/public/pm/bd-board.js +113 -0
  100. package/workspace/public/pm/bd-drag.js +59 -0
  101. package/workspace/public/pm/bd-model.js +152 -0
  102. package/workspace/public/pm/bd-timeline.js +233 -0
  103. package/workspace/public/pm/bd-tree.js +244 -0
  104. package/workspace/public/pm/board-copy.js +57 -0
  105. package/workspace/public/pm/breakdown-api.js +58 -0
  106. package/workspace/public/pm/breakdown-copy.js +138 -0
  107. package/workspace/public/pm/components.css +3054 -0
  108. package/workspace/public/pm/cycle-review.js +125 -0
  109. package/workspace/public/pm/data.js +40 -187
  110. package/workspace/public/pm/dest-board.js +438 -0
  111. package/workspace/public/pm/dest-initiatives.js +134 -0
  112. package/workspace/public/pm/dest-reports.js +270 -0
  113. package/workspace/public/pm/dest-settings.js +217 -0
  114. package/workspace/public/pm/dest-today.js +176 -0
  115. package/workspace/public/pm/dialogs.js +88 -0
  116. package/workspace/public/pm/display-word.js +56 -0
  117. package/workspace/public/pm/dock.js +105 -0
  118. package/workspace/public/pm/fixture.js +8 -227
  119. package/workspace/public/pm/frame-copy.js +94 -0
  120. package/workspace/public/pm/handoff-box.js +82 -0
  121. package/workspace/public/pm/http.js +88 -0
  122. package/workspace/public/pm/icons.js +53 -0
  123. package/workspace/public/pm/initiative-list.js +218 -0
  124. package/workspace/public/pm/initiative-sheet.js +261 -0
  125. package/workspace/public/pm/initiatives-api.js +59 -0
  126. package/workspace/public/pm/initiatives-copy.js +215 -0
  127. package/workspace/public/pm/initiatives-timeline-copy.js +59 -0
  128. package/workspace/public/pm/initiatives-timeline.js +410 -0
  129. package/workspace/public/pm/initiatives-ui.js +123 -0
  130. package/workspace/public/pm/item-sheet.js +251 -0
  131. package/workspace/public/pm/keep-card.js +204 -0
  132. package/workspace/public/pm/lifecycle.js +7 -1
  133. package/workspace/public/pm/mobile.css +49 -20
  134. package/workspace/public/pm/mobile.js +157 -29
  135. package/workspace/public/pm/modes-live.js +7 -883
  136. package/workspace/public/pm/modes.js +32 -1024
  137. package/workspace/public/pm/new-initiative-sheet.js +74 -0
  138. package/workspace/public/pm/next-step-ui.js +3 -2
  139. package/workspace/public/pm/palette-copy.js +51 -0
  140. package/workspace/public/pm/palette.js +312 -0
  141. package/workspace/public/pm/panel-breakdown.js +702 -0
  142. package/workspace/public/pm/panel-cockpit.js +63 -295
  143. package/workspace/public/pm/panel-detail.js +25 -172
  144. package/workspace/public/pm/panel-initiatives.js +1 -19
  145. package/workspace/public/pm/panel-repos.js +31 -81
  146. package/workspace/public/pm/panel-tabs.js +197 -110
  147. package/workspace/public/pm/panel-trees.js +5 -2
  148. package/workspace/public/pm/portfolio-report.js +61 -0
  149. package/workspace/public/pm/primitives.js +656 -0
  150. package/workspace/public/pm/print.css +33 -0
  151. package/workspace/public/pm/provider-cards.js +136 -0
  152. package/workspace/public/pm/registry.js +194 -0
  153. package/workspace/public/pm/release-card.js +110 -0
  154. package/workspace/public/pm/report-view.js +309 -0
  155. package/workspace/public/pm/reports-copy.js +114 -0
  156. package/workspace/public/pm/roadmap.js +271 -0
  157. package/workspace/public/pm/sig-watch.js +41 -0
  158. package/workspace/public/pm/state.js +8 -4
  159. package/workspace/public/pm/status-banner.js +13 -5
  160. package/workspace/public/pm/tab-doc.js +2 -4
  161. package/workspace/public/pm/tab-term.js +12 -2
  162. package/workspace/public/pm/tab-web.js +3 -2
  163. package/workspace/public/pm/timeline-copy.js +48 -0
  164. package/workspace/public/pm/today-cards.js +244 -0
  165. package/workspace/public/pm/today-copy.js +97 -0
  166. package/workspace/public/pm/tokens.css +16 -18
  167. package/workspace/public/pm/topbar.js +102 -0
  168. package/workspace/public/pm/triage-card.js +60 -0
  169. package/workspace/public/pm/undo-hold.js +103 -0
  170. package/workspace/public/pm/view-addrepo.js +2 -1
  171. package/workspace/public/pm/view-livenotes.js +3 -1
  172. package/workspace/public/pm/view-today.js +7 -162
  173. package/workspace/public/vendor/Sortable.min.js +2 -0
  174. package/workspace/public/vendor/lucide-sprite.js +79 -0
  175. package/workspace/replaced-tests.json +2864 -0
  176. package/workspace/run-tests.js +56 -5
  177. package/workspace/server.js +312 -36
  178. package/workspace/sheet-answer.js +61 -0
  179. package/workspace/skills/measure.skill +41 -0
  180. package/workspace/skills/report-biweekly.skill +23 -25
  181. package/workspace/skills/tracker-refresh.skill +50 -0
  182. package/workspace/test-files.json +19 -3
  183. package/workspace/test-lists/base.json +6 -0
  184. package/workspace/test-lists/run2-a.json +6 -0
  185. package/workspace/test-lists/run2-b.json +13 -0
  186. package/workspace/test-lists/run2-c.json +19 -0
  187. package/workspace/test-lists/run2-d.json +22 -0
  188. package/workspace/test-lists/run3-0.json +5 -0
  189. package/workspace/test-lists/run3-a.json +5 -0
  190. package/workspace/test-lists/run3-b.json +6 -0
  191. package/workspace/test-lists/run3-c.json +15 -0
  192. package/workspace/test-lists/run3-d.json +6 -0
  193. package/workspace/test-lists/run4-a.json +8 -0
  194. package/workspace/test-lists/run4-b.json +6 -0
  195. package/workspace/test-lists/run4-c.json +13 -0
  196. package/workspace/test-lists/run4-d.json +9 -0
  197. package/workspace/test-lists/run5-a.json +6 -0
  198. package/workspace/test-lists/run5-b.json +6 -0
  199. package/workspace/test-lists/run5-c.json +15 -0
  200. package/workspace/test-run.js +27 -1
  201. package/workspace/third-party/README.md +44 -0
  202. package/workspace/third-party/lucide-static-1.52.0.LICENSE +43 -0
  203. package/workspace/third-party/sortablejs-1.15.7.LICENSE +21 -0
  204. package/workspace/design-parity.js +0 -673
  205. package/workspace/public/pm/tab-task.js +0 -86
  206. package/workspace/public/pm/view-guide.js +0 -141
  207. package/workspace/public/pm/view-knowledge.js +0 -225
  208. package/workspace/public/pm/view-memory.js +0 -122
  209. package/workspace/public/pm/view-portfolio.js +0 -153
  210. package/workspace/public/pm/view-reports.js +0 -190
  211. package/workspace/public/pm/view-scheduled.js +0 -161
  212. package/workspace/public/pm/view-skills.js +0 -216
@@ -5,7 +5,7 @@
5
5
  // character: the comparator diffs rendered text, so a reworded label is a bug.
6
6
  //
7
7
  // Pure data + pure functions. No DOM, no fetch, no state. Where the prototype
8
- // SYNTHESISES data (tasks from steps, faux-shell replies, doc bodies, new
8
+ // SYNTHESISES data (faux-shell replies, doc bodies, new
9
9
  // initiative records) the synthesis function is ported alongside the data so the
10
10
  // app derives exactly what the prototype derives.
11
11
  //
@@ -32,10 +32,6 @@ export const wash = (c, p) => `color-mix(in oklab, ${c} ${p || 10}%, transparent
32
32
  export const DOT = { done: ['✓', C.suc], current: ['●', C.brand], pending: ['○', C.mut], blocked: ['✗', C.des] };
33
33
  export const SDOT = { working: ['●', C.suc], quiet: ['◐', C.wrn], needs: ['●', C.brand], blocked: ['✗', C.des], exited: ['○', C.mut] };
34
34
  export const TONE = { warn: C.wrn, bad: C.des, info: C.inf, ok: C.suc };
35
- export const TDOT = { done: ['✓', C.suc], doing: ['◐', C.inf], blocked: ['✗', C.des], todo: ['○', C.mut] };
36
-
37
- // Task status → colour in the task pane (line 2014).
38
- export const TASK_STATUS_COLOR = { done: C.suc, doing: C.inf, blocked: C.des, todo: C.mut };
39
35
 
40
36
  // Literal, non-token colours. Panel 4 and the terminal are hard-coded in the
41
37
  // prototype (lines 1975–1978, 1984–1988, 2103) — do NOT tokenise them.
@@ -82,30 +78,6 @@ export const PROFILES = [
82
78
  }
83
79
  ];
84
80
 
85
- // Component.INTG (lines 1048–1053)
86
- export const INTG = [
87
- { id: 'jira', name: 'Jira', scope: 'project ATL', st: 'connected', synced: '12:38', does: 'handed-off initiatives read epic + story status from here — their build and eval ladder stages render from the epic, not from local files' },
88
- { id: 'confluence', name: 'Confluence', scope: 'space PM', st: 'connected', synced: '11:02', does: 'PRDs publish here when spec completes; the Confluence page is the handoff copy — planning/prd/ stays the source of truth' },
89
- { id: 'slack', name: 'Slack', scope: '#pm-updates', st: 'connected', synced: '12:40', does: 'session and blocker notifications post here' },
90
- { id: 'google-workspace', name: 'Google Workspace', scope: 'drive + calendar', st: 'connected', synced: '09:15', does: 'calendar holds scheduled-task windows; research docs import from Drive' }
91
- ];
92
-
93
- // line 1952 — the two connections that can preview a document.
94
- export const INTG_PREVIEWS = {
95
- jira: ['jira://ATL-482', 'open ATL-482 preview'],
96
- confluence: ['confluence://PM/crm-import-wizard-prd', 'open PRD page preview']
97
- };
98
-
99
- /* ────────────────────────────── skills ────────────────────────────── */
100
-
101
- // SKILLS (lines 1491–1496)
102
- export const SKILLS = [
103
- { name: 'write-prd', d: 'draft a PRD from an initiative record', reads: 'planning/initiatives/<slug>.md', writes: 'planning/prd/<slug>-prd.md', usedIn: 'spec stage of adlc-feature, adlc-research' },
104
- { name: 'contract-run', d: 'agree done_criteria + test_method, log the run', reads: 'planning/prd/<slug>-prd.md', writes: 'planning/runs/agent-runs.jsonl', usedIn: 'contract stage of adlc-feature' },
105
- { name: 'score-rubric', d: 'run Review-Gate, append verdict', reads: 'planning/evals/<slug>-eval-rubric.md', writes: 'planning/evals/evals.jsonl', usedIn: 'eval stage of adlc-feature; verify stage of health-upkeep' },
106
- { name: 'kanban-view', d: 'project the ladder as a board (same data)', reads: 'planning/initiatives/*.md', writes: 'nothing — a pure projection', usedIn: 'any initiative panel' }
107
- ];
108
-
109
81
  /* ────────────────────────────── knowledge ────────────────────────────── */
110
82
 
111
83
  // Component.KNOW (lines 1082–1087)
@@ -122,49 +94,6 @@ export const INGESTED_KNOW = {
122
94
  'M-ADR-12': { grp: 'mobile · ingested', d: 'crash budget — 2% of sessions', badge: 'ingested', path: 'docs/adr/0012-crash-budget.md', body: 'Imported from acme/atlas-mobile by repo scan. Crash rate above 2% of sessions freezes feature work until triaged. Review it, then accept or supersede.', rels: [] }
123
95
  };
124
96
 
125
- /* ─────────────────── scheduled tasks / agents / memory ─────────────────── */
126
-
127
- // schedList() (lines 1055–1061)
128
- const SCHED_SEED = [
129
- { name: 'nightly evals sweep', when: '02:00 daily', runCmd: 'pmos run score-rubric --all', scope: 'all initiatives with a rubric', profile: 'claude-personal', enabled: true, log: [{ ts: '02:00', t: 'ok — 0 failures', c: 'var(--success)' }, { ts: 'yesterday 02:00', t: 'ok — 0 failures', c: 'var(--success)' }] },
130
- { name: 'prd freshness check', when: 'mon 09:00', runCmd: 'pmos check prd-freshness', scope: 'all initiatives', profile: 'claude-work', enabled: true, log: [{ ts: 'mon 09:00', t: 'ok — 2 stale PRDs flagged', c: 'var(--success)' }] },
131
- { name: 'agent-runs compaction', when: 'fri 18:00', runCmd: 'pmos compact agent-runs', scope: 'planning/runs/', profile: 'claude-personal', enabled: false, log: [{ ts: 'fri 18:00', t: 'skipped — repo busy', c: 'var(--warning)' }] }
132
- ];
133
-
134
- // agentList() (lines 1062–1067)
135
- const AGENTS_SEED = [
136
- { name: 'sonnet-builder', role: 'builder', model: 'claude-sonnet-4-5', profile: 'claude-work', paused: false, sessions: [{ name: 'spine build', slug: 'workspace-pm-spine', st: 'working' }, { name: 'rubric fix', slug: 'workspace-pm-spine', st: 'needs' }] },
137
- { name: 'haiku-scout', role: 'scout', model: 'claude-haiku-4', profile: 'claude-personal', paused: false, sessions: [{ name: 'doc-editor scout', slug: 'workspace-doc-editor', st: 'quiet' }] }
138
- ];
139
-
140
- // memory() (lines 1068–1081)
141
- const MEMORY_SEED = {
142
- short: [
143
- { id: 's1', text: 'spine build: current diff touches navigator.tsx + spine.ts', src: 'session · spine build', ts: '12:39' },
144
- { id: 's2', text: 'rubric fix: waiting on PM to confirm 0–10 scoring scale', src: 'session · rubric fix', ts: '12:22' },
145
- { id: 's3', text: 'doc-editor scout: PRD lacks acceptance criteria — flagged', src: 'session · doc-editor scout', ts: '12:23' }
146
- ],
147
- long: [
148
- { id: 'l1', text: 'PM prefers diffs over prose summaries', src: 'promoted 2026-07-12', pinned: true },
149
- { id: 'l2', text: 'repo uses pnpm — never run npm install', src: 'promoted 2026-06-30', pinned: false },
150
- { id: 'l3', text: 'no verdict recorded is not a pass (house rule)', src: 'added by PM 2026-07-02', pinned: true }
151
- ]
152
- };
153
-
154
- // A new agent (line 1526) and a new long-term memory note (line 1538).
155
- export const NEW_AGENT = { role: 'builder', model: 'claude-sonnet-4-5', paused: false, sessions: [] };
156
- export const NEW_MEMORY_NOTE = { text: 'new note — edit me', src: 'added by PM · now', pinned: false };
157
- // A new scheduled task (line 1476).
158
- export const NEW_SCHED = { when: 'daily 06:00', runCmd: '', scope: 'all initiatives', enabled: true, log: [] };
159
- // The line a scheduled task shows when it has never run (line 1846).
160
- export const SCHED_NEVER_RUN = { ts: '—', t: 'never run yet — that is stated, not implied', c: C.mut };
161
-
162
- const clone = v => JSON.parse(JSON.stringify(v));
163
- /** Fresh, mutable copies of the three editable seed lists. */
164
- export const defaultSched = () => clone(SCHED_SEED);
165
- export const defaultAgents = () => clone(AGENTS_SEED);
166
- export const defaultMemory = () => clone(MEMORY_SEED);
167
-
168
97
  /* ───────────────────── repositories + board groups ───────────────────── */
169
98
 
170
99
  // allProds (line 1431) — the two repositories present before anything is added.
@@ -261,23 +190,6 @@ export const DATA = {
261
190
  steps: [
262
191
  { k: 'intake', s: 'current', t: 'nothing filed yet — a successful read of nothing', action: 'file the research brief', cmd: 'planning/prd/pmos-ui-research-brief.md' },
263
192
  { k: 'explore', s: 'pending', t: '' }, { k: 'synthesize', s: 'pending', t: '' }, { k: 'decide', s: 'pending', t: '' }],
264
- tasks: {
265
- intake: [
266
- { title: 'frame the discovery question', status: 'done', instruction: 'Write the single, falsifiable question this discovery must answer, and file it at the top of the brief.', input: 'the KR this sits under + the raw ask', expect: 'One sentence a stranger could judge as answered or not.', evalNote: 'a reviewer agrees the question is specific and falsifiable', output: 'planning/prd/pmos-ui-research-brief.md' },
267
- { title: 'list what we know vs. must learn', status: 'doing', instruction: 'Separate standing assumptions from open questions; capture each unknown as its own line so it can be assigned later.', input: 'the brief + any prior notes on this space', expect: 'Two short lists — known, unknown — in the brief.', evalNote: 'every unknown is phrased as something an observation could resolve', output: '—' },
268
- { title: 'set the go / drop gate', status: 'todo', instruction: 'Define the evidence that would make this worth picking up — and the evidence that means drop it. Write both as explicit criteria.', input: 'the unknowns list', expect: 'Explicit drop criteria so discovery can end cleanly.', evalNote: 'the gate can be evaluated without further judgement calls', output: '—' }
269
- ],
270
- explore: [
271
- { title: 'shadow 5 PMs, one session each', status: 'todo', instruction: 'Observe five real terminal-workspace sessions end to end; take raw, timestamped notes without interpreting yet.', input: 'the go/drop gate + a recruit list', expect: 'Raw notes per session, filed under research/.', evalNote: 'notes are observations, not conclusions', output: '—' },
272
- { title: 'mine support + sales-call themes', status: 'todo', instruction: 'Pull recurring asks from prior transcripts and tag each with a theme and a count.', input: 'support + sales transcript archive', expect: 'A themed list with counts.', evalNote: 'themes are mutually exclusive and cover the corpus', output: '—' }
273
- ],
274
- synthesize: [
275
- { title: 'cluster findings into candidate needs', status: 'todo', instruction: 'Group the observations into 3–5 need statements, each backed by evidence lines.', input: 'session notes + theme list', expect: 'A short, ranked list of candidate needs.', evalNote: 'each need cites at least two independent observations', output: '—' }
276
- ],
277
- decide: [
278
- { title: 'write the pick-up / drop recommendation', status: 'todo', instruction: 'Recommend whether this becomes a real initiative, measured against the go/drop gate, and record the rationale.', input: 'candidate needs + the gate', expect: 'A decision record a PM can accept or reject.', evalNote: 'the recommendation maps directly to the gate criteria', output: '—' }
279
- ]
280
- },
281
193
  sessions: [], noSess: 'No sessions — normal, not an error.',
282
194
  arts: [], eval: null, l2: ['intake', 'nothing filed', C.mut]
283
195
  },
@@ -488,63 +400,14 @@ export function rawFor(tab, savedDocs = {}) {
488
400
 
489
401
  /* ────────────────────────── modes + titles ────────────────────────── */
490
402
 
491
- // MENU (line 1404) — computed by the prototype but never rendered; kept for parity.
492
- export const MENU = [['product', '◧'], ['scheduled', '◔'], ['workflow', '▤'], ['skills', '◇'], ['knowledge', '◈'], ['agent', '◐'], ['memory', '◒'], ['connect', '⌁']];
493
-
494
- // modeTitle (line 1409) — panel 2's heading per mode.
495
- export const MODE_TITLES = { product: 'initiatives', scheduled: 'scheduled tasks', workflow: 'workflows', skills: 'skills', knowledge: 'knowledge', agent: 'agents', memory: 'memory', connect: 'connections', focus: 'focus' };
403
+ // modeTitle (line 1409) and detailTitle (line 1699): the headings of panel 2 and panel 3, for the modes and details that remain (R29).
404
+ export const MODE_TITLES = { product: 'initiatives', workflow: 'workflows', validate: 'validate', today: 'today' };
496
405
  export const modeTitle = mode => MODE_TITLES[mode] || 'initiatives';
497
406
 
498
- // modeMeta (line 2051) — glyph + label for the settings drawer entries.
499
- export const MODE_META = { focus: ['◎', 'focus'], scheduled: ['◔', 'scheduled tasks'], workflow: ['▤', 'workflows'], skills: ['◇', 'skills'], knowledge: ['◈', 'knowledge'], agent: ['◐', 'agents'], memory: ['◒', 'memory'], connect: ['⌁', 'connections'] };
500
-
501
- // settingsGroups (lines 2053–2056) — EXACTLY two groups, no `workflow` entry.
502
- export const SETTINGS_GROUPS = [
503
- { label: 'this repository', items: ['agent', 'memory', 'knowledge'] },
504
- { label: 'all repositories', items: ['focus', 'skills', 'connect', 'scheduled'] }
505
- ];
506
-
507
407
  // detailTitle (line 1699) — panel 3's heading per detail kind.
508
- export const DETAIL_TITLES = { init: 'initiative', flow: 'workflow', sched: 'scheduled task', skill: 'skill', know: 'decision', agent: 'agent', mem: 'memory', prof: 'claude profile', intg: 'connection', addprod: 'add product', new: 'file new' };
408
+ export const DETAIL_TITLES = { init: 'initiative', flow: 'workflow', skill: 'skill', prof: 'claude profile', addprod: 'add product', new: 'file new' };
509
409
  export const detailTitle = kind => DETAIL_TITLES[kind] || 'detail';
510
410
 
511
- /* ────────────────────────────── focus mode ────────────────────────────── */
512
-
513
- // focusFirst / focusWaiting / focusSyn / focusSaw (lines 1544–1566).
514
- export const FOCUS_FIRST = [
515
- { n: '1', slug: 'workspace-harness-tower', prod2: 'workspace', why: 'read failed — the board lies until the frontmatter is fixed', read: 'read: YAML error at line 3 · nothing derived' },
516
- { n: '2', slug: 'workspace-pm-spine', prod2: 'workspace', why: 'contract owed — agents build against nothing until you agree it', read: 'read: PRD · runs.jsonl — no contract line' },
517
- { n: '3', slug: 'crm-billing-sync', prod2: 'atlas-crm', why: 'stories 4/9 — the Jira handoff waits on you', read: 'read: PRD draft · stories file' },
518
- { n: '4', slug: 'workspace-doc-editor', prod2: 'workspace', why: '2 intake blockers — each names its fix', read: 'read: initiative record · KR WIP 3/3' }
519
- ];
520
- export const FOCUS_WAITING = [
521
- { slug: 'crm-import-wizard', prod2: 'atlas-crm', why: 'app team builds — Jira ATL-482 · 3/7 stories done', read: 'synced 12:38 · nothing owed by you' }
522
- ];
523
- // `action` replaces the prototype's inline handler; the mode module binds it.
524
- export const FOCUS_SYN = [
525
- { t: 'sonnet-builder · spine build — running, no input needed', hasBtn: false },
526
- { t: 'haiku-scout can draft the billing-sync stories — you edit instead of write', hasBtn: true, btn: '▶ delegate', action: 'delegate-billing-stories' }
527
- ];
528
- // The session the delegate button starts (line 1558).
529
- export const DELEGATE_SESSION = { slug: 'crm-billing-sync', sess: { name: 'stories draft · haiku-scout', st: 'working', label: 'working', profile: 'claude-personal' } };
530
-
531
- /** focusSaw (line 1563) — the unpinned count is read from live memory. */
532
- export function focusSaw(memory) {
533
- const unpinned = memory.long.filter(m => !m.pinned).length;
534
- return [
535
- { t: 'groom long-term memory — ' + unpinned + ' unpinned entries may be compacted away', action: 'mode:memory' },
536
- { t: 'health budget — shadcn upkeep accepted 07-21 · 19% of budget spent', action: 'jump:workspace|workspace-shadcn-design-system' }
537
- ];
538
- }
539
-
540
- /** focusEnds (line 1567) — one line per repository naming its first group. */
541
- export function focusEnds(products) {
542
- return products.map(([id]) => {
543
- const g0 = (ALLGROUPS[id] || [])[0];
544
- return g0 ? { prod: id, obj: g0.gid + ' · ' + g0.gname } : null;
545
- }).filter(Boolean);
546
- }
547
-
548
411
  /* ──────────────────────── derivation: repos + board ──────────────────────── */
549
412
 
550
413
  /** allProds (line 1431) — seed repositories plus anything the PM added. */
@@ -663,66 +526,8 @@ export function flowUse(userInits = {}) {
663
526
  return use;
664
527
  }
665
528
 
666
- /* ─────────────────── derivation: phases, tasks, detail ─────────────────── */
529
+ /* ─────────────────── derivation: detail ─────────────────── */
667
530
 
668
- /**
669
- * synthTasks (lines 1720–1734) — the tasks a phase gets when the record does
670
- * not author any. Titles depend on the step's status; statuses and every field
671
- * are derived, never invented per initiative.
672
- */
673
- export function synthTasks(slug, k, s) {
674
- const titles = {
675
- done: ['read the inputs for ' + k, 'produce the ' + k + ' output', 'record the result'],
676
- current: ['read the inputs for ' + k, 'produce the ' + k + ' output', 'record the result'],
677
- blocked: ['resolve the ' + k + ' blocker', 'produce the ' + k + ' output'],
678
- pending: ['read the inputs for ' + k, 'produce the ' + k + ' output']
679
- }[s] || ['work the ' + k + ' step'];
680
- return titles.map((title, idx) => {
681
- let st = 'todo';
682
- if (s === 'done') st = 'done';
683
- else if (s === 'current') st = idx === 0 ? 'done' : (idx === 1 ? 'doing' : 'todo');
684
- else if (s === 'blocked') st = idx === 0 ? 'blocked' : 'todo';
685
- return {
686
- title, status: st,
687
- instruction: 'Run the ' + k + ' step for ' + slug + ' — ' + title + '. Read the referenced inputs, do the work, and write the phase output back to planning/.',
688
- input: 'planning/initiatives/' + slug + '.md + prior phase outputs',
689
- expect: 'A reviewable ' + k + ' artifact exists on disk so the phase can advance.',
690
- evalNote: st === 'done' ? 'passed the ' + k + ' rubric' : 'graded against the ' + k + ' rubric before the phase advances',
691
- output: st === 'done' ? 'planning/' + k + '/' + slug + '-' + k + '.md' : '—'
692
- };
693
- });
694
- }
695
-
696
- /** Authored tasks win; otherwise synthesise (line 1737). */
697
- export function tasksForPhase(record, step, slug) {
698
- return (record.tasks && record.tasks[step.k]) ? record.tasks[step.k] : synthTasks(slug, step.k, step.s);
699
- }
700
-
701
- /** Phase status from its tasks (lines 1738–1743). Order matters. */
702
- export function phaseStatus(tasks) {
703
- const doneN = tasks.filter(t => t.status === 'done').length;
704
- if (tasks.some(t => t.status === 'blocked')) return 'blocked';
705
- if (tasks.some(t => t.status === 'doing')) return 'doing';
706
- if (doneN === tasks.length) return 'done';
707
- if (doneN > 0) return 'doing';
708
- return 'todo';
709
- }
710
-
711
- /**
712
- * phases (lines 1736–1756), without presentation. One entry per step:
713
- * `{ key, step, tasks, doneN, status, count }`. The panel adds dots, carets and
714
- * handlers — `TDOT[status]` gives the glyph + colour.
715
- */
716
- export function phasesFor(record, slug) {
717
- return (record.steps || []).map(s => {
718
- const tasks = tasksForPhase(record, s, slug);
719
- const doneN = tasks.filter(t => t.status === 'done').length;
720
- return { key: s.k, label: s.k, step: s, tasks, doneN, status: phaseStatus(tasks), count: doneN + '/' + tasks.length };
721
- });
722
- }
723
-
724
- /** Task tab id (line 1750). */
725
- export const taskTabId = (slug, phaseKey, idx) => 'task:' + slug + ':' + phaseKey + '#' + idx;
726
531
  /** Terminal tab id (line 1399). */
727
532
  export const termTabId = (slug, sessName) => 'term:' + slug + ':' + sessName;
728
533
  /** Doc tab id (line 1400). */
@@ -829,20 +634,6 @@ export function makeLink(slug, src, existing = []) {
829
634
  export const sessionProfileNote = id => (id === 'claude-work'
830
635
  ? 'claude-work — workspace MCP live: jira · confluence · slack · google-workspace'
831
636
  : 'claude-personal — no workspace MCP; the session cannot touch Jira or Confluence');
832
- // scheduled-task note (line 1844)
833
- export const schedProfileNote = id => ((id || 'claude-work') === 'claude-work'
834
- ? 'runs with workspace MCP — can read Jira and Confluence'
835
- : 'runs without workspace MCP — local files only');
836
- // agent note (line 1892)
837
- export const agentProfileNote = id => ((id || 'claude-work') === 'claude-work'
838
- ? 'claude-work — this agent reaches Jira, Confluence, Slack, Google Workspace over MCP'
839
- : 'claude-personal — no workspace MCP; this agent cannot touch Jira or Confluence');
840
- // memory note (lines 1918–1920)
841
- export const memoryNote = scope => (scope === 'short'
842
- ? 'Dies when its session exits. Promote it to keep it — the agent will see it in every future run.'
843
- : 'Injected into every agent run. Pinned entries survive compaction; unpinned ones may be summarized away.');
844
- export const memoryPath = scope => (scope === 'short' ? 'runtime — session context' : 'memory/long-term.jsonl');
845
-
846
637
  /* ─────────────────────── the faux shell (fixture only) ─────────────────────── */
847
638
 
848
639
  /** The five lines a terminal pane opens with (lines 1983–1989). */
@@ -989,14 +780,11 @@ export const ADD_SOURCES = [['github', 'github repo'], ['local', 'local folder']
989
780
  * exports; they are needed in normal mode too.
990
781
  */
991
782
  export const FIXTURE = {
992
- C, DOT, SDOT, TDOT, TONE,
993
- FLOWS, PROFILES, INTG, INTG_PREVIEWS, SKILLS, KNOW, INGESTED_KNOW,
783
+ C, DOT, SDOT, TONE,
784
+ FLOWS, PROFILES, KNOW, INGESTED_KNOW,
994
785
  PRODUCTS, ADDABLE_PRODUCTS, ADD_DEFAULTS, ALLGROUPS, DATA, RAW_DOCS,
995
- MENU, MODE_META, MODE_TITLES, SETTINGS_GROUPS, DETAIL_TITLES,
996
- FOCUS_FIRST, FOCUS_WAITING, FOCUS_SYN, DELEGATE_SESSION,
786
+ MODE_TITLES, DETAIL_TITLES,
997
787
  TAB_COLORS, TERM_COLORS, SCRATCH_SEND_COLOR,
998
- sched: SCHED_SEED, agents: AGENTS_SEED, memory: MEMORY_SEED,
999
- defaultSched, defaultAgents, defaultMemory
1000
788
  };
1001
789
 
1002
790
  export default FIXTURE;
@@ -1009,7 +797,6 @@ export default FIXTURE;
1009
797
  const COCKPIT_SEED = (() => { try { return new URLSearchParams(location.search).get('fixture') === 'cockpit'; } catch { return false; } })();
1010
798
  const SEED_DEFAULTS = { microsites: 'ooh-weather-expansion', pmos: 'cockpit-terminal-mod', 'scarlet': 'scarlet-onboarding' };
1011
799
  if (COCKPIT_SEED) {
1012
- const task = (title, status, k) => ({ title, status, instruction: 'Run ' + title + '.', input: 'planning/ + prior phase outputs', expect: 'A reviewable ' + k + ' artifact exists.', evalNote: 'graded against the ' + k + ' rubric', output: status === 'done' ? 'planning/' + k + '/' + k + '.md' : '—' });
1013
800
  PRODUCTS.splice(0, PRODUCTS.length, ['microsites', '6', 'github'], ['pmos', '3', 'local'], ['scarlet', '1', 'github']);
1014
801
  for (const k of Object.keys(ALLGROUPS)) delete ALLGROUPS[k];
1015
802
  ALLGROUPS.microsites = [{ gid: 'O2', idc: C.fg, gname: 'Microsites ship on time', gct: '6', hasKr: true, krid: 'KR1', krname: 'q3-2026', krct: '6',
@@ -1024,12 +811,6 @@ if (COCKPIT_SEED) {
1024
811
  { k: 'spec', s: 'done', t: '' }, { k: 'build', s: 'done', t: '' }, { k: 'eval', s: 'blocked', t: '' }, { k: 'accepted', s: 'pending', t: '' }], {
1025
812
  pills: [{ t: 'feature', c: C.mut }, { t: 'q3-2026 · O2 · KR1', c: C.mut }, { t: '⇄ Jira OOH-212', c: C.mut }],
1026
813
  blocker: { label: 'Merge', evidence: 'needs a grant for PR 41', next: '' },
1027
- tasks: {
1028
- spec: [task('Write the PRD', 'done', 'spec'), task('Write the rubric', 'done', 'spec')],
1029
- build: [task('Build the forecast units', 'done', 'build'), task('Wire the regional feed', 'done', 'build'), task('Add the tests', 'done', 'build')],
1030
- eval: [task('Run independent review', 'blocked', 'eval'), task('Record outcome check', 'todo', 'eval')],
1031
- accepted: [task('Accept the run', 'todo', 'accepted')],
1032
- },
1033
814
  sessions: [{ name: 'claude · merge PR 41', st: 'working', label: 'working' }, { name: 'review run', st: 'working', label: 'working' }],
1034
815
  arts: [{ role: 'PRD', path: 'planning/prd/ooh-weather-expansion-prd.md', dest: null }] });
1035
816
  DATA['psle-paywall-revamp'] = mk('PSLE paywall revamp', 'accepted', oneStep('accepted', 'pending'));
@@ -0,0 +1,94 @@
1
+ /* pm/frame-copy.js — every string of the frame: the top bar, the dock, the phone chrome and the Settings destination
2
+ * (pm-os-absorb Run 3, slice 3A; PRD R9, R10, R15).
3
+ *
4
+ * Pure data, no import: glossary.test.js loads this file in Node and holds every string, and guide/layout.md, to the R2
5
+ * rules. The words follow the PMOS-STE instructional register: short sentences, one instruction each, the active voice.
6
+ */
7
+
8
+ export const COPY = {
9
+ repo: {
10
+ all: 'All repositories',
11
+ none: 'No repository',
12
+ menu: 'Choose a repository',
13
+ open: (name) => 'Repository: ' + name + '. Choose a repository.',
14
+ },
15
+ nav: {
16
+ label: 'Destinations',
17
+ today: 'Today',
18
+ initiatives: 'Initiatives',
19
+ board: 'Board',
20
+ reports: 'Reports',
21
+ agent: 'Agent',
22
+ settings: 'Settings',
23
+ },
24
+ /** The Today count on the top bar and on the phone tab, for a screen reader. */
25
+ needs: (n) => (n === 1 ? '1 needs you' : n + ' need you'),
26
+ search: 'Search or capture',
27
+ searchKey: '⌘K',
28
+ sync: {
29
+ label: 'Sync',
30
+ busy: 'Syncing',
31
+ title: 'Sync this repository: fetch, bring in the work of the other machine, record your changes, push.',
32
+ done: 'The repository is in sync.',
33
+ stopped: (step, why) => 'Sync stopped at ' + step + ': ' + why,
34
+ },
35
+ settings: 'Settings',
36
+ dock: {
37
+ open: 'Open the agent dock (⌘J)',
38
+ fold: 'Fold the agent dock (⌘J)',
39
+ session: 'Session',
40
+ sessionMenu: 'Start a session',
41
+ newSession: 'New session',
42
+ withOptions: 'Session with options',
43
+ skillsHead: 'Start from a skill',
44
+ noSkills: 'No skills to start from.',
45
+ skillsUnread: 'The skills list did not load.',
46
+ },
47
+ detail: { close: 'Close the detail' },
48
+ phone: {
49
+ back: 'Back',
50
+ pin: 'Search or capture',
51
+ },
52
+ page: {
53
+ title: 'Settings',
54
+ intro: 'Settings for this workspace. Your work is in Today, Initiatives, Board and Reports.',
55
+ repos: 'Repositories',
56
+ reposEmpty: 'No repository is registered. Add one to start.',
57
+ add: 'Add repository',
58
+ addClose: 'Close Add repository',
59
+ current: 'Current',
60
+ use: 'Use this repository',
61
+ show: 'Show its initiatives',
62
+ rename: 'Rename',
63
+ remove: 'Remove from PMOS',
64
+ more: (name) => 'More for ' + name,
65
+ profiles: 'Profiles',
66
+ profilesRow: 'Claude profiles',
67
+ profilesMeta: 'Open the profiles a Claude session can start with.',
68
+ profilesOpen: 'Open the profiles',
69
+ sessions: 'New sessions',
70
+ cli: 'Default CLI',
71
+ cliShell: 'Shell',
72
+ cliClaude: 'Claude',
73
+ model: 'Default model',
74
+ modelAuto: 'Auto',
75
+ modelFast: 'Fast',
76
+ modelBalanced: 'Balanced',
77
+ modelDeep: 'Deep',
78
+ sessionsNote: 'The dock starts new sessions with these defaults. A model applies only to a Claude session.',
79
+ validate: 'Validate consent',
80
+ validateOn: 'Validation is on for this repository.',
81
+ validateOff: 'Validation is off. Nothing from this repository is sent.',
82
+ validateUnread: 'The consent of this repository did not load.',
83
+ validateNoRepo: 'Choose a repository to set its consent.',
84
+ validateOpen: 'Open Validate',
85
+ turnOn: 'Turn on',
86
+ turnOff: 'Turn off',
87
+ theme: 'Theme',
88
+ dark: 'Dark',
89
+ light: 'Light',
90
+ },
91
+ };
92
+
93
+ /* The rules of glossary.test.js apply to every string above and to guide/layout.md, with no exemption. */
94
+ export const GLOSSARY = { guide: 'layout.md' };
@@ -0,0 +1,82 @@
1
+ /* pm/handoff-box.js — Hand to agent, inside the item sheet (pm-os-absorb Run 1, PRD R14).
2
+ *
3
+ * Two presses, as the next-step Work path does: the first asks the server what it WOULD start (prepare, which starts
4
+ * nothing) and shows it in one line — "Agent will: <first line of the prompt> · <where>" — with the full prompt and the
5
+ * branch folded under Details. Go starts exactly that: it sends the hash of the prompt the PM was shown, and the server
6
+ * refuses a prompt that changed in the meantime. On success the sheet closes and the new session opens in its terminal tab.
7
+ *
8
+ * Nothing else starts a session from the Breakdown: no drag, drop or state change calls a handoff route.
9
+ */
10
+
11
+ import { el } from './dom.js';
12
+ import { button, toast } from './primitives.js';
13
+ import { COPY } from './breakdown-copy.js';
14
+ import { prepareHandoff, goHandoff } from './breakdown-api.js';
15
+
16
+ let iniMod = null;
17
+ const iniReady = () => (iniMod ? Promise.resolve(iniMod) : import('./panel-initiatives.js').then((m) => { iniMod = m; return m; }).catch(() => null));
18
+
19
+ /** Open the terminal tab of a running session, the way next-step-ui.js does after Go. */
20
+ export async function openSessionTab(slug, session) {
21
+ const m = await iniReady();
22
+ if (m && session) m.openTerm(slug, { id: session.id, name: session.name || session.id, st: 'working' });
23
+ }
24
+
25
+ /**
26
+ * handoffBox({ ws, slug, item, sessions, onCancel, onStarted(session) }) -> the box element. It prepares at once.
27
+ * `sessions` is the app's session list, used only to name a session the server says is already bound.
28
+ */
29
+ export function handoffBox({ ws, slug, item, sessions, onCancel, onStarted }) {
30
+ const box = el('div', { class: 'bd-handoff', 'data-bd-handoff': item.id, role: 'group', 'aria-label': COPY.handToAgent });
31
+ const say = (...kids) => { while (box.firstChild) box.removeChild(box.firstChild); for (const k of kids) if (k) box.appendChild(k); };
32
+ const cancel = button({ label: COPY.cancel, variant: 'ghost', attrs: { 'data-bd-handoff-cancel': '' }, onClick: () => { if (onCancel) onCancel(); } });
33
+
34
+ const busy = (sessionId) => {
35
+ const s = (sessions || []).find((x) => x && x.id === sessionId) || (sessionId ? { id: sessionId, name: sessionId } : null);
36
+ say(el('div', { class: 'bd-handoff__line' }, COPY.busy),
37
+ el('div', { class: 'bd-handoff__actions' },
38
+ s ? button({ label: COPY.openSession, variant: 'outline', attrs: { 'data-bd-handoff-open': '' }, onClick: () => openSessionTab(slug, s) }) : null,
39
+ cancel));
40
+ };
41
+ const failed = (text) => say(el('div', { class: 'bd-handoff__line bd-handoff__error' }, text),
42
+ el('div', { class: 'bd-handoff__actions' }, cancel));
43
+
44
+ const ready = (prep) => {
45
+ const go = button({
46
+ label: COPY.go, variant: 'primary', attrs: { 'data-bd-handoff-go': '' },
47
+ onClick: async () => {
48
+ go.disabled = true;
49
+ const out = await goHandoff(ws, item.id, prep.prompt_sha);
50
+ if (out && out.ok !== false && out.session) {
51
+ if (onStarted) onStarted(out.session);
52
+ await openSessionTab(slug, out.session);
53
+ toast({ text: COPY.sessionStarted(out.session.name || out.session.id) });
54
+ return;
55
+ }
56
+ if (out && out.session) { busy(out.session); return; }
57
+ failed((out && out.error) || 'nothing was started');
58
+ },
59
+ });
60
+ const facts = el('dl', { class: 'bd-handoff__facts' },
61
+ el('dt', null, COPY.branch), el('dd', null, (prep.details && prep.details.branch) || prep.where || ''),
62
+ el('dt', null, COPY.folder), el('dd', null, (prep.details && prep.details.tree) || ''));
63
+ say(el('div', { class: 'bd-handoff__line', 'data-bd-handoff-line': '' }, prep.line || COPY.agentWill(prep.first || '', prep.where || '')),
64
+ el('div', { class: 'bd-handoff__actions' }, go, cancel),
65
+ el('details', { class: 'bd-handoff__details' },
66
+ el('summary', null, COPY.details),
67
+ el('pre', { class: 'bd-handoff__prompt' }, prep.prompt || ''),
68
+ facts));
69
+ go.focus({ preventScroll: true });
70
+ // the whole box in view, above the sheet's sticky action row (scroll-margin in components.css)
71
+ if (typeof box.scrollIntoView === 'function') box.scrollIntoView({ block: 'nearest' });
72
+ };
73
+
74
+ say(el('div', { class: 'bd-handoff__line bd-muted' }, COPY.preparing), el('div', { class: 'bd-handoff__actions' }, cancel));
75
+ prepareHandoff(ws, item.id).then((out) => {
76
+ if (!box.isConnected && !box.parentNode) return;
77
+ if (out && out.ok !== false) ready(out);
78
+ else if (out && out.session) busy(out.session);
79
+ else failed((out && out.error) || 'could not prepare');
80
+ });
81
+ return box;
82
+ }
@@ -0,0 +1,88 @@
1
+ /* pm/http.js — the one way new page code calls the workspace server (pm-os-absorb Runs 2 to 5, PRD R25).
2
+ *
3
+ * pmPost(path, body, { keepalive }) → Promise of the server's JSON, or { ok:false, status?, error }
4
+ * pmGet(path) → the same
5
+ * pmHeaders() → the headers every request carries
6
+ * pressFetch(url, init) → fetch with pmHeaders; a 403 from a stale nonce is retried once with the new one
7
+ * pageNonce() → this launch's nonce as the page holds it ('' when none)
8
+ *
9
+ * Every PM-only press (Move to Now, adopt, Issue, Release, lifecycle, Accept) goes through pmPost, so Run 2 adds the
10
+ * per-launch nonce in pmHeaders and every press carries it at once. Relative URLs only, to this server; the browser
11
+ * sends the page's own Origin header. Nothing here sends `by`, `via` or `at`: the writer sets them from the call path.
12
+ * None of these throws. In FIXTURE mode none of them makes a request.
13
+ *
14
+ * THE NONCE (Run 2, slice 2A). The server writes a per-launch value into `<meta name="pmos-nonce">` when it serves the
15
+ * page. pmHeaders reads it once and sends it as `x-pmos-nonce`; the server refuses a press without it (403). The older
16
+ * POST helpers (data.js postJson, breakdown-api.js call, view-addrepo.js act) send through pressFetch, so they carry it
17
+ * too and keep their own error texts. A restarted server mints a new value, and an open page still holds the old one:
18
+ * on a 403, pressFetch reads the meta again from GET / and, when the value changed, sends the same press once more.
19
+ * The terminal's WebSocket sends the nonce as the `pmos-nonce` query parameter (tab-term.js), because a browser
20
+ * WebSocket cannot set a header; without it a new session starts with no initiative.
21
+ */
22
+
23
+ import { FIXTURE } from './state.js';
24
+
25
+ let nonce = null;
26
+ /** The meta's value, read once. '' when the page has none (fixture mode, or a server that mints none). */
27
+ function launchNonce() {
28
+ if (nonce !== null) return nonce;
29
+ let v = '';
30
+ try {
31
+ const m = typeof document !== 'undefined' ? document.querySelector('meta[name="pmos-nonce"]') : null;
32
+ v = (m && m.getAttribute('content')) || '';
33
+ } catch { v = ''; }
34
+ nonce = v;
35
+ return nonce;
36
+ }
37
+
38
+ export const pageNonce = () => launchNonce();
39
+
40
+ /** Read the nonce again from GET /. True only when the server now holds a different, non-empty value. */
41
+ async function refreshNonce() {
42
+ if (FIXTURE) return false;
43
+ let html;
44
+ try {
45
+ const r = await fetch('/', { cache: 'no-store' });
46
+ if (!r.ok) return false;
47
+ html = await r.text();
48
+ } catch { return false; }
49
+ const m = /<meta name="pmos-nonce" content="([^"]*)"/.exec(html || '');
50
+ const v = m ? m[1] : '';
51
+ if (!v || v === launchNonce()) return false;
52
+ nonce = v;
53
+ try { const el = document.querySelector('meta[name="pmos-nonce"]'); if (el) el.setAttribute('content', v); } catch { /* no DOM */ }
54
+ return true;
55
+ }
56
+
57
+ /** fetch with this page's headers. Throws as fetch throws; the callers turn that into their own error text. */
58
+ export async function pressFetch(url, init = {}) {
59
+ const send = () => fetch(url, { ...init, headers: { ...pmHeaders(), ...(init.headers || {}) } });
60
+ const res = await send();
61
+ if (res.status !== 403 || !(await refreshNonce())) return res;
62
+ return send(); // the workspace restarted since this page loaded: the same press, once, with the new nonce
63
+ }
64
+
65
+ export function pmHeaders() {
66
+ const n = launchNonce();
67
+ return n ? { 'Content-Type': 'application/json', 'x-pmos-nonce': n } : { 'Content-Type': 'application/json' };
68
+ }
69
+
70
+ async function request(method, path, body, opts) {
71
+ if (FIXTURE) return { ok: false, error: 'fixture mode' };
72
+ let res;
73
+ try {
74
+ res = method === 'GET' ? await fetch(path, { headers: pmHeaders() }) : await pressFetch(path, {
75
+ method, body: JSON.stringify(body === undefined ? {} : body), keepalive: !!(opts && opts.keepalive),
76
+ });
77
+ } catch (e) {
78
+ return { ok: false, error: 'could not reach the workspace server — ' + ((e && e.message) || String(e)) };
79
+ }
80
+ let out;
81
+ try { out = await res.json(); } catch { return { ok: false, status: res.status, error: path + ' did not answer JSON (HTTP ' + res.status + ')' }; }
82
+ if (!res.ok && out && out.ok !== false) return { ok: false, status: res.status, error: 'HTTP ' + res.status + ' from ' + path };
83
+ if (out && typeof out === 'object' && !res.ok) out.status = res.status;
84
+ return out;
85
+ }
86
+
87
+ export const pmPost = (path, body, opts) => request('POST', path, body, opts);
88
+ export const pmGet = (path) => request('GET', path);
@@ -0,0 +1,53 @@
1
+ /* pm/icons.js — the lucide icons the Breakdown uses (D86 point 6).
2
+ *
3
+ * The 29 symbols live in one vendored sprite, /vendor/lucide-sprite.js, generated from lucide-static 1.52.0 (see
4
+ * workspace/third-party/README.md). `ensureSprite()` loads it once and puts it in the overlay layer; `icon(name)` returns
5
+ * an <svg> that points at one symbol. Icons are decoration only: every one is aria-hidden, and a control that shows only
6
+ * an icon carries its own aria-label (primitives.js `iconButton` refuses to build one without).
7
+ */
8
+
9
+ import { layer } from './primitives.js';
10
+
11
+ const SVG_NS = 'http://www.w3.org/2000/svg';
12
+ let loading = null;
13
+
14
+ /** Load the sprite once and insert it into the layer. Resolves to true when the symbols are on the page. */
15
+ export function ensureSprite() {
16
+ const host = layer();
17
+ if (!host) return Promise.resolve(false);
18
+ if (host.querySelector('.pmc-sprite')) return Promise.resolve(true);
19
+ if (!loading) {
20
+ loading = import('/vendor/lucide-sprite.js').then((m) => {
21
+ const holder = layer();
22
+ if (!holder || holder.querySelector('.pmc-sprite')) return true;
23
+ const box = document.createElement('div');
24
+ box.className = 'pmc-sprite';
25
+ box.setAttribute('aria-hidden', 'true');
26
+ box.innerHTML = m.SPRITE;
27
+ holder.appendChild(box);
28
+ return true;
29
+ }).catch((e) => {
30
+ loading = null;
31
+ console.warn('[breakdown] the icon sprite did not load:', (e && e.message) || e);
32
+ return false;
33
+ });
34
+ }
35
+ return loading;
36
+ }
37
+
38
+ /** `<svg class="pmc-icon" aria-hidden="true" focusable="false"><use href="#lucide-NAME"></use></svg>`, 16px, currentColor. */
39
+ export function icon(name) {
40
+ const svg = document.createElementNS(SVG_NS, 'svg');
41
+ svg.setAttribute('class', 'pmc-icon');
42
+ svg.setAttribute('aria-hidden', 'true');
43
+ svg.setAttribute('focusable', 'false');
44
+ /* 16 × 16 before components.css arrives: without a size an <svg> renders at the UA default of 300 × 150, and the
45
+ * detail jumped by ~150px on first paint. The stylesheet still governs the size once it loads. */
46
+ svg.setAttribute('width', '16');
47
+ svg.setAttribute('height', '16');
48
+ svg.setAttribute('data-icon', name);
49
+ const use = document.createElementNS(SVG_NS, 'use');
50
+ use.setAttribute('href', '#lucide-' + name);
51
+ svg.appendChild(use);
52
+ return svg;
53
+ }