create-pmos 0.37.0 → 0.38.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/create-pmos.js +49 -9
- package/package.json +1 -1
- package/payload/AGENTS.md +4 -0
- package/payload/KIT-VERSION +2 -2
- package/payload/okf/core/concepts/frontmatter-format.md +27 -61
- package/payload/okf/core/templates/decision-record-template.md +34 -0
- package/payload/okf/core/templates/index.md +7 -2
- package/payload/okf/core/templates/pm-brief-template.md +34 -0
- package/payload/okf/core/templates/project-plan-template.md +43 -0
- package/payload/okf/core/templates/report-weekly-template.md +35 -0
- package/payload/okf/core/templates/strategy-long-range-template.md +37 -0
- package/payload/okf/core/templates/strategy-one-pager-template.md +34 -0
- package/payload/planning/evals/eval-calibration.md +8 -0
- package/payload/planning/okrs/health-budgets.md +8 -0
- package/payload/scripts/anchor-resolve.js +56 -6
- package/payload/scripts/gate-initiative.sh +32 -0
- package/payload/scripts/okf-upgrade.js +232 -0
- package/payload/scripts/pmos-memory.js +375 -0
- package/payload/scripts/pmos-model.js +239 -0
- package/payload/scripts/pmos-skills.js +274 -0
- package/payload/scripts/workflow-state.js +178 -10
- package/payload/scripts/workflow-sweep.js +2 -1
- package/payload/state/discoveries.md +8 -0
- package/payload/state/index.md +7 -0
- package/payload/state/log.md +7 -0
- package/workspace/README.md +61 -204
- package/workspace/bin/pmos.js +23 -4
- package/workspace/lib/board-cache.js +18 -8
- package/workspace/lib/console-state.js +2 -0
- package/workspace/lib/engine/anchor-resolve.js +56 -6
- package/workspace/lib/engine/pmos-memory.js +375 -0
- package/workspace/lib/engine/pmos-model.js +239 -0
- package/workspace/lib/engine/pmos-skills.js +274 -0
- package/workspace/lib/engine/workflow-state.js +178 -10
- package/workspace/lib/focus.js +5 -6
- package/workspace/lib/memory.js +54 -0
- package/workspace/lib/models.js +50 -0
- package/workspace/lib/preview-kind.js +12 -1
- package/workspace/lib/schedule-logic.js +31 -2
- package/workspace/lib/skill-store.js +5 -1
- package/workspace/lib/skills-registry.js +74 -0
- package/workspace/lib/sync.js +132 -0
- package/workspace/lib/tree-share.js +2 -0
- package/workspace/package-lock.json +1 -1
- package/workspace/package.json +3 -3
- package/workspace/public/pm/app.js +32 -2
- package/workspace/public/pm/data.js +160 -10
- package/workspace/public/pm/lifecycle.js +105 -0
- package/workspace/public/pm/markdown.js +10 -1
- package/workspace/public/pm/modes-live.js +437 -0
- package/workspace/public/pm/modes.js +8 -5
- package/workspace/public/pm/panel-detail.js +22 -2
- package/workspace/public/pm/panel-initiatives.js +57 -12
- package/workspace/public/pm/panel-newsess.js +25 -0
- package/workspace/public/pm/panel-repos.js +130 -4
- package/workspace/public/pm/panel-tabs.js +49 -2
- package/workspace/public/pm/state.js +11 -1
- package/workspace/public/pm/tab-doc.js +160 -26
- package/workspace/public/pm/tab-term.js +2 -0
- package/workspace/public/pm/tab-web.js +89 -0
- package/workspace/public/pm/tokens.css +4 -0
- package/workspace/public/pm/xlsx-read.js +117 -0
- package/workspace/server.js +736 -42
- package/workspace/skills/gather-requirements.skill +53 -0
- package/workspace/skills/memory-groom.skill +46 -0
- package/workspace/skills/report-biweekly.skill +53 -0
- package/workspace/skills/report-daily.skill +53 -0
- package/workspace/skills/report-monthly.skill +53 -0
- package/workspace/skills/report-special.skill +49 -0
- package/workspace/skills/report-weekly.skill +53 -0
- package/workspace/skills/skill-discover.skill +49 -0
- package/workspace/skills/skill-generate.skill +33 -0
|
@@ -0,0 +1,437 @@
|
|
|
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(readJson).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
|
+
/* A route this server does not have answers 404 with the plain text `not found`. Parsing that as JSON
|
|
44
|
+
* showed the PM "Unexpected token 'o', \"not found\" is not valid JSON" — true, and no help. The usual
|
|
45
|
+
* cause is a server still running code older than the page, so say that and the one command. */
|
|
46
|
+
function readJson(r) {
|
|
47
|
+
return r.text().then((t) => {
|
|
48
|
+
try { return JSON.parse(t); } catch {
|
|
49
|
+
throw new Error(r.status === 404
|
|
50
|
+
? 'this server does not have that route — it is probably running an older version than the page. Run `pmos` again to restart it'
|
|
51
|
+
: 'the server answered ' + r.status + ' with something that is not JSON');
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
const post = (url, body) => fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) })
|
|
56
|
+
.then(readJson).catch((e) => ({ ok: false, error: (e && e.message) || String(e) }));
|
|
57
|
+
function status(r, what) {
|
|
58
|
+
if (r.state === 'loading' && !r.data) return el('div', { style: NOTE }, iv('reading ' + what + ' …'));
|
|
59
|
+
if (r.state === 'error') return el('div', { style: NOTE + ';color:var(--destructive)' }, iv(what + ' could not be read — ' + (r.error || 'error')));
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
async function openDocIn(ws, path, repo) {
|
|
63
|
+
const ini = await import('./panel-initiatives.js');
|
|
64
|
+
const tree = ws ? { ws, branch: null, linked: true } : null;
|
|
65
|
+
if (repo && repo !== state.product) {
|
|
66
|
+
const repos = await import('./panel-repos.js');
|
|
67
|
+
repos.selectRepo(repo);
|
|
68
|
+
}
|
|
69
|
+
ini.openDoc(null, path, 'FILE', tree);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/* ── portfolio ──────────────────────────────────────────────────────────────────────────────── */
|
|
73
|
+
let reportNote = null;
|
|
74
|
+
async function writeReport(kind) {
|
|
75
|
+
let q = '';
|
|
76
|
+
if (kind === 'query') {
|
|
77
|
+
q = typeof window !== 'undefined' && window.prompt ? window.prompt('Report on initiatives matching:', state.q || '') : '';
|
|
78
|
+
if (!q || !q.trim()) return;
|
|
79
|
+
}
|
|
80
|
+
reportNote = { tone: 'mut', text: 'writing the ' + kind + ' report …' };
|
|
81
|
+
setState({});
|
|
82
|
+
const out = await post('/api/reports', { kind, q });
|
|
83
|
+
if (!out || out.ok === false) { reportNote = { tone: 'bad', text: (out && out.error) || 'the report was not written' }; setState({}); return; }
|
|
84
|
+
reportNote = { tone: 'ok', text: 'written to ' + out.path + (out.unread && out.unread.length ? ' · not fully read: ' + out.unread.join(', ') : '') };
|
|
85
|
+
const pf = cache.get('portfolio');
|
|
86
|
+
const hubName = pf && pf.data && pf.data.hub ? pf.data.hub.name : null;
|
|
87
|
+
await openDocIn(out.ws, out.path, hubName);
|
|
88
|
+
// selecting the hub to open the report switches to its initiatives; stay on the portfolio, where
|
|
89
|
+
// the PM pressed the button and where the note saying what was written is shown
|
|
90
|
+
reload('portfolio');
|
|
91
|
+
setState({ mode: 'portfolio', col1: true });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function land(row) {
|
|
95
|
+
const repos = await import('./panel-repos.js');
|
|
96
|
+
repos.selectRepo(row.repo);
|
|
97
|
+
setState({ sel: row.id, mode: 'product', detail: { kind: 'init' }, col2: true });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function portfolioList() {
|
|
101
|
+
const r = read('portfolio', '/api/portfolio');
|
|
102
|
+
const kids = [];
|
|
103
|
+
kids.push(el('div', { style: 'display:flex;gap:4px;flex-wrap:wrap;padding:8px 14px 4px', 'data-r4': 'report-bar' },
|
|
104
|
+
el('span', { style: 'font-family:var(--mono);font-size:10px;color:var(--muted-foreground);align-self:center;margin-right:4px' }, 'report →'),
|
|
105
|
+
['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))),
|
|
106
|
+
el('button', { onClick: () => { reload('portfolio'); setState({}); }, style: BTN + ';margin-left:auto', hover: 'color:var(--foreground)', title: 'read again' }, '↻')));
|
|
107
|
+
if (reportNote) kids.push(el('div', { 'data-r4': 'report-note', style: NOTE + ';color:' + (reportNote.tone === 'bad' ? 'var(--destructive)' : 'var(--muted-foreground)') }, iv(reportNote.text)));
|
|
108
|
+
const st = status(r, 'the portfolio');
|
|
109
|
+
if (st) return [...kids, st];
|
|
110
|
+
const pf = r.data || { repos: [], rows: [] };
|
|
111
|
+
if (!pf.hub) kids.push(el('div', { style: NOTE }, iv('No hub yet — reports need a portfolio.yaml in your PM repository (D-3).')));
|
|
112
|
+
const q = String(state.q || '').trim().toLowerCase();
|
|
113
|
+
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)));
|
|
114
|
+
// coverage first: what was read, how fresh, and what was NOT read
|
|
115
|
+
kids.push(el('div', { style: LABEL }, iv('coverage · ' + (pf.repos || []).length + ' repositories')));
|
|
116
|
+
for (const rp of pf.repos || []) {
|
|
117
|
+
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);
|
|
118
|
+
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)') },
|
|
119
|
+
iv(rp.name + (rp.domain ? ' (' + rp.domain + ')' : '') + ' · ' + rp.count + ' · ' + s)));
|
|
120
|
+
}
|
|
121
|
+
// then the initiatives, domain › repository
|
|
122
|
+
const byDomain = new Map();
|
|
123
|
+
for (const x of rows) {
|
|
124
|
+
const d = x.domain || 'no domain declared';
|
|
125
|
+
if (!byDomain.has(d)) byDomain.set(d, new Map());
|
|
126
|
+
const m = byDomain.get(d);
|
|
127
|
+
if (!m.has(x.repo)) m.set(x.repo, []);
|
|
128
|
+
m.get(x.repo).push(x);
|
|
129
|
+
}
|
|
130
|
+
for (const [dom, repos] of [...byDomain.entries()].sort((a, b) => a[0].localeCompare(b[0]))) {
|
|
131
|
+
kids.push(el('div', { style: LABEL + ';color:var(--foreground)', 'data-r4': 'domain' }, iv('domain · ' + dom)));
|
|
132
|
+
for (const [repo, list] of repos) {
|
|
133
|
+
kids.push(el('div', { style: LABEL + ';padding-top:4px' }, iv(repo + ' · ' + list.length)));
|
|
134
|
+
for (const x of list) {
|
|
135
|
+
const stage = x.accepted ? 'accepted' : x.stage || 'not derived';
|
|
136
|
+
kids.push(el('button', { onClick: () => land(x), style: ROW, hover: 'background:var(--accent)', 'data-r4': 'row', 'data-id': x.id, 'data-repo': x.repo },
|
|
137
|
+
el('span', { style: 'display:flex;gap:8px;align-items:baseline' },
|
|
138
|
+
el('span', { style: 'font-family:var(--mono);font-size:12px' }, iv('#' + x.id)),
|
|
139
|
+
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 : '')))),
|
|
140
|
+
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 : '')))));
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (!rows.length) kids.push(el('div', { style: NOTE }, iv(q ? 'no initiative matches "' + q + '"' : 'no initiatives in the repositories that were read')));
|
|
145
|
+
return [el('div', { style: 'padding:2px 0 8px' }, kids)];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/* ── D71 — set up the recommended schedules for the selected repository, on a press ───────────── */
|
|
149
|
+
let seedNote = null;
|
|
150
|
+
function seedRow(set, label, hint) {
|
|
151
|
+
const product = state.product;
|
|
152
|
+
const note = seedNote && seedNote.set === set ? seedNote.text : null;
|
|
153
|
+
return el('div', { 'data-r7': 'seed-' + set, style: 'padding:8px 14px;border-bottom:1px solid var(--border)' },
|
|
154
|
+
el('button', { style: BTN, hover: 'color:var(--foreground)', title: hint, onClick: async () => {
|
|
155
|
+
const data = await import('./data.js');
|
|
156
|
+
const ws = product ? data.wsOf(product) : null;
|
|
157
|
+
if (!ws) { seedNote = { set, text: 'select a repository first' }; setState({}); return; }
|
|
158
|
+
const out = await post('/api/schedules/seed', { ws, set });
|
|
159
|
+
seedNote = { set, text: !out || out.ok === false ? ((out && out.error) || 'not set up')
|
|
160
|
+
: out.created.length ? 'scheduled: ' + out.created.map((c) => c.name + ' (' + c.cadence + ' ' + c.time + ')').join(', ')
|
|
161
|
+
: 'nothing to add — ' + out.kept.map((k) => k.skill + ': ' + k.why).join('; ') };
|
|
162
|
+
reload('schedules'); setState({});
|
|
163
|
+
} }, label),
|
|
164
|
+
note ? el('span', { style: 'display:block;font-size:11px;color:var(--muted-foreground);margin-top:4px' }, iv(note)) : null);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/* ── R11 — schedule ONE special report: what it is for, a date and a time ─────────────────────── */
|
|
168
|
+
let specialNote = null;
|
|
169
|
+
function specialRow() {
|
|
170
|
+
return el('div', { 'data-r11': 'special', style: 'padding:8px 14px;border-bottom:1px solid var(--border)' },
|
|
171
|
+
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',
|
|
172
|
+
onClick: async () => {
|
|
173
|
+
const product = state.product;
|
|
174
|
+
const d = await import('./data.js');
|
|
175
|
+
const ws = product ? d.wsOf(product) : null;
|
|
176
|
+
if (!ws) { specialNote = 'select a repository first'; setState({}); return; }
|
|
177
|
+
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;
|
|
178
|
+
if (!what || !what.trim()) return;
|
|
179
|
+
const dflt = new Date(Date.now() + 86400000).toISOString().slice(0, 10);
|
|
180
|
+
const date = window.prompt('Which day should the draft be ready? (YYYY-MM-DD)', dflt);
|
|
181
|
+
if (!date) return;
|
|
182
|
+
const time = window.prompt('At what time? (HH:MM, this machine\'s clock)', '07:30');
|
|
183
|
+
if (!time) return;
|
|
184
|
+
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() });
|
|
185
|
+
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');
|
|
186
|
+
reload('schedules'); setState({});
|
|
187
|
+
} }, 'schedule a special report'),
|
|
188
|
+
specialNote ? el('span', { 'data-r11': 'special-note', style: 'display:block;font-size:11px;color:var(--muted-foreground);margin-top:4px' }, iv(specialNote)) : null);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/* ── scheduled ──────────────────────────────────────────────────────────────────────────────── */
|
|
192
|
+
export function scheduledLive() {
|
|
193
|
+
const r = read('schedules', '/api/schedules?all=1');
|
|
194
|
+
const st = status(r, 'the schedules');
|
|
195
|
+
if (st) return [st];
|
|
196
|
+
const list = (r.data && r.data.schedules) || [];
|
|
197
|
+
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'),
|
|
198
|
+
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.'),
|
|
199
|
+
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'),
|
|
200
|
+
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'),
|
|
201
|
+
specialRow()];
|
|
202
|
+
for (const s of list) {
|
|
203
|
+
const name = s.label || s.name || s.task || s.skill || s.id;
|
|
204
|
+
const cadence = (s.cadence || '') + (s.day ? ' ' + s.day : '') + (s.time ? ' at ' + s.time : '');
|
|
205
|
+
kids.push(el('div', { style: ROW, 'data-r4': 'schedule' },
|
|
206
|
+
el('span', { style: 'display:flex;gap:8px;align-items:baseline' },
|
|
207
|
+
el('span', { style: 'font-size:12.5px' }, iv(name)),
|
|
208
|
+
el('span', { style: 'margin-left:auto;font-family:var(--mono);font-size:10.5px;color:var(--muted-foreground)' }, iv(s.enabled === false ? 'disabled' : cadence))),
|
|
209
|
+
el('span', { style: 'display:block;font-family:var(--mono);font-size:10.5px;color:var(--muted-foreground)' },
|
|
210
|
+
iv('next ' + (s.next ? String(s.next).slice(0, 16).replace('T', ' ') : '—') + ' · last ' + (s.lastRun ? String(s.lastRun).slice(0, 16).replace('T', ' ') : 'never'))),
|
|
211
|
+
el('span', { style: 'display:flex;gap:4px;margin-top:4px' },
|
|
212
|
+
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'),
|
|
213
|
+
el('button', { onClick: async () => {
|
|
214
|
+
if (typeof window !== 'undefined' && window.confirm && !window.confirm('Delete the schedule "' + name + '"?')) return;
|
|
215
|
+
await post('/api/schedules/delete', { id: s.id }); reload('schedules'); setState({});
|
|
216
|
+
}, style: BTN, hover: 'color:var(--destructive)', 'data-r4': 'delete' }, 'delete'))));
|
|
217
|
+
}
|
|
218
|
+
if (!list.length) kids.push(el('div', { style: NOTE }, iv('No scheduled tasks — a successful read of nothing.')));
|
|
219
|
+
return [el('div', { style: 'padding:6px 0' }, kids)];
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/* ── memory (D72) — the repository's curated memory, for real ─────────────────────────────────── */
|
|
223
|
+
let memNote = null;
|
|
224
|
+
async function memWs() { const d = await import('./data.js'); return state.product ? d.wsOf(state.product) : null; }
|
|
225
|
+
async function memDo(body) {
|
|
226
|
+
const ws = await memWs();
|
|
227
|
+
if (!ws) { memNote = 'select a repository first'; setState({}); return; }
|
|
228
|
+
const out = await post('/api/memory', { ws, ...body });
|
|
229
|
+
memNote = out && out.ok ? (out.staged ? 'held for approval' : null) : ((out && out.error) || 'not saved');
|
|
230
|
+
if (out && !out.ok && out.entries && out.entries.length) memNote += ' — current entries: ' + out.entries.map((e) => '“' + e.text.slice(0, 40) + '”').join(', ');
|
|
231
|
+
reload('memory'); setState({});
|
|
232
|
+
}
|
|
233
|
+
const ask = (q, v) => (typeof window !== 'undefined' && window.prompt ? window.prompt(q, v || '') : null);
|
|
234
|
+
let dataMod = null;
|
|
235
|
+
import('./data.js').then((m) => { dataMod = m; setState({}); }).catch(() => { dataMod = null; });
|
|
236
|
+
export function memoryLive() {
|
|
237
|
+
const wsId = dataMod && state.product ? dataMod.wsOf(state.product) : null;
|
|
238
|
+
const ws = wsId || 'none';
|
|
239
|
+
if (!wsId) return [el('div', { style: NOTE }, iv(state.product ? 'reading the repository …' : 'select a repository to see its memory'))];
|
|
240
|
+
const r = read('memory:' + ws, '/api/memory?ws=' + encodeURIComponent(wsId));
|
|
241
|
+
const st = status(r, 'the memory');
|
|
242
|
+
const kids = [];
|
|
243
|
+
const d = r.data || {};
|
|
244
|
+
const actions = el('div', { style: 'display:flex;gap:4px;flex-wrap:wrap;padding:8px 14px 4px', 'data-r9': 'memory-bar' },
|
|
245
|
+
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'),
|
|
246
|
+
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'),
|
|
247
|
+
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',
|
|
248
|
+
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({}); } },
|
|
249
|
+
d.wired ? 'wired ✓' : 'wire into CLAUDE.md'),
|
|
250
|
+
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',
|
|
251
|
+
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({}); } },
|
|
252
|
+
'approval ' + (d.config && d.config.write_approval ? 'on' : 'off')));
|
|
253
|
+
kids.push(actions);
|
|
254
|
+
if (memNote) kids.push(el('div', { 'data-r9': 'note', style: NOTE + ';color:var(--destructive)' }, iv(memNote)));
|
|
255
|
+
if (st) return [...kids, st];
|
|
256
|
+
for (const t of ['memory', 'user']) {
|
|
257
|
+
const u = (d.usage || {})[t] || { used: 0, limit: 0, percent: 0 };
|
|
258
|
+
kids.push(el('div', { style: LABEL, 'data-r9': 'usage-' + t }, iv((t === 'memory' ? 'repository memory' : 'about you') + ' · ' + u.percent + '% · ' + u.used + '/' + u.limit)));
|
|
259
|
+
const es = (d.entries || []).filter((e) => e.target === t);
|
|
260
|
+
if (!es.length) kids.push(el('div', { style: NOTE }, iv('Nothing recorded yet.')));
|
|
261
|
+
for (const e of es) {
|
|
262
|
+
kids.push(el('div', { style: ROW, 'data-r9': 'entry', 'data-id': e.id },
|
|
263
|
+
el('span', { style: 'display:block;font-size:12px;line-height:1.45' }, iv(e.text)),
|
|
264
|
+
el('span', { style: 'display:block;font-family:var(--mono);font-size:10px;color:var(--muted-foreground);margin-top:2px' },
|
|
265
|
+
iv([e.confidence, e.stale ? 'stale' : null, e.source ? 'source ' + e.source : null, e.by, String(e.at).slice(0, 10)].filter(Boolean).join(' · '))),
|
|
266
|
+
el('span', { style: 'display:flex;gap:4px;margin-top:4px' },
|
|
267
|
+
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,
|
|
268
|
+
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'),
|
|
269
|
+
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'))));
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
if ((d.pending || []).length) {
|
|
273
|
+
kids.push(el('div', { style: LABEL + ';color:var(--foreground)', 'data-r9': 'pending-label' }, iv('waiting for your approval · ' + d.pending.length)));
|
|
274
|
+
for (const p of d.pending) {
|
|
275
|
+
kids.push(el('div', { style: ROW, 'data-r9': 'pending', 'data-eid': p.eid },
|
|
276
|
+
el('span', { style: 'display:block;font-size:12px' }, iv(p.op + ' · ' + (p.text || p.id))),
|
|
277
|
+
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', ' '))),
|
|
278
|
+
el('span', { style: 'display:flex;gap:4px;margin-top:4px' },
|
|
279
|
+
el('button', { style: BTN, hover: 'color:var(--foreground)', 'data-r9': 'approve', onClick: () => memDo({ op: 'approve', ref: p.eid }) }, 'approve'),
|
|
280
|
+
el('button', { style: BTN, hover: 'color:var(--destructive)', 'data-r9': 'reject', onClick: () => memDo({ op: 'reject', ref: p.eid }) }, 'reject'))));
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
if ((d.blocked || []).length) {
|
|
284
|
+
kids.push(el('div', { style: LABEL + ';color:var(--destructive)', 'data-r9': 'blocked-label' }, iv('blocked by the scan · ' + d.blocked.length)));
|
|
285
|
+
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')));
|
|
286
|
+
}
|
|
287
|
+
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.')));
|
|
288
|
+
return [el('div', { style: 'padding:2px 0 8px' }, kids)];
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/* ── skills ─────────────────────────────────────────────────────────────────────────────────── */
|
|
292
|
+
let candNote = null; const openText = new Map();
|
|
293
|
+
async function candDo(ws, body) {
|
|
294
|
+
const out = await post('/api/skills/decide', { ws, ...body });
|
|
295
|
+
candNote = out && out.ok ? (out.note || null) : ((out && out.error) || 'not done');
|
|
296
|
+
if (out && out.ok && body.op === 'read') openText.set(body.id + '@' + body.sha, out);
|
|
297
|
+
reload('skillcands:' + ws); reload('skills:' + ws); setState({});
|
|
298
|
+
return out;
|
|
299
|
+
}
|
|
300
|
+
function candidatesBlock(wsId) {
|
|
301
|
+
if (!wsId) return [];
|
|
302
|
+
const r = read('skillcands:' + wsId, '/api/skills/candidates?ws=' + encodeURIComponent(wsId));
|
|
303
|
+
const d = r.data || {};
|
|
304
|
+
const rows = d.candidates || [];
|
|
305
|
+
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'))];
|
|
306
|
+
if (candNote) kids.push(el('div', { 'data-r13': 'cand-note', style: NOTE }, iv(candNote)));
|
|
307
|
+
if (r.state === 'error') kids.push(el('div', { style: NOTE + ';color:var(--destructive)' }, iv('the candidates could not be read — ' + (r.error || ''))));
|
|
308
|
+
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.')));
|
|
309
|
+
for (const c of rows) {
|
|
310
|
+
const key = c.id + '@' + c.sha; const shown = openText.get(key);
|
|
311
|
+
const soft = (c.scan && c.scan.soft) || [];
|
|
312
|
+
kids.push(el('div', { style: ROW, 'data-r13': 'cand', 'data-id': c.id, 'data-state': c.state + (c.running ? ':running' : '') },
|
|
313
|
+
el('span', { style: 'display:flex;gap:8px;align-items:baseline' },
|
|
314
|
+
el('span', { style: 'font-family:var(--mono);font-size:12px' }, iv(c.id)),
|
|
315
|
+
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)' : '')))),
|
|
316
|
+
el('span', { style: 'display:block;font-size:11px;color:var(--muted-foreground)' }, iv(c.why)),
|
|
317
|
+
el('span', { style: 'display:block;font-family:var(--mono);font-size:10px;color:var(--muted-foreground)' },
|
|
318
|
+
iv([c.origin, c.license, c.author, c.source_url, '#' + c.sha12].filter(Boolean).join(' · '))),
|
|
319
|
+
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,
|
|
320
|
+
(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,
|
|
321
|
+
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,
|
|
322
|
+
el('span', { style: 'display:flex;gap:4px;margin-top:4px;flex-wrap:wrap' },
|
|
323
|
+
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'),
|
|
324
|
+
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',
|
|
325
|
+
onClick: async () => {
|
|
326
|
+
let ack = false;
|
|
327
|
+
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; }
|
|
328
|
+
await candDo(wsId, { op: 'approve', id: c.id, sha: c.sha, ack_license: ack });
|
|
329
|
+
} }, 'approve') : null,
|
|
330
|
+
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,
|
|
331
|
+
(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),
|
|
332
|
+
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));
|
|
333
|
+
}
|
|
334
|
+
return kids;
|
|
335
|
+
}
|
|
336
|
+
/* Discovery is its own section at the TOP: it used to sit under the whole skill list, below the fold,
|
|
337
|
+
* so a PM who scrolled nothing saw no discovery at all. */
|
|
338
|
+
function discoveryBlock(wsId) {
|
|
339
|
+
const sch = read('schedules', '/api/schedules?all=1');
|
|
340
|
+
const mine = ((sch.data && sch.data.schedules) || []).find((x) => x.skill === 'skill-discover' || x.task === 'skill-discover');
|
|
341
|
+
const kids = [el('div', { style: LABEL + ';color:var(--foreground)', 'data-r13': 'discovery-label' }, iv('skill discovery')),
|
|
342
|
+
el('div', { style: NOTE }, iv('Finds skills in your own repository and from sources you name. Nothing it finds runs until you read it and approve it.')),
|
|
343
|
+
el('div', { style: 'display:flex;gap:4px;flex-wrap:wrap;padding:0 14px 6px' },
|
|
344
|
+
mine ? el('button', { style: BTN, hover: 'color:var(--foreground)', 'data-r13': 'discover-now', onClick: async () => { await post('/api/schedules/run', { id: mine.id }); reload('schedules'); setState({}); } }, 'find skills now') : null,
|
|
345
|
+
mine ? el('span', { style: 'font-family:var(--mono);font-size:10.5px;color:var(--muted-foreground);align-self:center' }, iv('weekly · next ' + (mine.next ? String(mine.next).slice(0, 16).replace('T', ' ') : '—')))
|
|
346
|
+
: 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'))];
|
|
347
|
+
return [...kids, ...candidatesBlock(wsId)];
|
|
348
|
+
}
|
|
349
|
+
export function skillsLive() {
|
|
350
|
+
const wsId = dataMod && state.product ? dataMod.wsOf(state.product) : null;
|
|
351
|
+
const r = read('skills:' + (wsId || ''), '/api/skills' + (wsId ? '?ws=' + encodeURIComponent(wsId) : ''));
|
|
352
|
+
const st = status(r, 'the skills');
|
|
353
|
+
const head = discoveryBlock(wsId);
|
|
354
|
+
if (st) return [el('div', { style: 'padding:6px 0' }, [...head, st])];
|
|
355
|
+
const list = (r.data && r.data.skills) || [];
|
|
356
|
+
const open = state.detail && state.detail.kind === 'skill' ? state.detail.id : null;
|
|
357
|
+
const kids = list.map((sk) => el('button', { style: ROW + (open === sk.id ? ';background:var(--accent)' : ''), hover: 'background:var(--accent)', 'data-r4': 'skill', 'data-id': sk.id,
|
|
358
|
+
onClick: () => setState({ detail: { kind: 'skill', id: sk.id } }) },
|
|
359
|
+
el('span', { style: 'display:flex;gap:8px;align-items:baseline' },
|
|
360
|
+
el('span', { style: 'font-family:var(--mono);font-size:12px' }, iv(sk.label || sk.skill || sk.id)),
|
|
361
|
+
el('span', { style: 'margin-left:auto;font-family:var(--mono);font-size:10px;color:var(--muted-foreground)' }, iv(sk.source))),
|
|
362
|
+
el('span', { style: 'display:block;font-size:11px;color:var(--muted-foreground)' }, iv(sk.description))));
|
|
363
|
+
if (!list.length) kids.push(el('div', { style: NOTE }, iv('No skills — a successful read of nothing.')));
|
|
364
|
+
return [el('div', { style: 'padding:6px 0' }, [...head, el('div', { style: LABEL + ';color:var(--foreground)' }, iv('skills · ' + list.length + ' · click one to read it')), ...kids])];
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/* The detail of one skill (panel 3). The text is read from the server on open. Your own skills are
|
|
368
|
+
* editable and save in place; a builtin or an approved discovered skill is read-only, and "make my own
|
|
369
|
+
* copy" writes an editable one beside it, so the original is never changed under a review. */
|
|
370
|
+
let skillDraft = { id: null, text: null }; let skillNote = null;
|
|
371
|
+
export function skillDetailLive(det) {
|
|
372
|
+
const id = det && det.id;
|
|
373
|
+
const wsId = dataMod && state.product ? dataMod.wsOf(state.product) : null;
|
|
374
|
+
const r = read('skill:' + id, '/api/skills/read?id=' + encodeURIComponent(id || '') + (wsId ? '&ws=' + encodeURIComponent(wsId) : ''));
|
|
375
|
+
const st = status(r, 'this skill');
|
|
376
|
+
if (st) return [st];
|
|
377
|
+
const d = r.data;
|
|
378
|
+
if (skillDraft.id !== id) skillDraft = { id, text: null };
|
|
379
|
+
const text = skillDraft.text !== null ? skillDraft.text : d.text;
|
|
380
|
+
const dirty = skillDraft.text !== null && skillDraft.text !== d.text;
|
|
381
|
+
const out = [el('div', { style: 'padding:14px 14px 6px' },
|
|
382
|
+
el('div', { style: 'font-family:var(--mono);font-size:13px' }, iv(d.label)),
|
|
383
|
+
el('div', { style: 'font-size:11.5px;color:var(--muted-foreground);margin-top:2px' }, iv(d.source + (d.editable ? ' · yours — edit it below' : ' · read-only') + ' · ' + d.file)),
|
|
384
|
+
el('div', { style: 'font-size:12px;margin-top:6px' }, iv(d.description)),
|
|
385
|
+
(d.output || []).length ? el('div', { style: 'font-size:11px;color:var(--muted-foreground);margin-top:6px' }, iv('always produces: ' + d.output.join(' · '))) : null)];
|
|
386
|
+
out.push(el('div', { style: 'padding:6px 14px' }, el('textarea', { 'data-r13': 'skill-text', spellcheck: 'false', readonly: d.editable ? null : 'readonly',
|
|
387
|
+
style: 'width:100%;min-height:340px;font-family:var(--mono);font-size:11.5px;line-height:1.5;background:var(--secondary);border:1px solid var(--border);color:var(--foreground);padding:8px;resize:vertical',
|
|
388
|
+
onInput: (e) => { skillDraft = { id, text: e.target.value }; } }, text)));
|
|
389
|
+
out.push(el('div', { style: 'display:flex;gap:6px;align-items:center;padding:6px 14px 14px;flex-wrap:wrap' },
|
|
390
|
+
d.editable ? el('button', { style: BTN, hover: 'color:var(--foreground)', 'data-r13': 'skill-save', onClick: async () => {
|
|
391
|
+
const res = await post('/api/skills/save', { id, text: skillDraft.text !== null ? skillDraft.text : d.text });
|
|
392
|
+
skillNote = res && res.ok ? 'saved' : ((res && res.error) || 'not saved');
|
|
393
|
+
if (res && res.ok) { skillDraft = { id, text: null }; reload('skill:' + id); reload('skills:' + (wsId || '')); }
|
|
394
|
+
setState({});
|
|
395
|
+
} }, 'save') : el('button', { style: BTN, hover: 'color:var(--foreground)', 'data-r13': 'skill-copy', onClick: async () => {
|
|
396
|
+
const res = await post('/api/skills/copy', { id, ws: wsId });
|
|
397
|
+
if (res && res.ok) { skillNote = 'copied as ' + res.id + ' — it is yours to edit'; reload('skills:' + (wsId || '')); setState({ detail: { kind: 'skill', id: res.id } }); }
|
|
398
|
+
else { skillNote = (res && res.error) || 'not copied'; setState({}); }
|
|
399
|
+
} }, 'make my own copy to edit'),
|
|
400
|
+
dirty ? el('span', { style: 'font-size:11px;color:var(--muted-foreground)' }, 'unsaved changes') : null,
|
|
401
|
+
skillNote ? el('span', { 'data-r13': 'skill-note', style: 'font-size:11px;color:var(--muted-foreground)' }, iv(skillNote)) : null));
|
|
402
|
+
return out;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/* ── knowledge ──────────────────────────────────────────────────────────────────────────────── */
|
|
406
|
+
function flatten(node, out, depth) {
|
|
407
|
+
for (const d of node.docs || []) out.push({ path: d.path, name: d.name, depth });
|
|
408
|
+
if (depth < 3) for (const sub of node.dirs || []) { out.push({ dir: sub.name || sub.path, depth }); flatten(sub, out, depth + 1); }
|
|
409
|
+
return out;
|
|
410
|
+
}
|
|
411
|
+
export function knowledgeLive() {
|
|
412
|
+
const ws = state.wsId || '';
|
|
413
|
+
const r = read('library:' + ws, '/api/library?ws=' + encodeURIComponent(ws));
|
|
414
|
+
const st = status(r, 'the knowledge');
|
|
415
|
+
if (st) return [st];
|
|
416
|
+
const trees = (r.data && r.data.knowledge) || [];
|
|
417
|
+
const kids = [el('div', { style: NOTE }, iv('This repository\'s knowledge: the OKF bundle, skills and build-skills. Open a document to read it.'))];
|
|
418
|
+
for (const t of trees) {
|
|
419
|
+
kids.push(el('div', { style: LABEL }, iv(t.path || t.name || 'knowledge')));
|
|
420
|
+
for (const it of flatten(t, [], 0)) {
|
|
421
|
+
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; }
|
|
422
|
+
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)));
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
// R11 — the notes PMOS and its agents wrote here (requirement notes, report drafts and definitions, memory).
|
|
426
|
+
const notes = (r.data && r.data.notes) || [];
|
|
427
|
+
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.')));
|
|
428
|
+
for (const t of notes) {
|
|
429
|
+
kids.push(el('div', { style: LABEL, 'data-r11': 'notes-label' }, iv(t.path || t.name || 'notes')));
|
|
430
|
+
for (const it of flatten(t, [], 0)) {
|
|
431
|
+
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; }
|
|
432
|
+
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)));
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
if (!trees.length && !notes.length) kids.push(el('div', { style: NOTE }, iv('No knowledge bundle in this repository — a successful read of nothing.')));
|
|
436
|
+
return [el('div', { style: 'padding:2px 0 8px' }, kids)];
|
|
437
|
+
}
|
|
@@ -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,19 +1038,21 @@ function intgBody(det) {
|
|
|
1037
1038
|
|
|
1038
1039
|
const LISTS = {
|
|
1039
1040
|
focus: focusList,
|
|
1040
|
-
|
|
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 = {
|
|
1050
1053
|
flow: flowBody,
|
|
1051
1054
|
sched: schedBody,
|
|
1052
|
-
skill: skillBody,
|
|
1055
|
+
skill: (det) => (FIXTURE ? skillBody(det) : LIVE.skillDetailLive(det)),
|
|
1053
1056
|
know: knowBody,
|
|
1054
1057
|
agent: agentBody,
|
|
1055
1058
|
mem: memBody,
|
|
@@ -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
|
-
|
|
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;
|