create-pmos 0.42.0 → 0.44.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 (39) hide show
  1. package/bin/cockpit-install.js +130 -0
  2. package/bin/create-pmos.js +47 -4
  3. package/package.json +1 -1
  4. package/payload/AGENTS-HUMAN.md +10 -25
  5. package/payload/AGENTS.md +4 -0
  6. package/payload/KIT-VERSION +1 -1
  7. package/payload/mods/.claude-plugin/marketplace.json +12 -0
  8. package/payload/mods/pmos-cockpit/.claude-plugin/plugin.json +9 -0
  9. package/payload/mods/pmos-cockpit/README.md +60 -0
  10. package/payload/mods/pmos-cockpit/hooks/hooks.json +1 -0
  11. package/payload/mods/pmos-cockpit/hooks/lib/commands.mjs +53 -0
  12. package/payload/mods/pmos-cockpit/hooks/lib/grants.mjs +88 -0
  13. package/payload/mods/pmos-cockpit/hooks/lib/guard.mjs +561 -0
  14. package/payload/mods/pmos-cockpit/hooks/lib/initiative.mjs +54 -0
  15. package/payload/mods/pmos-cockpit/hooks/lib/layout.mjs +31 -0
  16. package/payload/mods/pmos-cockpit/hooks/lib/profile.mjs +17 -0
  17. package/payload/mods/pmos-cockpit/hooks/lib/skills.mjs +69 -0
  18. package/payload/mods/pmos-cockpit/hooks/register.tsx +460 -0
  19. package/payload/mods/pmos-cockpit/types/index.d.ts +34 -0
  20. package/payload/planning/evals/eval-calibration.md +1 -1
  21. package/payload/planning/okrs/health-budgets.md +1 -1
  22. package/payload/scripts/pmos-log.sh +119 -7
  23. package/payload/skills/pmos-router.skill +1 -1
  24. package/payload/skills/prototype.skill +1 -1
  25. package/payload/skills/strategic-brief.skill +1 -1
  26. package/payload/skills-native/harness-refine/SKILL.md +1 -1
  27. package/payload/skills-native/pmos-router/SKILL.md +1 -1
  28. package/payload/skills-native/prototype/SKILL.md +1 -1
  29. package/payload/skills-native/strategic-brief/SKILL.md +1 -1
  30. package/payload/state/discoveries.md +1 -1
  31. package/workspace/README.md +4 -0
  32. package/workspace/guide/proactive-engine.md +21 -2
  33. package/workspace/lib/prepare-queue.js +76 -0
  34. package/workspace/lib/proactive-cli.js +57 -1
  35. package/workspace/lib/proactive.js +3 -2
  36. package/workspace/package-lock.json +1 -1
  37. package/workspace/package.json +3 -3
  38. package/workspace/public/pm/modes-live.js +71 -2
  39. package/workspace/server.js +101 -1
@@ -831,7 +831,7 @@ async function tScan() {
831
831
  }
832
832
  async function tSettings(patch) {
833
833
  const out = await post('/api/proactive/settings', patch);
834
- reload('today'); reload('brief');
834
+ reload('today'); reload('brief'); reload('prepare');
835
835
  tSay(!out || out.ok === false ? 'bad' : 'ok', !out || out.ok === false ? ((out && out.error) || 'not saved') : 'saved');
836
836
  }
