create-pmos 0.167.0 → 0.169.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 (59) hide show
  1. package/package.json +1 -1
  2. package/payload/KIT-VERSION +1 -1
  3. package/payload/planning/evals/eval-calibration.md +1 -1
  4. package/payload/planning/okrs/health-budgets.md +1 -1
  5. package/payload/state/discoveries.md +1 -1
  6. package/workspace/c3-driver-fixture.js +61 -0
  7. package/workspace/c3-driver-test.js +164 -0
  8. package/workspace/extension/README.md +33 -0
  9. package/workspace/extension/address.js +20 -0
  10. package/workspace/extension/content.js +76 -0
  11. package/workspace/extension/icons/icon-128.png +0 -0
  12. package/workspace/extension/icons/icon-16.png +0 -0
  13. package/workspace/extension/icons/icon-32.png +0 -0
  14. package/workspace/extension/icons/icon-48.png +0 -0
  15. package/workspace/extension/manifest.json +15 -0
  16. package/workspace/extension/options.css +33 -0
  17. package/workspace/extension/options.html +26 -0
  18. package/workspace/extension/options.js +24 -0
  19. package/workspace/extension/panel.css +21 -0
  20. package/workspace/extension/panel.html +14 -0
  21. package/workspace/extension/panel.js +67 -0
  22. package/workspace/guide/console-calendar.md +14 -0
  23. package/workspace/guide/index.md +1 -0
  24. package/workspace/guide/initiative-driver.md +87 -0
  25. package/workspace/lib/driver-cli.js +207 -0
  26. package/workspace/lib/driver-decide.js +118 -0
  27. package/workspace/lib/driver-facts.js +113 -0
  28. package/workspace/lib/driver-needs.js +107 -0
  29. package/workspace/lib/driver-run.js +187 -0
  30. package/workspace/lib/driver.js +408 -0
  31. package/workspace/lib/extension-cli.js +39 -0
  32. package/workspace/lib/headless.js +13 -2
  33. package/workspace/lib/meeting-focus.js +48 -1
  34. package/workspace/lib/needs-providers.js +1 -0
  35. package/workspace/lib/pmos-verbs.js +6 -0
  36. package/workspace/lib/route-modules.js +1 -0
  37. package/workspace/lib/routes-boards.js +11 -1
  38. package/workspace/lib/routes-driver.js +167 -0
  39. package/workspace/lib/routes-meeting-focus.js +17 -1
  40. package/workspace/package-lock.json +2 -2
  41. package/workspace/package.json +1 -1
  42. package/workspace/public/pm/c3-brief-needs.js +19 -3
  43. package/workspace/public/pm/c3-demo-focus.js +12 -2
  44. package/workspace/public/pm/c3-demo.js +1 -1
  45. package/workspace/public/pm/c3-driver-work.js +110 -0
  46. package/workspace/public/pm/c3-driver.css +18 -0
  47. package/workspace/public/pm/c3-driver.js +298 -0
  48. package/workspace/public/pm/c3-focus.css +31 -0
  49. package/workspace/public/pm/c3-focus.js +122 -21
  50. package/workspace/public/pm/c3-ii-tabs.js +3 -2
  51. package/workspace/public/pm/c3-ini-ui.js +4 -2
  52. package/workspace/public/pm/c3-initiatives.js +14 -0
  53. package/workspace/public/pm/c3-shell.js +2 -2
  54. package/workspace/public/pm/pm-work-view.js +12 -2
  55. package/workspace/server.js +21 -1
  56. package/workspace/skills/initiative-drive.skill +50 -0
  57. package/workspace/skills/work-unblock.skill +42 -0
  58. package/workspace/test-files.json +6 -0
  59. package/workspace/test-lists/c3-meeting-focus.json +13 -4
