create-pmos 0.46.0 → 0.47.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 (33) hide show
  1. package/package.json +1 -1
  2. package/payload/KIT-VERSION +1 -1
  3. package/payload/mods/pmos-cockpit/.claude-plugin/plugin.json +1 -1
  4. package/payload/mods/pmos-cockpit/README.md +9 -1
  5. package/payload/mods/pmos-cockpit/hooks/lib/handover-run.mjs +33 -0
  6. package/payload/mods/pmos-cockpit/hooks/lib/handover.mjs +267 -0
  7. package/payload/mods/pmos-cockpit/hooks/register.tsx +152 -0
  8. package/payload/mods/pmos-cockpit/types/index.d.ts +16 -1
  9. package/payload/okf/core/concepts/frontmatter-format.md +6 -1
  10. package/payload/planning/evals/eval-calibration.md +1 -1
  11. package/payload/planning/okrs/health-budgets.md +1 -1
  12. package/payload/skills/SKILL-FORMAT.md +1 -0
  13. package/payload/state/discoveries.md +1 -1
  14. package/workspace/README.md +1 -1
  15. package/workspace/guide/initiatives-and-next-steps.md +1 -0
  16. package/workspace/guide/knowledge-graph.md +6 -1
  17. package/workspace/guide/layout.md +1 -0
  18. package/workspace/guide/proactive-engine.md +21 -3
  19. package/workspace/guide/scheduled-tasks-and-cli.md +1 -0
  20. package/workspace/guide/statuses.md +1 -0
  21. package/workspace/guide/validate-with-typesafe.md +1 -0
  22. package/workspace/lib/kg-cli.js +2 -1
  23. package/workspace/lib/kg-dangling-baseline.json +1 -0
  24. package/workspace/lib/kg-dangling-rules.json +1 -0
  25. package/workspace/lib/kg.js +20 -8
  26. package/workspace/lib/learn.js +280 -0
  27. package/workspace/lib/proactive-cli.js +49 -1
  28. package/workspace/lib/proactive.js +8 -7
  29. package/workspace/package-lock.json +1 -1
  30. package/workspace/package.json +3 -3
  31. package/workspace/public/pm/modes-live.js +52 -4
  32. package/workspace/server.js +104 -3
  33. package/workspace/skills/morning-brief.skill +1 -0
@@ -16,13 +16,14 @@
16
16
  * pmos proactive prepare plan --initiative ID [--repo PATH]
17
17
  * pmos proactive prepare queue --initiative ID --item ITEM --sha HASH [--repo PATH]
18
18
  * pmos proactive prepare status | cancel ID | run
19
+ * pmos proactive learn [status] | run | lessons | approve ID | reject ID | retire ID | restore TRIGGER | reset
19
20
  */
20
21
  const fs = require('fs');
21
22
  const path = require('path');
22
23
  const { execFileSync } = require('child_process');
23
24
  const { parseArgs } = require('./schedule-cli');
24
25
 
25
- const COMMANDS = { proactive: ['today', 'scan', 'settings', 'feedback', 'brief', 'prepare'] };
26
+ const COMMANDS = { proactive: ['today', 'scan', 'settings', 'feedback', 'brief', 'prepare', 'learn'] };
26
27
  const BRIEF_SOURCES = ['calendar', 'wispr', 'mail', 'jira', 'confluence'];
27
28
  const onOff = (v, what) => (v === 'on' || v === 'true' ? true : v === 'off' || v === 'false' ? false : { err: `${what} takes on or off` });
28
29
 
@@ -141,6 +142,53 @@ async function run(argv, { call, say, cwd = process.cwd() }) {
141
142
  for (const e of r.entries) say(`${e.id} ${e.status} ${e.label}${e.reason ? ' — ' + e.reason : ''}`);
142
143
  return 0;
143
144
  }
