create-pmos 0.47.0 → 0.49.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 (78) hide show
  1. package/package.json +1 -1
  2. package/payload/AGENTS-HUMAN.md +1 -1
  3. package/payload/AGENTS.md +1 -1
  4. package/payload/KIT-VERSION +2 -2
  5. package/payload/mods/pmos-cockpit/.claude-plugin/plugin.json +1 -1
  6. package/payload/mods/pmos-cockpit/README.md +14 -3
  7. package/payload/mods/pmos-cockpit/hooks/lib/commands.mjs +34 -6
  8. package/payload/mods/pmos-cockpit/hooks/lib/grants.mjs +22 -3
  9. package/payload/mods/pmos-cockpit/hooks/lib/guard.mjs +16 -3
  10. package/payload/mods/pmos-cockpit/hooks/lib/route.mjs +95 -0
  11. package/payload/mods/pmos-cockpit/hooks/register.tsx +104 -19
  12. package/payload/mods/pmos-cockpit/types/index.d.ts +1 -1
  13. package/payload/planning/evals/eval-calibration.md +1 -1
  14. package/payload/planning/okrs/health-budgets.md +1 -1
  15. package/payload/scripts/pmos-log.sh +18 -8
  16. package/payload/scripts/pmos-model.js +233 -2
  17. package/payload/scripts/status-facts.js +214 -0
  18. package/payload/scripts/workflow-state.js +59 -2
  19. package/payload/state/discoveries.md +1 -1
  20. package/payload/state/log.md +1 -1
  21. package/workspace/README.md +2 -2
  22. package/workspace/bin/live-notes.js +133 -0
  23. package/workspace/design-parity.js +328 -21
  24. package/workspace/doc-category-bench.js +165 -0
  25. package/workspace/fixtures/doc-category-labelled.json +167 -0
  26. package/workspace/guide/index.md +2 -1
  27. package/workspace/guide/initiatives-and-next-steps.md +17 -2
  28. package/workspace/guide/knowledge-graph.md +94 -1
  29. package/workspace/guide/layout.md +31 -2
  30. package/workspace/guide/live-notes.md +78 -0
  31. package/workspace/guide/proactive-engine.md +6 -2
  32. package/workspace/guide/validate-with-typesafe.md +31 -2
  33. package/workspace/lib/claims.js +51 -2
  34. package/workspace/lib/doc-category-corrections.js +113 -0
  35. package/workspace/lib/doc-category-writeback.js +110 -0
  36. package/workspace/lib/doc-category.js +225 -0
  37. package/workspace/lib/engine/pmos-model.js +233 -2
  38. package/workspace/lib/engine/status-facts.js +214 -0
  39. package/workspace/lib/engine/workflow-state.js +59 -2
  40. package/workspace/lib/jev-categorize.js +433 -0
  41. package/workspace/lib/jev-claims.js +59 -15
  42. package/workspace/lib/jev-live.js +37 -0
  43. package/workspace/lib/jev-route.js +139 -0
  44. package/workspace/lib/kg-cli.js +3 -1
  45. package/workspace/lib/kg.js +6 -1
  46. package/workspace/lib/learn.js +5 -3
  47. package/workspace/lib/live-notes.js +1035 -0
  48. package/workspace/lib/typesafe-settings.js +10 -2
  49. package/workspace/package-lock.json +1 -1
  50. package/workspace/package.json +5 -4
  51. package/workspace/public/pm/app.js +148 -79
  52. package/workspace/public/pm/data.js +78 -0
  53. package/workspace/public/pm/fixture.js +40 -2
  54. package/workspace/public/pm/modes-live.js +279 -18
  55. package/workspace/public/pm/panel-cockpit.js +638 -0
  56. package/workspace/public/pm/panel-detail.js +212 -8
  57. package/workspace/public/pm/panel-initiatives.js +9 -9
  58. package/workspace/public/pm/panel-newsess.js +145 -86
  59. package/workspace/public/pm/panel-repos.js +34 -17
  60. package/workspace/public/pm/panel-tabs.js +249 -29
  61. package/workspace/public/pm/state.js +48 -4
  62. package/workspace/public/pm/tab-doc.js +244 -16
  63. package/workspace/public/pm/tab-term.js +11 -1
  64. package/workspace/public/pm/tokens.css +4 -3
  65. package/workspace/public/pm/view-addrepo.js +207 -0
  66. package/workspace/public/pm/view-files.js +197 -0
  67. package/workspace/public/pm/view-guide.js +141 -0
  68. package/workspace/public/pm/view-knowledge.js +225 -0
  69. package/workspace/public/pm/view-livenotes.js +397 -0
  70. package/workspace/public/pm/view-memory.js +122 -0
  71. package/workspace/public/pm/view-portfolio.js +153 -0
  72. package/workspace/public/pm/view-reports.js +190 -0
  73. package/workspace/public/pm/view-scheduled.js +161 -0
  74. package/workspace/public/pm/view-skills.js +216 -0
  75. package/workspace/public/pm/view-today.js +168 -0
  76. package/workspace/public/pm/view-validate.js +171 -0
  77. package/workspace/server.js +349 -7
  78. package/workspace/skills/live-notes.skill +58 -0