837
837
  function tCard(c) {
@@ -843,6 +843,7 @@ function tCard(c) {
843
843
  el('span', { style: 'display:block;font-size:11px;color:var(--muted-foreground);margin:2px 0 4px' }, iv(c.why + (c.evidence ? ' — ' + c.evidence : ''))),
844
844
  el('span', { style: 'display:flex;gap:4px;flex-wrap:wrap' },
845
845
  c.open ? b('open', 'open', () => tOpen(c)) : null,
846
+ c.initiative_id && c.ws ? el('button', { style: BTN, hover: 'color:var(--foreground)', 'data-r23': 'prepare', onClick: () => bgShowPlan(c) }, 'prepare in the background') : null,
846
847
  b('useful', 'useful', () => tFeedback({ candidate_id: c.id, verdict: 'useful' }, 'thanks — recorded as useful')),
847
848
  b('not useful', 'not-useful', () => tFeedback({ candidate_id: c.id, verdict: 'not-useful' }, 'recorded as not useful')),
848
849
  b('wrong priority', 'wrong-priority', () => tFeedback({ candidate_id: c.id, verdict: 'wrong-priority' }, 'recorded as wrong priority')),
@@ -900,6 +901,72 @@ function briefSection() {
900
901
  }
901
902
  return kids;
902
903
  }
904
+
905
+ /* background preparation (proactive-engine D3): the PM approves one plan; the workspace starts it later, under the same rules as a next-step go */
906
+ let bgPlan = null; // { cid, state: 'loading'|'ok'|'error', data, error }
907
+ let bgNote = null;
908
+ async function bgShowPlan(c) {
909
+ if (bgPlan && bgPlan.cid === c.id) { bgPlan = null; setState({}); return; }
910
+ bgPlan = { cid: c.id, state: 'loading' }; setState({});
911
+ const out = await post('/api/proactive/prepare/plan', { ws: c.ws, initiative_id: c.initiative_id });
912
+ bgPlan = !out || out.ok === false ? { cid: c.id, state: 'error', error: (out && out.error) || 'no plan' } : { cid: c.id, state: 'ok', data: out, c };
913
+ setState({});
914
+ }
915
+ async function bgQueue(c, d) {
916
+ const out = await post('/api/proactive/prepare/queue', { ws: d.ws, initiative_id: d.initiative_id, item_id: d.item.id, prompt_sha: d.prompt_sha, candidate_id: c.id, confirm: true });
917
+ bgNote = !out || out.ok === false ? { tone: 'bad', text: (out && out.error) || 'not queued' } : { tone: 'ok', text: 'queued — the workspace starts it when the budget allows' + (d.background.quiet ? ', after quiet hours' : '') + '. It expires in ' + d.background.expire_hours + ' hours.' };
918
+ if (out && out.ok) bgPlan = null;
919
+ reload('prepare'); setState({});
920
+ }
921
+ async function bgCancel(e) {
922
+ const out = await post('/api/proactive/prepare/cancel', { id: e.id });
923
+ bgNote = !out || out.ok === false ? { tone: 'bad', text: (out && out.error) || 'not cancelled' } : { tone: 'ok', text: 'cancelled' };
924
+ reload('prepare'); setState({});
925
+ }
926
+ async function bgRun() {
927
+ const out = await post('/api/proactive/prepare/run', {});
928
+ bgNote = !out || out.ok === false ? { tone: 'bad', text: (out && out.error) || 'the run failed' }
929
+ : { tone: 'ok', text: out.results.length ? out.results.map((x) => x.action + (x.reason ? ' (' + x.reason + ')' : '')).join('; ') : 'nothing is queued' };
930
+ reload('prepare'); setState({});
931
+ }
932
+ function bgPlanView() {
933
+ if (!bgPlan) return null;
934
+ if (bgPlan.state === 'loading') return el('div', { style: NOTE, 'data-r23': 'plan-loading' }, iv('reading the plan …'));
935
+ if (bgPlan.state === 'error') return el('div', { style: NOTE + ';color:var(--destructive)', 'data-r23': 'plan-error' }, iv(bgPlan.error));
936
+ const d = bgPlan.data; const c = bgPlan.c;
937
+ const kids = [el('div', { style: 'font-size:12px', 'data-r23': 'plan-step' }, iv('Step: ' + d.item.key + ' — ' + d.item.text))];
938
+ if (d.refused) { kids.push(el('div', { style: NOTE + ';color:var(--destructive)', 'data-r23': 'plan-refused' }, iv(d.refused.why))); }
939
+ else {
940
+ kids.push(el('div', { style: NOTE, 'data-r23': 'plan-where' }, iv(d.plan.why || d.plan.plan)));
941
+ kids.push(el('div', { style: NOTE, 'data-r23': 'plan-budget' }, iv(d.budget.running + ' of ' + d.budget.limits.concurrent + ' prepared sessions running · ' + d.budget.startedToday + ' of ' + d.budget.limits.perDay + ' started today. The session proposes only: no push, pull request, send, post, acceptance or merge. No outside source and no typed scoring.')));
942
+ kids.push(el('pre', { 'data-r23': 'plan-prompt', style: 'white-space:pre-wrap;font-family:var(--mono);font-size:10.5px;max-height:140px;overflow:auto;border:1px solid var(--border);padding:6px;margin:4px 0' }, iv(d.prompt)));
943
+ if (!d.background.enabled) kids.push(el('div', { style: NOTE, 'data-r23': 'plan-off' }, iv('Background prepare is off. Turn it on in the settings below to queue this.')));
944
+ else kids.push(el('button', { style: BTN + ';color:var(--foreground);border-color:var(--foreground)', hover: 'background:var(--accent)', 'data-r23': 'queue', onClick: () => bgQueue(c, d) }, 'queue it (plan ' + d.prompt_sha + ')'));
945
+ }
946
+ return el('div', { 'data-r23': 'plan', style: 'padding:6px 14px 8px;border-bottom:1px solid var(--border)' }, kids);
947
+ }
948
+ function backgroundSection() {
949
+ const r = read('prepare', '/api/proactive/prepare');
950
+ const st = status(r, 'the background queue');
951
+ const kids = [el('div', { style: LABEL }, iv('background preparation'))];
952
+ if (st) return [...kids, st];
953
+ const d = r.data;
954
+ kids.push(el('div', { style: NOTE, 'data-r23': 'bg-state' }, iv((d.enabled ? 'On' : 'Off') + '. ' + d.budget.running + ' of ' + d.budget.limits.concurrent + ' prepared sessions running · ' + d.budget.startedToday + ' of ' + d.budget.limits.perDay + ' started today' + (d.quiet ? ' · quiet hours now' : '') + '. An approval is for one plan and expires in ' + d.expire_hours + ' hours. It runs only while the workspace server runs.')));
955
+ if (bgNote) kids.push(el('div', { 'data-r23': 'bg-note', style: NOTE + ';color:' + (bgNote.tone === 'bad' ? 'var(--destructive)' : 'var(--foreground)') }, iv(bgNote.text)));
956
+ if (!d.entries.length) kids.push(el('div', { style: NOTE, 'data-r23': 'bg-empty' }, iv('Nothing has been queued.')));
957
+ for (const e of d.entries) {
958
+ kids.push(el('div', { 'data-r23': 'entry', 'data-id': e.id, 'data-status': e.status, style: 'padding:6px 14px;border-bottom:1px solid var(--border)' },
959
+ el('span', { style: 'display:flex;gap:8px;align-items:baseline' },
960
+ el('span', { style: 'font-size:12px' }, iv(e.label)),
961
+ el('span', { style: 'margin-left:auto;font-family:var(--mono);font-size:10px;color:var(--muted-foreground)' }, iv(e.status))),
962
+ e.reason ? el('span', { style: 'display:block;font-size:11px;color:var(--muted-foreground)', 'data-r23': 'entry-reason' }, iv(e.reason)) : null,
963
+ el('span', { style: 'display:flex;gap:4px;margin-top:3px' },
964
+ e.status === 'queued' ? el('button', { style: BTN, hover: 'color:var(--foreground)', 'data-r23': 'cancel', onClick: () => bgCancel(e) }, 'cancel') : null,
965
+ e.status === 'started' && e.session_id ? el('button', { style: BTN, hover: 'color:var(--foreground)', 'data-r23': 'switch', onClick: async () => { const ini = await import('./panel-initiatives.js'); ini.openTerm(null, { id: e.session_id, name: e.label }); } }, 'switch to it') : null)));
966
+ }
967
+ if (d.entries.some((e) => e.status === 'queued')) kids.push(el('div', { style: 'padding:4px 14px' }, el('button', { style: BTN, hover: 'color:var(--foreground)', title: 'look at the queue now; ignores quiet hours only', 'data-r23': 'run', onClick: bgRun }, 'run the queue now')));
968
+ return kids;
969
+ }
903
970
  export function todayList() {
904
971
  const r = read('today', '/api/proactive/today');
905
972
  const kids = [el('div', { style: NOTE, 'data-r22': 'intro' }, iv('What is waiting for you, worked out from the ledgers and git on this computer. It runs only while the workspace server runs, reads no mail or calendar, and changes nothing: your feedback only changes how this list is ordered later.'))];
@@ -925,7 +992,8 @@ export function todayList() {
925
992
  if ((d.unreadable || []).length && !tRetry && typeof setTimeout === 'function') tRetry = setTimeout(() => { tRetry = null; reload('today'); setState({}); }, 4000);
926
993
  for (const u of d.unreadable || []) kids.push(el('div', { style: NOTE + ';color:var(--destructive)', 'data-r22': 'unreadable' }, iv('Not read — ' + (u.name || u.repo || 'a repository') + ': ' + (u.error || 'unknown reason') + '. Nothing is shown for it, which is not the same as nothing waiting.')));
927
994
  if (d.empty) kids.push(el('div', { style: NOTE, 'data-r22': 'empty' }, iv('Nothing is waiting on any repository that was read.')));
928
- for (const c of d.candidates || []) kids.push(tCard(c));
995
+ for (const c of d.candidates || []) { kids.push(tCard(c)); if (bgPlan && bgPlan.cid === c.id) kids.push(bgPlanView()); }
996
+ kids.push(...backgroundSection());
929
997
  if (d.dismissed) kids.push(el('div', { style: NOTE, 'data-r22': 'dismissed' }, iv(d.dismissed + ' dismissed item(s) hidden for 7 days.')));
930
998
  // settings
931
999
  kids.push(el('div', { style: LABEL }, iv('settings')));
@@ -933,6 +1001,7 @@ export function todayList() {
933
1001
  el('input', { type: 'checkbox', checked: on ? 'checked' : null, 'data-r22': attr, onChange: () => tSettings(patch(!on)) }), iv(label));
934
1002
  kids.push(chk('engine on', s.enabled, 'set-enabled', (v) => ({ enabled: v })));
935
1003
  for (const t of Object.keys(s.triggers)) kids.push(chk('watch: ' + t, s.triggers[t], 'set-' + t, (v) => ({ triggers: { [t]: v } })));
1004
+ kids.push(chk('background prepare (starts approved sessions by itself)', s.backgroundPrepare, 'set-bg', (v) => ({ backgroundPrepare: v })));
936
1005
  kids.push(el('div', { style: LABEL }, iv('morning brief')));
937
1006
  for (const k of Object.keys(s.briefSources || {})) kids.push(chk('read ' + k + (k === 'mail' ? ' (titles and senders only)' : ''), s.briefSources[k], 'set-src-' + k, (v) => ({ briefSources: { [k]: v } })));
938
1007
  kids.push(chk('typed scoring (sends titles and metadata to the Typesafe service)', s.briefScoring, 'set-scoring', (v) => ({ briefScoring: v })));
@@ -79,6 +79,7 @@ const sessionBudget = require('./lib/session-budget');
79
79
  const judgmentLib = require('./lib/judgment');
80
80
  const proactiveLib = require('./lib/proactive');
81
81
  const briefLib = require('./lib/brief');
82
+ const prepareQueue = require('./lib/prepare-queue');
82
83
  const harness = require('./lib/harness-parse');
83
84
  const transcript = require('./lib/transcript-read');
84
85
  const skillStore = require('./lib/skill-store');
@@ -2189,7 +2190,7 @@ function checkDueSchedules() {
2189
2190
  return fired;
2190
2191
  }
2191
2192
  // the tick — the persistent server checks on an interval; unref'd so it never blocks exit
2192
- try { const _tick = setInterval(() => { checkDueSchedules(); proactiveTick(); }, 60000); if (_tick.unref) _tick.unref(); } catch {}
2193
+ try { const _tick = setInterval(() => { checkDueSchedules(); proactiveTick(); try { prepareTick({ manual: false }); } catch { /* the next tick tries again */ } }, 60000); if (_tick.unref) _tick.unref(); } catch {}
2193
2194
 
2194
2195
  // ── Sessions: real PTYs, owned by the server, with disk-backed scrollback + metadata. ──
2195
2196
  /** @type {Map<string, {id,ws,name,agent,cwd,state,note,created,pty,buf,cols,rows,clients:Set}>} */
@@ -3358,6 +3359,58 @@ function prepareNextStep(body) {
3358
3359
  judgment: { on: judgmentOn, outside_service: judgmentOn, needs_key: judgmentOn, note: judgmentOn ? 'The question text leaves this machine for the Typesafe service. It needs TYPESAFE_API_KEY in the session.' : 'No outside service is used.' } };
3359
3360
  }
3360
3361
 
3362
+ // ── Background prepare, slice D3 (initiative proactive-engine) ──────────────────────────────────────────────────
3363
+ // The PM approves ONE plan hash from Today. The 60-second tick later starts that session through the SAME path a next-step
3364
+ // `go` uses (startWork with origin next-step and the prompt that was shown), only while background prepare is on, outside quiet
3365
+ // hours, with session budget, and while the plan still has the approved hash. Every rule is in lib/prepare-queue.js.
3366
+ const PRO_QUEUE = () => path.join(STATE, 'prepare-queue.jsonl');
3367
+ function queueEntries() { return prepareQueue.fold(nsRead(PRO_QUEUE())); }
3368
+ // The step an agent session may work on now for an initiative: the first `work` step the ladder lets it start.
3369
+ function currentWorkItem(w, id) {
3370
+ const got = nextStepsFor(w, id);
3371
+ if (!got.ok) return { ok: false, status: got.status || 500, error: got.error };
3372
+ const item = got.ns.items.find((x) => x.act && x.act.type === 'work' && !nextStepActions.refusal(x, got.ns));
3373
+ if (item) return { ok: true, item };
3374
+ const first = got.ns.items.find((x) => x.first) || got.ns.items[0];
3375
+ const why = first ? nextStepActions.refusal(first, got.ns) : null;
3376
+ return { ok: false, status: 409, error: 'No step is open for an agent session right now' + (why ? ': ' + why.why : '.') };
3377
+ }
3378
+ function preparePlanFor(w, id, itemId) {
3379
+ return prepareNextStep({ ws: w.id, initiative_id: id, item_id: itemId, agent: 'claude', judgment: false });
3380
+ }
3381
+ function prepareState() {
3382
+ const settings = proSettings();
3383
+ return { enabled: settings.backgroundPrepare === true, quiet: proactiveLib.inQuietHours(Date.now(), settings.quiet), budget: nsBudget(), max_queued: prepareQueue.MAX_QUEUED, expire_hours: prepareQueue.EXPIRE_MS / 3600000 };
3384
+ }
3385
+ function prepareTick({ manual = false } = {}) {
3386
+ const out = [];
3387
+ const settings = proSettings();
3388
+ const queued = queueEntries().filter((e) => e.status === 'queued');
3389
+ if (!queued.length) return out;
3390
+ const now = Date.now();
3391
+ const quiet = proactiveLib.inQuietHours(now, settings.quiet);
3392
+ for (const e of queued) {
3393
+ const w = getWs(e.ws);
3394
+ const fresh = w ? preparePlanFor(w, e.initiative_id, e.item_id) : { ok: false, error: 'the repository is no longer registered' };
3395
+ const d = prepareQueue.decide({ entry: e, now, settings, quiet, manual, fresh, budget: nsBudget() });
3396
+ if (d.action === 'wait') { out.push({ id: e.id, action: 'wait', reason: d.reason }); continue; }
3397
+ if (d.action === 'expire' || d.action === 'skip') {
3398
+ nsAppend(PRO_QUEUE(), { id: e.id, at: nowISO(), event: d.action === 'expire' ? 'expired' : 'skipped', reason: d.reason }, 'prepare queue');
3399
+ out.push({ id: e.id, action: d.action, reason: d.reason }); continue;
3400
+ }
3401
+ const started = startWork({ initiative: fresh.initiative_id, ws: fresh.ws, agent: fresh.agent, name: `${fresh.item.key} · ${fresh.initiative_id} (background)`, origin: 'next-step', initialInput: fresh.prompt + '\r' });
3402
+ if (!started.ok) {
3403
+ nsAppend(PRO_QUEUE(), { id: e.id, at: nowISO(), event: 'skipped', reason: 'it could not start — ' + started.error }, 'prepare queue');
3404
+ out.push({ id: e.id, action: 'skip', reason: started.error }); continue;
3405
+ }
3406
+ nsAppend(NS_RUNS(), { id: 'nsr-' + Date.now().toString(36), at: nowISO(), ws: fresh.ws, initiative_id: fresh.initiative_id, item_id: fresh.item.id, key: fresh.item.key, session_id: started.session.id,
3407
+ branch: fresh.plan.branch || null, plan: fresh.plan.plan, prompt_sha: fresh.prompt_sha, judgment: false, profile: null, via: 'background', queue_id: e.id }, 'next-step run');
3408
+ nsAppend(PRO_QUEUE(), { id: e.id, at: nowISO(), event: 'started', session_id: started.session.id }, 'prepare queue');
3409
+ out.push({ id: e.id, action: 'start', session_id: started.session.id });
3410
+ }
3411
+ return out;
3412
+ }
3413
+
3361
3414
  // ── Proactive engine, slice D1 (initiative proactive-engine) ─────────────────────────────────────────────────────
3362
3415
  // A scan reads ledgers and git for five conditions and appends each new one to signals.jsonl; Today is computed from what
3363
3416
  // is true now. No model call, no network call, no write into any repository, no session started (lib/proactive.js is
@@ -6221,6 +6274,53 @@ async function handleRequest(req, res) {
6221
6274
  if (!w) return json(res, { ok: false, error: 'no workspace' }, 400);
6222
6275
  return json(res, { ok: true, ws: w.id, name: w.name, ...validateLib.readFindings(validationFindingsFile(w)) });
6223
6276
  }
6277
+ /* ── background prepare (D3): plan is read-only; queue records an approval; the runner starts it later ── */
6278
+ if (p === '/api/proactive/prepare/plan' && req.method === 'POST') {
6279
+ let body = {}; try { body = JSON.parse((await readBody(req)) || '{}'); } catch { return json(res, { ok: false, error: 'invalid JSON body' }, 400); }
6280
+ const w = getWs(body.ws);
6281
+ if (!w) return json(res, { ok: false, error: 'no workspace' }, 400);
6282
+ const id = String(body.initiative_id || '');
6283
+ if (!fourPanel.validId(id)) return json(res, { ok: false, error: 'an initiative id is required' }, 400);
6284
+ const cur = currentWorkItem(w, id);
6285
+ if (!cur.ok) return json(res, { ok: false, error: cur.error }, cur.status || 409);
6286
+ const r = preparePlanFor(w, id, cur.item.id);
6287
+ return json(res, { ...r, background: prepareState() }, r.ok ? 200 : (r.status || 400));
6288
+ }
6289
+ if (p === '/api/proactive/prepare/queue' && req.method === 'POST') {
6290
+ let body = {}; try { body = JSON.parse((await readBody(req)) || '{}'); } catch { return json(res, { ok: false, error: 'invalid JSON body' }, 400); }
6291
+ const c = prepareQueue.checkRequest(body);
6292
+ if (!c.ok) return json(res, { ok: false, error: c.error }, 400);
6293
+ const why = prepareQueue.canQueue({ settings: proSettings(), entries: queueEntries(), req: c.req });
6294
+ if (why) return json(res, { ok: false, error: why }, 409);
6295
+ const w = getWs(c.req.ws);
6296
+ if (!w) return json(res, { ok: false, error: 'no workspace' }, 400);
6297
+ const r = preparePlanFor(w, c.req.initiative_id, c.req.item_id);
6298
+ if (!r.ok) return json(res, { ok: false, error: r.error }, r.status || 400);
6299
+ if (r.refused) return json(res, { ok: false, refused: r.refused, error: r.refused.why }, 409);
6300
+ if (r.prompt_sha !== c.req.prompt_sha) return json(res, { ok: false, error: 'the plan changed since you reviewed it — read it again', prompt_sha: r.prompt_sha }, 409);
6301
+ const rec = { id: 'pq-' + Date.now().toString(36) + Math.random().toString(36).slice(2, 5), at: nowISO(), event: 'queued', ws: w.id, initiative_id: c.req.initiative_id, item_id: c.req.item_id, key: r.item.key,
6302
+ candidate_id: c.req.candidate_id, prompt_sha: r.prompt_sha, branch: r.plan.branch || null };
6303
+ const wrote = nsAppend(PRO_QUEUE(), rec, 'prepare queue');
6304
+ if (!wrote.ok) return json(res, { ok: false, error: wrote.error }, 500);
6305
+ return json(res, { ok: true, entry: rec });
6306
+ }
6307
+ if (p === '/api/proactive/prepare/cancel' && req.method === 'POST') {
6308
+ let body = {}; try { body = JSON.parse((await readBody(req)) || '{}'); } catch { return json(res, { ok: false, error: 'invalid JSON body' }, 400); }
6309
+ const e = queueEntries().find((x) => x.id === String(body.id || ''));
6310
+ if (!e) return json(res, { ok: false, error: 'no such queued entry' }, 404);
6311
+ if (e.status !== 'queued') return json(res, { ok: false, error: `it is already ${e.status} — only a queued entry can be cancelled` }, 409);
6312
+ const wrote = nsAppend(PRO_QUEUE(), { id: e.id, at: nowISO(), event: 'cancelled', reason: 'cancelled by you' }, 'prepare queue');
6313
+ if (!wrote.ok) return json(res, { ok: false, error: wrote.error }, 500);
6314
+ return json(res, { ok: true });
6315
+ }
6316
+ if (p === '/api/proactive/prepare/run' && req.method === 'POST') {
6317
+ // A PM press: look at the queue now. Quiet hours are ignored; the switch, the budget, the plan hash and the 24 hours are not.
6318
+ return json(res, { ok: true, results: prepareTick({ manual: true }) });
6319
+ }
6320
+ if (p === '/api/proactive/prepare' && req.method === 'GET') {
6321
+ const entries = queueEntries().slice(-30).reverse().map((e) => ({ ...e, label: e.key + ' · ' + e.initiative_id }));
6322
+ return json(res, { ok: true, ...prepareState(), entries });
6323
+ }
6224
6324
  /* ── proactive engine (D1): Today is computed, feedback is recorded, nothing is sent, started or changed ── */
6225
6325
  if (p === '/api/proactive/today' && req.method === 'GET') {
6226
6326
  const now = Date.now();