create-pmos 0.46.0 → 0.48.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 (83) 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 +1 -1
  5. package/payload/mods/pmos-cockpit/.claude-plugin/plugin.json +1 -1
  6. package/payload/mods/pmos-cockpit/README.md +13 -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/handover-run.mjs +33 -0
  11. package/payload/mods/pmos-cockpit/hooks/lib/handover.mjs +267 -0
  12. package/payload/mods/pmos-cockpit/hooks/register.tsx +189 -18
  13. package/payload/mods/pmos-cockpit/types/index.d.ts +16 -1
  14. package/payload/okf/core/concepts/frontmatter-format.md +6 -1
  15. package/payload/planning/evals/eval-calibration.md +1 -1
  16. package/payload/planning/okrs/health-budgets.md +1 -1
  17. package/payload/scripts/pmos-log.sh +18 -8
  18. package/payload/scripts/pmos-model.js +233 -2
  19. package/payload/scripts/status-facts.js +204 -0
  20. package/payload/scripts/workflow-state.js +52 -0
  21. package/payload/skills/SKILL-FORMAT.md +1 -0
  22. package/payload/state/discoveries.md +1 -1
  23. package/workspace/README.md +3 -3
  24. package/workspace/bin/live-notes.js +133 -0
  25. package/workspace/design-parity.js +328 -21
  26. package/workspace/doc-category-bench.js +165 -0
  27. package/workspace/fixtures/doc-category-labelled.json +167 -0
  28. package/workspace/guide/initiatives-and-next-steps.md +16 -2
  29. package/workspace/guide/knowledge-graph.md +89 -2
  30. package/workspace/guide/layout.md +30 -2
  31. package/workspace/guide/proactive-engine.md +26 -4
  32. package/workspace/guide/scheduled-tasks-and-cli.md +1 -0
  33. package/workspace/guide/statuses.md +1 -0
  34. package/workspace/guide/validate-with-typesafe.md +17 -1
  35. package/workspace/lib/claims.js +51 -2
  36. package/workspace/lib/doc-category-corrections.js +113 -0
  37. package/workspace/lib/doc-category-writeback.js +110 -0
  38. package/workspace/lib/doc-category.js +225 -0
  39. package/workspace/lib/engine/pmos-model.js +233 -2
  40. package/workspace/lib/engine/status-facts.js +204 -0
  41. package/workspace/lib/engine/workflow-state.js +52 -0
  42. package/workspace/lib/jev-categorize.js +399 -0
  43. package/workspace/lib/jev-claims.js +59 -15
  44. package/workspace/lib/jev-route.js +129 -0
  45. package/workspace/lib/kg-cli.js +5 -2
  46. package/workspace/lib/kg-dangling-baseline.json +1 -0
  47. package/workspace/lib/kg-dangling-rules.json +1 -0
  48. package/workspace/lib/kg.js +26 -9
  49. package/workspace/lib/learn.js +282 -0
  50. package/workspace/lib/live-notes.js +890 -0
  51. package/workspace/lib/proactive-cli.js +49 -1
  52. package/workspace/lib/proactive.js +8 -7
  53. package/workspace/lib/typesafe-settings.js +10 -2
  54. package/workspace/package-lock.json +1 -1
  55. package/workspace/package.json +5 -4
  56. package/workspace/public/pm/app.js +148 -79
  57. package/workspace/public/pm/data.js +58 -0
  58. package/workspace/public/pm/fixture.js +40 -2
  59. package/workspace/public/pm/modes-live.js +267 -21
  60. package/workspace/public/pm/panel-cockpit.js +636 -0
  61. package/workspace/public/pm/panel-detail.js +210 -8
  62. package/workspace/public/pm/panel-initiatives.js +9 -9
  63. package/workspace/public/pm/panel-newsess.js +145 -86
  64. package/workspace/public/pm/panel-repos.js +34 -17
  65. package/workspace/public/pm/panel-tabs.js +145 -27
  66. package/workspace/public/pm/state.js +45 -4
  67. package/workspace/public/pm/tab-doc.js +244 -16
  68. package/workspace/public/pm/tab-term.js +11 -1
  69. package/workspace/public/pm/tokens.css +4 -3
  70. package/workspace/public/pm/view-addrepo.js +207 -0
  71. package/workspace/public/pm/view-files.js +197 -0
  72. package/workspace/public/pm/view-guide.js +141 -0
  73. package/workspace/public/pm/view-knowledge.js +225 -0
  74. package/workspace/public/pm/view-memory.js +122 -0
  75. package/workspace/public/pm/view-portfolio.js +153 -0
  76. package/workspace/public/pm/view-reports.js +190 -0
  77. package/workspace/public/pm/view-scheduled.js +161 -0
  78. package/workspace/public/pm/view-skills.js +216 -0
  79. package/workspace/public/pm/view-today.js +168 -0
  80. package/workspace/public/pm/view-validate.js +171 -0
  81. package/workspace/server.js +394 -10
  82. package/workspace/skills/live-notes.skill +58 -0
  83. package/workspace/skills/morning-brief.skill +1 -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,22 @@ 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