@@ -50,7 +50,8 @@
50
50
  */
51
51
 
52
52
  import { el, setChildren } from './dom.js';
53
- import { state, setState, FIXTURE } from './state.js';
53
+ import { state, setState, FIXTURE, LEGACY_LAYOUT_FLAG } from './state.js';
54
+ import { isMobile } from './mobile.js';
54
55
  import * as FX from './fixture.js';
55
56
  import { openDoc, openTab, openTerm, fileNewModel } from './panel-initiatives.js';
56
57
  import { addProductBody } from './panel-repos.js';
@@ -59,6 +60,10 @@ import { statusBanner, lifecycleNotes } from './status-banner.js';
59
60
  import { stepActions } from './next-step-ui.js';
60
61
  import { boardOf, records, currentProduct } from './board-read.js';
61
62
 
63
+ /** The cockpit's own look of this panel (workspace-cockpit-nav): the mockup's header, chips and phase rows. A phone and `?layout=legacy`
64
+ * keep the prototype's look, which the older suites and the phone still read. */
65
+ const cockpitLook = () => !LEGACY_LAYOUT_FLAG && !isMobile();
66
+
62
67
  /* ── the prototype's small helpers ─────────────────────────────────────────────────────────── */
63
68
 
64
69
  /** `{{ expr }}` — an interpolated value, wrapped the way the prototype's runtime wraps it. */
