create-pmos 0.211.0 → 0.212.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.
@@ -79,6 +79,9 @@ const validateLib = require('./lib/validate.js');
79
79
  const pageAgent = require('./lib/page-agent.js'); // page-starts-agent — the fixed prompts a page press may start
80
80
  const scanRunLib = require('./lib/scan-run.js'); // horizon-scan — the watch list suggest run and the scheduled scans, in the 60-second tick
81
81
  const reportFinderLib = require('./lib/report-finder.js'); // report-workspace — the weekly look for the reports the PM already sends
82
+ const refreshLib = require('./lib/refresh.js'); // data-refresh-settings — the sources, their schedules and the refresh log
83
+ const refreshRunLib = require('./lib/refresh-run.js'); // data-refresh-settings — the engine bound to this server (created by the tick below)
84
+ let refreshRun = null;
82
85
  const driverRunLib = require('./lib/driver-run.js'); // initiative-driver — wakes, the unblock sweep and the runs, in the 60-second tick
83
86
  const headlessLib = require('./lib/headless.js'); // workspace-headless-jobs — which jobs run as `claude -p`, and with which tools
84
87
  const liveNotes = require('./lib/live-notes.js'); // jev-live-notes — reads and appends a repository's live-notes ledger; no network
@@ -1945,7 +1948,7 @@ let kitBusy = null; // workspace-kit-update: the one kit or app update
1945
1948
  const autoSync = require('./lib/auto-sync').createAutoSync({
1946
1949
  roots: () => [...new Set([...workspaces.values()].map((w) => w.root))],
1947
1950
  busy: syncing,
1948
- onDone: (root) => boardCache.invalidate(root),
1951
+ onDone: (root, out) => { boardCache.invalidate(root); try { if (refreshRun) refreshRun.codeRun('sync', { trigger: 'change', result: out && out.ok ? 'done' : 'failed', got: out && out.ok ? (out.message || 'synced') : null, error: out && !out.ok ? out.message || 'the sync failed' : null }); } catch { /* the log is not the sync's business */ } },
1949
1952
  label: `${os.hostname()} (auto-sync)`,
1950
1953
  });
1951
1954
  // workspace-data-freshness (R2 to R4): the facts cache is kept in the runtime state folder, and ONE change detector per
@@ -1958,7 +1961,13 @@ const freshness = require('./lib/freshness.js').create({
1958
1961
  workspaces: () => [...workspaces.values()],
1959
1962
  schedules: () => readSchedules(),
1960
1963
  factsStatus: (root) => factsAsync.status(root),
1961
- onChange: (w) => { boardCache.board(w.root, { refreshNow: true }); factsAsync.refresh(w.root); },
1964
+ onChange: (w) => {
1965
+ // data-refresh-settings: the repository source is the rebuild on change; switched off in Data refresh, a change rebuilds nothing.
1966
+ if (refreshRun && !refreshRun.jobGate('repo').ok) return;
1967
+ boardCache.board(w.root, { refreshNow: true }); factsAsync.refresh(w.root);
1968
+ if (refreshRun) refreshRun.codeRun('repo', { trigger: 'change', ws: w.id, got: 'a change in the repository' });
1969
+ },
1970
+ scheduleOf: (skill) => (refreshRun && refreshRun.managedSkill(skill) ? refreshRun.scheduleOfSkill(skill) : undefined),
1962
1971
  pollMs: Math.max(250, Number(process.env.PMOS_FRESH_POLL_MS) || 10000),
1963
1972
  stableMs: Math.max(0, Number(process.env.PMOS_FRESH_STABLE_MS) || 3000),
1964
1973
  });
@@ -2621,7 +2630,62 @@ const coreDocsTick = () => {
2621
2630
  proSettings, inQuiet: (now, quiet) => proactiveLib.inQuietHours(now, quiet), isLead: (w) => isPlanNeedsTree(w) }, Number.isFinite(t) ? { now: t } : {});
2622
2631
  } catch (e) { warnWriteFail('core documents tick', e); }
