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.
Files changed (36) hide show
  1. package/README.md +38 -0
  2. package/package.json +1 -1
  3. package/payload/KIT-VERSION +1 -1
  4. package/workspace/README.md +44 -12
  5. package/workspace/design-parity.js +358 -0
  6. package/workspace/lib/find-browser.js +60 -0
  7. package/workspace/lib/four-panel.js +345 -0
  8. package/workspace/lib/git-info.js +28 -1
  9. package/workspace/lib/mobile-nav.js +32 -2
  10. package/workspace/package-lock.json +29 -2
  11. package/workspace/package.json +13 -5
  12. package/workspace/public/index.html +79 -409
  13. package/workspace/public/pm/app.js +242 -0
  14. package/workspace/public/pm/data.js +877 -0
  15. package/workspace/public/pm/dom.js +116 -0
  16. package/workspace/public/pm/fixture.js +979 -0
  17. package/workspace/public/pm/mobile.css +200 -0
  18. package/workspace/public/pm/mobile.js +403 -0
  19. package/workspace/public/pm/modes.js +1076 -0
  20. package/workspace/public/pm/panel-detail.js +648 -0
  21. package/workspace/public/pm/panel-initiatives.js +426 -0
  22. package/workspace/public/pm/panel-repos.js +460 -0
  23. package/workspace/public/pm/panel-tabs.js +487 -0
  24. package/workspace/public/pm/state.js +164 -0
  25. package/workspace/public/pm/tab-doc.js +279 -0
  26. package/workspace/public/pm/tab-task.js +83 -0
  27. package/workspace/public/pm/tab-term.js +560 -0
  28. package/workspace/public/pm/tokens.css +73 -0
  29. package/workspace/public/vendor/ibm-plex-mono-500.woff2 +0 -0
  30. package/workspace/server.js +158 -8
  31. package/workspace/focus.test.js +0 -650
  32. package/workspace/handoff.test.js +0 -443
  33. package/workspace/profiles.test.js +0 -362
  34. package/workspace/public/app.js +0 -5560
  35. package/workspace/public/styles.css +0 -2258
  36. package/workspace/repo-scan.test.js +0 -327
