create-pmos 0.45.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 +24 -2
- 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/lib/mode.mjs +45 -0
- package/payload/mods/pmos-cockpit/hooks/lib/review.mjs +117 -0
- package/payload/mods/pmos-cockpit/hooks/lib/unpushed.mjs +53 -0
- package/payload/mods/pmos-cockpit/hooks/register.tsx +283 -3
- package/payload/mods/pmos-cockpit/types/index.d.ts +17 -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/lib/session-flag.js +35 -0
- 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 +108 -5
- package/workspace/skills/morning-brief.skill +1 -0
|
@@ -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
|
@@ -63,6 +63,7 @@ ensureSpawnHelperExecutable();
|
|
|
63
63
|
const { checkSurface } = require('./lib/surface-contract');
|
|
64
64
|
const { createSinkGuard } = require('./lib/sink.js');
|
|
65
65
|
const tsSettings = require('./lib/typesafe-settings.js');
|
|
66
|
+
const sessionFlag = require('./lib/session-flag'); // PMOS_WORKSPACE=1 in each agent terminal (cockpit mod, M12)
|
|
66
67
|
const tsClient = require('./lib/typesafe.js');
|
|
67
68
|
const jevClaims = require('./lib/jev-claims.js'); // D80 — the bridge between the claims ledger and the one TypeSafe module
|
|
68
69
|
const validateLib = require('./lib/validate.js');
|
|
@@ -78,6 +79,7 @@ const nextStepActions = require('./lib/next-step-actions');
|
|
|
78
79
|
const sessionBudget = require('./lib/session-budget');
|
|
79
80
|
const judgmentLib = require('./lib/judgment');
|
|
80
81
|
const proactiveLib = require('./lib/proactive');
|
|
82
|
+
const learnLib = require('./lib/learn');
|
|
81
83
|
const briefLib = require('./lib/brief');
|
|
82
84
|
const prepareQueue = require('./lib/prepare-queue');
|
|
83
85
|
const harness = require('./lib/harness-parse');
|
|
@@ -326,7 +328,8 @@ function writePrefs(prefs) {
|
|
|
326
328
|
function sessionEnvWithProfile(profile) {
|
|
327
329
|
const env = sessionEnv();
|
|
328
330
|
for (const [k, v] of Object.entries(profileOverlay(profile))) env[k] = v;
|
|
329
|
-
|
|
331
|
+
// the workspace's own flag goes on LAST, so a profile cannot unset it (lib/session-flag.js)
|
|
332
|
+
return sessionFlag.withFlag(env);
|
|
330
333
|
}
|
|
331
334
|
// Just the DELTA a profile introduces, stringified — never merged with the base. The tmux path
|
|
332
335
|
// needs these separately from sessionEnv() because a pane's environment does not come from the
|
|
@@ -3017,7 +3020,7 @@ function spawnSession({ name, agent, cwd, ws, origin, initialInput, profile, ini
|
|
|
3017
3020
|
// the OVERLAY, not the merged env — the merged env is for the direct-PTY path below, which
|
|
3018
3021
|
// really does spawn the process itself. See createTmuxSession for why the two differ.
|
|
3019
3022
|
// The launch args are plain tokens by construction (pmos-model's strict pattern), so joining them is safe.
|
|
3020
|
-
if (!createTmuxSession(tname, launchArgs.length ? [cmd, ...launchArgs].join(' ') : cmd, workdir, 80, 24, profileOverlay(pres.profile))) {
|
|
3023
|
+
if (!createTmuxSession(tname, launchArgs.length ? [cmd, ...launchArgs].join(' ') : cmd, workdir, 80, 24, sessionFlag.tmuxOverlay(profileOverlay(pres.profile)))) {
|
|
3021
3024
|
const why = `could not start a tmux session for '${cmd}'. Run \`npm run doctor\` in workspace/.`;
|
|
3022
3025
|
s.state = 'exited'; s.tmux = null; s.note = why;
|
|
3023
3026
|
s.buf = `\x1b[38;5;203m✗ ${why}\x1b[0m\r\n\x1b[38;5;244mFix the above, then ↻ respawn.\x1b[0m\r\n`;
|
|
@@ -3448,7 +3451,7 @@ function proactiveCtx(now, settings) {
|
|
|
3448
3451
|
for (const b of pf.rows.filter((x) => x.ws === r.ws)) {
|
|
3449
3452
|
if (!b || !b.id) continue;
|
|
3450
3453
|
const lc = life.get(b.id); const stopped = !!(lc && lc.state !== 'active');
|
|
3451
|
-
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) });
|
|
3452
3455
|
}
|
|
3453
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 }));
|
|
3454
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 });
|
|
@@ -3468,7 +3471,12 @@ function proactiveToday(now) {
|
|
|
3468
3471
|
const d = proactiveLib.detect(ctx);
|
|
3469
3472
|
const dismissed = proactiveLib.dismissedIds(nsRead(PRO_FEEDBACK()), now);
|
|
3470
3473
|
const live = d.candidates.filter((c) => !dismissed.has(c.id));
|
|
3471
|
-
|
|
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 };
|
|
3472
3480
|
}
|
|
3473
3481
|
function proactiveScan({ manual }) {
|
|
3474
3482
|
const now = Date.now();
|
|
@@ -3481,7 +3489,7 @@ function proactiveScan({ manual }) {
|
|
|
3481
3489
|
const fresh = proactiveLib.newSignals(d.candidates, recent, now);
|
|
3482
3490
|
let wrote = 0;
|
|
3483
3491
|
for (const c of fresh) {
|
|
3484
|
-
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');
|
|
3485
3493
|
if (w.ok) wrote++;
|
|
3486
3494
|
}
|
|
3487
3495
|
return { ok: true, skipped: null, appended: wrote, found: d.candidates.length, unreadable: d.unreadable };
|
|
@@ -3491,6 +3499,58 @@ function proactiveTick() {
|
|
|
3491
3499
|
if (now - proLastScan < PRO_SCAN_EVERY_MS) return;
|
|
3492
3500
|
proLastScan = now;
|
|
3493
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 };
|
|
3494
3554
|
}
|
|
3495
3555
|
|
|
3496
3556
|
// ── Worktree housekeeping: the app that creates the folders shows and clears them ──────────────
|
|
@@ -6374,6 +6434,49 @@ async function handleRequest(req, res) {
|
|
|
6374
6434
|
return json(res, { ok: true, settings: n.settings });
|
|
6375
6435
|
}
|
|
6376
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
|
+
}
|
|
6377
6480
|
if (p === '/api/lifecycle' && req.method === 'POST') {
|
|
6378
6481
|
// D70 — close, hold, cancel or reopen an initiative. Only reached by an explicit PM press. It
|
|
6379
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:
|