create-pmos 0.46.0 → 0.48.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/AGENTS-HUMAN.md +1 -1
- package/payload/AGENTS.md +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 +13 -3
- package/payload/mods/pmos-cockpit/hooks/lib/commands.mjs +34 -6
- package/payload/mods/pmos-cockpit/hooks/lib/grants.mjs +22 -3
- package/payload/mods/pmos-cockpit/hooks/lib/guard.mjs +16 -3
- 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 +189 -18
- 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/scripts/pmos-log.sh +18 -8
- package/payload/scripts/pmos-model.js +233 -2
- package/payload/scripts/status-facts.js +204 -0
- package/payload/scripts/workflow-state.js +52 -0
- package/payload/skills/SKILL-FORMAT.md +1 -0
- package/payload/state/discoveries.md +1 -1
- package/workspace/README.md +3 -3
- package/workspace/bin/live-notes.js +133 -0
- package/workspace/design-parity.js +328 -21
- package/workspace/doc-category-bench.js +165 -0
- package/workspace/fixtures/doc-category-labelled.json +167 -0
- package/workspace/guide/initiatives-and-next-steps.md +16 -2
- package/workspace/guide/knowledge-graph.md +89 -2
- package/workspace/guide/layout.md +30 -2
- package/workspace/guide/proactive-engine.md +26 -4
- package/workspace/guide/scheduled-tasks-and-cli.md +1 -0
- package/workspace/guide/statuses.md +1 -0
- package/workspace/guide/validate-with-typesafe.md +17 -1
- package/workspace/lib/claims.js +51 -2
- package/workspace/lib/doc-category-corrections.js +113 -0
- package/workspace/lib/doc-category-writeback.js +110 -0
- package/workspace/lib/doc-category.js +225 -0
- package/workspace/lib/engine/pmos-model.js +233 -2
- package/workspace/lib/engine/status-facts.js +204 -0
- package/workspace/lib/engine/workflow-state.js +52 -0
- package/workspace/lib/jev-categorize.js +399 -0
- package/workspace/lib/jev-claims.js +59 -15
- package/workspace/lib/jev-route.js +129 -0
- package/workspace/lib/kg-cli.js +5 -2
- package/workspace/lib/kg-dangling-baseline.json +1 -0
- package/workspace/lib/kg-dangling-rules.json +1 -0
- package/workspace/lib/kg.js +26 -9
- package/workspace/lib/learn.js +282 -0
- package/workspace/lib/live-notes.js +890 -0
- package/workspace/lib/proactive-cli.js +49 -1
- package/workspace/lib/proactive.js +8 -7
- package/workspace/lib/typesafe-settings.js +10 -2
- package/workspace/package-lock.json +1 -1
- package/workspace/package.json +5 -4
- package/workspace/public/pm/app.js +148 -79
- package/workspace/public/pm/data.js +58 -0
- package/workspace/public/pm/fixture.js +40 -2
- package/workspace/public/pm/modes-live.js +267 -21
- package/workspace/public/pm/panel-cockpit.js +636 -0
- package/workspace/public/pm/panel-detail.js +210 -8
- package/workspace/public/pm/panel-initiatives.js +9 -9
- package/workspace/public/pm/panel-newsess.js +145 -86
- package/workspace/public/pm/panel-repos.js +34 -17
- package/workspace/public/pm/panel-tabs.js +145 -27
- package/workspace/public/pm/state.js +45 -4
- package/workspace/public/pm/tab-doc.js +244 -16
- package/workspace/public/pm/tab-term.js +11 -1
- package/workspace/public/pm/tokens.css +4 -3
- package/workspace/public/pm/view-addrepo.js +207 -0
- package/workspace/public/pm/view-files.js +197 -0
- package/workspace/public/pm/view-guide.js +141 -0
- package/workspace/public/pm/view-knowledge.js +225 -0
- package/workspace/public/pm/view-memory.js +122 -0
- package/workspace/public/pm/view-portfolio.js +153 -0
- package/workspace/public/pm/view-reports.js +190 -0
- package/workspace/public/pm/view-scheduled.js +161 -0
- package/workspace/public/pm/view-skills.js +216 -0
- package/workspace/public/pm/view-today.js +168 -0
- package/workspace/public/pm/view-validate.js +171 -0
- package/workspace/server.js +394 -10
- package/workspace/skills/live-notes.skill +58 -0
- package/workspace/skills/morning-brief.skill +1 -0
package/workspace/server.js
CHANGED
|
@@ -20,6 +20,12 @@
|
|
|
20
20
|
* under state/ (the reconcile seam). Request headers are never logged (D08).
|
|
21
21
|
*/
|
|
22
22
|
'use strict';
|
|
23
|
+
// The workspace reads git in the background (repo-watch, git-info, claims), and a plain `git status` takes the index lock to
|
|
24
|
+
// refresh the index. A press that writes, such as Sync's `git add`, then fails on `index.lock: File exists` when the two
|
|
25
|
+
// meet. With optional locks off, the server's reads never take that lock. Set only when the PM has not set it, and taken back
|
|
26
|
+
// out of every terminal session's environment (sessionEnv), so the PM's own shells and agents are unchanged.
|
|
27
|
+
const SET_GIT_OPTIONAL_LOCKS = process.env.GIT_OPTIONAL_LOCKS === undefined;
|
|
28
|
+
if (SET_GIT_OPTIONAL_LOCKS) process.env.GIT_OPTIONAL_LOCKS = '0';
|
|
23
29
|
const http = require('http');
|
|
24
30
|
const fs = require('fs');
|
|
25
31
|
const os = require('os');
|
|
@@ -65,9 +71,15 @@ const { createSinkGuard } = require('./lib/sink.js');
|
|
|
65
71
|
const tsSettings = require('./lib/typesafe-settings.js');
|
|
66
72
|
const sessionFlag = require('./lib/session-flag'); // PMOS_WORKSPACE=1 in each agent terminal (cockpit mod, M12)
|
|
67
73
|
const tsClient = require('./lib/typesafe.js');
|
|
74
|
+
const jevRoute = require('./lib/jev-route.js'); // prompt-model-routing — the bridge between prompt routing and the one TypeSafe module
|
|
68
75
|
const jevClaims = require('./lib/jev-claims.js'); // D80 — the bridge between the claims ledger and the one TypeSafe module
|
|
76
|
+
const jevCategorize = require('./lib/jev-categorize.js'); // D82 — the bridge between the document categorizer and the one TypeSafe module
|
|
69
77
|
const validateLib = require('./lib/validate.js');
|
|
78
|
+
const liveNotes = require('./lib/live-notes.js'); // jev-live-notes — reads and appends a repository's live-notes ledger; no network
|
|
70
79
|
const claimsLib = require('./lib/claims.js');
|
|
80
|
+
const docCategory = require('./lib/doc-category.js'); // jev-doc-categorizer Run 1 — the PM-facing kind of a document, derived, local, no model
|
|
81
|
+
const docCatCorrections = require('./lib/doc-category-corrections.js'); // jev-doc-categorizer Run 3 — the PM's own category, per user, outside the repository
|
|
82
|
+
const docCatWriteback = require('./lib/doc-category-writeback.js'); // …and the separate press that writes it into the file's frontmatter
|
|
71
83
|
const repoWatch = require('./lib/repo-watch.js');
|
|
72
84
|
tsSettings.adoptEnv(process.env); // D79 — take the env key into memory and delete it, before anything is spawned
|
|
73
85
|
const scheduleLogic = require('./lib/schedule-logic');
|
|
@@ -79,6 +91,7 @@ const nextStepActions = require('./lib/next-step-actions');
|
|
|
79
91
|
const sessionBudget = require('./lib/session-budget');
|
|
80
92
|
const judgmentLib = require('./lib/judgment');
|
|
81
93
|
const proactiveLib = require('./lib/proactive');
|
|
94
|
+
const learnLib = require('./lib/learn');
|
|
82
95
|
const briefLib = require('./lib/brief');
|
|
83
96
|
const prepareQueue = require('./lib/prepare-queue');
|
|
84
97
|
const harness = require('./lib/harness-parse');
|
|
@@ -277,6 +290,7 @@ function sessionEnv() {
|
|
|
277
290
|
// D79 — the TypeSafe key is the workspace server's own credential. A terminal runs an agent that can
|
|
278
291
|
// print its environment, so the key is never handed to one, whether it came from settings or the shell.
|
|
279
292
|
delete env[tsSettings.ENV_NAME];
|
|
293
|
+
if (SET_GIT_OPTIONAL_LOCKS) delete env.GIT_OPTIONAL_LOCKS;
|
|
280
294
|
return env;
|
|
281
295
|
}
|
|
282
296
|
|
|
@@ -1535,6 +1549,85 @@ function stampEdit(text, at) {
|
|
|
1535
1549
|
const next = fm.replace(re, `generated: {by: human:wawan, at: "${at}"}`);
|
|
1536
1550
|
return next === fm ? text : text.replace(fm, () => next);
|
|
1537
1551
|
}
|
|
1552
|
+
// ── Document category (jev-doc-categorizer Run 1) ───────────────────────────────────────────────────
|
|
1553
|
+
// A derived label beside the document's own `type`: local rules only, no model, nothing written. The library lists it with the
|
|
1554
|
+
// basis that decided it, so the PM can see why. A file is read once per modification; only its first 16 KB is needed, because
|
|
1555
|
+
// the evidence is the frontmatter, the name and the headings.
|
|
1556
|
+
const catCache = new Map();
|
|
1557
|
+
function categoryInput(full) {
|
|
1558
|
+
let st; try { st = fs.statSync(full); } catch { return null; }
|
|
1559
|
+
const hit = catCache.get(full);
|
|
1560
|
+
if (hit && hit.m === st.mtimeMs) return hit;
|
|
1561
|
+
let text = '';
|
|
1562
|
+
try { const fd = fs.openSync(full, 'r'); const buf = Buffer.alloc(16384); const n = fs.readSync(fd, buf, 0, 16384, 0); fs.closeSync(fd); text = buf.slice(0, n).toString('utf8'); } catch { return null; }
|
|
1563
|
+
let fm = {}; try { fm = relatedDocs.frontmatter(text) || {}; } catch { /* no frontmatter */ }
|
|
1564
|
+
const ent = { m: st.mtimeMs, fm, text };
|
|
1565
|
+
catCache.set(full, ent);
|
|
1566
|
+
return ent;
|
|
1567
|
+
}
|
|
1568
|
+
function namedRecords(root) {
|
|
1569
|
+
const records = [];
|
|
1570
|
+
for (const rel of ['planning/initiatives', 'pmos/state/initiatives', 'state/initiatives']) {
|
|
1571
|
+
const d = path.join(root, ...rel.split('/'));
|
|
1572
|
+
let names = []; try { names = fs.readdirSync(d).filter((f) => f.endsWith('.md')); } catch { continue; }
|
|
1573
|
+
for (const f of names) { const c = categoryInput(path.join(d, f)); if (c) records.push({ rel: rel + '/' + f, fm: c.fm }); }
|
|
1574
|
+
}
|
|
1575
|
+
return docCategory.namedFromRecords(records);
|
|
1576
|
+
}
|
|
1577
|
+
// Run 3 (C7) — the PM's corrections for one repository live in the per-user runtime directory, keyed like the claims cache
|
|
1578
|
+
// (sha1 of the root), never in the repository.
|
|
1579
|
+
function doccatDir(root) { return docCatCorrections.repoDir(STATE, root); }
|
|
1580
|
+
// Adds `category`, `category_basis` and `category_why` to every markdown document anywhere in a library answer, and
|
|
1581
|
+
// `category_kind` for a ticket. A stored correction is passed in, so it wins with basis `pm correction`; the bases that
|
|
1582
|
+
// disagree with the winner come as `category_disagrees`.
|
|
1583
|
+
function annotateCategories(root, node, named, corr = {}, seen = new Set()) {
|
|
1584
|
+
if (!node || typeof node !== 'object' || seen.has(node)) return node;
|
|
1585
|
+
seen.add(node);
|
|
1586
|
+
if (Array.isArray(node)) { for (const x of node) annotateCategories(root, x, named, corr, seen); return node; }
|
|
1587
|
+
if (typeof node.path === 'string' && /\.md$/i.test(node.path)) {
|
|
1588
|
+
const c = categoryInput(path.join(root, node.path));
|
|
1589
|
+
if (c) {
|
|
1590
|
+
const rel = node.path.split(path.sep).join('/');
|
|
1591
|
+
const r = docCategory.categorize({ rel, fm: c.fm, text: c.text, named, correction: Object.prototype.hasOwnProperty.call(corr, rel) ? corr[rel] : null });
|
|
1592
|
+
node.category = r.category; node.category_basis = r.basis; node.category_why = r.via;
|
|
1593
|
+
if (r.ticket_kind) node.category_kind = r.ticket_kind;
|
|
1594
|
+
if (r.disagrees.length) node.category_disagrees = r.disagrees;
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
for (const k of Object.keys(node)) if (node[k] && typeof node[k] === 'object') annotateCategories(root, node[k], named, corr, seen);
|
|
1598
|
+
return node;
|
|
1599
|
+
}
|
|
1600
|
+
// ── D82 — Jev's category for a file the local rules leave undetermined ─────────────────────────────────
|
|
1601
|
+
// Its state lives beside the corrections, in doccatDir(root): per user, outside the repository.
|
|
1602
|
+
// Reads Jev's CACHED answers only; sends nothing. A decided answer becomes the category with the basis `jev`; an unsure one
|
|
1603
|
+
// leaves the file `undetermined` and says which two options led. Advisory (D40): nothing downstream reads it as fact.
|
|
1604
|
+
function annotateJev(root, node, cache, seen = new Set()) {
|
|
1605
|
+
if (!node || typeof node !== 'object' || seen.has(node)) return node;
|
|
1606
|
+
seen.add(node);
|
|
1607
|
+
if (Array.isArray(node)) { for (const x of node) annotateJev(root, x, cache, seen); return node; }
|
|
1608
|
+
if (node.category === docCategory.UNDETERMINED && typeof node.path === 'string' && /\.md$/i.test(node.path)) {
|
|
1609
|
+
const c = categoryInput(path.join(root, node.path));
|
|
1610
|
+
const l = c ? jevCategorize.lookup(cache, { fm: c.fm, text: c.text }) : null;
|
|
1611
|
+
if (l && l.decided) { node.category = l.category; node.category_basis = 'jev'; node.category_why = l.sentence; if (l.ticket_kind) node.ticket_kind = l.ticket_kind; }
|
|
1612
|
+
else if (l) node.category_why = l.sentence;
|
|
1613
|
+
}
|
|
1614
|
+
for (const k of Object.keys(node)) if (node[k] && typeof node[k] === 'object') annotateJev(root, node[k], cache, seen);
|
|
1615
|
+
return node;
|
|
1616
|
+
}
|
|
1617
|
+
// Every Markdown document the library lists, with what the categorizer needs. Read through the same per-file cache.
|
|
1618
|
+
function doccatDocs(root, lib) {
|
|
1619
|
+
const rels = new Set();
|
|
1620
|
+
(function walk(n, seen) {
|
|
1621
|
+
if (!n || typeof n !== 'object' || seen.has(n)) return; seen.add(n);
|
|
1622
|
+
if (Array.isArray(n)) { for (const x of n) walk(x, seen); return; }
|
|
1623
|
+
if (typeof n.path === 'string' && /\.md$/i.test(n.path)) rels.add(n.path.split(path.sep).join('/'));
|
|
1624
|
+
for (const k of Object.keys(n)) if (n[k] && typeof n[k] === 'object') walk(n[k], seen);
|
|
1625
|
+
})(lib, new Set());
|
|
1626
|
+
const docs = [];
|
|
1627
|
+
const corrections = docCatCorrections.get(doccatDir(root)); // C7 — a corrected file is decided, so it is never sent
|
|
1628
|
+
for (const rel of rels) { const c = categoryInput(path.join(root, ...rel.split('/'))); if (c) docs.push({ rel, fm: c.fm, text: c.text, correction: corrections[rel] || null }); }
|
|
1629
|
+
return docs;
|
|
1630
|
+
}
|
|
1538
1631
|
function library(root) {
|
|
1539
1632
|
const shape = wsShape(root);
|
|
1540
1633
|
const roots = libraryTree(root);
|
|
@@ -1552,7 +1645,8 @@ function library(root) {
|
|
|
1552
1645
|
const notes = NOTE_DIRS.map((d) => path.join(root, ...d.split('/'))).filter((d) => isDir(d))
|
|
1553
1646
|
.map((d) => tree(root, d, {}, 0, { n: TREE_MAX_NODES }))
|
|
1554
1647
|
.filter((t) => t.dirs.length || t.docs.length);
|
|
1555
|
-
|
|
1648
|
+
const out = { shape, roots, initiatives, ungrouped, knowledge, notes, governance: looseDocs(root) };
|
|
1649
|
+
return annotateJev(root, annotateCategories(root, out, namedRecords(root), docCatCorrections.get(doccatDir(root))), jevCategorize.readCache(doccatDir(root)));
|
|
1556
1650
|
}
|
|
1557
1651
|
|
|
1558
1652
|
// ── On-demand surfaces (D60 / the workspace-ondemand-surfaces PRD). ────────────────────────────
|
|
@@ -3450,7 +3544,7 @@ function proactiveCtx(now, settings) {
|
|
|
3450
3544
|
for (const b of pf.rows.filter((x) => x.ws === r.ws)) {
|
|
3451
3545
|
if (!b || !b.id) continue;
|
|
3452
3546
|
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) });
|
|
3547
|
+
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
3548
|
}
|
|
3455
3549
|
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
3550
|
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 +3564,12 @@ function proactiveToday(now) {
|
|
|
3470
3564
|
const d = proactiveLib.detect(ctx);
|
|
3471
3565
|
const dismissed = proactiveLib.dismissedIds(nsRead(PRO_FEEDBACK()), now);
|
|
3472
3566
|
const live = d.candidates.filter((c) => !dismissed.has(c.id));
|
|
3473
|
-
|
|
3567
|
+
// 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.
|
|
3568
|
+
const L = learnState(now, settings);
|
|
3569
|
+
const shown = live.filter((c) => !L.narrowed.some((n) => n.kind === c.kind));
|
|
3570
|
+
const bucket = learnLib.bucketOf(now);
|
|
3571
|
+
const ranked = settings.enabled ? proactiveLib.rank(shown, now, (c) => learnLib.adjust(c, L.weights, bucket, settings.pauseLearning)) : [];
|
|
3572
|
+
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
3573
|
}
|
|
3475
3574
|
function proactiveScan({ manual }) {
|
|
3476
3575
|
const now = Date.now();
|
|
@@ -3483,7 +3582,7 @@ function proactiveScan({ manual }) {
|
|
|
3483
3582
|
const fresh = proactiveLib.newSignals(d.candidates, recent, now);
|
|
3484
3583
|
let wrote = 0;
|
|
3485
3584
|
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');
|
|
3585
|
+
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
3586
|
if (w.ok) wrote++;
|
|
3488
3587
|
}
|
|
3489
3588
|
return { ok: true, skipped: null, appended: wrote, found: d.candidates.length, unreadable: d.unreadable };
|
|
@@ -3493,6 +3592,58 @@ function proactiveTick() {
|
|
|
3493
3592
|
if (now - proLastScan < PRO_SCAN_EVERY_MS) return;
|
|
3494
3593
|
proLastScan = now;
|
|
3495
3594
|
try { proactiveScan({ manual: false }); } catch (e) { warnWriteFail('proactive scan', e); }
|
|
3595
|
+
try { learnWeekly({ manual: false }); } catch (e) { warnWriteFail('proactive weekly review', e); }
|
|
3596
|
+
}
|
|
3597
|
+
|
|
3598
|
+
// ── Learning, slice D4 (initiative proactive-engine) ─────────────────────────────────────────────────────────────
|
|
3599
|
+
// The PM's verdicts and review decisions are evidence. lib/learn.js turns them into bounded weights, a weekly rollup,
|
|
3600
|
+
// lessons the PM must approve, and a list of triggers left off Today. Everything is computed from ledgers in the state
|
|
3601
|
+
// folder: no model call, no network call, no write into a repository, no session started. Pause learning gives zeros.
|
|
3602
|
+
const PRO_LEARN_EVENTS = () => path.join(STATE, 'learn-events.jsonl');
|
|
3603
|
+
const PRO_LESSONS = () => path.join(STATE, 'lessons.jsonl');
|
|
3604
|
+
const PRO_ROLLUPS = () => path.join(STATE, 'proactive-rollups.jsonl');
|
|
3605
|
+
function learnEvidence() {
|
|
3606
|
+
const ev = nsRead(PRO_LEARN_EVENTS());
|
|
3607
|
+
const resetAt = Math.max(0, ...ev.filter((e) => e && e.event === 'reset').map((e) => Date.parse(e.at)).filter(Number.isFinite));
|
|
3608
|
+
const restores = ev.filter((e) => e && e.event === 'restore' && e.kind).map((e) => ({ kind: e.kind, at: Date.parse(e.at) }));
|
|
3609
|
+
const runs = nsRead(NS_RUNS());
|
|
3610
|
+
const got = learnLib.evidence({ feedback: nsRead(PRO_FEEDBACK()), signals: nsRead(PRO_SIGNALS()).slice(-5000), reviews: nsRead(NS_REVIEWS()), runs, queue: queueEntries(), resetAt });
|
|
3611
|
+
const preparedAt = runs.map((r) => Date.parse(r && r.at)).filter((t) => Number.isFinite(t) && t > resetAt);
|
|
3612
|
+
return { ...got, restores, preparedAt, resetAt };
|
|
3613
|
+
}
|
|
3614
|
+
function learnState(now, settings) {
|
|
3615
|
+
const st = settings || proSettings();
|
|
3616
|
+
const e = learnEvidence();
|
|
3617
|
+
const paused = st.pauseLearning === true;
|
|
3618
|
+
const lessons = learnLib.foldLessons(nsRead(PRO_LESSONS()));
|
|
3619
|
+
const snaps = nsRead(PRO_ROLLUPS());
|
|
3620
|
+
return {
|
|
3621
|
+
paused, resetAt: e.resetAt ? new Date(e.resetAt).toISOString() : null, evidenceCount: e.rows.length, rows: e.rows, missed: e.missed, preparedAt: e.preparedAt,
|
|
3622
|
+
weights: paused ? { kind: {}, lane: {}, time: {} } : learnLib.weights(e.rows),
|
|
3623
|
+
narrowed: paused ? [] : learnLib.narrowing(e.rows, e.restores, now),
|
|
3624
|
+
rollup: learnLib.rollup(e.rows, e.missed, e.preparedAt, { now }),
|
|
3625
|
+
lessons, snapshots: snaps.slice(-8).reverse(),
|
|
3626
|
+
};
|
|
3627
|
+
}
|
|
3628
|
+
// The weekly run: one snapshot of the trailing 7 days and the lessons it proposes. It never catches up after a gap.
|
|
3629
|
+
function learnWeekly({ manual }) {
|
|
3630
|
+
const now = Date.now();
|
|
3631
|
+
const st = proSettings();
|
|
3632
|
+
if (st.pauseLearning) return { ok: false, error: 'learning is paused — switch pause learning off to run the weekly review' };
|
|
3633
|
+
if (!manual && !st.enabled) return { ok: true, ran: false, skipped: 'the engine is off' };
|
|
3634
|
+
const L = learnState(now, st);
|
|
3635
|
+
if (!manual && !learnLib.snapshotDue(L.snapshots, { now, hasEvidence: L.evidenceCount > 0 })) return { ok: true, ran: false, skipped: 'not due' };
|
|
3636
|
+
const at = new Date(now).toISOString();
|
|
3637
|
+
const props = learnLib.propose(L.rows, L.missed, now, L.lessons);
|
|
3638
|
+
const written = [];
|
|
3639
|
+
for (const p of props) {
|
|
3640
|
+
const rec = { event: 'proposed', id: learnLib.lessonId(p.key, at), key: p.key, text: p.text, evidence: p.evidence, at };
|
|
3641
|
+
if (nsAppend(PRO_LESSONS(), rec, 'lesson').ok) written.push(rec);
|
|
3642
|
+
}
|
|
3643
|
+
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) };
|
|
3644
|
+
const w = nsAppend(PRO_ROLLUPS(), snap, 'weekly rollup');
|
|
3645
|
+
if (!w.ok) return { ok: false, error: w.error };
|
|
3646
|
+
return { ok: true, ran: true, snapshot: snap, proposed: written };
|
|
3496
3647
|
}
|
|
3497
3648
|
|
|
3498
3649
|
// ── Worktree housekeeping: the app that creates the folders shows and clears them ──────────────
|
|
@@ -4536,19 +4687,39 @@ async function runClaims(w, { sweep = false } = {}) {
|
|
|
4536
4687
|
// D80 — after the local check, and only when all four gates hold (the bridge decides), ask Jev about changed claim pairs.
|
|
4537
4688
|
if (out.ok) {
|
|
4538
4689
|
const f = tsSettings.getRepo(STATE, w.root);
|
|
4539
|
-
const jev = await jevClaims.run({ root: w.root, cacheDir, ledgerFile, key: tsSettings.getKey(STATE), flags: f, exclude: f.exclude, changed: out.run ? out.run.changed : null });
|
|
4690
|
+
const jev = await jevClaims.run({ root: w.root, cacheDir, ledgerFile, key: tsSettings.getKey(STATE), flags: f, exclude: f.exclude, changed: out.run ? out.run.changed : null, askOpts: jevModelOpts(w.root) });
|
|
4540
4691
|
const fresh = claimsLib.readView({ cacheDir, ledgerFile });
|
|
4541
4692
|
return { ...out, ...fresh, jev };
|
|
4542
4693
|
}
|
|
4543
4694
|
return out;
|
|
4544
4695
|
} finally { claimsRunning.delete(w.root); }
|
|
4545
4696
|
}
|
|
4697
|
+
// prompt-model-routing R11 — the Jev version a PM names in the model policy reaches the claim checks and keys their cache. None named: the module's own default.
|
|
4698
|
+
function jevModelOpts(root) { const m = jevRoute.modelFromPolicy(root); return m ? { model: m } : {}; }
|
|
4546
4699
|
// D80 — what the page needs to explain the Jev switch: which gates hold, and what the last pass did.
|
|
4547
4700
|
function jevGateInfo(w) {
|
|
4548
4701
|
const f = tsSettings.getRepo(STATE, w.root);
|
|
4549
4702
|
const g = jevClaims.gates({ key: tsSettings.status(STATE).present, consent: f.consent, watch: f.watch, jev: f.jev });
|
|
4550
4703
|
const { cacheDir } = claimsPaths(w);
|
|
4551
|
-
return { switch: f.jev, open: g.open, missing: g.missing, sentence: g.open ? null : jevClaims.gateSentence(g.missing), ...jevClaims.status(cacheDir) };
|
|
4704
|
+
return { switch: f.jev, status_read: f.status_read, open: g.open, missing: g.missing, sentence: g.open ? null : jevClaims.gateSentence(g.missing), ...jevClaims.status(cacheDir) };
|
|
4705
|
+
}
|
|
4706
|
+
// D82 — what the page needs to explain the categorize switch: which gates hold, and what the last pass did.
|
|
4707
|
+
const doccatRunning = new Set();
|
|
4708
|
+
const doccatAutoAt = new Map(); // root → time of the last automatic pass; at most one a minute
|
|
4709
|
+
function doccatInfo(w) {
|
|
4710
|
+
const f = tsSettings.getRepo(STATE, w.root);
|
|
4711
|
+
const g = jevCategorize.gates({ key: tsSettings.status(STATE).present, consent: f.consent, categorize: f.categorize });
|
|
4712
|
+
return { switch: f.categorize, open: g.open, missing: g.missing, sentence: g.open ? null : jevCategorize.gateSentence(g.missing), running: doccatRunning.has(w.root), ...jevCategorize.status(doccatDir(w.root)) };
|
|
4713
|
+
}
|
|
4714
|
+
// The automatic pass, after a library listing. The bridge checks the three gates and the confirmed first run; this only
|
|
4715
|
+
// keeps it to one pass at a time and one a minute, and never makes the listing wait for it.
|
|
4716
|
+
function doccatAuto(w, lib) {
|
|
4717
|
+
const f = tsSettings.getRepo(STATE, w.root);
|
|
4718
|
+
if (!f.categorize || doccatRunning.has(w.root) || Date.now() - (doccatAutoAt.get(w.root) || 0) < 60000) return;
|
|
4719
|
+
doccatAutoAt.set(w.root, Date.now());
|
|
4720
|
+
doccatRunning.add(w.root);
|
|
4721
|
+
jevCategorize.runAuto({ docs: doccatDocs(w.root, lib), named: namedRecords(w.root), cacheDir: doccatDir(w.root), key: tsSettings.getKey(STATE), flags: f, exclude: f.exclude })
|
|
4722
|
+
.catch(() => null).finally(() => doccatRunning.delete(w.root));
|
|
4552
4723
|
}
|
|
4553
4724
|
const claimsLast = new Map(); // root → { at, run, error }
|
|
4554
4725
|
const claimsWatcher = repoWatch.createWatcher({
|
|
@@ -5014,7 +5185,9 @@ async function handleRequest(req, res) {
|
|
|
5014
5185
|
if (p === '/api/library') {
|
|
5015
5186
|
const w = getWs(url.searchParams.get('ws'));
|
|
5016
5187
|
if (!w) return json(res, { ws: null, shape: 'plain', roots: [], initiatives: [], ungrouped: [], knowledge: [], governance: [] });
|
|
5017
|
-
|
|
5188
|
+
const lib = library(w.root);
|
|
5189
|
+
doccatAuto(w, lib); // D82 — in the background, and only when the three gates hold and the first run was confirmed
|
|
5190
|
+
return json(res, { ws: w.id, ...lib, doccat: doccatInfo(w) });
|
|
5018
5191
|
}
|
|
5019
5192
|
if (p === '/api/surface') {
|
|
5020
5193
|
// On-demand P/D/M/R surface, derived through the local state adapter only (no network).
|
|
@@ -5369,6 +5542,36 @@ async function handleRequest(req, res) {
|
|
|
5369
5542
|
let md; try { md = fs.readFileSync(path.join(DELIV_DIR, id + '.md'), 'utf8'); } catch { res.writeHead(404); return res.end('not found'); }
|
|
5370
5543
|
res.writeHead(200, { 'Content-Type': 'text/markdown; charset=utf-8' }); return res.end(md);
|
|
5371
5544
|
}
|
|
5545
|
+
// ── prompt-model-routing — route ANY prompt to a tier. A SUGGESTION: nothing here starts, switches or types into a session. ──
|
|
5546
|
+
if (p === '/api/model/route/switch' && req.method === 'POST') {
|
|
5547
|
+
let body = {}; try { body = JSON.parse((await readBody(req)) || '{}'); } catch { return json(res, { ok: false, error: 'invalid JSON body' }, 400); }
|
|
5548
|
+
const w = getWs(body.ws); if (!w) return json(res, { ok: false, error: 'no workspace' }, 400);
|
|
5549
|
+
if (typeof body.route !== 'boolean') return json(res, { ok: false, error: 'route must be true or false' }, 400);
|
|
5550
|
+
const out = tsSettings.setRepo(STATE, w.root, { route: body.route });
|
|
5551
|
+
if (!out.ok) return json(res, { ok: false, error: out.error }, 500);
|
|
5552
|
+
const f = tsSettings.getRepo(STATE, w.root);
|
|
5553
|
+
const g = jevRoute.gates({ key: tsSettings.status(STATE).present, consent: f.consent, route: f.route });
|
|
5554
|
+
return json(res, { ok: true, ws: w.id, route: f.route, open: g.open, missing: g.missing, sentence: g.open ? null : jevRoute.gateSentence(g.missing) });
|
|
5555
|
+
}
|
|
5556
|
+
if (p === '/api/model/route' && req.method === 'POST') {
|
|
5557
|
+
let body = {}; try { body = JSON.parse((await readBody(req)) || '{}'); } catch { return json(res, { ok: false, error: 'invalid JSON body' }, 400); }
|
|
5558
|
+
const w = getWs(body.ws); if (!w) return json(res, { ok: false, error: 'no workspace' }, 400);
|
|
5559
|
+
if (typeof body.prompt !== 'string' || !body.prompt.trim() || body.prompt.length > 60000) return json(res, { ok: false, error: 'prompt must be text of up to 60000 characters' }, 400);
|
|
5560
|
+
const f = tsSettings.getRepo(STATE, w.root);
|
|
5561
|
+
const out = await jevRoute.routePrompt({ root: w.root, prompt: body.prompt, ctx: { skill: typeof body.skill === 'string' ? body.skill : undefined, source: body.source, running: true },
|
|
5562
|
+
key: tsSettings.getKey(STATE), flags: { consent: f.consent, route: f.route, exclude: f.exclude } });
|
|
5563
|
+
if (!out.ok) return json(res, { ok: false, error: out.error }, 400);
|
|
5564
|
+
if (out.shadow) return json(res, { ok: true, shadow: true, id: out.id }); // shadow: logged, nothing shown, nothing applied
|
|
5565
|
+
return json(res, { ok: true, ws: w.id, id: out.id, tier: out.tier, model: out.model, effort: out.effort, stage: out.stage, confidence: out.confidence, why: out.why,
|
|
5566
|
+
applies: out.applies, sent: out.sent, withheld: out.withheld ? out.withheld.why : null, gate: out.gate });
|
|
5567
|
+
}
|
|
5568
|
+
if (p === '/api/model/route/outcome' && req.method === 'POST') {
|
|
5569
|
+
let body = {}; try { body = JSON.parse((await readBody(req)) || '{}'); } catch { return json(res, { ok: false, error: 'invalid JSON body' }, 400); }
|
|
5570
|
+
const w = getWs(body.ws); if (!w) return json(res, { ok: false, error: 'no workspace' }, 400);
|
|
5571
|
+
const mc = modelLib.core(); if (!mc.ok) return json(res, { ok: false, error: mc.error }, 500);
|
|
5572
|
+
if (!mc.core.attachOutcome(w.root, body.id, { outcome: body.outcome, applied_tier: body.tier })) return json(res, { ok: false, error: 'an id from a routed prompt and an outcome of ' + mc.core.OUTCOMES.join(', ') + ' are needed (and the decisions ledger opted in)' }, 400);
|
|
5573
|
+
return json(res, { ok: true });
|
|
5574
|
+
}
|
|
5372
5575
|
if (p === '/api/session/model' && req.method === 'POST') {
|
|
5373
5576
|
// D75 — switch the model of a RUNNING Claude session, on the PM's press only. It types the harness's own
|
|
5374
5577
|
// `/model <model>` command into the pane; effort is not changed by it. Never automatic, never for a binary
|
|
@@ -5682,7 +5885,12 @@ async function handleRequest(req, res) {
|
|
|
5682
5885
|
try { nextStepActions.annotate(nextSteps, (it) => nextStepPathOf(root, it)); attachRuns(nextSteps, id); } catch (e) { void e; }
|
|
5683
5886
|
let lcHistory = [];
|
|
5684
5887
|
try { lcHistory = lifecycleHistory(w, id); } catch { lcHistory = []; }
|
|
5685
|
-
|
|
5888
|
+
// D83 — what Jev suggests about the documents tied to this initiative, read from the claims ledger. Advisory: the
|
|
5889
|
+
// panel shows it beside the ladder and nothing derives a step, a stage or a count from it.
|
|
5890
|
+
let suggestions = null;
|
|
5891
|
+
try { if (w) suggestions = jevClaims.suggestionsFor({ ledgerFile: claimsPaths(w).ledgerFile, initiativeId: id }); }
|
|
5892
|
+
catch { suggestions = { ok: false, reason: 'the suggestions could not be read', items: [] }; }
|
|
5893
|
+
return json(res, { ws: w ? w.id : null, ...out, lanes, conversations, files, next_steps: nextSteps, lifecycle_history: lcHistory, suggestions });
|
|
5686
5894
|
}
|
|
5687
5895
|
if (p === '/api/audit' && req.method === 'GET') {
|
|
5688
5896
|
/* The workflow audit (D77): the sweep run on one repository, read-only. It reports state that contradicts
|
|
@@ -6173,6 +6381,62 @@ async function handleRequest(req, res) {
|
|
|
6173
6381
|
if (!out.ok) return json(res, { ok: false, error: out.error }, 500);
|
|
6174
6382
|
return json(res, { ok: true, ws: w.id, watching: tsSettings.getRepo(STATE, w.root).watch === true });
|
|
6175
6383
|
}
|
|
6384
|
+
// ── jev-doc-categorizer Run 3 (C7) — the PM corrects a category, and may write it into the file ─────────────────────
|
|
6385
|
+
// A correction is per-user state OUTSIDE the repository and never edits a file. The write-back is a separate press with
|
|
6386
|
+
// two steps — the diff first, then a confirmed write that names the sha1 of the text the PM saw — and it is the ONLY
|
|
6387
|
+
// write into the repository on this surface.
|
|
6388
|
+
if (p === '/api/doccat/corrections' && req.method === 'GET') {
|
|
6389
|
+
const w = getWs(url.searchParams.get('ws'));
|
|
6390
|
+
if (!w) return json(res, { ok: false, error: 'no workspace' }, 400);
|
|
6391
|
+
return json(res, { ok: true, ws: w.id, corrections: docCatCorrections.get(doccatDir(w.root)) });
|
|
6392
|
+
}
|
|
6393
|
+
if ((p === '/api/doccat/correct' || p === '/api/doccat/writeback') && req.method === 'POST') {
|
|
6394
|
+
let body = {};
|
|
6395
|
+
try { body = JSON.parse((await readBody(req)) || '{}'); }
|
|
6396
|
+
catch { return json(res, { ok: false, error: 'invalid JSON body' }, 400); }
|
|
6397
|
+
const w = getWs(body.ws);
|
|
6398
|
+
if (!w) return json(res, { ok: false, error: 'no workspace' }, 400);
|
|
6399
|
+
// Path guard: the store's own check (relative, no `..`, Markdown only), then the same inside-the-workspace rule every
|
|
6400
|
+
// read and write on this surface uses, then the real path, so a symlink cannot lead out of the repository.
|
|
6401
|
+
const chk = docCatCorrections.checkRel(body.path);
|
|
6402
|
+
if (chk.error) return json(res, { ok: false, error: chk.error }, 400);
|
|
6403
|
+
const rel = chk.rel;
|
|
6404
|
+
const full = path.resolve(w.root, ...rel.split('/'));
|
|
6405
|
+
if (!insideRoot(w.root, full)) return json(res, { ok: false, error: 'outside workspace' }, 404);
|
|
6406
|
+
let lst = null; try { lst = fs.lstatSync(full); } catch { /* reported below */ }
|
|
6407
|
+
if (!lst) return json(res, { ok: false, error: 'There is no such document in this repository.' }, 404);
|
|
6408
|
+
let real = null; try { real = fs.realpathSync(full); } catch { /* reported below */ }
|
|
6409
|
+
let realRoot = w.root; try { realRoot = fs.realpathSync(w.root); } catch { /* keep the root as given */ }
|
|
6410
|
+
if (!real || !insideRoot(realRoot, real)) return json(res, { ok: false, error: 'outside workspace' }, 404);
|
|
6411
|
+
const dir = doccatDir(w.root);
|
|
6412
|
+
if (p === '/api/doccat/correct') {
|
|
6413
|
+
const out = body.clear === true
|
|
6414
|
+
? docCatCorrections.clear(dir, rel)
|
|
6415
|
+
: docCatCorrections.set(dir, rel, { category: body.category, ticket_kind: body.ticket_kind === undefined ? null : body.ticket_kind });
|
|
6416
|
+
if (!out.ok) return json(res, { ok: false, error: out.error }, /could not be saved|symlink/.test(out.error) ? 500 : 400);
|
|
6417
|
+
return json(res, { ok: true, ws: w.id, path: rel, cleared: body.clear === true ? out.cleared : undefined, correction: out.correction || null, corrections: docCatCorrections.get(dir) });
|
|
6418
|
+
}
|
|
6419
|
+
// the write-back
|
|
6420
|
+
if (lst.isSymbolicLink() || !lst.isFile()) return json(res, { ok: false, error: 'This document is a link or not a plain file, so the category was not written into it.' }, 400);
|
|
6421
|
+
let current;
|
|
6422
|
+
try { current = fs.readFileSync(full, 'utf8'); }
|
|
6423
|
+
catch { return json(res, { ok: false, error: 'could not read the document' }, 404); }
|
|
6424
|
+
const corr = { category: body.category, ticket_kind: body.ticket_kind === undefined ? null : body.ticket_kind };
|
|
6425
|
+
if (body.confirm !== true) {
|
|
6426
|
+
const plan = docCatWriteback.plan(current, corr);
|
|
6427
|
+
if (!plan.ok) return json(res, { ok: false, error: plan.error, sha: plan.sha }, 400);
|
|
6428
|
+
return json(res, { ok: true, ws: w.id, path: rel, written: false, diff: plan.diff, changes: plan.changes, sha: plan.sha, next_sha: plan.next_sha, unchanged: plan.unchanged });
|
|
6429
|
+
}
|
|
6430
|
+
const done = docCatWriteback.apply(current, corr, body.expect_sha, body.expect_next_sha);
|
|
6431
|
+
if (!done.ok) return json(res, { ok: false, error: done.error, stale: !!done.stale, sha: done.sha }, done.stale ? 409 : 400);
|
|
6432
|
+
if (done.unchanged) return json(res, { ok: true, ws: w.id, path: rel, written: false, unchanged: true, diff: '' });
|
|
6433
|
+
try { fs.writeFileSync(full, done.next); }
|
|
6434
|
+
catch (e) {
|
|
6435
|
+
warnWriteFail('category write-back (' + rel + ')', e);
|
|
6436
|
+
return json(res, { ok: false, error: 'could not write the document — nothing was changed' }, 500);
|
|
6437
|
+
}
|
|
6438
|
+
return json(res, { ok: true, ws: w.id, path: rel, written: true, diff: done.diff, changes: done.changes, sha: docCatWriteback.sha1(done.next) });
|
|
6439
|
+
}
|
|
6176
6440
|
// ── D80 — Jev on changed claim pairs: the switch, a resume after a pause, and the backlog (plan, then confirmed run) ──
|
|
6177
6441
|
if (p === '/api/claims/jev' && req.method === 'POST') {
|
|
6178
6442
|
let body = {};
|
|
@@ -6185,6 +6449,18 @@ async function handleRequest(req, res) {
|
|
|
6185
6449
|
if (!out.ok) return json(res, { ok: false, error: out.error }, 500);
|
|
6186
6450
|
return json(res, { ok: true, ws: w.id, jev: jevGateInfo(w) });
|
|
6187
6451
|
}
|
|
6452
|
+
// D83 — the second switch: also read the documents tied to each initiative for the two status questions. Default off.
|
|
6453
|
+
if (p === '/api/claims/jev/status' && req.method === 'POST') {
|
|
6454
|
+
let body = {};
|
|
6455
|
+
try { body = JSON.parse((await readBody(req)) || '{}'); }
|
|
6456
|
+
catch { return json(res, { ok: false, error: 'invalid JSON body' }, 400); }
|
|
6457
|
+
const w = getWs(body.ws);
|
|
6458
|
+
if (!w) return json(res, { ok: false, error: 'no workspace' }, 400);
|
|
6459
|
+
if (typeof body.status_read !== 'boolean') return json(res, { ok: false, error: 'status_read must be true or false' }, 400);
|
|
6460
|
+
const out = tsSettings.setRepo(STATE, w.root, { status_read: body.status_read });
|
|
6461
|
+
if (!out.ok) return json(res, { ok: false, error: out.error }, 500);
|
|
6462
|
+
return json(res, { ok: true, ws: w.id, jev: jevGateInfo(w) });
|
|
6463
|
+
}
|
|
6188
6464
|
if (p === '/api/claims/jev/resume' && req.method === 'POST') {
|
|
6189
6465
|
let body = {};
|
|
6190
6466
|
try { body = JSON.parse((await readBody(req)) || '{}'); }
|
|
@@ -6204,14 +6480,61 @@ async function handleRequest(req, res) {
|
|
|
6204
6480
|
const g = jevClaims.gates({ key: tsSettings.status(STATE).present, consent: f.consent, watch: true, jev: f.jev });
|
|
6205
6481
|
if (!g.open) return json(res, { ok: false, error: jevClaims.gateSentence(g.missing) }, 409);
|
|
6206
6482
|
const { cacheDir, ledgerFile } = claimsPaths(w);
|
|
6207
|
-
if (body.confirm !== true) return json(res, { ok: true, ws: w.id, plan: jevClaims.backlogPlan({ root: w.root, cacheDir, exclude: f.exclude }) });
|
|
6483
|
+
if (body.confirm !== true) return json(res, { ok: true, ws: w.id, plan: jevClaims.backlogPlan({ root: w.root, cacheDir, exclude: f.exclude, model: jevRoute.modelFromPolicy(w.root) || undefined, flags: f }) });
|
|
6208
6484
|
if (claimsRunning.has(w.root)) return json(res, { ok: false, busy: true, error: 'a check is already running for this repository' }, 409);
|
|
6209
6485
|
claimsRunning.add(w.root);
|
|
6210
6486
|
try {
|
|
6211
|
-
const out = await jevClaims.runBacklog({ root: w.root, cacheDir, ledgerFile, key: tsSettings.getKey(STATE), flags: f, exclude: f.exclude, expectRequests: body.expect_requests });
|
|
6487
|
+
const out = await jevClaims.runBacklog({ root: w.root, cacheDir, ledgerFile, key: tsSettings.getKey(STATE), flags: f, exclude: f.exclude, expectRequests: body.expect_requests, askOpts: jevModelOpts(w.root) });
|
|
6212
6488
|
return json(res, { ws: w.id, ...out, jev: jevGateInfo(w), ...claimsLib.readView({ cacheDir, ledgerFile }) }, out.ok ? 200 : (out.conflict ? 409 : 500));
|
|
6213
6489
|
} finally { claimsRunning.delete(w.root); }
|
|
6214
6490
|
}
|
|
6491
|
+
// ── D82 — Jev on documents the local rules leave undetermined: the switch, a resume, and the run (plan, then confirmed) ──
|
|
6492
|
+
if (p === '/api/doccat/jev' && req.method === 'GET') {
|
|
6493
|
+
const w = getWs(url.searchParams.get('ws'));
|
|
6494
|
+
if (!w) return json(res, { ok: false, error: 'no workspace' }, 400);
|
|
6495
|
+
return json(res, { ok: true, ws: w.id, doccat: doccatInfo(w) });
|
|
6496
|
+
}
|
|
6497
|
+
if (p === '/api/doccat/jev' && req.method === 'POST') {
|
|
6498
|
+
let body = {};
|
|
6499
|
+
try { body = JSON.parse((await readBody(req)) || '{}'); }
|
|
6500
|
+
catch { return json(res, { ok: false, error: 'invalid JSON body' }, 400); }
|
|
6501
|
+
const w = getWs(body.ws);
|
|
6502
|
+
if (!w) return json(res, { ok: false, error: 'no workspace' }, 400);
|
|
6503
|
+
if (typeof body.categorize !== 'boolean') return json(res, { ok: false, error: 'categorize must be true or false' }, 400);
|
|
6504
|
+
const out = tsSettings.setRepo(STATE, w.root, { categorize: body.categorize });
|
|
6505
|
+
if (!out.ok) return json(res, { ok: false, error: out.error }, 500);
|
|
6506
|
+
return json(res, { ok: true, ws: w.id, doccat: doccatInfo(w) });
|
|
6507
|
+
}
|
|
6508
|
+
if (p === '/api/doccat/jev/resume' && req.method === 'POST') {
|
|
6509
|
+
let body = {};
|
|
6510
|
+
try { body = JSON.parse((await readBody(req)) || '{}'); }
|
|
6511
|
+
catch { return json(res, { ok: false, error: 'invalid JSON body' }, 400); }
|
|
6512
|
+
const w = getWs(body.ws);
|
|
6513
|
+
if (!w) return json(res, { ok: false, error: 'no workspace' }, 400);
|
|
6514
|
+
jevCategorize.resume(doccatDir(w.root));
|
|
6515
|
+
return json(res, { ok: true, ws: w.id, doccat: doccatInfo(w) });
|
|
6516
|
+
}
|
|
6517
|
+
if (p === '/api/doccat/jev/run' && req.method === 'POST') {
|
|
6518
|
+
let body = {};
|
|
6519
|
+
try { body = JSON.parse((await readBody(req)) || '{}'); }
|
|
6520
|
+
catch { return json(res, { ok: false, error: 'invalid JSON body' }, 400); }
|
|
6521
|
+
const w = getWs(body.ws);
|
|
6522
|
+
if (!w) return json(res, { ok: false, error: 'no workspace' }, 400);
|
|
6523
|
+
const f = tsSettings.getRepo(STATE, w.root);
|
|
6524
|
+
const g = jevCategorize.gates({ key: tsSettings.status(STATE).present, consent: f.consent, categorize: f.categorize });
|
|
6525
|
+
if (!g.open) return json(res, { ok: false, error: jevCategorize.gateSentence(g.missing), doccat: doccatInfo(w) }, 409);
|
|
6526
|
+
const cacheDir = doccatDir(w.root);
|
|
6527
|
+
const docs = doccatDocs(w.root, library(w.root));
|
|
6528
|
+
const named = namedRecords(w.root);
|
|
6529
|
+
if (body.confirm !== true) return json(res, { ok: true, ws: w.id, plan: jevCategorize.plan({ docs, named, cacheDir, exclude: f.exclude }) });
|
|
6530
|
+
if (doccatRunning.has(w.root)) return json(res, { ok: false, busy: true, error: 'Jev is already categorizing this repository' }, 409);
|
|
6531
|
+
doccatRunning.add(w.root);
|
|
6532
|
+
try {
|
|
6533
|
+
const out = await jevCategorize.runConfirmed({ docs, named, cacheDir, key: tsSettings.getKey(STATE), flags: f, exclude: f.exclude, expectRequests: body.expect_requests,
|
|
6534
|
+
expectFingerprint: typeof body.expect_fingerprint === 'string' ? body.expect_fingerprint : null });
|
|
6535
|
+
return json(res, { ws: w.id, ...out, doccat: doccatInfo(w) }, out.ok ? 200 : (out.conflict || out.busy ? 409 : 500));
|
|
6536
|
+
} finally { doccatRunning.delete(w.root); }
|
|
6537
|
+
}
|
|
6215
6538
|
// ── D79 — repository validation through TypeSafe ──────────────────────────────────────────────────
|
|
6216
6539
|
// The ONLY routes that lead to an outbound request, and only through lib/typesafe.js. The key is never
|
|
6217
6540
|
// in a response: `status` says present or absent. Nothing here edits a repository file.
|
|
@@ -6242,6 +6565,24 @@ async function handleRequest(req, res) {
|
|
|
6242
6565
|
}
|
|
6243
6566
|
return json(res, { ok: true, ws: w.id, name: w.name, ...tsSettings.getRepo(STATE, w.root), key: tsSettings.status(STATE) });
|
|
6244
6567
|
}
|
|
6568
|
+
/* ── live notes (jev-live-notes, Run 2): the ranked proposals of a conversation, and the PM's accept, change or reject.
|
|
6569
|
+
* Reads the repository's ledger; the one write is a decision row. No request leaves this process, and no page is built yet. ── */
|
|
6570
|
+
if (p === '/api/live-notes' && req.method === 'GET') {
|
|
6571
|
+
const wsq = url.searchParams.get('ws');
|
|
6572
|
+
const w = wsq ? workspaces.get(wsq) : homeWorkspace(); // a named workspace the server does not know is refused, never replaced by the home one
|
|
6573
|
+
if (!w) return json(res, { ok: false, error: 'no such workspace' }, 400);
|
|
6574
|
+
const r = liveNotes.apiList({ root: w.root, session: url.searchParams.get('session') || null });
|
|
6575
|
+
return json(res, r, r.ok ? 200 : (r.status || 400));
|
|
6576
|
+
}
|
|
6577
|
+
if (p === '/api/live-notes/decide' && req.method === 'POST') {
|
|
6578
|
+
let body = {};
|
|
6579
|
+
try { body = JSON.parse((await readBody(req)) || '{}'); }
|
|
6580
|
+
catch { return json(res, { ok: false, error: 'invalid JSON body' }, 400); }
|
|
6581
|
+
const w = body.ws ? workspaces.get(String(body.ws)) : homeWorkspace();
|
|
6582
|
+
if (!w) return json(res, { ok: false, error: 'no such workspace' }, 400);
|
|
6583
|
+
const r = liveNotes.apiDecide({ root: w.root, runtimeDir: STATE, session: body.session, segment: body.segment, action: body.action, initiative: body.initiative || null, relation: body.relation || null, exclude: tsSettings.getRepo(STATE, w.root).exclude });
|
|
6584
|
+
return json(res, r, r.ok ? 200 : (r.status || 400));
|
|
6585
|
+
}
|
|
6245
6586
|
if (p === '/api/validate/plan' && req.method === 'POST') {
|
|
6246
6587
|
let body = {};
|
|
6247
6588
|
try { body = JSON.parse((await readBody(req)) || '{}'); }
|
|
@@ -6376,6 +6717,49 @@ async function handleRequest(req, res) {
|
|
|
6376
6717
|
return json(res, { ok: true, settings: n.settings });
|
|
6377
6718
|
}
|
|
6378
6719
|
}
|
|
6720
|
+
/* ── learning (D4): the PM's verdicts become bounded weights, a rollup and lessons that wait for the PM's press ── */
|
|
6721
|
+
if (p === '/api/proactive/learn' && req.method === 'GET') {
|
|
6722
|
+
const now = Date.now(); const st = proSettings(); const L = learnState(now, st);
|
|
6723
|
+
const { rows, missed, preparedAt, weights: _w, ...pub } = L;
|
|
6724
|
+
const cells = [];
|
|
6725
|
+
for (const dim of ['kind', 'lane', 'time']) for (const [k, c] of Object.entries(L.weights[dim])) cells.push({ dim, key: k, ...c });
|
|
6726
|
+
cells.sort((a, b) => Math.abs(b.delta) - Math.abs(a.delta) || b.n - a.n);
|
|
6727
|
+
return json(res, { ok: true, as_of: new Date(now).toISOString(), ...pub, cells, caps: learnLib.CAP, min_cell: learnLib.MIN_CELL,
|
|
6728
|
+
approved: L.lessons.filter((x) => x.status === 'approved'), max_approved: learnLib.MAX_APPROVED,
|
|
6729
|
+
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.' });
|
|
6730
|
+
}
|
|
6731
|
+
if (p === '/api/proactive/learn/run' && req.method === 'POST') {
|
|
6732
|
+
const r = learnWeekly({ manual: true });
|
|
6733
|
+
return json(res, r, r.ok ? 200 : (/paused/.test(r.error || '') ? 409 : 500));
|
|
6734
|
+
}
|
|
6735
|
+
if (p === '/api/proactive/learn/lesson' && req.method === 'POST') {
|
|
6736
|
+
let body = {}; try { body = JSON.parse((await readBody(req)) || '{}'); } catch { return json(res, { ok: false, error: 'invalid JSON body' }, 400); }
|
|
6737
|
+
const c = learnLib.checkLessonRequest(body);
|
|
6738
|
+
if (!c.ok) return json(res, { ok: false, error: c.error }, 400);
|
|
6739
|
+
const lessons = learnLib.foldLessons(nsRead(PRO_LESSONS()));
|
|
6740
|
+
const why = learnLib.decideLesson(lessons, c.req);
|
|
6741
|
+
if (why) return json(res, { ok: false, error: why }, why === 'no such lesson' ? 404 : 409);
|
|
6742
|
+
const ev = { approve: 'approved', reject: 'rejected', retire: 'retired' }[c.req.decision];
|
|
6743
|
+
const w = nsAppend(PRO_LESSONS(), { event: ev, id: c.req.id, at: nowISO(), pm: 'wawan' }, 'lesson');
|
|
6744
|
+
if (!w.ok) return json(res, { ok: false, error: w.error }, 500);
|
|
6745
|
+
return json(res, { ok: true, id: c.req.id, status: ev });
|
|
6746
|
+
}
|
|
6747
|
+
if (p === '/api/proactive/learn/restore' && req.method === 'POST') {
|
|
6748
|
+
let body = {}; try { body = JSON.parse((await readBody(req)) || '{}'); } catch { return json(res, { ok: false, error: 'invalid JSON body' }, 400); }
|
|
6749
|
+
if (body.confirm !== true) return json(res, { ok: false, error: 'confirm is required' }, 400);
|
|
6750
|
+
const L = learnState(Date.now());
|
|
6751
|
+
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);
|
|
6752
|
+
const w = nsAppend(PRO_LEARN_EVENTS(), { event: 'restore', kind: body.kind, at: nowISO(), pm: 'wawan' }, 'learn event');
|
|
6753
|
+
if (!w.ok) return json(res, { ok: false, error: w.error }, 500);
|
|
6754
|
+
return json(res, { ok: true, kind: body.kind });
|
|
6755
|
+
}
|
|
6756
|
+
if (p === '/api/proactive/learn/reset' && req.method === 'POST') {
|
|
6757
|
+
let body = {}; try { body = JSON.parse((await readBody(req)) || '{}'); } catch { return json(res, { ok: false, error: 'invalid JSON body' }, 400); }
|
|
6758
|
+
if (body.confirm !== true) return json(res, { ok: false, error: 'confirm is required: this makes the engine ignore all earlier feedback' }, 400);
|
|
6759
|
+
const w = nsAppend(PRO_LEARN_EVENTS(), { event: 'reset', at: nowISO(), pm: 'wawan' }, 'learn event');
|
|
6760
|
+
if (!w.ok) return json(res, { ok: false, error: w.error }, 500);
|
|
6761
|
+
return json(res, { ok: true });
|
|
6762
|
+
}
|
|
6379
6763
|
if (p === '/api/lifecycle' && req.method === 'POST') {
|
|
6380
6764
|
// D70 — close, hold, cancel or reopen an initiative. Only reached by an explicit PM press. It
|
|
6381
6765
|
// appends ONE line to lifecycle.jsonl and never writes an acceptance.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: live-notes
|
|
3
|
+
label: "Live notes"
|
|
4
|
+
description: "Take the talking points of a conversation, place each against the open initiatives, add a short cited context note where one is needed, and propose titles for new-initiative candidates. Proposals only; nothing is created, changed or decided."
|
|
5
|
+
run: agent
|
|
6
|
+
agent: claude
|
|
7
|
+
tier: balanced
|
|
8
|
+
version: 1.1.0
|
|
9
|
+
owner: wawan
|
|
10
|
+
risk: low
|
|
11
|
+
scope: read:connectors, read:state, write:state/live-notes
|
|
12
|
+
output:
|
|
13
|
+
- Placed talking points
|
|
14
|
+
- Context notes
|
|
15
|
+
- Candidates for new initiatives
|
|
16
|
+
- Sources reached and not reached
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
/live-notes — place a conversation against this repository's initiatives (jev-live-notes). Work only from the text you are given or can read with the connectors you already have. Do not guess.
|
|
20
|
+
|
|
21
|
+
Rules that always hold:
|
|
22
|
+
1. PROPOSALS ONLY. Never create, edit, accept, close or hold an initiative. Never write a decision, an acceptance or a run row. A candidate for a new initiative becomes one only when the PM presses through pmos-router.
|
|
23
|
+
2. READ-ONLY on every source. Never send, reply, post, comment, create, edit, label or delete anything in a connected source.
|
|
24
|
+
3. NOTHING LEAVES THIS MACHINE UNLESS THE COMMAND SAYS SO. Send a talking point to Typesafe only when the command's `path` line says `jev-agent`, and then only the point's words and the option list, never a speaker's name, a file or a key. On any other path, send nothing to any outside service. Report the path as the command says it.
|
|
25
|
+
4. NOTHING INVENTED. A source you cannot reach is reported as not reached. A note you cannot support from the repository's own text says the text did not settle the point.
|
|
26
|
+
5. PRIVATE STAYS PRIVATE. Skip anything marked private, personal, confidential or HR. The command withholds secret-shaped text; never paste it back.
|
|
27
|
+
6. SHORT. A context note is at most 600 characters and cites the repository's own words exactly.
|
|
28
|
+
|
|
29
|
+
Procedure (each step ends with a check that shows it is done):
|
|
30
|
+
1. Find the segments. Segments are one per line in a file: plain text, or a JSON object with "text", "speaker", "source". If the PM gave you a file, use it. Otherwise use the source step below, or ask the PM for the text.
|
|
31
|
+
Done when: you can name the segments file, or you have said no text was available.
|
|
32
|
+
Source step, Wispr Flow meetings (use it only when the PM asked for a meeting, or for what was said since the last time): run `pmos proactive settings` and read the `wispr` line under "brief sources". If it is off, say "wispr: off" and stop; do not read it. If it is on:
|
|
33
|
+
a. List meetings with the Wispr Flow `search_meetings` tool, `since` the time of the last meeting you ingested (keep that cursor in your own notes for this session; with none, ask the PM which meeting). Use only the meeting's title, time, id, share link and whether it has a transcript.
|
|
34
|
+
b. Skip any meeting whose title or folder says private, personal, confidential or HR. Count the skipped ones and say the count; do not read them.
|
|
35
|
+
c. For each other meeting that has a transcript, read it with `get_meeting` and `view_transcript`, one bounded range at a time, using the continuation offset until it ends. Write the transcript text to a file in the scratch folder. Never copy the meeting's notes or summary anywhere.
|
|
36
|
+
d. Run `node workspace/bin/live-notes.js ingest --repo . --transcript <that file> --source "wispr:<meeting id>" --session <id>`. It adds only what is new, so reading a growing transcript again is safe. The pointer on each point names the meeting; give the PM the share link in your final message.
|
|
37
|
+
e. If the connector is not available in this session, or a call fails, say "wispr: not reached" with the reason. Do not make up a transcript.
|
|
38
|
+
Done when: each meeting you tried is read, skipped (counted) or not reached, and ingest printed its session and any duplicates skipped.
|
|
39
|
+
2. Run `node workspace/bin/live-notes.js run --repo . --segments <file> --session <id>`. Give `--anchor domain:<id>` when it says too many initiatives match. It prints the path it ran, any withheld lines, and a prompt.
|
|
40
|
+
Done when: the command printed a session id and either rows or a prompt.
|
|
41
|
+
3. Answer the prompt it printed, in its exact shape: one `LIVENOTE {json}` line per talking point, to a file in the scratch folder. Use "answers" only when the point answers a named open question, and name it. If the prompt says to use the Typesafe skill, use it for the initiative and relation choices and write "by":"jev"; if the skill or the key is missing, or a call fails, decide yourself and write "by":"agent". Never write "by":"jev" for a choice you made yourself. Then run `node workspace/bin/live-notes.js run --repo . --session <id> --answers <that file>`.
|
|
42
|
+
Done when: every talking point has a proposed row, or the command named why one was refused.
|
|
43
|
+
4. Run `node workspace/bin/live-notes.js plan --repo . --session <id>`. For each context note it lists, read the files it names, then write one `LIVECONTEXT {json}` line: `{"segment":"<id>","note":"<at most 600 characters>","settled":true,"cites":[{"path":"<repository path>","quote":"<an exact passage of that file, at least 8 characters>"}]}`. Set "settled" to false when the initiative's own text does not decide the point. Use the tier the plan names for each step: ask `pmos-model recommend "<the work>" --skill live-notes` if you can launch a sub-task on a model.
|
|
44
|
+
For each candidate it lists, write one `LIVECANDIDATE {json}` line: `{"candidate":"<id>","title":"<at most 80 characters>","lane":"feature|discovery|chore|incident|content|prototype"}`. Run the pmos-router sharpness and prior-rejection questions in your head first; the command records the prior-rejection check on the title.
|
|
45
|
+
Put all the lines in one file and run `node workspace/bin/live-notes.js context --repo . --session <id> --answers <that file>`. Read any rejection, fix the line and run it once more. Do not work around a refusal.
|
|
46
|
+
Done when: the command says how many notes and drafts it accepted and nothing is left rejected.
|
|
47
|
+
5. Show the PM the list and wait. The PM's accept, change and reject go in through `node workspace/bin/live-notes.js decide`, one per point, only when the PM says so. Do not decide for them.
|
|
48
|
+
Done when: you have shown the list and recorded nothing the PM did not say.
|
|
49
|
+
6. When the PM says the conversation is over, run `node workspace/bin/live-notes.js close --repo . --session <id>`. It appends a section to today's requirements note and deletes the raw transcript.
|
|
50
|
+
Done when: the command printed the note path and "transcript: deleted", or the retention date the PM set.
|
|
51
|
+
7. Finish by saying in two lines how many points were placed, how many are still unanswered, which path ran, and which sources were not reached. Do not commit; the PM's Sync carries the files.
|
|
52
|
+
Done when: the two lines are said once and no commit was made.
|
|
53
|
+
|
|
54
|
+
Output contract — produce EXACTLY these sections in your final message:
|
|
55
|
+
- Placed talking points
|
|
56
|
+
- Context notes
|
|
57
|
+
- Candidates for new initiatives
|
|
58
|
+
- Sources reached and not reached
|