@@ -0,0 +1,1076 @@
1
+ /* pm/modes.js — the seven secondary modes behind panel 1's "settings & tools" drawer.
2
+ *
3
+ * A PORT of the prototype's non-`product` branches: the panel-2 lists (template lines 130–178 and
4
+ * 227–320) and the panel-3 bodies (template lines 450–663, 771–835), with their derivations
5
+ * (lines 1467–1570, 1683–1697, 1809–1961). Every inline `style` string below is copied from that
6
+ * file character for character and every literal is the prototype's literal — the acceptance bar
7
+ * is a mechanical diff, so a reworded label or a tidied padding is a defect, not an improvement.
8
+ *
9
+ * ── What "mode" means here ──────────────────────────────────────────────────────────────────
10
+ * `state.mode` switches panels 2 AND 3 together. The drawer offers exactly seven:
11
+ *
12
+ * this repository agent · memory · knowledge
13
+ * all repositories focus · skills · connect · scheduled
14
+ *
15
+ * `workflow` is an eighth mode that exists in the prototype's code and is deliberately absent
16
+ * from the drawer (a workflow is decided by the agent at runtime, not configured — PRD, panel 1).
17
+ * Its list and its `flow` detail are ported anyway, because they are reachable programmatically
18
+ * and a half-ported mode is a trap for whoever re-enables it.
19
+ *
20
+ * ── The two entry points, and who owns what ─────────────────────────────────────────────────
21
+ * renderMode(hostList, hostDetail, mode) — panel 2's body for `mode`, APPENDED to `hostList`.
22
+ * panel-initiatives.js has already put its sticky
23
+ * header and its search box there and every mode
24
+ * keeps both, so this appends and never clears.
25
+ * `hostDetail` is optional and normally null: panel 3
26
+ * is painted by its own owner, which asks this module
27
+ * for a body through `detailBody`.
28
+ * detailBody(kind, detail) — the nodes panel 3 puts BELOW its sticky header for
29
+ * a secondary detail kind (`flow · sched · skill ·
30
+ * know · agent · mem · prof · intg`).
31
+ *
32
+ * ── Where the data comes from ───────────────────────────────────────────────────────────────
33
+ * The four editable lists — workflows, scheduled tasks, agents, memory — live on the state keys
34
+ * the prototype used (`flowsMap`, `schedList`, `agentList`, `memory`), each `null` until edited.
35
+ * `null` resolves to the FIXTURE seed in fixture mode and to an EMPTY list otherwise: seed rows
36
+ * rendering in the real app is "the beautiful shell" the PRD names as a failure mode. The data
37
+ * layer publishes real values into those same keys, plus `skills`, `know`, `profiles` and
38
+ * `intgs`; until it does, a mode renders its section labels and one honest italic line rather
39
+ * than a blank panel. In fixture mode every list is non-empty, so that line never renders and the
40
+ * comparator sees the prototype's markup exactly.
41
+ *
42
+ * No network, no derived state cached (D33): every list, count, colour and active row below is
43
+ * computed at read time, on every render.
44
+ */
45
+
46
+ import { el, setChildren } from './dom.js';
47
+ import { state, setState, FIXTURE } from './state.js';
48
+ import * as FX from './fixture.js';
49
+ import { openDoc, openTerm } from './panel-initiatives.js';
50
+
51
+ /* ── the prototype's small helpers ─────────────────────────────────────────────────────────── */
52
+
53
+ /** `{{ expr }}` — an interpolated value, wrapped the way the prototype's runtime wraps it. */
54
+ const iv = (v) => el('span', null, v === null || v === undefined ? '' : String(v));
55
+
56
+ /** selRow (line 1402) — an active row is washed and gets the 2px brand left border. */
57
+ const selRow = (on) => ({
58
+ bg: on ? 'var(--accent)' : 'transparent',
59
+ bl: on ? 'var(--brand)' : 'transparent',
60
+ });
61
+
62
+ /** setDetail (line 1401) — open panel 3 on a detail kind, un-collapsing it if the PM folded it. */
63
+ const setDetail = (d) => setState({ detail: d, col2: true });
64
+
65
+ /** The detail descriptor, never undefined. */
66
+ const detOf = () => state.detail || { kind: 'init' };
67
+
68
+ const C = FX.C;
69
+
70
+ /* ── style constants that repeat verbatim across modes ─────────────────────────────────────── */
71
+
72
+ // Section label, 9px top padding (focus's five, connections' second, memory's second).
73
+ const LABEL9 = 'padding:9px 14px 4px;font-family:var(--mono);font-size:10px;letter-spacing:.12em;text-transform:uppercase;color:var(--muted-foreground);border-bottom:1px solid var(--border)';
74
+ // Section label, 7px top padding (knowledge groups, connections' first, memory's first).
75
+ const LABEL7 = 'padding:7px 14px 4px;font-family:var(--mono);font-size:10px;letter-spacing:.12em;text-transform:uppercase;color:var(--muted-foreground);border-bottom:1px solid var(--border)';
76
+ // The shared secondary button. Panel 3 uses it bare; panel 2's list actions add the margin.
77
+ const SEC_BTN = 'padding:5px 12px;min-height:30px;background:var(--secondary);border:1px solid var(--border);font-size:12px';
78
+ const ACTION_BTN = 'margin:10px 14px;' + SEC_BTN;
79
+ const ACTION_HOVER = 'border-color:var(--ring)';
80
+ // A list row: 8px padding (skills, knowledge, agents, connections, workflows) …
81
+ const row8 = (r) => 'display:block;width:100%;padding:8px 14px;min-height:44px;border-bottom:1px solid oklch(1 0 0 / 5%);border-left:2px solid ' + r.bl + ';background:' + r.bg;
82
+ // … and 7px padding (scheduled, focus queue, memory).
83
+ const row7 = (r) => 'display:block;width:100%;padding:7px 14px;min-height:44px;border-bottom:1px solid oklch(1 0 0 / 5%);border-left:2px solid ' + r.bl + ';background:' + r.bg;
84
+ const ROW_HOVER = 'background:var(--accent)';
85
+ // The prototype's brand-filled button (▶ run now, ▶ delegate, ↥ promote …).
86
+ const BRAND_BTN = 'padding:5px 12px;min-height:30px;background:var(--brand);color:oklch(0.987 0.002 197.1);font-size:12px;font-weight:500';
87
+ const DESTRUCTIVE_BTN = 'padding:5px 12px;min-height:30px;border:1px solid var(--destructive);color:var(--destructive);font-size:12px';
88
+ const DESTRUCTIVE_HOVER = 'background:color-mix(in oklab, var(--destructive) 10%, transparent)';
89
+ // The two-column field grid every secondary detail uses; `pt` is the label's top padding.
90
+ const FIELD_ROW = 'display:grid;grid-template-columns:76px 1fr;gap:10px;padding:8px 14px;border-bottom:1px solid var(--border)';
91
+ const FIELD_ROW10 = 'display:grid;grid-template-columns:76px 1fr;gap:10px;padding:10px 14px;border-bottom:1px solid var(--border)';
92
+ const fieldLabel = (pt) => 'font-family:var(--mono);font-size:9.5px;letter-spacing:.12em;text-transform:uppercase;color:var(--muted-foreground);padding-top:' + pt + 'px';
93
+ const SECTION_LABEL = 'padding:10px 14px 4px;font-family:var(--mono);font-size:9.5px;letter-spacing:.12em;text-transform:uppercase;color:var(--muted-foreground)';
94
+ const FOOTER = 'margin-top:auto;padding:8px 14px;border-top:1px solid var(--border);font-family:var(--mono);font-size:10px;color:var(--muted-foreground)';
95
+ const HEAD_BLOCK = 'padding:13px 14px 11px;border-bottom:1px solid var(--border)';
96
+ const HEAD_NAME = 'font-family:var(--mono);font-size:15px;font-weight:600';
97
+ const HEAD_SUB = 'font-size:12px;color:var(--muted-foreground);margin-top:2px';
98
+ const ITALIC_EMPTY = 'font-size:11.5px;color:var(--muted-foreground);font-style:italic';
99
+ const PILL = 'font-family:var(--mono);font-size:10px;padding:2px 6px;background:var(--secondary);border:1px solid var(--border)';
100
+
101
+ /**
102
+ * The one line a mode shows when its list is genuinely empty. It is the prototype's own copy for
103
+ * "nothing is filed" — a read that succeeded and found nothing is not an error, and it is not a
104
+ * spinner either. In fixture mode no list is empty, so this never renders into the diff.
105
+ */
106
+ const emptyLine = () => el('div', {
107
+ style: 'padding:5px 14px 6px;font-size:11px;color:var(--muted-foreground);font-style:italic',
108
+ }, 'nothing filed yet — a successful read of nothing');
109
+
110
+ /* ── the state-backed lists ────────────────────────────────────────────────────────────────── */
111
+
112
+ const seedOr = (seed, empty) => (FIXTURE ? seed() : empty);
113
+
114
+ /** flowsMap() (line 1054). */
115
+ const flowsMap = () => state.flowsMap || seedOr(() => FX.FLOWS, {});
116
+ /** schedList() (lines 1055–1061). */
117
+ const schedList = () => state.schedList || seedOr(FX.defaultSched, []);
118
+ /** agentList() (lines 1062–1067). */
119
+ const agentList = () => state.agentList || seedOr(FX.defaultAgents, []);
120
+ /** memory() (lines 1068–1081). */
121
+ const memoryOf = () => state.memory || seedOr(FX.defaultMemory, { short: [], long: [] });
122
+ /** SKILLS (lines 1491–1496). */
123
+ const skillList = () => state.skills || seedOr(() => FX.SKILLS, []);
124
+ /** KNOW (line 1502) — the base decisions with anything a repo ingest proposed layered on. */
125
+ const knowMap = () => ({ ...(state.know || seedOr(() => FX.KNOW, {})), ...(state.extraKnow || {}) });
126
+ /** PROFILES / INTG (line 1684). */
127
+ const profileList = () => state.profiles || seedOr(() => FX.PROFILES, []);
128
+ const intgList = () => state.intgs || seedOr(() => FX.INTG, []);
129
+
130
+ /* ── the board, read the way panels 2 and 3 read it ────────────────────────────────────────── */
131
+
132
+ const boardOf = () =>
133
+ (state.board && typeof state.board === 'object' ? state.board : (FIXTURE ? FX.ALLGROUPS : {}));
134
+
135
+ function records() {
136
+ const base = state.records && typeof state.records === 'object'
137
+ ? state.records
138
+ : (FIXTURE ? FX.DATA : {});
139
+ return { ...base, ...state.userInits };
140
+ }
141
+
142
+ function currentProduct() {
143
+ if (state.product) return state.product;
144
+ const keys = Object.keys(boardOf());
145
+ return keys.length ? keys[0] : 'workspace';
146
+ }
147
+
148
+ /** The initiative a mode acts on when it starts a session (line 1397). */
149
+ function selectedSlug() {
150
+ if (state.sel !== null && state.sel !== undefined) return state.sel;
151
+ const board = boardOf();
152
+ const g0 = (board[currentProduct()] || [])[0];
153
+ return g0 && g0.slugs && g0.slugs.length ? g0.slugs[0] : null;
154
+ }
155
+
156
+ /** allProds (line 1431), as far as this module needs it — focus's objective lines. */
157
+ function productTuples() {
158
+ if (FIXTURE && !state.board) return FX.productsList(state.addedProducts || []);
159
+ const added = (state.addedProducts || []).map((x) => [x.id, x.sub, x.kind || 'github']);
160
+ return [...Object.keys(boardOf()).map((id) => [id, '', 'github']), ...added];
161
+ }
162
+
163
+ /** jump (line 1542) — pick a repository AND an initiative, without leaving the current mode. */
164
+ const jump = (prod2, slug) => setState({ product: prod2, sel: slug, detail: { kind: 'init' }, col2: true });
165
+
166
+ /** Start a session against an initiative and open its terminal tab (lines 1558–1559, 1863–1864). */
167
+ function startSession(slug, sess) {
168
+ if (!slug) return;
169
+ setState(
170
+ (s) => ({ extraSess: { ...s.extraSess, [slug]: [...(s.extraSess[slug] || []), sess] } }),
171
+ () => openTerm(slug, sess),
172
+ );
173
+ }
174
+
175
+ /** profChips (lines 1685–1689) — the claude-work / claude-personal picker, used by 2 details. */
176
+ function profChips(cur, onPick) {
177
+ return profileList().map((x) => ({
178
+ l: x.id.replace('claude-', ''),
179
+ on: () => onPick(x.id),
180
+ bg: cur === x.id ? 'var(--secondary)' : 'transparent',
181
+ bc: cur === x.id ? 'var(--input)' : 'var(--border)',
182
+ color: cur === x.id ? 'var(--foreground)' : 'var(--muted-foreground)',
183
+ }));
184
+ }
185
+
186
+ function profChipNode(pb) {
187
+ return el('button', {
188
+ onClick: pb.on,
189
+ style: 'font-family:var(--mono);font-size:10.5px;padding:4px 8px;min-height:26px;background:' + pb.bg + ';border:1px solid ' + pb.bc + ';color:' + pb.color,
190
+ hover: ACTION_HOVER,
191
+ }, iv(pb.l));
192
+ }
193
+
194
+ /* ══════════════════════════════════════════════════════════════════════════════════════════════
195
+ * PANEL 2 — one list per mode (template lines 130–178, 227–320)
196
+ * ════════════════════════════════════════════════════════════════════════════════════════════ */
197
+
198
+ /* ── focus (lines 130–178, derivation 1541–1570) ───────────────────────────────────────────── */
199
+
200
+ function focusList() {
201
+ const det = detOf();
202
+ const selSlug = state.sel;
203
+ const fRow = (f) => ({
204
+ ...f,
205
+ on: () => jump(f.prod2, f.slug),
206
+ ...selRow(selSlug === f.slug && det.kind === 'init'),
207
+ });
208
+ /* The queue is a JUDGEMENT about what is blocked on the PM, so it is never invented here. In
209
+ * fixture mode it is the prototype's four ranked items; in real mode it is whatever the data
210
+ * layer published on `state.focus` ({first, waiting, syn}); with neither, each section renders
211
+ * its label and one honest empty line. */
212
+ const F = state.focus || (FIXTURE
213
+ ? { first: FX.FOCUS_FIRST, waiting: FX.FOCUS_WAITING, syn: FX.FOCUS_SYN }
214
+ : { first: [], waiting: [], syn: [] });
215
+ const live = FIXTURE || !!state.focus;
216
+ const first = (F.first || []).map(fRow);
217
+ const waiting = (F.waiting || []).map(fRow);
218
+ const MEM = memoryOf();
219
+
220
+ const syn = (F.syn || []).map((f) => ({
221
+ ...f,
222
+ on: () => {
223
+ if (f.action === 'delegate-billing-stories') {
224
+ const d = FX.DELEGATE_SESSION;
225
+ startSession(d.slug, { ...d.sess });
226
+ }
227
+ },
228
+ }));
229
+
230
+ const saw = live ? FX.focusSaw(MEM).map((f) => ({
231
+ ...f,
232
+ on: () => {
233
+ if (f.action === 'mode:memory') { setState({ mode: 'memory' }); return; }
234
+ if (f.action && f.action.startsWith('jump:')) {
235
+ const [prod2, slug] = f.action.slice(5).split('|');
236
+ jump(prod2, slug);
237
+ }
238
+ },
239
+ })) : [];
240
+
241
+ const board = boardOf();
242
+ const ends = productTuples().map(([id]) => {
243
+ const g0 = (board[id] || [])[0];
244
+ return g0 ? { prod: id, obj: g0.gid + ' · ' + g0.gname } : null;
245
+ }).filter(Boolean);
246
+
247
+ const out = [];
248
+ out.push(el('div', { style: 'padding:8px 14px 4px;font-size:11.5px;color:var(--muted-foreground);line-height:1.5' },
249
+ 'This list holds only items where ',
250
+ el('span', { style: 'color:var(--foreground)' }, 'you'),
251
+ ' are the blocker. Everything else is running on an agent or waiting on another team.'));
252
+
253
+ out.push(el('div', { style: LABEL9 }, 'objectives · what this quarter is for'));
254
+ for (const fe of ends) {
255
+ out.push(el('div', { style: 'display:flex;gap:8px;align-items:baseline;padding:6px 14px;border-bottom:1px solid oklch(1 0 0 / 5%)' },
256
+ el('span', { style: 'flex:none;font-family:var(--mono);font-size:10.5px;color:var(--brand)' }, iv(fe.prod)),
257
+ el('span', { style: 'font-size:11px;color:var(--muted-foreground);overflow:hidden;text-overflow:ellipsis;white-space:nowrap' }, iv(fe.obj))));
258
+ }
259
+ if (!ends.length) out.push(emptyLine());
260
+
261
+ out.push(el('div', { style: LABEL9 }, 'your queue · ranked — do #1 first'));
262
+ out.push(el('div', { style: 'padding:5px 14px 3px;font-size:10.5px;color:var(--muted-foreground)' },
263
+ 'each item shows what was read before it asks you to act'));
264
+ for (const ff of first) {
265
+ out.push(el('button', { onClick: ff.on, style: row7(ff), hover: ROW_HOVER },
266
+ el('span', { style: 'display:flex;gap:8px;align-items:baseline' },
267
+ el('span', { style: 'flex:none;font-family:var(--mono);font-size:11px;color:var(--warning)' }, iv(ff.n)),
268
+ el('span', { style: 'font-family:var(--mono);font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap' }, iv(ff.slug)),
269
+ el('span', { style: 'margin-left:auto;flex:none;font-family:var(--mono);font-size:10px;color:var(--muted-foreground)' }, iv(ff.prod2))),
270
+ el('span', { style: 'display:block;font-size:11px;margin-top:1px' }, iv(ff.why)),
271
+ el('span', { style: 'display:block;font-family:var(--mono);font-size:10px;color:var(--muted-foreground)' }, iv(ff.read))));
272
+ }
273
+ if (!first.length) out.push(emptyLine());
274
+
275
+ out.push(el('div', { style: LABEL9 }, 'handoffs · waiting on other teams'));
276
+ for (const fw of waiting) {
277
+ out.push(el('button', { onClick: fw.on, style: row7(fw), hover: ROW_HOVER },
278
+ el('span', { style: 'display:flex;gap:8px;align-items:baseline' },
279
+ el('span', { style: 'font-family:var(--mono);font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap' }, iv(fw.slug)),
280
+ el('span', { style: 'margin-left:auto;flex:none;font-family:var(--mono);font-size:10px;color:var(--muted-foreground)' }, iv(fw.prod2))),
281
+ el('span', { style: 'display:block;font-size:11px;color:var(--muted-foreground);margin-top:1px' }, iv(fw.why)),
282
+ el('span', { style: 'display:block;font-family:var(--mono);font-size:10px;color:var(--muted-foreground)' }, iv(fw.read))));
283
+ }
284
+ if (!waiting.length) out.push(emptyLine());
285
+
286
+ out.push(el('div', { style: LABEL9 }, 'delegated · agents in parallel'));
287
+ for (const fy of syn) {
288
+ out.push(el('div', { style: 'display:flex;gap:8px;align-items:center;padding:6px 14px;border-bottom:1px solid oklch(1 0 0 / 5%)' },
289
+ el('span', { style: 'font-size:11.5px;color:var(--muted-foreground);flex:1;min-width:0;line-height:1.45' }, iv(fy.t)),
290
+ fy.hasBtn && el('button', {
291
+ onClick: fy.on,
292
+ style: 'flex:none;font-family:var(--mono);font-size:10.5px;padding:4px 9px;min-height:26px;background:var(--brand);color:oklch(0.987 0.002 197.1);font-weight:500',
293
+ }, iv(fy.btn))));
294
+ }
295
+ if (!syn.length) out.push(emptyLine());
296
+
297
+ out.push(el('div', { style: LABEL9 }, 'upkeep · keep the system sharp'));
298
+ for (const fs2 of saw) {
299
+ out.push(el('button', {
300
+ onClick: fs2.on,
301
+ style: 'display:block;width:100%;padding:7px 14px;min-height:40px;border-bottom:1px solid oklch(1 0 0 / 5%)',
302
+ hover: ROW_HOVER,
303
+ }, el('span', { style: 'display:block;font-size:11.5px;color:var(--muted-foreground);line-height:1.45' }, iv(fs2.t))));
304
+ }
305
+ if (!saw.length) out.push(emptyLine());
306
+ return out;
307
+ }
308
+
309
+ /* ── scheduled (lines 227–237, derivation 1467–1477) ───────────────────────────────────────── */
310
+
311
+ function schedRows() {
312
+ const det = detOf();
313
+ return schedList().map((t) => {
314
+ const on = det.kind === 'sched' && det.id === t.name;
315
+ const last = (t.log || [])[0];
316
+ return {
317
+ name: t.name, when: t.when,
318
+ st: t.enabled ? (last ? last.ts + ' — ' + last.t : 'never run yet') : 'disabled',
319
+ c: t.enabled ? (last ? last.c : C.mut) : C.mut,
320
+ ...selRow(on),
321
+ on: () => setDetail({ kind: 'sched', id: t.name }),
322
+ };
323
+ });
324
+ }
325
+
326
+ function addSched() {
327
+ const list = schedList();
328
+ const name = 'custom task ' + (list.filter((t) => t.name.startsWith('custom task')).length + 1);
329
+ setState({ schedList: [...list, { name, ...FX.NEW_SCHED, log: [] }] },
330
+ () => setDetail({ kind: 'sched', id: name }));
331
+ }
332
+
333
+ function scheduledList() {
334
+ const rows = schedRows();
335
+ const kids = [];
336
+ for (const sr of rows) {
337
+ kids.push(el('button', { onClick: sr.on, style: row7(sr), hover: ROW_HOVER },
338
+ el('span', { style: 'display:flex;gap:8px;align-items:baseline' },
339
+ el('span', { style: 'font-size:12.5px' }, iv(sr.name)),
340
+ el('span', { style: 'margin-left:auto;font-family:var(--mono);font-size:10.5px;color:var(--muted-foreground)' }, iv(sr.when))),
341
+ el('span', { style: 'display:block;font-family:var(--mono);font-size:10.5px;color:' + sr.c }, iv(sr.st))));
342
+ }
343
+ if (!rows.length) kids.push(emptyLine());
344
+ kids.push(el('button', { onClick: addSched, style: ACTION_BTN, hover: ACTION_HOVER }, '+ schedule a task'));
345
+ return [el('div', { style: 'padding:6px 0' }, kids)];
346
+ }
347
+
348
+ /* ── workflow (lines 239–249, derivation 1478–1489) ────────────────────────────────────────── */
349
+
350
+ /** flowUse (line 1480) — which initiatives declare each workflow. */
351
+ function flowUse() {
352
+ const use = {};
353
+ const DATA = records();
354
+ Object.keys(DATA).forEach((slug) => {
355
+ const i = DATA[slug];
356
+ if (i && i.flow) (use[i.flow] = use[i.flow] || []).push(slug);
357
+ });
358
+ return use;
359
+ }
360
+
361
+ function addFlow() {
362
+ const map = flowsMap();
363
+ let n = 1; while (map['custom-' + n]) n++;
364
+ const name = 'custom-' + n;
365
+ setState({ flowsMap: { ...map, [name]: [...FX.NEW_FLOW_STAGES] } },
366
+ () => setDetail({ kind: 'flow', id: name }));
367
+ }
368
+
369
+ function workflowList() {
370
+ const det = detOf();
371
+ const use = flowUse();
372
+ const map = flowsMap();
373
+ const out = [];
374
+ out.push(el('div', { style: 'padding:8px 14px 4px;font-size:11.5px;color:var(--muted-foreground);line-height:1.5' },
375
+ 'Workflow templates — an initiative picks one; its ladder renders from it. Keep this list short: templates dictate how agents work.'));
376
+ const names = Object.keys(map);
377
+ for (const name of names) {
378
+ const on = det.kind === 'flow' && det.id === name;
379
+ const fl = {
380
+ name, chain: map[name].join(' → '), used: (use[name] || []).length + ' initiatives', ...selRow(on),
381
+ };
382
+ out.push(el('button', {
383
+ onClick: () => setDetail({ kind: 'flow', id: name }), style: row8(fl), hover: ROW_HOVER,
384
+ },
385
+ el('span', { style: 'display:flex;gap:8px;align-items:baseline' },
386
+ el('span', { style: 'font-family:var(--mono);font-size:12px;color:var(--foreground)' }, iv(fl.name)),
387
+ el('span', { style: 'margin-left:auto;font-size:10.5px;color:var(--muted-foreground)' }, iv(fl.used))),
388
+ el('span', { style: 'display:block;font-family:var(--mono);font-size:10px;color:var(--info);margin-top:2px;overflow-wrap:anywhere' }, iv(fl.chain))));
389
+ }
390
+ if (!names.length) out.push(emptyLine());
391
+ out.push(el('button', { onClick: addFlow, style: ACTION_BTN, hover: ACTION_HOVER }, '+ new workflow'));
392
+ out.push(el('div', { style: 'padding:0 14px 10px;font-size:11px;line-height:1.5;color:var(--muted-foreground)' },
393
+ 'Creating a template is the easy part. Applying one is the commitment — an initiative opts in by setting ',
394
+ el('span', { style: 'font-family:var(--mono)' }, 'flow:'),
395
+ ' in its record, and its ladder re-derives from the new stages.'));
396
+ return out;
397
+ }
398
+
399
+ /* ── skills (lines 251–258, derivation 1490–1500) ──────────────────────────────────────────── */
400
+
401
+ function skillsList() {
402
+ const det = detOf();
403
+ const list = skillList();
404
+ const out = list.map((sk) => {
405
+ const r = selRow(det.kind === 'skill' && det.id === sk.name);
406
+ return el('button', {
407
+ onClick: () => setDetail({ kind: 'skill', id: sk.name }), style: row8(r), hover: ROW_HOVER,
408
+ },
409
+ el('span', { style: 'display:block;font-family:var(--mono);font-size:12px' }, iv(sk.name)),
410
+ el('span', { style: 'display:block;font-size:11px;color:var(--muted-foreground)' }, iv(sk.d)));
411
+ });
412
+ if (!list.length) out.push(emptyLine());
413
+ return out;
414
+ }
415
+
416
+ /* ── knowledge (lines 260–273, derivation 1501–1510) ───────────────────────────────────────── */
417
+
418
+ function knowledgeList() {
419
+ const det = detOf();
420
+ const KNOW = knowMap();
421
+ const kGroups = {};
422
+ Object.keys(KNOW).forEach((id) => {
423
+ const k = KNOW[id];
424
+ (kGroups[k.grp] = kGroups[k.grp] || []).push([id, k]);
425
+ });
426
+
427
+ const out = [];
428
+ out.push(el('div', { style: 'padding:8px 14px 4px;font-size:11.5px;color:var(--muted-foreground)' },
429
+ 'Decisions, grouped by domain — related decisions link to each other.'));
430
+ const names = Object.keys(kGroups);
431
+ for (const name of names) {
432
+ const rows = kGroups[name].map(([id, k]) => {
433
+ const r = selRow(det.kind === 'know' && det.id === id);
434
+ const idColor = k.badge === 'superseded' ? 'var(--muted-foreground)' : 'var(--brand)';
435
+ return el('button', {
436
+ onClick: () => setDetail({ kind: 'know', id }),
437
+ style: 'display:block;width:100%;padding:8px 14px 8px 20px;min-height:44px;border-bottom:1px solid oklch(1 0 0 / 5%);border-left:2px solid ' + r.bl + ';background:' + r.bg,
438
+ hover: ROW_HOVER,
439
+ },
440
+ el('span', { style: 'display:flex;gap:8px;align-items:baseline' },
441
+ el('span', { style: 'font-family:var(--mono);font-size:12px;color:' + idColor }, iv(id)),
442
+ el('span', { style: 'font-family:var(--mono);font-size:10px;color:var(--muted-foreground)' }, iv(k.badge))),
443
+ el('span', { style: 'display:block;font-size:11.5px;color:var(--muted-foreground)' }, iv(k.d)));
444
+ });
445
+ out.push(el('div', null, el('div', { style: LABEL7 }, iv(name)), rows));
446
+ }
447
+ if (!names.length) out.push(emptyLine());
448
+ return out;
449
+ }
450
+
451
+ /* ── agents (lines 275–283, derivation 1511–1527) ──────────────────────────────────────────── */
452
+
453
+ function addAgent() {
454
+ const list = agentList();
455
+ let n = 1; while (list.find((a) => a.name === 'agent-' + n)) n++;
456
+ const name = 'agent-' + n;
457
+ setState({ agentList: [...list, { name, ...FX.NEW_AGENT, sessions: [] }] },
458
+ () => setDetail({ kind: 'agent', id: name }));
459
+ }
460
+
461
+ function agentsList() {
462
+ const det = detOf();
463
+ const list = agentList();
464
+ const out = list.map((a) => {
465
+ const r = selRow(det.kind === 'agent' && det.id === a.name);
466
+ const working = (a.sessions || []).filter((s) => s.st === 'working' || s.st === 'needs').length;
467
+ const st = a.paused ? 'paused — takes no new work'
468
+ : (working ? working + ' session' + (working > 1 ? 's' : '') + ' working' : 'idle');
469
+ const c = a.paused ? C.mut : (working ? C.suc : C.mut);
470
+ return el('button', {
471
+ onClick: () => setDetail({ kind: 'agent', id: a.name }), style: row8(r), hover: ROW_HOVER,
472
+ },
473
+ el('span', { style: 'display:flex;gap:8px;align-items:baseline' },
474
+ el('span', { style: 'flex:none;font-size:10px;color:' + c }, '●'),
475
+ el('span', { style: 'font-family:var(--mono);font-size:12px' }, iv(a.name)),
476
+ el('span', { style: 'margin-left:auto;font-size:10.5px;color:var(--muted-foreground)' }, iv(a.role))),
477
+ el('span', { style: 'display:block;font-size:11px;color:var(--muted-foreground);padding-left:18px' }, iv(st)));
478
+ });
479
+ if (!list.length) out.push(emptyLine());
480
+ out.push(el('button', { onClick: addAgent, style: ACTION_BTN, hover: ACTION_HOVER }, '+ register agent'));
481
+ return out;
482
+ }
483
+
484
+ /* ── connections (lines 285–301, derivation 1683–1697) ─────────────────────────────────────── */
485
+
486
+ function connectList() {
487
+ const det = detOf();
488
+ const out = [];
489
+ out.push(el('div', { style: 'padding:8px 14px 4px;font-size:11.5px;color:var(--muted-foreground);line-height:1.5' },
490
+ 'Claude profiles and MCP connections. Workspace MCP — Jira, Confluence, Slack, Google Workspace — is only reachable on claude-work.'));
491
+
492
+ out.push(el('div', { style: LABEL7 }, 'claude profiles'));
493
+ const profs = profileList();
494
+ for (const x of profs) {
495
+ const r = selRow(det.kind === 'prof' && det.id === x.id);
496
+ const mcpLine = x.mcp.length ? 'mcp: ' + x.mcp.join(' · ') : 'mcp: none';
497
+ const mcpColor = x.mcp.length ? C.inf : C.mut;
498
+ out.push(el('button', {
499
+ onClick: () => setDetail({ kind: 'prof', id: x.id }), style: row8(r), hover: ROW_HOVER,
500
+ },
501
+ el('span', { style: 'display:flex;gap:8px;align-items:baseline' },
502
+ el('span', { style: 'font-family:var(--mono);font-size:12px' }, iv(x.id)),
503
+ el('span', { style: 'margin-left:auto;font-size:10.5px;color:var(--muted-foreground)' }, iv(x.account))),
504
+ el('span', { style: 'display:block;font-family:var(--mono);font-size:10px;color:' + mcpColor }, iv(mcpLine))));
505
+ }
506
+ if (!profs.length) out.push(emptyLine());
507
+
508
+ out.push(el('div', { style: LABEL9 }, 'connections · via claude-work'));
509
+ const conns = intgList();
510
+ for (const g of conns) {
511
+ const r = selRow(det.kind === 'intg' && det.id === g.id);
512
+ const st = g.st + ' · synced ' + ((state.intgSynced && state.intgSynced[g.id]) || g.synced);
513
+ out.push(el('button', {
514
+ onClick: () => setDetail({ kind: 'intg', id: g.id }), style: row8(r), hover: ROW_HOVER,
515
+ },
516
+ el('span', { style: 'display:flex;gap:8px;align-items:baseline' },
517
+ el('span', { style: 'flex:none;font-size:10px;color:var(--success)' }, '●'),
518
+ el('span', { style: 'font-family:var(--mono);font-size:12px' }, iv(g.name)),
519
+ el('span', { style: 'margin-left:auto;font-size:10.5px;color:var(--muted-foreground)' }, iv(g.scope))),
520
+ el('span', { style: 'display:block;font-family:var(--mono);font-size:10px;color:var(--muted-foreground);padding-left:18px' }, iv(st))));
521
+ }
522
+ // PMOS holds no credential and makes no outbound request (PRD, "External sync"). With nothing
523
+ // configured the panel says exactly that instead of listing a connection that does not exist.
524
+ if (!conns.length) {
525
+ out.push(el('div', { style: 'padding:5px 14px 6px;font-size:11px;color:var(--muted-foreground);font-style:italic' },
526
+ 'no connector configured — nothing was read, and nothing will be written'));
527
+ }
528
+ return out;
529
+ }
530
+
531
+ /* ── memory (lines 303–320, derivation 1528–1540) ──────────────────────────────────────────── */
532
+
533
+ function addMem() {
534
+ const MEM = memoryOf();
535
+ let n = 1; while (MEM.long.find((m) => m.id === 'ln' + n)) n++;
536
+ const m = { id: 'ln' + n, ...FX.NEW_MEMORY_NOTE };
537
+ setState({ memory: { ...MEM, long: [...MEM.long, m] } },
538
+ () => setDetail({ kind: 'mem', id: m.id, scope: 'long' }));
539
+ }
540
+
541
+ function memoryList() {
542
+ const det = detOf();
543
+ const MEM = memoryOf();
544
+ const rowOf = (m, scope) => ({
545
+ ...m, pin: m.pinned ? '⚑ pinned' : '',
546
+ ...selRow(det.kind === 'mem' && det.id === m.id),
547
+ on: () => setDetail({ kind: 'mem', id: m.id, scope }),
548
+ });
549
+
550
+ const out = [];
551
+ out.push(el('div', { style: 'padding:8px 14px 4px;font-size:11.5px;color:var(--muted-foreground);line-height:1.5' },
552
+ 'Agent memory — short-term dies with the session; long-term survives and is injected into every run.'));
553
+
554
+ out.push(el('div', { style: LABEL7 }, 'short-term · session context'));
555
+ const shorts = MEM.short.map((m) => rowOf(m, 'short'));
556
+ for (const me of shorts) {
557
+ out.push(el('button', { onClick: me.on, style: row7(me), hover: ROW_HOVER },
558
+ el('span', { style: 'display:block;font-size:11.5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap' }, iv(me.text)),
559
+ el('span', { style: 'display:block;font-family:var(--mono);font-size:10px;color:var(--muted-foreground)' },
560
+ iv(me.src), ' · ', iv(me.ts))));
561
+ }
562
+ if (!shorts.length) out.push(emptyLine());
563
+
564
+ out.push(el('div', { style: LABEL9 }, 'long-term · durable'));
565
+ const longs = MEM.long.map((m) => rowOf(m, 'long'));
566
+ for (const me of longs) {
567
+ out.push(el('button', { onClick: me.on, style: row7(me), hover: ROW_HOVER },
568
+ el('span', { style: 'display:flex;gap:6px;align-items:baseline' },
569
+ el('span', { style: 'font-size:11.5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap' }, iv(me.text)),
570
+ el('span', { style: 'margin-left:auto;flex:none;font-family:var(--mono);font-size:10px;color:var(--warning)' }, iv(me.pin))),
571
+ el('span', { style: 'display:block;font-family:var(--mono);font-size:10px;color:var(--muted-foreground)' }, iv(me.src))));
572
+ }
573
+ if (!longs.length) out.push(emptyLine());
574
+ out.push(el('button', { onClick: addMem, style: ACTION_BTN, hover: ACTION_HOVER }, '+ add a long-term note'));
575
+ return out;
576
+ }
577
+
578
+ /* ══════════════════════════════════════════════════════════════════════════════════════════════
579
+ * PANEL 3 — one body per secondary detail kind (template lines 450–663, 771–835)
580
+ * ════════════════════════════════════════════════════════════════════════════════════════════ */
581
+
582
+ /* ── `dFlow` (lines 450–489, derivation 1809–1833) ─────────────────────────────────────────── */
583
+
584
+ function flowBody(det) {
585
+ const map = flowsMap();
586
+ const use = flowUse();
587
+ let fd = { name: '', used: '', stages: [], usedBy: [], unused: false, canDelete: false, noDelete: false, onDelete: () => {}, onNewStage: () => {} };
588
+ if (map[det.id]) {
589
+ const name = det.id, stages = map[det.id], ub = use[name] || [];
590
+ const setStages = (st2) => setState({ flowsMap: { ...map, [name]: st2 } });
591
+ fd = {
592
+ name, used: ub.length + ' initiatives use it',
593
+ stages: stages.map((k, idx) => ({
594
+ n: String(idx + 1).padStart(2, '0'), k,
595
+ onUp: () => {
596
+ if (idx === 0) return;
597
+ const s2 = [...stages];
598
+ const tmp = s2[idx - 1]; s2[idx - 1] = s2[idx]; s2[idx] = tmp;
599
+ setStages(s2);
600
+ },
601
+ onDel: () => setStages(stages.filter((_, j) => j !== idx)),
602
+ })),
603
+ onNewStage: (e) => {
604
+ if (e.key !== 'Enter') return;
605
+ const v = e.target.value.trim().toLowerCase().replace(/\s+/g, '-');
606
+ if (!v) return;
607
+ e.target.value = '';
608
+ setStages([...stages, v]);
609
+ },
610
+ usedBy: ub.map((slug) => ({ slug, on: () => setState({ sel: slug, mode: 'product', detail: { kind: 'init' }, col2: true }) })),
611
+ unused: ub.length === 0,
612
+ canDelete: ub.length === 0, noDelete: ub.length > 0,
613
+ onDelete: () => {
614
+ const m2 = { ...map }; delete m2[det.id];
615
+ setState({ flowsMap: m2, detail: { kind: 'init' } });
616
+ },
617
+ };
618
+ }
619
+
620
+ const out = [];
621
+ out.push(el('div', { style: HEAD_BLOCK },
622
+ el('div', { style: HEAD_NAME }, iv(fd.name)),
623
+ el('div', { style: HEAD_SUB }, 'workflow template · ', iv(fd.used))));
624
+ out.push(el('div', { style: SECTION_LABEL }, 'stages'));
625
+ out.push(el('div', { style: 'display:flex;flex-direction:column;gap:2px;padding:4px 14px 6px' },
626
+ fd.stages.map((fs) => el('div', { style: 'display:flex;gap:9px;align-items:center;min-height:34px;padding:0 8px;background:var(--background);border:1px solid var(--border)' },
627
+ el('span', { style: 'flex:none;font-family:var(--mono);font-size:10px;color:var(--muted-foreground)' }, iv(fs.n)),
628
+ el('span', { style: 'font-family:var(--mono);font-size:11.5px;letter-spacing:.06em' }, iv(fs.k)),
629
+ el('button', { onClick: fs.onUp, style: 'margin-left:auto;color:var(--muted-foreground);padding:3px 5px;font-size:11px', hover: 'color:var(--foreground)' }, '↑'),
630
+ el('button', { onClick: fs.onDel, style: 'color:var(--muted-foreground);padding:3px 5px;font-size:12px', hover: 'color:var(--destructive)' }, '×')))));
631
+ out.push(el('div', { style: 'display:flex;gap:6px;padding:2px 14px 10px' },
632
+ el('input', {
633
+ onKeyDown: fd.onNewStage, placeholder: 'new stage — Enter to add',
634
+ style: 'flex:1;min-width:0;font-family:var(--mono);font-size:11.5px;padding:6px 9px;border:1px dashed var(--input);min-height:32px',
635
+ })));
636
+ out.push(el('div', { style: 'border-top:1px solid var(--border);display:grid;grid-template-columns:76px 1fr;gap:10px;padding:8px 14px' },
637
+ el('span', { style: fieldLabel(2) }, 'used by'),
638
+ el('div', { style: 'display:flex;flex-direction:column;gap:2px;min-width:0' },
639
+ fd.usedBy.map((ub) => el('button', {
640
+ onClick: ub.on, style: 'font-family:var(--mono);font-size:11px;color:var(--brand);padding:3px 0;min-height:26px', hover: 'color:var(--foreground)',
641
+ }, iv(ub.slug))),
642
+ fd.unused && el('span', { style: ITALIC_EMPTY }, 'no initiatives use this yet'))));
643
+ out.push(el('div', { style: 'padding:10px 14px;border-top:1px solid var(--border)' },
644
+ fd.canDelete && el('button', { onClick: fd.onDelete, style: DESTRUCTIVE_BTN, hover: DESTRUCTIVE_HOVER }, 'delete workflow'),
645
+ fd.noDelete && el('span', { style: ITALIC_EMPTY }, 'in use — reassign its initiatives before deleting')));
646
+ out.push(el('div', { style: FOOTER }, 'planning/workflows/', iv(fd.name), '.yaml · edits apply to new ladders'));
647
+ return out;
648
+ }
649
+
650
+ /* ── `dSched` (lines 491–537, derivation 1835–1855) ────────────────────────────────────────── */
651
+
652
+ function schedBody(det) {
653
+ const list = schedList();
654
+ let sd = { name: '', when: '', runCmd: '', scope: '', enabledLabel: '', toggleLabel: '', log: [], profBtns: [], profNote: '', onWhen: () => {}, onRunCmd: () => {}, onScope: () => {}, onRun: () => {}, onToggle: () => {}, onDelete: () => {} };
655
+ const t = list.find((x) => x.name === det.id);
656
+ if (t) {
657
+ const save = (t2) => setState({ schedList: list.map((x) => (x.name === det.id ? t2 : x)) });
658
+ const prof = t.profile || 'claude-work';
659
+ sd = {
660
+ name: t.name, when: t.when, runCmd: t.runCmd || '', scope: t.scope || '',
661
+ enabledLabel: t.enabled ? 'enabled' : 'disabled',
662
+ profBtns: profChips(prof, (id) => save({ ...t, profile: id })),
663
+ profNote: prof === 'claude-work'
664
+ ? 'runs with workspace MCP — can read Jira and Confluence'
665
+ : 'runs without workspace MCP — local files only',
666
+ toggleLabel: t.enabled ? '‖ disable' : '▷ enable',
667
+ log: (t.log || []).length ? t.log : [{ ...FX.SCHED_NEVER_RUN }],
668
+ onWhen: (e) => save({ ...t, when: e.target.value }),
669
+ onRunCmd: (e) => save({ ...t, runCmd: e.target.value }),
670
+ onScope: (e) => save({ ...t, scope: e.target.value }),
671
+ onRun: () => save({
672
+ ...t,
673
+ log: [{
674
+ ts: '12:41 today',
675
+ t: t.runCmd ? 'ok — ran `' + t.runCmd + '` manually, 0 failures' : 'nothing ran — no command defined yet',
676
+ c: t.runCmd ? 'var(--success)' : 'var(--warning)',
677
+ }, ...(t.log || [])],
678
+ }),
679
+ onToggle: () => save({ ...t, enabled: !t.enabled }),
680
+ onDelete: () => setState({ schedList: list.filter((x) => x.name !== det.id), detail: { kind: 'init' } }),
681
+ };
682
+ }
683
+
684
+ const INPUT = 'font-family:var(--mono);font-size:11.5px;padding:6px 9px;border:1px solid var(--input);min-height:32px';
685
+ const out = [];
686
+ out.push(el('div', { style: HEAD_BLOCK },
687
+ el('div', { style: HEAD_NAME }, iv(sd.name)),
688
+ el('div', { style: HEAD_SUB }, 'scheduled task · ', iv(sd.enabledLabel))));
689
+ out.push(el('div', { style: FIELD_ROW10 },
690
+ el('span', { style: fieldLabel(8) }, 'cadence'),
691
+ el('input', { onChange: sd.onWhen, value: sd.when, style: INPUT })));
692
+ out.push(el('div', { style: FIELD_ROW10 },
693
+ el('span', { style: fieldLabel(8) }, 'runs'),
694
+ el('div', { style: 'min-width:0' },
695
+ el('input', { onChange: sd.onRunCmd, value: sd.runCmd, placeholder: 'pmos run <skill> …', style: 'width:100%;' + INPUT }),
696
+ el('div', { style: 'font-size:11px;color:var(--muted-foreground);margin-top:4px' },
697
+ 'the command this task executes on schedule — any pmos skill or shell command'))));
698
+ out.push(el('div', { style: FIELD_ROW10 },
699
+ el('span', { style: fieldLabel(8) }, 'scope'),
700
+ el('input', { onChange: sd.onScope, value: sd.scope, placeholder: 'all initiatives, or a slug', style: INPUT })));
701
+ out.push(el('div', { style: FIELD_ROW10 },
702
+ el('span', { style: fieldLabel(8) }, 'profile'),
703
+ el('div', { style: 'min-width:0' },
704
+ el('div', { style: 'display:flex;gap:4px' }, sd.profBtns.map(profChipNode)),
705
+ el('div', { style: 'font-size:11px;color:var(--muted-foreground);margin-top:4px;line-height:1.5' }, iv(sd.profNote)))));
706
+ out.push(el('div', { style: 'display:flex;gap:6px;padding:10px 14px;border-bottom:1px solid var(--border)' },
707
+ el('button', { onClick: sd.onRun, style: BRAND_BTN }, '▶ run now'),
708
+ el('button', { onClick: sd.onToggle, style: SEC_BTN, hover: ACTION_HOVER }, iv(sd.toggleLabel)),
709
+ el('button', { onClick: sd.onDelete, style: 'margin-left:auto;' + DESTRUCTIVE_BTN, hover: DESTRUCTIVE_HOVER }, 'delete')));
710
+ out.push(el('div', { style: SECTION_LABEL }, 'run log'));
711
+ out.push(el('div', { style: 'display:flex;flex-direction:column;padding:4px 14px 12px' },
712
+ sd.log.map((lg) => el('div', { style: 'display:flex;gap:9px;align-items:baseline;padding:5px 0;border-bottom:1px solid oklch(1 0 0 / 5%)' },
713
+ el('span', { style: 'flex:none;font-family:var(--mono);font-size:10.5px;color:var(--muted-foreground)' }, iv(lg.ts)),
714
+ el('span', { style: 'font-size:12px;color:' + lg.c }, iv(lg.t))))));
715
+ out.push(el('div', { style: FOOTER }, 'planning/scheduled/', iv(sd.name), '.yaml'));
716
+ return out;
717
+ }
718
+
719
+ /* ── `dSkill` (lines 539–561, derivation 1857–1868) ────────────────────────────────────────── */
720
+
721
+ function skillBody(det) {
722
+ const selSlug = selectedSlug();
723
+ let kd = { name: '', d: '', reads: '', writes: '', usedIn: '', onRun: () => {}, onOpen: () => {} };
724
+ const sk = skillList().find((x) => x.name === det.id);
725
+ if (sk) {
726
+ kd = {
727
+ ...sk,
728
+ onRun: () => startSession(selSlug, { name: sk.name + ' run', st: 'working', label: 'working' }),
729
+ onOpen: () => openDoc(null, 'skills/' + sk.name + '/SKILL.md', 'SKILL'),
730
+ };
731
+ }
732
+
733
+ const out = [];
734
+ out.push(el('div', { style: HEAD_BLOCK },
735
+ el('div', { style: HEAD_NAME }, iv(kd.name)),
736
+ el('div', { style: HEAD_SUB }, 'skill · ', iv(kd.d))));
737
+ const wrap = 'font-family:var(--mono);font-size:11px;color:var(--muted-foreground);overflow-wrap:anywhere';
738
+ out.push(el('div', { style: FIELD_ROW },
739
+ el('span', { style: fieldLabel(2) }, 'reads'), el('span', { style: wrap }, iv(kd.reads))));
740
+ out.push(el('div', { style: FIELD_ROW },
741
+ el('span', { style: fieldLabel(2) }, 'writes'), el('span', { style: wrap }, iv(kd.writes))));
742
+ out.push(el('div', { style: FIELD_ROW },
743
+ el('span', { style: fieldLabel(2) }, 'used in'),
744
+ el('span', { style: 'font-size:12px;color:var(--muted-foreground)' }, iv(kd.usedIn))));
745
+ out.push(el('div', { style: 'display:flex;gap:6px;padding:12px 14px' },
746
+ el('button', { onClick: kd.onRun, style: BRAND_BTN }, '▶ run in a session'),
747
+ el('button', { onClick: kd.onOpen, style: SEC_BTN, hover: ACTION_HOVER }, 'open definition')));
748
+ out.push(el('div', { style: FOOTER }, 'skills/', iv(kd.name), '/SKILL.md'));
749
+ return out;
750
+ }
751
+
752
+ /* ── `dKnow` (lines 563–589, derivation 1870–1882) ─────────────────────────────────────────── */
753
+
754
+ function knowBody(det) {
755
+ const KNOW = knowMap();
756
+ let nd = { id: '', d: '', badge: '', badgeColor: C.mut, idColor: C.brand, body: '', rels: [], noRels: false, path: '', onOpen: () => {} };
757
+ if (KNOW[det.id]) {
758
+ const k = KNOW[det.id];
759
+ nd = {
760
+ id: det.id, d: k.d, badge: k.badge,
761
+ badgeColor: k.badge === 'active' ? C.suc : C.wrn,
762
+ idColor: k.badge === 'superseded' ? 'var(--muted-foreground)' : 'var(--brand)',
763
+ body: k.body, path: k.path,
764
+ rels: (k.rels || []).map(([kind, id2]) => ({
765
+ kind, id: id2, d: KNOW[id2] ? KNOW[id2].d : '',
766
+ on: () => setDetail({ kind: 'know', id: id2 }),
767
+ })),
768
+ noRels: (k.rels || []).length === 0,
769
+ onOpen: () => openDoc(null, k.path, 'DECISION'),
770
+ };
771
+ }
772
+
773
+ const out = [];
774
+ out.push(el('div', { style: HEAD_BLOCK },
775
+ el('div', { style: 'display:flex;gap:9px;align-items:baseline' },
776
+ el('span', { style: 'font-family:var(--mono);font-size:15px;font-weight:600;color:' + nd.idColor }, iv(nd.id)),
777
+ el('span', { style: PILL + ';color:' + nd.badgeColor }, iv(nd.badge))),
778
+ el('div', { style: HEAD_SUB }, iv(nd.d))));
779
+ out.push(el('div', { style: 'padding:12px 14px;border-bottom:1px solid var(--border);font-size:13px;line-height:1.55' }, iv(nd.body)));
780
+ out.push(el('div', { style: SECTION_LABEL }, 'relationships'));
781
+ out.push(el('div', { style: 'display:flex;flex-direction:column;gap:2px;padding:4px 14px 10px' },
782
+ nd.rels.map((rl) => el('button', {
783
+ onClick: rl.on,
784
+ style: 'display:flex;gap:9px;align-items:center;min-height:34px;padding:0 9px;background:var(--background);border:1px solid var(--border)',
785
+ hover: ACTION_HOVER,
786
+ },
787
+ el('span', { style: 'flex:none;width:86px;font-family:var(--mono);font-size:9.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--muted-foreground)' }, iv(rl.kind)),
788
+ el('span', { style: 'font-family:var(--mono);font-size:11.5px;color:var(--brand)' }, iv(rl.id)),
789
+ el('span', { style: 'font-size:11px;color:var(--muted-foreground);overflow:hidden;text-overflow:ellipsis;white-space:nowrap' }, iv(rl.d)))),
790
+ nd.noRels && el('span', { style: ITALIC_EMPTY }, 'stands alone — no linked decisions')));
791
+ out.push(el('div', { style: 'padding:2px 14px 12px' },
792
+ el('button', { onClick: nd.onOpen, style: SEC_BTN, hover: ACTION_HOVER }, 'open file')));
793
+ out.push(el('div', { style: FOOTER }, iv(nd.path)));
794
+ return out;
795
+ }
796
+
797
+ /* ── `dAgent` (lines 591–642, derivation 1884–1907) ────────────────────────────────────────── */
798
+
799
+ function agentBody(det) {
800
+ const list = agentList();
801
+ const MEM = memoryOf();
802
+ const selSlug = selectedSlug();
803
+ let ad = { name: '', role: '', model: '', st: '', c: C.mut, sessions: [], noSess: false, memLine: '', toggleLabel: '', profBtns: [], profNote: '', onToggle: () => {}, onStart: () => {}, onMem: () => {} };
804
+ const a = list.find((x) => x.name === det.id);
805
+ if (a) {
806
+ const sessions = a.sessions || [];
807
+ const working = sessions.filter((s) => s.st === 'working' || s.st === 'needs').length;
808
+ const prof = a.profile || 'claude-work';
809
+ ad = {
810
+ name: a.name, role: a.role, model: a.model,
811
+ profBtns: profChips(prof, (id) => setState({ agentList: list.map((x) => (x.name === a.name ? { ...x, profile: id } : x)) })),
812
+ profNote: prof === 'claude-work'
813
+ ? 'claude-work — this agent reaches Jira, Confluence, Slack, Google Workspace over MCP'
814
+ : 'claude-personal — no workspace MCP; this agent cannot touch Jira or Confluence',
815
+ st: a.paused ? 'paused — takes no new work' : (working ? working + ' sessions working' : 'idle'),
816
+ c: a.paused ? C.mut : (working ? C.suc : C.mut),
817
+ sessions: sessions.map((s) => {
818
+ const dot = FX.SDOT[s.st] || FX.SDOT.exited;
819
+ return {
820
+ name: s.name, slug: s.slug, dot: dot[0], color: dot[1],
821
+ on: () => openTerm(s.slug, { name: s.name, st: s.st }),
822
+ };
823
+ }),
824
+ noSess: sessions.length === 0,
825
+ memLine: MEM.short.length + ' short-term entries · ' + MEM.long.length + ' long-term',
826
+ toggleLabel: a.paused ? '▷ resume' : '‖ pause',
827
+ onToggle: () => setState({ agentList: list.map((x) => (x.name === a.name ? { ...x, paused: !x.paused } : x)) }),
828
+ onStart: () => {
829
+ const s2 = { name: a.name + ' session', slug: selSlug, st: 'working', profile: prof };
830
+ setState(
831
+ { agentList: list.map((x) => (x.name === a.name ? { ...x, sessions: [...(x.sessions || []), s2] } : x)) },
832
+ () => openTerm(selSlug, { name: s2.name, st: 'working' }),
833
+ );
834
+ },
835
+ onMem: () => setState({ mode: 'memory' }),
836
+ };
837
+ }
838
+
839
+ const out = [];
840
+ out.push(el('div', { style: HEAD_BLOCK },
841
+ el('div', { style: 'display:flex;gap:9px;align-items:baseline' },
842
+ el('span', { style: 'flex:none;font-size:10px;color:' + ad.c }, '●'),
843
+ el('span', { style: HEAD_NAME }, iv(ad.name)),
844
+ el('span', { style: 'margin-left:auto;' + PILL + ';color:var(--muted-foreground)' }, iv(ad.role))),
845
+ el('div', { style: HEAD_SUB }, iv(ad.st))));
846
+ out.push(el('div', { style: FIELD_ROW },
847
+ el('span', { style: fieldLabel(2) }, 'model'),
848
+ el('span', { style: 'font-family:var(--mono);font-size:11.5px' }, iv(ad.model))));
849
+ out.push(el('div', { style: FIELD_ROW },
850
+ el('span', { style: fieldLabel(2) }, 'profile'),
851
+ el('div', { style: 'min-width:0' },
852
+ el('div', { style: 'display:flex;gap:4px' }, ad.profBtns.map(profChipNode)),
853
+ el('div', { style: 'font-size:11px;color:var(--muted-foreground);margin-top:4px;line-height:1.5' }, iv(ad.profNote)))));
854
+ out.push(el('div', { style: FIELD_ROW },
855
+ el('span', { style: fieldLabel(2) }, 'sessions'),
856
+ el('div', { style: 'display:flex;flex-direction:column;gap:2px;min-width:0' },
857
+ ad.sessions.map((s) => el('button', {
858
+ onClick: s.on, style: 'display:flex;gap:8px;align-items:center;padding:5px 6px;margin:0 -6px;min-height:34px', hover: ROW_HOVER,
859
+ },
860
+ el('span', { style: 'flex:none;font-size:10px;color:' + s.color }, iv(s.dot)),
861
+ el('span', { style: 'font-size:12.5px' }, iv(s.name)),
862
+ el('span', { style: 'margin-left:auto;font-family:var(--mono);font-size:10px;color:var(--muted-foreground)' }, iv(s.slug)))),
863
+ ad.noSess && el('span', { style: ITALIC_EMPTY }, 'no sessions attached'))));
864
+ out.push(el('div', { style: FIELD_ROW },
865
+ el('span', { style: fieldLabel(2) }, 'memory'),
866
+ el('div', { style: 'min-width:0' },
867
+ el('div', { style: 'font-size:12px;color:var(--muted-foreground)' }, iv(ad.memLine)),
868
+ el('button', {
869
+ onClick: ad.onMem,
870
+ style: 'margin-top:3px;font-family:var(--mono);font-size:11px;color:var(--brand);padding:2px 0;min-height:24px',
871
+ hover: 'color:var(--foreground)',
872
+ }, 'open memory →'))));
873
+ out.push(el('div', { style: 'display:flex;gap:6px;padding:12px 14px' },
874
+ el('button', { onClick: ad.onToggle, style: SEC_BTN, hover: ACTION_HOVER }, iv(ad.toggleLabel)),
875
+ el('button', { onClick: ad.onStart, style: BRAND_BTN }, '▶ new session')));
876
+ out.push(el('div', { style: FOOTER }, 'agents/', iv(ad.name), '.yaml'));
877
+ return out;
878
+ }
879
+
880
+ /* ── `dMem` (lines 644–663, derivation 1909–1935) ──────────────────────────────────────────── */
881
+
882
+ function memBody(det) {
883
+ const MEM = memoryOf();
884
+ let md = { scope: '', scopeColor: C.inf, src: '', text: '', note: '', canPromote: false, canPin: false, pinLabel: '', path: '', onPromote: () => {}, onPin: () => {}, onForget: () => {} };
885
+ const list = det.scope === 'short' ? MEM.short : MEM.long;
886
+ const m = list.find((x) => x.id === det.id);
887
+ if (m) {
888
+ md = {
889
+ scope: det.scope === 'short' ? 'short-term' : 'long-term',
890
+ scopeColor: det.scope === 'short' ? C.inf : C.suc,
891
+ src: m.src + (m.ts ? ' · ' + m.ts : ''), text: m.text,
892
+ note: det.scope === 'short'
893
+ ? 'Dies when its session exits. Promote it to keep it — the agent will see it in every future run.'
894
+ : 'Injected into every agent run. Pinned entries survive compaction; unpinned ones may be summarized away.',
895
+ canPromote: det.scope === 'short', canPin: det.scope === 'long',
896
+ pinLabel: m.pinned ? '⚑ unpin' : '⚑ pin',
897
+ path: det.scope === 'short' ? 'runtime — session context' : 'memory/long-term.jsonl',
898
+ onPromote: () => {
899
+ const nm = { id: 'p' + m.id, text: m.text, src: 'promoted just now', pinned: false };
900
+ setState({ memory: { short: MEM.short.filter((x) => x.id !== m.id), long: [...MEM.long, nm] } },
901
+ () => setDetail({ kind: 'mem', id: nm.id, scope: 'long' }));
902
+ },
903
+ onPin: () => setState({ memory: { ...MEM, long: MEM.long.map((x) => (x.id === m.id ? { ...x, pinned: !x.pinned } : x)) } }),
904
+ onForget: () => setState({
905
+ memory: det.scope === 'short'
906
+ ? { ...MEM, short: MEM.short.filter((x) => x.id !== m.id) }
907
+ : { ...MEM, long: MEM.long.filter((x) => x.id !== m.id) },
908
+ detail: { kind: 'init' },
909
+ }),
910
+ };
911
+ }
912
+
913
+ const out = [];
914
+ out.push(el('div', { style: HEAD_BLOCK },
915
+ el('div', { style: 'display:flex;gap:8px;align-items:baseline' },
916
+ el('span', { style: PILL + ';color:' + md.scopeColor }, iv(md.scope)),
917
+ el('span', { style: 'font-family:var(--mono);font-size:10px;color:var(--muted-foreground)' }, iv(md.src)))));
918
+ out.push(el('div', { style: 'padding:12px 14px;border-bottom:1px solid var(--border);font-size:13.5px;line-height:1.55' }, iv(md.text)));
919
+ out.push(el('div', { style: 'padding:10px 14px;font-size:11.5px;line-height:1.55;color:var(--muted-foreground)' }, iv(md.note)));
920
+ out.push(el('div', { style: 'display:flex;gap:6px;padding:2px 14px 12px;flex-wrap:wrap' },
921
+ md.canPromote && el('button', { onClick: md.onPromote, style: BRAND_BTN }, '↥ promote to long-term'),
922
+ md.canPin && el('button', { onClick: md.onPin, style: SEC_BTN, hover: ACTION_HOVER }, iv(md.pinLabel)),
923
+ el('button', { onClick: md.onForget, style: DESTRUCTIVE_BTN, hover: DESTRUCTIVE_HOVER }, 'forget')));
924
+ out.push(el('div', { style: FOOTER }, iv(md.path)));
925
+ return out;
926
+ }
927
+
928
+ /* ── `dProf` (lines 771–800, derivation 1937–1946) ─────────────────────────────────────────── */
929
+
930
+ function profBody(det) {
931
+ let pf = { id: '', account: '', mcp: [], noMcp: false, usedBy: [], unused: false, note: '' };
932
+ const x = profileList().find((z) => z.id === det.id);
933
+ if (x) {
934
+ const usedBy = [
935
+ ...agentList().filter((a) => (a.profile || 'claude-work') === x.id).map((a) => ({ t: a.name, k: 'agent' })),
936
+ ...schedList().filter((t) => (t.profile || 'claude-work') === x.id).map((t) => ({ t: t.name, k: 'scheduled task' })),
937
+ ];
938
+ pf = { ...x, noMcp: x.mcp.length === 0, usedBy, unused: usedBy.length === 0 };
939
+ }
940
+
941
+ const out = [];
942
+ out.push(el('div', { style: HEAD_BLOCK },
943
+ el('div', { style: HEAD_NAME }, iv(pf.id)),
944
+ el('div', { style: HEAD_SUB }, 'claude profile · ', iv(pf.account))));
945
+ out.push(el('div', { style: FIELD_ROW },
946
+ el('span', { style: fieldLabel(2) }, 'mcp'),
947
+ el('div', { style: 'display:flex;gap:4px;flex-wrap:wrap;min-width:0' },
948
+ pf.mcp.map((mc) => el('span', { style: PILL + ';color:var(--info)' }, iv(mc))),
949
+ pf.noMcp && el('span', { style: ITALIC_EMPTY }, 'none — workspace MCP unavailable on this profile'))));
950
+ out.push(el('div', { style: FIELD_ROW },
951
+ el('span', { style: fieldLabel(2) }, 'used by'),
952
+ el('div', { style: 'display:flex;flex-direction:column;gap:2px;min-width:0' },
953
+ pf.usedBy.map((ub) => el('span', { style: 'font-size:12px' },
954
+ el('span', { style: 'font-family:var(--mono)' }, iv(ub.t)),
955
+ ' ',
956
+ el('span', { style: 'color:var(--muted-foreground)' }, '· ', iv(ub.k)))),
957
+ pf.unused && el('span', { style: ITALIC_EMPTY }, 'nothing runs on this profile right now'))));
958
+ out.push(el('div', { style: 'padding:10px 14px;font-size:11.5px;line-height:1.55;color:var(--muted-foreground)' }, iv(pf.note)));
959
+ out.push(el('div', { style: FOOTER }, '~/.claude/profiles/', iv(pf.id), '.json'));
960
+ return out;
961
+ }
962
+
963
+ /* ── `dIntg` (lines 802–835, derivation 1948–1961) ─────────────────────────────────────────── */
964
+
965
+ function intgBody(det) {
966
+ let ig = { id: '', name: '', scope: '', st: '', synced: '', does: '', hasPreview: false, previewLabel: '', onPreview: () => {}, onSync: () => {} };
967
+ const x = intgList().find((z) => z.id === det.id);
968
+ if (x) {
969
+ const pv = FX.INTG_PREVIEWS[x.id];
970
+ ig = {
971
+ ...x,
972
+ synced: (state.intgSynced && state.intgSynced[x.id]) || x.synced,
973
+ hasPreview: !!pv, previewLabel: pv ? pv[1] : '',
974
+ onPreview: () => { if (pv) openDoc(null, pv[0], x.name.toUpperCase()); },
975
+ onSync: () => setState((s) => ({ intgSynced: { ...s.intgSynced, [x.id]: '12:41 today' } })),
976
+ };
977
+ }
978
+
979
+ const out = [];
980
+ out.push(el('div', { style: HEAD_BLOCK },
981
+ el('div', { style: 'display:flex;gap:9px;align-items:baseline' },
982
+ el('span', { style: 'flex:none;font-size:10px;color:var(--success)' }, '●'),
983
+ el('span', { style: HEAD_NAME }, iv(ig.name)),
984
+ el('span', { style: 'margin-left:auto;' + PILL + ';color:var(--success)' }, iv(ig.st))),
985
+ el('div', { style: HEAD_SUB }, iv(ig.scope))));
986
+ out.push(el('div', { style: FIELD_ROW },
987
+ el('span', { style: fieldLabel(2) }, 'profile'),
988
+ el('div', { style: 'min-width:0' },
989
+ el('span', { style: 'font-family:var(--mono);font-size:11.5px' }, 'claude-work'),
990
+ el('div', { style: 'font-size:11px;color:var(--muted-foreground);margin-top:2px;line-height:1.5' },
991
+ 'the MCP server runs under claude-work — sessions on claude-personal cannot see it'))));
992
+ out.push(el('div', { style: FIELD_ROW },
993
+ el('span', { style: fieldLabel(2) }, 'synced'),
994
+ el('div', { style: 'display:flex;gap:8px;align-items:center;min-width:0' },
995
+ el('span', { style: 'font-family:var(--mono);font-size:11.5px' }, iv(ig.synced)),
996
+ el('button', {
997
+ onClick: ig.onSync,
998
+ style: 'font-family:var(--mono);font-size:10.5px;padding:4px 10px;min-height:26px;background:var(--secondary);border:1px solid var(--border)',
999
+ hover: ACTION_HOVER,
1000
+ }, '⟳ sync now'))));
1001
+ out.push(el('div', { style: FIELD_ROW },
1002
+ el('span', { style: fieldLabel(2) }, 'syncs'),
1003
+ el('span', { style: 'font-size:12px;line-height:1.55;color:var(--muted-foreground)' }, iv(ig.does))));
1004
+ if (ig.hasPreview) {
1005
+ out.push(el('div', { style: 'padding:12px 14px' },
1006
+ el('button', { onClick: ig.onPreview, style: SEC_BTN, hover: ACTION_HOVER }, iv(ig.previewLabel))));
1007
+ }
1008
+ out.push(el('div', { style: FOOTER }, 'mcp/', iv(ig.id), '.json · via claude-work'));
1009
+ return out;
1010
+ }
1011
+
1012
+ /* ══════════════════════════════════════════════════════════════════════════════════════════════
1013
+ * the two entry points
1014
+ * ════════════════════════════════════════════════════════════════════════════════════════════ */
1015
+
1016
+ const LISTS = {
1017
+ focus: focusList,
1018
+ scheduled: scheduledList,
1019
+ workflow: workflowList,
1020
+ skills: skillsList,
1021
+ knowledge: knowledgeList,
1022
+ agent: agentsList,
1023
+ connect: connectList,
1024
+ memory: memoryList,
1025
+ };
1026
+
1027
+ const BODIES = {
1028
+ flow: flowBody,
1029
+ sched: schedBody,
1030
+ skill: skillBody,
1031
+ know: knowBody,
1032
+ agent: agentBody,
1033
+ mem: memBody,
1034
+ prof: profBody,
1035
+ intg: intgBody,
1036
+ };
1037
+
1038
+ /** Is `mode` one this module renders? `product` belongs to panel-initiatives.js. */
1039
+ export function hasMode(mode) {
1040
+ return Object.prototype.hasOwnProperty.call(LISTS, mode);
1041
+ }
1042
+
1043
+ /** Is `kind` a detail body this module owns? */
1044
+ export function hasDetail(kind) {
1045
+ return Object.prototype.hasOwnProperty.call(BODIES, kind);
1046
+ }
1047
+
1048
+ /**
1049
+ * Panel 3's body for a secondary detail kind — the nodes that go BELOW panel-detail.js's sticky
1050
+ * header. An unknown kind returns null so the caller can fall back to its own empty copy.
1051
+ */
1052
+ export function detailBody(kind, detail) {
1053
+ const fn = BODIES[kind];
1054
+ if (!fn) return null;
1055
+ return fn(detail || detOf());
1056
+ }
1057
+
1058
+ /**
1059
+ * Panel 2's body for `mode`, appended to `hostList` below the header and the search box that
1060
+ * panel-initiatives.js has already put there. `hostDetail`, when given, is filled with the
1061
+ * matching detail body; it is normally null because panel 3 paints itself.
1062
+ */
1063
+ export function renderMode(hostList, hostDetail, mode) {
1064
+ const m = mode || state.mode || 'product';
1065
+ const fn = LISTS[m];
1066
+ if (hostList && fn) for (const node of fn()) if (node) hostList.appendChild(node);
1067
+
1068
+ if (hostDetail) {
1069
+ const kind = (state.detail && state.detail.kind) || 'init';
1070
+ const body = detailBody(kind, state.detail);
1071
+ setChildren(hostDetail, body || []);
1072
+ }
1073
+ return hostList;
1074
+ }
1075
+
1076
+ export default renderMode;