create-pmos 0.124.0 → 0.125.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 (125) hide show
  1. package/package.json +1 -1
  2. package/payload/KIT-VERSION +2 -2
  3. package/payload/planning/evals/eval-calibration.md +1 -1
  4. package/payload/planning/okrs/health-budgets.md +1 -1
  5. package/payload/scripts/nine-stages.js +81 -0
  6. package/payload/scripts/workflow-state.js +2 -0
  7. package/payload/state/discoveries.md +1 -1
  8. package/payload/state/log.md +1 -1
  9. package/workspace/c3-one-home-test.js +131 -0
  10. package/workspace/fixtures/initiatives-api/list.json +171 -3
  11. package/workspace/fixtures/initiatives-api/roadmap.json +228 -4
  12. package/workspace/fixtures/initiatives-api/sheet.json +57 -1
  13. package/workspace/fixtures/one-home-baseline.json +83 -0
  14. package/workspace/guide/layout.md +2 -0
  15. package/workspace/lib/engine/nine-stages.js +81 -0
  16. package/workspace/lib/engine/workflow-state.js +2 -0
  17. package/workspace/lib/facts-async.js +51 -0
  18. package/workspace/lib/item-steps.js +52 -0
  19. package/workspace/lib/jira-status.js +8 -3
  20. package/workspace/lib/jobs-to-be-done.js +52 -0
  21. package/workspace/lib/outbox-store.js +87 -0
  22. package/workspace/lib/pm-items.js +115 -0
  23. package/workspace/lib/route-modules.js +21 -0
  24. package/workspace/lib/routes-briefing.js +100 -0
  25. package/workspace/lib/routes-c3-init.js +53 -0
  26. package/workspace/lib/routes-c3-life.js +119 -0
  27. package/workspace/lib/routes-c3-strategy.js +169 -0
  28. package/workspace/lib/routes-c3-timeline.js +191 -0
  29. package/workspace/lib/routes-calendar-import.js +60 -0
  30. package/workspace/lib/routes-calendar-views.js +61 -0
  31. package/workspace/lib/routes-doc-health.js +79 -0
  32. package/workspace/lib/routes-doc-sheet.js +169 -0
  33. package/workspace/lib/routes-forecast.js +85 -0
  34. package/workspace/lib/routes-hooks.js +121 -0
  35. package/workspace/lib/routes-initiatives.js +3 -0
  36. package/workspace/lib/routes-intake.js +24 -0
  37. package/workspace/lib/routes-outbox.js +97 -0
  38. package/workspace/lib/routes-pm-items.js +216 -0
  39. package/workspace/lib/routes-records.js +61 -0
  40. package/workspace/lib/routes-releases.js +129 -0
  41. package/workspace/lib/routes-report-facts.js +84 -0
  42. package/workspace/lib/routes-report-families.js +32 -0
  43. package/workspace/lib/routes-roadmap-ops.js +141 -0
  44. package/workspace/lib/routes-runs.js +119 -0
  45. package/workspace/lib/routes-signals.js +50 -0
  46. package/workspace/lib/run-answers.js +69 -0
  47. package/workspace/package-lock.json +2 -2
  48. package/workspace/package.json +1 -1
  49. package/workspace/public/c3.html +26 -0
  50. package/workspace/public/pm/briefing-deck.js +415 -0
  51. package/workspace/public/pm/briefing-model.js +176 -0
  52. package/workspace/public/pm/c3-breakdown.js +240 -0
  53. package/workspace/public/pm/c3-brief.js +278 -0
  54. package/workspace/public/pm/c3-cal-strategy.js +528 -0
  55. package/workspace/public/pm/c3-case.js +173 -0
  56. package/workspace/public/pm/c3-demo-brief-ix.js +350 -0
  57. package/workspace/public/pm/c3-demo-cal-ix.js +74 -0
  58. package/workspace/public/pm/c3-demo-cal2.js +66 -0
  59. package/workspace/public/pm/c3-demo-cal3.js +173 -0
  60. package/workspace/public/pm/c3-demo-data.js +560 -0
  61. package/workspace/public/pm/c3-demo-init.js +373 -0
  62. package/workspace/public/pm/c3-demo-life.js +77 -0
  63. package/workspace/public/pm/c3-demo-misc2.js +46 -0
  64. package/workspace/public/pm/c3-demo-plan2.js +30 -0
  65. package/workspace/public/pm/c3-demo-rep-ix.js +126 -0
  66. package/workspace/public/pm/c3-demo-rep2.js +92 -0
  67. package/workspace/public/pm/c3-demo-term.js +153 -0
  68. package/workspace/public/pm/c3-demo-time.js +49 -0
  69. package/workspace/public/pm/c3-demo-work-ix.js +80 -0
  70. package/workspace/public/pm/c3-demo-work2.js +13 -0
  71. package/workspace/public/pm/c3-demo.js +113 -0
  72. package/workspace/public/pm/c3-doc-sheet.js +314 -0
  73. package/workspace/public/pm/c3-dock.js +180 -0
  74. package/workspace/public/pm/c3-docs.js +290 -0
  75. package/workspace/public/pm/c3-expand.js +96 -0
  76. package/workspace/public/pm/c3-init-extras.js +170 -0
  77. package/workspace/public/pm/c3-lifecycle-panels.js +476 -0
  78. package/workspace/public/pm/c3-misc-panels.js +89 -0
  79. package/workspace/public/pm/c3-plan-panels.js +163 -0
  80. package/workspace/public/pm/c3-plan.js +192 -0
  81. package/workspace/public/pm/c3-roadmap.js +249 -0
  82. package/workspace/public/pm/c3-sheet.js +107 -0
  83. package/workspace/public/pm/c3-shell.js +637 -0
  84. package/workspace/public/pm/c3-signals.js +196 -0
  85. package/workspace/public/pm/c3-strategy-panels.js +45 -0
  86. package/workspace/public/pm/c3-swipe.js +138 -0
  87. package/workspace/public/pm/c3-terminal-data.js +26 -0
  88. package/workspace/public/pm/c3-terminal-edge.js +91 -0
  89. package/workspace/public/pm/c3-terminal-icons.js +41 -0
  90. package/workspace/public/pm/c3-terminal-results.js +141 -0
  91. package/workspace/public/pm/c3-terminal.js +52 -0
  92. package/workspace/public/pm/c3-tickets.js +163 -0
  93. package/workspace/public/pm/c3-timeline.js +201 -0
  94. package/workspace/public/pm/c3-toast.js +16 -0
  95. package/workspace/public/pm/calendar-views.js +1015 -0
  96. package/workspace/public/pm/console3-lift.md +220 -0
  97. package/workspace/public/pm/console3.css +1480 -0
  98. package/workspace/public/pm/dest-initiatives.js +16 -3
  99. package/workspace/public/pm/dest-mywork.js +8 -1
  100. package/workspace/public/pm/dest-reports.js +47 -1
  101. package/workspace/public/pm/dest-today.js +55 -1
  102. package/workspace/public/pm/doc-health-panel.js +110 -0
  103. package/workspace/public/pm/doc-rail.js +9 -3
  104. package/workspace/public/pm/lifecycle-lens-model.js +228 -0
  105. package/workspace/public/pm/lifecycle-lens.js +261 -0
  106. package/workspace/public/pm/new-initiative-sheet.js +116 -21
  107. package/workspace/public/pm/outbox-panel.js +125 -0
  108. package/workspace/public/pm/pm-work-view.js +919 -0
  109. package/workspace/public/pm/releases-view.js +248 -0
  110. package/workspace/public/pm/report-families-view.js +503 -0
  111. package/workspace/public/pm/run-card.js +244 -0
  112. package/workspace/public/pm/sessions-panel.js +149 -0
  113. package/workspace/public/pm/shell-bell.js +162 -0
  114. package/workspace/public/pm/shell-model.js +83 -0
  115. package/workspace/public/pm/shell-phone.js +124 -0
  116. package/workspace/public/pm/shell-readaloud.js +53 -0
  117. package/workspace/public/pm/shell-recording.js +110 -0
  118. package/workspace/public/pm/shell-ticker.js +90 -0
  119. package/workspace/public/pm/slide-confirm.js +163 -0
  120. package/workspace/public/pm/timeline-forecast-view.js +164 -0
  121. package/workspace/public/pm/today-day.js +8 -2
  122. package/workspace/public/pm/tokens.css +1 -0
  123. package/workspace/server.js +5 -2
  124. package/workspace/test-files.json +8 -0
  125. package/workspace/test-lists/browser-suites-green.json +5 -0
