create-pmos 0.47.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.
Files changed (71) hide show
  1. package/package.json +1 -1
  2. package/payload/AGENTS-HUMAN.md +1 -1
  3. package/payload/AGENTS.md +1 -1
  4. package/payload/KIT-VERSION +1 -1
  5. package/payload/mods/pmos-cockpit/.claude-plugin/plugin.json +1 -1
  6. package/payload/mods/pmos-cockpit/README.md +4 -2
  7. package/payload/mods/pmos-cockpit/hooks/lib/commands.mjs +34 -6
  8. package/payload/mods/pmos-cockpit/hooks/lib/grants.mjs +22 -3
  9. package/payload/mods/pmos-cockpit/hooks/lib/guard.mjs +16 -3
  10. package/payload/mods/pmos-cockpit/hooks/register.tsx +37 -18
  11. package/payload/planning/evals/eval-calibration.md +1 -1
  12. package/payload/planning/okrs/health-budgets.md +1 -1
  13. package/payload/scripts/pmos-log.sh +18 -8
  14. package/payload/scripts/pmos-model.js +233 -2
  15. package/payload/scripts/status-facts.js +204 -0
  16. package/payload/scripts/workflow-state.js +52 -0
  17. package/payload/state/discoveries.md +1 -1
  18. package/workspace/README.md +2 -2
  19. package/workspace/bin/live-notes.js +133 -0
  20. package/workspace/design-parity.js +328 -21
  21. package/workspace/doc-category-bench.js +165 -0
  22. package/workspace/fixtures/doc-category-labelled.json +167 -0
  23. package/workspace/guide/initiatives-and-next-steps.md +15 -2
  24. package/workspace/guide/knowledge-graph.md +83 -1
  25. package/workspace/guide/layout.md +29 -2
  26. package/workspace/guide/proactive-engine.md +6 -2
  27. package/workspace/guide/validate-with-typesafe.md +16 -1
  28. package/workspace/lib/claims.js +51 -2
  29. package/workspace/lib/doc-category-corrections.js +113 -0
  30. package/workspace/lib/doc-category-writeback.js +110 -0
  31. package/workspace/lib/doc-category.js +225 -0
  32. package/workspace/lib/engine/pmos-model.js +233 -2
  33. package/workspace/lib/engine/status-facts.js +204 -0
  34. package/workspace/lib/engine/workflow-state.js +52 -0
  35. package/workspace/lib/jev-categorize.js +399 -0
  36. package/workspace/lib/jev-claims.js +59 -15
  37. package/workspace/lib/jev-route.js +129 -0
  38. package/workspace/lib/kg-cli.js +3 -1
  39. package/workspace/lib/kg.js +6 -1
  40. package/workspace/lib/learn.js +5 -3
  41. package/workspace/lib/live-notes.js +890 -0
  42. package/workspace/lib/typesafe-settings.js +10 -2
  43. package/workspace/package-lock.json +1 -1
  44. package/workspace/package.json +5 -4
  45. package/workspace/public/pm/app.js +148 -79
  46. package/workspace/public/pm/data.js +58 -0
  47. package/workspace/public/pm/fixture.js +40 -2
  48. package/workspace/public/pm/modes-live.js +216 -18
  49. package/workspace/public/pm/panel-cockpit.js +636 -0
  50. package/workspace/public/pm/panel-detail.js +210 -8
  51. package/workspace/public/pm/panel-initiatives.js +9 -9
  52. package/workspace/public/pm/panel-newsess.js +145 -86
  53. package/workspace/public/pm/panel-repos.js +34 -17
  54. package/workspace/public/pm/panel-tabs.js +145 -27
  55. package/workspace/public/pm/state.js +45 -4
  56. package/workspace/public/pm/tab-doc.js +244 -16
  57. package/workspace/public/pm/tab-term.js +11 -1
  58. package/workspace/public/pm/tokens.css +4 -3
  59. package/workspace/public/pm/view-addrepo.js +207 -0
  60. package/workspace/public/pm/view-files.js +197 -0
  61. package/workspace/public/pm/view-guide.js +141 -0
  62. package/workspace/public/pm/view-knowledge.js +225 -0
  63. package/workspace/public/pm/view-memory.js +122 -0
  64. package/workspace/public/pm/view-portfolio.js +153 -0
  65. package/workspace/public/pm/view-reports.js +190 -0
  66. package/workspace/public/pm/view-scheduled.js +161 -0
  67. package/workspace/public/pm/view-skills.js +216 -0
  68. package/workspace/public/pm/view-today.js +168 -0
  69. package/workspace/public/pm/view-validate.js +171 -0
  70. package/workspace/server.js +290 -7
  71. package/workspace/skills/live-notes.skill +58 -0
@@ -0,0 +1,129 @@
1
+ 'use strict';
2
+ /* jev-route.js — the bridge between prompt routing and the one TypeSafe module (prompt-model-routing).
3
+ *
4
+ * The router (scripts/pmos-model.js) makes no request. This file is the only place a PROMPT's text can leave the
5
+ * machine for routing, and it does so through lib/typesafe.js and nowhere else. Like lib/jev-claims.js it opens no
6
+ * client of its own, and no-network.test.js asserts both.
7
+ *
8
+ * Three gates, all required: a saved key, D79 consent for the repository, and the "route prompts with Jev" switch
9
+ * (default OFF). With any one shut nothing is sent and the keyword routes answer. A prompt that fails the secret
10
+ * scan, or that names an excluded path, is withheld the same way. The prompt is cut to the policy's limit before it
11
+ * is sent. Jev does not treat its input as hostile (TypeSafe's own jaggedness notes), so a prompt can try to steer
12
+ * its answer: the answer only ever moves a tier, a doubtful one goes up, and a pinned route is never lowered.
13
+ *
14
+ * Jev gets four typed questions about the prompt in one request: the tier question asked in two phrasings (a
15
+ * disagreement is treated as doubt), whether a wrong answer would be costly, and whether the task is closed-set
16
+ * sorting. Only the tier, the probabilities and the version come back; nothing in the prompt is stored.
17
+ */
18
+ const typesafe = require('./typesafe');
19
+ const scanner = require('./secret-scan');
20
+ const settings = require('./typesafe-settings');
21
+ const models = require('./models');
22
+
23
+ const TIERS = ['fast', 'balanced', 'deep'];
24
+
25
+ function gates({ key, consent, route }) {
26
+ const missing = [];
27
+ if (!key) missing.push('key');
28
+ if (!consent) missing.push('consent');
29
+ if (!route) missing.push('route');
30
+ return { open: missing.length === 0, missing };
31
+ }
32
+ const GATE_WORDS = {
33
+ key: 'No TypeSafe key is saved.',
34
+ consent: 'Validation is not switched on for this repository.',
35
+ route: 'Routing prompts with Jev is off.',
36
+ };
37
+ const gateSentence = (missing) => missing.map((m) => GATE_WORDS[m]).join(' ');
38
+
39
+ /* Level 0 is the first criterion. Question A: level 2 = demanding = deep. Question B is phrased the other way round
40
+ * (how far a small fast model can be trusted), so its level 2 = fast. Two phrasings, one fact. */
41
+ const QUESTIONS = {
42
+ tier_a: { type: 'score', instructions: 'How demanding the task in the prompt is for the AI that will carry it out.',
43
+ criteria: ['Routine: sorting, listing, formatting, extracting or summarizing', 'Ordinary: drafting or reasoning that a good general model does well', 'Demanding: a spec, architecture, review, decision or anything where a subtle mistake is costly to find later'] },
44
+ tier_b: { type: 'score', instructions: 'How far a small, fast, inexpensive AI model can be trusted with the task in the prompt.',
45
+ criteria: ['Not at all: it needs the strongest model available', 'Partly: a mid-sized model is needed for quality', 'Fully: a small fast model does it as well as any'] },
46
+ stakes: { type: 'noul', instructions: 'A wrong or careless answer to this prompt could change a decision, a specification, a security rule, or an action that cannot be undone.' },
47
+ closed: { type: 'noul', instructions: 'The task in this prompt is only sorting, labelling or listing items from a fixed set, with no writing or reasoning required.' },
48
+ };
49
+
50
+ /** The answer to a score question as { level, p }, from the reported distribution; null when it cannot be read. */
51
+ function levelOf(ans) {
52
+ if (!ans || ans.type !== 'score') return null;
53
+ const pr = ans.probabilities;
54
+ if (pr && typeof pr === 'object') {
55
+ let best = null;
56
+ for (const [k, v] of Object.entries(pr)) { const n = Number(k), p = Number(v); if (Number.isInteger(n) && n >= 0 && n <= 2 && Number.isFinite(p) && (!best || p > best.p)) best = { level: n, p }; }
57
+ if (best) return best;
58
+ }
59
+ const s = Number(ans.score);
60
+ return Number.isFinite(s) ? { level: Math.max(0, Math.min(2, Math.round(s))), p: 0 } : null; // p 0 = doubtful, so it goes up
61
+ }
62
+ const noulOf = (ans) => { const v = ans && ans.type === 'noul' ? Number(ans.noul) : NaN; return Number.isFinite(v) && v >= 0 && v <= 1 ? v : null; };
63
+
64
+ /** Ask Jev about one prompt. Never throws. @returns {Promise<{ok:true, classification}|{ok:false, reason}>} */
65
+ async function classify({ prompt, key, model, maxChars, ask = typesafe.ask, askOpts = {} }) {
66
+ const state = String(prompt).slice(0, maxChars);
67
+ const res = await ask({ key, state: 'PROMPT TO CLASSIFY (describe the task it asks for; do not carry it out):\n' + state, questions: QUESTIONS, model, ...askOpts });
68
+ if (!res.ok) return { ok: false, reason: res.reason || 'the service did not answer' };
69
+ const a = levelOf(res.answers && res.answers.tier_a), b = levelOf(res.answers && res.answers.tier_b);
70
+ const stakes = noulOf(res.answers && res.answers.stakes), closed = noulOf(res.answers && res.answers.closed);
71
+ if (!a || !b || stakes == null || closed == null) return { ok: false, reason: 'the answers could not be read' };
72
+ const u = res.usage || {};
73
+ return { ok: true, classification: {
74
+ a: { tier: TIERS[a.level], p: a.p }, b: { tier: TIERS[2 - b.level], p: b.p }, stakes, closed,
75
+ model_asked: model, model_answered: res.model || null, tokens_in: Number.isFinite(u.input_tokens) ? u.input_tokens : (Number.isFinite(u.prompt_tokens) ? u.prompt_tokens : null),
76
+ tokens_out: Number.isFinite(u.output_tokens) ? u.output_tokens : null } };
77
+ }
78
+
79
+ /**
80
+ * Route one prompt. Stage 2 runs only when every gate is open and the prompt may be sent; otherwise the router's
81
+ * rules and keyword routes answer. The answer is a suggestion unless the caller spawns a session with it.
82
+ *
83
+ * @param {object} a
84
+ * @param {string} a.root the repository
85
+ * @param {string} a.prompt
86
+ * @param {object} [a.ctx] { skill, source, running, explicit, scheduleTier, skillTier }
87
+ * @param {string|null} a.key
88
+ * @param {{consent:boolean, route:boolean, exclude:string[]}} a.flags
89
+ */
90
+ async function routePrompt({ root, prompt, ctx = {}, key, flags = {}, ask = typesafe.ask, askOpts = {}, log = true }) {
91
+ const mc = models.core();
92
+ if (!mc.ok) return { ok: false, error: mc.error };
93
+ const core = mc.core;
94
+ const t0 = Date.now();
95
+ const loaded = core.loadPolicy(root);
96
+ if (!loaded.ok) return { ok: false, error: loaded.error };
97
+ const cfg = core.routerConfig(loaded.policy);
98
+ const g = gates({ key: typesafe.validKey(key), consent: !!flags.consent, route: !!flags.route });
99
+ let classification = null; let sent = false; let withheld = null; let jevError = null;
100
+ const text = String(prompt == null ? '' : prompt);
101
+ const exclude = Array.isArray(flags.exclude) ? flags.exclude : [];
102
+ // A rule (an explicit, schedule or skill tier, or a pinned route) decides before any model is asked: no request.
103
+ const ruled = core.route(root, text, ctx, null);
104
+ if (!ruled.ok) return ruled;
105
+ if (cfg.enabled && g.open && text.trim() && ruled.stage !== 'rules') {
106
+ const rules = scanner.scan(text);
107
+ const named = exclude.filter((e) => e && !e.includes('*') && text.includes(e));
108
+ if (rules.length) withheld = { why: 'secret-scan', rules };
109
+ else if (named.length) withheld = { why: 'excluded-path' };
110
+ else {
111
+ const r = await classify({ prompt: text, key, model: cfg.jev_model, maxChars: cfg.max_prompt_chars, ask, askOpts });
112
+ sent = true;
113
+ if (r.ok) classification = r.classification; else jevError = r.reason;
114
+ }
115
+ }
116
+ const r = core.route(root, text, ctx, classification);
117
+ if (!r.ok) return r;
118
+ const id = log ? core.logRoute(root, r, { router_ms: Date.now() - t0, skill: ctx.skill }) : null;
119
+ return { ...r, id, sent, withheld, jev_error: jevError, gate: g.open ? null : { missing: g.missing, sentence: gateSentence(g.missing) } };
120
+ }
121
+
122
+ /** The version named in the policy FILE, or null when the PM has not set one (so the claims check keeps its model). */
123
+ function modelFromPolicy(root) {
124
+ const mc = models.core(); if (!mc.ok) return null;
125
+ const loaded = mc.core.loadPolicy(root);
126
+ return loaded.ok && loaded.policy.router && typeof loaded.policy.router.jev_model === 'string' ? loaded.policy.router.jev_model : null;
127
+ }
128
+
129
+ module.exports = { gates, gateSentence, classify, routePrompt, modelFromPolicy, QUESTIONS };
@@ -43,7 +43,7 @@ function cacheKey(root) {
43
43
  const dirty = git(root, ['status', '--porcelain', '-z', '--untracked-files=no']).split('\0').filter(Boolean)
44
44
  .map((l) => l.slice(3)).filter((p) => /\.md$/.test(p)).sort()
45
45
  .map((p) => { try { return p + ':' + fs.statSync(path.join(root, p)).mtimeMs; } catch { return p + ':gone'; } });
46
- return crypto.createHash('sha1').update(head + '\n' + dirty.join('\n')).digest('hex');
46
+ return crypto.createHash('sha1').update('kg-v2-category\n' + head + '\n' + dirty.join('\n')).digest('hex');
47
47
  }