+ if (!facts.length) return null;
389
+ return el('div', { 'data-facts': 'status-from-artifacts', style: 'border-top:1px solid var(--border);padding:8px 14px 8px' },
390
+ 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')),
391
+ ...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' },
392
+ el('span', { style: 'font-size:9px;color:' + (FX.TONE[f.tone] || FX.C.mut) }, iv('●')),
393
+ el('span', { style: 'color:var(--muted-foreground)' }, iv(f.label)),
394
+ el('span', { style: 'color:var(--foreground)' }, iv(f.text)),
395
+ )));
396
+ }
397
+
377
398
  function phaseBlock(slug, rec) {
378
399
  const phases = FX.phasesFor(rec, slug);
379
400
  if (!phases.length) return null;
@@ -735,7 +756,7 @@ function syncSummary(sy, dest) {
735
756
  const ROLE_CHIP = { 'user stories': 'stories' };
736
757
  function roleChip(role) { return ROLE_CHIP[role] || role; }
737
758
 
738
- function artifactRow(slug, a) {
759
+ function artifactRow(slug, a, noHead) {
739
760
  const key = FX.artKey(slug, a.path);
740
761
  // `hasOwnProperty`, not `||`. An unlinked artifact records `null` under its key, and a falsy check
741
762
  // would fall straight through to the server's `a.dest` and re-draw the link the PM just withdrew.
@@ -841,7 +862,7 @@ function artifactRow(slug, a) {
841
862
  }, '+ ', iv(ls.l))),
842
863
  );
843
864
 
844
- return el('div', { style: 'min-width:0' }, head, linkRow, openHere ? destForm(openHere, '') : null);
865
+ return el('div', { style: 'min-width:0' }, noHead ? null : head, linkRow, openHere ? destForm(openHere, '') : null);
845
866
  }
846
867
 
847
868
  // Linked: the destination pill, the sync button, the PM's own controls, and the status line —
@@ -909,7 +930,7 @@ function artifactRow(slug, a) {
909
930
  panel = realSyncPanel(slug, a, key, dest);
910
931
  }
911
932
 
912
- return el('div', { style: 'min-width:0' }, head, pillRow, openHere ? destForm(openHere, dest.target) : null, panel);
933
+ return el('div', { style: 'min-width:0' }, noHead ? null : head, pillRow, openHere ? destForm(openHere, dest.target) : null, panel);
913
934
  }
914
935
 
915
936
  /**
@@ -1139,6 +1160,98 @@ function footer(rec) {
1139
1160
 
1140
1161
  /* ── the `dInit` branch, assembled in the prototype's order ─────────────────────────────────── */
1141
1162
 