@@ -66,7 +66,63 @@
66
66
  "label": "7 of 12"
67
67
  },
68
68
  "next_due": "2026-11-14",
69
- "last_activity": "2026-10-04T09:00:00Z"
69
+ "last_activity": "2026-10-04T09:00:00Z",
70
+ "nine_stages": [
71
+ {
72
+ "key": "discover",
73
+ "label": "Discover",
74
+ "status": "pending",
75
+ "accepted": false
76
+ },
77
+ {
78
+ "key": "ideate",
79
+ "label": "Ideate",
80
+ "status": "pending",
81
+ "accepted": false
82
+ },
83
+ {
84
+ "key": "strategize",
85
+ "label": "Strategize",
86
+ "status": "pending",
87
+ "accepted": false
88
+ },
89
+ {
90
+ "key": "prioritize",
91
+ "label": "Prioritize",
92
+ "status": "pending",
93
+ "accepted": false
94
+ },
95
+ {
96
+ "key": "plan",
97
+ "label": "Plan",
98
+ "status": "pending",
99
+ "accepted": false
100
+ },
101
+ {
102
+ "key": "build",
103
+ "label": "Build",
104
+ "status": "pending",
105
+ "accepted": false
106
+ },
107
+ {
108
+ "key": "deliver",
109
+ "label": "Deliver",
110
+ "status": "pending",
111
+ "accepted": false
112
+ },
113
+ {
114
+ "key": "monitor",
115
+ "label": "Monitor",
116
+ "status": "pending",
117
+ "accepted": false
118
+ },
119
+ {
120
+ "key": "evaluate",
121
+ "label": "Evaluate",
122
+ "status": "pending",
123
+ "accepted": false
124
+ }
125
+ ]
70
126
  },
