create-pmos 0.47.0 → 0.49.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 +2 -2
- package/payload/mods/pmos-cockpit/.claude-plugin/plugin.json +1 -1
- package/payload/mods/pmos-cockpit/README.md +14 -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/route.mjs +95 -0
- package/payload/mods/pmos-cockpit/hooks/register.tsx +104 -19
- package/payload/mods/pmos-cockpit/types/index.d.ts +1 -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 +214 -0
- package/payload/scripts/workflow-state.js +59 -2
- package/payload/state/discoveries.md +1 -1
- package/payload/state/log.md +1 -1
- package/workspace/README.md +2 -2
- 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/index.md +2 -1
- package/workspace/guide/initiatives-and-next-steps.md +17 -2
- package/workspace/guide/knowledge-graph.md +94 -1
- package/workspace/guide/layout.md +31 -2
- package/workspace/guide/live-notes.md +78 -0
- package/workspace/guide/proactive-engine.md +6 -2
- package/workspace/guide/validate-with-typesafe.md +31 -2
- 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 +214 -0
- package/workspace/lib/engine/workflow-state.js +59 -2
- package/workspace/lib/jev-categorize.js +433 -0
- package/workspace/lib/jev-claims.js +59 -15
- package/workspace/lib/jev-live.js +37 -0
- package/workspace/lib/jev-route.js +139 -0
- package/workspace/lib/kg-cli.js +3 -1
- package/workspace/lib/kg.js +6 -1
- package/workspace/lib/learn.js +5 -3
- package/workspace/lib/live-notes.js +1035 -0
- 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 +78 -0
- package/workspace/public/pm/fixture.js +40 -2
- package/workspace/public/pm/modes-live.js +279 -18
- package/workspace/public/pm/panel-cockpit.js +638 -0
- package/workspace/public/pm/panel-detail.js +212 -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 +249 -29
- package/workspace/public/pm/state.js +48 -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-livenotes.js +397 -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 +349 -7
- package/workspace/skills/live-notes.skill +58 -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,16 @@ 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
|
|
79
|
+
const jevLive = require('./lib/jev-live.js'); // jev-live-notes Run 7 (D85) — the live view's one bridge to Jev, behind three gates
|
|
70
80
|
const claimsLib = require('./lib/claims.js');
|
|
81
|
+
const docCategory = require('./lib/doc-category.js'); // jev-doc-categorizer Run 1 — the PM-facing kind of a document, derived, local, no model
|
|
82
|
+
const docCatCorrections = require('./lib/doc-category-corrections.js'); // jev-doc-categorizer Run 3 — the PM's own category, per user, outside the repository
|
|
83
|
+
const docCatWriteback = require('./lib/doc-category-writeback.js'); // …and the separate press that writes it into the file's frontmatter
|
|
71
84
|
const repoWatch = require('./lib/repo-watch.js');
|
|
72
85
|
tsSettings.adoptEnv(process.env); // D79 — take the env key into memory and delete it, before anything is spawned
|
|
73
86
|
const scheduleLogic = require('./lib/schedule-logic');
|
|
@@ -278,6 +291,11 @@ function sessionEnv() {
|
|
|
278
291
|
// D79 — the TypeSafe key is the workspace server's own credential. A terminal runs an agent that can
|
|
279
292
|
// print its environment, so the key is never handed to one, whether it came from settings or the shell.
|
|
280
293
|
delete env[tsSettings.ENV_NAME];
|
|
294
|
+
if (SET_GIT_OPTIONAL_LOCKS) delete env.GIT_OPTIONAL_LOCKS;
|
|
295
|
+
// prompt-model-routing Run 3 — where the cockpit mod asks for a prompt's route: this server, on loopback.
|
|
296
|
+
// An address only; the Jev key stays here (D79).
|
|
297
|
+
const routeUrl = jevRoute.routeUrl(PORT);
|
|
298
|
+
if (routeUrl) env.PMOS_ROUTE_URL = routeUrl; else delete env.PMOS_ROUTE_URL;
|
|
281
299
|
return env;
|
|
282
300
|
}
|
|
283
301
|
|
|
@@ -457,6 +475,12 @@ function homeWorkspace() {
|
|
|
457
475
|
return HOME_ENABLED ? workspaces.get(wsId(SELF_ROOT)) : [...workspaces.values()][0];
|
|
458
476
|
}
|
|
459
477
|
function getWs(id) { return (id && workspaces.get(id)) || homeWorkspace(); }
|
|
478
|
+
/** The registered workspace whose root is this path exactly, or null. Never the home workspace by default. */
|
|
479
|
+
function wsByRoot(root) {
|
|
480
|
+
const want = path.resolve(String(root));
|
|
481
|
+
for (const w of workspaces.values()) if (w && path.resolve(w.root) === want) return w;
|
|
482
|
+
return null;
|
|
483
|
+
}
|
|
460
484
|
// R2/R3 — what the sidebar tree needs and cannot derive for itself: which REPOSITORY a registered
|
|
461
485
|
// working tree belongs to, and which branch it is on. `git worktree list` names the main worktree
|
|
462
486
|
// first from any of a repo's trees (git-info.worktreesAt), so index 0 is the group key: it is what
|
|
@@ -1536,6 +1560,85 @@ function stampEdit(text, at) {
|
|
|
1536
1560
|
const next = fm.replace(re, `generated: {by: human:wawan, at: "${at}"}`);
|
|
1537
1561
|
return next === fm ? text : text.replace(fm, () => next);
|
|
1538
1562
|
}
|
|
1563
|
+
// ── Document category (jev-doc-categorizer Run 1) ───────────────────────────────────────────────────
|
|
1564
|
+
// A derived label beside the document's own `type`: local rules only, no model, nothing written. The library lists it with the
|
|
1565
|
+
// 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
|
|
1566
|
+
// the evidence is the frontmatter, the name and the headings.
|
|
1567
|
+
const catCache = new Map();
|
|
1568
|
+
function categoryInput(full) {
|
|
1569
|
+
let st; try { st = fs.statSync(full); } catch { return null; }
|
|
1570
|
+
const hit = catCache.get(full);
|
|
1571
|
+
if (hit && hit.m === st.mtimeMs) return hit;
|
|
1572
|
+
let text = '';
|
|
1573
|
+
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; }
|
|
1574
|
+
let fm = {}; try { fm = relatedDocs.frontmatter(text) || {}; } catch { /* no frontmatter */ }
|
|
1575
|
+
const ent = { m: st.mtimeMs, fm, text };
|
|
1576
|
+
catCache.set(full, ent);
|
|
1577
|
+
return ent;
|
|
1578
|
+
}
|
|
1579
|
+
function namedRecords(root) {
|
|
1580
|
+
const records = [];
|
|
1581
|
+
for (const rel of ['planning/initiatives', 'pmos/state/initiatives', 'state/initiatives']) {
|
|
1582
|
+
const d = path.join(root, ...rel.split('/'));
|
|
1583
|
+
let names = []; try { names = fs.readdirSync(d).filter((f) => f.endsWith('.md')); } catch { continue; }
|
|
1584
|
+
for (const f of names) { const c = categoryInput(path.join(d, f)); if (c) records.push({ rel: rel + '/' + f, fm: c.fm }); }
|
|
1585
|
+
}
|
|
1586
|
+
return docCategory.namedFromRecords(records);
|
|
1587
|
+
}
|
|
1588
|
+
// Run 3 (C7) — the PM's corrections for one repository live in the per-user runtime directory, keyed like the claims cache
|
|
1589
|
+
// (sha1 of the root), never in the repository.
|
|
1590
|
+
function doccatDir(root) { return docCatCorrections.repoDir(STATE, root); }
|
|
1591
|
+
// Adds `category`, `category_basis` and `category_why` to every markdown document anywhere in a library answer, and
|
|
1592
|
+
// `category_kind` for a ticket. A stored correction is passed in, so it wins with basis `pm correction`; the bases that
|
|
1593
|
+
// disagree with the winner come as `category_disagrees`.
|
|
1594
|
+
function annotateCategories(root, node, named, corr = {}, seen = new Set()) {
|
|
1595
|
+
if (!node || typeof node !== 'object' || seen.has(node)) return node;
|
|
1596
|
+
seen.add(node);
|
|
1597
|
+
if (Array.isArray(node)) { for (const x of node) annotateCategories(root, x, named, corr, seen); return node; }
|
|
1598
|
+
if (typeof node.path === 'string' && /\.md$/i.test(node.path)) {
|
|
1599
|
+
const c = categoryInput(path.join(root, node.path));
|
|
1600
|
+
if (c) {
|
|
1601
|
+
const rel = node.path.split(path.sep).join('/');
|
|
1602
|
+
const r = docCategory.categorize({ rel, fm: c.fm, text: c.text, named, correction: Object.prototype.hasOwnProperty.call(corr, rel) ? corr[rel] : null });
|
|
1603
|
+
node.category = r.category; node.category_basis = r.basis; node.category_why = r.via;
|
|
1604
|
+
if (r.ticket_kind) node.category_kind = r.ticket_kind;
|
|
1605
|
+
if (r.disagrees.length) node.category_disagrees = r.disagrees;
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
for (const k of Object.keys(node)) if (node[k] && typeof node[k] === 'object') annotateCategories(root, node[k], named, corr, seen);
|
|
1609
|
+
return node;
|
|
1610
|
+
}
|
|
1611
|
+
// ── D82 — Jev's category for a file the local rules leave undetermined ─────────────────────────────────
|
|
1612
|
+
// Its state lives beside the corrections, in doccatDir(root): per user, outside the repository.
|
|
1613
|
+
// Reads Jev's CACHED answers only; sends nothing. A decided answer becomes the category with the basis `jev`; an unsure one
|
|
1614
|
+
// leaves the file `undetermined` and says which two options led. Advisory (D40): nothing downstream reads it as fact.
|
|
1615
|
+
function annotateJev(root, node, cache, seen = new Set()) {
|
|
1616
|
+
if (!node || typeof node !== 'object' || seen.has(node)) return node;
|
|
1617
|
+
seen.add(node);
|
|
1618
|
+
if (Array.isArray(node)) { for (const x of node) annotateJev(root, x, cache, seen); return node; }
|
|
1619
|
+
if (node.category === docCategory.UNDETERMINED && typeof node.path === 'string' && /\.md$/i.test(node.path)) {
|
|
1620
|
+
const c = categoryInput(path.join(root, node.path));
|
|
1621
|
+
const l = c ? jevCategorize.lookup(cache, { fm: c.fm, text: c.text }) : null;
|
|
1622
|
+
if (l && l.decided) { node.category = l.category; node.category_basis = 'jev'; node.category_why = l.sentence; if (l.ticket_kind) node.category_kind = l.ticket_kind; }
|
|
1623
|
+
else if (l) { node.category_why = l.sentence; node.category_jev = 'unsure'; }
|
|
1624
|
+
}
|
|
1625
|
+
for (const k of Object.keys(node)) if (node[k] && typeof node[k] === 'object') annotateJev(root, node[k], cache, seen);
|
|
1626
|
+
return node;
|
|
1627
|
+
}
|
|
1628
|
+
// Every Markdown document the library lists, with what the categorizer needs. Read through the same per-file cache.
|
|
1629
|
+
function doccatDocs(root, lib) {
|
|
1630
|
+
const rels = new Set();
|
|
1631
|
+
(function walk(n, seen) {
|
|
1632
|
+
if (!n || typeof n !== 'object' || seen.has(n)) return; seen.add(n);
|
|
1633
|
+
if (Array.isArray(n)) { for (const x of n) walk(x, seen); return; }
|
|
1634
|
+
if (typeof n.path === 'string' && /\.md$/i.test(n.path)) rels.add(n.path.split(path.sep).join('/'));
|
|
1635
|
+
for (const k of Object.keys(n)) if (n[k] && typeof n[k] === 'object') walk(n[k], seen);
|
|
1636
|
+
})(lib, new Set());
|
|
1637
|
+
const docs = [];
|
|
1638
|
+
const corrections = docCatCorrections.get(doccatDir(root)); // C7 — a corrected file is decided, so it is never sent
|
|
1639
|
+
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 }); }
|
|
1640
|
+
return docs;
|
|
1641
|
+
}
|
|
1539
1642
|
function library(root) {
|
|
1540
1643
|
const shape = wsShape(root);
|
|
1541
1644
|
const roots = libraryTree(root);
|
|
@@ -1553,7 +1656,8 @@ function library(root) {
|
|
|
1553
1656
|
const notes = NOTE_DIRS.map((d) => path.join(root, ...d.split('/'))).filter((d) => isDir(d))
|
|
1554
1657
|
.map((d) => tree(root, d, {}, 0, { n: TREE_MAX_NODES }))
|
|
1555
1658
|
.filter((t) => t.dirs.length || t.docs.length);
|
|
1556
|
-
|
|
1659
|
+
const out = { shape, roots, initiatives, ungrouped, knowledge, notes, governance: looseDocs(root) };
|
|
1660
|
+
return annotateJev(root, annotateCategories(root, out, namedRecords(root), docCatCorrections.get(doccatDir(root))), jevCategorize.readCache(doccatDir(root)));
|
|
1557
1661
|
}
|
|
1558
1662
|
|
|
1559
1663
|
// ── On-demand surfaces (D60 / the workspace-ondemand-surfaces PRD). ────────────────────────────
|
|
@@ -4594,19 +4698,51 @@ async function runClaims(w, { sweep = false } = {}) {
|
|
|
4594
4698
|
// D80 — after the local check, and only when all four gates hold (the bridge decides), ask Jev about changed claim pairs.
|
|
4595
4699
|
if (out.ok) {
|
|
4596
4700
|
const f = tsSettings.getRepo(STATE, w.root);
|
|
4597
|
-
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 });
|
|
4701
|
+
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) });
|
|
4598
4702
|
const fresh = claimsLib.readView({ cacheDir, ledgerFile });
|
|
4599
4703
|
return { ...out, ...fresh, jev };
|
|
4600
4704
|
}
|
|
4601
4705
|
return out;
|
|
4602
4706
|
} finally { claimsRunning.delete(w.root); }
|
|
4603
4707
|
}
|
|
4708
|
+
// 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.
|
|
4709
|
+
function jevModelOpts(root) { const m = jevRoute.modelFromPolicy(root); return m ? { model: m } : {}; }
|
|
4604
4710
|
// D80 — what the page needs to explain the Jev switch: which gates hold, and what the last pass did.
|
|
4605
4711
|
function jevGateInfo(w) {
|
|
4606
4712
|
const f = tsSettings.getRepo(STATE, w.root);
|
|
4607
4713
|
const g = jevClaims.gates({ key: tsSettings.status(STATE).present, consent: f.consent, watch: f.watch, jev: f.jev });
|
|
4608
4714
|
const { cacheDir } = claimsPaths(w);
|
|
4609
|
-
return { switch: f.jev, open: g.open, missing: g.missing, sentence: g.open ? null : jevClaims.gateSentence(g.missing), ...jevClaims.status(cacheDir) };
|
|
4715
|
+
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) };
|
|
4716
|
+
}
|
|
4717
|
+
// D85 — the live-notes add route's sender: lib/typesafe.js behind the key, consent and the switch, or none. The key goes to the engine
|
|
4718
|
+
// only when the sender does, and never into a response.
|
|
4719
|
+
function liveSender(w) {
|
|
4720
|
+
const key = tsSettings.getKey(STATE);
|
|
4721
|
+
const flags = { consent: tsSettings.getRepo(STATE, w.root).consent, live: liveNotes.getSwitch(STATE, w.root).live };
|
|
4722
|
+
const snd = jevLive.senderFor(key, flags, jevModelOpts(w.root));
|
|
4723
|
+
return { key: snd.ask ? key : null, flags, ask: snd.ask, askOpts: snd.askOpts };
|
|
4724
|
+
}
|
|
4725
|
+
// D85 — what the live-notes view needs to explain its Jev switch. Booleans and sentences only; the key never leaves the store.
|
|
4726
|
+
function liveJevInfo(w) {
|
|
4727
|
+
return jevLive.infoFor(tsSettings.status(STATE).present, { consent: tsSettings.getRepo(STATE, w.root).consent, live: liveNotes.getSwitch(STATE, w.root).live });
|
|
4728
|
+
}
|
|
4729
|
+
// D82 — what the page needs to explain the categorize switch: which gates hold, and what the last pass did.
|
|
4730
|
+
const doccatRunning = new Set();
|
|
4731
|
+
const doccatAutoAt = new Map(); // root → time of the last automatic pass; at most one a minute
|
|
4732
|
+
function doccatInfo(w) {
|
|
4733
|
+
const f = tsSettings.getRepo(STATE, w.root);
|
|
4734
|
+
const g = jevCategorize.gates({ key: tsSettings.status(STATE).present, consent: f.consent, categorize: f.categorize });
|
|
4735
|
+
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)) };
|
|
4736
|
+
}
|
|
4737
|
+
// The automatic pass, after a library listing. The bridge checks the three gates and the confirmed first run; this only
|
|
4738
|
+
// keeps it to one pass at a time and one a minute, and never makes the listing wait for it.
|
|
4739
|
+
function doccatAuto(w, lib) {
|
|
4740
|
+
const f = tsSettings.getRepo(STATE, w.root);
|
|
4741
|
+
if (!f.categorize || doccatRunning.has(w.root) || Date.now() - (doccatAutoAt.get(w.root) || 0) < 60000) return;
|
|
4742
|
+
doccatAutoAt.set(w.root, Date.now());
|
|
4743
|
+
doccatRunning.add(w.root);
|
|
4744
|
+
jevCategorize.runAuto({ docs: doccatDocs(w.root, lib), named: namedRecords(w.root), cacheDir: doccatDir(w.root), key: tsSettings.getKey(STATE), flags: f, exclude: f.exclude })
|
|
4745
|
+
.catch(() => null).finally(() => doccatRunning.delete(w.root));
|
|
4610
4746
|
}
|
|
4611
4747
|
const claimsLast = new Map(); // root → { at, run, error }
|
|
4612
4748
|
const claimsWatcher = repoWatch.createWatcher({
|
|
@@ -5072,7 +5208,9 @@ async function handleRequest(req, res) {
|
|
|
5072
5208
|
if (p === '/api/library') {
|
|
5073
5209
|
const w = getWs(url.searchParams.get('ws'));
|
|
5074
5210
|
if (!w) return json(res, { ws: null, shape: 'plain', roots: [], initiatives: [], ungrouped: [], knowledge: [], governance: [] });
|
|
5075
|
-
|
|
5211
|
+
const lib = library(w.root);
|
|
5212
|
+
doccatAuto(w, lib); // D82 — in the background, and only when the three gates hold and the first run was confirmed
|
|
5213
|
+
return json(res, { ws: w.id, ...lib, doccat: doccatInfo(w) });
|
|
5076
5214
|
}
|
|
5077
5215
|
if (p === '/api/surface') {
|
|
5078
5216
|
// On-demand P/D/M/R surface, derived through the local state adapter only (no network).
|
|
@@ -5427,6 +5565,38 @@ async function handleRequest(req, res) {
|
|
|
5427
5565
|
let md; try { md = fs.readFileSync(path.join(DELIV_DIR, id + '.md'), 'utf8'); } catch { res.writeHead(404); return res.end('not found'); }
|
|
5428
5566
|
res.writeHead(200, { 'Content-Type': 'text/markdown; charset=utf-8' }); return res.end(md);
|
|
5429
5567
|
}
|
|
5568
|
+
// ── prompt-model-routing — route ANY prompt to a tier. A SUGGESTION: nothing here starts, switches or types into a session. ──
|
|
5569
|
+
if (p === '/api/model/route/switch' && req.method === 'POST') {
|
|
5570
|
+
let body = {}; try { body = JSON.parse((await readBody(req)) || '{}'); } catch { return json(res, { ok: false, error: 'invalid JSON body' }, 400); }
|
|
5571
|
+
const w = getWs(body.ws); if (!w) return json(res, { ok: false, error: 'no workspace' }, 400);
|
|
5572
|
+
if (typeof body.route !== 'boolean') return json(res, { ok: false, error: 'route must be true or false' }, 400);
|
|
5573
|
+
const out = tsSettings.setRepo(STATE, w.root, { route: body.route });
|
|
5574
|
+
if (!out.ok) return json(res, { ok: false, error: out.error }, 500);
|
|
5575
|
+
const f = tsSettings.getRepo(STATE, w.root);
|
|
5576
|
+
const g = jevRoute.gates({ key: tsSettings.status(STATE).present, consent: f.consent, route: f.route });
|
|
5577
|
+
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) });
|
|
5578
|
+
}
|
|
5579
|
+
if (p === '/api/model/route' && req.method === 'POST') {
|
|
5580
|
+
let body = {}; try { body = JSON.parse((await readBody(req)) || '{}'); } catch { return json(res, { ok: false, error: 'invalid JSON body' }, 400); }
|
|
5581
|
+
// A terminal's cockpit mod names its repository by root; the page names a workspace by id.
|
|
5582
|
+
const w = typeof body.root === 'string' ? wsByRoot(body.root) : getWs(body.ws);
|
|
5583
|
+
if (!w) return json(res, { ok: false, error: 'no workspace for that repository' }, 400);
|
|
5584
|
+
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);
|
|
5585
|
+
const f = tsSettings.getRepo(STATE, w.root);
|
|
5586
|
+
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 },
|
|
5587
|
+
key: tsSettings.getKey(STATE), flags: { consent: f.consent, route: f.route, exclude: f.exclude }, askOpts: body.interactive === true ? jevRoute.INTERACTIVE : {} });
|
|
5588
|
+
if (!out.ok) return json(res, { ok: false, error: out.error }, 400);
|
|
5589
|
+
if (out.shadow) return json(res, { ok: true, shadow: true, id: out.id }); // shadow: logged, nothing shown, nothing applied
|
|
5590
|
+
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,
|
|
5591
|
+
applies: out.applies, sent: out.sent, withheld: out.withheld ? out.withheld.why : null, gate: out.gate });
|
|
5592
|
+
}
|
|
5593
|
+
if (p === '/api/model/route/outcome' && req.method === 'POST') {
|
|
5594
|
+
let body = {}; try { body = JSON.parse((await readBody(req)) || '{}'); } catch { return json(res, { ok: false, error: 'invalid JSON body' }, 400); }
|
|
5595
|
+
const w = getWs(body.ws); if (!w) return json(res, { ok: false, error: 'no workspace' }, 400);
|
|
5596
|
+
const mc = modelLib.core(); if (!mc.ok) return json(res, { ok: false, error: mc.error }, 500);
|
|
5597
|
+
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);
|
|
5598
|
+
return json(res, { ok: true });
|
|
5599
|
+
}
|
|
5430
5600
|
if (p === '/api/session/model' && req.method === 'POST') {
|
|
5431
5601
|
// D75 — switch the model of a RUNNING Claude session, on the PM's press only. It types the harness's own
|
|
5432
5602
|
// `/model <model>` command into the pane; effort is not changed by it. Never automatic, never for a binary
|
|
@@ -5740,7 +5910,12 @@ async function handleRequest(req, res) {
|
|
|
5740
5910
|
try { nextStepActions.annotate(nextSteps, (it) => nextStepPathOf(root, it)); attachRuns(nextSteps, id); } catch (e) { void e; }
|
|
5741
5911
|
let lcHistory = [];
|
|
5742
5912
|
try { lcHistory = lifecycleHistory(w, id); } catch { lcHistory = []; }
|
|
5743
|
-
|
|
5913
|
+
// D83 — what Jev suggests about the documents tied to this initiative, read from the claims ledger. Advisory: the
|
|
5914
|
+
// panel shows it beside the ladder and nothing derives a step, a stage or a count from it.
|
|
5915
|
+
let suggestions = null;
|
|
5916
|
+
try { if (w) suggestions = jevClaims.suggestionsFor({ ledgerFile: claimsPaths(w).ledgerFile, initiativeId: id }); }
|
|
5917
|
+
catch { suggestions = { ok: false, reason: 'the suggestions could not be read', items: [] }; }
|
|
5918
|
+
return json(res, { ws: w ? w.id : null, ...out, lanes, conversations, files, next_steps: nextSteps, lifecycle_history: lcHistory, suggestions });
|
|
5744
5919
|
}
|
|
5745
5920
|
if (p === '/api/audit' && req.method === 'GET') {
|
|
5746
5921
|
/* The workflow audit (D77): the sweep run on one repository, read-only. It reports state that contradicts
|
|
@@ -6231,6 +6406,62 @@ async function handleRequest(req, res) {
|
|
|
6231
6406
|
if (!out.ok) return json(res, { ok: false, error: out.error }, 500);
|
|
6232
6407
|
return json(res, { ok: true, ws: w.id, watching: tsSettings.getRepo(STATE, w.root).watch === true });
|
|
6233
6408
|
}
|
|
6409
|
+
// ── jev-doc-categorizer Run 3 (C7) — the PM corrects a category, and may write it into the file ─────────────────────
|
|
6410
|
+
// A correction is per-user state OUTSIDE the repository and never edits a file. The write-back is a separate press with
|
|
6411
|
+
// two steps — the diff first, then a confirmed write that names the sha1 of the text the PM saw — and it is the ONLY
|
|
6412
|
+
// write into the repository on this surface.
|
|
6413
|
+
if (p === '/api/doccat/corrections' && req.method === 'GET') {
|
|
6414
|
+
const w = getWs(url.searchParams.get('ws'));
|
|
6415
|
+
if (!w) return json(res, { ok: false, error: 'no workspace' }, 400);
|
|
6416
|
+
return json(res, { ok: true, ws: w.id, corrections: docCatCorrections.get(doccatDir(w.root)) });
|
|
6417
|
+
}
|
|
6418
|
+
if ((p === '/api/doccat/correct' || p === '/api/doccat/writeback') && req.method === 'POST') {
|
|
6419
|
+
let body = {};
|
|
6420
|
+
try { body = JSON.parse((await readBody(req)) || '{}'); }
|
|
6421
|
+
catch { return json(res, { ok: false, error: 'invalid JSON body' }, 400); }
|
|
6422
|
+
const w = getWs(body.ws);
|
|
6423
|
+
if (!w) return json(res, { ok: false, error: 'no workspace' }, 400);
|
|
6424
|
+
// Path guard: the store's own check (relative, no `..`, Markdown only), then the same inside-the-workspace rule every
|
|
6425
|
+
// read and write on this surface uses, then the real path, so a symlink cannot lead out of the repository.
|
|
6426
|
+
const chk = docCatCorrections.checkRel(body.path);
|
|
6427
|
+
if (chk.error) return json(res, { ok: false, error: chk.error }, 400);
|
|
6428
|
+
const rel = chk.rel;
|
|
6429
|
+
const full = path.resolve(w.root, ...rel.split('/'));
|
|
6430
|
+
if (!insideRoot(w.root, full)) return json(res, { ok: false, error: 'outside workspace' }, 404);
|
|
6431
|
+
let lst = null; try { lst = fs.lstatSync(full); } catch { /* reported below */ }
|
|
6432
|
+
if (!lst) return json(res, { ok: false, error: 'There is no such document in this repository.' }, 404);
|
|
6433
|
+
let real = null; try { real = fs.realpathSync(full); } catch { /* reported below */ }
|
|
6434
|
+
let realRoot = w.root; try { realRoot = fs.realpathSync(w.root); } catch { /* keep the root as given */ }
|
|
6435
|
+
if (!real || !insideRoot(realRoot, real)) return json(res, { ok: false, error: 'outside workspace' }, 404);
|
|
6436
|
+
const dir = doccatDir(w.root);
|
|
6437
|
+
if (p === '/api/doccat/correct') {
|
|
6438
|
+
const out = body.clear === true
|
|
6439
|
+
? docCatCorrections.clear(dir, rel)
|
|
6440
|
+
: docCatCorrections.set(dir, rel, { category: body.category, ticket_kind: body.ticket_kind === undefined ? null : body.ticket_kind });
|
|
6441
|
+
if (!out.ok) return json(res, { ok: false, error: out.error }, /could not be saved|symlink/.test(out.error) ? 500 : 400);
|
|
6442
|
+
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) });
|
|
6443
|
+
}
|
|
6444
|
+
// the write-back
|
|
6445
|
+
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);
|
|
6446
|
+
let current;
|
|
6447
|
+
try { current = fs.readFileSync(full, 'utf8'); }
|
|
6448
|
+
catch { return json(res, { ok: false, error: 'could not read the document' }, 404); }
|
|
6449
|
+
const corr = { category: body.category, ticket_kind: body.ticket_kind === undefined ? null : body.ticket_kind };
|
|
6450
|
+
if (body.confirm !== true) {
|
|
6451
|
+
const plan = docCatWriteback.plan(current, corr);
|
|
6452
|
+
if (!plan.ok) return json(res, { ok: false, error: plan.error, sha: plan.sha }, 400);
|
|
6453
|
+
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 });
|
|
6454
|
+
}
|
|
6455
|
+
const done = docCatWriteback.apply(current, corr, body.expect_sha, body.expect_next_sha);
|
|
6456
|
+
if (!done.ok) return json(res, { ok: false, error: done.error, stale: !!done.stale, sha: done.sha }, done.stale ? 409 : 400);
|
|
6457
|
+
if (done.unchanged) return json(res, { ok: true, ws: w.id, path: rel, written: false, unchanged: true, diff: '' });
|
|
6458
|
+
try { fs.writeFileSync(full, done.next); }
|
|
6459
|
+
catch (e) {
|
|
6460
|
+
warnWriteFail('category write-back (' + rel + ')', e);
|
|
6461
|
+
return json(res, { ok: false, error: 'could not write the document — nothing was changed' }, 500);
|
|
6462
|
+
}
|
|
6463
|
+
return json(res, { ok: true, ws: w.id, path: rel, written: true, diff: done.diff, changes: done.changes, sha: docCatWriteback.sha1(done.next) });
|
|
6464
|
+
}
|
|
6234
6465
|
// ── D80 — Jev on changed claim pairs: the switch, a resume after a pause, and the backlog (plan, then confirmed run) ──
|
|
6235
6466
|
if (p === '/api/claims/jev' && req.method === 'POST') {
|
|
6236
6467
|
let body = {};
|
|
@@ -6243,6 +6474,18 @@ async function handleRequest(req, res) {
|
|
|
6243
6474
|
if (!out.ok) return json(res, { ok: false, error: out.error }, 500);
|
|
6244
6475
|
return json(res, { ok: true, ws: w.id, jev: jevGateInfo(w) });
|
|
6245
6476
|
}
|
|
6477
|
+
// D83 — the second switch: also read the documents tied to each initiative for the two status questions. Default off.
|
|
6478
|
+
if (p === '/api/claims/jev/status' && req.method === 'POST') {
|
|
6479
|
+
let body = {};
|
|
6480
|
+
try { body = JSON.parse((await readBody(req)) || '{}'); }
|
|
6481
|
+
catch { return json(res, { ok: false, error: 'invalid JSON body' }, 400); }
|
|
6482
|
+
const w = getWs(body.ws);
|
|
6483
|
+
if (!w) return json(res, { ok: false, error: 'no workspace' }, 400);
|
|
6484
|
+
if (typeof body.status_read !== 'boolean') return json(res, { ok: false, error: 'status_read must be true or false' }, 400);
|
|
6485
|
+
const out = tsSettings.setRepo(STATE, w.root, { status_read: body.status_read });
|
|
6486
|
+
if (!out.ok) return json(res, { ok: false, error: out.error }, 500);
|
|
6487
|
+
return json(res, { ok: true, ws: w.id, jev: jevGateInfo(w) });
|
|
6488
|
+
}
|
|
6246
6489
|
if (p === '/api/claims/jev/resume' && req.method === 'POST') {
|
|
6247
6490
|
let body = {};
|
|
6248
6491
|
try { body = JSON.parse((await readBody(req)) || '{}'); }
|
|
@@ -6262,14 +6505,61 @@ async function handleRequest(req, res) {
|
|
|
6262
6505
|
const g = jevClaims.gates({ key: tsSettings.status(STATE).present, consent: f.consent, watch: true, jev: f.jev });
|
|
6263
6506
|
if (!g.open) return json(res, { ok: false, error: jevClaims.gateSentence(g.missing) }, 409);
|
|
6264
6507
|
const { cacheDir, ledgerFile } = claimsPaths(w);
|
|
6265
|
-
if (body.confirm !== true) return json(res, { ok: true, ws: w.id, plan: jevClaims.backlogPlan({ root: w.root, cacheDir, exclude: f.exclude }) });
|
|
6508
|
+
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 }) });
|
|
6266
6509
|
if (claimsRunning.has(w.root)) return json(res, { ok: false, busy: true, error: 'a check is already running for this repository' }, 409);
|
|
6267
6510
|
claimsRunning.add(w.root);
|
|
6268
6511
|
try {
|
|
6269
|
-
const out = await jevClaims.runBacklog({ root: w.root, cacheDir, ledgerFile, key: tsSettings.getKey(STATE), flags: f, exclude: f.exclude, expectRequests: body.expect_requests });
|
|
6512
|
+
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) });
|
|
6270
6513
|
return json(res, { ws: w.id, ...out, jev: jevGateInfo(w), ...claimsLib.readView({ cacheDir, ledgerFile }) }, out.ok ? 200 : (out.conflict ? 409 : 500));
|
|
6271
6514
|
} finally { claimsRunning.delete(w.root); }
|
|
6272
6515
|
}
|
|
6516
|
+
// ── D82 — Jev on documents the local rules leave undetermined: the switch, a resume, and the run (plan, then confirmed) ──
|
|
6517
|
+
if (p === '/api/doccat/jev' && req.method === 'GET') {
|
|
6518
|
+
const w = getWs(url.searchParams.get('ws'));
|
|
6519
|
+
if (!w) return json(res, { ok: false, error: 'no workspace' }, 400);
|
|
6520
|
+
return json(res, { ok: true, ws: w.id, doccat: doccatInfo(w) });
|
|
6521
|
+
}
|
|
6522
|
+
if (p === '/api/doccat/jev' && req.method === 'POST') {
|
|
6523
|
+
let body = {};
|
|
6524
|
+
try { body = JSON.parse((await readBody(req)) || '{}'); }
|
|
6525
|
+
catch { return json(res, { ok: false, error: 'invalid JSON body' }, 400); }
|
|
6526
|
+
const w = getWs(body.ws);
|
|
6527
|
+
if (!w) return json(res, { ok: false, error: 'no workspace' }, 400);
|
|
6528
|
+
if (typeof body.categorize !== 'boolean') return json(res, { ok: false, error: 'categorize must be true or false' }, 400);
|
|
6529
|
+
const out = tsSettings.setRepo(STATE, w.root, { categorize: body.categorize });
|
|
6530
|
+
if (!out.ok) return json(res, { ok: false, error: out.error }, 500);
|
|
6531
|
+
return json(res, { ok: true, ws: w.id, doccat: doccatInfo(w) });
|
|
6532
|
+
}
|
|
6533
|
+
if (p === '/api/doccat/jev/resume' && req.method === 'POST') {
|
|
6534
|
+
let body = {};
|
|
6535
|
+
try { body = JSON.parse((await readBody(req)) || '{}'); }
|
|
6536
|
+
catch { return json(res, { ok: false, error: 'invalid JSON body' }, 400); }
|
|
6537
|
+
const w = getWs(body.ws);
|
|
6538
|
+
if (!w) return json(res, { ok: false, error: 'no workspace' }, 400);
|
|
6539
|
+
jevCategorize.resume(doccatDir(w.root));
|
|
6540
|
+
return json(res, { ok: true, ws: w.id, doccat: doccatInfo(w) });
|
|
6541
|
+
}
|
|
6542
|
+
if (p === '/api/doccat/jev/run' && req.method === 'POST') {
|
|
6543
|
+
let body = {};
|
|
6544
|
+
try { body = JSON.parse((await readBody(req)) || '{}'); }
|
|
6545
|
+
catch { return json(res, { ok: false, error: 'invalid JSON body' }, 400); }
|
|
6546
|
+
const w = getWs(body.ws);
|
|
6547
|
+
if (!w) return json(res, { ok: false, error: 'no workspace' }, 400);
|
|
6548
|
+
const f = tsSettings.getRepo(STATE, w.root);
|
|
6549
|
+
const g = jevCategorize.gates({ key: tsSettings.status(STATE).present, consent: f.consent, categorize: f.categorize });
|
|
6550
|
+
if (!g.open) return json(res, { ok: false, error: jevCategorize.gateSentence(g.missing), doccat: doccatInfo(w) }, 409);
|
|
6551
|
+
const cacheDir = doccatDir(w.root);
|
|
6552
|
+
const docs = doccatDocs(w.root, library(w.root));
|
|
6553
|
+
const named = namedRecords(w.root);
|
|
6554
|
+
if (body.confirm !== true) return json(res, { ok: true, ws: w.id, plan: jevCategorize.plan({ docs, named, cacheDir, exclude: f.exclude }) });
|
|
6555
|
+
if (doccatRunning.has(w.root)) return json(res, { ok: false, busy: true, error: 'Jev is already categorizing this repository' }, 409);
|
|
6556
|
+
doccatRunning.add(w.root);
|
|
6557
|
+
try {
|
|
6558
|
+
const out = await jevCategorize.runConfirmed({ docs, named, cacheDir, key: tsSettings.getKey(STATE), flags: f, exclude: f.exclude, expectRequests: body.expect_requests,
|
|
6559
|
+
expectFingerprint: typeof body.expect_fingerprint === 'string' ? body.expect_fingerprint : null });
|
|
6560
|
+
return json(res, { ws: w.id, ...out, doccat: doccatInfo(w) }, out.ok ? 200 : (out.conflict || out.busy ? 409 : 500));
|
|
6561
|
+
} finally { doccatRunning.delete(w.root); }
|
|
6562
|
+
}
|
|
6273
6563
|
// ── D79 — repository validation through TypeSafe ──────────────────────────────────────────────────
|
|
6274
6564
|
// The ONLY routes that lead to an outbound request, and only through lib/typesafe.js. The key is never
|
|
6275
6565
|
// in a response: `status` says present or absent. Nothing here edits a repository file.
|
|
@@ -6300,6 +6590,58 @@ async function handleRequest(req, res) {
|
|
|
6300
6590
|
}
|
|
6301
6591
|
return json(res, { ok: true, ws: w.id, name: w.name, ...tsSettings.getRepo(STATE, w.root), key: tsSettings.status(STATE) });
|
|
6302
6592
|
}
|
|
6593
|
+
/* ── live notes (jev-live-notes, Run 2): the ranked proposals of a conversation, and the PM's accept, change or reject.
|
|
6594
|
+
* Reads the repository's ledger; the writes are ledger rows and, on close, the day's note. No request leaves this process.
|
|
6595
|
+
* The page is the cockpit's live-notes view (public/pm/view-livenotes.js, Run 6). ── */
|
|
6596
|
+
if (p === '/api/live-notes' && req.method === 'GET') {
|
|
6597
|
+
const wsq = url.searchParams.get('ws');
|
|
6598
|
+
const w = wsq ? workspaces.get(wsq) : homeWorkspace(); // a named workspace the server does not know is refused, never replaced by the home one
|
|
6599
|
+
if (!w) return json(res, { ok: false, error: 'no such workspace' }, 400);
|
|
6600
|
+
const r = liveNotes.apiList({ root: w.root, session: url.searchParams.get('session') || null, exclude: tsSettings.getRepo(STATE, w.root).exclude });
|
|
6601
|
+
if (r.ok) r.jev = liveJevInfo(w);
|
|
6602
|
+
return json(res, r, r.ok ? 200 : (r.status || 400));
|
|
6603
|
+
}
|
|
6604
|
+
if (p === '/api/live-notes/decide' && req.method === 'POST') {
|
|
6605
|
+
let body = {};
|
|
6606
|
+
try { body = JSON.parse((await readBody(req)) || '{}'); }
|
|
6607
|
+
catch { return json(res, { ok: false, error: 'invalid JSON body' }, 400); }
|
|
6608
|
+
const w = body.ws ? workspaces.get(String(body.ws)) : homeWorkspace();
|
|
6609
|
+
if (!w) return json(res, { ok: false, error: 'no such workspace' }, 400);
|
|
6610
|
+
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, question: body.question == null ? null : body.question, exclude: tsSettings.getRepo(STATE, w.root).exclude });
|
|
6611
|
+
return json(res, r, r.ok ? 200 : (r.status || 400));
|
|
6612
|
+
}
|
|
6613
|
+
/* Run 7 (D85): the per-repository switch "live notes may use Jev". Default off; it is one of three gates, with the key and consent. */
|
|
6614
|
+
if (p === '/api/live-notes/switch' && req.method === 'POST') {
|
|
6615
|
+
let body = {};
|
|
6616
|
+
try { body = JSON.parse((await readBody(req)) || '{}'); }
|
|
6617
|
+
catch { return json(res, { ok: false, error: 'invalid JSON body' }, 400); }
|
|
6618
|
+
const w = body.ws ? workspaces.get(String(body.ws)) : homeWorkspace();
|
|
6619
|
+
if (!w) return json(res, { ok: false, error: 'no such workspace' }, 400);
|
|
6620
|
+
if (typeof body.live !== 'boolean') return json(res, { ok: false, error: 'say live: true or live: false' }, 400);
|
|
6621
|
+
liveNotes.setSwitch(STATE, w.root, { live: body.live });
|
|
6622
|
+
return json(res, { ok: true, ws: w.id, jev: liveJevInfo(w) });
|
|
6623
|
+
}
|
|
6624
|
+
/* Run 6: the live-notes view's writes — start or resume a session, add points, close. A proposal is a local word match made in
|
|
6625
|
+
* this process, unless all three D85 gates hold: then `add` hands each cleaned point to Jev through lib/jev-live.js. */
|
|
6626
|
+
if ((p === '/api/live-notes/session' || p === '/api/live-notes/add' || p === '/api/live-notes/close' || p === '/api/live-notes/private') && req.method === 'POST') {
|
|
6627
|
+
let body = {};
|
|
6628
|
+
try { body = JSON.parse((await readBody(req)) || '{}'); }
|
|
6629
|
+
catch { return json(res, { ok: false, error: 'invalid JSON body' }, 400); }
|
|
6630
|
+
const w = body.ws ? workspaces.get(String(body.ws)) : homeWorkspace();
|
|
6631
|
+
if (!w) return json(res, { ok: false, error: 'no such workspace' }, 400);
|
|
6632
|
+
const exclude = tsSettings.getRepo(STATE, w.root).exclude;
|
|
6633
|
+
let r;
|
|
6634
|
+
if (p === '/api/live-notes/session') r = liveNotes.apiOpen({ root: w.root, runtimeDir: STATE, session: body.session || null, anchor: body.anchor || null, exclude });
|
|
6635
|
+
else if (p === '/api/live-notes/add') {
|
|
6636
|
+
const snd = liveSender(w);
|
|
6637
|
+
r = await liveNotes.apiAdd({ root: w.root, runtimeDir: STATE, session: body.session, text: body.text, source: body.source === 'speech' ? 'speech' : 'typed', exclude,
|
|
6638
|
+
key: snd.key, flags: snd.flags, ask: snd.ask, askOpts: snd.askOpts });
|
|
6639
|
+
}
|
|
6640
|
+
else if (p === '/api/live-notes/private') r = liveNotes.apiMarkPrivate({ root: w.root, session: body.session });
|
|
6641
|
+
else r = liveNotes.apiClose({ root: w.root, runtimeDir: STATE, session: body.session, retentionDays: liveNotes.getSwitch(STATE, w.root).retention_days, exclude });
|
|
6642
|
+
if (r.ok && p !== '/api/live-notes/close') { r.jev = liveJevInfo(w); if (r.note) r.note = r.jev.note; }
|
|
6643
|
+
return json(res, r, r.ok ? 200 : (r.status || 400));
|
|
6644
|
+
}
|
|
6303
6645
|
if (p === '/api/validate/plan' && req.method === 'POST') {
|
|
6304
6646
|
let body = {};
|
|
6305
6647
|
try { body = JSON.parse((await readBody(req)) || '{}'); }
|
|
@@ -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
|