@@ -0,0 +1,107 @@
1
+ 'use strict';
2
+ /* driver-needs.js — the needs provider of the Initiative Driver (initiative initiative-driver, PRD sections 3 to 5).
3
+ * lib/needs-providers.js folds these rows into server.js deriveNeeds, and from there into /api/needs and Today.
4
+ *
5
+ * kinds: driver-drafts, driver-blocked
6
+ * rowsFor(w, { lead }) → rows · sig(w) → string · titlesOf(w) · rowsOf(state, { titleOf, now, ledgerRel })
7
+ *
8
+ * driver-drafts one row per initiative or Work task with open proposals: "Agent has 3 drafts on <title>".
9
+ * driver-blocked one row for a target the PM is blocking: its drafts have waited more than a day, and today's sweep raised
10
+ * the finding again. The sweep starts no run for it; the row is the reminder.
11
+ *
12
+ * SHADOW MODE raises no row (PRD section 5): the board mode, taken from the board of the target's latest run, decides. The
13
+ * rows are repository-wide, so a tree that is not the lead returns []. A row never carries a session token.
14
+ */
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+ const crypto = require('crypto');
18
+ const D = require('./driver.js');
19
+
20
+ const NAME = 'driver-needs';
21
+ const plural = (n, one, many) => `${n} ${n === 1 ? one : many}`;
22
+ const KIND_WORD = { initiative: 'the initiative', work: 'the Work task' };
23
+
24
+ /** Titles for the targets of a workspace: initiative titles from the cached workflow rows, Work titles from the Work items. */
25
+ function titlesOf(w) {
26
+ const out = new Map();
27
+ try {
28
+ const r = require('./initiative-rows.js').peek(w.root);
29
+ if (r && r.ok) for (const x of r.rows) out.set(`initiative:${x.id}`, x.title || x.id);
30
+ } catch { /* the id stands in */ }
31
+ try {
32
+ const pmItems = require('./pm-items.js');
33
+ const f = path.join(require('./state-root.js').defaultRoot(), 'pm-items', 'items.jsonl');
34
+ for (const i of pmItems.fold(pmItems.readLines(f))) out.set(`work:${i.id}`, i.title || i.id);
35
+ } catch { /* the id stands in */ }
36
+ return out;
37
+ }
38
+
39
+ function rowsOf(state, { titleOf = () => null, now = Date.now(), ledgerRel = 'pmos/state/driver.jsonl' } = {}) {
40
+ const rows = [];
41
+ const open = new Map();
42
+ for (const p of D.openProposals(state)) {
43
+ const k = D.targetKey(p.target);
44
+ if (!open.has(k)) open.set(k, { target: p.target, list: [] });
45
+ open.get(k).list.push(p);
46
+ }
47
+ const shadow = (target) => {
48
+ let board = target.kind === 'work' ? 'work' : null;
49
+ if (target.kind !== 'work') {
50
+ const runs = [...state.runs.values()].filter((r) => D.targetKey(r.target) === D.targetKey(target) && r.board);
51
+ if (runs.length) board = runs[runs.length - 1].board;
52
+ }
53
+ return D.modeOf(state, board) === 'shadow';
54
+ };
55
+ for (const [k, g] of open) {
56
+ if (shadow(g.target)) continue;
57
+ const title = titleOf(g.target) || g.target.id;
58
+ const whats = [...new Set(g.list.map((p) => p.what))].join(', ');
59
+ rows.push({
60
+ run_id: `${NAME}:drafts:${k}`, kind: 'driver-drafts', initiative_id: g.target.kind === 'initiative' ? g.target.id : null,
61
+ question: `Agent has ${plural(g.list.length, 'draft', 'drafts')} on ${title}`, doc: null,
62
+ today: { kind: 7, source: NAME, cause: `The agent filed ${plural(g.list.length, 'draft', 'drafts')} for ${KIND_WORD[g.target.kind]}: ${whats}`, evidence: ledgerRel, act: { label: 'Review', cli: null } },
63
+ });
64
+ }
65
+ const today = D.localDay(now);
66
+ const seen = new Set();
67
+ for (const b of state.blockers) {
68
+ if (b.owner !== 'pm' || b.day !== today || !b.target) continue;
69
+ const k = D.targetKey(b.target);
70
+ if (seen.has(k) || !open.has(k) || shadow(b.target)) continue;
71
+ const g = open.get(k);
72
+ const oldest = g.list.map((p) => Date.parse(p.at)).filter(Number.isFinite).sort((a, c) => a - c)[0];
73
+ if (!(now - oldest >= D.PM_WAIT_MS)) continue;
74
+ seen.add(k);
75
+ const days = Math.max(1, Math.floor((now - oldest) / D.DAY));
76
+ const title = titleOf(g.target) || g.target.id;
77
+ rows.push({
78
+ run_id: `${NAME}:blocked:${k}`, kind: 'driver-blocked', initiative_id: g.target.kind === 'initiative' ? g.target.id : null,
79
+ question: `${title} is waiting on you: the drafts are ${plural(days, 'day', 'days')} old`, doc: null,
80
+ today: { kind: 7, source: NAME, cause: 'The agent starts no new run on this until you keep, send back or drop its drafts', evidence: ledgerRel, act: { label: 'Review', cli: null } },
81
+ });
82
+ }
83
+ return rows;
84
+ }
85
+
86
+ function rowsFor(w, { lead } = {}) {
87
+ if (!lead || !w || !w.root) return [];
88
+ const dir = D.stateDirOf(w.root);
89
+ if (!dir) return [];
90
+ const file = D.ledgerFile(dir);
91
+ const state = D.fold(D.readLines(file));
92
+ if (!state.proposals.size) return [];
93
+ const titles = titlesOf(w);
94
+ let rel = 'driver.jsonl';
95
+ try { rel = path.relative(w.root, file).split(path.sep).join('/'); } catch { /* the plain name stands */ }
96
+ return rowsOf(state, { titleOf: (t) => titles.get(`${t.kind}:${t.id}`) || null, now: Date.now(), ledgerRel: rel });
97
+ }
98
+
99
+ function sig(w) {
100
+ if (!w || !w.root) return 'none';
101
+ const dir = D.stateDirOf(w.root);
102
+ let st = 'none';
103
+ try { const s = fs.statSync(D.ledgerFile(dir)); st = `${s.size}:${s.mtimeMs}:${s.ino}`; } catch { /* no ledger yet */ }
104
+ return crypto.createHash('sha1').update(`${st}|${D.localDay(Date.now())}`).digest('hex');
105
+ }
106
+
107
+ module.exports = { kinds: ['driver-drafts', 'driver-blocked'], rowsFor, sig, titlesOf, rowsOf };
@@ -0,0 +1,187 @@
1
+ 'use strict';
2
+ /* driver-run.js — the Initiative Driver's part of the server tick (initiative initiative-driver, PRD sections 1 to 3).
3
+ *
4
+ * create(deps) → { tick(opts), active() }
5
+ *
6
+ * server.js builds one driver at start and calls tick() from the 60-second tick. One tick, per repository (the lead tree of each):
7
+ * 1. writes the ledger's `epoch` on the first scan, so only events AFTER it wake a run (an old initiative is history);
8
+ * 2. closes runs that were alive when the server stopped (`run-end`, reason "interrupted"): they still count against the day;
9
+ * 3. finds the wakes: a `created` placement line (the page, Triage accept and `pmos initiative new` all write one), a board
10
+ * `move` line, a proposal the PM sent back with a note. Each key wakes once (the ledger's `wake` line makes it idempotent);
11
+ * 4. every 30 minutes inside the day-check window, and outside quiet hours, runs the unblock sweep (no model, lib/driver.js);
12
+ * 5. starts the queued runs the rules allow (mode, pause, budget, 3 at once) through spawnSession with origin `drive:<kind>:<id>`
13
+ * and the skill prompt, which lib/headless.js turns into a `claude -p` run with a fixed tool list.
14
+ * `PMOS_DRIVER=off` does none of it. A run ends in a `run-end` line with its reason when it fails or times out.
15
+ *
16
+ * deps: { gather?(w, ctx, opts) → facts answer (tests), workspaces() → [w], isLead(w), stateDirFor(w), ctx (for lib/routes-boards.js), runtimeDir, settings() → proactive settings,
17
+ * inQuiet(now, quiet) → bool, spawn(spec) → session row, promptFor(kind) → skill body | null, boardOf(w, id) → board id | null,
18
+ * homeId() → the workspace id that holds the Work tasks, headlessOff() → bool, env, warn(what, err) }
19
+ * `spawn(spec)` gets { name, ws, origin, initialInput, initiative, onDone }; it returns the session row ({ id, state, headless }).
20
+ */
21
+ const fs = require('fs');
22
+ const path = require('path');
23
+ const D = require('./driver.js');
24
+ const facts = require('./driver-facts.js');
25
+ const proactive = require('./proactive.js');
26
+
27
+ const off = (env) => /^(off|0|false|no)$/i.test(String((env || process.env).PMOS_DRIVER || '').trim());
28
+ const hhmm = (ms) => { const d = new Date(ms); return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`; };
29
+ const dayCheckOpen = (settings, now, inQuiet) => {
30
+ const dc = (settings && settings.dayCheck) || proactive.DEFAULTS.dayCheck;
31
+ const t = hhmm(now);
32
+ return settings && settings.enabled === false ? false : (t >= dc.start && t < dc.end && !inQuiet(now, settings && settings.quiet));
33
+ };
34
+
35
+ /** The lines of a plain JSONL file that parse; a missing file is empty. */
36
+ function readJsonl(f) {
37
+ let text = '';
38
+ try { text = fs.readFileSync(f, 'utf8'); } catch { return []; }
39
+ const out = [];
40
+ for (const raw of text.split('\n')) { if (!raw.trim()) continue; try { const l = JSON.parse(raw); if (l && typeof l === 'object') out.push(l); } catch { /* torn line */ } }
41
+ return out;
42
+ }
43
+
44
+ function create(deps) {
45
+ const active = new Map(); // run id → { target, ledger, session }
46
+ const lastSweep = new Map(); // ledger file → ms
47
+ const warn = deps.warn || (() => {});
48
+ const gather = deps.gather || facts.gather; // tests pass their own facts
49
+
50
+ function readState(file) { return D.fold(D.readLines(file)); }
51
+
52
+ function describe(w, target, wakeLines, state, factsNow) {
53
+ const lines = [];
54
+ const reasons = [...new Set(wakeLines.map((x) => x.reason))];
55
+ let title = target.id; let stage = null; let facts1 = null;
56
+ if (factsNow) {
57
+ facts1 = target.kind === 'work' ? factsNow.work.find((x) => x.id === target.id) : factsNow.initiatives.find((x) => x.id === target.id);
58
+ if (facts1 && facts1.title) title = facts1.title;
59
+ }
60
+ lines.push(target.kind === 'work'
61
+ ? `Target: Work task ${target.id} "${title}" (workspace id ${target.ws}). Skill: work-unblock.`
62
+ : `Target: initiative ${target.id} "${title}" in repository ${w.name || path.basename(w.root)} (workspace id ${target.ws}). Skill: initiative-drive.`);
63
+ lines.push(`Why this run started: ${reasons.join(', ')}.`);
64
+ if (facts1 && target.kind === 'initiative') {
65
+ stage = facts1.cat;
66
+ lines.push(`Stage category: ${stage || 'unknown'}. Kept problem statement: ${facts1.has_problem ? 'yes' : 'no'}. Kept evidence: ${facts1.has_evidence ? 'yes' : 'no'}. PRD: ${facts1.has_prd ? 'yes' : 'no'}. Anchor: ${facts1.has_anchor ? 'yes' : 'no'}.`);
67
+ }
68
+ if (facts1 && target.kind === 'work') {
69
+ lines.push(`Placement: ${facts1.placement}. Due: ${facts1.due || 'none'}. Waiting on: ${facts1.waiting_on || 'nobody'}${facts1.waiting_since ? ` since ${String(facts1.waiting_since).slice(0, 10)}` : ''}. Steps: ${facts1.steps}.`);
70
+ }
71
+ const blockers = wakeLines.filter((x) => x.blocker).map((x) => x.blocker);
72
+ if (blockers.length) lines.push(`Blockers the sweep found: ${[...new Set(blockers)].join(', ')}.`);
73
+ const retNotes = wakeLines.filter((x) => x.reason === 'answered').map((x) => state.proposals.get(String(x.key).replace(/^answered:/, ''))).filter(Boolean);
74
+ for (const p of retNotes) lines.push(`The PM sent back ${p.id} (${p.what}) with this note: "${p.note}". Revise it and file a new proposal.`);
75
+ const mine = [...state.proposals.values()].filter((p) => D.targetKey(p.target) === D.targetKey(target));
76
+ if (mine.length) lines.push(`Proposals so far: ${mine.map((p) => `${p.id} ${p.what} (${p.state})`).join('; ')}. Do not file one again.`);
77
+ return lines;
78
+ }
79
+
80
+ function startRun(w, file, group, state, factsNow, now) {
81
+ const kind = group.target.kind;
82
+ const skill = deps.promptFor(kind === 'work' ? 'work-unblock' : 'initiative-drive');
83
+ const runId = D.newId('run');
84
+ const wakeLines = state.wakes.filter((x) => group.wakes.includes(x.key));
85
+ const board = group.board || null;
86
+ if (!skill) {
87
+ D.append(file, { ev: 'run-start', run: runId, target: group.target, board, wakes: group.wakes, session: null }, { now });
88
+ D.append(file, { ev: 'run-end', run: runId, ok: false, reason: `the ${kind === 'work' ? 'work-unblock' : 'initiative-drive'} skill is not installed`, agent_run: agentRun(runId, group.target, false, now) }, { now });
89
+ return;
90
+ }
91
+ const prompt = `${skill}\n\nThis run:\n${describe(w, group.target, wakeLines, state, factsNow).map((l) => '- ' + l).join('\n')}\n\nFile every result with \`pmos driver propose\`. Send nothing, edit nothing.`;
92
+ let session = null;
93
+ try {
94
+ session = deps.spawn({
95
+ name: `Driver · ${kind} ${group.target.id}`, ws: w.id, origin: `drive:${kind}:${group.target.id}`, initialInput: prompt + '\r',
96
+ initiative: kind === 'initiative' ? group.target.id : undefined,
97
+ onDone: (r) => endRun(file, runId, group.target, r),
98
+ });
99
+ } catch (e) { warn('driver run', e); }
100
+ D.append(file, { ev: 'run-start', run: runId, target: group.target, board, wakes: group.wakes, session: session ? session.id : null }, { now });
101
+ if (!session || session.state === 'exited' || session.headless !== 'drive') {
102
+ // A start that did not become a headless run (no `claude` on the PATH, a refused model choice, a pane because headless
103
+ // is off) is a failed run. It counts against the day, as every failed run does. Never leave a driver in a pane.
104
+ if (session && session.headless !== 'drive' && session.state !== 'exited' && deps.stop) { try { deps.stop(session); } catch { /* best effort */ } }
105
+ D.append(file, { ev: 'run-end', run: runId, ok: false, reason: (session && session.note) || 'the run could not start as a background run', agent_run: agentRun(runId, group.target, false, now) }, { now });
106
+ return;
107
+ }
108
+ active.set(runId, { target: group.target, ledger: file, session: session.id });
109
+ }
110
+
111
+ const agentRun = (runId, target, ok, now) => ({ run_id: runId, initiative_id: target.kind === 'initiative' ? target.id : null, kind: 'driver', target_kind: target.kind,
112
+ ok, human_corrections: 0, builder_model: 'not recorded in the repo', created_at: new Date(now == null ? Date.now() : now).toISOString() });
113
+
114
+ function endRun(file, runId, target, r) {
115
+ active.delete(runId);
116
+ const res = (r && r.result) || {};
117
+ const ok = !!(r && !r.timedOut && !r.stopped && r.code === 0 && res.ok && !res.isError);
118
+ const reason = ok ? null : (r && r.note) || 'the run failed';
119
+ try {
120
+ D.append(file, { ev: 'run-end', run: runId, ok, ...(reason ? { reason } : {}), ...(res.denied && res.denied.length ? { denied: res.denied } : {}),
121
+ agent_run: agentRun(runId, target, ok) });
122
+ } catch (e) { warn('driver run end', e); }
123
+ }
124
+
125
+ function tick({ now = Date.now(), env } = {}) {
126
+ if (off(env || deps.env)) return { off: true };
127
+ const summary = { epoch: 0, woke: 0, swept: 0, started: 0, refused: 0 };
128
+ const leads = [];
129
+ for (const w of deps.workspaces()) {
130
+ let dir = null;
131
+ try { dir = deps.isLead(w) ? deps.stateDirFor(w) : null; } catch { dir = null; }
132
+ if (dir) leads.push({ w, file: D.ledgerFile(dir), dir });
133
+ }
134
+ const settings = deps.settings();
135
+ const home = deps.homeId();
136
+ for (const { w, file, dir } of leads) {
137
+ try {
138
+ let state = readState(file);
139
+ if (!state.epoch) { D.append(file, { ev: 'epoch' }, { now }); state = readState(file); summary.epoch++; }
140
+ // Runs that were alive when the server stopped are over; they keep their place in the day's count.
141
+ for (const r of state.runs.values()) {
142
+ if (!r.end && !active.has(r.run)) D.append(file, { ev: 'run-end', run: r.run, ok: false, reason: 'interrupted: the workspace restarted', agent_run: agentRun(r.run, r.target, false, now) }, { now });
143
+ }
144
+ const boardOf = (id) => { try { return deps.boardOf(w, id); } catch { return null; } };
145
+ let created = [];
146
+ try { created = require('./plan-ledgers.js').readLines(w.root, 'placements').lines.map((x) => x.rec).filter((r) => r && r.ev === 'created'); } catch { created = []; }
147
+ const moved = readJsonl(path.join(dir, 'boards.jsonl')).filter((e) => e.kind === 'move');
148
+ const newWakes = [...D.wakesFromLedgers({ created, moved }, state, { ws: w.id, epoch: state.epoch, boardOf }), ...D.answeredWakes(state, { boardOf })];
149
+ for (const l of newWakes) { D.append(file, l, { now }); summary.woke++; }
150
+ state = readState(file);
151
+ // The unblock sweep: deterministic, every 30 minutes inside the day-check window.
152
+ const due = !lastSweep.has(file) || now - lastSweep.get(file) >= D.SWEEP_EVERY_MS;
153
+ let factsNow = null;
154
+ if (due && dayCheckOpen(settings, now, deps.inQuiet)) {
155
+ const g = gather(w, deps.ctx, { now, includeWork: w.id === home, runtimeDir: deps.runtimeDir, driverState: state });
156
+ if (g.ok) {
157
+ factsNow = g.facts;
158
+ lastSweep.set(file, now);
159
+ const sw = D.sweepLines(g.facts, state, { now, stalledDays: settings.stalledDays });
160
+ for (const l of sw.lines) { D.append(file, l, { now }); if (l.ev === 'wake') summary.swept++; }
161
+ state = readState(file);
162
+ } else if (!g.pending) warn('driver sweep', new Error(g.error));
163
+ }
164
+ // Start what the rules allow.
165
+ if (deps.headlessOff()) continue; // a driver never runs in a pane
166
+ const running = [...active.values()];
167
+ const p = D.plan(state, { now, running: active.size, activeTargets: running.map((r) => D.targetKey(r.target)) });
168
+ for (const r of p.refused) { D.append(file, { ev: 'refused', target: r.target, wakes: r.wakes, reason: r.reason }, { now }); summary.refused++; }
169
+ if (p.start.length && !factsNow) {
170
+ const g = gather(w, deps.ctx, { now, includeWork: w.id === home, runtimeDir: deps.runtimeDir, driverState: state });
171
+ factsNow = g.ok ? g.facts : null;
172
+ }
173
+ let live = active.size;
174
+ for (const group of p.start) {
175
+ if (live >= D.BUDGET.concurrent) break;
176
+ startRun(w, file, group, state, factsNow, now);
177
+ live = active.size; summary.started++;
178
+ }
179
+ } catch (e) { warn('driver tick', e); }
180
+ }
181
+ return summary;
182
+ }
183
+
184
+ return { tick, active: () => [...active.keys()] };
185
+ }
186
+
187
+ module.exports = { create, off, dayCheckOpen };