71
127
  {
72
128
  "id": "audit-log",
@@ -108,7 +164,63 @@
108
164
  "label": "3 of 5"
109
165
  },
110
166
  "next_due": "2026-10-20",
111
- "last_activity": "2026-09-29T15:00:00Z"
167
+ "last_activity": "2026-09-29T15:00:00Z",
168
+ "nine_stages": [
169
+ {
170
+ "key": "discover",
171
+ "label": "Discover",
172
+ "status": "pending",
173
+ "accepted": false
174
+ },
175
+ {
176
+ "key": "ideate",
177
+ "label": "Ideate",
178
+ "status": "pending",
179
+ "accepted": false
180
+ },
181
+ {
182
+ "key": "strategize",
183
+ "label": "Strategize",
184
+ "status": "pending",
185
+ "accepted": false
186
+ },
187
+ {
188
+ "key": "prioritize",
189
+ "label": "Prioritize",
190
+ "status": "pending",
191
+ "accepted": false
192
+ },
193
+ {
194
+ "key": "plan",
195
+ "label": "Plan",
196
+ "status": "pending",
197
+ "accepted": false
198
+ },
199
+ {
200
+ "key": "build",
201
+ "label": "Build",
202
+ "status": "pending",
203
+ "accepted": false
204
+ },
205
+ {
206
+ "key": "deliver",
207
+ "label": "Deliver",
208
+ "status": "pending",
209
+ "accepted": false
210
+ },
211
+ {
212
+ "key": "monitor",
213
+ "label": "Monitor",
214
+ "status": "pending",
215
+ "accepted": false
216
+ },
217
+ {
218
+ "key": "evaluate",
219
+ "label": "Evaluate",
220
+ "status": "pending",
221
+ "accepted": false
222
+ }
223
+ ]
112
224
  }
113
225
  ],
114
226
  "next": [
@@ -145,7 +257,63 @@
145
257
  "label": "no breakdown"
146
258
  },
147
259
  "next_due": null,
148
- "last_activity": "2026-09-20T10:00:00Z"
260
+ "last_activity": "2026-09-20T10:00:00Z",
261
+ "nine_stages": [
262
+ {
263
+ "key": "discover",
264
+ "label": "Discover",
265
+ "status": "pending",
266
+ "accepted": false
267
+ },
268
+ {
269
+ "key": "ideate",
270
+ "label": "Ideate",
271
+ "status": "pending",
272
+ "accepted": false
273
+ },
274
+ {
275
+ "key": "strategize",
276
+ "label": "Strategize",
277
+ "status": "pending",
278
+ "accepted": false
279
+ },
280
+ {
281
+ "key": "prioritize",
282
+ "label": "Prioritize",
283
+ "status": "pending",
284
+ "accepted": false
285
+ },
286
+ {
287
+ "key": "plan",
288
+ "label": "Plan",
289
+ "status": "pending",
290
+ "accepted": false
291
+ },
292
+ {
293
+ "key": "build",
294
+ "label": "Build",
295
+ "status": "pending",
296
+ "accepted": false
297
+ },
298
+ {
299
+ "key": "deliver",
300
+ "label": "Deliver",
301
+ "status": "pending",
302
+ "accepted": false
303
+ },
304
+ {
305
+ "key": "monitor",
306
+ "label": "Monitor",
307
+ "status": "pending",
308
+ "accepted": false
309
+ },
310
+ {
311
+ "key": "evaluate",
312
+ "label": "Evaluate",
313
+ "status": "pending",
314
+ "accepted": false
315
+ }
316
+ ]
149
317
  }