1163
+ /* ── the cockpit look: the mockup's title block and phase rows (planning/design/cockpit-mockup/WsA.dc.html) ────────────────── */
1164
+
1165
+ 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';
1166
+ const CK_SEP = () => el('div', { style: 'height:1px;background:var(--border)' });
1167
+
1168
+ /** Title, summary and chips: the lane, the anchor and the flow, as pills. */
1169
+ function cockpitTitle(name, tag, pills) {
1170
+ return [
1171
+ el('div', { style: 'font:600 17px var(--sans);overflow-wrap:anywhere' }, iv(name)),
1172
+ el('div', { style: 'color:var(--muted-foreground)' }, iv(tag)),
1173
+ el('div', { style: 'display:flex;gap:6px;flex-wrap:wrap' }, pills.map((pl) => el('span', { style: CK_BADGE }, iv(pl.t)))),
1174
+ ];
1175
+ }
1176
+
1177
+ const CK_PH_DOT = { done: 'var(--success)', blocked: 'var(--destructive)', doing: 'var(--info)', todo: 'var(--muted-foreground)' };
1178
+
1179
+ /** The phases as the mockup draws them: a row per phase with its count, its tasks beneath while it is open. */
1180
+ function phaseBlockCockpit(slug, rec) {
1181
+ const phases = FX.phasesFor(rec, slug);
1182
+ if (!phases.length) return null;
1183
+ const collP = state.collapsedPhases || {};
1184
+ const repo = repoOf(slug);
1185
+ /* Run 4 (the mockup): only the phase the initiative is at opens by itself — the same pointer the breadcrumb uses (the first
1186
+ * 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
1187
+ * opened or folded keeps that choice (`collapsedPhases` holds true or false for it). */
1188
+ const stepAt = Array.isArray(rec.steps) ? FX.targetStepIndex(rec.steps) : -1;
1189
+ const atKey = stepAt >= 0 && rec.steps[stepAt] ? rec.steps[stepAt].k : null;
1190
+ const anyTaskTab = (state.tabs || []).some((t) => t.id === state.active && t.kind === 'task' && t.slug === slug);
1191
+ return el('div', { 'data-cockpit': 'phases', style: 'display:flex;flex-direction:column;gap:2px' }, phases.map((ph) => {
1192
+ const pkey = slug + '|' + ph.key;
1193
+ const open = Object.prototype.hasOwnProperty.call(collP, pkey) ? !collP[pkey] : ph.key === atKey;
1194
+ // the task the initiative is at: the first blocked (else in-progress) task of that phase, marked while no task tab is in front
1195
+ const atTask = ph.key === atKey && !anyTaskTab ? ph.tasks.findIndex((t) => t.status === 'blocked') : -1;
1196
+ const atIdx = atTask >= 0 ? atTask : (ph.key === atKey && !anyTaskTab ? ph.tasks.findIndex((t) => t.status === 'doing') : -1);
1197
+ const head = el('button', {
1198
+ onClick: () => setState((s) => ({ collapsedPhases: { ...s.collapsedPhases, [pkey]: open } })),
1199
+ 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%',
1200
+ },
1201
+ el('span', null, open ? '▾' : '▸'),
1202
+ 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) }),
1203
+ ph.label.charAt(0).toUpperCase() + ph.label.slice(1),
1204
+ el('span', { style: 'margin-left:auto;color:var(--muted-foreground)' }, iv(ph.count)),
1205
+ );
1206
+ const rows = open ? ph.tasks.map((t, ti) => {
1207
+ const tid = FX.taskTabId(slug, ph.key, ti);
1208
+ const active = state.active === tid || ti === atIdx;
1209
+ const [glyph, color] = FX.TDOT[t.status] || FX.TDOT.todo;
1210
+ return el('button', {
1211
+ onClick: () => openTask({
1212
+ id: tid, kind: 'task', role: 'TASK', title: t.title, slug, repo, phase: ph.key,
1213
+ status: t.status, instruction: t.instruction, input: t.input, expect: t.expect,
1214
+ evalNote: t.evalNote, output: t.output,
1215
+ }),
1216
+ 'aria-current': ti === atIdx ? 'step' : null,
1217
+ 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'),
1218
+ hover: 'background:var(--accent)',
1219
+ },
1220
+ el('span', { style: 'color:' + color }, glyph),
1221
+ el('span', { style: 'flex-grow:1' }, iv(t.title)),
1222
+ el('span', { style: 'color:var(--muted-foreground)' }, '↗'),
1223
+ );
1224
+ }) : [];
1225
+ return [head, rows];
1226
+ }));
1227
+ }
1228
+
1229
+ /** The blocker card (workspace-cockpit-nav): drawn only when a rung of the ladder is blocking, in the engine's own words. The cockpit
1230
+ * 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
1231
+ * grants ledger behind it exists (PRD Q3) — the slot keeps the card's height, so nothing moves when the button arrives. */
1232
+ function blockerCard(b) {
1233
+ if (cockpitLook()) {
1234
+ const said = el('span', { style: 'color:var(--destructive)' }, iv(b.label + ' blocked' + (b.evidence ? ': ' + b.evidence : '')));
1235
+ return el('div', {
1236
+ 'data-cockpit': 'blocker',
1237
+ 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',
1238
+ },
1239
+ b.next
1240
+ ? el('div', { style: 'display:flex;flex-direction:column;gap:4px;min-width:0' }, said,
1241
+ el('span', { style: 'color:var(--muted-foreground);font-size:12px;line-height:1.5' }, iv('next: ' + b.next)))
1242
+ : said,
1243
+ el('span', { 'data-cockpit': 'grant-slot', 'aria-hidden': 'true', style: 'flex:none;width:0;height:30px' }),
1244
+ );
1245
+ }
1246
+ return el('div', {
1247
+ 'data-cockpit': 'blocker',
1248
+ 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',
1249
+ },
1250
+ el('span', { style: 'color:var(--destructive);font-size:13px' }, iv(b.label + ' blocked' + (b.evidence ? ': ' + b.evidence : ''))),
1251
+ b.next ? el('span', { style: 'color:var(--muted-foreground);font-size:12px;line-height:1.5' }, iv('next: ' + b.next)) : null,
1252
+ );
1253
+ }
1254
+
1142
1255
  function initBody(slug) {
1143
1256
  const rec = slug ? records()[slug] : null;
1144
1257
  if (!rec) return [noneBody()];
@@ -1153,7 +1266,12 @@ function initBody(slug) {
1153
1266
  return out;
1154
1267
  }
1155
1268
 
1156
- out.push(headerBlock(slug, rec.tag || '', rec.pills || [], rec.crumb || null, lanePicker(slug, rec)));
1269
+ const ck = cockpitLook();
1270
+ if (ck) return cockpitInitBody(slug, rec);
1271
+ {
1272
+ out.push(headerBlock(slug, rec.tag || '', rec.pills || [], rec.crumb || null, lanePicker(slug, rec)));
1273
+ if (rec.blocker) out.push(el('div', { style: 'margin:12px 14px 0' }, blockerCard(rec.blocker)));
1274
+ }
1157
1275
  { const sb = statusBanner(repoOf(slug), slug, rec); if (sb) out.push(sb); }
1158
1276
  { const lc = lifecycleRow(repoOf(slug), slug); if (lc) { out.push(lc); const ln = lifecycleNotes(rec); if (ln) out.push(ln); } }
1159
1277
 
@@ -1170,8 +1288,9 @@ function initBody(slug) {
1170
1288
  const nextB = nextStepsBlock(slug, rec);
1171
1289
  if (nextB) out.push(nextB);
1172
1290
 
1173
- const phases = phaseBlock(slug, rec);
1174
- if (phases) out.push(phases);
1291
+ if (!ck) { const phases = phaseBlock(slug, rec); if (phases) out.push(phases); }
1292
+
1293
+ { const fb = factsBlock(rec); if (fb) out.push(fb); }
1175
1294
 
1176
1295
  out.push(el('div', { style: 'border-top:1px solid var(--border)' },
1177
1296
  linksRow(slug),
@@ -1182,6 +1301,70 @@ function initBody(slug) {
1182
1301
  return out;
1183
1302
  }
1184
1303
 
1304
+ /* ── the cockpit's initiative (Run 4: the mockup's column, section for section) ─────────────────────────────────────────────
1305
+ * The detail host is the mockup's column: 16px padding and a 12px gap between its sections (renderDetail sets that). In order:
1306
+ * title, summary, chips, the blocker card, a rule, the phases, then what the mockup does not draw (the anchor line, the lane picker,
1307
+ * the status, the sessions, the next steps, the facts), a rule, the artifacts, the links and the derivation line. A section with
1308
+ * 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
1309
+ * column, so they keep it (`FULL`). */
1310
+ const FULL = 'margin:0 -16px';
1311
+ function cockpitInitBody(slug, rec) {
1312
+ const ph = phaseBlockCockpit(slug, rec);
1313
+ const out = [...cockpitTitle(slug, rec.tag || '', rec.pills || [])];
1314
+ if (rec.blocker) out.push(blockerCard(rec.blocker));
1315
+ if (ph) out.push(CK_SEP(), ph);
1316
+ const more = [crumbBlock(rec.crumb || null), lanePicker(slug, rec)].filter(Boolean);
1317
+ if (more.length) out.push(el('div', null, more));
1318
+ const wide = [];
1319
+ { const sb = statusBanner(repoOf(slug), slug, rec); if (sb) wide.push(sb); }
1320
+ { const lc = lifecycleRow(repoOf(slug), slug); if (lc) { wide.push(lc); const ln = lifecycleNotes(rec); if (ln) wide.push(ln); } }
1321
+ if (rec.noLadder) {
1322
+ const ansC = FX.TONE[(rec.answer && rec.answer.tone) || 'info'] || FX.C.mut;
1323
+ wide.push(...noLadderBlock(FX.wash(ansC), ansC, (rec.answer && rec.answer.text) || '', rec.noLadder));
1324
+ }
1325
+ { const sess = sessionsBlock(slug); if (sess) wide.push(sess); }
1326
+ { const nextB = nextStepsBlock(slug, rec); if (nextB) wide.push(nextB); }
1327
+ { const fb = factsBlock(rec); if (fb) wide.push(fb); }
1328
+ if (wide.length) out.push(el('div', { style: FULL }, wide));
1329
+ out.push(CK_SEP(), cockpitArtifacts(slug, rec));
1330
+ // The links row's + controls record a reference through the server; the fixture has no server and no links, so it has none.
1331
+ const links = (rec.links || []).length || ((state.initLinks || {})[slug] || []).length || !FIXTURE ? linksRow(slug) : null;
1332
+ if (links) out.push(el('div', { style: FULL + ';border-top:1px solid var(--border)' }, links));
1333
+ // The derivation line says which working tree the panel was read from and when. The fixture read none, so it has no line.
1334
+ if (!FIXTURE) out.push(el('div', { style: 'margin:auto -16px -16px' }, footer(rec)));
1335
+ return out;
1336
+ }
1337
+
1338
+ /** The mockup's artifacts: a label, then a row per artifact — its role, its path — with the artifact's link and sync controls
1339
+ * under the row (`data-art-links`). The mockup shows a sync badge in the row ("⇄ in sync"); the app never claims that, because it
1340
+ * has never compared the two documents (see SYNC_NOT_A_SYNC), so the row says what is recorded through the controls instead. */
1341
+ const CK_LBL = 'font:500 10px var(--mono);letter-spacing:.1em;text-transform:uppercase;color:var(--muted-foreground)';
1342
+ function cockpitArtifacts(slug, rec) {
1343
+ const arts = rec.arts || [];
1344
+ const rows = [];
1345
+ for (const a of arts) {
1346
+ rows.push(el('button', {
1347
+ onClick: () => openDoc(slug, a.path, a.role), 'data-art-row': a.path, title: a.path,
1348
+ style: 'display:flex;align-items:center;gap:8px;width:100%;text-align:left', hover: 'background:var(--accent)',
1349
+ },
1350
+ el('span', { style: CK_BADGE + ';font-family:var(--mono)' }, iv(roleChip(a.role))),
1351
+ el('span', { 'data-art-path': '', style: 'flex-grow:1;min-width:0;color:var(--muted-foreground);font:11px var(--mono)' }, iv(shortPath(a.path))),
1352
+ ));
1353
+ rows.push(el('div', { 'data-art-links': a.path }, artifactRow(slug, a, true)));
1354
+ }
1355
+ if (!arts.length) rows.push(el('span', { style: 'font-size:11.5px;color:var(--muted-foreground);font-style:italic' }, 'none filed'));
1356
+ const conv = conversationsBlock(slug, rec), git = gitFilesBlock(slug, rec);
1357
+ return el('div', { 'data-cockpit': 'artifacts', style: 'display:flex;flex-direction:column;gap:6px' },
1358
+ el('div', { style: CK_LBL }, 'Artifacts'), rows, conv, git);
1359
+ }
1360
+
1361
+ /** 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
1362
+ * the end of the name both stay readable ("planning/prd/ooh-weather…-prd.md"). The row's tooltip carries the whole path. */
1363
+ export function shortPath(p) {
1364
+ const s0 = String(p || '');
1365
+ return s0.length <= 32 ? s0 : s0.slice(0, 24) + '…' + s0.slice(-7);
1366
+ }
1367
+
1185
1368
  /* ── `dNone` (lines 838–840) ────────────────────────────────────────────────────────────────── */
1186
1369
 
1187
1370
  function noneBody() {
@@ -1439,7 +1622,26 @@ export function renderDetail(host) {
1439
1622
  else if (kind === 'none') body = [noneBody()];
1440
1623
  else body = modeDetailBody(kind);
1441
1624
 
1442
- return setChildren(host, [stickyHeader, ...body]);
1625
+ // 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
1626
+ const bare = cockpitLook() && kind === 'init';
1627
+ /* 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
1628
+ * gap between sections. The legacy shell, a phone and Focus keep the panel's sidebar fill and draw their own padding. */
1629
+ const ckCol = cockpitLook() && !state.layoutFocus;
1630
+ host.style.background = ckCol ? 'transparent' : 'var(--sidebar)';
1631
+ host.style.padding = ckCol && bare ? '16px' : '';
1632
+ host.style.gap = ckCol && bare ? '12px' : '';
1633
+ if (bare && state.layoutFocus) {
1634
+ // 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
1635
+ const slug = selectedSlug();
1636
+ const rows = (state.statuses && state.statuses[state.product]) || {};
1637
+ const key = window.PMOSStatusGroups ? window.PMOSStatusGroups.keyOf(rows[slug]) : 'unknown';
1638
+ const ask = key === 'needs-you' ? 'Needs you' : key === 'awaiting-acceptance' ? 'Awaiting your acceptance' : 'Nothing is asked of you';
1639
+ 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)' },
1640
+ el('span', { style: 'font:500 12px var(--sans);min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap' }, ask),
1641
+ 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'));
1642
+ return setChildren(host, [bar, ...body]);
1643
+ }
1644
+ return setChildren(host, bare ? body : [stickyHeader, ...body]);
1443
1645
  }
1444
1646
 
1445
1647
  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',