@@ -374,6 +379,24 @@ function noLadderBlock(ansBg, ansC, ansText, noLadderText) {
374
379
  * all done → done; some done → doing; else todo. Collapse is per phase PER INITIATIVE (`pkey`), so
375
380
  * folding `build` on one initiative does not fold it on the next.
376
381
  */
382
+ /**
383
+ * What the documents say, beside the ladder and never in it (status-from-artifacts, Run 1). One plain
384
+ * line each, with its basis. A flag colours its own dot only; no phase, count or total reads it.
385
+ */
386
+ function factsBlock(rec) {
387
+ const facts = (rec && rec.facts) || [];
388
+ // An initiative with a ladder always gets the section; with nothing to compare it says what each check waits for.
389
+ if (!facts.length && !(rec && rec.factsWhy)) return null;
390
+ return el('div', { 'data-facts': 'status-from-artifacts', style: 'border-top:1px solid var(--border);padding:8px 14px 8px' },
391
+ el('div', { style: 'font-family:var(--mono);font-size:9.5px;letter-spacing:.12em;text-transform:uppercase;color:var(--muted-foreground);padding-bottom:5px' }, iv('What the documents say')),
392
+ facts.length ? null : el('div', { 'data-facts-empty': '', style: 'font-size:12px;line-height:1.45;color:var(--muted-foreground)' }, iv(rec.factsWhy)),
393
+ ...facts.map((f) => el('div', { style: 'display:grid;grid-template-columns:14px 104px 1fr;gap:6px;align-items:baseline;padding:2px 0;font-size:12px;line-height:1.45' },
394
+ el('span', { style: 'font-size:9px;color:' + (FX.TONE[f.tone] || FX.C.mut) }, iv('●')),
395
+ el('span', { style: 'color:var(--muted-foreground)' }, iv(f.label)),
396
+ el('span', { style: 'color:var(--foreground)' }, iv(f.text)),
397
+ )));
398
+ }
399
+
377
400
  function phaseBlock(slug, rec) {
378
401
  const phases = FX.phasesFor(rec, slug);
379
402
  if (!phases.length) return null;
@@ -735,7 +758,7 @@ function syncSummary(sy, dest) {
735
758
  const ROLE_CHIP = { 'user stories': 'stories' };
736
759
  function roleChip(role) { return ROLE_CHIP[role] || role; }
737
760
 
738
- function artifactRow(slug, a) {
761
+ function artifactRow(slug, a, noHead) {
739
762
  const key = FX.artKey(slug, a.path);
740
763
  // `hasOwnProperty`, not `||`. An unlinked artifact records `null` under its key, and a falsy check
741
764
  // would fall straight through to the server's `a.dest` and re-draw the link the PM just withdrew.
@@ -841,7 +864,7 @@ function artifactRow(slug, a) {
841
864
  }, '+ ', iv(ls.l))),
842
865
  );
843
866
 
844
- return el('div', { style: 'min-width:0' }, head, linkRow, openHere ? destForm(openHere, '') : null);
867
+ return el('div', { style: 'min-width:0' }, noHead ? null : head, linkRow, openHere ? destForm(openHere, '') : null);
845
868
  }
846
869
 
847
870
  // Linked: the destination pill, the sync button, the PM's own controls, and the status line —
@@ -909,7 +932,7 @@ function artifactRow(slug, a) {
909
932
  panel = realSyncPanel(slug, a, key, dest);
910
933
  }
911
934
 
912
- return el('div', { style: 'min-width:0' }, head, pillRow, openHere ? destForm(openHere, dest.target) : null, panel);
935
+ return el('div', { style: 'min-width:0' }, noHead ? null : head, pillRow, openHere ? destForm(openHere, dest.target) : null, panel);
913
936
  }
914
937
 
915
938
  /**
@@ -1139,6 +1162,98 @@ function footer(rec) {
1139
1162
 
1140
1163
  /* ── the `dInit` branch, assembled in the prototype's order ─────────────────────────────────── */
1141
1164
 