150
318
  ],
151
319
  "later": []
@@ -191,7 +359,63 @@
191
359
  "label": "no breakdown"
192
360
  },
193
361
  "next_due": null,
194
- "last_activity": "2026-07-01T10:00:00Z"
362
+ "last_activity": "2026-07-01T10:00:00Z",
363
+ "nine_stages": [
364
+ {
365
+ "key": "discover",
366
+ "label": "Discover",
367
+ "status": "pending",
368
+ "accepted": false
369
+ },
370
+ {
371
+ "key": "ideate",
372
+ "label": "Ideate",
373
+ "status": "pending",
374
+ "accepted": false
375
+ },
376
+ {
377
+ "key": "strategize",
378
+ "label": "Strategize",
379
+ "status": "pending",
380
+ "accepted": false
381
+ },
382
+ {
383
+ "key": "prioritize",
384
+ "label": "Prioritize",
385
+ "status": "pending",
386
+ "accepted": false
387
+ },
388
+ {
389
+ "key": "plan",
390
+ "label": "Plan",
391
+ "status": "pending",
392
+ "accepted": false
393
+ },
394
+ {
395
+ "key": "build",
396
+ "label": "Build",
397
+ "status": "pending",
398
+ "accepted": false
399
+ },
400
+ {
401
+ "key": "deliver",
402
+ "label": "Deliver",
403
+ "status": "pending",
404
+ "accepted": false
405
+ },
406
+ {
407
+ "key": "monitor",
408
+ "label": "Monitor",
409
+ "status": "pending",
410
+ "accepted": false
411
+ },
412
+ {
413
+ "key": "evaluate",
414
+ "label": "Evaluate",
415
+ "status": "pending",
416
+ "accepted": false
417
+ }
418
+ ]
195
419
  }
196
420
  ],
197
421
  "proposals": [
@@ -48,7 +48,63 @@
48
48
  "label": "7 of 12"
49
49
  },
50
50
  "next_due": "2026-11-14",
51
- "last_activity": "2026-10-04T09:00:00Z"
51
+ "last_activity": "2026-10-04T09:00:00Z",
52
+ "nine_stages": [
53
+ {
54
+ "key": "discover",
55
+ "label": "Discover",
56
+ "status": "pending",
57
+ "accepted": false
58
+ },
59
+ {
60
+ "key": "ideate",
61
+ "label": "Ideate",
62
+ "status": "pending",
63
+ "accepted": false
64
+ },
65
+ {
66
+ "key": "strategize",
67
+ "label": "Strategize",
68
+ "status": "pending",
69
+ "accepted": false
70
+ },
71
+ {
72
+ "key": "prioritize",
73
+ "label": "Prioritize",
74
+ "status": "pending",
75
+ "accepted": false
76
+ },
77
+ {
78
+ "key": "plan",
79
+ "label": "Plan",
80
+ "status": "pending",
81
+ "accepted": false
82
+ },
83
+ {
84
+ "key": "build",
85
+ "label": "Build",
86
+ "status": "pending",
87
+ "accepted": false
88
+ },
89
+ {
90
+ "key": "deliver",
91
+ "label": "Deliver",
92
+ "status": "pending",
93
+ "accepted": false
94
+ },
95
+ {
96
+ "key": "monitor",
97
+ "label": "Monitor",
98
+ "status": "pending",
99
+ "accepted": false
100
+ },
101
+ {
102
+ "key": "evaluate",
103
+ "label": "Evaluate",
104
+ "status": "pending",
105
+ "accepted": false
106
+ }
107
+ ]
52
108
  },