2623
2632
  };
2624
- try { const _tick = setInterval(() => { checkDueSchedules(); proactiveTick(); try { prepareTick({ manual: false }); } catch { /* the next tick tries again */ } try { dayTick(); } catch (e) { warnWriteFail('day run tick', e); } autoSync.tick().catch(() => {}); dashboardScheduleTick(); driverTick(); scanTick(); reportFinderTick(); coreDocsTick(); reportDesignTick(); }, TICK_MS); if (_tick.unref) _tick.unref(); } catch {}
2633
+ // data-refresh-settings: the refresh engine. It starts the skill sources that are due (calendar, Jira, work assigned to you, tracker,
2634
+ // the morning brief), writes the refresh log, and gates the job ticks below: a job that is off, or whose input is still running, waits.
2635
+ const REFRESH_BOOT = Date.now();
2636
+ refreshRun = refreshRunLib.create({
2637
+ stateDir: STATE,
2638
+ now: () => { const t = process.env.PMOS_REFRESH_NOW ? Date.parse(process.env.PMOS_REFRESH_NOW) : NaN; return Number.isFinite(t) ? t + (Date.now() - REFRESH_BOOT) : Date.now(); },
2639
+ homeWs: () => { const h = homeWorkspace(); return h ? h.id : null; },
2640
+ proactive: () => proSettings(),
2641
+ schedules: { read: readSchedules, write: writeSchedules },
2642
+ startSkill: (wsId, skill, name, origin) => startSkillSession(wsId, skill, name, origin),
2643
+ sessionAlive: (id) => { const x = sessions.get(String(id || '')); return !!x && x.state !== 'exited'; },
2644
+ freshRows: (wsIdArg) => { const w = getWs(wsIdArg || ''); return w ? freshness.sources({ id: w.id, root: w.root, repoStateDir: (() => { try { return stateDirFor(w); } catch { return null; } })() }) : []; },
2645
+ copyPaths: (id, wsIdArg) => {
2646
+ const w = getWs(wsIdArg || '');
2647
+ if (id === 'mywork') return [path.join(STATE, 'my-work', 'read.json')];
2648
+ if (id === 'jira' && w) { let d = null; try { d = stateDirFor(w); } catch { d = null; } return d ? [path.join(d, 'tracker', 'jira')] : []; }
2649
+ return [];
2650
+ },
2651
+ tell: ({ source, label, error, ws }) => {
2652
+ const w = getWs(ws || '') || homeWorkspace(); const nf = w ? needsFileFor(w) : null; if (!nf) return;
2653
+ let needs = []; try { needs = JSON.parse(fs.readFileSync(nf, 'utf8')); } catch { needs = []; }
2654
+ needs.push({ run_id: `refresh-${source}-${Date.now()}`, kind: 'refresh', initiative_id: label, question: `${label} could not refresh: ${String(error).slice(0, 300)} See Settings, Data refresh.` });
2655
+ fs.writeFileSync(nf, JSON.stringify(needs)); invalidateNeedsAgg(w.id);
2656
+ },
2657
+ setDayCheckEvery: (every) => {
2658
+ let raw = {}; try { raw = JSON.parse(fs.readFileSync(PRO_SETTINGS(), 'utf8')); } catch { raw = {}; }
2659
+ const cur = proSettings();
2660
+ const n = proactiveLib.normalizeSettings({ ...raw, dayCheck: { ...cur.dayCheck, every: Number(every) } });
2661
+ if (!n.ok) throw new Error(n.error);
2662
+ fs.mkdirSync(STATE, { recursive: true }); fs.writeFileSync(PRO_SETTINGS(), JSON.stringify(n.settings) + '\n');
2663
+ },
2664
+ setRepoWatch: (on) => { for (const w of workspaces.values()) freshness.setEnabled(w.root, on); },
2665
+ jobs: {
2666
+ daycheck: () => {
2667
+ const date = briefLib.localDate(Date.now()); const run = readDayRun(date);
2668
+ if (!run) return { ok: false, error: 'There is no plan for today yet. Press Prepare my day first; the day check reads that plan.' };
2669
+ const c = startDayCheck(run, 'pressed', Date.now());
2670
+ if (!c.ok) return { ok: false, error: c.error };
2671
+ writeDayRun(c.run); return { ok: true, started: 'session', session: c.session };
2672
+ },
2673
+ scan: (wsIdArg) => { const w = getWs(wsIdArg || '') || homeWorkspace(); if (!w) return { ok: false, error: 'no workspace' }; const r = scanner.startNow(w, {}, { by: 'pm' }); return r.ok ? { ok: true } : { ok: false, status: r.status, error: r.error }; },
2674
+ coredocs: () => { let m; try { m = require('./lib/routes-core-docs.js'); } catch { return { ok: false, error: 'core documents are not installed' }; }
2675
+ m.tick({ getWs, stateDirFor, workspaceList: () => [...workspaces.values()], runtimeDir: STATE, selfRoot: EMBEDDED_SELF ? SELF_ROOT : null, initiativeIndex,
2676
+ spawnSession, stopSession: (id) => { const x = sessions.get(String(id || '')); if (x && x.child) stopHeadless(x); }, invalidateNeedsAgg,
2677
+ proSettings, inQuiet: () => false, isLead: (w) => isPlanNeedsTree(w) }, { force: true });
2678
+ return { ok: true }; },
2679
+ driver: () => { const r = driver.tick({ sweepNow: true }); return r && r.off ? { ok: false, error: 'The initiative driver is switched off on this computer.' } : { ok: true }; },
2680
+ finder: () => reportFinder.runNow(),
2681
+ signals: () => { const t0 = Date.now(); const r = proactiveScan({ manual: true }); refreshRun.codeRun('signals', { trigger: 'pressed', took_ms: Date.now() - t0, got: r && r.ok ? `${r.appended || 0} new signals` : null, result: r && r.ok ? 'done' : 'failed', error: r && r.ok ? null : (r && r.error) || 'the scan failed' }); return { ok: true, started: 'done' }; },
2682
+ repo: () => { for (const w of workspaces.values()) { boardCache.board(w.root, { refreshNow: true }); factsAsync.refresh(w.root, { force: true }); } refreshRun.codeRun('repo', { trigger: 'pressed', got: 'board and initiative facts rebuilt' }); return { ok: true, started: 'rebuild' }; },
2683
+ sync: () => { autoSync.tick().then((out) => { const ran = (out || []).filter(Boolean); refreshRun.codeRun('sync', { trigger: 'pressed', result: ran.every((x) => x.ok) ? 'done' : 'failed', got: ran.length ? `${ran.length} repositories synced` : 'nothing to sync', error: ran.filter((x) => !x.ok).map((x) => x.message).join('; ') || null }); }).catch(() => {}); return { ok: true, started: 'sync' }; },
2684
+ },
2685
+ warn: (what, e) => warnWriteFail(what, e),
2686
+ });
2687
+ const gated = (id, fn) => () => { let g = { ok: true }; try { g = refreshRun.jobGate(id); } catch { g = { ok: true }; } if (g.ok) fn(); };
2688
+ try { const _tick = setInterval(() => { checkDueSchedules(); proactiveTick(); try { prepareTick({ manual: false }); } catch { /* the next tick tries again */ } try { dayTick(); } catch (e) { warnWriteFail('day run tick', e); } try { refreshRun.tick(); } catch (e) { warnWriteFail('refresh tick', e); } gated('sync', () => autoSync.tick().catch(() => {}))(); dashboardScheduleTick(); gated('driver', driverTick)(); gated('scan', scanTick)(); gated('finder', reportFinderTick)(); gated('coredocs', coreDocsTick)(); reportDesignTick(); }, TICK_MS); if (_tick.unref) _tick.unref(); } catch {}
2625
2689
 
