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.
- package/package.json +1 -1
- package/payload/KIT-VERSION +1 -1
- package/payload/mods/pmos-cockpit/.claude-plugin/plugin.json +1 -1
- package/payload/mods/pmos-cockpit/README.md +9 -1
- package/payload/mods/pmos-cockpit/hooks/lib/handover-run.mjs +33 -0
- package/payload/mods/pmos-cockpit/hooks/lib/handover.mjs +267 -0
- package/payload/mods/pmos-cockpit/hooks/register.tsx +152 -0
- package/payload/mods/pmos-cockpit/types/index.d.ts +16 -1
- package/payload/okf/core/concepts/frontmatter-format.md +6 -1
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/skills/SKILL-FORMAT.md +1 -0
- package/payload/state/discoveries.md +1 -1
- package/workspace/README.md +1 -1
- package/workspace/guide/initiatives-and-next-steps.md +1 -0
- package/workspace/guide/knowledge-graph.md +6 -1
- package/workspace/guide/layout.md +1 -0
- package/workspace/guide/proactive-engine.md +21 -3
- package/workspace/guide/scheduled-tasks-and-cli.md +1 -0
- package/workspace/guide/statuses.md +1 -0
- package/workspace/guide/validate-with-typesafe.md +1 -0
- package/workspace/lib/kg-cli.js +2 -1
- package/workspace/lib/kg-dangling-baseline.json +1 -0
- package/workspace/lib/kg-dangling-rules.json +1 -0
- package/workspace/lib/kg.js +20 -8
- package/workspace/lib/learn.js +280 -0
- package/workspace/lib/proactive-cli.js +49 -1
- package/workspace/lib/proactive.js +8 -7
- package/workspace/package-lock.json +1 -1
- package/workspace/package.json +3 -3
- package/workspace/public/pm/modes-live.js +52 -4
- package/workspace/server.js +104 -3
- 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
|
-
|
|
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) })
|
|
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
|
|
package/workspace/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmos-workspace",
|
|
3
|
-
"version": "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(
|
|
1011
|
-
kids.push(chk('pause learning (nothing
|
|
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)' });
|
package/workspace/server.js
CHANGED
|
@@ -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
|
-
|
|
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:
|