create-pmos 0.36.0 → 0.38.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 (71) hide show
  1. package/bin/create-pmos.js +22 -3
  2. package/package.json +1 -1
  3. package/payload/AGENTS.md +6 -0
  4. package/payload/KIT-VERSION +2 -2
  5. package/payload/okf/core/concepts/frontmatter-format.md +27 -61
  6. package/payload/okf/core/templates/decision-record-template.md +34 -0
  7. package/payload/okf/core/templates/index.md +7 -2
  8. package/payload/okf/core/templates/pm-brief-template.md +34 -0
  9. package/payload/okf/core/templates/project-plan-template.md +43 -0
  10. package/payload/okf/core/templates/report-weekly-template.md +35 -0
  11. package/payload/okf/core/templates/strategy-long-range-template.md +37 -0
  12. package/payload/okf/core/templates/strategy-one-pager-template.md +34 -0
  13. package/payload/planning/evals/eval-calibration.md +8 -0
  14. package/payload/planning/okrs/health-budgets.md +8 -0
  15. package/payload/scripts/anchor-resolve.js +56 -6
  16. package/payload/scripts/gate-initiative.sh +32 -0
  17. package/payload/scripts/okf-upgrade.js +232 -0
  18. package/payload/scripts/pmos-memory.js +375 -0
  19. package/payload/scripts/pmos-model.js +239 -0
  20. package/payload/scripts/pmos-skills.js +274 -0
  21. package/payload/scripts/workflow-state.js +178 -10
  22. package/payload/scripts/workflow-sweep.js +2 -1
  23. package/payload/state/discoveries.md +8 -0
  24. package/payload/state/index.md +7 -0
  25. package/payload/state/log.md +7 -0
  26. package/workspace/README.md +61 -204
  27. package/workspace/lib/board-cache.js +18 -8
  28. package/workspace/lib/console-state.js +2 -0
  29. package/workspace/lib/engine/anchor-resolve.js +56 -6
  30. package/workspace/lib/engine/pmos-memory.js +375 -0
  31. package/workspace/lib/engine/pmos-model.js +239 -0
  32. package/workspace/lib/engine/pmos-skills.js +274 -0
  33. package/workspace/lib/engine/workflow-state.js +178 -10
  34. package/workspace/lib/focus.js +5 -6
  35. package/workspace/lib/memory.js +54 -0
  36. package/workspace/lib/models.js +50 -0
  37. package/workspace/lib/preview-kind.js +12 -1
  38. package/workspace/lib/schedule-logic.js +31 -2
  39. package/workspace/lib/skill-store.js +5 -1
  40. package/workspace/lib/skills-registry.js +74 -0
  41. package/workspace/lib/sync.js +132 -0
  42. package/workspace/lib/tree-share.js +2 -0
  43. package/workspace/package-lock.json +2 -2
  44. package/workspace/package.json +3 -3
  45. package/workspace/public/pm/app.js +32 -2
  46. package/workspace/public/pm/data.js +160 -10
  47. package/workspace/public/pm/lifecycle.js +105 -0
  48. package/workspace/public/pm/markdown.js +10 -1
  49. package/workspace/public/pm/modes-live.js +371 -0
  50. package/workspace/public/pm/modes.js +7 -4
  51. package/workspace/public/pm/panel-detail.js +22 -2
  52. package/workspace/public/pm/panel-initiatives.js +57 -12
  53. package/workspace/public/pm/panel-newsess.js +25 -0
  54. package/workspace/public/pm/panel-repos.js +130 -4
  55. package/workspace/public/pm/panel-tabs.js +49 -2
  56. package/workspace/public/pm/state.js +11 -1
  57. package/workspace/public/pm/tab-doc.js +160 -26
  58. package/workspace/public/pm/tab-term.js +2 -0
  59. package/workspace/public/pm/tab-web.js +89 -0
  60. package/workspace/public/pm/tokens.css +4 -0
  61. package/workspace/public/pm/xlsx-read.js +117 -0
  62. package/workspace/server.js +690 -40
  63. package/workspace/skills/gather-requirements.skill +53 -0
  64. package/workspace/skills/memory-groom.skill +46 -0
  65. package/workspace/skills/report-biweekly.skill +53 -0
  66. package/workspace/skills/report-daily.skill +53 -0
  67. package/workspace/skills/report-monthly.skill +53 -0
  68. package/workspace/skills/report-special.skill +49 -0
  69. package/workspace/skills/report-weekly.skill +53 -0
  70. package/workspace/skills/skill-discover.skill +49 -0
  71. package/workspace/skills/skill-generate.skill +33 -0