145
+ if (verb === 'learn') {
146
+ const sub = pos[0] || 'status';
147
+ const rate = (x, what) => (x.rate === null ? `${what}: not enough evidence (${x.n} of ${x.min} needed)` : `${what}: ${Math.round(x.rate * 100)}% (${x.n} counted)`);
148
+ const decide = async (decision, past) => {
149
+ if (!pos[1]) return fail(`usage: pmos proactive learn ${decision} ID`);
150
+ const r = await call('POST', '/api/proactive/learn/lesson', { id: pos[1], decision, confirm: true });
151
+ if (!r || r.ok === false) return fail(`refused — ${(r && r.error) || 'the server did not answer'}. Nothing changed.`);
152
+ say(`${past} ${r.id}`); return 0;
153
+ };
154
+ if (sub === 'approve') return decide('approve', 'approved');
155
+ if (sub === 'reject') return decide('reject', 'rejected');
156
+ if (sub === 'retire') return decide('retire', 'retired');
157
+ if (sub === 'restore') {
158
+ if (!pos[1]) return fail('usage: pmos proactive learn restore TRIGGER');
159
+ const r = await call('POST', '/api/proactive/learn/restore', { kind: pos[1], confirm: true });
160
+ if (!r || r.ok === false) return fail(`refused — ${(r && r.error) || 'the server did not answer'}. Nothing changed.`);
161
+ say(`restored ${r.kind} — it comes back on Today and stays until 10 new ratings of it have arrived`); return 0;
162
+ }
163
+ if (sub === 'reset') {
164
+ const r = await call('POST', '/api/proactive/learn/reset', { confirm: true });
165
+ if (!r || r.ok === false) return fail(`refused — ${(r && r.error) || 'the server did not answer'}. Nothing changed.`);
166
+ say('reset — earlier feedback is ignored from now on. It is not deleted.'); return 0;
167
+ }
168
+ if (sub === 'run') {
169
+ const r = await call('POST', '/api/proactive/learn/run', {});
170
+ if (!r || r.ok === false) return fail(`refused — ${(r && r.error) || 'the server did not answer'}`);
171
+ say(`weekly review taken — ${r.proposed.length} lesson(s) proposed${r.snapshot.narrowed.length ? ', narrowed: ' + r.snapshot.narrowed.join(', ') : ''}`);
172
+ return 0;
173
+ }
174
+ const r = await call('GET', '/api/proactive/learn');
175
+ if (!r || r.ok === false) return fail((r && r.error) || 'the server did not answer');
176
+ if (sub === 'lessons') {
177
+ if (r.paused) { say('Learning is paused: no lesson applies.'); return 0; }
178
+ if (!r.approved.length) say('No approved lessons.');
179
+ for (const l of r.approved) say(`- ${l.text}`);
180
+ return 0;
181
+ }
182
+ if (sub !== 'status') return fail('usage: pmos proactive learn [status|run|lessons|approve ID|reject ID|retire ID|restore TRIGGER|reset]');
183
+ say(`learning ${r.paused ? 'paused' : 'on'} · ${r.evidenceCount} piece(s) of evidence${r.resetAt ? ' since ' + r.resetAt : ''}`);
184
+ const ro = r.rollup;
185
+ say(`last ${ro.days} days — ${rate(ro.hit, 'hit rate')} · ${rate(ro.asIs, 'used as is')} · ${rate(ro.corrections, 'corrections per prepared run')} · ${ro.missed} reported missed`);
186
+ say('watermelon check: ' + ro.watermelon.note + (ro.watermelon.reasons.length ? ' ' + ro.watermelon.reasons.join('; ') + '.' : ''));
187
+ for (const c of r.cells.filter((x) => x.delta)) say(`weight ${c.dim} ${c.key}: ${c.delta > 0 ? '+' : ''}${c.delta} (${c.n} events, net ${c.sum})`);
188
+ for (const n of r.narrowed) say(`narrowed: ${n.kind} — useful ${Math.round(n.rate * 100)}% over ${n.n} ratings in ${n.days} days (floor ${Math.round(n.floor * 100)}%). Restore with: pmos proactive learn restore ${n.kind}`);
189
+ for (const l of r.lessons.filter((x) => x.status === 'proposed' || x.status === 'approved')) say(`${l.id} ${l.status} ${l.text}`);
190
+ return 0;
191
+ }
144
192
  if (verb === 'brief') {
145
193
  const sub = pos[0] || 'show';
146
194
  if (sub === 'submit') {
@@ -39,14 +39,14 @@ const clip = (s, n) => String(s == null ? '' : s).replace(/\s+/g, ' ').trim().sl
39
39
  const days = (ms) => Math.floor(ms / DAY);
40
40
 
41
41
  function cand(kind, key, p) {
42
- return { id: idOf(key), key, kind, owner: OWNER[kind], initiative_id: p.initiative_id || null, ws: p.ws || null, repo: p.repo || null,
42
+ return { id: idOf(key), key, kind, owner: OWNER[kind], initiative_id: p.initiative_id || null, ws: p.ws || null, repo: p.repo || null, lane: p.lane || null,
43
43
  title: clip(p.title, 120), why: clip(p.why, 200), evidence: clip(p.evidence, 240), confidence: p.confidence, since: p.since == null ? null : p.since,
44
44
  open: p.open || null };
45
45
  }
46
46
 
47
47
  /**
48
48
  * @param {{now:number, settings:object, repos:Array, nextStepRuns:Array}} ctx
49
- * repos[]: {name, ws, error?, initiatives:[{id,title,active,lastRun,hasRuns}], needs:[{initiative_id,question,run_id}],
49
+ * repos[]: {name, ws, error?, initiatives:[{id,title,active,lane,lastRun,hasRuns}], needs:[{initiative_id,question,run_id}],
50
50
  * returned:[{initiative_id,note,at}], schedules:[{id,name,lastStatus,lastRun}]}
51
51
  * nextStepRuns[]: {session_id, initiative_id, ws, key, ended, reviewed, at}
52
52
  * @returns {{candidates:Array, unreadable:Array<{repo:string,error:string}>}}
@@ -61,7 +61,7 @@ function detect(ctx) {
61
61
  for (const n of r.needs || []) {
62
62
  const ini = byId.get(n.initiative_id);
63
63
  out.push(cand('needs', `needs:${r.ws}:${n.run_id || n.initiative_id + ':' + clip(n.question, 60)}`, {
64
- initiative_id: n.initiative_id, ws: r.ws, repo: r.name, title: ini ? ini.title || ini.id : (n.initiative_id || 'an open item'),
64
+ initiative_id: n.initiative_id, ws: r.ws, repo: r.name, lane: ini ? ini.lane : null, title: ini ? ini.title || ini.id : (n.initiative_id || 'an open item'),
65
65
  why: 'An open item is waiting on you.', evidence: n.question, confidence: 0.95, since: null,
66
66
  open: n.initiative_id ? { type: 'initiative', initiative_id: n.initiative_id, ws: r.ws } : null }));
67
67
  }
@@ -71,7 +71,7 @@ function detect(ctx) {
71
71
  const ini = byId.get(x.initiative_id);
72
72
  if (ini && !ini.active) continue;
73
73
  out.push(cand('returned', `returned:${r.ws}:${x.initiative_id}:${x.at || ''}`, {
74
- initiative_id: x.initiative_id, ws: r.ws, repo: r.name, title: ini ? ini.title || ini.id : x.initiative_id,
74
+ initiative_id: x.initiative_id, ws: r.ws, repo: r.name, lane: ini ? ini.lane : null, title: ini ? ini.title || ini.id : x.initiative_id,
75
75
  why: 'You returned this at the Acceptance Gate and it has not been accepted since.', evidence: x.note || 'returned', confidence: 0.95, since: x.at ? Date.parse(x.at) || null : null,
76
76
  open: { type: 'initiative', initiative_id: x.initiative_id, ws: r.ws } }));
77
77
  }
@@ -93,7 +93,7 @@ function detect(ctx) {
93
93
  if (idle < limit) continue;
94
94
  const d = days(idle);
95
95
  out.push(cand('stalled', `stalled:${r.ws}:${i.id}`, {
96
- initiative_id: i.id, ws: r.ws, repo: r.name, title: i.title || i.id,
96
+ initiative_id: i.id, ws: r.ws, repo: r.name, lane: i.lane, title: i.title || i.id,
97
97
  why: `In progress, with no agent run for ${d} days.`, evidence: `last run ${new Date(i.lastRun).toISOString().slice(0, 10)}; the threshold is ${st.stalledDays || DEFAULTS.stalledDays} days`,
98
98
  confidence: Math.min(0.85, +(0.5 + 0.05 * (d - (st.stalledDays || DEFAULTS.stalledDays))).toFixed(2)), since: i.lastRun,
99
99
  open: { type: 'initiative', initiative_id: i.id, ws: r.ws } }));
@@ -112,9 +112,10 @@ function detect(ctx) {
112
112
  return { candidates: out, unreadable };
113
113
  }
114
114
 
115
- function rank(cands, now) {
115
+ /** `adj` is optional (slice D4): c → {delta, why[]}. Without it the order is the fixed one, unchanged. */
116
+ function rank(cands, now, adj) {
116
117
  const score = (c) => BASE[c.kind] + (c.since != null && now != null ? Math.min(30, Math.max(0, days(now - c.since))) : 0);
117
- return cands.map((c) => ({ ...c, rank: score(c) })).sort((a, b) => b.rank - a.rank || (a.id < b.id ? -1 : 1))
118
+ return cands.map((c) => { const a = typeof adj === 'function' ? adj(c) : null; const d = a && a.delta ? a.delta : 0; return { ...c, rank: score(c) + d, adjust: d, adjust_why: a && a.why ? a.why.join('; ') : '' }; }).sort((a, b) => b.rank - a.rank || (a.id < b.id ? -1 : 1))
118
119
  .map((c, i) => ({ ...c, n: i + 1 }));
119
120
  }
120
121
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.46.0",
3
+ "version": "0.47.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.46.0",
3
+ "version": "0.47.0",
4
4
  "private": true,
5
5
  "description": "PMOS Terminal Workspace — local companion hosting real PTYs beside the library and gates (D62)",
6
6
  "license": "MIT",
@@ -10,12 +10,12 @@
10
10
  "scripts": {
11
11
  "start": "node server.js",
12
12
  "test": "node test.js && npm run test:modules",
13
- "test:modules": "node proactive.test.js && node focus.test.js && node handoff.test.js && node repo-scan.test.js && node profiles.test.js && node external-linking.test.js && node story-artifacts.test.js && node no-network.test.js && node typesafe-validation.test.js && node claims.test.js && node jev-claims.test.js && node repo-watch.test.js && node fixture-parity.test.mjs && node markdown.test.mjs && node start-policy.test.js && node state-isolation.test.js && node sync.test.js && node schedule-cli.test.js && node initiative-surface.test.js && node guide.test.js && node next-step-actions.test.js && node kg.test.js && node proactive-prepare.test.js && node session-flag.test.js",
13
+ "test:modules": "node proactive.test.js && node focus.test.js && node handoff.test.js && node repo-scan.test.js && node profiles.test.js && node external-linking.test.js && node story-artifacts.test.js && node no-network.test.js && node typesafe-validation.test.js && node claims.test.js && node jev-claims.test.js && node repo-watch.test.js && node fixture-parity.test.mjs && node markdown.test.mjs && node start-policy.test.js && node state-isolation.test.js && node sync.test.js && node schedule-cli.test.js && node initiative-surface.test.js && node guide.test.js && node next-step-actions.test.js && node kg.test.js && node proactive-prepare.test.js && node learn.test.js && node session-flag.test.js",
14
14
  "test:ui": "node ui-test.js",
15
15
  "test:parity": "node design-parity.js --states",
16
16
  "test:all": "npm test && npm run test:panels && npm run test:ui && npm run test:parity && npm run test:offline",
17
17
  "doctor": "node doctor.js",
18
- "test:panels": "node pm-shell-test.js && node pm-repos-test.js && node pm-initiatives-test.js && node pm-detail-test.js && node pm-tabs-test.js && node pm-term-test.js && node pm-modes-test.js && node pm-r1-test.js && node pm-r2-test.js && node pm-r3-test.js && node pm-r4-test.js && node pm-r5-test.js && node pm-r6-test.js && node pm-r7-test.js && node pm-r8-test.js && node pm-r9-test.js && node pm-r10-test.js && node pm-r11-test.js && node pm-r12-test.js && node pm-r13-test.js && node pm-r14-test.js && node pm-r15-test.js && node pm-r16-test.js && node pm-r17-test.js && node pm-r16-test.js && node pm-r18-test.js && node pm-r19-test.js && node pm-r20-test.js && node pm-r21-test.js && node pm-r22-test.js && node pm-r23-test.js && node pm-r24-test.js && node pm-r25-test.js",
18
+ "test:panels": "node pm-shell-test.js && node pm-repos-test.js && node pm-initiatives-test.js && node pm-detail-test.js && node pm-tabs-test.js && node pm-term-test.js && node pm-modes-test.js && node pm-r1-test.js && node pm-r2-test.js && node pm-r3-test.js && node pm-r4-test.js && node pm-r5-test.js && node pm-r6-test.js && node pm-r7-test.js && node pm-r8-test.js && node pm-r9-test.js && node pm-r10-test.js && node pm-r11-test.js && node pm-r12-test.js && node pm-r13-test.js && node pm-r14-test.js && node pm-r15-test.js && node pm-r16-test.js && node pm-r17-test.js && node pm-r16-test.js && node pm-r18-test.js && node pm-r19-test.js && node pm-r20-test.js && node pm-r21-test.js && node pm-r22-test.js && node pm-r23-test.js && node pm-r24-test.js && node pm-r25-test.js && node pm-r26-test.js",
19
19
  "test:offline": "node offline-tarball.test.js"
20
20
  },
21
21
  "engines": {
@@ -816,6 +816,7 @@ const OWNER_WORD = { pm: 'yours', agent: 'the agent\'s' };
816
816
  async function tFeedback(body, say) {
817
817
  const out = await post('/api/proactive/feedback', body);
818
818
  if (!out || out.ok === false) return tSay('bad', (out && out.error) || 'feedback was not saved');
819
+ reload('learn');
819
820
  tSay('ok', say);
820
821
  }
821
822
  async function tOpen(c) {
@@ -831,7 +832,7 @@ async function tScan() {
831
832
  }
832
833
  async function tSettings(patch) {
833
834
  const out = await post('/api/proactive/settings', patch);
834
- reload('today'); reload('brief'); reload('prepare');
835
+ reload('today'); reload('brief'); reload('prepare'); reload('learn');
835
836
  tSay(!out || out.ok === false ? 'bad' : 'ok', !out || out.ok === false ? ((out && out.error) || 'not saved') : 'saved');
836
837
  }
837
838
  function tCard(c) {
@@ -841,6 +842,7 @@ function tCard(c) {
841
842
  el('span', { style: 'font-size:12.5px' }, iv(c.title)),
842
843
  el('span', { style: 'margin-left:auto;font-family:var(--mono);font-size:10px;color:var(--muted-foreground)' }, iv(c.kind + ' · ' + (OWNER_WORD[c.owner] || c.owner)))),
843
844
  el('span', { style: 'display:block;font-size:11px;color:var(--muted-foreground);margin:2px 0 4px' }, iv(c.why + (c.evidence ? ' — ' + c.evidence : ''))),
845
+ c.adjust ? el('span', { style: 'display:block;font-size:10.5px;color:var(--muted-foreground);margin:0 0 4px', 'data-r26': 'adjust' }, iv('Learned from your feedback: ' + c.adjust_why + '.')) : null,
844
846
  el('span', { style: 'display:flex;gap:4px;flex-wrap:wrap' },
845
847
  c.open ? b('open', 'open', () => tOpen(c)) : null,
846
848
  c.initiative_id && c.ws ? el('button', { style: BTN, hover: 'color:var(--foreground)', 'data-r23': 'prepare', onClick: () => bgShowPlan(c) }, 'prepare in the background') : null,
@@ -967,6 +969,51 @@ function backgroundSection() {
967
969
  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
970
  return kids;
969
971
  }
972
+ /* learning (proactive-engine D4): bounded weights, a weekly rollup, lessons that wait for the PM's press, triggers left off */
973
+ let lNote = null;
974
+ async function lPost(url, body, say) {
975
+ const out = await post(url, body);
976
+ lNote = !out || out.ok === false ? { tone: 'bad', text: (out && out.error) || 'not done — nothing changed' } : { tone: 'ok', text: typeof say === 'function' ? say(out) : say };
977
+ reload('learn'); reload('today'); setState({});
978
+ }
979
+ const lRate = (x, what) => x.rate === null ? what + ': not enough evidence (' + x.n + ' of ' + x.min + ' needed)' : what + ': ' + Math.round(x.rate * 100) + '% (' + x.n + ' counted)';
980
+ function learningSection() {
981
+ const r = read('learn', '/api/proactive/learn');
982
+ const st = status(r, 'what it learned');
983
+ const kids = [el('div', { style: LABEL }, iv('learning'))];
984
+ if (st) return [...kids, st];
985
+ const d = r.data; const ro = d.rollup;
986
+ const b = (label, attr, fn, title) => el('button', { style: BTN, hover: 'color:var(--foreground)', title: title || null, 'data-r26': attr, onClick: fn }, label);
987
+ kids.push(el('div', { style: NOTE, 'data-r26': 'state' }, iv((d.paused ? 'Paused: nothing is weighted, narrowed or proposed, and feedback is still recorded. ' : 'On. ') + d.evidenceCount + ' piece(s) of evidence' + (d.resetAt ? ' since you reset it' : '') + '. Computed from your own feedback and review decisions on this computer; no model is called. It cannot lower or hide what blocks on you.')));
988
+ kids.push(el('div', { style: NOTE, 'data-r26': 'rollup' }, iv('Last ' + ro.days + ' days — ' + lRate(ro.hit, 'hit rate') + ' · ' + lRate(ro.asIs, 'used as is') + ' · ' + lRate(ro.corrections, 'corrections per prepared run') + ' · ' + ro.missed + ' reported missed')));
989
+ kids.push(el('div', { style: NOTE + (ro.watermelon.flag ? ';color:var(--destructive)' : ''), 'data-r26': 'watermelon' }, iv('Watermelon check: ' + ro.watermelon.note + (ro.watermelon.reasons.length ? ' ' + ro.watermelon.reasons.join('; ') + '.' : ''))));
990
+ const cells = d.cells.filter((c) => c.delta);
991
+ if (!cells.length) kids.push(el('div', { style: NOTE, 'data-r26': 'no-weights' }, iv('No weight has moved yet. A cell moves after ' + r.data.min_cell + ' events, by at most ' + d.caps.kind + ' points per trigger, ' + d.caps.lane + ' per lane and ' + d.caps.time + ' per trigger and time of day, ' + d.caps.total + ' in all.')));
992
+ for (const c of cells) kids.push(el('div', { style: 'padding:2px 14px;font-size:11.5px', 'data-r26': 'weight' }, iv(c.dim + ' ' + c.key.replace('|', ' in the ') + ': ' + (c.delta > 0 ? '+' : '') + c.delta + ' (' + c.n + ' events, net ' + c.sum + ')')));
993
+ for (const n of d.narrowed) kids.push(el('div', { 'data-r26': 'narrowed', 'data-kind': n.kind, style: 'padding:6px 14px;border-bottom:1px solid var(--border)' },
994
+ el('span', { style: 'display:block;font-size:12px' }, iv('Left off Today: ' + n.label.toLowerCase() + '. Useful ' + Math.round(n.rate * 100) + '% over ' + n.n + ' ratings in ' + n.days + ' days; the floor is ' + Math.round(n.floor * 100) + '%. They are still scanned.')),
995
+ b('restore', 'restore', () => lPost('/api/proactive/learn/restore', { kind: n.kind, confirm: true }, 'restored — it stays until 10 new ratings of it have arrived'))));
996
+ const proposed = d.lessons.filter((x) => x.status === 'proposed');
997
+ kids.push(el('div', { style: LABEL }, iv('lessons to approve')));
998
+ if (!proposed.length) kids.push(el('div', { style: NOTE, 'data-r26': 'no-lessons' }, iv('None waiting. A weekly review proposes lessons; none enters anything until you approve it.')));
999
+ for (const l of proposed) kids.push(el('div', { 'data-r26': 'lesson', 'data-id': l.id, style: 'padding:6px 14px;border-bottom:1px solid var(--border)' },
1000
+ el('span', { style: 'display:block;font-size:12px' }, iv(l.text)),
1001
+ el('span', { style: 'display:block;font-size:10.5px;color:var(--muted-foreground);margin:2px 0 4px' }, iv(l.evidence)),
1002
+ el('span', { style: 'display:flex;gap:4px' },
1003
+ b('approve', 'approve', () => lPost('/api/proactive/learn/lesson', { id: l.id, decision: 'approve', confirm: true }, 'approved — the morning-brief session will read it'), 'the morning-brief session reads it; nothing else does'),
1004
+ b('reject', 'reject', () => lPost('/api/proactive/learn/lesson', { id: l.id, decision: 'reject', confirm: true }, 'rejected — it is not proposed again for 28 days')))));
1005
+ if (d.approved.length) kids.push(el('div', { style: LABEL }, iv('approved lessons (' + d.approved.length + ' of ' + d.max_approved + ')')));
1006
+ for (const l of d.approved) kids.push(el('div', { 'data-r26': 'approved', 'data-id': l.id, style: 'padding:6px 14px;border-bottom:1px solid var(--border)' },
1007
+ el('span', { style: 'display:block;font-size:12px' }, iv(l.text)),
1008
+ b('retire', 'retire', () => lPost('/api/proactive/learn/lesson', { id: l.id, decision: 'retire', confirm: true }, 'retired'))));
1009
+ if (lNote) kids.push(el('div', { 'data-r26': 'note', style: NOTE + ';color:' + (lNote.tone === 'bad' ? 'var(--destructive)' : 'var(--foreground)') }, iv(lNote.text)));
1010
+ const last = d.snapshots[0];
1011
+ kids.push(el('div', { style: NOTE, 'data-r26': 'last-run' }, iv(last ? 'Last weekly review: ' + String(last.at).replace('T', ' ').slice(0, 16) + ' UTC. It runs about every 7 days, only while the workspace server runs, and does not catch up.' : 'No weekly review has run yet. It runs about every 7 days once there is feedback, only while the workspace server runs, and does not catch up.')));
1012
+ kids.push(el('div', { style: 'display:flex;gap:4px;flex-wrap:wrap;padding:2px 14px 6px' },
1013
+ b('run the weekly review now', 'run', () => lPost('/api/proactive/learn/run', {}, (o) => 'review taken — ' + o.proposed.length + ' lesson(s) proposed'), 'refused while learning is paused'),
1014
+ b('forget what it learned', 'reset', () => lPost('/api/proactive/learn/reset', { confirm: true }, 'reset — earlier feedback is ignored from now on, not deleted'), 'earlier feedback stops counting; nothing is deleted')));
1015
+ return kids;
1016
+ }
970
1017
  export function todayList() {
971
1018
  const r = read('today', '/api/proactive/today');
972
1019
  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.'))];
@@ -974,7 +1021,7 @@ export function todayList() {
974
1021
  kids.push(el('div', { style: LABEL }, iv('waiting on records')));
975
1022
  kids.push(el('div', { style: 'display:flex;gap:4px;flex-wrap:wrap;padding:4px 14px' },
976
1023
  el('button', { style: BTN, hover: 'color:var(--foreground)', 'data-r22': 'scan', onClick: tScan }, 'scan now'),
977
- el('button', { style: BTN, hover: 'color:var(--foreground)', title: 'read the brief and the list again', 'data-r22': 'refresh', onClick: () => { reload('today'); reload('brief'); setState({}); } }, '↻ refresh'),
1024
+ el('button', { style: BTN, hover: 'color:var(--foreground)', title: 'read the brief and the list again', 'data-r22': 'refresh', onClick: () => { reload('today'); reload('brief'); reload('learn'); setState({}); } }, '↻ refresh'),
978
1025
  el('button', { style: BTN, hover: 'color:var(--foreground)', 'data-r22': 'missed', onClick: () => { tMissed = !tMissed; setState({}); } }, 'something was missed')));
979
1026
  if (tMissed) {
980
1027
  const ta = el('input', { type: 'text', placeholder: 'what should it have shown you?', 'data-r22': 'missed-input', style: 'flex:1;min-width:0;font-size:11.5px;padding:4px 8px;border:1px solid var(--border);background:var(--background);color:var(--foreground);min-height:28px' });
@@ -993,6 +1040,7 @@ export function todayList() {
993
1040
  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.')));
994
1041
  if (d.empty) kids.push(el('div', { style: NOTE, 'data-r22': 'empty' }, iv('Nothing is waiting on any repository that was read.')));
995
1042
  for (const c of d.candidates || []) { kids.push(tCard(c)); if (bgPlan && bgPlan.cid === c.id) kids.push(bgPlanView()); }
1043
+ for (const n of d.narrowed || []) kids.push(el('div', { style: NOTE, 'data-r26': 'narrowed-note' }, iv(n.hidden + ' ' + n.label.toLowerCase() + ' hidden: you rated them useful only ' + Math.round(n.rate * 100) + '% of the time. Restore them under learning below.')));
996
1044
  kids.push(...backgroundSection());
997
1045
  if (d.dismissed) kids.push(el('div', { style: NOTE, 'data-r22': 'dismissed' }, iv(d.dismissed + ' dismissed item(s) hidden for 7 days.')));
998
1046
  // settings
@@ -1007,8 +1055,8 @@ export function todayList() {
1007
1055
  kids.push(chk('typed scoring (sends titles and metadata to the Typesafe service)', s.briefScoring, 'set-scoring', (v) => ({ briefScoring: v })));
1008
1056
  const nm = el('input', { type: 'text', maxlength: '40', placeholder: 'your name for the greeting', value: s.name || '', 'data-r22': 'set-name', style: 'width:180px;font-size:11.5px;padding:2px 6px;border:1px solid var(--border);background:var(--background);color:var(--foreground)' });
1009
1057
  kids.push(el('div', { style: 'display:flex;gap:6px;align-items:center;padding:4px 14px;font-size:12px' }, nm, el('button', { style: BTN, 'data-r22': 'set-name-save', onClick: () => tSettings({ name: nm.value }) }, 'save')));
1010
- kids.push(el('div', { style: LABEL }, iv('learning')));
1011
- kids.push(chk('pause learning (nothing learns yet)', s.pauseLearning, 'set-pause', (v) => ({ pauseLearning: v })));
1058
+ kids.push(...learningSection());
1059
+ kids.push(chk('pause learning (no weights, no lessons, nothing narrowed)', s.pauseLearning, 'set-pause', (v) => ({ pauseLearning: v })));
1012
1060
  const days = el('input', { type: 'number', min: '1', max: '90', value: String(s.stalledDays), 'data-r22': 'set-stalled-days', style: 'width:60px;font-size:11.5px;padding:2px 6px;border:1px solid var(--border);background:var(--background);color:var(--foreground)' });
1013
1061
  const qs = el('input', { type: 'text', placeholder: '22:00', value: s.quiet ? s.quiet.start : '', 'data-r22': 'set-quiet-start', style: 'width:56px;font-size:11.5px;padding:2px 6px;border:1px solid var(--border);background:var(--background);color:var(--foreground)' });
1014
1062
  const qe = el('input', { type: 'text', placeholder: '07:00', value: s.quiet ? s.quiet.end : '', 'data-r22': 'set-quiet-end', style: 'width:56px;font-size:11.5px;padding:2px 6px;border:1px solid var(--border);background:var(--background);color:var(--foreground)' });
@@ -79,6 +79,7 @@ const nextStepActions = require('./lib/next-step-actions');
79
79
  const sessionBudget = require('./lib/session-budget');
80
80
  const judgmentLib = require('./lib/judgment');
81
81
  const proactiveLib = require('./lib/proactive');
82
+ const learnLib = require('./lib/learn');
82
83
  const briefLib = require('./lib/brief');
83
84
  const prepareQueue = require('./lib/prepare-queue');
84
85
  const harness = require('./lib/harness-parse');
@@ -3450,7 +3451,7 @@ function proactiveCtx(now, settings) {
3450
3451
  for (const b of pf.rows.filter((x) => x.ws === r.ws)) {
3451
3452
  if (!b || !b.id) continue;
3452
3453
  const lc = life.get(b.id); const stopped = !!(lc && lc.state !== 'active');
3453
- entry.initiatives.push({ id: b.id, title: b.title || b.id, active: !b.accepted && !stopped, lastRun: lastRun.has(b.id) ? lastRun.get(b.id) : null, hasRuns: hasRuns.has(b.id) });
3454
+ entry.initiatives.push({ id: b.id, title: b.title || b.id, active: !b.accepted && !stopped, lane: b.lane || null, lastRun: lastRun.has(b.id) ? lastRun.get(b.id) : null, hasRuns: hasRuns.has(b.id) });
3454
3455
  }
3455
3456
  entry.needs = (readNeeds(w) || []).filter((n) => n).map((n) => ({ initiative_id: n.initiative_id || null, question: n.question || '', run_id: n.run_id || null }));
3456
3457
  for (const [id, a] of acc) if (a && a.action === 'return') entry.returned.push({ initiative_id: id, note: 'returned at the Acceptance Gate', at: a.at });
@@ -3470,7 +3471,12 @@ function proactiveToday(now) {
3470
3471
  const d = proactiveLib.detect(ctx);
3471
3472
  const dismissed = proactiveLib.dismissedIds(nsRead(PRO_FEEDBACK()), now);
3472
3473
  const live = d.candidates.filter((c) => !dismissed.has(c.id));
3473
- return { settings, unreadable: d.unreadable, dismissed: d.candidates.length - live.length, candidates: settings.enabled ? proactiveLib.rank(live, now) : [], off: !settings.enabled };
3474
+ // D4: the PM's own verdicts move a few bounded weights, and a trigger rated badly enough is left off and named. Paused, both are off.
3475
+ const L = learnState(now, settings);
3476
+ const shown = live.filter((c) => !L.narrowed.some((n) => n.kind === c.kind));
3477
+ const bucket = learnLib.bucketOf(now);
3478
+ const ranked = settings.enabled ? proactiveLib.rank(shown, now, (c) => learnLib.adjust(c, L.weights, bucket, settings.pauseLearning)) : [];
3479
+ return { settings, unreadable: d.unreadable, dismissed: d.candidates.length - live.length, narrowed: L.narrowed.map((n) => ({ ...n, hidden: live.filter((c) => c.kind === n.kind).length })), candidates: ranked, off: !settings.enabled };
3474
3480
  }
3475
3481
  function proactiveScan({ manual }) {
3476
3482
  const now = Date.now();
@@ -3483,7 +3489,7 @@ function proactiveScan({ manual }) {
3483
3489
  const fresh = proactiveLib.newSignals(d.candidates, recent, now);
3484
3490
  let wrote = 0;
3485
3491
  for (const c of fresh) {
3486
- const w = nsAppend(PRO_SIGNALS(), { at: new Date(now).toISOString(), key: c.key, id: c.id, kind: c.kind, initiative_id: c.initiative_id, ws: c.ws, why: c.why, evidence: c.evidence, confidence: c.confidence, manual: !!manual }, 'proactive signal');
3492
+ const w = nsAppend(PRO_SIGNALS(), { at: new Date(now).toISOString(), key: c.key, id: c.id, kind: c.kind, lane: c.lane || null, initiative_id: c.initiative_id, ws: c.ws, why: c.why, evidence: c.evidence, confidence: c.confidence, manual: !!manual }, 'proactive signal');
3487
3493
  if (w.ok) wrote++;
3488
3494
  }
3489
3495
  return { ok: true, skipped: null, appended: wrote, found: d.candidates.length, unreadable: d.unreadable };
@@ -3493,6 +3499,58 @@ function proactiveTick() {
3493
3499
  if (now - proLastScan < PRO_SCAN_EVERY_MS) return;
3494
3500
  proLastScan = now;
3495
3501
  try { proactiveScan({ manual: false }); } catch (e) { warnWriteFail('proactive scan', e); }
3502
+ try { learnWeekly({ manual: false }); } catch (e) { warnWriteFail('proactive weekly review', e); }
3503
+ }
3504
+
3505
+ // ── Learning, slice D4 (initiative proactive-engine) ─────────────────────────────────────────────────────────────
3506
+ // The PM's verdicts and review decisions are evidence. lib/learn.js turns them into bounded weights, a weekly rollup,
3507
+ // lessons the PM must approve, and a list of triggers left off Today. Everything is computed from ledgers in the state
3508
+ // folder: no model call, no network call, no write into a repository, no session started. Pause learning gives zeros.
3509
+ const PRO_LEARN_EVENTS = () => path.join(STATE, 'learn-events.jsonl');
3510
+ const PRO_LESSONS = () => path.join(STATE, 'lessons.jsonl');
3511
+ const PRO_ROLLUPS = () => path.join(STATE, 'proactive-rollups.jsonl');
3512
+ function learnEvidence() {
3513
+ const ev = nsRead(PRO_LEARN_EVENTS());
3514
+ const resetAt = Math.max(0, ...ev.filter((e) => e && e.event === 'reset').map((e) => Date.parse(e.at)).filter(Number.isFinite));
3515
+ const restores = ev.filter((e) => e && e.event === 'restore' && e.kind).map((e) => ({ kind: e.kind, at: Date.parse(e.at) }));
3516
+ const runs = nsRead(NS_RUNS());
3517
+ const got = learnLib.evidence({ feedback: nsRead(PRO_FEEDBACK()), signals: nsRead(PRO_SIGNALS()).slice(-5000), reviews: nsRead(NS_REVIEWS()), runs, queue: queueEntries(), resetAt });
3518
+ const preparedAt = runs.map((r) => Date.parse(r && r.at)).filter((t) => Number.isFinite(t) && t > resetAt);
3519
+ return { ...got, restores, preparedAt, resetAt };
3520
+ }
3521
+ function learnState(now, settings) {
3522
+ const st = settings || proSettings();
3523
+ const e = learnEvidence();
3524
+ const paused = st.pauseLearning === true;
3525
+ const lessons = learnLib.foldLessons(nsRead(PRO_LESSONS()));
3526
+ const snaps = nsRead(PRO_ROLLUPS());
3527
+ return {
3528
+ paused, resetAt: e.resetAt ? new Date(e.resetAt).toISOString() : null, evidenceCount: e.rows.length, rows: e.rows, missed: e.missed, preparedAt: e.preparedAt,
3529
+ weights: paused ? { kind: {}, lane: {}, time: {} } : learnLib.weights(e.rows),
3530
+ narrowed: paused ? [] : learnLib.narrowing(e.rows, e.restores, now),
3531
+ rollup: learnLib.rollup(e.rows, e.missed, e.preparedAt, { now }),
3532
+ lessons, snapshots: snaps.slice(-8).reverse(),
3533
+ };
3534
+ }
3535
+ // The weekly run: one snapshot of the trailing 7 days and the lessons it proposes. It never catches up after a gap.
3536
+ function learnWeekly({ manual }) {
3537
+ const now = Date.now();
3538
+ const st = proSettings();
3539
+ if (st.pauseLearning) return { ok: false, error: 'learning is paused — switch pause learning off to run the weekly review' };
3540
+ if (!manual && !st.enabled) return { ok: true, ran: false, skipped: 'the engine is off' };
3541
+ const L = learnState(now, st);
3542
+ if (!manual && !learnLib.snapshotDue(L.snapshots, { now, hasEvidence: L.evidenceCount > 0 })) return { ok: true, ran: false, skipped: 'not due' };
3543
+ const at = new Date(now).toISOString();
3544
+ const props = learnLib.propose(L.rows, L.missed, now, L.lessons);
3545
+ const written = [];
3546
+ for (const p of props) {
3547
+ const rec = { event: 'proposed', id: learnLib.lessonId(p.key, at), key: p.key, text: p.text, evidence: p.evidence, at };
3548
+ if (nsAppend(PRO_LESSONS(), rec, 'lesson').ok) written.push(rec);
3549
+ }
3550
+ const snap = { at, manual: !!manual, window_days: L.rollup.days, rollup: L.rollup, proposed: written.map((x) => x.id), narrowed: L.narrowed.map((n) => n.kind) };
3551
+ const w = nsAppend(PRO_ROLLUPS(), snap, 'weekly rollup');
3552
+ if (!w.ok) return { ok: false, error: w.error };
3553
+ return { ok: true, ran: true, snapshot: snap, proposed: written };
3496
3554
  }
3497
3555
 
3498
3556
  // ── Worktree housekeeping: the app that creates the folders shows and clears them ──────────────
@@ -6376,6 +6434,49 @@ async function handleRequest(req, res) {
6376
6434
  return json(res, { ok: true, settings: n.settings });
6377
6435
  }
6378
6436
  }
6437
+ /* ── learning (D4): the PM's verdicts become bounded weights, a rollup and lessons that wait for the PM's press ── */
6438
+ if (p === '/api/proactive/learn' && req.method === 'GET') {
6439
+ const now = Date.now(); const st = proSettings(); const L = learnState(now, st);
6440
+ const { rows, missed, preparedAt, weights: _w, ...pub } = L;
6441
+ const cells = [];
6442
+ for (const dim of ['kind', 'lane', 'time']) for (const [k, c] of Object.entries(L.weights[dim])) cells.push({ dim, key: k, ...c });
6443
+ cells.sort((a, b) => Math.abs(b.delta) - Math.abs(a.delta) || b.n - a.n);
6444
+ return json(res, { ok: true, as_of: new Date(now).toISOString(), ...pub, cells, caps: learnLib.CAP, min_cell: learnLib.MIN_CELL,
6445
+ approved: L.lessons.filter((x) => x.status === 'approved'), max_approved: learnLib.MAX_APPROVED,
6446
+ note: 'Computed from your own feedback and review decisions in the workspace state folder. No model is called. A lesson changes nothing until you approve it, and then only the morning-brief session reads it.' });
6447
+ }
6448
+ if (p === '/api/proactive/learn/run' && req.method === 'POST') {
6449
+ const r = learnWeekly({ manual: true });
6450
+ return json(res, r, r.ok ? 200 : (/paused/.test(r.error || '') ? 409 : 500));
6451
+ }
6452
+ if (p === '/api/proactive/learn/lesson' && req.method === 'POST') {
6453
+ let body = {}; try { body = JSON.parse((await readBody(req)) || '{}'); } catch { return json(res, { ok: false, error: 'invalid JSON body' }, 400); }
6454
+ const c = learnLib.checkLessonRequest(body);
6455
+ if (!c.ok) return json(res, { ok: false, error: c.error }, 400);
6456
+ const lessons = learnLib.foldLessons(nsRead(PRO_LESSONS()));
6457
+ const why = learnLib.decideLesson(lessons, c.req);
6458
+ if (why) return json(res, { ok: false, error: why }, why === 'no such lesson' ? 404 : 409);
6459
+ const ev = { approve: 'approved', reject: 'rejected', retire: 'retired' }[c.req.decision];
6460
+ const w = nsAppend(PRO_LESSONS(), { event: ev, id: c.req.id, at: nowISO(), pm: 'wawan' }, 'lesson');
6461
+ if (!w.ok) return json(res, { ok: false, error: w.error }, 500);
6462
+ return json(res, { ok: true, id: c.req.id, status: ev });
6463
+ }
6464
+ if (p === '/api/proactive/learn/restore' && req.method === 'POST') {
6465
+ let body = {}; try { body = JSON.parse((await readBody(req)) || '{}'); } catch { return json(res, { ok: false, error: 'invalid JSON body' }, 400); }
6466
+ if (body.confirm !== true) return json(res, { ok: false, error: 'confirm is required' }, 400);
6467
+ const L = learnState(Date.now());
6468
+ if (!L.narrowed.some((n) => n.kind === body.kind)) return json(res, { ok: false, error: `${String(body.kind || '').slice(0, 20) || 'that trigger'} is not narrowed — nothing to restore` }, 409);
6469
+ const w = nsAppend(PRO_LEARN_EVENTS(), { event: 'restore', kind: body.kind, at: nowISO(), pm: 'wawan' }, 'learn event');
6470
+ if (!w.ok) return json(res, { ok: false, error: w.error }, 500);
6471
+ return json(res, { ok: true, kind: body.kind });
6472
+ }
6473
+ if (p === '/api/proactive/learn/reset' && req.method === 'POST') {
6474
+ let body = {}; try { body = JSON.parse((await readBody(req)) || '{}'); } catch { return json(res, { ok: false, error: 'invalid JSON body' }, 400); }
6475
+ if (body.confirm !== true) return json(res, { ok: false, error: 'confirm is required: this makes the engine ignore all earlier feedback' }, 400);
6476
+ const w = nsAppend(PRO_LEARN_EVENTS(), { event: 'reset', at: nowISO(), pm: 'wawan' }, 'learn event');
6477
+ if (!w.ok) return json(res, { ok: false, error: w.error }, 500);
6478
+ return json(res, { ok: true });
6479
+ }
6379
6480
  if (p === '/api/lifecycle' && req.method === 'POST') {
6380
6481
  // D70 — close, hold, cancel or reopen an initiative. Only reached by an explicit PM press. It
6381
6482
  // appends ONE line to lifecycle.jsonl and never writes an acceptance.
@@ -23,6 +23,7 @@ Rules that always hold:
23
23
 
24
24
  Procedure:
25
25
  1. Run `pmos proactive settings` and `pmos proactive today`. The second lists what the workspace derived from the ledgers. Each of its items may become a priority with source "derived".
26
+ Then run `pmos proactive learn lessons`. Each line is a preference the PM approved, written from their own feedback. Use it only to decide how confident to be and where an item goes. A lesson never asks you to read another source, to switch a setting on, or to write anything.
26
27
  2. Read today's events from the calendar and today's and tomorrow's meeting notes from Wispr Flow, if those sources are on and reachable. Read Jira issues assigned to the PM or due soon, and Confluence pages changed since yesterday, if on and reachable. Read mail only if on: subjects and senders of threads waiting on the PM, no bodies.
27
28
  3. For each thing the PM should act on today, write one item: a short title, why it is for today, its source, the initiative id if one applies, who acts (pm or agent), effort (minutes, an-hour, half-day, day), your confidence from 0 to 1 that it belongs on today's list, and one short evidence line. Give a low confidence when you are guessing. The workspace shows an item under 0.5 as "worth asking", not as a priority.
28
29
  4. Write the brief as a JSON file in the scratch folder of this session, in this shape, and nothing else: