create-pmos 0.15.0 → 0.16.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/README.md +38 -0
- package/package.json +1 -1
- package/payload/KIT-VERSION +1 -1
- package/workspace/README.md +44 -12
- package/workspace/design-parity.js +358 -0
- package/workspace/lib/find-browser.js +60 -0
- package/workspace/lib/four-panel.js +345 -0
- package/workspace/lib/git-info.js +28 -1
- package/workspace/lib/mobile-nav.js +32 -2
- package/workspace/package-lock.json +29 -2
- package/workspace/package.json +13 -5
- package/workspace/public/index.html +79 -409
- package/workspace/public/pm/app.js +242 -0
- package/workspace/public/pm/data.js +877 -0
- package/workspace/public/pm/dom.js +116 -0
- package/workspace/public/pm/fixture.js +979 -0
- package/workspace/public/pm/mobile.css +200 -0
- package/workspace/public/pm/mobile.js +403 -0
- package/workspace/public/pm/modes.js +1076 -0
- package/workspace/public/pm/panel-detail.js +648 -0
- package/workspace/public/pm/panel-initiatives.js +426 -0
- package/workspace/public/pm/panel-repos.js +460 -0
- package/workspace/public/pm/panel-tabs.js +487 -0
- package/workspace/public/pm/state.js +164 -0
- package/workspace/public/pm/tab-doc.js +279 -0
- package/workspace/public/pm/tab-task.js +83 -0
- package/workspace/public/pm/tab-term.js +560 -0
- package/workspace/public/pm/tokens.css +73 -0
- package/workspace/public/vendor/ibm-plex-mono-500.woff2 +0 -0
- package/workspace/server.js +158 -8
- package/workspace/focus.test.js +0 -650
- package/workspace/handoff.test.js +0 -443
- package/workspace/profiles.test.js +0 -362
- package/workspace/public/app.js +0 -5560
- package/workspace/public/styles.css +0 -2258
- package/workspace/repo-scan.test.js +0 -327
|
@@ -0,0 +1,979 @@
|
|
|
1
|
+
// pm/fixture.js — the prototype's seed data, transcribed verbatim.
|
|
2
|
+
//
|
|
3
|
+
// Source: "PMOS Workspace Prototype.dc.html", the <script data-dc-script> class
|
|
4
|
+
// (lines 1007–2126). Every string here is copied from that file character for
|
|
5
|
+
// character: the comparator diffs rendered text, so a reworded label is a bug.
|
|
6
|
+
//
|
|
7
|
+
// Pure data + pure functions. No DOM, no fetch, no state. Where the prototype
|
|
8
|
+
// SYNTHESISES data (tasks from steps, faux-shell replies, doc bodies, new
|
|
9
|
+
// initiative records) the synthesis function is ported alongside the data so the
|
|
10
|
+
// app derives exactly what the prototype derives.
|
|
11
|
+
//
|
|
12
|
+
// Line references in comments are into the prototype file.
|
|
13
|
+
|
|
14
|
+
/* ────────────────────────────── colours ────────────────────────────── */
|
|
15
|
+
|
|
16
|
+
// Component.C (line 1037) — the semantic constants the seed records embed.
|
|
17
|
+
export const C = {
|
|
18
|
+
pur: 'var(--purple)',
|
|
19
|
+
mut: 'var(--muted-foreground)',
|
|
20
|
+
wrn: 'var(--warning)',
|
|
21
|
+
suc: 'var(--success)',
|
|
22
|
+
inf: 'var(--info)',
|
|
23
|
+
des: 'var(--destructive)',
|
|
24
|
+
brand: 'var(--brand)',
|
|
25
|
+
fg: 'var(--foreground)'
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// line 1391
|
|
29
|
+
export const wash = (c, p) => `color-mix(in oklab, ${c} ${p || 10}%, transparent)`;
|
|
30
|
+
|
|
31
|
+
// Step dots (line 1388), session dots (1389), answer tones (1390), task dots (1719).
|
|
32
|
+
export const DOT = { done: ['✓', C.suc], current: ['●', C.brand], pending: ['○', C.mut], blocked: ['✗', C.des] };
|
|
33
|
+
export const SDOT = { working: ['●', C.suc], quiet: ['◐', C.wrn], needs: ['●', C.brand], blocked: ['✗', C.des], exited: ['○', C.mut] };
|
|
34
|
+
export const TONE = { warn: C.wrn, bad: C.des, info: C.inf, ok: C.suc };
|
|
35
|
+
export const TDOT = { done: ['✓', C.suc], doing: ['◐', C.inf], blocked: ['✗', C.des], todo: ['○', C.mut] };
|
|
36
|
+
|
|
37
|
+
// Task status → colour in the task pane (line 2014).
|
|
38
|
+
export const TASK_STATUS_COLOR = { done: C.suc, doing: C.inf, blocked: C.des, todo: C.mut };
|
|
39
|
+
|
|
40
|
+
// Literal, non-token colours. Panel 4 and the terminal are hard-coded in the
|
|
41
|
+
// prototype (lines 1975–1978, 1984–1988, 2103) — do NOT tokenise them.
|
|
42
|
+
export const TAB_COLORS = {
|
|
43
|
+
activeBg: 'oklch(0.218 0.008 223.9)',
|
|
44
|
+
activeBorder: 'oklch(1 0 0 / 14%)',
|
|
45
|
+
idleBorder: 'oklch(1 0 0 / 7%)',
|
|
46
|
+
activeText: '#f9fbfb',
|
|
47
|
+
idleText: 'oklch(0.6 0.02 213)',
|
|
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' }
|
|
50
|
+
};
|
|
51
|
+
export const TERM_COLORS = { prompt: '#47cf79', dim: '#6f7d81', text: '#c4ccce' };
|
|
52
|
+
export const SCRATCH_SEND_COLOR = { on: 'oklch(0.85 0.01 214)', off: 'oklch(0.55 0.02 213)' };
|
|
53
|
+
|
|
54
|
+
/* ────────────────────────────── workflows ────────────────────────────── */
|
|
55
|
+
|
|
56
|
+
// Component.FLOWS (lines 1038–1043)
|
|
57
|
+
export const FLOWS = {
|
|
58
|
+
'adlc-feature': ['intake', 'spec', 'contract', 'build', 'eval', 'accepted'],
|
|
59
|
+
'adlc-research': ['intake', 'explore', 'synthesize', 'decide'],
|
|
60
|
+
'health-upkeep': ['intake', 'fix', 'verify', 'accepted'],
|
|
61
|
+
'adlc-handoff': ['intake', 'spec', 'handoff', 'build', 'eval', 'accepted']
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// A brand-new custom flow starts here (line 1488).
|
|
65
|
+
export const NEW_FLOW_STAGES = ['intake', 'spec', 'build', 'accepted'];
|
|
66
|
+
|
|
67
|
+
/* ─────────────────────── profiles / integrations ─────────────────────── */
|
|
68
|
+
|
|
69
|
+
// Component.PROFILES (lines 1044–1047)
|
|
70
|
+
export const PROFILES = [
|
|
71
|
+
{
|
|
72
|
+
id: 'claude-work',
|
|
73
|
+
account: 'you@acme.com',
|
|
74
|
+
mcp: ['jira', 'confluence', 'slack', 'google-workspace'],
|
|
75
|
+
note: 'The only profile that can reach workspace MCP — Jira, Confluence, Slack, Google Workspace. Anything that touches company systems must run here.'
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
id: 'claude-personal',
|
|
79
|
+
account: 'you@gmail.com',
|
|
80
|
+
mcp: [],
|
|
81
|
+
note: 'No workspace MCP. Fine for local coding, research, and anything that never touches company systems.'
|
|
82
|
+
}
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
// Component.INTG (lines 1048–1053)
|
|
86
|
+
export const INTG = [
|
|
87
|
+
{ id: 'jira', name: 'Jira', scope: 'project ATL', st: 'connected', synced: '12:38', does: 'handed-off initiatives read epic + story status from here — their build and eval ladder stages render from the epic, not from local files' },
|
|
88
|
+
{ id: 'confluence', name: 'Confluence', scope: 'space PM', st: 'connected', synced: '11:02', does: 'PRDs publish here when spec completes; the Confluence page is the handoff copy — planning/prd/ stays the source of truth' },
|
|
89
|
+
{ id: 'slack', name: 'Slack', scope: '#pm-updates', st: 'connected', synced: '12:40', does: 'session and blocker notifications post here' },
|
|
90
|
+
{ id: 'google-workspace', name: 'Google Workspace', scope: 'drive + calendar', st: 'connected', synced: '09:15', does: 'calendar holds scheduled-task windows; research docs import from Drive' }
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
// line 1952 — the two connections that can preview a document.
|
|
94
|
+
export const INTG_PREVIEWS = {
|
|
95
|
+
jira: ['jira://ATL-482', 'open ATL-482 preview'],
|
|
96
|
+
confluence: ['confluence://PM/crm-import-wizard-prd', 'open PRD page preview']
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/* ────────────────────────────── skills ────────────────────────────── */
|
|
100
|
+
|
|
101
|
+
// SKILLS (lines 1491–1496)
|
|
102
|
+
export const SKILLS = [
|
|
103
|
+
{ name: 'write-prd', d: 'draft a PRD from an initiative record', reads: 'planning/initiatives/<slug>.md', writes: 'planning/prd/<slug>-prd.md', usedIn: 'spec stage of adlc-feature, adlc-research' },
|
|
104
|
+
{ name: 'contract-run', d: 'agree done_criteria + test_method, log the run', reads: 'planning/prd/<slug>-prd.md', writes: 'planning/runs/agent-runs.jsonl', usedIn: 'contract stage of adlc-feature' },
|
|
105
|
+
{ name: 'score-rubric', d: 'run Review-Gate, append verdict', reads: 'planning/evals/<slug>-eval-rubric.md', writes: 'planning/evals/evals.jsonl', usedIn: 'eval stage of adlc-feature; verify stage of health-upkeep' },
|
|
106
|
+
{ name: 'kanban-view', d: 'project the ladder as a board (same data)', reads: 'planning/initiatives/*.md', writes: 'nothing — a pure projection', usedIn: 'any initiative panel' }
|
|
107
|
+
];
|
|
108
|
+
|
|
109
|
+
/* ────────────────────────────── knowledge ────────────────────────────── */
|
|
110
|
+
|
|
111
|
+
// Component.KNOW (lines 1082–1087)
|
|
112
|
+
export const KNOW = {
|
|
113
|
+
D59: { grp: 'governance', d: 'unanchored work is rejected', badge: 'active', path: 'planning/decisions/D59.md', body: 'Every initiative must resolve an anchor upward to a KR or a health budget. Work with an empty parent_kr is rejected at intake — listed under "unanchored", never silently mixed into the board.', rels: [['supersedes', 'D12'], ['applies to', 'D33']] },
|
|
114
|
+
D12: { grp: 'governance', d: 'soft warnings for unanchored work', badge: 'superseded', path: 'planning/decisions/D12.md', body: 'Unanchored initiatives get a warning banner but stay on the board. Superseded by D59 after the junk-drawer incident of May 2026.', rels: [['superseded by', 'D59']] },
|
|
115
|
+
D41: { grp: 'workspace', d: 'the terminal stays dark in both themes', badge: 'active', path: 'planning/decisions/D41.md', body: 'Column 3 is xterm.js and keeps a dark palette in both themes. A terminal is a terminal, not a themed surface.', rels: [['informed by', 'D33']] },
|
|
116
|
+
D33: { grp: 'workspace', d: 'derive, never store, panel state', badge: 'active', path: 'planning/decisions/D33.md', body: 'The initiative panel derives everything from files on disk at read time. No cached panel state — a wrong cache is worse than a slow read.', rels: [['informs', 'D41'], ['applied by', 'D59']] }
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// Knowledge added by the atlas-mobile ingest (lines 1597–1601).
|
|
120
|
+
export const INGESTED_KNOW = {
|
|
121
|
+
'M-ADR-7': { grp: 'mobile · ingested', d: 'offline-first sync queue', badge: 'ingested', path: 'docs/adr/0007-offline-sync.md', body: 'Imported from acme/atlas-mobile by repo scan. Writes queue locally and reconcile when the network returns. Ingested knowledge is proposed, not decided — review it, then accept or supersede.', rels: [] },
|
|
122
|
+
'M-ADR-12': { grp: 'mobile · ingested', d: 'crash budget — 2% of sessions', badge: 'ingested', path: 'docs/adr/0012-crash-budget.md', body: 'Imported from acme/atlas-mobile by repo scan. Crash rate above 2% of sessions freezes feature work until triaged. Review it, then accept or supersede.', rels: [] }
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
/* ─────────────────── scheduled tasks / agents / memory ─────────────────── */
|
|
126
|
+
|
|
127
|
+
// schedList() (lines 1055–1061)
|
|
128
|
+
const SCHED_SEED = [
|
|
129
|
+
{ name: 'nightly evals sweep', when: '02:00 daily', runCmd: 'pmos run score-rubric --all', scope: 'all initiatives with a rubric', profile: 'claude-personal', enabled: true, log: [{ ts: '02:00', t: 'ok — 0 failures', c: 'var(--success)' }, { ts: 'yesterday 02:00', t: 'ok — 0 failures', c: 'var(--success)' }] },
|
|
130
|
+
{ name: 'prd freshness check', when: 'mon 09:00', runCmd: 'pmos check prd-freshness', scope: 'all initiatives', profile: 'claude-work', enabled: true, log: [{ ts: 'mon 09:00', t: 'ok — 2 stale PRDs flagged', c: 'var(--success)' }] },
|
|
131
|
+
{ name: 'agent-runs compaction', when: 'fri 18:00', runCmd: 'pmos compact agent-runs', scope: 'planning/runs/', profile: 'claude-personal', enabled: false, log: [{ ts: 'fri 18:00', t: 'skipped — repo busy', c: 'var(--warning)' }] }
|
|
132
|
+
];
|
|
133
|
+
|
|
134
|
+
// agentList() (lines 1062–1067)
|
|
135
|
+
const AGENTS_SEED = [
|
|
136
|
+
{ name: 'sonnet-builder', role: 'builder', model: 'claude-sonnet-4-5', profile: 'claude-work', paused: false, sessions: [{ name: 'spine build', slug: 'workspace-pm-spine', st: 'working' }, { name: 'rubric fix', slug: 'workspace-pm-spine', st: 'needs' }] },
|
|
137
|
+
{ name: 'haiku-scout', role: 'scout', model: 'claude-haiku-4', profile: 'claude-personal', paused: false, sessions: [{ name: 'doc-editor scout', slug: 'workspace-doc-editor', st: 'quiet' }] }
|
|
138
|
+
];
|
|
139
|
+
|
|
140
|
+
// memory() (lines 1068–1081)
|
|
141
|
+
const MEMORY_SEED = {
|
|
142
|
+
short: [
|
|
143
|
+
{ id: 's1', text: 'spine build: current diff touches navigator.tsx + spine.ts', src: 'session · spine build', ts: '12:39' },
|
|
144
|
+
{ id: 's2', text: 'rubric fix: waiting on PM to confirm 0–10 scoring scale', src: 'session · rubric fix', ts: '12:22' },
|
|
145
|
+
{ id: 's3', text: 'doc-editor scout: PRD lacks acceptance criteria — flagged', src: 'session · doc-editor scout', ts: '12:23' }
|
|
146
|
+
],
|
|
147
|
+
long: [
|
|
148
|
+
{ id: 'l1', text: 'PM prefers diffs over prose summaries', src: 'promoted 2026-07-12', pinned: true },
|
|
149
|
+
{ id: 'l2', text: 'repo uses pnpm — never run npm install', src: 'promoted 2026-06-30', pinned: false },
|
|
150
|
+
{ id: 'l3', text: 'no verdict recorded is not a pass (house rule)', src: 'added by PM 2026-07-02', pinned: true }
|
|
151
|
+
]
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
// A new agent (line 1526) and a new long-term memory note (line 1538).
|
|
155
|
+
export const NEW_AGENT = { role: 'builder', model: 'claude-sonnet-4-5', paused: false, sessions: [] };
|
|
156
|
+
export const NEW_MEMORY_NOTE = { text: 'new note — edit me', src: 'added by PM · now', pinned: false };
|
|
157
|
+
// A new scheduled task (line 1476).
|
|
158
|
+
export const NEW_SCHED = { when: 'daily 06:00', runCmd: '', scope: 'all initiatives', enabled: true, log: [] };
|
|
159
|
+
// The line a scheduled task shows when it has never run (line 1846).
|
|
160
|
+
export const SCHED_NEVER_RUN = { ts: '—', t: 'never run yet — that is stated, not implied', c: C.mut };
|
|
161
|
+
|
|
162
|
+
const clone = v => JSON.parse(JSON.stringify(v));
|
|
163
|
+
/** Fresh, mutable copies of the three editable seed lists. */
|
|
164
|
+
export const defaultSched = () => clone(SCHED_SEED);
|
|
165
|
+
export const defaultAgents = () => clone(AGENTS_SEED);
|
|
166
|
+
export const defaultMemory = () => clone(MEMORY_SEED);
|
|
167
|
+
|
|
168
|
+
/* ───────────────────── repositories + board groups ───────────────────── */
|
|
169
|
+
|
|
170
|
+
// allProds (line 1431) — the two repositories present before anything is added.
|
|
171
|
+
export const PRODUCTS = [
|
|
172
|
+
['workspace', '7 initiatives', 'local'],
|
|
173
|
+
['atlas-crm', '2 initiatives', 'github']
|
|
174
|
+
];
|
|
175
|
+
|
|
176
|
+
// The two repositories the add-product form can create (lines 1596, 1613).
|
|
177
|
+
export const ADDABLE_PRODUCTS = {
|
|
178
|
+
'atlas-mobile': { id: 'atlas-mobile', sub: '2 initiatives', kind: 'github' },
|
|
179
|
+
'notes-cli': { id: 'notes-cli', sub: '0 initiatives', kind: 'local' }
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
// Add-product form defaults (line 1012).
|
|
183
|
+
export const ADD_DEFAULTS = { addSrc: 'github', addRepo: 'github.com/acme/atlas-mobile', addPath: '~/code/notes-cli' };
|
|
184
|
+
|
|
185
|
+
// Ingest logs (lines 1585–1616). `{repo}` / `{path}` interpolate the form value.
|
|
186
|
+
export const INGEST_LOG_GITHUB = repo => [
|
|
187
|
+
{ t: '$ pmos product add ' + repo, c: 'var(--success)' },
|
|
188
|
+
{ t: 'clone … ok — 1,204 files', c: 'var(--muted-foreground)' },
|
|
189
|
+
{ t: 'scan planning/ … 2 initiative records found', c: 'var(--muted-foreground)' },
|
|
190
|
+
{ t: 'scan docs/adr/ … 2 ADRs imported → knowledge · mobile', c: 'var(--muted-foreground)' },
|
|
191
|
+
{ t: 'propose board … anchors guessed, not decided — confirm them', c: 'var(--warning)' },
|
|
192
|
+
{ t: 'done — product atlas-mobile added', c: 'var(--success)' }
|
|
193
|
+
];
|
|
194
|
+
export const INGEST_LOG_LOCAL = path => [
|
|
195
|
+
{ t: '$ pmos product add ' + path, c: 'var(--success)' },
|
|
196
|
+
{ t: 'scan planning/ … nothing filed — a successful read of nothing', c: 'var(--muted-foreground)' },
|
|
197
|
+
{ t: 'done — product notes-cli added with an empty board', c: 'var(--success)' }
|
|
198
|
+
];
|
|
199
|
+
export const INGEST_ALREADY = id => [{ t: id + ' already added — pick it on the board', c: 'var(--warning)' }];
|
|
200
|
+
|
|
201
|
+
// ALLGROUPS (lines 1411–1424) — Objective → KR → Initiative, per repository.
|
|
202
|
+
export const ALLGROUPS = {
|
|
203
|
+
workspace: [
|
|
204
|
+
{ gid: 'O1', idc: C.fg, gname: 'PMOS has a trustworthy harness a PM can run an SDLC through', gct: '44', hasKr: true, krid: 'KR1', krname: 'P0 harness complete and PM-accepted', krct: '23', slugs: ['workspace-pm-spine', 'workspace-doc-editor', 'pmos-ui-research', 'initiative-kr-join', 'workspace-harness-tower'] },
|
|
205
|
+
{ gid: 'health:tech', idc: C.inf, gname: 'health budget — upkeep, not a bet', gct: '19', hasKr: false, slugs: ['workspace-shadcn-design-system'] },
|
|
206
|
+
{ gid: 'unanchored', idc: C.wrn, gname: 'no resolvable anchor — rejected work under D59', gct: '30', hasKr: false, slugs: ['agent-memory-research'] }
|
|
207
|
+
],
|
|
208
|
+
'atlas-crm': [
|
|
209
|
+
{ gid: 'O1', idc: C.fg, gname: 'CRM onboarding is self-serve by Q4', gct: '12', hasKr: true, krid: 'KR2', krname: 'import wizard shipped · under 2% failed imports', krct: '6', slugs: ['crm-import-wizard', 'crm-billing-sync'] }
|
|
210
|
+
],
|
|
211
|
+
'atlas-mobile': [
|
|
212
|
+
{ gid: 'ingested', idc: C.wrn, gname: 'proposed from repo scan — confirm anchors', gct: '2', hasKr: false, slugs: ['mob-onboarding-revamp', 'mob-crash-triage'] }
|
|
213
|
+
],
|
|
214
|
+
'notes-cli': []
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
// ensureUnanchored (line 1633) — the group a rejected new initiative lands in.
|
|
218
|
+
export const unanchoredGroup = () => ({ gid: 'unanchored', dyn: true, idc: C.wrn, gname: 'no resolvable anchor — rejected work under D59', gct: '', hasKr: false, slugs: [] });
|
|
219
|
+
// A user-filed objective (line 1650).
|
|
220
|
+
export const userObjectiveGroup = (gid, gname) => ({ gid, dyn: true, idc: C.fg, gname, gct: '', hasKr: false, slugs: [] });
|
|
221
|
+
|
|
222
|
+
/* ──────────────────────── the initiative records ──────────────────────── */
|
|
223
|
+
|
|
224
|
+
// data() (lines 1088–1249). Ten records, verbatim.
|
|
225
|
+
export const DATA = {
|
|
226
|
+
'workspace-pm-spine': {
|
|
227
|
+
tag: 'The navigator becomes Objective → KR → Initiative', stage: 'build', flow: 'adlc-feature',
|
|
228
|
+
pills: [{ t: 'feature', c: C.pur }, { t: 'q3-2026:o1:kr1', c: C.mut }, { t: 'flow adlc-feature', c: C.inf }],
|
|
229
|
+
branch: 'initiative/workspace-pm-spine',
|
|
230
|
+
answer: { tone: 'warn', text: 'Agree done_criteria + test_method BEFORE building' },
|
|
231
|
+
steps: [
|
|
232
|
+
{ k: 'intake', s: 'done', t: 'anchored to q3-2026:o1:kr1' },
|
|
233
|
+
{ k: 'spec', s: 'done', t: 'PRD + rubric filed' },
|
|
234
|
+
{ k: 'contract', s: 'blocked', t: 'no run logged', action: 'agree done_criteria + test_method BEFORE building', cmd: 'planning/runs/agent-runs.jsonl' },
|
|
235
|
+
{ k: 'build', s: 'current', t: '4 commits on implementation paths' },
|
|
236
|
+
{ k: 'eval', s: 'pending', t: 'score against the rubric → evals.jsonl' },
|
|
237
|
+
{ k: 'accepted', s: 'pending', t: '' }],
|
|
238
|
+
sessions: [{ name: 'spine build', st: 'working', label: 'working' }, { name: 'rubric fix', st: 'needs', label: 'needs you' }],
|
|
239
|
+
links: [{ kind: 'research', label: 'terminal-workspace field notes', path: 'gdoc://workspace-pm-spine-research', st: 'auto · session' }, { kind: 'benchmark', label: 'latency baseline sheet', path: 'gdoc://workspace-pm-spine-benchmark', st: 'auto · session' }],
|
|
240
|
+
arts: [{ role: 'PRD', path: 'planning/prd/workspace-pm-spine-prd.md', dest: { kind: 'confluence', target: 'PM / workspace-pm-spine-prd' } }, { role: 'RUBRIC', path: 'planning/evals/workspace-pm-spine-eval-rubric.md' }],
|
|
241
|
+
eval: null, l2: ['build', '→ contract owed', C.wrn]
|
|
242
|
+
},
|
|
243
|
+
'workspace-doc-editor': {
|
|
244
|
+
tag: 'Edit planning docs without leaving the workspace', stage: 'intake', flow: 'adlc-feature',
|
|
245
|
+
pills: [{ t: 'feature', c: C.pur }, { t: 'q3-2026:o1:kr1', c: C.mut }, { t: 'flow adlc-feature', c: C.inf }],
|
|
246
|
+
branch: 'initiative/workspace-doc-editor',
|
|
247
|
+
answer: { tone: 'bad', text: 'Clear 2 intake blockers — each has a concrete fix' },
|
|
248
|
+
steps: [
|
|
249
|
+
{ k: 'intake', s: 'blocked', t: '✗ 2 blocked', action: 'set parent_kr (KR at WIP limit 3/3), then add acceptance criteria to the PRD', cmd: 'planning/initiatives/workspace-doc-editor.md' },
|
|
250
|
+
{ k: 'spec', s: 'done', t: 'PRD filed' },
|
|
251
|
+
{ k: 'contract', s: 'pending', t: '' }, { k: 'build', s: 'pending', t: '' },
|
|
252
|
+
{ k: 'eval', s: 'pending', t: '' }, { k: 'accepted', s: 'pending', t: '' }],
|
|
253
|
+
sessions: [{ name: 'doc-editor scout', st: 'quiet', label: 'quiet 18m' }],
|
|
254
|
+
arts: [{ role: 'PRD', path: 'planning/prd/workspace-doc-editor-prd.md' }],
|
|
255
|
+
eval: null, l2: ['intake', '✗ 2 blocked', C.des]
|
|
256
|
+
},
|
|
257
|
+
'pmos-ui-research': {
|
|
258
|
+
tag: 'What PMs actually do all day in a terminal workspace', stage: 'intake', flow: 'adlc-research',
|
|
259
|
+
pills: [{ t: 'research', c: C.pur }, { t: 'flow adlc-research', c: C.inf }], branch: null,
|
|
260
|
+
answer: { tone: 'info', text: 'File the research brief — nothing is filed for this initiative yet.' },
|
|
261
|
+
steps: [
|
|
262
|
+
{ k: 'intake', s: 'current', t: 'nothing filed yet — a successful read of nothing', action: 'file the research brief', cmd: 'planning/prd/pmos-ui-research-brief.md' },
|
|
263
|
+
{ k: 'explore', s: 'pending', t: '' }, { k: 'synthesize', s: 'pending', t: '' }, { k: 'decide', s: 'pending', t: '' }],
|
|
264
|
+
tasks: {
|
|
265
|
+
intake: [
|
|
266
|
+
{ title: 'frame the discovery question', status: 'done', instruction: 'Write the single, falsifiable question this discovery must answer, and file it at the top of the brief.', input: 'the KR this sits under + the raw ask', expect: 'One sentence a stranger could judge as answered or not.', evalNote: 'a reviewer agrees the question is specific and falsifiable', output: 'planning/prd/pmos-ui-research-brief.md' },
|
|
267
|
+
{ title: 'list what we know vs. must learn', status: 'doing', instruction: 'Separate standing assumptions from open questions; capture each unknown as its own line so it can be assigned later.', input: 'the brief + any prior notes on this space', expect: 'Two short lists — known, unknown — in the brief.', evalNote: 'every unknown is phrased as something an observation could resolve', output: '—' },
|
|
268
|
+
{ title: 'set the go / drop gate', status: 'todo', instruction: 'Define the evidence that would make this worth picking up — and the evidence that means drop it. Write both as explicit criteria.', input: 'the unknowns list', expect: 'Explicit drop criteria so discovery can end cleanly.', evalNote: 'the gate can be evaluated without further judgement calls', output: '—' }
|
|
269
|
+
],
|
|
270
|
+
explore: [
|
|
271
|
+
{ title: 'shadow 5 PMs, one session each', status: 'todo', instruction: 'Observe five real terminal-workspace sessions end to end; take raw, timestamped notes without interpreting yet.', input: 'the go/drop gate + a recruit list', expect: 'Raw notes per session, filed under research/.', evalNote: 'notes are observations, not conclusions', output: '—' },
|
|
272
|
+
{ title: 'mine support + sales-call themes', status: 'todo', instruction: 'Pull recurring asks from prior transcripts and tag each with a theme and a count.', input: 'support + sales transcript archive', expect: 'A themed list with counts.', evalNote: 'themes are mutually exclusive and cover the corpus', output: '—' }
|
|
273
|
+
],
|
|
274
|
+
synthesize: [
|
|
275
|
+
{ title: 'cluster findings into candidate needs', status: 'todo', instruction: 'Group the observations into 3–5 need statements, each backed by evidence lines.', input: 'session notes + theme list', expect: 'A short, ranked list of candidate needs.', evalNote: 'each need cites at least two independent observations', output: '—' }
|
|
276
|
+
],
|
|
277
|
+
decide: [
|
|
278
|
+
{ title: 'write the pick-up / drop recommendation', status: 'todo', instruction: 'Recommend whether this becomes a real initiative, measured against the go/drop gate, and record the rationale.', input: 'candidate needs + the gate', expect: 'A decision record a PM can accept or reject.', evalNote: 'the recommendation maps directly to the gate criteria', output: '—' }
|
|
279
|
+
]
|
|
280
|
+
},
|
|
281
|
+
sessions: [], noSess: 'No sessions — normal, not an error.',
|
|
282
|
+
arts: [], eval: null, l2: ['intake', 'nothing filed', C.mut]
|
|
283
|
+
},
|
|
284
|
+
'initiative-kr-join': {
|
|
285
|
+
tag: 'Initiative↔KR join', stage: 'no record', flow: null,
|
|
286
|
+
pills: [{ t: 'no record', c: C.wrn }], branch: null,
|
|
287
|
+
noLadder: 'No initiative record on this board — the artifacts below are what the library can see; no loop position is claimed.',
|
|
288
|
+
answer: { tone: 'warn', text: 'File an initiative record — artifacts exist, but no loop position is claimed.' },
|
|
289
|
+
steps: [], sessions: [], noSess: 'No working tree is on initiative/initiative-kr-join — a normal state.',
|
|
290
|
+
arts: [{ role: 'PRD', path: 'planning/prd/initiative-kr-join-prd.md' }, { role: 'RUBRIC', path: 'planning/evals/initiative-kr-join-eval-rubric.md' }, { role: 'DOC', path: 'planning/initiatives/initiative-kr-join.md' }],
|
|
291
|
+
eval: null, l2: ['—', 'no record', C.wrn]
|
|
292
|
+
},
|
|
293
|
+
'workspace-harness-tower': {
|
|
294
|
+
readError: 'read failed — planning/initiatives/workspace-harness-tower.md: YAML frontmatter unparseable at line 3 (expected key: value, got "stage build"). Fix the file; this panel will not pretend it is empty.',
|
|
295
|
+
l2: ['read failed', 'frontmatter unparseable', C.des]
|
|
296
|
+
},
|
|
297
|
+
'workspace-shadcn-design-system': {
|
|
298
|
+
tag: 'One token layer shared with the sibling product', stage: 'accepted', flow: 'health-upkeep',
|
|
299
|
+
pills: [{ t: 'health:tech', c: C.inf }, { t: 'flow health-upkeep', c: C.inf }],
|
|
300
|
+
branch: 'initiative/workspace-shadcn-design-system',
|
|
301
|
+
answer: { tone: 'ok', text: 'Accepted — nothing owed. New work should be a new initiative.' },
|
|
302
|
+
steps: [
|
|
303
|
+
{ k: 'intake', s: 'done', t: 'health budget — upkeep, not a bet' },
|
|
304
|
+
{ k: 'fix', s: 'done', t: 'ported radix-sera / mist' },
|
|
305
|
+
{ k: 'verify', s: 'done', t: 'scored 9/10 against the rubric' },
|
|
306
|
+
{ k: 'accepted', s: 'done', t: 'PM-accepted 2026-07-21' }],
|
|
307
|
+
sessions: [], noSess: 'No sessions — accepted work has none. Normal.',
|
|
308
|
+
arts: [{ role: 'PRD', path: 'planning/prd/workspace-shadcn-design-system-prd.md' }, { role: 'RUBRIC', path: 'planning/evals/workspace-shadcn-design-system-eval-rubric.md' }],
|
|
309
|
+
eval: { pass: true, text: 'PASS — 9/10 · evals.jsonl #114 · 2026-07-21' }, l2: ['accepted', '', C.suc]
|
|
310
|
+
},
|
|
311
|
+
'agent-memory-research': {
|
|
312
|
+
tag: 'Rejected under D59 — unanchored work', stage: 'intake', flow: 'adlc-feature',
|
|
313
|
+
pills: [{ t: 'unanchored', c: C.des }], branch: null,
|
|
314
|
+
answer: { tone: 'bad', text: 'Anchor it: parent_kr is empty — unanchored work is rejected under D59' },
|
|
315
|
+
steps: [
|
|
316
|
+
{ k: 'intake', s: 'blocked', t: '✗ rejected — no resolvable anchor', action: 'set parent_kr in planning/initiatives/agent-memory-research.md', cmd: 'planning/initiatives/agent-memory-research.md' },
|
|
317
|
+
{ k: 'spec', s: 'pending', t: '' }, { k: 'contract', s: 'pending', t: '' },
|
|
318
|
+
{ k: 'build', s: 'pending', t: '' }, { k: 'eval', s: 'pending', t: '' }, { k: 'accepted', s: 'pending', t: '' }],
|
|
319
|
+
sessions: [], noSess: 'No sessions — rejected work cannot start one.',
|
|
320
|
+
arts: [{ role: 'DOC', path: 'planning/initiatives/agent-memory-research.md' }],
|
|
321
|
+
eval: null, l2: ['intake', 'rejected · no anchor', C.des]
|
|
322
|
+
},
|
|
323
|
+
'crm-import-wizard': {
|
|
324
|
+
tag: 'Spec owned here — build handed off to the app team on Jira', stage: 'build', flow: 'adlc-handoff',
|
|
325
|
+
pills: [{ t: 'feature', c: C.pur }, { t: 'q3-2026:o1:kr2', c: C.mut }, { t: 'flow adlc-handoff', c: C.inf }, { t: '⇄ jira ATL-482', c: C.inf }],
|
|
326
|
+
branch: null,
|
|
327
|
+
answer: { tone: 'info', text: 'Nothing owed here — build is with the app team. Jira ATL-482: 3/7 stories done, synced 12:38.' },
|
|
328
|
+
steps: [
|
|
329
|
+
{ k: 'intake', s: 'done', t: 'anchored to q3-2026:o1:kr2' },
|
|
330
|
+
{ k: 'spec', s: 'done', t: 'PRD + 7 user stories filed' },
|
|
331
|
+
{ k: 'handoff', s: 'done', t: 'PRD → Confluence · stories → Jira ATL-482' },
|
|
332
|
+
{ k: 'build', s: 'current', t: '3 of 7 stories done', src: 'jira' },
|
|
333
|
+
{ k: 'eval', s: 'pending', t: 'acceptance review when the epic closes', src: 'jira' },
|
|
334
|
+
{ k: 'accepted', s: 'pending', t: '' }],
|
|
335
|
+
links: [
|
|
336
|
+
{ kind: 'jira', label: 'ATL-482 · import wizard epic', st: '3/7 done · synced 12:38', path: 'jira://ATL-482' },
|
|
337
|
+
{ kind: 'confluence', label: 'PM / CRM import wizard PRD', st: 'published · v4', path: 'confluence://PM/crm-import-wizard-prd' }],
|
|
338
|
+
sessions: [], noSess: 'No sessions — the build is not on this machine. Status reads from Jira.',
|
|
339
|
+
arts: [{ role: 'PRD', path: 'planning/prd/crm-import-wizard-prd.md' }, { role: 'STORIES', path: 'planning/prd/crm-import-wizard-stories.md' }],
|
|
340
|
+
eval: null, l2: ['build', '⇄ on jira · 3/7 done', C.inf]
|
|
341
|
+
},
|
|
342
|
+
'crm-billing-sync': {
|
|
343
|
+
tag: 'Stripe → CRM invoice sync', stage: 'spec', flow: 'adlc-handoff',
|
|
344
|
+
pills: [{ t: 'feature', c: C.pur }, { t: 'q3-2026:o1:kr2', c: C.mut }, { t: 'flow adlc-handoff', c: C.inf }],
|
|
345
|
+
branch: null,
|
|
346
|
+
answer: { tone: 'warn', text: 'Finish the user stories — handoff to Jira needs the full list.' },
|
|
347
|
+
steps: [
|
|
348
|
+
{ k: 'intake', s: 'done', t: 'anchored to q3-2026:o1:kr2' },
|
|
349
|
+
{ k: 'spec', s: 'current', t: 'PRD drafted · stories 4/9 written', action: 'finish the user stories — handoff to Jira needs the full list', cmd: 'planning/prd/crm-billing-sync-stories.md' },
|
|
350
|
+
{ k: 'handoff', s: 'pending', t: '' },
|
|
351
|
+
{ k: 'build', s: 'pending', t: '', src: 'jira' },
|
|
352
|
+
{ k: 'eval', s: 'pending', t: '', src: 'jira' },
|
|
353
|
+
{ k: 'accepted', s: 'pending', t: '' }],
|
|
354
|
+
sessions: [{ name: 'stories draft', st: 'needs', label: 'needs you' }],
|
|
355
|
+
arts: [{ role: 'PRD', path: 'planning/prd/crm-billing-sync-prd.md' }],
|
|
356
|
+
eval: null, l2: ['spec', 'stories 4/9', C.wrn]
|
|
357
|
+
},
|
|
358
|
+
'mob-onboarding-revamp': {
|
|
359
|
+
tag: 'Ingested from acme/atlas-mobile — anchor proposed, not confirmed', stage: 'intake', flow: 'adlc-feature',
|
|
360
|
+
pills: [{ t: 'feature', c: C.pur }, { t: 'anchor? proposed', c: C.wrn }, { t: 'ingested', c: C.inf }],
|
|
361
|
+
branch: null,
|
|
362
|
+
answer: { tone: 'warn', text: 'Confirm the proposed anchor — the repo scan guessed it from the README, it does not decide.' },
|
|
363
|
+
steps: [
|
|
364
|
+
{ k: 'intake', s: 'current', t: 'ingested · anchor guessed from repo scan', action: 'confirm parent_kr — the scan proposed an onboarding KR', cmd: 'planning/initiatives/mob-onboarding-revamp.md' },
|
|
365
|
+
{ k: 'spec', s: 'pending', t: '' }, { k: 'contract', s: 'pending', t: '' },
|
|
366
|
+
{ k: 'build', s: 'pending', t: '' }, { k: 'eval', s: 'pending', t: '' }, { k: 'accepted', s: 'pending', t: '' }],
|
|
367
|
+
sessions: [], noSess: 'No sessions — ingested initiatives start quiet.',
|
|
368
|
+
arts: [{ role: 'DOC', path: 'planning/initiatives/mob-onboarding-revamp.md' }],
|
|
369
|
+
eval: null, l2: ['intake', 'anchor proposed — confirm', C.wrn]
|
|
370
|
+
},
|
|
371
|
+
'mob-crash-triage': {
|
|
372
|
+
tag: 'Crash rate 2.4% — over the 2% budget (M-ADR-12)', stage: 'fix', flow: 'health-upkeep',
|
|
373
|
+
pills: [{ t: 'health:mobile', c: C.inf }, { t: 'flow health-upkeep', c: C.inf }, { t: 'ingested', c: C.inf }],
|
|
374
|
+
branch: null,
|
|
375
|
+
answer: { tone: 'warn', text: 'Crash rate over budget — M-ADR-12 freezes feature work until triaged.' },
|
|
376
|
+
steps: [
|
|
377
|
+
{ k: 'intake', s: 'done', t: 'health budget — imported with the repo' },
|
|
378
|
+
{ k: 'fix', s: 'current', t: '2.4% vs 2.0% budget · top crash: sync queue', action: 'assign a builder agent to triage the top 3 crash signatures' },
|
|
379
|
+
{ k: 'verify', s: 'pending', t: '' }, { k: 'accepted', s: 'pending', t: '' }],
|
|
380
|
+
sessions: [], arts: [],
|
|
381
|
+
eval: null, l2: ['fix', '2.4% over budget', C.wrn]
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
/* ────────────────────────────── documents ────────────────────────────── */
|
|
386
|
+
|
|
387
|
+
// rawDocs() (lines 1250–1335). Built once, exactly as the prototype builds it:
|
|
388
|
+
// four authored bodies, one generated page per KNOW entry (base KNOW only — the
|
|
389
|
+
// ingested M-ADR pages fall through to the generic template in rawFor), and
|
|
390
|
+
// evals.jsonl.
|
|
391
|
+
export const RAW_DOCS = (() => {
|
|
392
|
+
const out = {
|
|
393
|
+
'planning/prd/workspace-pm-spine-prd.md': `---
|
|
394
|
+
path: planning/prd/workspace-pm-spine-prd.md
|
|
395
|
+
role: PRD
|
|
396
|
+
initiative: workspace-pm-spine
|
|
397
|
+
---
|
|
398
|
+
|
|
399
|
+
# PRD: the workspace PM spine
|
|
400
|
+
|
|
401
|
+
## Problem
|
|
402
|
+
|
|
403
|
+
The navigator lists repos and sessions, but the PM thinks in Objective → KR → Initiative. The spine should be the primary navigation, with repos and sessions hanging off initiatives.
|
|
404
|
+
|
|
405
|
+
## Done criteria
|
|
406
|
+
|
|
407
|
+
- Column 1 renders the O/KR/Initiative ladder from planning/initiatives/*.md
|
|
408
|
+
- Selecting an initiative derives workflow, evals, sessions, artifacts
|
|
409
|
+
- Unanchored work is visibly rejected, never silently listed
|
|
410
|
+
- A failed read renders red with the parse error, not an empty panel
|
|
411
|
+
|
|
412
|
+
## Test method
|
|
413
|
+
|
|
414
|
+
Golden-file board fixtures: healthy, blocked, empty, no-record, unparseable. Screenshot diff per theme.`,
|
|
415
|
+
'planning/evals/workspace-pm-spine-eval-rubric.md': `---
|
|
416
|
+
path: planning/evals/workspace-pm-spine-eval-rubric.md
|
|
417
|
+
role: RUBRIC
|
|
418
|
+
initiative: workspace-pm-spine
|
|
419
|
+
---
|
|
420
|
+
|
|
421
|
+
# Eval rubric: workspace-pm-spine
|
|
422
|
+
|
|
423
|
+
## Rubric
|
|
424
|
+
|
|
425
|
+
- Within 5s of selecting an initiative, the PM can state: where it is, what is next, what blocks it (3 pts)
|
|
426
|
+
- Every blocked step names a concrete fix — a path, a skill, a command (3 pts)
|
|
427
|
+
- Honest states: failed read is red with a reason; empty is calm (2 pts)
|
|
428
|
+
- Both themes ship; terminal stays dark (2 pts)
|
|
429
|
+
|
|
430
|
+
## Verdict
|
|
431
|
+
|
|
432
|
+
Append the scored run to planning/evals/evals.jsonl. No verdict recorded is not a pass.`
|
|
433
|
+
};
|
|
434
|
+
out['jira://ATL-482'] = `---
|
|
435
|
+
path: jira://ATL-482
|
|
436
|
+
role: JIRA EPIC
|
|
437
|
+
profile: claude-work · mcp jira
|
|
438
|
+
synced: 12:38
|
|
439
|
+
---
|
|
440
|
+
|
|
441
|
+
# ATL-482 — CRM import wizard
|
|
442
|
+
|
|
443
|
+
## Stories
|
|
444
|
+
|
|
445
|
+
- ATL-483 file upload + column mapping — done
|
|
446
|
+
- ATL-484 dedupe rules — done
|
|
447
|
+
- ATL-485 dry-run preview — done
|
|
448
|
+
- ATL-486 error report export — in progress
|
|
449
|
+
- ATL-487 rollback — todo
|
|
450
|
+
- ATL-488 permissions — todo
|
|
451
|
+
- ATL-489 telemetry — todo
|
|
452
|
+
|
|
453
|
+
## Sync
|
|
454
|
+
|
|
455
|
+
Read-only mirror. Status flows Jira → PMOS. The epic is edited in Jira, not here.`;
|
|
456
|
+
out['confluence://PM/crm-import-wizard-prd'] = `---
|
|
457
|
+
path: confluence://PM/crm-import-wizard-prd
|
|
458
|
+
role: CONFLUENCE PAGE
|
|
459
|
+
profile: claude-work · mcp confluence
|
|
460
|
+
published: v4 · 2026-07-28
|
|
461
|
+
---
|
|
462
|
+
|
|
463
|
+
# PM / CRM import wizard PRD
|
|
464
|
+
|
|
465
|
+
## Handoff copy
|
|
466
|
+
|
|
467
|
+
Published from planning/prd/crm-import-wizard-prd.md when spec completed. The repo file is the source of truth.
|
|
468
|
+
|
|
469
|
+
## Republish
|
|
470
|
+
|
|
471
|
+
pmos publish prd crm-import-wizard — overwrites this page with the current repo version.`;
|
|
472
|
+
Object.entries(KNOW).forEach(([id, k]) => {
|
|
473
|
+
out[k.path] = `---\npath: ${k.path}\nrole: DECISION\nstatus: ${k.badge}\n---\n\n# ${id}: ${k.d}\n\n## Decision\n\n${k.body}`;
|
|
474
|
+
});
|
|
475
|
+
out['planning/evals/evals.jsonl'] = `---\npath: planning/evals/evals.jsonl\nrole: EVALS\nformat: append-only jsonl\n---\n\n# evals.jsonl — scored runs\n\n## Entries\n\n- #114 · workspace-shadcn-design-system · PASS 9/10 · 2026-07-21\n- #113 · workspace-pm-spine · attempt aborted — contract missing · 2026-07-19\n- #112 · pmos-navigator-v1 · PASS 8/10 · 2026-07-14\n\n## Rule\n\nA verdict exists only if a line exists here. No line, no pass.`;
|
|
476
|
+
return out;
|
|
477
|
+
})();
|
|
478
|
+
|
|
479
|
+
/** rawFor (lines 1342–1349): saved edit → known doc → generated placeholder. */
|
|
480
|
+
export function rawFor(tab, savedDocs = {}) {
|
|
481
|
+
const saved = savedDocs[tab.path];
|
|
482
|
+
if (saved != null) return saved;
|
|
483
|
+
const known = RAW_DOCS[tab.path];
|
|
484
|
+
if (known) return known;
|
|
485
|
+
const base = tab.path.split('/').pop();
|
|
486
|
+
return `---\npath: ${tab.path}\nrole: ${tab.role}\ninitiative: ${tab.slug || '—'}\n---\n\n# ${base}\n\nPrototype preview — in the product this renders the real file from disk.\n\nIf the file were missing or unreadable, this pane would say so in red rather than showing an empty page.`;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/* ────────────────────────── modes + titles ────────────────────────── */
|
|
490
|
+
|
|
491
|
+
// MENU (line 1404) — computed by the prototype but never rendered; kept for parity.
|
|
492
|
+
export const MENU = [['product', '◧'], ['scheduled', '◔'], ['workflow', '▤'], ['skills', '◇'], ['knowledge', '◈'], ['agent', '◐'], ['memory', '◒'], ['connect', '⌁']];
|
|
493
|
+
|
|
494
|
+
// modeTitle (line 1409) — panel 2's heading per mode.
|
|
495
|
+
export const MODE_TITLES = { product: 'initiatives', scheduled: 'scheduled tasks', workflow: 'workflows', skills: 'skills', knowledge: 'knowledge', agent: 'agents', memory: 'memory', connect: 'connections', focus: 'focus' };
|
|
496
|
+
export const modeTitle = mode => MODE_TITLES[mode] || 'initiatives';
|
|
497
|
+
|
|
498
|
+
// modeMeta (line 2051) — glyph + label for the settings drawer entries.
|
|
499
|
+
export const MODE_META = { focus: ['◎', 'focus'], scheduled: ['◔', 'scheduled tasks'], workflow: ['▤', 'workflows'], skills: ['◇', 'skills'], knowledge: ['◈', 'knowledge'], agent: ['◐', 'agents'], memory: ['◒', 'memory'], connect: ['⌁', 'connections'] };
|
|
500
|
+
|
|
501
|
+
// settingsGroups (lines 2053–2056) — EXACTLY two groups, no `workflow` entry.
|
|
502
|
+
export const SETTINGS_GROUPS = [
|
|
503
|
+
{ label: 'this repository', items: ['agent', 'memory', 'knowledge'] },
|
|
504
|
+
{ label: 'all repositories', items: ['focus', 'skills', 'connect', 'scheduled'] }
|
|
505
|
+
];
|
|
506
|
+
|
|
507
|
+
// detailTitle (line 1699) — panel 3's heading per detail kind.
|
|
508
|
+
export const DETAIL_TITLES = { init: 'initiative', flow: 'workflow', sched: 'scheduled task', skill: 'skill', know: 'decision', agent: 'agent', mem: 'memory', prof: 'claude profile', intg: 'connection', addprod: 'add product', new: 'file new' };
|
|
509
|
+
export const detailTitle = kind => DETAIL_TITLES[kind] || 'detail';
|
|
510
|
+
|
|
511
|
+
/* ────────────────────────────── focus mode ────────────────────────────── */
|
|
512
|
+
|
|
513
|
+
// focusFirst / focusWaiting / focusSyn / focusSaw (lines 1544–1566).
|
|
514
|
+
export const FOCUS_FIRST = [
|
|
515
|
+
{ n: '1', slug: 'workspace-harness-tower', prod2: 'workspace', why: 'read failed — the board lies until the frontmatter is fixed', read: 'read: YAML error at line 3 · nothing derived' },
|
|
516
|
+
{ n: '2', slug: 'workspace-pm-spine', prod2: 'workspace', why: 'contract owed — agents build against nothing until you agree it', read: 'read: PRD · runs.jsonl — no contract line' },
|
|
517
|
+
{ n: '3', slug: 'crm-billing-sync', prod2: 'atlas-crm', why: 'stories 4/9 — the Jira handoff waits on you', read: 'read: PRD draft · stories file' },
|
|
518
|
+
{ n: '4', slug: 'workspace-doc-editor', prod2: 'workspace', why: '2 intake blockers — each names its fix', read: 'read: initiative record · KR WIP 3/3' }
|
|
519
|
+
];
|
|
520
|
+
export const FOCUS_WAITING = [
|
|
521
|
+
{ slug: 'crm-import-wizard', prod2: 'atlas-crm', why: 'app team builds — Jira ATL-482 · 3/7 stories done', read: 'synced 12:38 · nothing owed by you' }
|
|
522
|
+
];
|
|
523
|
+
// `action` replaces the prototype's inline handler; the mode module binds it.
|
|
524
|
+
export const FOCUS_SYN = [
|
|
525
|
+
{ t: 'sonnet-builder · spine build — running, no input needed', hasBtn: false },
|
|
526
|
+
{ t: 'haiku-scout can draft the billing-sync stories — you edit instead of write', hasBtn: true, btn: '▶ delegate', action: 'delegate-billing-stories' }
|
|
527
|
+
];
|
|
528
|
+
// The session the delegate button starts (line 1558).
|
|
529
|
+
export const DELEGATE_SESSION = { slug: 'crm-billing-sync', sess: { name: 'stories draft · haiku-scout', st: 'working', label: 'working', profile: 'claude-personal' } };
|
|
530
|
+
|
|
531
|
+
/** focusSaw (line 1563) — the unpinned count is read from live memory. */
|
|
532
|
+
export function focusSaw(memory) {
|
|
533
|
+
const unpinned = memory.long.filter(m => !m.pinned).length;
|
|
534
|
+
return [
|
|
535
|
+
{ t: 'groom long-term memory — ' + unpinned + ' unpinned entries may be compacted away', action: 'mode:memory' },
|
|
536
|
+
{ t: 'health budget — shadcn upkeep accepted 07-21 · 19% of budget spent', action: 'jump:workspace|workspace-shadcn-design-system' }
|
|
537
|
+
];
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/** focusEnds (line 1567) — one line per repository naming its first group. */
|
|
541
|
+
export function focusEnds(products) {
|
|
542
|
+
return products.map(([id]) => {
|
|
543
|
+
const g0 = (ALLGROUPS[id] || [])[0];
|
|
544
|
+
return g0 ? { prod: id, obj: g0.gid + ' · ' + g0.gname } : null;
|
|
545
|
+
}).filter(Boolean);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/* ──────────────────────── derivation: repos + board ──────────────────────── */
|
|
549
|
+
|
|
550
|
+
/** allProds (line 1431) — seed repositories plus anything the PM added. */
|
|
551
|
+
export function productsList(addedProducts = []) {
|
|
552
|
+
return [...PRODUCTS, ...addedProducts.map(x => [x.id, x.sub, x.kind || 'github'])];
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/** Repo row presentation fields (lines 1432–1438), minus the click handler. */
|
|
556
|
+
export function repoRow([id, sub, kind]) {
|
|
557
|
+
return {
|
|
558
|
+
name: id, sub, kind,
|
|
559
|
+
kindGlyph: kind === 'github' ? '⎇' : '▤',
|
|
560
|
+
kindLabel: kind === 'github' ? 'github' : 'local',
|
|
561
|
+
displaySub: (kind === 'github' ? 'github' : 'local') + ' · ' + sub
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/** The initiative selected when a repository is picked (line 1440). */
|
|
566
|
+
export function firstSlugOf(product) {
|
|
567
|
+
const g0 = (ALLGROUPS[product] || [])[0];
|
|
568
|
+
return g0 ? g0.slugs[0] : null;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/** DATA merged with user-filed initiatives (line 1392). */
|
|
572
|
+
export const allRecords = (userInits = {}) => ({ ...DATA, ...userInits });
|
|
573
|
+
|
|
574
|
+
/** One record (line 1701). */
|
|
575
|
+
export const recordFor = (slug, userInits = {}) => allRecords(userInits)[slug];
|
|
576
|
+
|
|
577
|
+
/** The default selection (line 1397). */
|
|
578
|
+
export const defaultSlug = product => (product === 'atlas-crm' ? 'crm-import-wizard' : 'workspace-pm-spine');
|
|
579
|
+
|
|
580
|
+
/** sessionsOf (line 1398) — authored sessions plus any started this session. */
|
|
581
|
+
export function sessionsOf(slug, { userInits = {}, extraSess = {} } = {}) {
|
|
582
|
+
const i = allRecords(userInits)[slug];
|
|
583
|
+
if (!i || i.readError) return [];
|
|
584
|
+
return [...(i.sessions || []), ...(extraSess[slug] || [])];
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/** runningCount (line 2050) — sessions that are working or need the PM. */
|
|
588
|
+
export function countRunning({ userInits = {}, extraSess = {} } = {}) {
|
|
589
|
+
let running = 0;
|
|
590
|
+
Object.keys(allRecords(userInits)).forEach(slug => {
|
|
591
|
+
sessionsOf(slug, { userInits, extraSess }).forEach(x => { if (x.st === 'working' || x.st === 'needs') running++; });
|
|
592
|
+
});
|
|
593
|
+
return running;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* GROUPS (lines 1425–1430) — the board's Objective/KR groups for a repository,
|
|
598
|
+
* with user-filed groups, user KRs and user-filed initiatives folded in.
|
|
599
|
+
*/
|
|
600
|
+
export function groupsFor(product, { userGroups = {}, userKrs = {}, addedInits = {} } = {}) {
|
|
601
|
+
const base = ALLGROUPS[product] || ALLGROUPS.workspace;
|
|
602
|
+
return [...base, ...(userGroups[product] || [])].map(g => {
|
|
603
|
+
const extra = addedInits[product + '|' + g.gid] || [];
|
|
604
|
+
const kr = userKrs[product + '|' + g.gid];
|
|
605
|
+
const slugs2 = [...g.slugs, ...extra];
|
|
606
|
+
return {
|
|
607
|
+
...g, slugs: slugs2,
|
|
608
|
+
...(kr ? { hasKr: true, krid: kr.krid, krname: kr.krname, krct: String(slugs2.length) } : {}),
|
|
609
|
+
gct: g.dyn ? String(slugs2.length) : g.gct
|
|
610
|
+
};
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/** Group name truncation (line 1456). */
|
|
615
|
+
export const shortName = gname => (gname.length > 34 ? gname.slice(0, 34).trim() + '…' : gname);
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Search-filtered groups (lines 1448–1458). Returns the surviving groups with
|
|
619
|
+
* their filtered slug list, plus the total number of initiative rows shown.
|
|
620
|
+
* `q` must already be lower-cased.
|
|
621
|
+
*/
|
|
622
|
+
export function visibleGroups(groups, q) {
|
|
623
|
+
let total = 0;
|
|
624
|
+
const out = groups.map(g => {
|
|
625
|
+
const slugs = g.slugs.filter(s => !q || s.includes(q));
|
|
626
|
+
total += slugs.length;
|
|
627
|
+
return { ...g, slugs, isEmpty: slugs.length === 0, gshort: shortName(g.gname) };
|
|
628
|
+
}).filter(g => g.slugs.length > 0 || (g.dyn && !q));
|
|
629
|
+
return { groups: out, total };
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* Search hits — "matches elsewhere" (lines 1459–1466). Handlers are returned as
|
|
634
|
+
* descriptors so this stays pure: `{type:'doc'|'term', …}`.
|
|
635
|
+
*/
|
|
636
|
+
export function searchHits(q, { userInits = {}, extraSess = {} } = {}) {
|
|
637
|
+
if (!q) return [];
|
|
638
|
+
const DATA_ALL = allRecords(userInits);
|
|
639
|
+
const hits = [];
|
|
640
|
+
Object.keys(DATA_ALL).forEach(slug => {
|
|
641
|
+
(DATA_ALL[slug].arts || []).forEach(a => {
|
|
642
|
+
if (a.path.toLowerCase().includes(q)) hits.push({ kind: 'artifact · ' + a.role, label: a.path, act: { type: 'doc', slug, path: a.path, role: a.role } });
|
|
643
|
+
});
|
|
644
|
+
sessionsOf(slug, { userInits, extraSess }).forEach(s => {
|
|
645
|
+
if (s.name.toLowerCase().includes(q)) hits.push({ kind: 'session · ' + slug, label: s.name + ' · ' + s.label, act: { type: 'term', slug, sess: s } });
|
|
646
|
+
});
|
|
647
|
+
});
|
|
648
|
+
return hits.slice(0, 8);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/** slugProduct (lines 1963–1965) — which repository owns each initiative slug. */
|
|
652
|
+
export function slugProductMap(addedInits = {}) {
|
|
653
|
+
const map = {};
|
|
654
|
+
Object.entries(ALLGROUPS).forEach(([prod, gs]) => (gs || []).forEach(g => (g.slugs || []).forEach(s => { map[s] = prod; })));
|
|
655
|
+
Object.entries(addedInits).forEach(([key, arr]) => { const prod = key.split('|')[0]; (arr || []).forEach(s => { map[s] = prod; }); });
|
|
656
|
+
return map;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/** flowUse (line 1480) — which initiatives use each workflow. */
|
|
660
|
+
export function flowUse(userInits = {}) {
|
|
661
|
+
const use = {};
|
|
662
|
+
Object.entries(allRecords(userInits)).forEach(([slug, i]) => { if (i.flow) (use[i.flow] = use[i.flow] || []).push(slug); });
|
|
663
|
+
return use;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/* ─────────────────── derivation: phases, tasks, detail ─────────────────── */
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* synthTasks (lines 1720–1734) — the tasks a phase gets when the record does
|
|
670
|
+
* not author any. Titles depend on the step's status; statuses and every field
|
|
671
|
+
* are derived, never invented per initiative.
|
|
672
|
+
*/
|
|
673
|
+
export function synthTasks(slug, k, s) {
|
|
674
|
+
const titles = {
|
|
675
|
+
done: ['read the inputs for ' + k, 'produce the ' + k + ' output', 'record the result'],
|
|
676
|
+
current: ['read the inputs for ' + k, 'produce the ' + k + ' output', 'record the result'],
|
|
677
|
+
blocked: ['resolve the ' + k + ' blocker', 'produce the ' + k + ' output'],
|
|
678
|
+
pending: ['read the inputs for ' + k, 'produce the ' + k + ' output']
|
|
679
|
+
}[s] || ['work the ' + k + ' step'];
|
|
680
|
+
return titles.map((title, idx) => {
|
|
681
|
+
let st = 'todo';
|
|
682
|
+
if (s === 'done') st = 'done';
|
|
683
|
+
else if (s === 'current') st = idx === 0 ? 'done' : (idx === 1 ? 'doing' : 'todo');
|
|
684
|
+
else if (s === 'blocked') st = idx === 0 ? 'blocked' : 'todo';
|
|
685
|
+
return {
|
|
686
|
+
title, status: st,
|
|
687
|
+
instruction: 'Run the ' + k + ' step for ' + slug + ' — ' + title + '. Read the referenced inputs, do the work, and write the phase output back to planning/.',
|
|
688
|
+
input: 'planning/initiatives/' + slug + '.md + prior phase outputs',
|
|
689
|
+
expect: 'A reviewable ' + k + ' artifact exists on disk so the phase can advance.',
|
|
690
|
+
evalNote: st === 'done' ? 'passed the ' + k + ' rubric' : 'graded against the ' + k + ' rubric before the phase advances',
|
|
691
|
+
output: st === 'done' ? 'planning/' + k + '/' + slug + '-' + k + '.md' : '—'
|
|
692
|
+
};
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/** Authored tasks win; otherwise synthesise (line 1737). */
|
|
697
|
+
export function tasksForPhase(record, step, slug) {
|
|
698
|
+
return (record.tasks && record.tasks[step.k]) ? record.tasks[step.k] : synthTasks(slug, step.k, step.s);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
/** Phase status from its tasks (lines 1738–1743). Order matters. */
|
|
702
|
+
export function phaseStatus(tasks) {
|
|
703
|
+
const doneN = tasks.filter(t => t.status === 'done').length;
|
|
704
|
+
if (tasks.some(t => t.status === 'blocked')) return 'blocked';
|
|
705
|
+
if (tasks.some(t => t.status === 'doing')) return 'doing';
|
|
706
|
+
if (doneN === tasks.length) return 'done';
|
|
707
|
+
if (doneN > 0) return 'doing';
|
|
708
|
+
return 'todo';
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/**
|
|
712
|
+
* phases (lines 1736–1756), without presentation. One entry per step:
|
|
713
|
+
* `{ key, step, tasks, doneN, status, count }`. The panel adds dots, carets and
|
|
714
|
+
* handlers — `TDOT[status]` gives the glyph + colour.
|
|
715
|
+
*/
|
|
716
|
+
export function phasesFor(record, slug) {
|
|
717
|
+
return (record.steps || []).map(s => {
|
|
718
|
+
const tasks = tasksForPhase(record, s, slug);
|
|
719
|
+
const doneN = tasks.filter(t => t.status === 'done').length;
|
|
720
|
+
return { key: s.k, label: s.k, step: s, tasks, doneN, status: phaseStatus(tasks), count: doneN + '/' + tasks.length };
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/** Task tab id (line 1750). */
|
|
725
|
+
export const taskTabId = (slug, phaseKey, idx) => 'task:' + slug + ':' + phaseKey + '#' + idx;
|
|
726
|
+
/** Terminal tab id (line 1399). */
|
|
727
|
+
export const termTabId = (slug, sessName) => 'term:' + slug + ':' + sessName;
|
|
728
|
+
/** Doc tab id (line 1400). */
|
|
729
|
+
export const docTabId = path => 'doc:' + path;
|
|
730
|
+
|
|
731
|
+
/** The step the answer line points at (line 1707): first blocked, else current. */
|
|
732
|
+
export function targetStepIndex(steps) {
|
|
733
|
+
let ti = steps.findIndex(r => r.s === 'blocked');
|
|
734
|
+
if (ti < 0) ti = steps.findIndex(r => r.s === 'current');
|
|
735
|
+
return ti;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/**
|
|
739
|
+
* Footer line (line 1805) / read-error footer (line 1704).
|
|
740
|
+
*
|
|
741
|
+
* `asOf` defaults to the prototype's literal so the fixture render stays byte-identical to the
|
|
742
|
+
* design. In REAL mode the caller passes the time the record was READ — stamped in data.js when
|
|
743
|
+
* `/api/initiative` answered — because this is the PROVENANCE line, the one place on the panel whose
|
|
744
|
+
* whole job is to say where the reading came from and when. Shipping a hard-coded 12:41:03 there
|
|
745
|
+
* stated a time that was never measured, on the line least entitled to do so. The same principle the
|
|
746
|
+
* PRD applied to the sync panel: identical to the design in shape, never in a claim the app cannot
|
|
747
|
+
* back.
|
|
748
|
+
*
|
|
749
|
+
* `null` is a THIRD case, distinct from both: a real record the app has not hydrated yet. It has no
|
|
750
|
+
* read time, so the line reports that instead of falling back to the design's literal — which is how
|
|
751
|
+
* a fabricated timestamp would get back onto the screen through the default argument.
|
|
752
|
+
*/
|
|
753
|
+
export function branchLine(record, asOf = '12:41:03') {
|
|
754
|
+
const when = asOf ? `as of ${asOf}` : 'not read yet';
|
|
755
|
+
if (record && record.readError) return `read failed · ${when}`;
|
|
756
|
+
return record && record.branch ? `derived on ${record.branch} · ${when}` : `no working tree · ${when}`;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
/** Eval row (lines 1773–1774) — no verdict is not a pass. */
|
|
760
|
+
export function evalLine(record) {
|
|
761
|
+
return {
|
|
762
|
+
color: record.eval && record.eval.pass ? C.suc : C.wrn,
|
|
763
|
+
text: record.eval ? record.eval.text : 'no verdict recorded — this is not a pass'
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
/** Default sessions note (line 1777). */
|
|
768
|
+
export const noSessionsText = record => record.noSess || 'No sessions on this initiative — normal, not an error.';
|
|
769
|
+
|
|
770
|
+
/* ────────────────────── derivation: links + artifacts ────────────────────── */
|
|
771
|
+
|
|
772
|
+
/** Artifact key (line 1779). */
|
|
773
|
+
export const artKey = (slug, path) => slug + '|' + path;
|
|
774
|
+
|
|
775
|
+
/** linkArt (lines 1337–1341) — the destination a link records. */
|
|
776
|
+
export function linkArtDest(key, kind) {
|
|
777
|
+
const base = key.split('|')[1].split('/').pop().replace('.md', '');
|
|
778
|
+
const target = kind === 'confluence' ? 'PM / ' + base : kind === 'jira' ? 'ATL epic · ' + base : base;
|
|
779
|
+
return { kind: kind === 'google' ? 'google doc' : kind, target };
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
/** The three sources an artifact can be linked to (line 1786). */
|
|
783
|
+
export const ART_LINK_SRCS = [{ l: 'Confluence', kind: 'confluence' }, { l: 'Jira', kind: 'jira' }, { l: 'Google Doc', kind: 'google' }];
|
|
784
|
+
/** The three sources a reference link can come from (line 1798). */
|
|
785
|
+
export const LINK_SRCS = [{ l: 'Jira epic', src: 'jira' }, { l: 'Confluence', src: 'confluence' }, { l: 'Google Doc', src: 'google' }];
|
|
786
|
+
|
|
787
|
+
/** The sync panel's difference summary (line 1790). */
|
|
788
|
+
export const artDiffText = dest => 'repo file is the source. read ' + (dest ? dest.kind : '') + ' — 2 sections differ (Acceptance Criteria, Rollout). Nothing written yet.';
|
|
789
|
+
/** The two approval outcomes (lines 1791–1792). */
|
|
790
|
+
export const artSyncPushed = dest => ({ color: 'var(--success)', text: 'pushed · repo → ' + dest.kind + ' · in sync 12:41' });
|
|
791
|
+
export const artSyncPulled = dest => ({ color: 'var(--warning)', text: 'pulled · ' + dest.kind + ' → repo · file updated' });
|
|
792
|
+
|
|
793
|
+
/** addLink (lines 1757–1766) — the link a source button appends. */
|
|
794
|
+
export function makeLink(slug, src, existing = []) {
|
|
795
|
+
if (src === 'jira') {
|
|
796
|
+
const num = 501 + existing.filter(x => x.kind === 'jira epic').length;
|
|
797
|
+
return { kind: 'jira epic', label: 'ATL-' + num + ' · ' + slug, path: 'jira://ATL-' + num, st: 'connected' };
|
|
798
|
+
}
|
|
799
|
+
if (src === 'confluence') return { kind: 'confluence', label: 'PM / ' + slug, path: 'confluence://PM/' + slug, st: 'page' };
|
|
800
|
+
return { kind: 'google doc', label: slug + ' brief', path: 'gdoc://' + slug + '-brief', st: 'doc' };
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/* ────────────────────────── profile note strings ────────────────────────── */
|
|
804
|
+
|
|
805
|
+
// panel 3 start-a-session note (line 1800)
|
|
806
|
+
export const sessionProfileNote = id => (id === 'claude-work'
|
|
807
|
+
? 'claude-work — workspace MCP live: jira · confluence · slack · google-workspace'
|
|
808
|
+
: 'claude-personal — no workspace MCP; the session cannot touch Jira or Confluence');
|
|
809
|
+
// scheduled-task note (line 1844)
|
|
810
|
+
export const schedProfileNote = id => ((id || 'claude-work') === 'claude-work'
|
|
811
|
+
? 'runs with workspace MCP — can read Jira and Confluence'
|
|
812
|
+
: 'runs without workspace MCP — local files only');
|
|
813
|
+
// agent note (line 1892)
|
|
814
|
+
export const agentProfileNote = id => ((id || 'claude-work') === 'claude-work'
|
|
815
|
+
? 'claude-work — this agent reaches Jira, Confluence, Slack, Google Workspace over MCP'
|
|
816
|
+
: 'claude-personal — no workspace MCP; this agent cannot touch Jira or Confluence');
|
|
817
|
+
// memory note (lines 1918–1920)
|
|
818
|
+
export const memoryNote = scope => (scope === 'short'
|
|
819
|
+
? 'Dies when its session exits. Promote it to keep it — the agent will see it in every future run.'
|
|
820
|
+
: 'Injected into every agent run. Pinned entries survive compaction; unpinned ones may be summarized away.');
|
|
821
|
+
export const memoryPath = scope => (scope === 'short' ? 'runtime — session context' : 'memory/long-term.jsonl');
|
|
822
|
+
|
|
823
|
+
/* ─────────────────────── the faux shell (fixture only) ─────────────────────── */
|
|
824
|
+
|
|
825
|
+
/** The five lines a terminal pane opens with (lines 1983–1989). */
|
|
826
|
+
export function termBaseLines(tab) {
|
|
827
|
+
return [
|
|
828
|
+
{ t: 'root@vm:~/pmos$ pmos attach --initiative ' + tab.slug + ' --session "' + tab.sess + '"', c: TERM_COLORS.prompt },
|
|
829
|
+
{ t: 'attached · state: ' + (tab.state || 'working') + ' · scrollback restored', c: TERM_COLORS.dim },
|
|
830
|
+
{ t: 'profile: ' + (tab.profile || 'claude-work') + (tab.profile === 'claude-personal' ? ' · mcp: none' : ' · mcp: jira confluence slack gworkspace'), c: TERM_COLORS.dim },
|
|
831
|
+
{ t: 'agent: reading planning/prd/' + tab.slug + '-prd.md', c: TERM_COLORS.text },
|
|
832
|
+
{ t: 'agent: working — last output 41s ago', c: TERM_COLORS.text }
|
|
833
|
+
];
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
/**
|
|
837
|
+
* The fake-shell command table (lines 1996–2004). Returns the reply string, or
|
|
838
|
+
* null for `clear` (which empties the history instead of printing).
|
|
839
|
+
*/
|
|
840
|
+
export function fauxShellReply(v, tab) {
|
|
841
|
+
return v === 'help'
|
|
842
|
+
? 'commands: pmos status · pmos profile · pmos run <skill> · pmos attach · ls planning/ · clear'
|
|
843
|
+
: v === 'clear' ? null
|
|
844
|
+
: v === 'pmos profile' ? 'profile ' + (tab.profile || 'claude-work') + (tab.profile === 'claude-personal' ? ' · mcp: none — pick a profile when starting a session, not mid-session' : ' · mcp: jira confluence slack gworkspace')
|
|
845
|
+
: v === 'pmos status' ? 'initiative ' + tab.slug + ' · session "' + tab.sess + '" · ' + (tab.state || 'working')
|
|
846
|
+
: v.startsWith('pmos run ') ? 'running ' + v.slice(9) + ' … ok — logged to planning/runs/agent-runs.jsonl'
|
|
847
|
+
: v.startsWith('ls') ? 'initiatives/ prd/ evals/ runs/ decisions/ workflows/ scheduled/'
|
|
848
|
+
: v.startsWith('pmos') ? 'pmos: ok — logged to planning/runs/agent-runs.jsonl'
|
|
849
|
+
: 'sh: ' + v.split(' ')[0] + ': wired to a fake shell in this prototype';
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
/** The echoed prompt line for a typed command (line 2005). */
|
|
853
|
+
export const termEcho = v => ({ t: 'root@vm:~/pmos$ ' + v, c: TERM_COLORS.prompt });
|
|
854
|
+
/** Scratchpad → terminal paste (lines 2107–2108). */
|
|
855
|
+
export function scratchLines(text) {
|
|
856
|
+
const lines = text.split('\n').filter(Boolean).map(l => ({ t: 'root@vm:~/pmos$ ' + l, c: TERM_COLORS.prompt }));
|
|
857
|
+
return [...lines, { t: 'pmos: ' + lines.length + ' line' + (lines.length > 1 ? 's' : '') + ' pasted from scratchpad', c: TERM_COLORS.dim }];
|
|
858
|
+
}
|
|
859
|
+
/** The copy payload for a terminal tab (line 2114). */
|
|
860
|
+
export const termCopyText = tab => 'pmos attach --initiative ' + tab.slug;
|
|
861
|
+
|
|
862
|
+
/* ────────────────────────── markdown + diff ────────────────────────── */
|
|
863
|
+
|
|
864
|
+
/** mdParse (lines 1350–1365) — frontmatter, H1 title, H2 blocks, lists, paras. */
|
|
865
|
+
export function mdParse(md) {
|
|
866
|
+
const lines = md.split('\n'); let i = 0; const fm = [];
|
|
867
|
+
if (lines[0] === '---') {
|
|
868
|
+
i = 1;
|
|
869
|
+
while (i < lines.length && lines[i] !== '---') {
|
|
870
|
+
const ix = lines[i].indexOf(':');
|
|
871
|
+
if (ix > 0) fm.push({ k: lines[i].slice(0, ix).trim(), v: lines[i].slice(ix + 1).trim() });
|
|
872
|
+
i++;
|
|
873
|
+
}
|
|
874
|
+
i++;
|
|
875
|
+
}
|
|
876
|
+
let title = ''; const blocks = []; let list = null, para = [];
|
|
877
|
+
const fp = () => { if (para.length) { blocks.push({ isP: true, p: para.join(' ') }); para = []; } };
|
|
878
|
+
const fl = () => { if (list) { blocks.push({ isList: true, items: list }); list = null; } };
|
|
879
|
+
for (; i < lines.length; i++) {
|
|
880
|
+
const l = lines[i];
|
|
881
|
+
if (l.startsWith('# ')) { fp(); fl(); title = l.slice(2); }
|
|
882
|
+
else if (l.startsWith('## ')) { fp(); fl(); blocks.push({ isH: true, h: l.slice(3) }); }
|
|
883
|
+
else if (l.startsWith('- ')) { fp(); if (!list) list = []; list.push(l.slice(2)); }
|
|
884
|
+
else if (!l.trim()) { fp(); fl(); }
|
|
885
|
+
else para.push(l);
|
|
886
|
+
}
|
|
887
|
+
fp(); fl();
|
|
888
|
+
return { title, fm, blocks };
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
/** diffLines (lines 1366–1379) — LCS line diff, coloured rows. */
|
|
892
|
+
export function diffLines(a, b) {
|
|
893
|
+
const A = a.split('\n'), B = b.split('\n'), n = A.length, m = B.length;
|
|
894
|
+
const L = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
|
|
895
|
+
for (let i = n - 1; i >= 0; i--) for (let j = m - 1; j >= 0; j--) L[i][j] = A[i] === B[j] ? L[i + 1][j + 1] + 1 : Math.max(L[i + 1][j], L[i][j + 1]);
|
|
896
|
+
const out = []; let i = 0, j = 0;
|
|
897
|
+
const rem = t => out.push({ t: '− ' + t, c: 'var(--destructive)', bg: 'color-mix(in oklab, var(--destructive) 9%, transparent)' });
|
|
898
|
+
const add = t => out.push({ t: '+ ' + t, c: 'var(--success)', bg: 'color-mix(in oklab, var(--success) 9%, transparent)' });
|
|
899
|
+
while (i < n && j < m) {
|
|
900
|
+
if (A[i] === B[j]) { out.push({ t: ' ' + (A[i] || ' '), c: 'var(--muted-foreground)', bg: 'transparent' }); i++; j++; }
|
|
901
|
+
else if (L[i + 1][j] >= L[i][j + 1]) { rem(A[i]); i++; } else { add(B[j]); j++; }
|
|
902
|
+
}
|
|
903
|
+
while (i < n) { rem(A[i]); i++; }
|
|
904
|
+
while (j < m) { add(B[j]); j++; }
|
|
905
|
+
return out;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
/** The row a clean diff shows (line 2037). */
|
|
909
|
+
export const NO_DIFF_ROW = { t: 'no changes — edit the file to see a diff', c: 'var(--muted-foreground)', bg: 'transparent' };
|
|
910
|
+
|
|
911
|
+
/** Raw-mode line colouring (line 2034). */
|
|
912
|
+
export function rawLine(l) {
|
|
913
|
+
return {
|
|
914
|
+
t: l || ' ',
|
|
915
|
+
c: l.startsWith('#') ? 'var(--brand)' : (l.startsWith('---') || l.includes(': ') && !l.startsWith('- ') ? 'var(--muted-foreground)' : (l.startsWith('- ') ? 'var(--foreground)' : 'var(--muted-foreground)')),
|
|
916
|
+
w: l.startsWith('#') ? '600' : '400'
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
/* ──────────────────── derivation: a newly filed initiative ──────────────────── */
|
|
921
|
+
|
|
922
|
+
/**
|
|
923
|
+
* The record the "file new · initiative" form synthesises (lines 1659–1670).
|
|
924
|
+
* `group` is the anchor group (undefined when unanchored); `flowsMap` is the
|
|
925
|
+
* live workflow map so a custom flow produces the right stage list.
|
|
926
|
+
*/
|
|
927
|
+
export function newInitiativeRecord({ slug, anchored, group, flow, flowsMap = FLOWS }) {
|
|
928
|
+
const stages = flowsMap[flow] || flowsMap['adlc-feature'];
|
|
929
|
+
return {
|
|
930
|
+
tag: anchored ? 'Filed just now — nothing else derived yet' : 'Rejected under D59 — unanchored work',
|
|
931
|
+
stage: 'intake', flow, branch: null,
|
|
932
|
+
pills: anchored
|
|
933
|
+
? [{ t: 'feature', c: C.pur }, { t: group ? (group.hasKr ? group.gid + ':' + group.krid : group.gid).toLowerCase() : '', c: C.mut }, { t: 'flow ' + flow, c: C.inf }]
|
|
934
|
+
: [{ t: 'unanchored', c: C.des }],
|
|
935
|
+
answer: anchored
|
|
936
|
+
? { tone: 'info', text: 'Intake is clean — file the spec next (run write-prd).' }
|
|
937
|
+
: { tone: 'bad', text: 'Anchor it: parent_kr is empty — unanchored work is rejected under D59' },
|
|
938
|
+
steps: stages.map((k, i) => i === 0
|
|
939
|
+
? {
|
|
940
|
+
k, s: anchored ? 'current' : 'blocked',
|
|
941
|
+
t: anchored ? 'filed just now — a successful read of almost nothing' : '✗ rejected — no resolvable anchor',
|
|
942
|
+
action: anchored ? 'file the spec — run write-prd' : 'set parent_kr in planning/initiatives/' + slug + '.md',
|
|
943
|
+
cmd: 'planning/initiatives/' + slug + '.md'
|
|
944
|
+
}
|
|
945
|
+
: { k, s: 'pending', t: '' }),
|
|
946
|
+
sessions: [], noSess: anchored ? 'No sessions yet — normal for fresh work.' : 'No sessions — rejected work cannot start one.',
|
|
947
|
+
arts: [{ role: 'DOC', path: 'planning/initiatives/' + slug + '.md' }], eval: null,
|
|
948
|
+
l2: anchored ? ['intake', 'filed just now', C.mut] : ['intake', 'rejected · no anchor', C.des]
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
/** File-new form: the anchor row shown for "no anchor" (line 1629). */
|
|
953
|
+
export const NO_ANCHOR_ROW = { label: 'none', sub: 'no anchor — rejected under D59' };
|
|
954
|
+
/** File-new: the create button label per type (line 1644). */
|
|
955
|
+
export const CREATE_LABELS = { obj: '+ file objective', kr: '+ file key result', init: '+ file initiative' };
|
|
956
|
+
/** File-new: the type chips (line 1637). */
|
|
957
|
+
export const NEW_TYPES = [['obj', 'objective'], ['kr', 'key result'], ['init', 'initiative']];
|
|
958
|
+
/** Add-product: the source chips (line 1577). */
|
|
959
|
+
export const ADD_SOURCES = [['github', 'github repo'], ['local', 'local folder']];
|
|
960
|
+
|
|
961
|
+
/* ────────────────────────────── the bundle ────────────────────────────── */
|
|
962
|
+
|
|
963
|
+
/**
|
|
964
|
+
* FIXTURE — every piece of seed data in one object, for `data.js` to normalise
|
|
965
|
+
* and for fixture mode to load. The derivation helpers above are separate named
|
|
966
|
+
* exports; they are needed in normal mode too.
|
|
967
|
+
*/
|
|
968
|
+
export const FIXTURE = {
|
|
969
|
+
C, DOT, SDOT, TDOT, TONE,
|
|
970
|
+
FLOWS, PROFILES, INTG, INTG_PREVIEWS, SKILLS, KNOW, INGESTED_KNOW,
|
|
971
|
+
PRODUCTS, ADDABLE_PRODUCTS, ADD_DEFAULTS, ALLGROUPS, DATA, RAW_DOCS,
|
|
972
|
+
MENU, MODE_META, MODE_TITLES, SETTINGS_GROUPS, DETAIL_TITLES,
|
|
973
|
+
FOCUS_FIRST, FOCUS_WAITING, FOCUS_SYN, DELEGATE_SESSION,
|
|
974
|
+
TAB_COLORS, TERM_COLORS, SCRATCH_SEND_COLOR,
|
|
975
|
+
sched: SCHED_SEED, agents: AGENTS_SEED, memory: MEMORY_SEED,
|
|
976
|
+
defaultSched, defaultAgents, defaultMemory
|
|
977
|
+
};
|
|
978
|
+
|
|
979
|
+
export default FIXTURE;
|