@@ -0,0 +1,371 @@
1
+ /* modes-live.js — the drawer modes that render SERVER data in live mode (overhaul-r4).
2
+ *
3
+ * modes.js renders the prototype's seeded lists, and in live mode those lists start empty and every
4
+ * edit is a client-only setState (seedOr). R1 hid the three modes with no server behind them; this
5
+ * module gives the other three their server truth, and adds the portfolio:
6
+ *
7
+ * portfolio — GET /api/portfolio: every registered repository's initiatives, grouped by domain
8
+ * then repository, freshness stated, unread repositories listed; a report bar that
9
+ * writes a weekly / OKR / query report into the hub (POST /api/reports, a PM press).
10
+ * scheduled — GET /api/schedules; run / delete through the existing routes.
11
+ * skills — GET /api/skills; a row with a file opens it.
12
+ * knowledge — GET /api/library's knowledge trees (OKF, skills, build-skills); a row opens it.
13
+ *
14
+ * Every list states its read: loading, failed (with the reason), or a successful read of nothing.
15
+ * Fixture mode never reaches this module (modes.js routes here only when !FIXTURE).
16
+ */
17
+ import { el } from './dom.js';
18
+ import { state, setState } from './state.js';
19
+
20
+ const iv = (v) => el('span', null, v === null || v === undefined ? '' : String(v));
21
+ const LABEL = 'padding:10px 14px 4px;font-family:var(--mono);font-size:9.5px;letter-spacing:.12em;text-transform:uppercase;color:var(--muted-foreground)';
22
+ const ROW = 'display:block;width:100%;padding:7px 14px;min-height:40px;text-align:left;border-bottom:1px solid oklch(1 0 0 / 5%)';
23
+ const BTN = 'font-family:var(--mono);font-size:10.5px;padding:3px 9px;border:1px solid var(--border);color:var(--muted-foreground);min-height:26px';
24
+ const NOTE = 'padding:8px 14px;font-size:11.5px;color:var(--muted-foreground);line-height:1.5';
25
+
26
+ /* one cached read per endpoint; `reload(key)` drops it so the next render refetches */
27
+ const cache = new Map();
28
+ function read(key, url) {
29
+ const hit = cache.get(key);
30
+ if (hit && (hit.state !== 'ok' || Date.now() - hit.at < 15000)) return hit;
31
+ if (!hit || hit.state === 'ok') {
32
+ const next = { state: hit ? hit.state : 'loading', data: hit ? hit.data : null, at: hit ? hit.at : 0 };
33
+ cache.set(key, next);
34
+ fetch(url).then((r) => r.json()).then(
35
+ (data) => { cache.set(key, { state: data && data.ok === false ? 'error' : 'ok', data, error: data && data.error, at: Date.now() }); setState({}); },
36
+ (e) => { cache.set(key, { state: 'error', error: (e && e.message) || String(e), at: Date.now() }); setState({}); },
37
+ );
38
+ return next;
39
+ }
40
+ return hit;
41
+ }
42
+ const reload = (key) => cache.delete(key);
43
+ const post = (url, body) => fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) })
44
+ .then((r) => r.json()).catch((e) => ({ ok: false, error: (e && e.message) || String(e) }));
45
+ function status(r, what) {
46
+ if (r.state === 'loading' && !r.data) return el('div', { style: NOTE }, iv('reading ' + what + ' …'));
47
+ if (r.state === 'error') return el('div', { style: NOTE + ';color:var(--destructive)' }, iv(what + ' could not be read — ' + (r.error || 'error')));
48
+ return null;
49
+ }
50
+ async function openDocIn(ws, path, repo) {
51
+ const ini = await import('./panel-initiatives.js');
52
+ const tree = ws ? { ws, branch: null, linked: true } : null;
53
+ if (repo && repo !== state.product) {
54
+ const repos = await import('./panel-repos.js');
55
+ repos.selectRepo(repo);
56
+ }
57
+ ini.openDoc(null, path, 'FILE', tree);
58
+ }
59
+
60
+ /* ── portfolio ──────────────────────────────────────────────────────────────────────────────── */
61
+ let reportNote = null;
62
+ async function writeReport(kind) {
63
+ let q = '';
64
+ if (kind === 'query') {
65
+ q = typeof window !== 'undefined' && window.prompt ? window.prompt('Report on initiatives matching:', state.q || '') : '';
66
+ if (!q || !q.trim()) return;
67
+ }
68
+ reportNote = { tone: 'mut', text: 'writing the ' + kind + ' report …' };
69
+ setState({});
70
+ const out = await post('/api/reports', { kind, q });
71
+ if (!out || out.ok === false) { reportNote = { tone: 'bad', text: (out && out.error) || 'the report was not written' }; setState({}); return; }
72
+ reportNote = { tone: 'ok', text: 'written to ' + out.path + (out.unread && out.unread.length ? ' · not fully read: ' + out.unread.join(', ') : '') };
73
+ const pf = cache.get('portfolio');
74
+ const hubName = pf && pf.data && pf.data.hub ? pf.data.hub.name : null;
75
+ await openDocIn(out.ws, out.path, hubName);
76
+ // selecting the hub to open the report switches to its initiatives; stay on the portfolio, where
77
+ // the PM pressed the button and where the note saying what was written is shown
78
+ reload('portfolio');
79
+ setState({ mode: 'portfolio', col1: true });
80
+ }
81
+
82
+ async function land(row) {
83
+ const repos = await import('./panel-repos.js');
84
+ repos.selectRepo(row.repo);
85
+ setState({ sel: row.id, mode: 'product', detail: { kind: 'init' }, col2: true });
86
+ }
87
+
88
+ export function portfolioList() {
89
+ const r = read('portfolio', '/api/portfolio');
90
+ const kids = [];
91
+ kids.push(el('div', { style: 'display:flex;gap:4px;flex-wrap:wrap;padding:8px 14px 4px', 'data-r4': 'report-bar' },
92
+ el('span', { style: 'font-family:var(--mono);font-size:10px;color:var(--muted-foreground);align-self:center;margin-right:4px' }, 'report →'),
93
+ ['weekly', 'okr', 'query'].map((k) => el('button', { onClick: () => writeReport(k), style: BTN, hover: 'color:var(--foreground)', 'data-r4': 'report-' + k }, iv(k === 'okr' ? 'OKR' : k))),
94
+ el('button', { onClick: () => { reload('portfolio'); setState({}); }, style: BTN + ';margin-left:auto', hover: 'color:var(--foreground)', title: 'read again' }, '↻')));
95
+ if (reportNote) kids.push(el('div', { 'data-r4': 'report-note', style: NOTE + ';color:' + (reportNote.tone === 'bad' ? 'var(--destructive)' : 'var(--muted-foreground)') }, iv(reportNote.text)));
96
+ const st = status(r, 'the portfolio');
97
+ if (st) return [...kids, st];
98
+ const pf = r.data || { repos: [], rows: [] };
99
+ if (!pf.hub) kids.push(el('div', { style: NOTE }, iv('No hub yet — reports need a portfolio.yaml in your PM repository (D-3).')));
100
+ const q = String(state.q || '').trim().toLowerCase();
101
+ const rows = (pf.rows || []).filter((x) => !q || [x.id, x.title, x.group, x.repo, x.domain].some((v) => v && String(v).toLowerCase().includes(q)));
102
+ // coverage first: what was read, how fresh, and what was NOT read
103
+ kids.push(el('div', { style: LABEL }, iv('coverage · ' + (pf.repos || []).length + ' repositories')));
104
+ for (const rp of pf.repos || []) {
105
+ const s = rp.error ? 'NOT READ — ' + rp.error : rp.pending ? 'board pending — stages arriving' : rp.stale ? 'stale · as of ' + String(rp.as_of).slice(11, 16) : 'fresh · as of ' + String(rp.as_of || '').slice(11, 16);
106
+ kids.push(el('div', { 'data-r4': 'coverage', style: 'padding:2px 14px;font-family:var(--mono);font-size:10.5px;color:' + (rp.error ? 'var(--destructive)' : 'var(--muted-foreground)') },
107
+ iv(rp.name + (rp.domain ? ' (' + rp.domain + ')' : '') + ' · ' + rp.count + ' · ' + s)));
108
+ }
109
+ // then the initiatives, domain › repository
110
+ const byDomain = new Map();
111
+ for (const x of rows) {
112
+ const d = x.domain || 'no domain declared';
113
+ if (!byDomain.has(d)) byDomain.set(d, new Map());
114
+ const m = byDomain.get(d);
115
+ if (!m.has(x.repo)) m.set(x.repo, []);
116
+ m.get(x.repo).push(x);
117
+ }
118
+ for (const [dom, repos] of [...byDomain.entries()].sort((a, b) => a[0].localeCompare(b[0]))) {
119
+ kids.push(el('div', { style: LABEL + ';color:var(--foreground)', 'data-r4': 'domain' }, iv('domain · ' + dom)));
120
+ for (const [repo, list] of repos) {
121
+ kids.push(el('div', { style: LABEL + ';padding-top:4px' }, iv(repo + ' · ' + list.length)));
122
+ for (const x of list) {
123
+ const stage = x.accepted ? 'accepted' : x.stage || 'not derived';
124
+ kids.push(el('button', { onClick: () => land(x), style: ROW, hover: 'background:var(--accent)', 'data-r4': 'row', 'data-id': x.id, 'data-repo': x.repo },
125
+ el('span', { style: 'display:flex;gap:8px;align-items:baseline' },
126
+ el('span', { style: 'font-family:var(--mono);font-size:12px' }, iv('#' + x.id)),
127
+ el('span', { style: 'margin-left:auto;font-family:var(--mono);font-size:10px;color:' + (x.blocked ? 'var(--destructive)' : 'var(--muted-foreground)') }, iv(stage + (x.blocked ? ' ✗' : '') + (x.sessions ? ' · ● ' + x.sessions : '')))),
128
+ el('span', { style: 'display:block;font-size:11px;color:var(--muted-foreground);overflow:hidden;text-overflow:ellipsis;white-space:nowrap' }, iv(x.group + (x.next ? ' — next: ' + x.next : '')))));
129
+ }
130
+ }
131
+ }
132
+ if (!rows.length) kids.push(el('div', { style: NOTE }, iv(q ? 'no initiative matches "' + q + '"' : 'no initiatives in the repositories that were read')));
133
+ return [el('div', { style: 'padding:2px 0 8px' }, kids)];
134
+ }
135
+
136
+ /* ── D71 — set up the recommended schedules for the selected repository, on a press ───────────── */
137
+ let seedNote = null;
138
+ function seedRow(set, label, hint) {
139
+ const product = state.product;
140
+ const note = seedNote && seedNote.set === set ? seedNote.text : null;
141
+ return el('div', { 'data-r7': 'seed-' + set, style: 'padding:8px 14px;border-bottom:1px solid var(--border)' },
142
+ el('button', { style: BTN, hover: 'color:var(--foreground)', title: hint, onClick: async () => {
143
+ const data = await import('./data.js');
144
+ const ws = product ? data.wsOf(product) : null;
145
+ if (!ws) { seedNote = { set, text: 'select a repository first' }; setState({}); return; }
146
+ const out = await post('/api/schedules/seed', { ws, set });
147
+ seedNote = { set, text: !out || out.ok === false ? ((out && out.error) || 'not set up')
148
+ : out.created.length ? 'scheduled: ' + out.created.map((c) => c.name + ' (' + c.cadence + ' ' + c.time + ')').join(', ')
149
+ : 'nothing to add — ' + out.kept.map((k) => k.skill + ': ' + k.why).join('; ') };
150
+ reload('schedules'); setState({});
151
+ } }, label),
152
+ note ? el('span', { style: 'display:block;font-size:11px;color:var(--muted-foreground);margin-top:4px' }, iv(note)) : null);
153
+ }
154
+
155
+ /* ── R11 — schedule ONE special report: what it is for, a date and a time ─────────────────────── */
156
+ let specialNote = null;
157
+ function specialRow() {
158
+ return el('div', { 'data-r11': 'special', style: 'padding:8px 14px;border-bottom:1px solid var(--border)' },
159
+ el('button', { style: BTN, hover: 'color:var(--foreground)', title: 'a one-off report: say what it is for and when you want the draft; it runs once',
160
+ onClick: async () => {
161
+ const product = state.product;
162
+ const d = await import('./data.js');
163
+ const ws = product ? d.wsOf(product) : null;
164
+ if (!ws) { specialNote = 'select a repository first'; setState({}); return; }
165
+ const what = typeof window !== 'undefined' && window.prompt ? window.prompt('What is the special report for? Who reads it, what period or topic, roughly how long?') : null;
166
+ if (!what || !what.trim()) return;
167
+ const dflt = new Date(Date.now() + 86400000).toISOString().slice(0, 10);
168
+ const date = window.prompt('Which day should the draft be ready? (YYYY-MM-DD)', dflt);
169
+ if (!date) return;
170
+ const time = window.prompt('At what time? (HH:MM, this machine\'s clock)', '07:30');
171
+ if (!time) return;
172
+ const out = await post('/api/schedules', { ws, name: 'Special report — ' + what.trim().slice(0, 40), cadence: 'once', date: date.trim(), time: time.trim(), skill: 'report-special', task: 'report-special', args: what.trim() });
173
+ specialNote = out && out.ok ? 'scheduled once for ' + date.trim() + ' at ' + time.trim() + ' — it fires only while the workspace is running' : ((out && out.error) || 'not scheduled');
174
+ reload('schedules'); setState({});
175
+ } }, 'schedule a special report'),
176
+ specialNote ? el('span', { 'data-r11': 'special-note', style: 'display:block;font-size:11px;color:var(--muted-foreground);margin-top:4px' }, iv(specialNote)) : null);
177
+ }
178
+
179
+ /* ── scheduled ──────────────────────────────────────────────────────────────────────────────── */
180
+ export function scheduledLive() {
181
+ const r = read('schedules', '/api/schedules?all=1');
182
+ const st = status(r, 'the schedules');
183
+ if (st) return [st];
184
+ const list = (r.data && r.data.schedules) || [];
185
+ const kids = [seedRow('requirements', 'set up daily requirement gathering', 'schedules the gather-requirements skill daily at 07:30 for the selected repository; notes are filed in that repository'),
186
+ seedRow('reports', 'set up report drafts (daily · weekly · bi-weekly · monthly)', 'schedules the four report skills for the selected repository; each follows your own definition and files a draft in that repository. A special report is run on demand.'),
187
+ seedRow('skills', 'set up weekly skill discovery', 'schedules the skill-discover skill weekly (Wednesday 09:00) for the selected repository; it files candidates for you to read and never approves them'),
188
+ seedRow('memory', 'set up weekly memory grooming', 'schedules the memory-groom skill weekly (Monday 08:00) for the selected repository; it proposes changes and never makes them'),
189
+ specialRow()];
190
+ for (const s of list) {
191
+ const name = s.label || s.name || s.task || s.skill || s.id;
192
+ const cadence = (s.cadence || '') + (s.day ? ' ' + s.day : '') + (s.time ? ' at ' + s.time : '');
193
+ kids.push(el('div', { style: ROW, 'data-r4': 'schedule' },
194
+ el('span', { style: 'display:flex;gap:8px;align-items:baseline' },
195
+ el('span', { style: 'font-size:12.5px' }, iv(name)),
196
+ el('span', { style: 'margin-left:auto;font-family:var(--mono);font-size:10.5px;color:var(--muted-foreground)' }, iv(s.enabled === false ? 'disabled' : cadence))),
197
+ el('span', { style: 'display:block;font-family:var(--mono);font-size:10.5px;color:var(--muted-foreground)' },
198
+ iv('next ' + (s.next ? String(s.next).slice(0, 16).replace('T', ' ') : '—') + ' · last ' + (s.lastRun ? String(s.lastRun).slice(0, 16).replace('T', ' ') : 'never'))),
199
+ el('span', { style: 'display:flex;gap:4px;margin-top:4px' },
200
+ el('button', { onClick: async () => { await post('/api/schedules/run', { id: s.id }); reload('schedules'); setState({}); }, style: BTN, hover: 'color:var(--foreground)', 'data-r4': 'run' }, 'run now'),
201
+ el('button', { onClick: async () => {
202
+ if (typeof window !== 'undefined' && window.confirm && !window.confirm('Delete the schedule "' + name + '"?')) return;
203
+ await post('/api/schedules/delete', { id: s.id }); reload('schedules'); setState({});
204
+ }, style: BTN, hover: 'color:var(--destructive)', 'data-r4': 'delete' }, 'delete'))));
205
+ }
206
+ if (!list.length) kids.push(el('div', { style: NOTE }, iv('No scheduled tasks — a successful read of nothing.')));
207
+ return [el('div', { style: 'padding:6px 0' }, kids)];
208
+ }
209
+
210
+ /* ── memory (D72) — the repository's curated memory, for real ─────────────────────────────────── */
211
+ let memNote = null;
212
+ async function memWs() { const d = await import('./data.js'); return state.product ? d.wsOf(state.product) : null; }
213
+ async function memDo(body) {
214
+ const ws = await memWs();
215
+ if (!ws) { memNote = 'select a repository first'; setState({}); return; }
216
+ const out = await post('/api/memory', { ws, ...body });
217
+ memNote = out && out.ok ? (out.staged ? 'held for approval' : null) : ((out && out.error) || 'not saved');
218
+ if (out && !out.ok && out.entries && out.entries.length) memNote += ' — current entries: ' + out.entries.map((e) => '“' + e.text.slice(0, 40) + '”').join(', ');
219
+ reload('memory'); setState({});
220
+ }
221
+ const ask = (q, v) => (typeof window !== 'undefined' && window.prompt ? window.prompt(q, v || '') : null);
222
+ let dataMod = null;
223
+ import('./data.js').then((m) => { dataMod = m; setState({}); }).catch(() => { dataMod = null; });
224
+ export function memoryLive() {
225
+ const wsId = dataMod && state.product ? dataMod.wsOf(state.product) : null;
226
+ const ws = wsId || 'none';
227
+ if (!wsId) return [el('div', { style: NOTE }, iv(state.product ? 'reading the repository …' : 'select a repository to see its memory'))];
228
+ const r = read('memory:' + ws, '/api/memory?ws=' + encodeURIComponent(wsId));
229
+ const st = status(r, 'the memory');
230
+ const kids = [];
231
+ const d = r.data || {};
232
+ const actions = el('div', { style: 'display:flex;gap:4px;flex-wrap:wrap;padding:8px 14px 4px', 'data-r9': 'memory-bar' },
233
+ el('button', { style: BTN, hover: 'color:var(--foreground)', 'data-r9': 'add', onClick: () => { const t = ask('Add to repository memory (one short fact):'); if (t && t.trim()) memDo({ op: 'add', target: 'memory', text: t.trim() }); } }, '+ memory'),
234
+ el('button', { style: BTN, hover: 'color:var(--foreground)', 'data-r9': 'add-user', onClick: () => { const t = ask('Add about you (a preference or how you work):'); if (t && t.trim()) memDo({ op: 'add', target: 'user', text: t.trim() }); } }, '+ about me'),
235
+ el('button', { style: BTN, hover: 'color:var(--foreground)', 'data-r9': 'wire', title: 'add a block to this repository\'s CLAUDE.md so each session reads the memory at start',
236
+ onClick: async () => { const w = await memWs(); const out = w ? await post('/api/memory/wire', { ws: w }) : null; memNote = out && out.ok ? (out.changed ? 'CLAUDE.md now imports the memory' : 'already wired') : ((out && out.error) || 'not wired'); reload('memory:' + ws); setState({}); } },
237
+ d.wired ? 'wired ✓' : 'wire into CLAUDE.md'),
238
+ el('button', { style: BTN + ';margin-left:auto', hover: 'color:var(--foreground)', 'data-r9': 'approval', title: 'when on, writes by an agent wait here for your approval',
239
+ onClick: async () => { const w = await memWs(); if (!w) return; await post('/api/memory/config', { ws: w, write_approval: !(d.config && d.config.write_approval) }); reload('memory:' + ws); setState({}); } },
240
+ 'approval ' + (d.config && d.config.write_approval ? 'on' : 'off')));
241
+ kids.push(actions);
242
+ if (memNote) kids.push(el('div', { 'data-r9': 'note', style: NOTE + ';color:var(--destructive)' }, iv(memNote)));
243
+ if (st) return [...kids, st];
244
+ for (const t of ['memory', 'user']) {
245
+ const u = (d.usage || {})[t] || { used: 0, limit: 0, percent: 0 };
246
+ kids.push(el('div', { style: LABEL, 'data-r9': 'usage-' + t }, iv((t === 'memory' ? 'repository memory' : 'about you') + ' · ' + u.percent + '% · ' + u.used + '/' + u.limit)));
247
+ const es = (d.entries || []).filter((e) => e.target === t);
248
+ if (!es.length) kids.push(el('div', { style: NOTE }, iv('Nothing recorded yet.')));
249
+ for (const e of es) {
250
+ kids.push(el('div', { style: ROW, 'data-r9': 'entry', 'data-id': e.id },
251
+ el('span', { style: 'display:block;font-size:12px;line-height:1.45' }, iv(e.text)),
252
+ el('span', { style: 'display:block;font-family:var(--mono);font-size:10px;color:var(--muted-foreground);margin-top:2px' },
253
+ iv([e.confidence, e.stale ? 'stale' : null, e.source ? 'source ' + e.source : null, e.by, String(e.at).slice(0, 10)].filter(Boolean).join(' · '))),
254
+ el('span', { style: 'display:flex;gap:4px;margin-top:4px' },
255
+ e.confidence === 'unconfirmed' ? el('button', { style: BTN, hover: 'color:var(--foreground)', 'data-r9': 'confirm', onClick: () => memDo({ op: 'confirm', target: t, old: e.text }) }, 'confirm') : null,
256
+ el('button', { style: BTN, hover: 'color:var(--foreground)', 'data-r9': 'edit', onClick: () => { const nt = ask('Edit this entry:', e.text); if (nt && nt.trim() && nt.trim() !== e.text) memDo({ op: 'replace', target: t, old: e.text, text: nt.trim() }); } }, 'edit'),
257
+ el('button', { style: BTN, hover: 'color:var(--destructive)', 'data-r9': 'remove', onClick: () => { if (window.confirm && !window.confirm('Remove this entry?')) return; memDo({ op: 'remove', target: t, old: e.text }); } }, 'remove'))));
258
+ }
259
+ }
260
+ if ((d.pending || []).length) {
261
+ kids.push(el('div', { style: LABEL + ';color:var(--foreground)', 'data-r9': 'pending-label' }, iv('waiting for your approval · ' + d.pending.length)));
262
+ for (const p of d.pending) {
263
+ kids.push(el('div', { style: ROW, 'data-r9': 'pending', 'data-eid': p.eid },
264
+ el('span', { style: 'display:block;font-size:12px' }, iv(p.op + ' · ' + (p.text || p.id))),
265
+ el('span', { style: 'display:block;font-family:var(--mono);font-size:10px;color:var(--muted-foreground)' }, iv(p.by + ' · ' + String(p.at).slice(0, 16).replace('T', ' '))),
266
+ el('span', { style: 'display:flex;gap:4px;margin-top:4px' },
267
+ el('button', { style: BTN, hover: 'color:var(--foreground)', 'data-r9': 'approve', onClick: () => memDo({ op: 'approve', ref: p.eid }) }, 'approve'),
268
+ el('button', { style: BTN, hover: 'color:var(--destructive)', 'data-r9': 'reject', onClick: () => memDo({ op: 'reject', ref: p.eid }) }, 'reject'))));
269
+ }
270
+ }
271
+ if ((d.blocked || []).length) {
272
+ kids.push(el('div', { style: LABEL + ';color:var(--destructive)', 'data-r9': 'blocked-label' }, iv('blocked by the scan · ' + d.blocked.length)));
273
+ for (const b of d.blocked) kids.push(el('div', { style: NOTE, 'data-r9': 'blocked' }, iv(b.reason + ' · ' + b.by + ' · ' + String(b.at).slice(0, 10) + ' · not shown to any session')));
274
+ }
275
+ kids.push(el('div', { style: NOTE }, iv('Sessions read this once, when they start. Unconfirmed items are included and labelled. It travels with the repository when you Sync.')));
276
+ return [el('div', { style: 'padding:2px 0 8px' }, kids)];
277
+ }
278
+
279
+ /* ── skills ─────────────────────────────────────────────────────────────────────────────────── */
280
+ let candNote = null; const openText = new Map();
281
+ async function candDo(ws, body) {
282
+ const out = await post('/api/skills/decide', { ws, ...body });
283
+ candNote = out && out.ok ? (out.note || null) : ((out && out.error) || 'not done');
284
+ if (out && out.ok && body.op === 'read') openText.set(body.id + '@' + body.sha, out);
285
+ reload('skillcands:' + ws); reload('skills:' + ws); setState({});
286
+ return out;
287
+ }
288
+ function candidatesBlock(wsId) {
289
+ if (!wsId) return [];
290
+ const r = read('skillcands:' + wsId, '/api/skills/candidates?ws=' + encodeURIComponent(wsId));
291
+ const d = r.data || {};
292
+ const rows = d.candidates || [];
293
+ const kids = [el('div', { style: LABEL + ';color:var(--foreground)', 'data-r13': 'cands-label' }, iv('found or generated · ' + rows.length + ' · none runs until you approve it'))];
294
+ if (candNote) kids.push(el('div', { 'data-r13': 'cand-note', style: NOTE }, iv(candNote)));
295
+ if (r.state === 'error') kids.push(el('div', { style: NOTE + ';color:var(--destructive)' }, iv('the candidates could not be read — ' + (r.error || ''))));
296
+ if (!rows.length && r.state !== 'error') kids.push(el('div', { style: NOTE }, iv('No candidates yet. Set up weekly discovery, or ask for skills to be generated from your repeated work.')));
297
+ for (const c of rows) {
298
+ const key = c.id + '@' + c.sha; const shown = openText.get(key);
299
+ const soft = (c.scan && c.scan.soft) || [];
300
+ kids.push(el('div', { style: ROW, 'data-r13': 'cand', 'data-id': c.id, 'data-state': c.state + (c.running ? ':running' : '') },
301
+ el('span', { style: 'display:flex;gap:8px;align-items:baseline' },
302
+ el('span', { style: 'font-family:var(--mono);font-size:12px' }, iv(c.id)),
303
+ el('span', { style: 'margin-left:auto;font-family:var(--mono);font-size:10px;color:var(--muted-foreground)' }, iv(c.running ? 'approved · running' : c.state + (c.approved ? ' (approved here)' : '')))),
304
+ el('span', { style: 'display:block;font-size:11px;color:var(--muted-foreground)' }, iv(c.why)),
305
+ el('span', { style: 'display:block;font-family:var(--mono);font-size:10px;color:var(--muted-foreground)' },
306
+ iv([c.origin, c.license, c.author, c.source_url, '#' + c.sha12].filter(Boolean).join(' · '))),
307
+ soft.length ? el('span', { 'data-r13': 'findings', style: 'display:block;font-size:10.5px;color:var(--destructive)' }, iv('flagged: ' + soft.map((f) => f.rule + ' (line ' + f.line + ')').join(', '))) : null,
308
+ (c.ignored_keys || []).length ? el('span', { style: 'display:block;font-size:10.5px;color:var(--muted-foreground)' }, iv('its own ' + c.ignored_keys.join(', ') + ' will be ignored — it runs as an agent skill on claude')) : null,
309
+ c.collides ? el('span', { style: 'display:block;font-size:10.5px;color:var(--destructive)' }, iv('a skill with this name already exists and wins over it')) : null,
310
+ el('span', { style: 'display:flex;gap:4px;margin-top:4px;flex-wrap:wrap' },
311
+ el('button', { style: BTN, hover: 'color:var(--foreground)', 'data-r13': 'read', onClick: () => { if (shown) { openText.delete(key); setState({}); } else candDo(wsId, { op: 'read', id: c.id, sha: c.sha }); } }, shown ? 'hide the text' : 'read it'),
312
+ c.state === 'quarantined' && !c.running ? el('button', { style: BTN, hover: 'color:var(--foreground)', 'data-r13': 'approve', disabled: c.read ? null : 'disabled', title: c.read ? 'approve exactly this text, on this machine' : 'open the full text first',
313
+ onClick: async () => {
314
+ let ack = false;
315
+ if (c.needs_license_ack) { ack = !!(window.confirm && window.confirm('Its licence is "' + c.license + '". Have you read it, and do you accept it?')); if (!ack) return; }
316
+ await candDo(wsId, { op: 'approve', id: c.id, sha: c.sha, ack_license: ack });
317
+ } }, 'approve') : null,
318
+ c.state === 'quarantined' ? el('button', { style: BTN, hover: 'color:var(--destructive)', 'data-r13': 'reject', onClick: () => { const why = window.prompt ? window.prompt('Why reject “' + c.id + '”? (optional)', '') : ''; if (why === null) return; candDo(wsId, { op: 'reject', id: c.id, sha: c.sha, reason: why }); } }, 'reject') : null,
319
+ (c.running || c.approved) && c.state === 'quarantined' ? el('button', { style: BTN, hover: 'color:var(--destructive)', 'data-r13': 'retire', onClick: () => candDo(wsId, { op: 'retire', id: c.id, sha: c.sha, reason: 'retired by the PM' }) }, 'retire') : null),
320
+ shown ? el('pre', { 'data-r13': 'text', style: 'white-space:pre-wrap;overflow-wrap:anywhere;font-family:var(--mono);font-size:11px;line-height:1.5;background:var(--secondary);border:1px solid var(--border);padding:8px;margin:6px 0 0;max-height:320px;overflow:auto' }, iv(shown.text)) : null));
321
+ }
322
+ return kids;
323
+ }
324
+ export function skillsLive() {
325
+ const wsId = dataMod && state.product ? dataMod.wsOf(state.product) : null;
326
+ const r = read('skills:' + (wsId || ''), '/api/skills' + (wsId ? '?ws=' + encodeURIComponent(wsId) : ''));
327
+ const st = status(r, 'the skills');
328
+ if (st) return [st];
329
+ const list = (r.data && r.data.skills) || [];
330
+ const kids = list.map((sk) => el('div', { style: ROW, 'data-r4': 'skill' },
331
+ el('span', { style: 'display:flex;gap:8px;align-items:baseline' },
332
+ el('span', { style: 'font-family:var(--mono);font-size:12px' }, iv(sk.label || sk.skill || sk.id)),
333
+ el('span', { style: 'margin-left:auto;font-family:var(--mono);font-size:10px;color:var(--muted-foreground)' }, iv(sk.source))),
334
+ el('span', { style: 'display:block;font-size:11px;color:var(--muted-foreground)' }, iv(sk.description))));
335
+ if (!list.length) kids.push(el('div', { style: NOTE }, iv('No skills — a successful read of nothing.')));
336
+ return [el('div', { style: 'padding:6px 0' }, [...kids, ...candidatesBlock(wsId)])];
337
+ }
338
+
339
+ /* ── knowledge ──────────────────────────────────────────────────────────────────────────────── */
340
+ function flatten(node, out, depth) {
341
+ for (const d of node.docs || []) out.push({ path: d.path, name: d.name, depth });
342
+ if (depth < 3) for (const sub of node.dirs || []) { out.push({ dir: sub.name || sub.path, depth }); flatten(sub, out, depth + 1); }
343
+ return out;
344
+ }
345
+ export function knowledgeLive() {
346
+ const ws = state.wsId || '';
347
+ const r = read('library:' + ws, '/api/library?ws=' + encodeURIComponent(ws));
348
+ const st = status(r, 'the knowledge');
349
+ if (st) return [st];
350
+ const trees = (r.data && r.data.knowledge) || [];
351
+ const kids = [el('div', { style: NOTE }, iv('This repository\'s knowledge: the OKF bundle, skills and build-skills. Open a document to read it.'))];
352
+ for (const t of trees) {
353
+ kids.push(el('div', { style: LABEL }, iv(t.path || t.name || 'knowledge')));
354
+ for (const it of flatten(t, [], 0)) {
355
+ if (it.dir) { kids.push(el('div', { style: 'padding:3px 14px 1px ' + (14 + it.depth * 12) + 'px;font-family:var(--mono);font-size:10px;color:var(--muted-foreground)' }, iv(it.dir + '/'))); continue; }
356
+ kids.push(el('button', { onClick: () => openDocIn(null, it.path), style: 'display:block;width:100%;text-align:left;padding:4px 14px 4px ' + (20 + it.depth * 12) + 'px;font-family:var(--mono);font-size:11.5px', hover: 'background:var(--accent)', 'data-r4': 'know' }, iv(it.name)));
357
+ }
358
+ }
359
+ // R11 — the notes PMOS and its agents wrote here (requirement notes, report drafts and definitions, memory).
360
+ const notes = (r.data && r.data.notes) || [];
361
+ if (notes.length) kids.push(el('div', { style: NOTE, 'data-r11': 'notes-intro' }, iv('Notes PMOS and your agents wrote in this repository — requirement notes, report drafts and definitions, memory.')));
362
+ for (const t of notes) {
363
+ kids.push(el('div', { style: LABEL, 'data-r11': 'notes-label' }, iv(t.path || t.name || 'notes')));
364
+ for (const it of flatten(t, [], 0)) {
365
+ if (it.dir) { kids.push(el('div', { style: 'padding:3px 14px 1px ' + (14 + it.depth * 12) + 'px;font-family:var(--mono);font-size:10px;color:var(--muted-foreground)' }, iv(it.dir + '/'))); continue; }
366
+ kids.push(el('button', { onClick: () => openDocIn(null, it.path), 'data-r11': 'note', style: 'display:block;width:100%;text-align:left;padding:4px 14px 4px ' + (20 + it.depth * 12) + 'px;font-family:var(--mono);font-size:11px;min-height:26px' }, iv(it.name || it.path)));
367
+ }
368
+ }
369
+ if (!trees.length && !notes.length) kids.push(el('div', { style: NOTE }, iv('No knowledge bundle in this repository — a successful read of nothing.')));
370
+ return [el('div', { style: 'padding:2px 0 8px' }, kids)];
371
+ }
@@ -48,6 +48,7 @@ import { state, setState, FIXTURE } from './state.js';
48
48
  import * as FX from './fixture.js';