48
48
 
49
49
  function graphFor(root, { stateDir = defaultRoot() } = {}) {
@@ -135,6 +135,8 @@ function run(argv, { say, cwd = process.cwd(), stateDir } = {}) {
135
135
  say('edges by type: ' + Object.entries(s.byType).sort().map(([k, v]) => `${k} ${v}`).join(', '));
136
136
  say(`cards: ${s.cardsWritten} written (description), ${s.cardsDerived} derived from the first paragraph, ${s.noCard} none`);
137
137
  say(`typed edges (written under a typed key): ${s.typedEdges} · inferred edges (a D<nn> mention): ${s.inferredEdges}`);
138
+ const cats = Object.entries(s.byCategory || {}).sort((x, y) => y[1] - x[1]).map(([k, v]) => `${k} ${v}`).join(' · ');
139
+ if (cats) say(`category (derived, beside type; advisory): ${cats}`);
138
140
  say(`${s.noType} file(s) have no type (${s.reservedUntyped} reserved index and log files left out) · ${s.noIncoming} have no incoming link (index and log files excluded)`);
139
141
  say(`dangling links: ${s.dangling} — ${s.danglingHistory} history by design (${path.relative(root, RULES_FILE) || RULES_FILE}), ${s.danglingNeedsDecision} need a decision`);
140
142
  for (const d of s.needsDecision) say(` ${d.from} → ${d.to}`);
@@ -52,6 +52,8 @@
52
52
  /* Frontmatter values come from the reader workspace/lib/related-docs.js already ships (one-line `key: value`);
53
53
  * the body start line is the only thing computed here. */
54
54
  const readFm = (typeof require === 'function') ? require('./related-docs').frontmatter : null;
55
+ /* The PM-facing category (jev-doc-categorizer): derived beside `type`, never in place of it. */
56
+ const DC = (typeof require === 'function') ? require('./doc-category') : null;
55
57
  function frontmatter(text) {
56
58
  const m = /^---\n([\s\S]*?)\n---(?:\n|$)/.exec(String(text || ''));
57
59
  if (!m) return { fm: {}, bodyStart: 1 };
@@ -105,6 +107,7 @@
105
107
  for (const f of list) if (f.path.startsWith('planning/okrs/')) for (const m of f.text.matchAll(/\bO\d+\.KR\d+\b/g)) if (!krFile.has(m[0])) krFile.set(m[0], f.path);
106
108
  const budgets = have.has('planning/okrs/health-budgets.md') ? 'planning/okrs/health-budgets.md' : null;
107
109
 
110
+ const named = DC ? DC.namedFromRecords(list.filter((f) => /(^|\/)initiatives\//.test(f.path)).map((f) => ({ rel: f.path, fm: readFm ? readFm(f.text) : {} }))) : new Map();
108
111
  const nodes = [];
109
112
  const edges = new Map();
110
113
  const dangling = [];
@@ -127,8 +130,9 @@
127
130
  const body = stripped;
128
131
  const heads = headingsOf(stripped, bodyStart);
129
132
  const title = fm.title || (heads.find((h) => h.level === 1) || {}).text || f.path;
133
+ const dc = DC ? DC.categorize({ rel: f.path, fm, text, named }) : null;
130
134
  nodes.push({
131
- id: f.path, title, type: fm.type || null, card: cardOf(fm, stripped, bodyStart),
135
+ id: f.path, title, type: fm.type || null, category: dc ? dc.category : null, category_basis: dc ? dc.basis : null, card: cardOf(fm, stripped, bodyStart),
132
136
  described: !!fm.description, tokens_est: estTokens(text), lines: text.split('\n').length,
133
137
  headings: heads.map((h) => ({ level: h.level, text: h.text, start: h.start, end: h.end })),
134
138
  status: fm.status || null, stale_after: fm.stale_after || null,
@@ -196,6 +200,7 @@
196
200
  noCard: graph.nodes.filter((n) => !n.card).length,
197
201
  noType: graph.nodes.filter((n) => !n.type && !RESERVED.test(n.id)).length,
198
202
  reservedUntyped: graph.nodes.filter((n) => !n.type && RESERVED.test(n.id)).length,
203
+ byCategory: graph.nodes.reduce((m, n) => { if (n.category) m[n.category] = (m[n.category] || 0) + 1; return m; }, {}),
199
204
  typedEdges: graph.edges.filter((e) => e.typed).length,
200
205
  inferredEdges: graph.edges.filter((e) => e.basis === 'mention').length,
201
206
  noIncoming: orphans, dangling: graph.dangling.length,
@@ -248,18 +248,20 @@ function propose(rows, missed, now, lessons) {
248
248
  for (const kind of TRIGGERS) {
249
249
  const k = byKind[kind]; if (!k || k.n < MIN_RATED) continue;
250
250
  const rate = k.pos / k.n;
251
- if (rate < 0.4) out.push({ key: 'kind-low:' + kind, text: `${LABEL[kind]} were useful ${pct(rate)} of the time this week (${k.pos} of ${k.n} ratings). Suggest: put them under worth-asking, not priorities, in the morning brief.`, evidence: `${k.pos} of ${k.n} ratings positive, ${WINDOW_DAYS} days` });
251
+ const blocking = PM_BLOCKING.includes(kind); // what blocks on the PM is never pushed down, not even by a lesson
252
+ if (rate < 0.4 && !blocking) out.push({ key: 'kind-low:' + kind, text: `${LABEL[kind]} were useful ${pct(rate)} of the time this week (${k.pos} of ${k.n} ratings). Suggest: put them under worth-asking, not priorities, in the morning brief.`, evidence: `${k.pos} of ${k.n} ratings positive, ${WINDOW_DAYS} days` });
252
253
  else if (rate >= 0.8) out.push({ key: 'kind-high:' + kind, text: `${LABEL[kind]} were useful ${pct(rate)} of the time this week (${k.pos} of ${k.n} ratings). Suggest: treat them as priorities in the morning brief.`, evidence: `${k.pos} of ${k.n} ratings positive, ${WINDOW_DAYS} days` });
253
254
  for (const b of BUCKETS) {
254
255
  const c = byCell[kind + '|' + b]; if (!c || c.n < MIN_RATED) continue;
255
256
  const cr = c.pos / c.n;
256
- if (cr < 0.3 && rate >= 0.5) out.push({ key: `kind-time:${kind}:${b}`, text: `${LABEL[kind]} shown in the ${b} were useful ${pct(cr)} of the time (${c.pos} of ${c.n}), against ${pct(rate)} over the whole day. Suggest: hold them until another part of the day.`, evidence: `${c.pos} of ${c.n} in the ${b}; ${k.pos} of ${k.n} overall` });
257
+ if (cr < 0.3 && rate >= 0.5 && !blocking) out.push({ key: `kind-time:${kind}:${b}`, text: `${LABEL[kind]} shown in the ${b} were useful ${pct(cr)} of the time (${c.pos} of ${c.n}), against ${pct(rate)} over the whole day. Suggest: hold them until another part of the day.`, evidence: `${c.pos} of ${c.n} in the ${b}; ${k.pos} of ${k.n} overall` });
257
258
  }
258
259
  }
259
260
  const notes = (missed || []).filter((m) => m.at > from && m.note).map((m) => m.note);
260
261
  if (notes.length >= 2) {
261
262
  const shown = notes.slice(0, 3).map((n) => `"${clip(n, 60)}"`).join('; ');
262
- out.push({ key: 'missed:' + hash(notes.slice().sort().join('|')).slice(0, 10), text: clip(`You reported ${notes.length} missed items this week: ${shown}. Suggest: look for things like these when preparing the morning brief.`, 300), evidence: `${notes.length} missed notes, ${WINDOW_DAYS} days` });
263
+ // One stable key, so a rejected missed lesson waits its 28 days like the others.
264
+ out.push({ key: 'missed', text: clip(`You reported ${notes.length} missed items this week: ${shown}. Suggest: look for things like these when preparing the morning brief.`, 300), evidence: `${notes.length} missed notes, ${WINDOW_DAYS} days` });
263
265
  }
264
266
  const live = new Set(); const wait = new Map();
265
267
  for (const l of lessons || []) {