create-pmos 0.150.0 → 0.152.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/payload/KIT-VERSION +1 -1
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/state/discoveries.md +1 -1
- package/workspace/guide/ask-the-agent.md +5 -5
- package/workspace/guide/console-calendar.md +1 -1
- package/workspace/guide/document-sheet.md +58 -0
- package/workspace/guide/index.md +1 -0
- package/workspace/lib/engine/doc-health.js +4 -2
- package/workspace/lib/engine/doc-sheet.js +49 -1
- package/workspace/lib/notes.js +31 -6
- package/workspace/lib/routes-doc-health.js +15 -4
- package/workspace/lib/routes-doc-sheet.js +154 -13
- package/workspace/lib/routes-notes.js +4 -4
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/c3-demo-boards.js +6 -1
- package/workspace/public/pm/c3-demo-init.js +44 -3
- package/workspace/public/pm/c3-demo-rep2.js +27 -6
- package/workspace/public/pm/c3-doc-sheet.js +586 -184
- package/workspace/public/pm/c3-md.js +395 -0
- package/workspace/public/pm/c3-notes.js +35 -16
- package/workspace/public/pm/console3.css +169 -0
- package/workspace/public/pm/dest-notes.js +1 -0
- package/workspace/public/pm/pm-work-view.js +1 -1
- package/workspace/public/pm/tokens.css +2 -2
- package/workspace/test-lists/c3-doc-sheet-rich.json +6 -0
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* [{ id, t, minute, text, owner, cat, cat_by, by: 'agent', open?, check?, dropped? }];
|
|
8
8
|
* an open bullet has cat null. bullets_state: { agent: 'on'|'off'|'failed',
|
|
9
9
|
* reason, session, next_at_ms, minute_ms, minutes }
|
|
10
|
-
* POST /api/notes/bullet { id, bullet, cat?, drop?, text? } → { ok, bullet, summary } the PM's correction to one bullet (Run 8)
|
|
10
|
+
* POST /api/notes/bullet { id, bullet, cat?, drop?, text?, owner?, due? } → { ok, bullet, summary } the PM's correction to one bullet (Run 8)
|
|
11
11
|
* POST /api/notes/new { title?, meeting?, initiative? } → { ok, note, reused } a calendar event that already has a note
|
|
12
12
|
* gets that note back (reused: true), so one meeting keeps one note (Run 4)
|
|
13
13
|
* POST /api/notes/link { id, meeting?, initiative?, from_event? } → { ok, note, kept? } the PM's link; null unlinks (Run 4)
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
* POST /api/notes/highlight-remove { id, hid } → { ok }
|
|
63
63
|
* POST /api/notes/wispr-fetch { id } → { ok, wispr } the PM asks for Wispr Flow's notes now, after the automatic fetch failed or
|
|
64
64
|
* found nothing; the agent session is asked at once and again every two minutes for up to 20 minutes
|
|
65
|
-
* POST /api/notes/final-edit { id, sec: 'overview'|'decision'|'action'|'question', i?, text } → { ok, final_notes } the PM's correction
|
|
65
|
+
* POST /api/notes/final-edit { id, sec: 'overview'|'decision'|'action'|'question', i?, text, field?: 'owner'|'due'|'why' } → { ok, final_notes } the PM's correction
|
|
66
66
|
*
|
|
67
67
|
* Notes are the PM's own and live in the per-user runtime directory (ctx.runtimeDir), never in a repository, so no route here
|
|
68
68
|
* writes into a registered workspace. Every POST goes through pm-guard `requirePagePress` (a body that is not an object or carries
|
|
@@ -812,7 +812,7 @@ async function handle(req, res, url, ctx) {
|
|
|
812
812
|
const q = queueFinal(ctx, id);
|
|
813
813
|
return done(q.queued ? { ok: true, queued: true } : { ok: false, status: 409, error: q.reason });
|
|
814
814
|
}
|
|
815
|
-
if (p === '/api/notes/bullet') return done(notes.editBullet(dir, { id, bid: body.bullet, cat: body.cat, drop: body.drop, text: body.text }));
|
|
815
|
+
if (p === '/api/notes/bullet') return done(notes.editBullet(dir, { id, bid: body.bullet, cat: body.cat, drop: body.drop, text: body.text, owner: body.owner, due: body.due }));
|
|
816
816
|
if (p === '/api/notes/line') {
|
|
817
817
|
const pick = (k) => (body[k] === undefined ? undefined : body[k]);
|
|
818
818
|
return done(notes.editLine(dir, { id, index: Number(body.index), cat: pick('cat'), summary: pick('summary'), tags: pick('tags'), people: pick('people'), text: pick('text') }));
|
|
@@ -963,7 +963,7 @@ async function handle(req, res, url, ctx) {
|
|
|
963
963
|
}
|
|
964
964
|
if (p === '/api/notes/highlight-remove') return done(notes.removeHighlight(dir, { id, hid: body.hid }));
|
|
965
965
|
if (p === '/api/notes/wispr-fetch') return done(fetchWisprNow(ctx, id));
|
|
966
|
-
if (p === '/api/notes/final-edit') return done(notes.editFinal(dir, { id, sec: body.sec, i: body.i, text: body.text }));
|
|
966
|
+
if (p === '/api/notes/final-edit') return done(notes.editFinal(dir, { id, sec: body.sec, i: body.i, text: body.text, field: body.field }));
|
|
967
967
|
// summarize-agent: the handoff file first (refused for a private note), then the PM's own agent session
|
|
968
968
|
let w = null;
|
|
969
969
|
if (body.ws != null) {
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmos-workspace",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.152.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "pmos-workspace",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.152.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@xterm/addon-fit": "^0.11.0",
|
package/workspace/package.json
CHANGED
|
@@ -562,7 +562,7 @@ export function handle(S, method, path, p, body) {
|
|
|
562
562
|
|
|
563
563
|
/* ── views of other routes, for the Releases, Docs and initiative tabs (c3-ib-releases.js, c3-ib-docs.js, c3-ii-tabs.js) ──
|
|
564
564
|
* These answer only a request that carries `board=` (the tabs add it; the old page never does), so the old demo fixtures keep answering
|
|
565
|
-
* everything else: GET /api/releases, /api/docs/library, /api/my-work, /api/c3-init/breakdown, /api/doc and /api/doc-sheet/revisions.
|
|
565
|
+
* everything else: GET /api/releases, /api/docs/library, /api/my-work, /api/c3-init/breakdown, /api/doc and /api/doc-sheet/revisions, /proposals and /log.
|
|
566
566
|
* The rows are made from the boards' featured initiatives. IDPF-07 gets no documents on purpose, so the empty state can be seen. */
|
|
567
567
|
function handleMore(S, method, path, p) {
|
|
568
568
|
if (method !== 'GET') return undefined;
|
|
@@ -574,6 +574,11 @@ function handleMore(S, method, path, p) {
|
|
|
574
574
|
if (path === '/api/doc') return { status: 200, text };
|
|
575
575
|
return okb({ path: p.path, revisions: [{ rev: 1, by: 'human:wawan', at: at(-3, 9) + '.000Z', why: 'First version', before_hash: '', after_hash: '' }] });
|
|
576
576
|
}
|
|
577
|
+
/* the document sheet's Proposals and Edit log tabs: none yet, one first version */
|
|
578
|
+
if ((path === '/api/doc-sheet/proposals' || path === '/api/doc-sheet/log') && /^planning\/ib\//.test(p.path || '')) {
|
|
579
|
+
if (path === '/api/doc-sheet/proposals') return okb({ path: p.path, proposals: [] });
|
|
580
|
+
return okb({ path: p.path, revisions: [{ rev: 1, by: 'human:wawan', at: at(-3, 9) + '.000Z', why: 'First version', via: 'sheet', text_by: 'person', before_hash: '', after_hash: '' }], events: [] });
|
|
581
|
+
}
|
|
577
582
|
if (!p.board) return undefined;
|
|
578
583
|
if (path === '/api/releases') {
|
|
579
584
|
return okb({ board: { name: 'Demo board' }, releases: S.releases.filter((x) => x.board === p.board).map((x) => ({ id: x.id, name: x.name, type: 'both', initiative_ids: x.scope.slice(), deploy_at: x.date + 'T11:00:00', release_at: x.date + 'T12:00:00', accepted_at: x.status === 'Released' ? x.date + 'T12:30:00' : null, gapFlags: [], tags: { detected: [], reason: null }, summary: [], status: x.status, board: x.board })) });
|
|
@@ -266,15 +266,56 @@ POST['/api/doc-sheet/edit'] = (b) => {
|
|
|
266
266
|
const PROPS = {};
|
|
267
267
|
POST['/api/doc-sheet/propose'] = (b) => {
|
|
268
268
|
const R = revsOf(b.path); if (!R) return { ok: false, error: 'could not read the document' };
|
|
269
|
-
const id = nextId('prop'); PROPS[id] = { path: b.path, prompt: b.prompt, after: b.after, hash: hashOf(b.after), before: R[R.length - 1].text };
|
|
269
|
+
const id = nextId('prop'); PROPS[id] = { path: b.path, prompt: b.prompt, after: b.after, hash: hashOf(b.after), before: R[R.length - 1].text, state: 'proposed', at: '2026-10-08T08:10:00.000Z' };
|
|
270
270
|
return { ok: true, id, state: 'proposed', hash: PROPS[id].hash, diff: [], voided_hash: null };
|
|
271
271
|
};
|
|
272
272
|
POST['/api/doc-sheet/accept'] = (b) => {
|
|
273
273
|
const p = PROPS[b.id]; if (!p || p.path !== b.path) return { ok: false, error: 'no such proposal' };
|
|
274
274
|
if (b.hash !== p.hash) return { ok: false, error: 'the hash does not match the proposal the PM saw', reason: 'hash mismatch' };
|
|
275
|
-
const R = revsOf(b.path);
|
|
276
|
-
|
|
275
|
+
const R = revsOf(b.path); const cur = R[R.length - 1].text;
|
|
276
|
+
const names = Array.isArray(b.sections) ? b.sections : null;
|
|
277
|
+
const m = names ? demoMerge(cur, p.before, p.after, names) : cur === p.before ? { ok: true, text: p.after } : { ok: false, stale: ['the document'] };
|
|
278
|
+
if (!m.ok) return { ok: false, error: 'stale: ' + m.stale.join(', ') + ' changed since this proposal was made', reason: 'stale' };
|
|
279
|
+
p.state = 'accepted'; p.taken = names || demoChanged(p.before, p.after); p.closed_at = '2026-10-08T08:12:00.000Z';
|
|
280
|
+
const rev = withRev(b.path, 'claude-code/session', p.prompt, m.text); const r = R[R.length - 1]; r.via = 'proposal'; r.text_by = 'agent'; r.proposal = b.id; r.sections = p.taken; r.of = demoChanged(p.before, p.after).length;
|
|
281
|
+
return { ok: true, path: b.path, rev };
|
|
277
282
|
};
|
|
283
|
+
/* the rich document sheet (c3-doc-sheet-rich): the proposals, the edit log, reject, a reason, mark current — the routes' shapes, from the fixtures */
|
|
284
|
+
const demoSecs = (t) => { const out = []; let cur = { name: 'Overview', head: '', body: '' }; for (const l of String(t).split('\n')) { const h = /^#{1,6}\s+(.*)$/.exec(l); if (h) { out.push(cur); cur = { name: h[1].trim(), head: l + '\n', body: '' }; } else cur.body += l + '\n'; } out.push(cur); return out.filter((x) => x.head || x.body.trim()); };
|
|
285
|
+
const demoChanged = (a, b) => { const A = demoSecs(a); const B = demoSecs(b); return [...B.filter((x) => { const y = A.find((q) => q.name === x.name); return !y || y.body.trim() !== x.body.trim(); }).map((x) => x.name), ...A.filter((y) => !B.some((x) => x.name === y.name)).map((y) => y.name)]; };
|
|
286
|
+
function demoMerge(cur, before, after, names) {
|
|
287
|
+
const C = demoSecs(cur); const A = demoSecs(before); const P = demoSecs(after);
|
|
288
|
+
const stale = names.filter((n) => { const a = A.find((x) => x.name === n); const c = C.find((x) => x.name === n); return a ? !c || c.body.trim() !== a.body.trim() : !!c; });
|
|
289
|
+
if (stale.length) return { ok: false, stale };
|
|
290
|
+
for (const n of names) { const p = P.find((x) => x.name === n); const k = C.findIndex((x) => x.name === n); if (!p) { if (k >= 0) C.splice(k, 1); } else if (k >= 0) C[k] = p; else C.push(p); }
|
|
291
|
+
return { ok: true, text: C.map((x) => x.head + x.body).join('').replace(/\n*$/, '\n') };
|
|
292
|
+
}
|
|
293
|
+
GET['/api/doc-sheet/proposals'] = (q) => {
|
|
294
|
+
const path = pathOf(q); const R = revsOf(path); if (!R) return { ok: false, error: 'could not read the document' };
|
|
295
|
+
const cur = demoSecs(R[R.length - 1].text);
|
|
296
|
+
return { ok: true, path, proposals: Object.entries(PROPS).filter(([, p]) => p.path === path).map(([id, p]) => {
|
|
297
|
+
const A = demoSecs(p.before); const B = demoSecs(p.after);
|
|
298
|
+
return { id, state: p.state, prompt: p.prompt, at: p.at, hash: p.hash, by: 'agent', why: p.why || null, taken: p.taken || null, of: null, closed_at: p.closed_at || null,
|
|
299
|
+
sections: demoChanged(p.before, p.after).map((name) => { const a = A.find((x) => x.name === name); const b = B.find((x) => x.name === name); const c = cur.find((x) => x.name === name);
|
|
300
|
+
return { name, kind: !a ? 'added' : !b ? 'removed' : 'changed', before: a ? a.body.trim() : '', after: b ? b.body.trim() : '', current: c ? c.body.trim() : null, stale: a ? !c || c.body.trim() !== a.body.trim() : !!c }; }) };
|
|
301
|
+
}).reverse() };
|
|
302
|
+
};
|
|
303
|
+
const DEMO_EVENTS = [];
|
|
304
|
+
GET['/api/doc-sheet/log'] = (q) => {
|
|
305
|
+
const path = pathOf(q); const R = revsOf(path); if (!R) return { ok: false, error: 'could not read the document' };
|
|
306
|
+
const props = Object.entries(PROPS).filter(([, p]) => p.path === path).map(([id, p]) => ({ kind: 'proposed', at: p.at, id, prompt: p.prompt, by: 'agent' }));
|
|
307
|
+
return { ok: true, path, revisions: R.map(({ text, ...r }) => ({ ...r, via: r.via || 'sheet', text_by: r.text_by || (/^claude/.test(r.by) ? 'agent' : 'person'), before_hash: '', after_hash: hashOf(text) })),
|
|
308
|
+
events: [...props, ...DEMO_EVENTS.filter((e) => e.path === path)] };
|
|
309
|
+
};
|
|
310
|
+
POST['/api/doc-sheet/reject'] = (b) => {
|
|
311
|
+
const p = PROPS[b.id]; if (!p || p.path !== b.path) return { ok: false, error: 'no such proposal' };
|
|
312
|
+
if (typeof b.why !== 'string' || !b.why.trim()) return { ok: false, error: 'a why is required' };
|
|
313
|
+
p.state = 'rejected'; p.why = b.why.trim(); p.closed_at = '2026-10-08T08:12:00.000Z';
|
|
314
|
+
DEMO_EVENTS.push({ path: b.path, kind: 'rejected', at: p.closed_at, id: b.id, why: p.why, by: 'pm' });
|
|
315
|
+
return { ok: true, id: b.id, state: 'rejected' };
|
|
316
|
+
};
|
|
317
|
+
POST['/api/doc-sheet/why'] = (b) => { const R = revsOf(b.path); const r = R && R.find((x) => x.rev === Number(b.rev)); if (!r) return { ok: false, error: 'no such revision' }; if (!String(b.why || '').trim()) return { ok: false, error: 'a why is required' }; r.why_added = b.why.trim(); r.why_added_at = '2026-10-08T08:12:00.000Z'; return { ok: true, rev: r.rev }; };
|
|
318
|
+
POST['/api/doc-sheet/mark-current'] = (b) => { if (!String(b.why || '').trim()) return { ok: false, error: 'a why is required' }; DEMO_EVENTS.push({ path: b.path, kind: 'marked', at: '2026-10-08T08:12:00.000Z', why: b.why.trim(), by: 'pm' }); return { ok: true, at: '2026-10-08T08:12:00.000Z' }; };
|
|
278
319
|
POST['/api/doc-sheet/change-note'] = (b) => ({ ok: true, note: 'A change note for the owners of ' + (b.downstream || []).join(', ') });
|
|
279
320
|
|
|
280
321
|
/* health: the six checks (the mockup's docHealth, line 4059). Checkout v2 PRD is recomputed from its text; the others answer the fixed N of M. */
|
|
@@ -4,12 +4,12 @@
|
|
|
4
4
|
* report section ([heading, text]). Its path here is planning/reports/<family>-<report>-<audience>.md; the text and the revisions are made on first
|
|
5
5
|
* read from window.__c3repix (c3-demo-rep-ix.js) and kept in memory for the page's life. Routes answered, in the REAL response shapes of
|
|
6
6
|
* workspace/lib/routes-doc-sheet.js and routes-doc-health.js:
|
|
7
|
-
* GET /api/doc, /api/doc-sheet/revisions, /api/doc-sheet/at
|
|
8
|
-
* POST /api/doc-sheet/edit, /propose, /accept, /api/doc-health/check
|
|
7
|
+
* GET /api/doc, /api/doc-sheet/revisions, /api/doc-sheet/at, /api/doc-sheet/proposals, /api/doc-sheet/log
|
|
8
|
+
* POST /api/doc-sheet/edit, /propose, /accept, /reject, /why, /mark-current, /api/doc-health/check
|
|
9
9
|
* A path outside planning/reports/ goes on to the earlier demo wrappers untouched. Nothing is saved anywhere. */
|
|
10
10
|
|
|
11
11
|
const PREFIX = 'planning/reports/';
|
|
12
|
-
const DOCS = {}; // path -> { revs: [{ rev, v, by, at, why, text }] }
|
|
12
|
+
const DOCS = {}; // path -> { revs: [{ rev, v, by, at, why, text }], events: [] }
|
|
13
13
|
const PROPS = {}; // id -> { path, hash, before, after, prompt }
|
|
14
14
|
let seq = 0;
|
|
15
15
|
|
|
@@ -29,10 +29,19 @@ function sectionsFor(path) {
|
|
|
29
29
|
function docOf(path) {
|
|
30
30
|
if (DOCS[path]) return DOCS[path];
|
|
31
31
|
const s = sectionsFor(path); if (!s) return null;
|
|
32
|
-
DOCS[path] = { revs: [{ rev: 1, v: 'v1', by: 'claude-code/session', at: '2026-10-08T08:12:00.000Z', why: 'Assembled from live data.', text: md(s) }] };
|
|
32
|
+
DOCS[path] = { revs: [{ rev: 1, v: 'v1', by: 'claude-code/session', at: '2026-10-08T08:12:00.000Z', why: 'Assembled from live data.', text: md(s) }], events: [] };
|
|
33
33
|
return DOCS[path];
|
|
34
34
|
}
|
|
35
|
-
const push = (path, by, why, text) => { const d = DOCS[path]; const rev = d.revs.length + 1; d.revs.push({ rev, v: 'v' + rev, by, at: '2026-10-08T08:12:00.000Z', why, text }); return rev; };
|
|
35
|
+
const push = (path, by, why, text, extra) => { const d = DOCS[path]; const rev = d.revs.length + 1; d.revs.push({ rev, v: 'v' + rev, by, at: '2026-10-08T08:12:00.000Z', why, text, ...extra }); return rev; };
|
|
36
|
+
/* the sections of a report text by heading, for the proposals' per-section view */
|
|
37
|
+
const secsOf = (t) => String(t).split(/^(?=## )/m).map((s) => ({ name: (/^## (.*)/.exec(s) || ['', 'Overview'])[1].trim(), body: s.replace(/^## .*\n?/, '').trim() }));
|
|
38
|
+
function propView(id, p, cur) {
|
|
39
|
+
const A = secsOf(p.before); const B = secsOf(p.after); const C = secsOf(cur);
|
|
40
|
+
const names = [...B.filter((x) => { const y = A.find((a) => a.name === x.name); return !y || y.body !== x.body; }).map((x) => x.name), ...A.filter((a) => !B.some((x) => x.name === a.name)).map((a) => a.name)];
|
|
41
|
+
return { id, state: p.state || 'proposed', prompt: p.prompt, at: '2026-10-08T08:12:00.000Z', hash: p.hash, by: 'agent', why: p.why || null, taken: null, of: null, closed_at: null,
|
|
42
|
+
sections: names.map((name) => { const a = A.find((x) => x.name === name); const b = B.find((x) => x.name === name); const c = C.find((x) => x.name === name);
|
|
43
|
+
return { name, kind: !a ? 'added' : !b ? 'removed' : 'changed', before: a ? a.body : '', after: b ? b.body : '', current: c ? c.body : null, stale: a ? !c || c.body !== a.body : !!c }; }) };
|
|
44
|
+
}
|
|
36
45
|
|
|
37
46
|
/* the mockup's docHealth for a report that is still a draft: Fresh passes, Complete does not (a draft), Traced passes, and the claims check. */
|
|
38
47
|
function health(path) {
|
|
@@ -52,6 +61,8 @@ function get(path, q) {
|
|
|
52
61
|
if (path === '/api/doc') return d ? new Response(d.revs[d.revs.length - 1].text, { status: 200, headers: { 'content-type': 'text/markdown; charset=utf-8' } }) : new Response('could not read the document', { status: 404 });
|
|
53
62
|
if (!d) return json({ ok: false, error: 'could not read the document' });
|
|
54
63
|
if (path === '/api/doc-sheet/revisions') return json({ ok: true, path: p, revisions: d.revs.map(({ text, ...r }) => ({ ...r, before_hash: '', after_hash: hashOf(text) })) });
|
|
64
|
+
if (path === '/api/doc-sheet/proposals') { const cur = d.revs[d.revs.length - 1].text; return json({ ok: true, path: p, proposals: Object.entries(PROPS).filter(([, x]) => x.path === p).map(([id, x]) => propView(id, x, cur)).reverse() }); }
|
|
65
|
+
if (path === '/api/doc-sheet/log') return json({ ok: true, path: p, revisions: d.revs.map(({ text, ...r }) => ({ ...r, via: r.via || 'sheet', text_by: r.text_by || (/^claude/.test(r.by) ? 'agent' : 'person'), before_hash: '', after_hash: hashOf(text) })), events: d.events });
|
|
55
66
|
if (path === '/api/doc-sheet/at') { const r = d.revs.find((x) => x.rev === Number(q.get('rev'))); return json(r ? { ok: true, path: p, rev: r.rev, text: r.text } : { ok: false, error: 'no such revision' }); }
|
|
56
67
|
return undefined;
|
|
57
68
|
}
|
|
@@ -68,8 +79,18 @@ function post(path, b) {
|
|
|
68
79
|
const p = PROPS[b.id]; if (!p || p.path !== b.path) return json({ ok: false, error: 'no such proposal' });
|
|
69
80
|
if (b.hash !== p.hash) return json({ ok: false, error: 'the hash does not match the proposal the PM saw', reason: 'hash mismatch' });
|
|
70
81
|
if (d.revs[d.revs.length - 1].text !== p.before) return json({ ok: false, error: 'stale: the document changed since this proposal was made', reason: 'stale' });
|
|
71
|
-
|
|
82
|
+
let text = p.after;
|
|
83
|
+
if (Array.isArray(b.sections) && b.sections.length) { /* only the ticked sections: the rest stay as they are */
|
|
84
|
+
const C = secsOf(p.before); const P = secsOf(p.after);
|
|
85
|
+
for (const n of b.sections) { const x = P.find((y) => y.name === n); const k = C.findIndex((y) => y.name === n); if (!x) { if (k >= 0) C.splice(k, 1); } else if (k >= 0) C[k] = x; else C.push(x); }
|
|
86
|
+
text = C.map((x) => '## ' + x.name + '\n' + x.body + '\n').join('\n'); /* a report is all '## ' sections (md above) */
|
|
87
|
+
}
|
|
88
|
+
p.state = 'accepted';
|
|
89
|
+
return json({ ok: true, path: b.path, rev: push(b.path, 'claude-code/session', p.prompt, text, { via: 'proposal', text_by: 'agent', proposal: b.id }) });
|
|
72
90
|
}
|
|
91
|
+
if (path === '/api/doc-sheet/reject') { const p = PROPS[b.id]; if (!p || p.path !== b.path) return json({ ok: false, error: 'no such proposal' }); p.state = 'rejected'; p.why = String(b.why || '').trim() || null; d.events.push({ kind: 'rejected', at: '2026-10-08T08:12:00.000Z', id: b.id, why: p.why, by: 'pm' }); return json({ ok: true, id: b.id, state: 'rejected' }); }
|
|
92
|
+
if (path === '/api/doc-sheet/why') { const r = d.revs.find((x) => x.rev === Number(b.rev)); if (!r) return json({ ok: false, error: 'no such revision' }); if (!String(b.why || '').trim()) return json({ ok: false, error: 'a why is required' }); r.why_added = b.why.trim(); return json({ ok: true, rev: r.rev }); }
|
|
93
|
+
if (path === '/api/doc-sheet/mark-current') { if (!String(b.why || '').trim()) return json({ ok: false, error: 'a why is required' }); d.events.push({ kind: 'marked', at: '2026-10-08T08:12:00.000Z', why: b.why.trim(), by: 'pm' }); return json({ ok: true, at: '2026-10-08T08:12:00.000Z' }); }
|
|
73
94
|
return undefined;
|
|
74
95
|
}
|
|
75
96
|
|