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.
- package/package.json +1 -1
- package/payload/AGENTS-HUMAN.md +1 -1
- package/payload/AGENTS.md +1 -1
- package/payload/KIT-VERSION +2 -2
- package/payload/mods/pmos-cockpit/.claude-plugin/plugin.json +1 -1
- package/payload/mods/pmos-cockpit/README.md +14 -3
- package/payload/mods/pmos-cockpit/hooks/lib/commands.mjs +34 -6
- package/payload/mods/pmos-cockpit/hooks/lib/grants.mjs +22 -3
- package/payload/mods/pmos-cockpit/hooks/lib/guard.mjs +16 -3
- package/payload/mods/pmos-cockpit/hooks/lib/route.mjs +95 -0
- package/payload/mods/pmos-cockpit/hooks/register.tsx +104 -19
- package/payload/mods/pmos-cockpit/types/index.d.ts +1 -1
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/scripts/pmos-log.sh +18 -8
- package/payload/scripts/pmos-model.js +233 -2
- package/payload/scripts/status-facts.js +214 -0
- package/payload/scripts/workflow-state.js +59 -2
- package/payload/state/discoveries.md +1 -1
- package/payload/state/log.md +1 -1
- package/workspace/README.md +2 -2
- package/workspace/bin/live-notes.js +133 -0
- package/workspace/design-parity.js +328 -21
- package/workspace/doc-category-bench.js +165 -0
- package/workspace/fixtures/doc-category-labelled.json +167 -0
- package/workspace/guide/index.md +2 -1
- package/workspace/guide/initiatives-and-next-steps.md +17 -2
- package/workspace/guide/knowledge-graph.md +94 -1
- package/workspace/guide/layout.md +31 -2
- package/workspace/guide/live-notes.md +78 -0
- package/workspace/guide/proactive-engine.md +6 -2
- package/workspace/guide/validate-with-typesafe.md +31 -2
- package/workspace/lib/claims.js +51 -2
- package/workspace/lib/doc-category-corrections.js +113 -0
- package/workspace/lib/doc-category-writeback.js +110 -0
- package/workspace/lib/doc-category.js +225 -0
- package/workspace/lib/engine/pmos-model.js +233 -2
- package/workspace/lib/engine/status-facts.js +214 -0
- package/workspace/lib/engine/workflow-state.js +59 -2
- package/workspace/lib/jev-categorize.js +433 -0
- package/workspace/lib/jev-claims.js +59 -15
- package/workspace/lib/jev-live.js +37 -0
- package/workspace/lib/jev-route.js +139 -0
- package/workspace/lib/kg-cli.js +3 -1
- package/workspace/lib/kg.js +6 -1
- package/workspace/lib/learn.js +5 -3
- package/workspace/lib/live-notes.js +1035 -0
- package/workspace/lib/typesafe-settings.js +10 -2
- package/workspace/package-lock.json +1 -1
- package/workspace/package.json +5 -4
- package/workspace/public/pm/app.js +148 -79
- package/workspace/public/pm/data.js +78 -0
- package/workspace/public/pm/fixture.js +40 -2
- package/workspace/public/pm/modes-live.js +279 -18
- package/workspace/public/pm/panel-cockpit.js +638 -0
- package/workspace/public/pm/panel-detail.js +212 -8
- package/workspace/public/pm/panel-initiatives.js +9 -9
- package/workspace/public/pm/panel-newsess.js +145 -86
- package/workspace/public/pm/panel-repos.js +34 -17
- package/workspace/public/pm/panel-tabs.js +249 -29
- package/workspace/public/pm/state.js +48 -4
- package/workspace/public/pm/tab-doc.js +244 -16
- package/workspace/public/pm/tab-term.js +11 -1
- package/workspace/public/pm/tokens.css +4 -3
- package/workspace/public/pm/view-addrepo.js +207 -0
- package/workspace/public/pm/view-files.js +197 -0
- package/workspace/public/pm/view-guide.js +141 -0
- package/workspace/public/pm/view-knowledge.js +225 -0
- package/workspace/public/pm/view-livenotes.js +397 -0
- package/workspace/public/pm/view-memory.js +122 -0
- package/workspace/public/pm/view-portfolio.js +153 -0
- package/workspace/public/pm/view-reports.js +190 -0
- package/workspace/public/pm/view-scheduled.js +161 -0
- package/workspace/public/pm/view-skills.js +216 -0
- package/workspace/public/pm/view-today.js +168 -0
- package/workspace/public/pm/view-validate.js +171 -0
- package/workspace/server.js +349 -7
- package/workspace/skills/live-notes.skill +58 -0
|
@@ -97,16 +97,24 @@ function getRepo(stateDir, root) {
|
|
|
97
97
|
// jev-claim-checks (D80): also ask Jev about changed claim pairs after each check. Default OFF, separate from
|
|
98
98
|
// `consent` and `watch`; a request needs all of key, consent, watch and this.
|
|
99
99
|
jev: !!(r && r.jev === true),
|
|
100
|
+
// jev-doc-categorizer (D82): ask Jev to categorize documents the local rules leave undetermined. Default OFF, separate
|
|
101
|
+
// from every other switch; a request needs key, consent and this.
|
|
102
|
+
categorize: !!(r && r.categorize === true),
|
|
103
|
+
// status-from-artifacts Run 2 (D83): also send the documents tied to an initiative for two status questions
|
|
104
|
+
// (is it a stub; does a tied note contradict a requirement). Default OFF, and only read when `jev` is on too.
|
|
105
|
+
status_read: !!(r && r.status_read === true),
|
|
106
|
+
// prompt-model-routing: classify prompts with Jev to choose a model tier. Default OFF; a request needs key, consent and this.
|
|
107
|
+
route: !!(r && r.route === true),
|
|
100
108
|
};
|
|
101
109
|
}
|
|
102
110
|
|
|
103
|
-
function setRepo(stateDir, root, { consent, exclude, watch, jev }) {
|
|
111
|
+
function setRepo(stateDir, root, { consent, exclude, watch, jev, categorize, route, status_read: statusRead }) {
|
|
104
112
|
const cur = read(stateDir);
|
|
105
113
|
const prev = getRepo(stateDir, root);
|
|
106
114
|
const ex = Array.isArray(exclude)
|
|
107
115
|
? exclude.map((x) => String(x).trim().replace(/^\/+/, '')).filter((x) => x && !x.includes('..') && x.length <= 200).slice(0, MAX_EXCLUDES)
|
|
108
116
|
: prev.exclude;
|
|
109
|
-
const repos = { ...cur.repos, [root]: { consent: typeof consent === 'boolean' ? consent : prev.consent, exclude: ex, watch: typeof watch === 'boolean' ? watch : prev.watch, jev: typeof jev === 'boolean' ? jev : prev.jev } };
|
|
117
|
+
const repos = { ...cur.repos, [root]: { consent: typeof consent === 'boolean' ? consent : prev.consent, exclude: ex, watch: typeof watch === 'boolean' ? watch : prev.watch, jev: typeof jev === 'boolean' ? jev : prev.jev, categorize: typeof categorize === 'boolean' ? categorize : prev.categorize, route: typeof route === 'boolean' ? route : prev.route, status_read: typeof statusRead === 'boolean' ? statusRead : prev.status_read } };
|
|
110
118
|
return write(stateDir, { key: cur.key, repos });
|
|
111
119
|
}
|
|
112
120
|
|
package/workspace/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmos-workspace",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.49.0",
|
|
4
4
|
"private": true,
|
|
5
5
|
"description": "PMOS Terminal Workspace — local companion hosting real PTYs beside the library and gates (D62)",
|
|
6
6
|
"license": "MIT",
|
|
@@ -10,12 +10,13 @@
|
|
|
10
10
|
"scripts": {
|
|
11
11
|
"start": "node server.js",
|
|
12
12
|
"test": "node test.js && npm run test:modules",
|
|
13
|
-
"test:modules": "node proactive.test.js && node focus.test.js && node handoff.test.js && node repo-scan.test.js && node profiles.test.js && node external-linking.test.js && node story-artifacts.test.js && node no-network.test.js && node typesafe-validation.test.js && node claims.test.js && node jev-claims.test.js && node repo-watch.test.js && node fixture-parity.test.mjs && node markdown.test.mjs && node start-policy.test.js && node state-isolation.test.js && node sync.test.js && node schedule-cli.test.js && node initiative-surface.test.js && node guide.test.js && node next-step-actions.test.js && node kg.test.js && node proactive-prepare.test.js && node learn.test.js && node session-flag.test.js",
|
|
13
|
+
"test:modules": "node proactive.test.js && node focus.test.js && node handoff.test.js && node repo-scan.test.js && node profiles.test.js && node external-linking.test.js && node story-artifacts.test.js && node no-network.test.js && node typesafe-validation.test.js && node claims.test.js && node status-facts-panel.test.js && node jev-claims.test.js && node jev-categorize.test.js && node jev-status.test.js && node jev-route.test.js && node repo-watch.test.js && node fixture-parity.test.mjs && node markdown.test.mjs && node start-policy.test.js && node state-isolation.test.js && node sync.test.js && node schedule-cli.test.js && node initiative-surface.test.js && node guide.test.js && node next-step-actions.test.js && node kg.test.js && node doc-category.test.js && node doc-category-corrections.test.js && node proactive-prepare.test.js && node learn.test.js && node session-flag.test.js && node live-notes.test.js",
|
|
14
14
|
"test:ui": "node ui-test.js",
|
|
15
|
-
"test:parity": "node design-parity.js --
|
|
15
|
+
"test:parity": "node design-parity.js --board=WsA",
|
|
16
|
+
"test:parity:four-panel": "node design-parity.js --states",
|
|
16
17
|
"test:all": "npm test && npm run test:panels && npm run test:ui && npm run test:parity && npm run test:offline",
|
|
17
18
|
"doctor": "node doctor.js",
|
|
18
|
-
"test:panels": "node pm-shell-test.js && node pm-repos-test.js && node pm-initiatives-test.js && node pm-detail-test.js && node pm-tabs-test.js && node pm-term-test.js && node pm-modes-test.js && node pm-r1-test.js && node pm-r2-test.js && node pm-r3-test.js && node pm-r4-test.js && node pm-r5-test.js && node pm-r6-test.js && node pm-r7-test.js && node pm-r8-test.js && node pm-r9-test.js && node pm-r10-test.js && node pm-r11-test.js && node pm-r12-test.js && node pm-r13-test.js && node pm-r14-test.js && node pm-r15-test.js && node pm-r16-test.js && node pm-r17-test.js && node pm-r16-test.js && node pm-r18-test.js && node pm-r19-test.js && node pm-r20-test.js && node pm-r21-test.js && node pm-r22-test.js && node pm-r23-test.js && node pm-r24-test.js && node pm-r25-test.js && node pm-r26-test.js",
|
|
19
|
+
"test:panels": "node pm-shell-test.js && node pm-repos-test.js && node pm-initiatives-test.js && node pm-detail-test.js && node pm-tabs-test.js && node pm-term-test.js && node pm-modes-test.js && node pm-r1-test.js && node pm-r2-test.js && node pm-r3-test.js && node pm-r4-test.js && node pm-r5-test.js && node pm-r6-test.js && node pm-r7-test.js && node pm-r8-test.js && node pm-r9-test.js && node pm-r10-test.js && node pm-r11-test.js && node pm-r12-test.js && node pm-r13-test.js && node pm-r14-test.js && node pm-r15-test.js && node pm-r16-test.js && node pm-r17-test.js && node pm-r16-test.js && node pm-r18-test.js && node pm-r19-test.js && node pm-r20-test.js && node pm-r21-test.js && node pm-r22-test.js && node pm-r23-test.js && node pm-r24-test.js && node pm-r25-test.js && node pm-r26-test.js && node pm-doc-category-test.js && node pm-r27-test.js && node pm-r28-test.js && node pm-r29-today-test.js && node pm-r29-portfolio-test.js && node pm-r29-knowledge-test.js && node pm-r29-memory-test.js && node pm-r29-skills-scheduled-test.js && node pm-r29-validate-guide-test.js && node pm-r29-reports-addrepo-test.js && node pm-r29-filespad-test.js && node pm-r29-newsession-alignment-test.js && node pm-live-notes-test.js && node pm-r30-test.js",
|
|
19
20
|
"test:offline": "node offline-tarball.test.js"
|
|
20
21
|
},
|
|
21
22
|
"engines": {
|
|
@@ -22,12 +22,17 @@
|
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
24
|
import { el, clear, setChildren } from './dom.js';
|
|
25
|
-
import { state, setState, subscribe, flush, FIXTURE } from './state.js';
|
|
25
|
+
import { state, setState, subscribe, flush, FIXTURE, LEGACY_LAYOUT_FLAG } from './state.js';
|
|
26
26
|
/* The phone surface (≤760px). It owns its own chrome and decides which single panel is visible;
|
|
27
27
|
* the shell's only part in it is to keep all four hosts mounted below the breakpoint and to hand
|
|
28
28
|
* the module a repaint. See pm/mobile.js. */
|
|
29
29
|
import { isMobile, mountMobile, applyMobile } from './mobile.js';
|
|
30
30
|
|
|
31
|
+
/* The LEGACY layout is the three-column shell of column-layout: repositories with the list under them, the detail, the tabs, and
|
|
32
|
+
* a list that a mode (skills, memory …) replaces. A phone still draws it until Run 4 of workspace-cockpit-nav, and `?layout=legacy`
|
|
33
|
+
* draws it on a desktop so the suites of the panels it uses keep guarding them. It is not a setting: nothing offers it. */
|
|
34
|
+
const legacyLayout = () => LEGACY_LAYOUT_FLAG || isMobile();
|
|
35
|
+
|
|
31
36
|
/* ── derived labels (the prototype's two title maps, lines 1409 / 1699) ─────────────────────── */
|
|
32
37
|
|
|
33
38
|
const MODE_TITLE = {
|
|
@@ -35,30 +40,38 @@ const MODE_TITLE = {
|
|
|
35
40
|
knowledge: 'knowledge', agent: 'agents', memory: 'memory', connect: 'connections', focus: 'focus',
|
|
36
41
|
portfolio: 'portfolio', guide: 'guide', validate: 'validate', today: 'today',
|
|
37
42
|
};
|
|
43
|
+
const modeTitle = () => MODE_TITLE[state.mode] || 'initiatives';
|
|
38
44
|
const DETAIL_TITLE = {
|
|
39
45
|
init: 'initiative', flow: 'workflow', sched: 'scheduled task', skill: 'skill', know: 'decision',
|
|
40
46
|
agent: 'agent', mem: 'memory', prof: 'claude profile', intg: 'connection',
|
|
41
47
|
addprod: 'add product', new: 'file new',
|
|
42
48
|
};
|
|
43
49
|
|
|
44
|
-
const modeTitle = () => MODE_TITLE[state.mode] || 'initiatives';
|
|
45
50
|
const detailTitle = () => DETAIL_TITLE[(state.detail && state.detail.kind) || ''] || 'detail';
|
|
46
51
|
|
|
47
52
|
/* Panels 1 and 2 are resizable: a drag handle on each right edge. The width is clamped, remembered
|
|
48
53
|
* (state.js PERSIST) and reset by a double-click. Before this the columns were a fixed 210px / 280px and a
|
|
49
54
|
* long repository or branch name was simply cut off. */
|
|
50
|
-
const
|
|
55
|
+
const W_LEGACY = { min: 220, max: 560, def: 280 };
|
|
56
|
+
/* The cockpit's list is 288px by default (the mockup's) and is resized and clamped the same way. */
|
|
57
|
+
const W_COCKPIT = { min: 220, max: 560, def: 288 };
|
|
58
|
+
const W0 = { get min() { return W_LEGACY.min; }, get max() { return W_LEGACY.max; }, get def() { return legacyLayout() ? W_LEGACY.def : W_COCKPIT.def; } };
|
|
51
59
|
const clampW = (v, r) => (typeof v === 'number' && Number.isFinite(v) ? Math.min(r.max, Math.max(r.min, Math.round(v))) : r.def);
|
|
52
60
|
const col0w = () => clampW(state.col0W, W0);
|
|
53
61
|
|
|
54
|
-
/*
|
|
62
|
+
/* workspace-cockpit-nav: on a desktop the body is FOUR columns — the 64px repository rail (a 65px track: the mockup draws the rail
|
|
63
|
+
* 64px wide with its 1px border outside that width, so the list starts at x=65), the list (288px), the detail (about
|
|
64
|
+
* 470px) and the tabs (the rest) — and the hosts keep the order they had. The rail is always drawn; `col1` folds the list to a 36px
|
|
65
|
+
* strip and `col2` folds the detail. The three-column shell below is what a PHONE still draws until Run 4.
|
|
66
|
+
*
|
|
67
|
+
* THREE columns (column-layout): navigation, detail, tabs. Navigation is the repository list with the list of the
|
|
55
68
|
* current mode UNDER it (initiatives, or memory, skills, knowledge …). The detail opens in the second column, the
|
|
56
69
|
* tabs fill the rest. The DOM still holds four hosts in the same order — repos, list, detail, tabs — and the grid
|
|
57
70
|
* puts the list host under the repositories host (see placeHosts). Folded away, the navigation column is a 36px
|
|
58
71
|
* rail; the list alone folds to a one-line bar under the repositories. */
|
|
59
|
-
const gridCols = () =>
|
|
60
|
-
(state.col0 ? col0w() + 'px' : '36px') + ' ' +
|
|
61
|
-
(state.col2 ? 'minmax(420px,
|
|
72
|
+
const gridCols = (legacy, focus) => focus ? 'minmax(0,1fr) 360px' : legacy
|
|
73
|
+
? (state.col0 ? col0w() + 'px' : '36px') + ' ' + (state.col2 ? 'minmax(420px,560px)' : '36px') + ' minmax(0,1fr)'
|
|
74
|
+
: '65px ' + (state.col1 ? col0w() + 'px' : '36px') + ' ' + (state.col2 ? 'minmax(420px,470px)' : '36px') + ' minmax(0,1fr)';
|
|
62
75
|
|
|
63
76
|
/** The right-hand 7px of a column is its drag handle. It is a hit-test on the host, not an extra element:
|
|
64
77
|
* the body grid is exactly four tracks and the panels repaint their own children. `key` is the state
|
|
@@ -142,43 +155,58 @@ function runningCount() {
|
|
|
142
155
|
*/
|
|
143
156
|
const root = el('div', {
|
|
144
157
|
'data-pm-theme': state.theme,
|
|
145
|
-
style: 'display:grid;grid-template-rows:
|
|
158
|
+
style: 'display:grid;grid-template-rows:48px minmax(0,1fr) 28px;height:100vh;overflow:hidden;background:var(--background);color:var(--foreground);font-family:var(--sans);font-size:13px;line-height:1.5',
|
|
146
159
|
});
|
|
147
160
|
|
|
161
|
+
/* The legacy layout has no rail to hang the menu from, so its theme control stays where it was: in the top bar. */
|
|
148
162
|
const themeBtn = el('button', {
|
|
149
163
|
onClick: () => setState((s) => ({ theme: s.theme === 'dark' ? 'light' : 'dark' })),
|
|
150
|
-
style: '
|
|
151
|
-
hover: 'border-color:
|
|
164
|
+
style: 'font-size:12px;color:var(--foreground);padding:5px 12px;border:1px solid var(--input);min-height:30px',
|
|
165
|
+
hover: 'border-color:var(--muted-foreground)',
|
|
152
166
|
}, 'theme · dark');
|
|
167
|
+
const crumb = el('span', { 'data-top': 'crumb', style: 'display:flex;align-items:center;gap:8px;color:var(--muted-foreground);min-width:0;overflow:hidden;white-space:nowrap' });
|
|
168
|
+
const jump = el('button', {
|
|
169
|
+
onClick: () => { if (legacyLayout()) focusSearch(); else setState({ paletteOpen: true, menuOpen: false }); },
|
|
170
|
+
'data-top': 'jump', 'aria-label': 'jump to anything',
|
|
171
|
+
style: 'margin-left:auto;display:flex;align-items:center;gap:8px;box-sizing:content-box;border:1px solid var(--input);border-radius:8px;height:32px;padding:0 12px;min-width:300px;color:var(--muted-foreground);font:13px var(--sans);line-height:1.45',
|
|
172
|
+
hover: 'background:var(--accent)',
|
|
173
|
+
html: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg>Jump to anything<span style="margin-left:auto;font:11px var(--mono)">⌘K</span>',
|
|
174
|
+
});
|
|
175
|
+
const liveBadge = el('span', { style: 'font:500 11px var(--sans);border:1px solid transparent;border-radius:999px;padding:0 8px;line-height:18px;white-space:nowrap;background:color-mix(in oklab, var(--success) 16%, transparent);color:var(--success)' }, '● live · local');
|
|
153
176
|
|
|
154
177
|
const topBar = el('div', {
|
|
155
|
-
|
|
178
|
+
'data-shell': 'top',
|
|
179
|
+
// its fill is set per layout in render(): none on the cockpit (as on the mockup), --background on the legacy shell
|
|
180
|
+
style: 'display:flex;align-items:center;gap:14px;padding:0 16px;border-bottom:1px solid var(--border);color:var(--foreground);min-width:0;line-height:1.45',
|
|
156
181
|
},
|
|
157
|
-
el('span', { style: '
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
el('span', { style: 'font-size:9px' }, '●'), 'LIVE · local'),
|
|
162
|
-
themeBtn,
|
|
182
|
+
el('span', { style: 'font-weight:600;font-size:15px' }, 'PMOS'),
|
|
183
|
+
crumb,
|
|
184
|
+
jump,
|
|
185
|
+
liveBadge,
|
|
163
186
|
);
|
|
164
187
|
|
|
165
|
-
const statusLeft = el('span', null, 'local ·
|
|
188
|
+
const statusLeft = el('span', null, 'local · 0 sessions running');
|
|
166
189
|
const statusBar = el('div', {
|
|
167
|
-
|
|
190
|
+
'data-shell': 'status',
|
|
191
|
+
style: 'display:flex;align-items:center;justify-content:space-between;padding:0 14px;color:var(--muted-foreground);font:11px var(--mono);border-top:1px solid var(--border)',
|
|
168
192
|
},
|
|
169
193
|
statusLeft,
|
|
170
|
-
el('span',
|
|
194
|
+
el('span', null, '⌘K jump · Esc closes tab'),
|
|
171
195
|
);
|
|
196
|
+
/* The rail's menu is drawn into this host, a sibling of the body so it can float over all four columns. */
|
|
197
|
+
const menuHost = el('div', { 'data-shell': 'menu', style: 'display:contents' });
|
|
198
|
+
const paletteHost = el('div', { id: 'pm-palette', 'data-shell': 'palette', style: 'display:contents' });
|
|
172
199
|
|
|
173
200
|
/* The body grid's own geometry, in one place so the initial build and every repaint below cannot
|
|
174
201
|
* disagree. The implicit ROW is stated explicitly as `minmax(0,1fr)` for the same reason the root's
|
|
175
202
|
* middle track is: an auto row is floored at its items' min-content height, and one tall panel
|
|
176
203
|
* would push the grid taller than the box it was given. */
|
|
177
|
-
//
|
|
178
|
-
|
|
179
|
-
|
|
204
|
+
// A cockpit desktop is one row of four columns; the legacy layout is two rows (repositories, then the list under them) on a desktop
|
|
205
|
+
// and one on a phone, where the hosts are shown one at a time.
|
|
206
|
+
const BODY_GRID_STYLE = (legacy, phone, focus) =>
|
|
207
|
+
'display:grid;grid-template-columns:' + gridCols(legacy, focus) + ';grid-template-rows:' + (legacy && !phone ? 'auto minmax(0,1fr)' : 'minmax(0,1fr)') + ';min-height:0;overflow:hidden';
|
|
180
208
|
|
|
181
|
-
const bodyGrid = el('div', { style: BODY_GRID_STYLE(false) });
|
|
209
|
+
const bodyGrid = el('div', { 'data-shell': 'body', style: BODY_GRID_STYLE(false, false) });
|
|
182
210
|
|
|
183
211
|
/** The collapsed 36px strip: a full-height button with a rotated vertical-rl label. */
|
|
184
212
|
function makeRail(flag) {
|
|
@@ -191,9 +219,10 @@ function makeRail(flag) {
|
|
|
191
219
|
return { btn, label };
|
|
192
220
|
}
|
|
193
221
|
|
|
194
|
-
const rail0 = makeRail('col0');
|
|
195
|
-
const
|
|
196
|
-
|
|
222
|
+
const rail0 = makeRail('col0'); // the phone's folded navigation strip (legacy layout)
|
|
223
|
+
const rail1 = makeRail('col1'); // a desktop's folded list
|
|
224
|
+
const rail2 = makeRail('col2'); // a folded detail
|
|
225
|
+
/* The list under the repositories, folded: one line, not a column (legacy layout). */
|
|
197
226
|
const listBar = el('button', {
|
|
198
227
|
onClick: () => setState((s) => ({ col1: !s.col1 })),
|
|
199
228
|
title: 'show the list',
|
|
@@ -202,8 +231,7 @@ const listBar = el('button', {
|
|
|
202
231
|
});
|
|
203
232
|
rail0.label.textContent = 'repositories ⟩';
|
|
204
233
|
/* overhaul-r1 R4: in live mode the collapsed repositories rail also carries one glyph per repository
|
|
205
|
-
* (panel-repos.js#railGlyphs)
|
|
206
|
-
* toggle keeps its label and stays the rest of the strip. */
|
|
234
|
+
* (panel-repos.js#railGlyphs). Only the phone's legacy layout folds the navigation column now; a desktop's rail never folds. */
|
|
207
235
|
const rail0Host = el('div', {
|
|
208
236
|
role: 'button', tabindex: '0', title: 'show the repositories panel',
|
|
209
237
|
onClick: () => setState((s) => ({ col0: !s.col0 })),
|
|
@@ -213,27 +241,29 @@ const rail0Host = el('div', {
|
|
|
213
241
|
});
|
|
214
242
|
|
|
215
243
|
/* `min-height:0` on all four: a grid item's automatic minimum is its content, so a panel taller
|
|
216
|
-
* than the viewport would otherwise stretch the row it sits in rather than scroll inside it.
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
244
|
+
* than the viewport would otherwise stretch the row it sits in rather than scroll inside it. */
|
|
245
|
+
const HOST_STYLE = {
|
|
246
|
+
repos: 'background:var(--sidebar);border-right:1px solid var(--border);display:flex;flex-direction:column;min-width:0;min-height:0;overflow-y:auto',
|
|
247
|
+
inits: 'background:var(--sidebar);border-right:1px solid var(--border);overflow-y:auto;min-width:0;min-height:0',
|
|
248
|
+
initsDesk: 'background:var(--sidebar);border-right:1px solid var(--border);display:flex;flex-direction:column;gap:12px;padding:12px;box-sizing:border-box;overflow:hidden;min-width:0;min-height:0',
|
|
249
|
+
};
|
|
250
|
+
const hostRepos = el('div', { id: 'pm-rail', style: HOST_STYLE.repos });
|
|
251
|
+
const hostInits = el('div', { id: 'pm-list', style: HOST_STYLE.initsDesk });
|
|
222
252
|
makeEdgeDrag(hostInits, 'col0W', W0, col0w);
|
|
223
|
-
const hostDetail = el('div', { style: 'background:var(--sidebar);border-right:1px solid var(--border);overflow-y:auto;min-width:0;min-height:0;display:flex;flex-direction:column' });
|
|
224
|
-
const hostTabs = el('div', { style: 'background:var(--term-bg);display:flex;flex-direction:column;min-width:0;min-height:0;overflow:hidden' });
|
|
253
|
+
const hostDetail = el('div', { id: 'pm-detail', style: 'background:var(--sidebar);border-right:1px solid var(--border);overflow-y:auto;min-width:0;min-height:0;display:flex;flex-direction:column' });
|
|
254
|
+
const hostTabs = el('div', { id: 'pm-tabs', style: 'background:var(--term-bg);display:flex;flex-direction:column;min-width:0;min-height:0;overflow:hidden' });
|
|
225
255
|
|
|
226
|
-
setChildren(root, [topBar, bodyGrid, statusBar]);
|
|
256
|
+
setChildren(root, [topBar, bodyGrid, statusBar, menuHost, paletteHost]);
|
|
227
257
|
|
|
228
258
|
/* ── panel modules, loaded at boot and tolerated when absent ────────────────────────────────── */
|
|
229
259
|
|
|
230
|
-
const mods = { repos: null, inits: null, detail: null, tabs: null, data: null, fixture: null };
|
|
260
|
+
const mods = { repos: null, inits: null, detail: null, tabs: null, data: null, fixture: null, cockpit: null };
|
|
231
261
|
|
|
232
262
|
async function loadModules() {
|
|
233
263
|
const want = [
|
|
234
264
|
['repos', './panel-repos.js'], ['inits', './panel-initiatives.js'],
|
|
235
265
|
['detail', './panel-detail.js'], ['tabs', './panel-tabs.js'],
|
|
236
|
-
['data', './data.js'], ['fixture', './fixture.js'],
|
|
266
|
+
['data', './data.js'], ['fixture', './fixture.js'], ['cockpit', './panel-cockpit.js'],
|
|
237
267
|
];
|
|
238
268
|
await Promise.all(want.map(async ([key, path]) => {
|
|
239
269
|
try { mods[key] = await import(path); } catch { mods[key] = null; }
|
|
@@ -249,10 +279,7 @@ function paint(mod, name, host) {
|
|
|
249
279
|
|
|
250
280
|
/* ── render ────────────────────────────────────────────────────────────────────────────────── */
|
|
251
281
|
|
|
252
|
-
/* The
|
|
253
|
-
* label, and its text is still exactly `repositories ⟩` — the glyphs draw their initials from a data
|
|
254
|
-
* attribute (tokens.css) and name themselves through aria-label, and CSS `order` lifts them above
|
|
255
|
-
* the label. A glyph click stops at the glyph: it selects, it does not expand. */
|
|
282
|
+
/* The phone's folded navigation strip (legacy layout): one control that re-opens the panel, its first child the rotated label. */
|
|
256
283
|
function railRepos() {
|
|
257
284
|
const glyphs = mods.repos && typeof mods.repos.railGlyphs === 'function' ? mods.repos.railGlyphs() : [];
|
|
258
285
|
if (!glyphs.length) {
|
|
@@ -265,15 +292,15 @@ function railRepos() {
|
|
|
265
292
|
return rail0Host;
|
|
266
293
|
}
|
|
267
294
|
|
|
268
|
-
/* Where each host sits in the grid.
|
|
269
|
-
*
|
|
270
|
-
*
|
|
271
|
-
*
|
|
272
|
-
function placeHosts(phone, kids) {
|
|
295
|
+
/* Where each host sits in the LEGACY grid. Column 1 holds the repositories (row 1) and the list under them (row 2); the detail and the
|
|
296
|
+
* tabs span both rows. The repositories block is capped so a long list of repositories cannot push the list off the screen. A folded
|
|
297
|
+
* column is a rail spanning both rows. On a phone the placement is cleared and mobile.css decides which single host is shown. On a
|
|
298
|
+
* cockpit desktop every host has its own column and nothing is placed. */
|
|
299
|
+
function placeHosts(legacy, phone, kids) {
|
|
273
300
|
const cell = (el0, col, row) => { if (el0) { el0.style.gridColumn = col; el0.style.gridRow = row; } };
|
|
274
301
|
const clear0 = (el0) => { el0.style.gridColumn = ''; el0.style.gridRow = ''; el0.style.maxHeight = ''; };
|
|
275
|
-
for (const h of [hostRepos, hostInits, hostDetail, hostTabs, listBar, rail0.btn, rail0Host, rail2.btn]) clear0(h);
|
|
276
|
-
if (phone) return;
|
|
302
|
+
for (const h of [hostRepos, hostInits, hostDetail, hostTabs, listBar, rail0.btn, rail0Host, rail1.btn, rail2.btn]) clear0(h);
|
|
303
|
+
if (!legacy || phone) return;
|
|
277
304
|
if (state.col0) {
|
|
278
305
|
cell(hostRepos, '1', '1');
|
|
279
306
|
hostRepos.style.maxHeight = 'calc((100vh - 70px) * .42)';
|
|
@@ -286,44 +313,76 @@ function placeHosts(phone, kids) {
|
|
|
286
313
|
cell(hostTabs, '3', '1 / 3');
|
|
287
314
|
}
|
|
288
315
|
|
|
316
|
+
let lastLegacy = null;
|
|
289
317
|
function render() {
|
|
290
318
|
root.setAttribute('data-pm-theme', state.theme);
|
|
291
|
-
|
|
319
|
+
const phone = isMobile();
|
|
320
|
+
const legacy = legacyLayout();
|
|
321
|
+
// the mockup's body line-height is 1.45; the legacy shell's panels are drawn for 1.5
|
|
322
|
+
root.style.lineHeight = legacy ? '1.5' : '1.45';
|
|
323
|
+
// the cockpit's bars are the mockup's 48px and 28px with their 1px rule outside that height (as its rail is 64px plus its rule);
|
|
324
|
+
// the legacy shell keeps 48px and 28px rule included. A phone's stylesheet replaces the rows altogether.
|
|
325
|
+
root.style.gridTemplateRows = legacy ? '48px minmax(0,1fr) 28px' : '49px minmax(0,1fr) 29px';
|
|
326
|
+
// the cockpit's bars have no fill of their own (the root's --background shows through, as on the mockup); the legacy shell's do
|
|
327
|
+
topBar.style.background = legacy ? 'var(--background)' : '';
|
|
328
|
+
statusBar.style.background = legacy ? 'var(--background)' : '';
|
|
329
|
+
// A cockpit desktop never shows a list mode in the list: it opens as a view tab (panel-cockpit.js)
|
|
330
|
+
if (!legacy && mods.cockpit && typeof mods.cockpit.absorbMode === 'function' && mods.cockpit.absorbMode()) return;
|
|
331
|
+
|
|
292
332
|
// overhaul-r1 R2.3: the active tab's line of work, when it is not the main tree
|
|
293
333
|
const actTab = (state.tabs || []).find((t) => t.id === state.active);
|
|
294
334
|
const br = actTab && mods.tabs && typeof mods.tabs.tabBranch === 'function' ? mods.tabs.tabBranch(actTab, true) : null;
|
|
295
|
-
statusLeft.textContent = 'local ·
|
|
335
|
+
statusLeft.textContent = 'local · ' + runningCount() + ' sessions running' + (br ? ' · ⎇ ' + br : '');
|
|
336
|
+
const parts = mods.cockpit && typeof mods.cockpit.breadcrumb === 'function' ? mods.cockpit.breadcrumb() : [];
|
|
337
|
+
setChildren(crumb, parts);
|
|
338
|
+
themeBtn.textContent = state.theme === 'dark' ? 'theme · dark' : 'theme · light';
|
|
339
|
+
if (legacy && themeBtn.parentNode !== topBar) topBar.appendChild(themeBtn);
|
|
340
|
+
if (!legacy && themeBtn.parentNode === topBar) topBar.removeChild(themeBtn);
|
|
296
341
|
|
|
297
|
-
//
|
|
298
|
-
// a panel away can still see what they folded.
|
|
342
|
+
// Folded strips name whatever is behind them, so a PM who folded a panel away can still see what they folded.
|
|
299
343
|
listBar.textContent = '▸ ' + modeTitle();
|
|
344
|
+
rail1.label.textContent = 'initiatives ⟩';
|
|
300
345
|
rail2.label.textContent = detailTitle() + ' ⟩';
|
|
301
346
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
const
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
347
|
+
if (lastLegacy !== legacy) {
|
|
348
|
+
lastLegacy = legacy;
|
|
349
|
+
hostInits.setAttribute('style', legacy ? HOST_STYLE.inits + ';border-top:1px solid var(--border)' : HOST_STYLE.initsDesk);
|
|
350
|
+
// the cockpit's rail is the mockup's centred column; the legacy repositories panel draws its own rows edge to edge
|
|
351
|
+
hostRepos.style.alignItems = legacy ? '' : 'center';
|
|
352
|
+
hostRepos.style.gap = legacy ? '' : '10px';
|
|
353
|
+
hostRepos.style.padding = legacy ? '' : '12px 0';
|
|
354
|
+
}
|
|
355
|
+
const focus = !legacy && !phone && !!state.layoutFocus;
|
|
356
|
+
bodyGrid.setAttribute('style', BODY_GRID_STYLE(legacy, phone, focus));
|
|
357
|
+
|
|
358
|
+
// On a phone there are no rails to collapse into and no second column to fold away: the four hosts all stay mounted, one is
|
|
359
|
+
// DISPLAYED (pm/mobile.css picks it from `data-mpanel`), and the PM's desktop arrangement is left exactly as they set it. Mounting
|
|
360
|
+
// all four keeps a live terminal alive: a host dropped from this list would be removed from the document.
|
|
361
|
+
let kids;
|
|
362
|
+
if (phone) kids = [hostRepos, hostInits, hostDetail, hostTabs];
|
|
363
|
+
else if (legacy) {
|
|
364
|
+
const listHost = state.col1 ? hostInits : listBar;
|
|
365
|
+
kids = [state.col0 ? hostRepos : railRepos(), state.col0 ? listHost : null, state.col2 ? hostDetail : rail2.btn, hostTabs].filter(Boolean);
|
|
366
|
+
} else if (focus) kids = [hostDetail, hostTabs]; // Focus: the tabs and a 360px detail; hostTabs is never moved, only the others come and go
|
|
367
|
+
else kids = [hostRepos, state.col1 ? hostInits : rail1.btn, state.col2 ? hostDetail : rail2.btn, hostTabs];
|
|
319
368
|
setChildren(bodyGrid, kids);
|
|
320
|
-
placeHosts(phone, kids);
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
if (
|
|
324
|
-
|
|
369
|
+
placeHosts(legacy, phone, kids);
|
|
370
|
+
if (focus) { hostTabs.style.gridColumn = '1'; hostTabs.style.gridRow = '1'; hostDetail.style.gridColumn = '2'; hostDetail.style.gridRow = '1'; }
|
|
371
|
+
|
|
372
|
+
if (legacy) {
|
|
373
|
+
if (phone || state.col0) paint(mods.repos, 'renderRepos', hostRepos);
|
|
374
|
+
if (phone || (state.col0 && state.col1)) paint(mods.inits, 'renderInitiatives', hostInits);
|
|
375
|
+
if (phone || state.col2) paint(mods.detail, 'renderDetail', hostDetail);
|
|
376
|
+
} else if (focus) {
|
|
377
|
+
paint(mods.detail, 'renderDetail', hostDetail);
|
|
378
|
+
} else {
|
|
379
|
+
paint(mods.cockpit, 'renderRail', hostRepos);
|
|
380
|
+
if (state.col1) paint(mods.cockpit, 'renderWorkList', hostInits);
|
|
381
|
+
if (state.col2) paint(mods.detail, 'renderDetail', hostDetail);
|
|
382
|
+
}
|
|
325
383
|
paint(mods.tabs, 'renderTabs', hostTabs);
|
|
326
|
-
|
|
384
|
+
paint(mods.cockpit, 'renderMenu', menuHost);
|
|
385
|
+
paint(mods.cockpit, 'renderPalette', paletteHost);
|
|
327
386
|
applyMobile();
|
|
328
387
|
}
|
|
329
388
|
|
|
@@ -338,9 +397,19 @@ function focusSearch() {
|
|
|
338
397
|
function onKeyDown(e) {
|
|
339
398
|
if ((e.metaKey || e.ctrlKey) && (e.key === 'k' || e.key === 'K')) {
|
|
340
399
|
e.preventDefault();
|
|
341
|
-
|
|
400
|
+
// the cockpit's ⌘K is the palette; the legacy layout (and a phone) keep focusing the list's filter
|
|
401
|
+
if (legacyLayout()) setState({ col1: true, menuOpen: false }, focusSearch);
|
|
402
|
+
else setState({ paletteOpen: !state.paletteOpen, menuOpen: false });
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter' && !legacyLayout()) {
|
|
406
|
+
e.preventDefault();
|
|
407
|
+
if (mods.cockpit && typeof mods.cockpit.toggleFocus === 'function') mods.cockpit.toggleFocus();
|
|
342
408
|
return;
|
|
343
409
|
}
|
|
410
|
+
if (e.key === 'Escape' && state.paletteOpen) { setState({ paletteOpen: false }); return; }
|
|
411
|
+
if (e.key === 'Escape' && state.menuOpen) { setState({ menuOpen: false }); return; }
|
|
412
|
+
if (e.key === 'Escape' && state.layoutFocus) { setState({ layoutFocus: false }); return; }
|
|
344
413
|
if (e.key === 'Escape' && !/INPUT|TEXTAREA/.test(e.target && e.target.tagName)) {
|
|
345
414
|
// Verbatim from the prototype: Esc closes the ACTIVE tab, the last remaining tab takes over,
|
|
346
415
|
// and a split pointing at the closed tab is dropped rather than left dangling.
|
|
@@ -336,6 +336,74 @@ function answerFor(row) {
|
|
|
336
336
|
};
|
|
337
337
|
}
|
|
338
338
|
|
|
339
|
+
/**
|
|
340
|
+
* The status-from-artifacts lines, derived by the engine (scripts/status-facts.js) and only worded
|
|
341
|
+
* here. Each is a flag beside the ladder with its basis. None is a step: nothing here changes a
|
|
342
|
+
* phase, a dot or a count, and a check that could not run says so instead of staying silent.
|
|
343
|
+
*/
|
|
344
|
+
function factsFor(row) {
|
|
345
|
+
if (!row) return [];
|
|
346
|
+
const out = [];
|
|
347
|
+
const sa = row.since_approval;
|
|
348
|
+
if (sa) {
|
|
349
|
+
if (sa.status === 'unknown') out.push({ tone: 'info', label: 'Since approval', text: 'Cannot tell. ' + (sa.reason || '') });
|
|
350
|
+
else {
|
|
351
|
+
for (const a of sa.approvals || []) {
|
|
352
|
+
const what = a.kind === 'contract' ? 'the contract was approved' : 'you accepted it';
|
|
353
|
+
if (a.status === 'unknown') out.push({ tone: 'info', label: 'Since approval', text: 'Cannot tell for ' + what + '. ' + (a.reason || '') });
|
|
354
|
+
else if (!(a.changed || []).length) out.push({ tone: 'ok', label: 'Since approval', text: (row.profile === 'pm-artifacts' ? 'No brief or deliverable' : 'No spec file') + ' changed after ' + what + '.' });
|
|
355
|
+
else {
|
|
356
|
+
const files = [...new Set(a.changed.map((x) => x.file.split('/').pop()))].join(', ');
|
|
357
|
+
const last = a.changed[a.changed.length - 1];
|
|
358
|
+
out.push({ tone: 'warn', label: 'Since approval', text: files + ' changed after ' + what + ' (' + a.changed_total + ' commit' + (a.changed_total === 1 ? '' : 's') + ', latest ' + last.sha + (last.date ? ' on ' + last.date : '') + ').' });
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
const cv = row.coverage;
|
|
364
|
+
if (cv) {
|
|
365
|
+
if (cv.status === 'unknown') out.push({ tone: 'info', label: 'Coverage', text: 'Cannot tell. ' + (cv.reason || '') });
|
|
366
|
+
else if (!cv.uncited.length && !cv.orphan_citations.length) out.push({ tone: 'ok', label: 'Coverage', text: 'The rubric cites all ' + cv.requirements + ' requirement ids.' });
|
|
367
|
+
else {
|
|
368
|
+
const bits = [];
|
|
369
|
+
if (cv.uncited.length) bits.push(cv.uncited.join(', ') + ' not cited in the rubric');
|
|
370
|
+
if (cv.orphan_citations.length) bits.push(cv.orphan_citations.join(', ') + ' cited but not in the PRD');
|
|
371
|
+
out.push({ tone: 'warn', label: 'Coverage', text: bits.join('; ') + '. A citation is not a grade.' });
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
if (row.record_says) out.push({ tone: 'warn', label: 'Record says', text: 'The status line ' + row.record_says.reason + '. The ladder is not changed.' });
|
|
375
|
+
return out;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* D83 — what Jev suggests about the documents tied to this initiative. A suggestion, in words, with the model that
|
|
380
|
+
* gave it. It is never a step: it is listed after the facts and nothing reads it to colour a phase.
|
|
381
|
+
*/
|
|
382
|
+
function suggestionLines(sg) {
|
|
383
|
+
if (!sg) return [];
|
|
384
|
+
if (sg.ok === false) return [{ tone: 'info', label: 'Documents suggest', text: 'Cannot tell. ' + (sg.reason || '') }];
|
|
385
|
+
return (sg.items || []).map((x) => ({ tone: 'info', label: 'Documents suggest', text: (x.sentence || (x.doc + ': ' + x.kind)) + ' A suggestion, not a check' + (x.model ? ' (' + x.model + ')' : '') + '.' }));
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Why "What the documents say" has no line yet, in words, so an empty section is never mistaken for a missing one. Each
|
|
390
|
+
* check names what it waits for. Null when there is no ladder (the panel says that elsewhere) or when a line exists.
|
|
391
|
+
*/
|
|
392
|
+
function factsWhy(row, lines) {
|
|
393
|
+
if (!row || (lines && lines.length)) return null;
|
|
394
|
+
const why = [];
|
|
395
|
+
if (row.profile === 'pm-artifacts') {
|
|
396
|
+
why.push('Since approval starts when you accept this work: after that, a commit to its brief or deliverable shows here.');
|
|
397
|
+
} else {
|
|
398
|
+
why.push('Since approval starts when a contract is approved or the work is accepted.');
|
|
399
|
+
if (row.lane !== 'feature') why.push('Coverage is for feature-lane initiatives, and this one is ' + (row.lane || 'not in a lane') + '.');
|
|
400
|
+
else why.push('Coverage needs both a PRD and a rubric named in the record.');
|
|
401
|
+
why.push('Record says checks a status line that starts with a stage word (intake, spec, contract, build, eval or accepted).');
|
|
402
|
+
}
|
|
403
|
+
why.push('Jev suggestions appear only when "read documents for status" is on in Validate.');
|
|
404
|
+
return 'Nothing to compare yet. ' + why.join(' ');
|
|
405
|
+
}
|
|
406
|
+
|
|
339
407
|
/** The Review-Gate line, read off the `review` rung — no verdict is not a pass. */
|
|
340
408
|
function evalFor(ladderSteps) {
|
|
341
409
|
const r = (ladderSteps || []).find((s) => s.key === 'review');
|
|
@@ -405,6 +473,12 @@ function artsFor(payload) {
|
|
|
405
473
|
});
|
|
406
474
|
}
|
|
407
475
|
|
|
476
|
+
/** The rung that blocks the initiative, as the card draws it: the engine's label, evidence and next step. Null when none blocks. */
|
|
477
|
+
function blockerOf(row) {
|
|
478
|
+
const s = row && Array.isArray(row.steps) ? row.steps.find((x) => x && x.blocking === true && x.status === 'blocked') : null;
|
|
479
|
+
return s ? { key: s.key, label: s.label || s.key, evidence: s.evidence || '', next: s.next || '' } : null;
|
|
480
|
+
}
|
|
481
|
+
|
|
408
482
|
/** The full record panel 3 renders, from one `/api/initiative` payload. */
|
|
409
483
|
function recordFrom(payload) {
|
|
410
484
|
if (!payload || payload.ok === false) {
|
|
@@ -454,11 +528,15 @@ function recordFrom(payload) {
|
|
|
454
528
|
arts: artsFor(payload),
|
|
455
529
|
// R4/R5/R6 — what remains to acceptance, the conversations that belong to it, and the files git says
|
|
456
530
|
// were changed for it. Each arrives already derived by the server; this layer only carries them.
|
|
531
|
+
// workspace-cockpit-nav R2: the first rung the ladder says is blocking, or null. Nothing is invented — the words are the engine's.
|
|
532
|
+
blocker: blockerOf(row),
|
|
457
533
|
nextSteps: payload.next_steps || null,
|
|
458
534
|
lcHistory: payload.lifecycle_history || [],
|
|
459
535
|
convs: Array.isArray(payload.conversations) ? payload.conversations : [],
|
|
460
536
|
gitFiles: payload.files || null,
|
|
461
537
|
eval: evalFor(row && row.steps),
|
|
538
|
+
facts: factsFor(row).concat(suggestionLines(payload.suggestions)),
|
|
539
|
+
factsWhy: factsWhy(row, factsFor(row).concat(suggestionLines(payload.suggestions))),
|
|
462
540
|
l2: l2For(row),
|
|
463
541
|
};
|
|
464
542
|
if (!row) {
|
|
@@ -46,7 +46,7 @@ export const TAB_COLORS = {
|
|
|
46
46
|
activeText: '#f9fbfb',
|
|
47
47
|
idleText: 'oklch(0.6 0.02 213)',
|
|
48
48
|
// dot colour by tab kind; anything that is not term/task (i.e. doc) is blue.
|
|
49
|
-
dot: { term: '#47cf79', task: '#b98cff', doc: '#2b7fff' }
|
|
49
|
+
dot: { term: '#47cf79', task: '#b98cff', doc: '#2b7fff', view: '#f2b24a' }
|
|
50
50
|
};
|
|
51
51
|
export const TERM_COLORS = { prompt: '#47cf79', dim: '#6f7d81', text: '#c4ccce' };
|
|
52
52
|
export const SCRATCH_SEND_COLOR = { on: 'oklch(0.85 0.01 214)', off: 'oklch(0.55 0.02 213)' };
|
|
@@ -575,7 +575,7 @@ export const allRecords = (userInits = {}) => ({ ...DATA, ...userInits });
|
|
|
575
575
|
export const recordFor = (slug, userInits = {}) => allRecords(userInits)[slug];
|
|
576
576
|
|
|
577
577
|
/** The default selection (line 1397). */
|
|
578
|
-
export const defaultSlug = product => (product === 'atlas-crm' ? 'crm-import-wizard' : 'workspace-pm-spine');
|
|
578
|
+
export const defaultSlug = product => (COCKPIT_SEED ? (SEED_DEFAULTS[product] || null) : product === 'atlas-crm' ? 'crm-import-wizard' : 'workspace-pm-spine');
|
|
579
579
|
|
|
580
580
|
/** sessionsOf (line 1398) — authored sessions plus any started this session. */
|
|
581
581
|
export function sessionsOf(slug, { userInits = {}, extraSess = {} } = {}) {
|
|
@@ -1000,3 +1000,41 @@ export const FIXTURE = {
|
|
|
1000
1000
|
};
|
|
1001
1001
|
|
|
1002
1002
|
export default FIXTURE;
|
|
1003
|
+
|
|
1004
|
+
|
|
1005
|
+
/* ───────────── the cockpit mockup's seed (`?fixture=cockpit`) ─────────────
|
|
1006
|
+
* planning/design/cockpit-mockup/WsA.dc.html draws three repositories and one initiative in detail. The seed is applied by
|
|
1007
|
+
* mutating the exports above in place, once, so every reader (panels, parity script) sees one set of data. It is the
|
|
1008
|
+
* mockup's fake data and nothing else; no value here is read from a repository. */
|
|
1009
|
+
const COCKPIT_SEED = (() => { try { return new URLSearchParams(location.search).get('fixture') === 'cockpit'; } catch { return false; } })();
|
|
1010
|
+
const SEED_DEFAULTS = { microsites: 'ooh-weather-expansion', pmos: 'cockpit-terminal-mod', 'scarlet': 'scarlet-onboarding' };
|
|
1011
|
+
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
|
+
PRODUCTS.splice(0, PRODUCTS.length, ['microsites', '6', 'github'], ['pmos', '3', 'local'], ['scarlet', '1', 'github']);
|
|
1014
|
+
for (const k of Object.keys(ALLGROUPS)) delete ALLGROUPS[k];
|
|
1015
|
+
ALLGROUPS.microsites = [{ gid: 'O2', idc: C.fg, gname: 'Microsites ship on time', gct: '6', hasKr: true, krid: 'KR1', krname: 'q3-2026', krct: '6',
|
|
1016
|
+
slugs: ['ooh-weather-expansion', 'psle-paywall-revamp', 'smrt-news-feed', 'lightbox-ga-tagging', 'login-metric-tagging', 'shortlink-redirects'] }];
|
|
1017
|
+
ALLGROUPS.pmos = [{ gid: 'health:tech', idc: C.inf, gname: 'health budget', gct: '3', hasKr: false, slugs: ['cockpit-terminal-mod', 'workspace-cockpit-nav', 'kit-publish'] }];
|
|
1018
|
+
ALLGROUPS['scarlet'] = [{ gid: 'O1', idc: C.fg, gname: 'Onboarding', gct: '1', hasKr: false, slugs: ['scarlet-onboarding'] }];
|
|
1019
|
+
for (const k of Object.keys(DATA)) delete DATA[k];
|
|
1020
|
+
const mk = (tag, stage, steps, extra) => ({ tag, stage, flow: 'adlc-feature', pills: [{ t: 'feature', c: C.pur }, { t: 'q3-2026 · O2 · KR1', c: C.mut }], branch: null,
|
|
1021
|
+
answer: { tone: 'info', text: tag }, steps, sessions: [], arts: [], eval: null, l2: [stage, '', C.mut], ...(extra || {}) });
|
|
1022
|
+
const oneStep = (k, s) => [{ k, s, t: '' }];
|
|
1023
|
+
DATA['ooh-weather-expansion'] = mk('OOH weather microsite: add regional forecast units', 'eval', [
|
|
1024
|
+
{ k: 'spec', s: 'done', t: '' }, { k: 'build', s: 'done', t: '' }, { k: 'eval', s: 'blocked', t: '' }, { k: 'accepted', s: 'pending', t: '' }], {
|
|
1025
|
+
pills: [{ t: 'feature', c: C.mut }, { t: 'q3-2026 · O2 · KR1', c: C.mut }, { t: '⇄ Jira OOH-212', c: C.mut }],
|
|
1026
|
+
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
|
+
sessions: [{ name: 'claude · merge PR 41', st: 'working', label: 'working' }, { name: 'review run', st: 'working', label: 'working' }],
|
|
1034
|
+
arts: [{ role: 'PRD', path: 'planning/prd/ooh-weather-expansion-prd.md', dest: null }] });
|
|
1035
|
+
DATA['psle-paywall-revamp'] = mk('PSLE paywall revamp', 'accepted', oneStep('accepted', 'pending'));
|
|
1036
|
+
DATA['smrt-news-feed'] = mk('SMRT news feed', 'build', oneStep('build', 'current'));
|
|
1037
|
+
for (const id of ['lightbox-ga-tagging', 'login-metric-tagging', 'shortlink-redirects']) DATA[id] = mk(id, 'accepted', oneStep('accepted', 'done'));
|
|
1038
|
+
for (const id of ['cockpit-terminal-mod', 'workspace-cockpit-nav', 'scarlet-onboarding']) DATA[id] = mk(id, 'spec', oneStep('spec', 'blocked'));
|
|
1039
|
+
DATA['kit-publish'] = mk('kit-publish', 'accepted', oneStep('accepted', 'pending'));
|
|
1040
|
+
}
|