2626
2690
  // ── Sessions: real PTYs, owned by the server, with disk-backed scrollback + metadata. ──
2627
2691
  /** @type {Map<string, {id,ws,name,agent,cwd,state,note,created,pty,buf,cols,rows,clients:Set}>} */
@@ -3408,6 +3472,7 @@ function attachPty(s, { seed = false } = {}) {
3408
3472
  : `exited (code ${exitCode})`;
3409
3473
  appendBuf(s, `\r\n\x1b[38;5;244m[${s.note}]\x1b[0m\r\n`);
3410
3474
  persist(s); pushSessions();
3475
+ noteSessionEnd(s);
3411
3476
  });
3412
3477
  return p;
3413
3478
  }
@@ -3583,6 +3648,7 @@ function startHeadless(base, workdir, kind, launchArgs, env, initialInput, addDi
3583
3648
  persist(s); pushSessions();
3584
3649
  // page-starts-agent: a caller that started this run from the page reads its result here (a proposal, never a write).
3585
3650
  if (typeof onDone === 'function') { try { onDone({ result: r, code, signal, timedOut, stopped: !!s.stoppedByPm, note: s.note }); } catch (e) { warnWriteFail('headless result', e); } }
3651
+ noteSessionEnd(s);
3586
3652
  });
3587
3653
  child.stdin.on('error', () => {});