53
109
  "stepper": [
54
110
  {
@@ -0,0 +1,83 @@
1
+ {
2
+ "_comment": "Items the console3 page makes actionable on more than one tab today (c3-one-home-test.js). Known-failing until the Phase 2 restructure of the console3 feedback plan; the suite fails only on a duplicate that is not listed here. Remove an entry when its duplicate is gone.",
3
+ "duplicates": [
4
+ {
5
+ "title": "accept the 15 oct interview invite",
6
+ "tabs": [
7
+ "brief",
8
+ "work"
9
+ ]
10
+ },
11
+ {
12
+ "title": "approve pricing-page copy before handoff",
13
+ "tabs": [
14
+ "brief",
15
+ "work"
16
+ ]
17
+ },
18
+ {
19
+ "title": "assign owners to 2 unowned workshop actions",
20
+ "tabs": [
21
+ "brief",
22
+ "work"
23
+ ]
24
+ },
25
+ {
26
+ "title": "confirm the 1 oct numbers before the 10:30 update",
27
+ "tabs": [
28
+ "brief",
29
+ "work"
30
+ ]
31
+ },
32
+ {
33
+ "title": "confirm the growth assumption in the q4 forecast",
34
+ "tabs": [
35
+ "brief",
36
+ "work"
37
+ ]
38
+ },
39
+ {
40
+ "title": "pick interview slots with 3 customers",
41
+ "tabs": [
42
+ "brief",
43
+ "work"
44
+ ]
45
+ },
46
+ {
47
+ "title": "review the 13:00 interview guide",
48
+ "tabs": [
49
+ "brief",
50
+ "work"
51
+ ]
52
+ },
53
+ {
54
+ "title": "say yes to sam: ship the logging change",
55
+ "tabs": [
56
+ "brief",
57
+ "work"
58
+ ]
59
+ },
60
+ {
61
+ "title": "self-serve onboarding: continue or park?",
62
+ "tabs": [
63
+ "brief",
64
+ "work"
65
+ ]
66
+ },
67
+ {
68
+ "title": "sso is live with 3 customers",
69
+ "tabs": [
70
+ "brief",
71
+ "init",
72
+ "rep"
73
+ ]
74
+ },
75
+ {
76
+ "title": "the board update is 70% ready",
77
+ "tabs": [
78
+ "init",
79
+ "rep"
80
+ ]
81
+ }
82
+ ]
83
+ }
@@ -102,6 +102,8 @@ When the workspace needs an answer from you, for example the name for a rename o
102
102
 
103
103
  The workspace is neutral. A primary button is the text colour on the page colour, in both themes. The one brand colour marks the Agenda dot and the Agenda count, and nothing else. The bar, the dock and the bottom bar follow the theme; only a terminal stays dark. Corners are square. Controls are icons from one set, not text symbols. On a touch screen every field is 16 pixels, so the phone does not zoom when you tap it, and every control is at least 44 pixels high.
104
104
 
105
+ On the console page (`/c3.html`), text on the blue accent and on a red badge is white in both themes. The page takes this colour from one token, so the colours stay the same when the theme changes.
106
+
105
107
  ## Keys
106
108
 
107
109
  | Key | What it does |
@@ -0,0 +1,81 @@
1
+ 'use strict';
2
+ /*
3
+ * The nine stage names as a label layer over the workflow engine's derived stage (run 3).
4
+ *
5
+ * The engine derives one of six rungs (intake, spec, contract, build, eval, accepted) from the steps
6
+ * it found. This file names the nine stages the PM sees and says which of them each lane uses. It
7
+ * never decides a stage itself: a stage is `done` only when a derived signal shows it, and a stage
8
+ * no record can yet show is `no-signal`, not guessed.
9
+ *
10
+ * The lane sets below are a proposal for the PM to correct (see the run 3 contract in the PRD).
11
+ */
12
+
13
+ const STAGES = ['discover', 'ideate', 'strategize', 'prioritize', 'plan', 'build', 'deliver', 'monitor', 'evaluate'];
14
+
15
+ const LABELS = {
16
+ discover: 'Discover', ideate: 'Ideate', strategize: 'Strategize', prioritize: 'Prioritize', plan: 'Plan',
17
+ build: 'Build', deliver: 'Deliver', monitor: 'Monitor', evaluate: 'Evaluate',
18
+ };
19
+
20
+ // Which stages each product-code lane uses. Stages not listed read `skipped`.
21
+ const LANE_STAGES = {
22
+ feature: STAGES,
23
+ discovery: ['discover', 'ideate', 'strategize', 'prioritize', 'evaluate'],
24
+ chore: ['plan', 'build', 'evaluate'],
25
+ incident: ['build', 'deliver', 'monitor', 'evaluate'],
26
+ content: ['ideate', 'plan', 'build', 'deliver'],
27
+ prototype: ['discover', 'ideate', 'evaluate'],
28
+ };
29
+
30
+ // Stages that have a derived signal today. Every other stage reads `no-signal` until its record exists.
31
+ const SIGNALLED = new Set(['discover', 'plan', 'build', 'evaluate']);
32
+
33
+ const RANK = { intake: 0, spec: 1, contract: 2, build: 3, eval: 4, accepted: 5 };
34
+
35
+ // Stages that gain a signal only when the caller passes the records (scripts/records readAll plus optional `releases`).
36
+ // With no records the output is exactly the older output, so the lifecycle-lens mirror stays equal.
37
+ const RECORD_SIGNALLED = new Set(['ideate', 'strategize', 'prioritize', 'monitor', 'deliver']);
38
+
39
+ // records: { initiative?, anchor?, ideas, hypotheses, decisions, bets, 'kr-metrics', releases } (every list optional).
40
+ // The initiative id and anchor come from `derived` (id, anchor) or from records.initiative / records.anchor.
41
+ function recordSignals(derived, records, buildDone) {
42
+ const id = (derived && derived.id) || records.initiative || null;
43
+ const anchor = (derived && derived.anchor) || records.anchor || null;
44
+ const mine = (list) => (list || []).filter((r) => id && r.initiative === id);
45
+ const kr = (records['kr-metrics'] || []).filter((m) => anchor && (m.rid === anchor || m.parent_kr === anchor || 'kr:' + m.rid === anchor));
46
+ return {
47
+ ideate: mine(records.ideas).length + mine(records.hypotheses).length > 0,
48
+ strategize: mine(records.decisions).length + mine(records.bets).length > 0,
49
+ prioritize: mine(records.bets).some((b) => ['now', 'next', 'later'].includes(b.horizon) && Number.isInteger(b.rank)),
50
+ monitor: !!buildDone && kr.some((m) => (m.readings || []).length > 0),
51
+ deliver: mine(records.releases).length > 0,
52
+ };
53
+ }
54
+
55
+ function nineStages(lane, derived, records) {
56
+ const laneSet = new Set(LANE_STAGES[lane] || STAGES);
57
+ const rank = RANK[derived && derived.stage] ?? 0;
58
+ const step = (key) => (derived && derived.steps || []).find((s) => s.key === key);
59
+ const done = {
60
+ discover: rank >= 1,
61
+ plan: rank >= 2,
62
+ build: rank >= 3 || (step('build') && step('build').status === 'done'),
63
+ evaluate: rank >= 4,
64
+ };
65
+ const accepted = rank >= 5;
66
+ const fromRecords = records && typeof records === 'object' ? recordSignals(derived, records, done.build) : null;
67
+ const out = [];
68
+ let currentSet = false;
69
+ for (const key of STAGES) {
70
+ let status;
71
+ if (!laneSet.has(key)) status = 'skipped';
72
+ else if (!SIGNALLED.has(key) && !(fromRecords && RECORD_SIGNALLED.has(key) && (key !== 'deliver' || fromRecords.deliver))) status = 'no-signal';
73
+ else if (done[key] || (fromRecords && fromRecords[key])) status = 'done';
74
+ else status = 'pending';
75
+ if (status === 'pending' && !currentSet) { status = 'current'; currentSet = true; }
76
+ out.push({ key, label: LABELS[key], status, accepted: key === 'evaluate' ? accepted : undefined });
77
+ }
78
+ return out;
79
+ }
80
+
81
+ module.exports = { STAGES, LABELS, LANE_STAGES, nineStages };
@@ -36,6 +36,7 @@
36
36
  * Zero dependencies, stock Node. It runs in CI, in a terminal, and inside the workspace server.
37
37
  */
38
38
 
39
+ const { nineStages } = require('./nine-stages');
39
40
  const fs = require('fs');
40
41
  const path = require('path');
41
42
  const { execFileSync } = require('child_process');
@@ -1178,6 +1179,7 @@ function computeWorkflowDerived(id, opts) {
1178
1179
  ok: true, id, lane, laneKnown: knownLane, legacy, in_change: inChange,
1179
1180
  last_activity: lastActivity, accepted: accepted.length > 0, stage, record: recRel,
1180
1181
  title: fm.title || id, anchor: fm.parent_kr || null, profile: 'product-code',
1182
+ nine_stages: nineStages(effLane, { stage, steps }),
1181
1183
  ...facts,
1182
1184
  steps, blockers: blockers.map((s) => s.key), gate_failures: gateFailures.map((s) => s.rule || s.key),
1183
1185
  next: next ? { step: next.key, why: next.evidence, action: next.next || next.evidence } : null,
@@ -0,0 +1,51 @@
1
+ 'use strict';
2
+ /* facts-async.js — the repo's initiative facts (scripts/signals/facts.js readRepoFacts) WITHOUT blocking the server.
3
+ *
4
+ * readRepoFacts derives every initiative's stage from git, once per initiative: minutes for ~300 records, and synchronous.
5
+ * Called inside the server process it stops every other request (the page sat "just loading"). This runs it in a child
6
+ * process (scripts/signals/facts-cli.js), keeps one result per root and day for five minutes, and shares one read in
7
+ * flight between callers, so a burst of requests starts one child, not one each. Once a result is older than five minutes it
8
+ * is still returned at once while a new read runs in the background (stale while revalidate), so only the first read waits.
9
+ */
10
+ const { execFile } = require('child_process');
11
+ const path = require('path');
12
+
13
+ const CLI = path.resolve(__dirname, '..', '..', 'scripts', 'signals', 'facts-cli.js');
14
+ const TTL_MS = 5 * 60 * 1000;
15
+ const memo = new Map(); // root|today -> { at, facts }
16
+ const inflight = new Map(); // root|today -> Promise
17
+
18
+ function readFactsAsync(root, opts = {}) {
19
+ const today = opts.today || '';
20
+ const resolved = path.resolve(root || process.cwd());
21
+ const key = resolved + '|' + today;
22
+ const clock = opts.clock || Date.now;
23
+ const hit = memo.get(key);
24
+ const fresh = hit && clock() - hit.at < (opts.ttlMs == null ? TTL_MS : opts.ttlMs);
25
+ if (fresh) return Promise.resolve(hit.facts);
26
+
27
+ const start = () => {
28
+ if (inflight.has(key)) return inflight.get(key);
29
+ const run = opts.run || ((r, t) => new Promise((resolve, reject) => {
30
+ execFile(process.execPath, [CLI, r, t], { maxBuffer: 512 * 1024 * 1024, timeout: 15 * 60 * 1000 }, (err, out) => {
31
+ if (err) return reject(err);
32
+ try { resolve(JSON.parse(out)); } catch (e) { reject(e); }
33
+ });
34
+ }));
35
+ const p = Promise.resolve(run(resolved, today)).then((facts) => {
36
+ memo.set(key, { at: clock(), facts });
37
+ return facts;
38
+ }).finally(() => { inflight.delete(key); });
39
+ inflight.set(key, p);
40
+ return p;
41
+ };
42
+
43
+ // Stale while revalidate: an expired result is still the best answer for now. Return it at once and refresh in the
44
+ // background, so only the very first read of a repository ever waits (opts.stale === false turns this off).
45
+ if (hit && opts.stale !== false) { start().catch(() => {}); return Promise.resolve(hit.facts); }
46
+ return start();
47
+ }
48
+
49
+ function resetFactsCache() { memo.clear(); inflight.clear(); }
50
+
51
+ module.exports = { readFactsAsync, resetFactsCache, TTL_MS };
@@ -0,0 +1,52 @@
1
+ 'use strict';
2
+ /* item-steps.js — per-item steps for the PM's work items (console3 wave A1). Append-only JSONL, like pm-items.js.
3
+ *
4
+ * A step is { id, text, who }, who = 'you' | 'agent-drafts' | 'agent-does'. Events: step_added, who_set, step_removed.
5
+ * OUTWARD RULE: a step that sends something to a person (send, email, message, tell, post, reply to) is never
6
+ * 'agent-does'. It is forced to 'agent-drafts': the agent drafts and the PM's slide releases it. The outbox is
7
+ * scripts/outbox.js; it is deliberately not wired here.
8
+ * The writer is pm-items.appendEvent (same single-writer rule).
9
+ */
10
+ const WHO = ['you', 'agent-drafts', 'agent-does'];
11
+ const OUTWARD = /\b(send|sends|sending|email|emails|message|messages|tell|post|reply to)\b/i;
12
+
13
+ const isOutward = (text) => OUTWARD.test(String(text || ''));
14
+
15
+ // The who a step may hold: outward steps never go beyond 'agent-drafts'.
16
+ function clampWho(text, who) {
17
+ if (!WHO.includes(who)) return null;
18
+ return who === 'agent-does' && isOutward(text) ? 'agent-drafts' : who;
19
+ }
20
+
21
+ function addStep(itemId, text, who, id) {
22
+ const t = String(text || '').trim();
23
+ if (!t) return { ok: false, error: 'the step needs text' };
24
+ const w = clampWho(t, who === undefined ? 'you' : who);
25
+ if (!w) return { ok: false, error: 'who must be you, agent-drafts or agent-does' };
26
+ return { ok: true, forced: w !== (who === undefined ? 'you' : who), event: { event: 'step_added', item_id: itemId, id, text: t, who: w } };
27
+ }
28
+
29
+ function setWho(lines, stepId, who) {
30
+ const step = foldSteps(lines).all.find((s) => s.id === stepId);
31
+ if (!step) return { ok: false, error: 'no such step' };
32
+ const w = clampWho(step.text, who);
33
+ if (!w) return { ok: false, error: 'who must be you, agent-drafts or agent-does' };
34
+ return { ok: true, who: w, forced: w !== who, event: { event: 'who_set', id: stepId, who: w } };
35
+ }
36
+
37
+ // Fold the lines into { byItem: { itemId: [step] }, all: [step] }, in the order added.
38
+ function foldSteps(lines) {
39
+ const steps = new Map();
40
+ for (const ev of lines || []) {
41
+ if (!ev || !ev.id) continue;
42
+ if (ev.event === 'step_added') steps.set(ev.id, { id: ev.id, item_id: ev.item_id, text: ev.text, who: clampWho(ev.text, ev.who) || 'you' });
43
+ else if (ev.event === 'who_set' && steps.has(ev.id)) { const s = steps.get(ev.id); s.who = clampWho(s.text, ev.who) || s.who; }
44
+ else if (ev.event === 'step_removed') steps.delete(ev.id);
45
+ }
46
+ const all = [...steps.values()];
47
+ const byItem = {};
48
+ for (const s of all) (byItem[s.item_id] = byItem[s.item_id] || []).push({ id: s.id, text: s.text, who: s.who });
49
+ return { byItem, all };
50
+ }
51
+
52
+ module.exports = { WHO, isOutward, clampWho, addStep, setWho, foldSteps };
@@ -14,7 +14,7 @@
14
14
  * which(root, key) → the initiative and ticket a key belongs to
15
15
  *
16
16
  * A READING is { key, summary, status, category: todo|doing|done, sprint: { name, state: active|future|closed } | null,
17
- * subtasks: { done, total }, flagged, reason, url }. A ticket the search did not return keeps its last reading, marked
17
+ * subtasks: { done, total }, fixVersion: [names], flagged, reason, url }. A ticket the search did not return keeps its last reading, marked
18
18
  * stale. Titles and statuses only: never a description, a comment or a person.
19
19
  *
20
20
  * BOUNDARY (J15). No network module, no child process. The only write is the snapshot file.
@@ -69,6 +69,11 @@ function flaggedIn(fields) {
69
69
  }
70
70
  return false;
71
71
  }
72
+ /** fixVersions (Jira's release tags) as a list of names; the flat shape may say fixVersion or fixVersions, a name or a list. */
73
+ function fixVersionsOf(v) {
74
+ const list = Array.isArray(v) ? v : v == null || v === '' ? [] : [v];
75
+ return [...new Set(list.map((x) => clip(nameOf(x), 80)).filter(Boolean))];
76
+ }
72
77
  const SPRINT_STATES = ['active', 'future', 'closed'];
73
78
  /** One sprint, from an object { name, state } or a bare name. */
74
79
  function sprintOne(v) {
@@ -128,7 +133,7 @@ function normalizeIssue(x) {
128
133
  const st = f.status || {};
129
134
  const status = nameOf(st) || 'Unknown';
130
135
  const flagged = flaggedIn(f) || /block|impediment|on hold/i.test(status);
131
- return { key, summary, status, category: categoryOf(status, st.statusCategory), sprint: sprintIn(f), subtasks: subtasksOf(f.subtasks),
136
+ return { key, summary, status, category: categoryOf(status, st.statusCategory), sprint: sprintIn(f), subtasks: subtasksOf(f.subtasks), fixVersion: fixVersionsOf(f.fixVersions),
132
137
  flagged, reason: flagged ? (flaggedIn(f) ? 'Flagged in Jira' : `Status: ${status}`) : null, url: urlOf(x, key) };
133
138
  }
134
139
  // The flat shape: what the skill tells the agent to write when it is unsure of the connector's shape.
@@ -136,7 +141,7 @@ function normalizeIssue(x) {
136
141
  if (!summary) return null;
137
142
  const status = oneLine(x.status) || 'Unknown';
138
143
  const flagged = x.flagged === true || /block|impediment|on hold/i.test(status);
139
- return { key, summary, status, category: categoryOf(status, x.category), sprint: sprintOf(x.sprint), subtasks: subtasksOf(x.subtasks),
144
+ return { key, summary, status, category: categoryOf(status, x.category), sprint: sprintOf(x.sprint), subtasks: subtasksOf(x.subtasks), fixVersion: fixVersionsOf(x.fixVersions != null ? x.fixVersions : x.fixVersion),
140
145
  flagged, reason: flagged ? clip(oneLine(x.reason || x.flag_reason || (x.flagged === true ? 'Flagged in Jira' : `Status: ${status}`)), 160) : null,
141
146
  url: urlOf(x, key) };
142
147
  }