1165
+ /* ── the cockpit look: the mockup's title block and phase rows (planning/design/cockpit-mockup/WsA.dc.html) ────────────────── */
1166
+
1167
+ const CK_BADGE = 'font:500 11px var(--sans);border:1px solid var(--border);border-radius:999px;padding:0 8px;line-height:18px;color:var(--foreground);white-space:nowrap';
1168
+ const CK_SEP = () => el('div', { style: 'height:1px;background:var(--border)' });
1169
+
1170
+ /** Title, summary and chips: the lane, the anchor and the flow, as pills. */
1171
+ function cockpitTitle(name, tag, pills) {
1172
+ return [
1173
+ el('div', { style: 'font:600 17px var(--sans);overflow-wrap:anywhere' }, iv(name)),
1174
+ el('div', { style: 'color:var(--muted-foreground)' }, iv(tag)),
1175
+ el('div', { style: 'display:flex;gap:6px;flex-wrap:wrap' }, pills.map((pl) => el('span', { style: CK_BADGE }, iv(pl.t)))),
1176
+ ];
1177
+ }
1178
+
1179
+ const CK_PH_DOT = { done: 'var(--success)', blocked: 'var(--destructive)', doing: 'var(--info)', todo: 'var(--muted-foreground)' };
1180
+
1181
+ /** The phases as the mockup draws them: a row per phase with its count, its tasks beneath while it is open. */
1182
+ function phaseBlockCockpit(slug, rec) {
1183
+ const phases = FX.phasesFor(rec, slug);
1184
+ if (!phases.length) return null;
1185
+ const collP = state.collapsedPhases || {};
1186
+ const repo = repoOf(slug);
1187
+ /* Run 4 (the mockup): only the phase the initiative is at opens by itself — the same pointer the breadcrumb uses (the first
1188
+ * blocked step, else the current one). Every other phase shows its row and its count, and opens on a click. A phase the PM has
1189
+ * opened or folded keeps that choice (`collapsedPhases` holds true or false for it). */
1190
+ const stepAt = Array.isArray(rec.steps) ? FX.targetStepIndex(rec.steps) : -1;
1191
+ const atKey = stepAt >= 0 && rec.steps[stepAt] ? rec.steps[stepAt].k : null;
1192
+ const anyTaskTab = (state.tabs || []).some((t) => t.id === state.active && t.kind === 'task' && t.slug === slug);
1193
+ return el('div', { 'data-cockpit': 'phases', style: 'display:flex;flex-direction:column;gap:2px' }, phases.map((ph) => {
1194
+ const pkey = slug + '|' + ph.key;
1195
+ const open = Object.prototype.hasOwnProperty.call(collP, pkey) ? !collP[pkey] : ph.key === atKey;
1196
+ // the task the initiative is at: the first blocked (else in-progress) task of that phase, marked while no task tab is in front
1197
+ const atTask = ph.key === atKey && !anyTaskTab ? ph.tasks.findIndex((t) => t.status === 'blocked') : -1;
1198
+ const atIdx = atTask >= 0 ? atTask : (ph.key === atKey && !anyTaskTab ? ph.tasks.findIndex((t) => t.status === 'doing') : -1);
1199
+ const head = el('button', {
1200
+ onClick: () => setState((s) => ({ collapsedPhases: { ...s.collapsedPhases, [pkey]: open } })),
1201
+ style: 'display:flex;align-items:center;gap:8px;min-height:32px;font:500 11px var(--mono);letter-spacing:.08em;text-transform:uppercase;text-align:left;width:100%',
1202
+ },
1203
+ el('span', null, open ? '▾' : '▸'),
1204
+ el('span', { style: 'width:8px;height:8px;border-radius:50%;display:inline-block;flex-shrink:0;background:' + (CK_PH_DOT[ph.status] || CK_PH_DOT.todo) }),
1205
+ ph.label.charAt(0).toUpperCase() + ph.label.slice(1),
1206
+ el('span', { style: 'margin-left:auto;color:var(--muted-foreground)' }, iv(ph.count)),
1207
+ );
1208
+ const rows = open ? ph.tasks.map((t, ti) => {
1209
+ const tid = FX.taskTabId(slug, ph.key, ti);
1210
+ const active = state.active === tid || ti === atIdx;
1211
+ const [glyph, color] = FX.TDOT[t.status] || FX.TDOT.todo;
1212
+ return el('button', {
1213
+ onClick: () => openTask({
1214
+ id: tid, kind: 'task', role: 'TASK', title: t.title, slug, repo, phase: ph.key,
1215
+ status: t.status, instruction: t.instruction, input: t.input, expect: t.expect,
1216
+ evalNote: t.evalNote, output: t.output,
1217
+ }),
1218
+ 'aria-current': ti === atIdx ? 'step' : null,
1219
+ style: 'display:flex;align-items:center;gap:10px;min-height:34px;padding:0 8px 0 24px;border-radius:8px;font:400 13px var(--sans);text-align:left;width:100%;background:' + (active ? 'var(--accent)' : 'transparent'),
1220
+ hover: 'background:var(--accent)',
1221
+ },
1222
+ el('span', { style: 'color:' + color }, glyph),
1223
+ el('span', { style: 'flex-grow:1' }, iv(t.title)),
1224
+ el('span', { style: 'color:var(--muted-foreground)' }, '↗'),
1225
+ );
1226
+ }) : [];
1227
+ return [head, rows];
1228
+ }));
1229
+ }
1230
+
1231
+ /** The blocker card (workspace-cockpit-nav): drawn only when a rung of the ladder is blocking, in the engine's own words. The cockpit
1232
+ * draws it as the mockup's row: the words on the left, and on the right the slot of the Grant button, which stays empty until the
1233
+ * grants ledger behind it exists (PRD Q3) — the slot keeps the card's height, so nothing moves when the button arrives. */
1234
+ function blockerCard(b) {
1235
+ if (cockpitLook()) {
1236
+ const said = el('span', { style: 'color:var(--destructive)' }, iv(b.label + ' blocked' + (b.evidence ? ': ' + b.evidence : '')));
1237
+ return el('div', {
1238
+ 'data-cockpit': 'blocker',
1239
+ style: 'border:1px solid color-mix(in oklab, var(--destructive) 50%, transparent);background:color-mix(in oklab, var(--destructive) 8%, transparent);border-radius:8px;padding:10px 12px;display:flex;align-items:center;justify-content:space-between;gap:10px',
1240
+ },
1241
+ b.next
1242
+ ? el('div', { style: 'display:flex;flex-direction:column;gap:4px;min-width:0' }, said,
1243
+ el('span', { style: 'color:var(--muted-foreground);font-size:12px;line-height:1.5' }, iv('next: ' + b.next)))
1244
+ : said,
1245
+ el('span', { 'data-cockpit': 'grant-slot', 'aria-hidden': 'true', style: 'flex:none;width:0;height:30px' }),
1246
+ );
1247
+ }
1248
+ return el('div', {
1249
+ 'data-cockpit': 'blocker',
1250
+ style: 'border:1px solid color-mix(in oklab, var(--destructive) 50%, transparent);background:color-mix(in oklab, var(--destructive) 8%, transparent);border-radius:8px;padding:10px 12px;display:flex;flex-direction:column;gap:4px',
1251
+ },
1252
+ el('span', { style: 'color:var(--destructive);font-size:13px' }, iv(b.label + ' blocked' + (b.evidence ? ': ' + b.evidence : ''))),
1253
+ b.next ? el('span', { style: 'color:var(--muted-foreground);font-size:12px;line-height:1.5' }, iv('next: ' + b.next)) : null,
1254
+ );
1255
+ }
1256
+
1142
1257
  function initBody(slug) {
1143
1258
  const rec = slug ? records()[slug] : null;
1144
1259
  if (!rec) return [noneBody()];
@@ -1153,7 +1268,12 @@ function initBody(slug) {
1153
1268
  return out;
1154
1269
  }
1155
1270
 
1156
- out.push(headerBlock(slug, rec.tag || '', rec.pills || [], rec.crumb || null, lanePicker(slug, rec)));
1271
+ const ck = cockpitLook();
1272
+ if (ck) return cockpitInitBody(slug, rec);
1273
+ {
1274
+ out.push(headerBlock(slug, rec.tag || '', rec.pills || [], rec.crumb || null, lanePicker(slug, rec)));
1275
+ if (rec.blocker) out.push(el('div', { style: 'margin:12px 14px 0' }, blockerCard(rec.blocker)));
1276
+ }
1157
1277
  { const sb = statusBanner(repoOf(slug), slug, rec); if (sb) out.push(sb); }
1158
1278
  { const lc = lifecycleRow(repoOf(slug), slug); if (lc) { out.push(lc); const ln = lifecycleNotes(rec); if (ln) out.push(ln); } }
1159
1279
 
@@ -1170,8 +1290,9 @@ function initBody(slug) {
1170
1290
  const nextB = nextStepsBlock(slug, rec);
1171
1291
  if (nextB) out.push(nextB);
1172
1292
 
1173
- const phases = phaseBlock(slug, rec);
1174
- if (phases) out.push(phases);
1293
+ if (!ck) { const phases = phaseBlock(slug, rec); if (phases) out.push(phases); }
1294
+
1295
+ { const fb = factsBlock(rec); if (fb) out.push(fb); }
1175
1296
 
1176
1297
  out.push(el('div', { style: 'border-top:1px solid var(--border)' },
1177
1298
  linksRow(slug),
@@ -1182,6 +1303,70 @@ function initBody(slug) {
1182
1303
  return out;
1183
1304
  }
1184
1305
 
1306
+ /* ── the cockpit's initiative (Run 4: the mockup's column, section for section) ─────────────────────────────────────────────
1307
+ * The detail host is the mockup's column: 16px padding and a 12px gap between its sections (renderDetail sets that). In order:
1308
+ * title, summary, chips, the blocker card, a rule, the phases, then what the mockup does not draw (the anchor line, the lane picker,
1309
+ * the status, the sessions, the next steps, the facts), a rule, the artifacts, the links and the derivation line. A section with
1310
+ * nothing to show is not drawn, so it leaves no gap. The sections the mockup does not draw were laid out for the full width of the
1311
+ * column, so they keep it (`FULL`). */
1312
+ const FULL = 'margin:0 -16px';
1313
+ function cockpitInitBody(slug, rec) {
1314
+ const ph = phaseBlockCockpit(slug, rec);
1315
+ const out = [...cockpitTitle(slug, rec.tag || '', rec.pills || [])];
1316
+ if (rec.blocker) out.push(blockerCard(rec.blocker));
1317
+ if (ph) out.push(CK_SEP(), ph);
1318
+ const more = [crumbBlock(rec.crumb || null), lanePicker(slug, rec)].filter(Boolean);
1319
+ if (more.length) out.push(el('div', null, more));
1320
+ const wide = [];
1321
+ { const sb = statusBanner(repoOf(slug), slug, rec); if (sb) wide.push(sb); }
1322
+ { const lc = lifecycleRow(repoOf(slug), slug); if (lc) { wide.push(lc); const ln = lifecycleNotes(rec); if (ln) wide.push(ln); } }
1323
+ if (rec.noLadder) {
1324
+ const ansC = FX.TONE[(rec.answer && rec.answer.tone) || 'info'] || FX.C.mut;
1325
+ wide.push(...noLadderBlock(FX.wash(ansC), ansC, (rec.answer && rec.answer.text) || '', rec.noLadder));
1326
+ }
1327
+ { const sess = sessionsBlock(slug); if (sess) wide.push(sess); }
1328
+ { const nextB = nextStepsBlock(slug, rec); if (nextB) wide.push(nextB); }
1329
+ { const fb = factsBlock(rec); if (fb) wide.push(fb); }
1330
+ if (wide.length) out.push(el('div', { style: FULL }, wide));
1331
+ out.push(CK_SEP(), cockpitArtifacts(slug, rec));
1332
+ // The links row's + controls record a reference through the server; the fixture has no server and no links, so it has none.
1333
+ const links = (rec.links || []).length || ((state.initLinks || {})[slug] || []).length || !FIXTURE ? linksRow(slug) : null;
1334
+ if (links) out.push(el('div', { style: FULL + ';border-top:1px solid var(--border)' }, links));
1335
+ // The derivation line says which working tree the panel was read from and when. The fixture read none, so it has no line.
1336
+ if (!FIXTURE) out.push(el('div', { style: 'margin:auto -16px -16px' }, footer(rec)));
1337
+ return out;
1338
+ }
1339
+
1340
+ /** The mockup's artifacts: a label, then a row per artifact — its role, its path — with the artifact's link and sync controls
1341
+ * under the row (`data-art-links`). The mockup shows a sync badge in the row ("⇄ in sync"); the app never claims that, because it
1342
+ * has never compared the two documents (see SYNC_NOT_A_SYNC), so the row says what is recorded through the controls instead. */
1343
+ const CK_LBL = 'font:500 10px var(--mono);letter-spacing:.1em;text-transform:uppercase;color:var(--muted-foreground)';
1344
+ function cockpitArtifacts(slug, rec) {
1345
+ const arts = rec.arts || [];
1346
+ const rows = [];
1347
+ for (const a of arts) {
1348
+ rows.push(el('button', {
1349
+ onClick: () => openDoc(slug, a.path, a.role), 'data-art-row': a.path, title: a.path,
1350
+ style: 'display:flex;align-items:center;gap:8px;width:100%;text-align:left', hover: 'background:var(--accent)',
1351
+ },
1352
+ el('span', { style: CK_BADGE + ';font-family:var(--mono)' }, iv(roleChip(a.role))),
1353
+ el('span', { 'data-art-path': '', style: 'flex-grow:1;min-width:0;color:var(--muted-foreground);font:11px var(--mono)' }, iv(shortPath(a.path))),
1354
+ ));
1355
+ rows.push(el('div', { 'data-art-links': a.path }, artifactRow(slug, a, true)));
1356
+ }
1357
+ if (!arts.length) rows.push(el('span', { style: 'font-size:11.5px;color:var(--muted-foreground);font-style:italic' }, 'none filed'));
1358
+ const conv = conversationsBlock(slug, rec), git = gitFilesBlock(slug, rec);
1359
+ return el('div', { 'data-cockpit': 'artifacts', style: 'display:flex;flex-direction:column;gap:6px' },
1360
+ el('div', { style: CK_LBL }, 'Artifacts'), rows, conv, git);
1361
+ }
1362
+
1363
+ /** A path as the mockup writes one: whole when it is short, else its first 24 characters, an ellipsis and its last 7, so the folder and
1364
+ * the end of the name both stay readable ("planning/prd/ooh-weather…-prd.md"). The row's tooltip carries the whole path. */
1365
+ export function shortPath(p) {
1366
+ const s0 = String(p || '');
1367
+ return s0.length <= 32 ? s0 : s0.slice(0, 24) + '…' + s0.slice(-7);
1368
+ }
1369
+
1185
1370
  /* ── `dNone` (lines 838–840) ────────────────────────────────────────────────────────────────── */
1186
1371
 
1187
1372
  function noneBody() {
@@ -1439,7 +1624,26 @@ export function renderDetail(host) {
1439
1624
  else if (kind === 'none') body = [noneBody()];
1440
1625
  else body = modeDetailBody(kind);
1441
1626
 
1442
- return setChildren(host, [stickyHeader, ...body]);
1627
+ // the cockpit draws an initiative without the panel's own title bar (the top bar's breadcrumb names it); its fold control returns with Focus
1628
+ const bare = cockpitLook() && kind === 'init';
1629
+ /* The cockpit's column (outside Focus) is the mockup's: the page's own background, and for an initiative 16px of padding and a 12px
1630
+ * gap between sections. The legacy shell, a phone and Focus keep the panel's sidebar fill and draw their own padding. */
1631
+ const ckCol = cockpitLook() && !state.layoutFocus;
1632
+ host.style.background = ckCol ? 'transparent' : 'var(--sidebar)';
1633
+ host.style.padding = ckCol && bare ? '16px' : '';
1634
+ host.style.gap = ckCol && bare ? '12px' : '';
1635
+ if (bare && state.layoutFocus) {
1636
+ // Focus (Run 2): the detail is a 360px slice with a sticky bar that names what is asked of the PM and leads back to the lists
1637
+ const slug = selectedSlug();
1638
+ const rows = (state.statuses && state.statuses[state.product]) || {};
1639
+ const key = window.PMOSStatusGroups ? window.PMOSStatusGroups.keyOf(rows[slug]) : 'unknown';
1640
+ const ask = key === 'needs-you' ? 'Needs you' : key === 'awaiting-acceptance' ? 'Awaiting your acceptance' : 'Nothing is asked of you';
1641
+ const bar = el('div', { 'data-focus': 'bar', style: 'position:sticky;top:0;z-index:2;display:flex;align-items:center;gap:8px;padding:8px 12px;border-bottom:1px solid var(--border);background:var(--sidebar)' },
1642
+ el('span', { style: 'font:500 12px var(--sans);min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap' }, ask),
1643
+ el('button', { 'data-focus': 'navigate', onClick: () => setState({ layoutFocus: false }), style: 'margin-left:auto;font:500 11px var(--sans);border:1px solid var(--border);border-radius:6px;padding:2px 8px', hover: 'background:var(--accent)' }, 'Navigate'));
1644
+ return setChildren(host, [bar, ...body]);
1645
+ }
1646
+ return setChildren(host, bare ? body : [stickyHeader, ...body]);
1443
1647
  }
1444
1648
 
1445
1649
  export default renderDetail;
@@ -96,7 +96,7 @@ function baseGroups(product) {
96
96
  }
97
97
 
98
98
  /** GROUPS (lines 1425–1430) — user groups appended, user KRs and user initiatives folded in. */
99
- function groupsFor(product) {
99
+ export function groupsFor(product) {
100
100
  const extraGroups = (state.userGroups && state.userGroups[product]) || [];
101
101
  return [...baseGroups(product), ...extraGroups].map((g) => {
102
102
  const extra = (state.addedInits && state.addedInits[product + '|' + g.gid]) || [];
@@ -117,10 +117,10 @@ function firstSlugOf(product) {
117
117
  return g0 && g0.slugs && g0.slugs.length ? g0.slugs[0] : null;
118
118
  }
119
119
 
120
- const selectedSlug = () => (state.sel !== null && state.sel !== undefined ? state.sel : firstSlugOf(currentProduct()));
120
+ export const selectedSlug = () => (state.sel !== null && state.sel !== undefined ? state.sel : firstSlugOf(currentProduct()));
121
121
 
122
122
  /** sessionsOf (line 1398) — a record that failed to read has no sessions, it does not throw. */
123
- function sessionsOf(slug, DATA) {
123
+ export function sessionsOf(slug, DATA) {
124
124
  const i = DATA[slug];
125
125
  if (!i || i.readError) return [];
126
126
  return [...(i.sessions || []), ...((state.extraSess && state.extraSess[slug]) || [])];
@@ -157,7 +157,7 @@ function liveSessions() {
157
157
  * why this returns the whole shape rather than just the map: a running agent PMOS cannot attribute
158
158
  * still has to be visible somewhere, and dropping it was half the defect this fixes.
159
159
  */
160
- function attribution() {
160
+ export function attribution() {
161
161
  const empty = { byInitiative: {}, via: {}, unattributed: [] };
162
162
  if (FIXTURE) return empty;
163
163
  const ws = WS();
@@ -172,7 +172,7 @@ function attribution() {
172
172
  * `onBoard` is what stops a tab or a working tree belonging to ANOTHER repository putting a row on
173
173
  * this repository's board — `slugProduct()` is the same map panel 4 scopes its tabs by.
174
174
  */
175
- function workingSet(attr, product) {
175
+ export function workingSet(attr, product) {
176
176
  if (FIXTURE) return [];
177
177
  const ws = WS();
178
178
  if (!ws) return [];
@@ -198,7 +198,7 @@ function workingSet(attr, product) {
198
198
  */
199
199
  const HASH = ['#', 'var(--muted-foreground)', ''];
200
200
 
201
- function leadOf(slug, live) {
201
+ export function leadOf(slug, live) {
202
202
  const rows = live[slug];
203
203
  if (!rows || !rows.length) return HASH;
204
204
  const ss = SS();
@@ -397,7 +397,7 @@ function groupNode(g, product) {
397
397
  * knowledge of what the session is for into a recorded declaration, which is the only way a session
398
398
  * started before this field existed can ever join an initiative.
399
399
  */
400
- function orphanRows(rows) {
400
+ export function orphanRows(rows) {
401
401
  const ss = SS();
402
402
  const ws = WS();
403
403
  const head = el('div', {
@@ -451,7 +451,7 @@ function orphanRows(rows) {
451
451
  * `state.extraSess`, the sessions this window started, which have no server row until the next poll.
452
452
  * `seen` is what stops one session arriving twice when both paths find it.
453
453
  */
454
- function searchHits(q, DATA, live) {
454
+ export function searchHits(q, DATA, live) {
455
455
  const hits = [];
456
456
  if (!q) return hits;
457
457
  const seen = new Set();
@@ -483,7 +483,7 @@ function searchHits(q, DATA, live) {
483
483
  return hits.slice(0, 8);
484
484
  }
485
485
 
486
- function hitNode(ht) {
486
+ export function hitNode(ht) {
487
487
  return el('button', {
488
488
  onClick: ht.on,
489
489
  style: 'display:block;width:100%;padding:6px 12px;min-height:40px',