3588
3654
  child.stdin.end(prompt);
@@ -3603,7 +3669,15 @@ function killHeadless(child, sig) {
3603
3669
  try { process.kill(-child.pid, sig); } catch { try { child.kill(sig); } catch {} }
3604
3670
  }
3605
3671
 
3606
- function spawnSession({ name, agent, cwd, ws, origin, initialInput, profile, initiative, modelAsk, planItem, workType, autoDelete, launchFlags, addDirs, onDone, scratch, extraEnv, writePaths }) {
3672
+ // data-refresh-settings: every session passes the refresh engine on its way out, so a background refresh writes its start line
3673
+ // (lib/refresh-run.js started: only an origin it knows is logged). The end is noted where a session exits (noteSessionEnd).
3674
+ function spawnSession(opts) {
3675
+ const s = spawnSessionInner(opts);
3676
+ try { if (s && refreshRun) refreshRun.started(s); } catch (e) { warnWriteFail('refresh log (start)', e); }
3677
+ return s;
3678
+ }
3679
+ function noteSessionEnd(s) { try { if (refreshRun) refreshRun.ended(s); } catch (e) { warnWriteFail('refresh log (end)', e); } }
3680
+ function spawnSessionInner({ name, agent, cwd, ws, origin, initialInput, profile, initiative, modelAsk, planItem, workType, autoDelete, launchFlags, addDirs, onDone, scratch, extraEnv, writePaths }) {
3607
3681
  const id = nextSessionId();
3608
3682
  /* pm-os-absorb Run 1 — every AGENT session gets a token, `<id>.<8 hex>`, in its environment as PMOS_SESSION, and
3609
3683
  * PMOS_BIN, this workspace's own bin/pmos.js. `pmos item` run inside it is then attributed `agent:<token>` (the token
@@ -3790,6 +3864,7 @@ function spawnSession({ name, agent, cwd, ws, origin, initialInput, profile, ini
3790
3864
  : `exited (code ${exitCode})`;
3791
3865
  appendBuf(s, `\r\n\x1b[38;5;244m[${s.note}]\x1b[0m\r\n`);
3792
3866
  persist(s); pushSessions();
3867
+ noteSessionEnd(s);
3793
3868
  });
3794
3869
  feed(s);
3795
3870
  persist(s); pushSessions(); pushWorkspaces();
@@ -4345,7 +4420,7 @@ function dayTick() {
4345
4420
  if (a.ok) run = a.run;
4346
4421
  run = startDayTasks(run, now);
4347
4422
  const due = dayRunLib.checkDue(run, { settings: proSettings(), now, events: brief ? brief.calendar || [] : [] });
4348
- if (due.due) { const c = startDayCheck(run, due.reason, now); if (c.ok) run = c.run; }
4423
+ if (due.due && refreshRun.jobGate('daycheck').ok) { const c = startDayCheck(run, due.reason, now); if (c.ok) run = c.run; }
4349
4424
  run.checks.next = due.next;
4350
4425
  if (JSON.stringify(run) !== before) writeDayRun(run);
4351
4426
  }
@@ -4364,13 +4439,15 @@ function myWorkRead(wsId) {
4364
4439
  return { ok: true, session: s.id };
4365
4440
  }
4366
4441
  /** Refresh now (workspace-data-freshness R7): start skill `skillId` in a session of repository `wsId`, as myWorkRead does. */
4367
- function startSkillSession(wsId, skillId, name) {
4442
+ function startSkillSession(wsId, skillId, name, origin) {
4368
4443
  const w = getWs(wsId);
4369
4444
  if (!w) return { ok: false, status: 400, error: 'no workspace' };
4370
4445
  const task = taskById(skillId, w.root);
4371
4446
  if (!task || task.kind !== 'agent' || !task.prompt) return { ok: false, status: 409, error: `the ${skillId} skill is not installed for this repository` };
4372
4447
  const prompt = skillStore.retargetFiling(task.prompt, filingContract(w));
4373
- const s = spawnSession({ name, agent: task.agent || 'claude', ws: w.id, origin: 'source-refresh', initialInput: prompt + '\r',
4448
+ // data-refresh-settings: a refresh of a source runs as `refresh:<source>`, so the refresh log records it whoever pressed it.
4449
+ const src = refreshLib.SKILL_OF.get(skillId);
4450
+ const s = spawnSession({ name, agent: task.agent || 'claude', ws: w.id, origin: origin || (src ? 'refresh:' + src : 'source-refresh'), initialInput: prompt + '\r',
4374
4451
  modelAsk: { scheduleTier: null, skillTier: task.tier || null, skill: task.skill } });
4375
4452
  if (!s || s.state === 'exited') return { ok: false, status: 500, error: (s && s.note) || 'the refresh session could not start' };
4376
4453
  return { ok: true, session: s.id };
@@ -4593,7 +4670,12 @@ function proactiveTick() {
4593
4670
  const now = Date.now();
4594
4671
  if (now - proLastScan < PRO_SCAN_EVERY_MS) return;
4595
4672
  proLastScan = now;
4596
- try { proactiveScan({ manual: false }); } catch (e) { warnWriteFail('proactive scan', e); }
4673
+ if (refreshRun.jobGate('signals').ok) {
4674
+ try {
4675
+ const r = proactiveScan({ manual: false });
4676
+ if (r && !r.skipped) refreshRun.codeRun('signals', { trigger: 'schedule', took_ms: Date.now() - now, got: `${r.appended || 0} new signals` });
4677
+ } catch (e) { warnWriteFail('proactive scan', e); refreshRun.codeRun('signals', { trigger: 'schedule', result: 'failed', error: String((e && e.message) || e) }); }
4678
+ }
4597
4679
  try { learnWeekly({ manual: false }); } catch (e) { warnWriteFail('proactive weekly review', e); }
4598
4680
  }
4599
4681
 
@@ -6662,6 +6744,8 @@ async function handleRequest(req, res) {
6662
6744
  const list = readSchedules(); const made = []; const kept = [];
6663
6745
  for (const def of set) {
6664
6746
  if (!taskById(def.skill, w.root)) { kept.push({ skill: def.skill, why: 'the skill is not installed' }); continue; }
6747
+ // data-refresh-settings (R3): a refresh skill is scheduled in Settings › Data refresh. The set turns its source on there.
6748
+ if (refreshRun.managedSkill(def.skill)) { const turned = refreshRun.ensureOn(refreshRun.managedSkill(def.skill), w.id); kept.push({ skill: def.skill, why: turned ? 'turned on in Settings, Data refresh' : 'already on in Settings, Data refresh', refresh: refreshRun.managedSkill(def.skill) }); continue; }
6665
6749
  if (list.some((x) => x.ws === w.id && x.skill === def.skill)) { kept.push({ skill: def.skill, why: 'already scheduled' }); continue; }
6666
6750
  const norm = scheduleLogic.normalizeSchedule({ ...def, task: def.skill });
6667
6751
  if (!norm.ok) { kept.push({ skill: def.skill, why: norm.error }); continue; }
@@ -8384,9 +8468,11 @@ async function handleRequest(req, res) {
8384
8468
  if (!taskById('morning-brief', w.root)) return json(res, { ok: false, error: 'the morning-brief skill is not installed for this repository' }, 409);
8385
8469
  const norm = scheduleLogic.normalizeSchedule({ ...SEED_SETS.brief[0], task: 'morning-brief' });
8386
8470
  if (!norm.ok) return json(res, { ok: false, error: norm.error }, 500);
8387
- sched = { id: 'sc-' + Date.now().toString(36) + 'g', ws: w.id, ...norm.schedule, profile: null, lastRun: null, lastStatus: null };
8388
- sched.nextRun = scheduleLogic.nextRun(sched);
8471
+ // data-refresh-settings (R3): the brief's schedule lives in Settings › Data refresh. The entry kept here only carries this run.
8472
+ sched = { id: 'sc-' + Date.now().toString(36) + 'g', ws: w.id, ...norm.schedule, profile: null, lastRun: null, lastStatus: null, enabled: false, endedReason: 'Moved to Settings › Data refresh' };
8473
+ sched.nextRun = null;
8389
8474
  const list = readSchedules(); list.unshift(sched); writeSchedules(list); seeded = true;
8475
+ try { refreshRun.ensureOn('brief', w.id); } catch (e) { warnWriteFail('refresh settings (brief)', e); }
8390
8476
  }
8391
8477
  const result = fireSchedule(sched);
8392
8478
  // A session that could not start is recorded as already ended, so the page says why instead of waiting on a read.
@@ -8634,6 +8720,8 @@ async function handleRequest(req, res) {
8634
8720
  // A brief for a day the PM pressed "Prepare my day": its agent priorities become tasks and start (G13), and a merge
8635
8721
  // closes the check that sent it (G22).
8636
8722
  try { dayAfterBrief(out.date, merged); } catch (e) { warnWriteFail('day run after the brief', e); }
8723
+ // data-refresh-settings: a full brief ends the morning brief's refresh run (its session stays open after it submits).
8724
+ if (body.merge !== true) { try { refreshRun.briefDone(`${(out.items || []).length} items`); } catch (e) { warnWriteFail('refresh log (brief)', e); } }
8637
8725
  return json(res, { ok: true, brief: out, ...(merged ? { merged } : {}), ...(remembered ? { remembered } : {}), ...(memoryError ? { memory_error: memoryError } : {}) });
8638
8726
  }
8639
8727
  if (req.method === 'GET') {
@@ -9082,7 +9170,9 @@ async function handleRequest(req, res) {
9082
9170
  // c3-initiative-boards: the PMOS checkout this server is embedded in, whose default board keeps its derived ladder
9083
9171
  selfRoot: EMBEDDED_SELF ? SELF_ROOT : null,
9084
9172
  // horizon-scan-portfolio: Scan now, Stop and an approved research proposal start and end runs in the scanner; scanClock fixes its clock in the suites
9085
- scanner, scanClock })) return;
9173
+ scanner, scanClock,
9174
+ // data-refresh-settings: Settings › Data refresh (lib/routes-refresh.js)
9175
+ refreshRun })) return;
9086
9176
  res.writeHead(404); res.end('not found');
9087
9177
  }
9088
9178
 
@@ -0,0 +1,5 @@
1
+ {
2
+ "_comment": "initiative data-refresh-settings: the refresh engine (lib/refresh.js) and its routes on a real server (lib/routes-refresh.js, lib/refresh-run.js).",
3
+ "modules": ["refresh.test.js", "refresh-routes.test.js"],
4
+ "seconds": { "refresh.test.js": 2, "refresh-routes.test.js": 25 }
5
+ }