49
49
  import { openDoc, openTerm } from './panel-initiatives.js';
50
50
  import { boardOf, records, currentProduct } from './board-read.js';
51
+ import * as LIVE from './modes-live.js'; // overhaul-r4: server-backed renderers, live mode only
51
52
 
52
53
  /* ── the prototype's small helpers ─────────────────────────────────────────────────────────── */
53
54
 
@@ -1037,13 +1038,15 @@ function intgBody(det) {
1037
1038
 
1038
1039
  const LISTS = {
1039
1040
  focus: focusList,
1040
- scheduled: scheduledList,
1041
+ // overhaul-r4: in live mode these three render their server routes; the fixture keeps the prototype's
1042
+ scheduled: () => (FIXTURE ? scheduledList() : LIVE.scheduledLive()),
1041
1043
  workflow: workflowList,
1042
- skills: skillsList,
1043
- knowledge: knowledgeList,
1044
+ skills: () => (FIXTURE ? skillsList() : LIVE.skillsLive()),
1045
+ knowledge: () => (FIXTURE ? knowledgeList() : LIVE.knowledgeLive()),
1046
+ portfolio: () => (FIXTURE ? [] : LIVE.portfolioList()),
1044
1047
  agent: agentsList,
1045
1048
  connect: connectList,
1046
- memory: memoryList,
1049
+ memory: () => (FIXTURE ? memoryList() : LIVE.memoryLive()),
1047
1050
  };
1048
1051
 
1049
1052
  const BODIES = {
@@ -54,6 +54,7 @@ import { state, setState, FIXTURE } from './state.js';
54
54
  import * as FX from './fixture.js';
55
55
  import { openDoc, openTab, openTerm, fileNewModel } from './panel-initiatives.js';
56
56
  import { addProductBody } from './panel-repos.js';
57
+ import { lifecycleRow } from './lifecycle.js';
57
58
  import { boardOf, records, currentProduct } from './board-read.js';
58
59
 
59
60
  /* ── the prototype's small helpers ─────────────────────────────────────────────────────────── */
@@ -271,7 +272,24 @@ function sessionsBlock(slug) {
271
272
 
272
273
  /* ── 1 · HEADER — slug, tag, pills (lines 338–346) ──────────────────────────────────────────── */
273
274
 
274
- function headerBlock(name, tag, pills) {
275
+ /* overhaul-r2 B3 — the breadcrumb, under the pills. Gaps are drawn muted and italic and SAY what is
276
+ * missing; they are never an empty slot. The KR's baseline → target line and a domain's standing
277
+ * goal sit beneath it, because "which metric does this move?" is the question the crumb answers. */
278
+ function crumbBlock(c) {
279
+ if (!c || !c.parts || !c.parts.length) return null;
280
+ const kids = [];
281
+ c.parts.forEach((pt, i) => {
282
+ if (i) kids.push(el('span', { style: 'color:var(--muted-foreground)' }, ' › '));
283
+ kids.push(el('span', { title: pt.k, style: pt.gap ? 'font-style:italic;color:var(--muted-foreground)' : 'color:var(--foreground)' }, iv(pt.v)));
284
+ });
285
+ return el('div', { 'data-r2': 'crumb', style: 'margin-top:8px;font-size:11.5px;line-height:1.5' },
286
+ el('div', null, kids),
287
+ c.line ? el('div', { 'data-r2': 'kr-line', style: 'font-family:var(--mono);font-size:10.5px;color:var(--muted-foreground);margin-top:3px' }, iv('measure · ' + c.line)) : null,
288
+ c.goal ? el('div', { 'data-r2': 'domain-goal', style: 'font-size:11px;color:var(--muted-foreground);margin-top:3px' }, iv('standing goal · ' + c.goal)) : null,
289
+ );
290
+ }
291
+
292
+ function headerBlock(name, tag, pills, crumb) {
275
293
  return el('div', { style: 'padding:13px 14px 11px;border-bottom:1px solid var(--border)' },
276
294
  el('div', { style: 'font-family:var(--mono);font-size:15px;font-weight:600;letter-spacing:-.01em;overflow:hidden;text-overflow:ellipsis;white-space:nowrap' }, iv(name)),
277
295
  el('div', { style: 'font-size:12px;color:var(--muted-foreground);margin-top:2px' }, iv(tag)),
@@ -279,6 +297,7 @@ function headerBlock(name, tag, pills) {
279
297
  pills.map((pl) => el('span', {
280
298
  style: 'font-family:var(--mono);font-size:10px;padding:2px 6px;background:var(--secondary);border:1px solid var(--border);color:' + pl.c,
281
299
  }, iv(pl.t)))),
300
+ crumbBlock(crumb),
282
301
  );
283
302
  }
284
303
 
@@ -982,7 +1001,8 @@ function initBody(slug) {
982
1001
  return out;
983
1002
  }
984
1003
 
985
- out.push(headerBlock(slug, rec.tag || '', rec.pills || []));
1004
+ out.push(headerBlock(slug, rec.tag || '', rec.pills || [], rec.crumb || null));
1005
+ { const lc = lifecycleRow(repoOf(slug), slug); if (lc) out.push(lc); }
986
1006
 
987
1007
  if (rec.noLadder) {
988
1008
  const ansC = FX.TONE[(rec.answer && rec.answer.tone) || 'info'] || FX.C.mut;
@@ -265,9 +265,15 @@ export function openTerm(slug, sess) {
265
265
  });
266
266
  }
267
267
 
268
- export function openDoc(slug, path, role) {
268
+ export function openDoc(slug, path, role, tree) {
269
+ /* `tree` — the line of work a search hit came from (overhaul-r1 R1.3). A hit on a LINKED worktree
270
+ * opens a tab that carries that tree's `ws` and `branch`, under its own id, so it is read from and
271
+ * saved to that tree and can sit beside the main tree's copy of the same path. A main-tree hit
272
+ * opens exactly the tab it always did. */
273
+ const linked = !!(tree && tree.ws && tree.linked);
269
274
  openTab({
270
- id: 'doc:' + path, kind: 'doc', title: path.split('/').pop(), slug,
275
+ id: 'doc:' + (linked ? tree.ws + ':' : '') + path, kind: 'doc', title: path.split('/').pop(), slug,
276
+ ...(linked ? { ws: tree.ws, branch: tree.branch || null } : {}),
271
277
  /* A file opened from the FOLDER search belongs to no initiative, so `slugProduct()` has no
272
278
  * answer for it — and `panel-tabs.visibleTabs` shows a tab whose repo is `undefined` in EVERY
273
279
  * repository, which would hang this workspace's file in another one's tab strip and read its
@@ -522,15 +528,20 @@ function dataModule() {
522
528
  return null;
523
529
  }
524
530
 
531
+ /* overhaul-r4 B2 — "all repositories": the same box, widened to every registered repository. The
532
+ * cache is keyed by scope too, so the two answers never mix. */
533
+ const scopeKey = (q) => (state.searchAll ? 'all\u0000' : '') + q;
525
534
  function requestFiles(q) {
526
- fileWanted = q;
535
+ const key = scopeKey(q);
536
+ const all = !!state.searchAll;
537
+ fileWanted = key;
527
538
  if (fileTimer) clearTimeout(fileTimer);
528
539
  fileTimer = setTimeout(() => {
529
540
  fileTimer = null;
530
541
  const mod = dataModule();
531
542
  if (!mod || typeof mod.searchFiles !== 'function') return;
532
- const asked = q;
533
- Promise.resolve(mod.searchFiles(asked)).then(
543
+ const asked = key;
544
+ Promise.resolve(mod.searchFiles(q, undefined, all)).then(
534
545
  (r) => {
535
546
  if (fileWanted !== asked) return; // the PM has typed past this query
536
547
  fileCache.set(asked, (r && r.ok === false)
@@ -539,6 +550,8 @@ function requestFiles(q) {
539
550
  hits: (r && r.hits) || [], truncated: !!(r && r.truncated),
540
551
  contentTruncated: !!(r && r.content_truncated),
541
552
  scanned: (r && r.scanned) || 0, contentScanned: (r && r.content_scanned) || 0, error: null,
553
+ trees: Array.isArray(r && r.trees) ? r.trees : [],
554
+ repos: Array.isArray(r && r.repos) ? r.repos : null,
542
555
  });
543
556
  if (fileCache.size > 40) fileCache.delete(fileCache.keys().next().value);
544
557
  setState({});
@@ -555,12 +568,34 @@ function requestFiles(q) {
555
568
  /** The result for `q`, or null while nothing has come back yet. Kicks the fetch off on a miss. */
556
569
  function fileResults(q) {
557
570
  if (!q || FIXTURE) return null;
558
- if (fileCache.has(q)) return fileCache.get(q);
571
+ if (fileCache.has(scopeKey(q))) return fileCache.get(scopeKey(q));
559
572
  requestFiles(q);
560
573
  return null;
561
574
  }
562
575
 
563
- const fileLabel = (h) => 'file · ' + (h.kind || 'file') + (h.match === 'content' ? ' · line ' + h.line : '');
576
+ /* overhaul-r1 R1.2 — every hit names its line of work. A hit merged across trees (identical
577
+ * content) names every branch; a hit from a linked worktree names its branch with `⑂`; a hit only
578
+ * on the main tree of a repository with no linked trees says nothing extra. */
579
+ const isLinked = (ws) => !!(dataMod && typeof dataMod.isLinkedTree === 'function' && dataMod.isLinkedTree(ws));
580
+ const hitTree = (h) => (h && h.ws ? { ws: h.ws, branch: h.branch || null, linked: isLinked(h.ws) } : null);
581
+ function branchTag(h) {
582
+ const bs = Array.isArray(h.branches) ? h.branches.filter(Boolean) : [];
583
+ if (bs.length > 1) return ' · on ' + bs.join(', ');
584
+ if (h.ws && isLinked(h.ws)) return ' · ⑂ ' + (h.branch || 'detached');
585
+ return '';
586
+ }
587
+ /* A hit from ANOTHER repository (all-repositories scope) selects that repository first, then opens
588
+ * the file from the hit's own tree, so it is read where it was found. */
589
+ async function openHit(h) {
590
+ if (h.repo && h.repo !== state.product) {
591
+ const repos = await import('./panel-repos.js');
592
+ repos.selectRepo(h.repo);
593
+ openDoc(null, h.path, 'FILE', h.ws ? { ws: h.ws, branch: h.branch || null, linked: true } : null);
594
+ return;
595
+ }
596
+ openDoc(null, h.path, 'FILE', hitTree(h));
597
+ }
598
+ const fileLabel = (h) => (h.repo && state.searchAll ? h.repo + ' · ' : '') + 'file · ' + (h.kind || 'file') + (h.match === 'content' ? ' · line ' + h.line : '') + branchTag(h);
564
599
 
565
600
  function fileHitNode(h) {
566
601
  const where = h.dir ? h.dir + '/' : '';
@@ -575,7 +610,7 @@ function fileHitNode(h) {
575
610
  );
576
611
  }
577
612
  return el('button', {
578
- onClick: () => openDoc(null, h.path, 'FILE'),
613
+ onClick: () => openHit(h),
579
614
  style: 'display:block;width:100%;padding:6px 12px;min-height:40px;text-align:left',
580
615
  hover: 'background:var(--accent)',
581
616
  title: h.path,
@@ -593,10 +628,17 @@ function fileHitNode(h) {
593
628
  export function fileHitsBlock(q) {
594
629
  if (!q || FIXTURE) return null;
595
630
  const r = fileResults(q);
631
+ const scope = state.searchAll ? 'files in every repository' : 'files in this folder';
596
632
  const head = (text, tone) => el('div', {
597
- style: 'padding:4px 12px 4px;font-family:var(--mono);font-size:10px;letter-spacing:.12em;'
633
+ style: 'display:flex;gap:8px;align-items:center;padding:4px 12px 4px;font-family:var(--mono);font-size:10px;letter-spacing:.12em;'
598
634
  + 'text-transform:uppercase;color:' + (tone || 'var(--muted-foreground)') + ';border-bottom:1px solid var(--border)',
599
- }, iv(text));
635
+ }, iv(String(text).replace(/^files in this folder/, scope)),
636
+ // overhaul-r4 B2 — widen the same search to every registered repository
637
+ el('button', {
638
+ onClick: () => setState((st) => ({ searchAll: !st.searchAll })),
639
+ style: 'margin-left:auto;letter-spacing:0;text-transform:none;font-size:10px;padding:1px 6px;border:1px solid var(--border);color:var(--muted-foreground)',
640
+ hover: 'color:var(--foreground)', 'data-r4': 'search-all', title: 'search every registered repository',
641
+ }, iv((state.searchAll ? '☑' : '☐') + ' all repositories')));
600
642
 
601
643
  if (!r) return el('div', { 'data-pm-files': '' }, head('files in this folder · searching…'));
602
644
  if (r.error) {
@@ -608,8 +650,11 @@ export function fileHitsBlock(q) {
608
650
  * conclude from an empty list. `contentTruncated` means every file was NAMED and some bodies
609
651
  * were not read, which is the ordinary state of any repository holding a lock file. Folding the
610
652
  * second into the first would light the serious flag permanently and thereby retire it. */
653
+ // overhaul-r1 R1.4: with several lines of work, say WHICH walk was cut short
654
+ const cut = (r.trees || []).length > 1 ? (r.trees || []).filter((t) => t.truncated).map((t) => t.branch || 'detached') : [];
655
+ const lines = (r.trees || []).length > 1 ? ' across ' + r.trees.length + ' lines of work' : '';
611
656
  const bound = r.truncated
612
- ? ' · BOUNDED — files beyond the walk limit were not looked at'
657
+ ? ' · BOUNDED — files beyond the walk limit were not looked at' + (cut.length ? ' (on ' + cut.join(', ') + ')' : '')
613
658
  : (r.contentTruncated ? ' · all ' + r.scanned + ' names searched; ' + r.contentScanned + ' bodies read' : '');
614
659
 
615
660
  if (!r.hits.length) {
@@ -620,7 +665,7 @@ export function fileHitsBlock(q) {
620
665
  ? 'no match in the ' + r.scanned + ' files searched — the walk was BOUNDED, more were not looked at'
621
666
  : 'no file matches — a successful read of nothing' + (r.contentTruncated ? bound : ''))));
622
667
  }
623
- const kids = [head('files in this folder · ' + r.hits.length + ' of ' + r.scanned + ' searched' + bound)];
668
+ const kids = [head('files in this folder · ' + r.hits.length + ' of ' + r.scanned + ' searched' + lines + bound)];
624
669
  for (const h of r.hits) kids.push(fileHitNode(h));
625
670
  return el('div', { 'data-pm-files': '' }, kids);
626
671
  }