create-pmos 0.48.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.
Files changed (35) hide show
  1. package/package.json +1 -1
  2. package/payload/KIT-VERSION +2 -2
  3. package/payload/mods/pmos-cockpit/.claude-plugin/plugin.json +1 -1
  4. package/payload/mods/pmos-cockpit/README.md +10 -1
  5. package/payload/mods/pmos-cockpit/hooks/lib/route.mjs +95 -0
  6. package/payload/mods/pmos-cockpit/hooks/register.tsx +67 -1
  7. package/payload/mods/pmos-cockpit/types/index.d.ts +1 -1
  8. package/payload/planning/evals/eval-calibration.md +1 -1
  9. package/payload/planning/okrs/health-budgets.md +1 -1
  10. package/payload/scripts/status-facts.js +13 -3
  11. package/payload/scripts/workflow-state.js +7 -2
  12. package/payload/state/discoveries.md +1 -1
  13. package/payload/state/log.md +1 -1
  14. package/workspace/guide/index.md +2 -1
  15. package/workspace/guide/initiatives-and-next-steps.md +3 -1
  16. package/workspace/guide/knowledge-graph.md +12 -1
  17. package/workspace/guide/layout.md +2 -0
  18. package/workspace/guide/live-notes.md +78 -0
  19. package/workspace/guide/validate-with-typesafe.md +15 -1
  20. package/workspace/lib/engine/status-facts.js +13 -3
  21. package/workspace/lib/engine/workflow-state.js +7 -2
  22. package/workspace/lib/jev-categorize.js +47 -13
  23. package/workspace/lib/jev-live.js +37 -0
  24. package/workspace/lib/jev-route.js +11 -1
  25. package/workspace/lib/live-notes.js +171 -26
  26. package/workspace/package-lock.json +1 -1
  27. package/workspace/package.json +2 -2
  28. package/workspace/public/pm/data.js +21 -1
  29. package/workspace/public/pm/modes-live.js +68 -5
  30. package/workspace/public/pm/panel-cockpit.js +3 -1
  31. package/workspace/public/pm/panel-detail.js +3 -1
  32. package/workspace/public/pm/panel-tabs.js +112 -10
  33. package/workspace/public/pm/state.js +3 -0
  34. package/workspace/public/pm/view-livenotes.js +397 -0
  35. package/workspace/server.js +66 -7
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-pmos",
3
- "version": "0.48.0",
3
+ "version": "0.49.0",
4
4
  "description": "Scaffold the PMOS portable kit — run the PMOS product-management workflow (initiative → lane-fit spec → contract → build → three gates, with work-type lanes for features, research, maintenance, incidents, and content) entirely inside your own repo, with all state as git-versioned files. Includes `workflow-sweep`, which audits the state every board derives from and separates real defects from work you simply have not done. `create-pmos workspace` also installs + launches the local Terminal Workspace GUI. No database, no network, no telemetry.",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -1,3 +1,3 @@
1
- kernel-source: pmos@df469ed
2
- assembled: 2026-10-04
1
+ kernel-source: pmos@676b0dc
2
+ assembled: 2026-10-05
3
3
  mode: vendor
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmos-cockpit",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Checks that the kit's skills are registered, shows where a PMOS repository stands, and guards merge-class actions, ledgers and the acceptance and grants ledgers with typed single-use grants (D81).",
5
5
  "types": "./types/index.d.ts",
6
6
  "author": {
@@ -32,6 +32,15 @@ A grant is one line in `grants.jsonl`. You mint it by typing `/pmos-grant merge:
32
32
 
33
33
  `/pmos-accept <run_id> pass|fail <note>` records your Acceptance through the same writer, with `pm_label` set. When your own message reads as an approval, a band offers Pass, Fail and Not now for the newest run of the branch's initiative that has no acceptance. Your press writes the row; the note quotes your message. Nothing is written from inferred text.
34
34
 
35
+ ## Model routing (prompt-model-routing, Run 3)
36
+
37
+ - **Every prompt is routed before it runs.** When the PMOS workspace started this terminal, it put its own loopback address in `PMOS_ROUTE_URL`. The mod asks that address for the route of each prompt you type and each prompt handed to a sub-agent. The workspace asks Jev only when its three gates are open: a saved key, consent for the repository, and the "route prompts with Jev" switch (D84). All three are on the workspace's validation page. The key stays in the workspace and never reaches this terminal.
38
+ - **The model switches automatically, only on a real answer.** When Jev or a rule decided the tier and it names another model, the mod runs `/model <model>` and then sends your prompt, once, unchanged. A toast names the model and the tier. When only the keyword routes answered, or Jev is off, slow or withheld the prompt, the model stays and you are told why once a session.
39
+ - **What is not routed.** Slash commands, prompts with an attachment or an `@file` mention (a resent prompt would lose them), prompts typed while a turn runs, and prompts under 40 characters, which usually refer back to the conversation.
40
+ - **Sub-agents.** A sub-agent gets the routed model when the Agent call named none. The read-only reviewer stays pinned (D40), and forks inherit.
41
+ - **Cost.** Switching model in the middle of a conversation gives up the prompt cache, so the next turn reads the whole conversation again at full price. `/pmos-route off` stops routing for the session; `/pmos-route status` says whether it is on.
42
+ - **Unmeasured.** Whether Jev picks the right tier is not measured yet. `pmos-model bakeoff` on your labelled prompts is how to find out.
43
+
35
44
  ## Independent review (M14)
36
45
 
37
46
  `/pmos-review [model]` starts a read-only reviewer for the newest run of the branch's initiative. The reviewer is a subagent with the tools Read, Grep and Glob and nothing else, and it runs on a model of a different family than yours (D40: a judge over-rewards its own family's style). The mod picks the model, or checks the one you name, and refuses when your own model cannot be placed in a family. Only your typed command starts it, and the model you build with is not offered it.
@@ -63,7 +72,7 @@ Before a merge-class action the mod runs the local gate for the branch's initiat
63
72
  - **`/pmos-handover confirm`**, or the Commit and push button, writes the note, stages exactly the planned paths, commits and runs `git push -u origin <branch>`. **`confirm local`**, or Commit only, does the same without the push. A confirmation is good for the plan it follows: a new commit, different changed files, another branch or ten minutes all send you back to a new plan, and nothing is written. The mod never skips a hook (a failing one is reported, the note stays on disk and nothing is pushed), never forces, and never retries a failed push; the local commit is kept. With no remote named `origin` it commits only and says so. A run that did not come from you is refused, in all three modes.
64
73
  - **The note** is `planning/handovers/<id>.md` in PMOS's own repository and `pmos/state/handovers/<id>.md` in a kit, one per initiative, replaced on each handover (git keeps the old ones). It holds the stage, next step and blocking steps from the ladder, the branch, the head commit, and your words quoted as yours. What git supplies (commit subjects, paths) sits inside one fenced block marked as copied data. It records a snapshot of one branch, not what was tried, dropped or decided in conversation, unless your words say so.
65
74
  - **The resume summary** is a band at session start when the branch's initiative has a note: when it was written, at which commit, the stage and next step it recorded, how many commits the branch has moved since (or that the history has changed, after a rebase), and your words cut to 80 characters. It has Dismiss and no other button, and is shown after the blocking bands and the cloud band. `/pmos-status` prints the same facts. It is for you: the prompt section is unchanged, so the model reads the note only if you ask it to.
66
- - **The one network use.** The push, after your confirmation and through git's own credentials. The mod opens no pull request. A hook, a credential prompt or a protected branch on the remote can still refuse the push.
75
+ - **The one network use.** The push, after your confirmation and through git's own credentials. (Model routing also asks the workspace on its loopback address; see Model routing.) The mod opens no pull request. A hook, a credential prompt or a protected branch on the remote can still refuse the push.
67
76
 
68
77
  ## What it does not cover
69
78
 
@@ -0,0 +1,95 @@
1
+ // Automatic model routing (prompt-model-routing, Run 3; D75 §6 and D84 point 4 as amended by the PM on
2
+ // 2026-10-04). Each prompt the person types, and each prompt handed to a sub-agent, is routed before it
3
+ // runs. The route comes from the workspace, which alone holds the Jev key (D79) and asks Jev only when
4
+ // D84's three gates are open. The mod switches the model only on an answer that Jev or a rule gave; a
5
+ // keyword guess never moves the model, because "no rule matched" is not evidence about the prompt.
6
+ //
7
+ // Everything here is pure. register.tsx asks the workspace, reads the session's model, and runs the switch.
8
+
9
+ import { REVIEWER_TYPE, familyOf } from './review.mjs';
10
+
11
+ // A prompt shorter than this usually refers back to the conversation ("yes", "go on", "fix that"), and
12
+ // its text says nothing about how hard the work is. It keeps the model the session is on.
13
+ export const MIN_CHARS = 40;
14
+ // What the mod waits for the workspace's answer, Jev included. The workspace bounds Jev itself; this
15
+ // bounds a workspace that does not answer. After it the prompt goes on unchanged.
16
+ export const ASK_LIMIT_MS = 4000;
17
+ // Only a loopback address the workspace wrote into the terminal's environment is ever asked.
18
+ const ROUTE_URL = /^http:\/\/127\.0\.0\.1:\d{2,5}\/api\/model\/route$/;
19
+ const MODEL = /^[A-Za-z0-9][A-Za-z0-9._:\/\[\]-]{0,99}$/;
20
+ const DECIDING = new Set(['jev', 'rules']);
21
+
22
+ /** The workspace's route address from PMOS_ROUTE_URL, or null when it is absent or not loopback. */
23
+ export function routeUrl(value) {
24
+ return typeof value === 'string' && ROUTE_URL.test(value) ? value : null;
25
+ }
26
+
27
+ /**
28
+ * Whether a submitted prompt is routed at all.
29
+ * @param {{ text?: string, attachments?: unknown[], origin?: { kind?: string }, turnId?: string }} e
30
+ * @returns {{ ok: true } | { ok: false, why: string }}
31
+ */
32
+ export function shouldRoute(e) {
33
+ const text = typeof e?.text === 'string' ? e.text : '';
34
+ if (e?.origin && e.origin.kind !== 'composer') return { ok: false, why: 'not typed by the person' };
35
+ if (e?.turnId) return { ok: false, why: 'typed while a turn runs; the model is not changed mid-turn' };
36
+ if (text.trimStart().startsWith('/')) return { ok: false, why: 'a slash command' };
37
+ if (Array.isArray(e?.attachments) && e.attachments.length) return { ok: false, why: 'it carries an attachment, which a resubmitted prompt would lose' };
38
+ if (/(^|\s)@[^\s@]/.test(text)) return { ok: false, why: 'it names a file with @, which a resubmitted prompt would not expand' };
39
+ if (text.trim().length < MIN_CHARS) return { ok: false, why: `under ${MIN_CHARS} characters, so it likely refers back to the conversation` };
40
+ return { ok: true };
41
+ }
42
+
43
+ /** The workspace's answer as the mod uses it, or null when it is not one. */
44
+ export function parseRoute(status, text) {
45
+ if (status !== 200 || typeof text !== 'string') return null;
46
+ let o; try { o = JSON.parse(text) } catch { return null }
47
+ if (!o || o.ok !== true) return null;
48
+ if (o.shadow === true) return { shadow: true, id: typeof o.id === 'string' ? o.id : null };
49
+ if (typeof o.tier !== 'string' || typeof o.model !== 'string' || !MODEL.test(o.model) || typeof o.stage !== 'string') return null;
50
+ return { shadow: false, id: typeof o.id === 'string' ? o.id : null, tier: o.tier, model: o.model, stage: o.stage,
51
+ why: typeof o.why === 'string' ? o.why.slice(0, 200) : '', sent: o.sent === true,
52
+ gate: o.gate && typeof o.gate.sentence === 'string' ? o.gate.sentence.slice(0, 300) : null,
53
+ withheld: typeof o.withheld === 'string' ? o.withheld : null };
54
+ }
55
+
56
+ /** Same model, compared by family so an alias matches its full id ('sonnet' and 'claude-sonnet-5-5'). */
57
+ export function sameModel(current, target) {
58
+ if (typeof current !== 'string' || typeof target !== 'string') return false;
59
+ if (current === target) return true;
60
+ const a = familyOf(current); const b = familyOf(target);
61
+ return a !== null && a === b;
62
+ }
63
+
64
+ /**
65
+ * What to do with a typed prompt, given the workspace's answer and the session's model.
66
+ * @returns {{ action: 'keep', why: string, tell?: true } | { action: 'switch', model: string, tier: string, why: string }}
67
+ * `tell` marks a reason the person should hear once a session: Jev was not asked, withheld or silent.
68
+ */
69
+ export function decide(answer, current) {
70
+ if (!answer) return { action: 'keep', why: 'the workspace gave no route' };
71
+ if (answer.shadow) return { action: 'keep', why: 'shadow mode: the route was logged and is not applied' };
72
+ if (!DECIDING.has(answer.stage)) {
73
+ const reason = answer.gate ? `Jev was not asked: ${answer.gate.replace(/[.\s]+$/, '')}` : answer.withheld ? `the prompt was withheld from Jev (${answer.withheld})` : 'Jev did not answer';
74
+ return { action: 'keep', why: `${reason}, so the model stays`, tell: true };
75
+ }
76
+ if (sameModel(current, answer.model)) return { action: 'keep', why: `already on the ${answer.tier} tier` };
77
+ return { action: 'switch', model: answer.model, tier: answer.tier, why: answer.why };
78
+ }
79
+
80
+ /**
81
+ * The model to give a sub-agent, or null to leave its spawn alone: when the Agent call named a model,
82
+ * when the agent is the read-only reviewer (pinned, D40), when it is a fork (forks inherit), or when the
83
+ * answer did not come from Jev or a rule.
84
+ */
85
+ export function subagentModel(spawn, answer) {
86
+ if (!spawn || spawn.model) return null;
87
+ if (spawn.subagentType === REVIEWER_TYPE || spawn.subagentType === 'fork') return null;
88
+ if (!answer || answer.shadow || !DECIDING.has(answer.stage)) return null;
89
+ return answer.model;
90
+ }
91
+
92
+ /** The one line the person sees when a typed prompt is switched. */
93
+ export function switchNote(d) {
94
+ return `PMOS routed this prompt to ${d.model} (${d.tier})${d.why ? `: ${d.why}` : ''}. /pmos-route off stops it.`.slice(0, 280);
95
+ }
@@ -18,6 +18,7 @@ import { UNPUSHED_PROBES, cloudBandText, interpretUnpushed } from './lib/unpushe
18
18
  import { HANDOVER_PROBES, buildPlan, countSince, interpretSince, notePaths, parseNote, planStillGood, renderNote, resumeText, sinceProbes, subjectsProbe } from './lib/handover.mjs'
19
19
  import { carryOut } from './lib/handover-run.mjs'
20
20
  import { REVIEWER_AGENT, REVIEWER_PROMPT, REVIEWER_TOOLS, REVIEWER_TYPE, buildReviewPrompt, offerText, parseVerdict, pickReviewerModel, planEval } from './lib/review.mjs'
21
+ import { ASK_LIMIT_MS, decide, parseRoute, routeUrl, shouldRoute, subagentModel, switchNote } from './lib/route.mjs'
21
22
 
22
23
  const status = atom({ plugin: 'pmos-cockpit', key: 'status' } as const, null)
23
24
  const isHidden = atom({ plugin: 'pmos-cockpit', key: 'isHidden' } as const, false)
@@ -40,6 +41,9 @@ const resumeGone = atom({ plugin: 'pmos-cockpit', key: 'resumeGone' } as const,
40
41
  // held until a person presses Record or types /pmos-record-review. Neither is ever written to a ledger.
41
42
  const review = atom({ plugin: 'pmos-cockpit', key: 'review' } as const, null)
42
43
  const reviewOffer = atom({ plugin: 'pmos-cockpit', key: 'reviewOffer' } as const, null)
44
+ // Model routing (prompt-model-routing, Run 3). On by default for the session; /pmos-route off stops it.
45
+ // `told` keeps the "why nothing was switched" toast to once a session.
46
+ const routing = atom({ plugin: 'pmos-cockpit', key: 'routing' } as const, { on: true, told: false })
43
47
  // The width the person's terminal draws the band at, seen at the last render. Lost on a reload, which
44
48
  // only returns the status line to its default width.
45
49
  let lastColumns = 0
@@ -80,6 +84,22 @@ async function readMode($: any): Promise<{ mode: string; signal: string | null }
80
84
 
81
85
  const isPerson = (e: any): boolean => e?.origin?.kind === 'composer' || e?.origin?.kind === 'bridge'
82
86
 
87
+ // Ask the workspace for a prompt's route. The workspace holds the Jev key and asks Jev only when D84's
88
+ // gates are open; the mod never sees the key and reaches no host but the loopback address the workspace
89
+ // put in the terminal's environment. A terminal started outside the workspace has none: nothing is asked.
90
+ async function askRoute($: any, prompt: string, source: 'pm' | 'agent'): Promise<any> {
91
+ let url: string | null = null
92
+ try { url = routeUrl(await $.env.get('PMOS_ROUTE_URL')) } catch { url = null }
93
+ if (url === null) return null
94
+ try {
95
+ const root: string = await $.session.root()
96
+ const ask = $.http.fetch(url, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ root, prompt, source, interactive: true }) })
97
+ .then((r: any) => parseRoute(r?.status, r?.text)).catch(() => null)
98
+ const late = $.clock.sleep(ASK_LIMIT_MS).then(() => null).catch(() => null)
99
+ return await Promise.race([ask, late])
100
+ } catch { return null }
101
+ }
102
+
83
103
  async function readText($: any, path: string): Promise<string | null> {
84
104
  try { return (await $.fs.exists(path)) ? await $.fs.read(path) : null } catch { return null }
85
105
  }
@@ -599,6 +619,7 @@ export const register: Register = (on) => {
599
619
  await $.command.register({ name: 'pmos-accept', description: 'Record your Acceptance of a run: /pmos-accept <run_id> pass|fail <note>' })
600
620
  await $.command.register({ name: 'pmos-review', description: 'Ask a read-only reviewer on another model to judge the newest run: /pmos-review [model]' })
601
621
  await $.command.register({ name: 'pmos-record-review', description: 'Record the reviewer verdict that is waiting (the same as pressing Record)' })
622
+ await $.command.register({ name: 'pmos-route', description: 'Model routing for this session: /pmos-route on | off | status' })
602
623
  try {
603
624
  await $.agent.register({ name: REVIEWER_AGENT, description: 'Independent read-only reviewer started by /pmos-review. Not for the builder to call.', prompt: REVIEWER_PROMPT, tools: [...REVIEWER_TOOLS] })
604
625
  } catch { /* the review command says so when it cannot start the reviewer */ }
@@ -823,7 +844,52 @@ export const register: Register = (on) => {
823
844
  if (e.origin?.kind === 'composer' && readsAsApproval(e.text)) {
824
845
  try { const o = await buildOffer($, e.text); await update($, offer, () => o) } catch { /* no offer */ }
825
846
  }
826
- return next(e)
847
+ // Model routing. A prompt that needs another model enters nothing now: the mod runs `/model` and
848
+ // then submits the same text as the person's own, so the turn runs on the routed model. Anything
849
+ // that fails on the way sends the prompt on unchanged; a prompt is never lost or held back.
850
+ const r: any = await read($, routing)
851
+ if (!r?.on || !shouldRoute(e).ok) return next(e)
852
+ const answer = await askRoute($, e.text, 'pm')
853
+ let current: string | undefined
854
+ try { current = await $.session.model() } catch { current = undefined }
855
+ const d: any = decide(answer, current as any)
856
+ if (d.action !== 'switch') {
857
+ if (d.tell && !r.told) {
858
+ await update($, routing, (x: any) => ({ ...x, told: true }))
859
+ $.ui.toast(`PMOS did not switch the model: ${d.why}.`.slice(0, 280))
860
+ }
861
+ return next(e)
862
+ }
863
+ const text = e.text
864
+ $.clock.after(0, () => {
865
+ void (async () => {
866
+ try { await $.command.run({ command: 'model', args: d.model }) } catch { /* the prompt still goes, on the current model */ }
867
+ try { await $.prompt.submit({ text, asUser: true }) } catch { $.ui.toast('PMOS could not send your prompt after switching the model. Send it again.') }
868
+ $.ui.toast(switchNote(d))
869
+ })()
870
+ })
871
+ return { text }
872
+ })
873
+
874
+ // Every prompt handed to a sub-agent is routed, unless the call named a model or the agent is pinned.
875
+ on('agent.spawn', async ($, e, next) => {
876
+ const r: any = await read($, routing)
877
+ if (!r?.on || e.model || e.subagentType === REVIEWER_TYPE || e.subagentType === 'fork' || typeof e.prompt !== 'string' || !e.prompt.trim()) return next(e)
878
+ const model = subagentModel(e, await askRoute($, e.prompt, 'agent'))
879
+ return next(model ? { ...e, model } : e)
880
+ })
881
+
882
+ on('command.run', { command: 'pmos-route' }, async ($, e) => {
883
+ const arg = String(e.args ?? '').trim()
884
+ if (arg === 'on' || arg === 'off') {
885
+ if (arg === 'on' && !isPerson(e)) return { text: 'Routing is turned on only by your own typed command. Nothing changed.' }
886
+ await update($, routing, (x: any) => ({ ...x, on: arg === 'on' }))
887
+ return { text: arg === 'on' ? 'Model routing is on for this session: each prompt you type and each sub-agent prompt is routed before it runs.' : 'Model routing is off for this session. The model changes only when you change it.' }
888
+ }
889
+ const r: any = await read($, routing)
890
+ let url: string | null = null
891
+ try { url = routeUrl(await $.env.get('PMOS_ROUTE_URL')) } catch { url = null }
892
+ return { text: `Model routing is ${r?.on ? 'on' : 'off'} for this session. ${url ? 'The workspace answers routes; the model switches only when Jev or a rule decided.' : 'This terminal was not started by the PMOS workspace, so no prompt is routed.'}` }
827
893
  })
828
894
 
829
895
  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
@@ -71,6 +71,6 @@ export type HandoverPlan = {
71
71
 
72
72
  declare module 'claude-code' {
73
73
  interface PluginState {
74
- 'pmos-cockpit': { status: CockpitStatus | null; isHidden: boolean; offer: { run_id: string; initiative_id: string; message: string } | null; view: ViewState | null; factsCache: FactsCache | null; mode: { mode: 'local' | 'hosted' | 'cloud'; signal: string | null } | null; review: { agentId: string; run_id: string; initiative_id: string; rubric_path: string; model: string } | null; reviewOffer: { run_id: string; initiative_id: string; rubric_path: string; verdict: string; score: number; evaluator_model: string; findings: string } | null; handoverPlan: HandoverPlan | null; resume: { text: string } | null; resumeGone: boolean }
74
+ 'pmos-cockpit': { status: CockpitStatus | null; isHidden: boolean; offer: { run_id: string; initiative_id: string; message: string } | null; view: ViewState | null; factsCache: FactsCache | null; mode: { mode: 'local' | 'hosted' | 'cloud'; signal: string | null } | null; review: { agentId: string; run_id: string; initiative_id: string; rubric_path: string; model: string } | null; reviewOffer: { run_id: string; initiative_id: string; rubric_path: string; verdict: string; score: number; evaluator_model: string; findings: string } | null; handoverPlan: HandoverPlan | null; resume: { text: string } | null; resumeGone: boolean; routing: { on: boolean; told: boolean } }
75
75
  }
76
76
  }
@@ -2,7 +2,7 @@
2
2
  type: Reference
3
3
  title: "Eval calibration log"
4
4
  description: "Append-only record of every PM override of an evaluator verdict."
5
- generated: {by: process:create-pmos, at: "2026-10-04T21:21:45Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-05T00:43:02Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -2,7 +2,7 @@
2
2
  type: Reference
3
3
  title: "Health budgets"
4
4
  description: "Closed registry of health-budget anchors (D59), edited only by the PM."
5
- generated: {by: process:create-pmos, at: "2026-10-04T21:21:45Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-05T00:43:02Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -109,8 +109,9 @@ function history(root, L) {
109
109
  const norm = (p) => String(p || '').trim().replace(/^\.\//, '').replace(/^\/+/, '');
110
110
 
111
111
  // ── since_approval ──────────────────────────────────────────────────────────────────────────────
112
- function sinceApproval({ root, L, fm, runs, accepted }) {
113
- const files = ['prd_path', 'rubric_path', 'brief_path'].map((k) => norm(fm[k])).filter((p) => p && !/^[<(…]/.test(p));
112
+ const SPEC_KEYS = ['prd_path', 'rubric_path', 'brief_path'];
113
+ function sinceApproval({ root, L, fm, runs, accepted, keys = SPEC_KEYS }) {
114
+ const files = [...new Set(keys.map((k) => norm(fm[k])).filter((p) => p && !/^[<(…]/.test(p)))];
114
115
  if (!files.length) return null;
115
116
  const contractRun = [...(runs || [])].reverse().find((r) => r && r.pre_run_contract && r.pre_run_contract.PM_approved === true);
116
117
  const acceptRow = [...(accepted || [])].reverse().find((a) => a && a.accepted === true);
@@ -201,4 +202,13 @@ function compute(c) {
201
202
  return { since_approval: since, coverage: cov, record_says: says };
202
203
  }
203
204
 
204
- module.exports = { compute, sinceApproval, coverage, recordSays, history };
205
+ // The pm-artifacts profile (D68): the brief and the deliverable after the PM's acceptance. A document repository has
206
+ // no contract runs and no requirement ids, and its stages are not the product-code stage words, so only this one applies.
207
+ function computeArtifacts({ root, L, fm, accepted }) {
208
+ let since = null;
209
+ try { since = sinceApproval({ root, L, fm, runs: [], accepted, keys: ['brief_path', 'prd_path', 'doc_path'] }); }
210
+ catch (e) { since = { status: 'unknown', reason: `since-approval check failed: ${e.message}` }; }
211
+ return { since_approval: since, coverage: null, record_says: null };
212
+ }
213
+
214
+ module.exports = { compute, computeArtifacts, sinceApproval, coverage, recordSays, history };
@@ -798,7 +798,7 @@ function computeWorkflowDerived(id, opts) {
798
798
 
799
799
  // ── D68: the pm-artifacts ladder replaces everything after the anchor ─────────────────────────
800
800
  if (pmProfile) {
801
- return pmLadder({ id, fm, lane, effLane, knownLane, legacy, steps, root, L, recRel, range, inChange, lastActivity });
801
+ return pmLadder({ id, fm, lane, effLane, knownLane, legacy, steps, root, L, recRel, range, inChange, lastActivity, opts });
802
802
  }
803
803
 
804
804
  const prd = String(fm.prd_path || '').trim();
@@ -1274,7 +1274,7 @@ function computeWorkflowDerived(id, opts) {
1274
1274
  * existence and their order fail CI for a non-legacy record). Review, approval and handoff are never
1275
1275
  * CI-blocking in either track: none can be true before the PR exists.
1276
1276
  */
1277
- function pmLadder({ id, fm, lane, effLane, knownLane, legacy, steps, root, L, recRel, range, inChange, lastActivity }) {
1277
+ function pmLadder({ id, fm, lane, effLane, knownLane, legacy, steps, root, L, recRel, range, inChange, lastActivity, opts }) {
1278
1278
  const declared = String(fm.track || '').trim().toLowerCase();
1279
1279
  const track = declared === 'light' || declared === 'governed' ? declared
1280
1280
  : (laneDefs.laneDef('pm-artifacts', effLane) || laneDefs.laneDef('pm-artifacts', 'prd')).track;
@@ -1348,10 +1348,15 @@ function pmLadder({ id, fm, lane, effLane, knownLane, legacy, steps, root, L, re
1348
1348
  const blockers = steps.filter((x) => x.status === 'blocked');
1349
1349
  const gateFailures = blockers.filter((x) => x.blocking);
1350
1350
  const next = steps.find((x) => x.status === 'blocked') || steps.find((x) => x.status === 'pending' && x.next) || null;
1351
+ // status-from-artifacts: a document repository gets the one fact that applies to it — the brief or the deliverable
1352
+ // changed after the PM accepted it. Coverage and the status-line check are product-code facts and stay null here.
1353
+ const facts = (opts && opts.facts === false) ? {}
1354
+ : statusFacts.computeArtifacts({ root, L, fm, accepted });
1351
1355
  return {
1352
1356
  ok: true, id, lane, laneKnown: knownLane, legacy, in_change: inChange,
1353
1357
  last_activity: lastActivity, accepted: accepted.length > 0, stage, record: recRel,
1354
1358
  title: fm.title || id, anchor: fm.parent_kr || null, profile: 'pm-artifacts', track,
1359
+ ...facts,
1355
1360
  steps, blockers: blockers.map((x) => x.key), gate_failures: gateFailures.map((x) => x.rule || x.key),
1356
1361
  next: next ? { step: next.key, why: next.evidence, action: next.next || next.evidence } : null,
1357
1362
  range: range.ok ? { base: range.base, commits: range.shas.length, whole_history: !!range.wholeHistory } : { error: range.reason },
@@ -2,7 +2,7 @@
2
2
  type: Reference
3
3
  title: "Discoveries and friction log"
4
4
  description: "Append-only record of what runs discovered and where the process rubbed."
5
- generated: {by: process:create-pmos, at: "2026-10-04T21:21:45Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-05T00:43:02Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  Newest first, ISO dates.
4
4
 
5
- ## 2026-10-04
5
+ ## 2026-10-05
6
6
 
7
7
  - Adopted OKF v0.2 (D73).
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  type: Guide
3
3
  title: "Guide: start here"
4
- timestamp: 2026-10-03
4
+ timestamp: 2026-10-04
5
5
  ---
6
6
 
7
7
  # Guide: start here
@@ -38,4 +38,5 @@ The workspace has three columns. [Layout](layout.md) explains where things open.
38
38
  - [Scheduled tasks and the command line](scheduled-tasks-and-cli.md): how to make, edit and run scheduled tasks.
39
39
  - [Validate a repository](validate-with-typesafe.md): what each kind of document is compared with, how to read the verdict, and what leaves your machine.
40
40
  - [Today and the proactive engine](proactive-engine.md): what Today finds, what you can do with an item, the settings, the commands and the limits.
41
+ - [Live notes](live-notes.md): take notes in a meeting, confirm each talking point, and what goes to Jev when you turn it on.
41
42
  - [Find knowledge without reading whole files](knowledge-graph.md): what `pmos kg` finds, its commands, how to read its numbers, and its limits.
@@ -26,7 +26,9 @@ These lines show facts about the documents that the phases do not show. Each lin
26
26
  - **Coverage.** A requirement id in the PRD that the rubric does not mention, or an id in the rubric that the PRD does not have. A mention is not a grade. If the rubric mentions no requirement id, the line says that coverage cannot be found.
27
27
  - **Record says.** The `status:` line in the initiative record names a stage that the phases contradict. Edit the record if the line is wrong. The phases do not change.
28
28
 
29
- If a check cannot run, for example in a shallow clone, the line says "Cannot tell" and gives the reason. A missing line means that the check has nothing to compare.
29
+ If a check cannot run, for example in a shallow clone, the line says "Cannot tell" and gives the reason. If no check has anything to compare yet, the section says "Nothing to compare yet" and tells you what each check waits for, such as an approved contract, a feature lane or a stage word in the status line.
30
+
31
+ In a documents repository (the pm-artifacts profile), only **Since approval** applies. It starts when you accept the work: after that, a commit to the brief or the deliverable shows here. Coverage and Record says are for product-code repositories.
30
32
 
31
33
  If **read documents for status** is on for the repository, lines that start with **Documents suggest** can follow. They are Jev's answers about the documents of this initiative: a PRD, rubric or brief that may be a stub, or a note that may contradict a requirement. Each line says *very likely*, *likely* or *possible*, says that it is a suggestion and names the model. Open the document to decide. See [Guide: validate a repository](validate-with-typesafe.md).
32
34
 
@@ -92,7 +92,18 @@ After the first confirmed send, the workspace sends only new or changed undeterm
92
92
 
93
93
  What is sent for each file: the title, the headings and the start of the text, at most 3,000 characters. The file path and the whole file are not sent. A file under an excluded path is not sent. A file whose start looks like a password or a key is not sent.
94
94
 
95
- Jev's answer shows as the category with `(jev)` after it. Point at it to see how sure Jev was, in words. If Jev was not sure, the file stays `undetermined`, and the tooltip names the two categories Jev was between. A Jev category is advisory. Each answer is kept on your computer, outside the repository, and the same text is not sent twice. Nothing is written into the repository.
95
+ Jev's answer shows as the category with `(jev)` after it. Point at it to see how sure Jev was, in words. If Jev was not sure, the file stays `undetermined` with `(jev unsure)` after it, and the tooltip names the two categories Jev was between. A Jev category is advisory. Each answer is kept on your computer, outside the repository, and the same text is not sent twice. Nothing is written into the repository.
96
+
97
+ After a pass, the Knowledge view shows what happened to each undetermined file. Files with a problem are at the top of the list:
98
+
99
+ - **failed**: the request did not succeed. The row gives the reason. The file keeps the local result, and the next pass sends it again.
100
+ - **not asked**: sending stopped before this file. The row gives the reason.
101
+ - **unreadable**: the workspace could not read Jev's answer. The file is not sent again until it changes.
102
+ - **unsure**: Jev was not sure. The row names the two categories, and the file stays `undetermined`.
103
+ - **withheld** or **excluded**: the file was not sent. The row says why.
104
+ - **categorized**: Jev gave a category. The row names it.
105
+
106
+ A line above the list counts the files in the view: decided by the local rules or your corrections, categorized by Jev, and still `undetermined`.
96
107
 
97
108
  If the service rejects the key two times, cannot be reached three times in a row, or sends three answers in a row that the workspace cannot read, sending stops for this repository. An answer that cannot be read is not sent again. Press **resume** to start again.
98
109
 
@@ -35,6 +35,8 @@ The detail shows the title, the summary, the chips, the blocker (when the initia
35
35
 
36
36
  Each tab shows a dot, its title and, under the title, its initiative and its session. Press × on a tab to close it. The `+ session` button is at the right end of the tabs.
37
37
 
38
+ If many tabs are open, the strip stays one row and scrolls sideways. Use the mouse wheel on the strip to scroll it. The tab in front always scrolls into view. Press `⇅ tabs at side` to show the tabs as a list down the left of the tabs column. In the list, each tab is one row, with its title, its initiative and session, and ×. The `+ session` button is at the top of the list, and the other controls are at the bottom. Press `⇆ tabs on top` to go back to the strip. The workspace remembers your choice in this browser. A phone and `?layout=legacy` always show the strip.
39
+
38
40
  ## Navigation
39
41
 
40
42
  The first column holds the list of repositories. Under it is the list for the current mode. When you choose a repository, its initiatives appear there. When you choose an item in the settings and tools drawer, its list replaces the initiatives. Choose a repository again to return to its initiatives.
@@ -0,0 +1,78 @@
1
+ ---
2
+ type: Guide
3
+ title: "Guide: live notes"
4
+ description: "How the Live notes view listens to a meeting, turns what is said into talking points, and lets you confirm each one, with an optional check by Jev."
5
+ timestamp: 2026-10-04
6
+ ---
7
+
8
+ # Guide: live notes
9
+
10
+ The Live notes view takes notes during a meeting. Each thing that is said becomes a talking point. The workspace guesses which open initiative the point is about. You confirm the guess, change it or reject it, one card at a time. Nothing changes an initiative until you confirm.
11
+
12
+ ## Open the view
13
+
14
+ 1. Select the repository in the navigation column.
15
+ 2. Open the menu and select **live notes** under **Library**. The view opens as a tab for that repository.
16
+ 3. Press **New session**. A session is one meeting.
17
+
18
+ If the repository has more than 255 open initiatives, the view asks you to select an anchor first. The session then uses only the initiatives of that anchor.
19
+
20
+ ## Add talking points
21
+
22
+ You can type the points or let the view listen.
23
+
24
+ - **Type.** Write one point for each line. Press Enter or **Add**. Shift+Enter starts a new line.
25
+ - **Listen.** Press **Start listening**. The browser recognises speech on this device. No audio leaves the device. The first time, the browser can ask you to download the language. Press **Download for** and the language name.
26
+
27
+ While the view listens, it shows **● Listening: notes are being taken**. Words that are not final show in grey and are not added. A phrase becomes a point at the end of a sentence or after a pause. Before the phrase becomes a point, the workspace cleans it: it removes filler words (um, uh, er, erm, hmm) and repeated words, adds a capital letter and adds a full stop.
28
+
29
+ Press **Stop** to stop listening. Press **This meeting is private** to stop listening and mark the session private. Nothing from a private session goes to Jev.
30
+
31
+ A browser that cannot recognise speech on the device says so. The view never sends audio to a cloud service. Chrome 139 and later can recognise speech on the device.
32
+
33
+ ## Confirm each point
34
+
35
+ Each point shows as a card. Cards that wait for you are at the top. The card shows the guess and where the guess came from:
36
+
37
+ | Source | What it means |
38
+ |---|---|
39
+ | local word match | The workspace compared the words of the point with the titles, intents and open questions of the open initiatives. A local guess is never shown above *possible*. |
40
+ | Jev | Jev compared the point with the same titles, intents and open questions. Its band is in words: *very likely*, *likely* or *possible*. |
41
+ | agent session | An agent session placed the point with the `live-notes` command. |
42
+
43
+ On each card:
44
+
45
+ 1. Select the initiative, or **none of these initiatives**.
46
+ 2. Select how the point relates to it: **validates**, **clarifies**, **extends**, **contradicts** or **answers a question**.
47
+ 3. For **answers a question**, select which open question it answers. The view refuses **answers a question** without one.
48
+ 4. Press **Confirm** or **Reject**.
49
+
50
+ If you change nothing, Confirm records *accept*. If you change the initiative, the relation or the question, it records *change*. The counter shows how many points are confirmed, changed, rejected, waiting and withheld.
51
+
52
+ A point that looks like a secret, such as a key or a token, is withheld. The view shows the reason and never the text.
53
+
54
+ ## Check with Jev
55
+
56
+ **Check with Jev** is off until you turn it on. When it is off, every guess is a local word match and nothing leaves this machine.
57
+
58
+ Jev checks a point only when all three of these are true:
59
+
60
+ 1. A TypeSafe key is saved (open **validate** in the settings and tools drawer).
61
+ 2. Validation is turned on for this repository (also under **validate**).
62
+ 3. **Check with Jev** is on in the Live notes view. It applies to this repository only.
63
+
64
+ If one is missing, the view says which one, in words.
65
+
66
+ When all three are true, the workspace sends each cleaned point to TypeSafe. It sends only the cleaned sentence and the titles, intents and open questions of the initiatives in the session. It does not send the speaker's name. It does not send a withheld point. It does not send a point from a private session. The key goes only in the request header.
67
+
68
+ If Jev cannot be reached, or its answer cannot be read, the card still gets the local guess. The card says that Jev was not reached. If the service rejects the key twice, or cannot be reached three times in a row, Jev pauses for that session and the view shows the reason. The local word match continues.
69
+
70
+ ## Close the session
71
+
72
+ Press **End and file the note**. The workspace writes a section in the day's requirements note, with your decisions. A rejected point is left out and counted. The raw transcript is deleted. The view shows the path of the note. A closed session is read-only.
73
+
74
+ ## Limits
75
+
76
+ - The local word match compares words only. It does not understand meaning.
77
+ - How well speech recognition and Jev work on a real meeting has not been measured. The share of guesses you change is the measure.
78
+ - The view does not read meetings from Wispr Flow. The `live-notes` command does that in an agent session.
@@ -76,13 +76,27 @@ The first check after you turn this switch on records the documents that exist a
76
76
 
77
77
  The answers appear in the initiative detail under **What the documents say**, as lines that start with **Documents suggest**. Each line says that it is a suggestion and names the model. A suggestion never changes a phase, a status or a count, and the workspace writes nothing to the document.
78
78
 
79
+ ## Route prompts with Jev
80
+
81
+ A third switch, **route prompts with Jev**, is below the consent switch. It is off until you turn it on. When it is off, no prompt is sent.
82
+
83
+ When the switch is on, a TypeSafe key is saved, and validation is on for the repository, the workspace asks Jev about each prompt that you type in a workspace terminal. Jev answers with a tier: fast, balanced or deep. If the answer names a model that is different from the model of the session, the PMOS cockpit mod changes the model with `/model` and then sends your prompt. A line in the terminal names the new model and the tier.
84
+
85
+ These prompts are not sent: a slash command, a prompt with an attachment, a prompt that names a file with `@`, and a prompt shorter than 40 characters. A prompt that matches a credential pattern, or that names an excluded path, is not sent. In these cases the model does not change.
86
+
87
+ If one of the three conditions is missing, the model does not change. The terminal tells you once which condition is missing. A guess from keywords never changes the model.
88
+
89
+ To stop routing in one terminal, type `/pmos-route off`. To start it again, type `/pmos-route on`.
90
+
91
+ A change of model starts a new prompt cache, so the next answer can cost more. The workspace does not yet measure if routing saves money.
92
+
79
93
  ## Check with TypeSafe
80
94
 
81
95
  ## What leaves your machine
82
96
 
83
97
  Validation is off until you turn it on. Until then, the workspace sends nothing.
84
98
 
85
- When you turn it on, the workspace sends document text to `api.typesafe.ai`. It sends nothing else. It does not send binary files. It does not send a document that you excluded. It does not send a document that matches a credential pattern, such as a private key or a token. It lists that document as withheld.
99
+ When you turn it on, the workspace sends document text to `api.typesafe.ai`. If **route prompts with Jev** is also on, it sends the text of the prompts that you type in a workspace terminal. It sends nothing else. It does not send binary files. It does not send a document that you excluded. It does not send a document that matches a credential pattern, such as a private key or a token. It lists that document as withheld.
86
100
 
87
101
  The workspace reads the TypeSafe key only on this machine. After you save the key, the screen does not show it again. The workspace does not give the key to a terminal or to an agent.
88
102
 
@@ -109,8 +109,9 @@ function history(root, L) {
109
109
  const norm = (p) => String(p || '').trim().replace(/^\.\//, '').replace(/^\/+/, '');
110
110
 
111
111
  // ── since_approval ──────────────────────────────────────────────────────────────────────────────
112
- function sinceApproval({ root, L, fm, runs, accepted }) {
113
- const files = ['prd_path', 'rubric_path', 'brief_path'].map((k) => norm(fm[k])).filter((p) => p && !/^[<(…]/.test(p));
112
+ const SPEC_KEYS = ['prd_path', 'rubric_path', 'brief_path'];
113
+ function sinceApproval({ root, L, fm, runs, accepted, keys = SPEC_KEYS }) {
114
+ const files = [...new Set(keys.map((k) => norm(fm[k])).filter((p) => p && !/^[<(…]/.test(p)))];
114
115
  if (!files.length) return null;
115
116
  const contractRun = [...(runs || [])].reverse().find((r) => r && r.pre_run_contract && r.pre_run_contract.PM_approved === true);
116
117
  const acceptRow = [...(accepted || [])].reverse().find((a) => a && a.accepted === true);
@@ -201,4 +202,13 @@ function compute(c) {
201
202
  return { since_approval: since, coverage: cov, record_says: says };
202
203
  }
203
204
 
204
- module.exports = { compute, sinceApproval, coverage, recordSays, history };
205
+ // The pm-artifacts profile (D68): the brief and the deliverable after the PM's acceptance. A document repository has
206
+ // no contract runs and no requirement ids, and its stages are not the product-code stage words, so only this one applies.
207
+ function computeArtifacts({ root, L, fm, accepted }) {
208
+ let since = null;
209
+ try { since = sinceApproval({ root, L, fm, runs: [], accepted, keys: ['brief_path', 'prd_path', 'doc_path'] }); }
210
+ catch (e) { since = { status: 'unknown', reason: `since-approval check failed: ${e.message}` }; }
211
+ return { since_approval: since, coverage: null, record_says: null };
212
+ }
213
+
214
+ module.exports = { compute, computeArtifacts, sinceApproval, coverage, recordSays, history };
@@ -798,7 +798,7 @@ function computeWorkflowDerived(id, opts) {
798
798
 
799
799
  // ── D68: the pm-artifacts ladder replaces everything after the anchor ─────────────────────────
800
800
  if (pmProfile) {
801
- return pmLadder({ id, fm, lane, effLane, knownLane, legacy, steps, root, L, recRel, range, inChange, lastActivity });
801
+ return pmLadder({ id, fm, lane, effLane, knownLane, legacy, steps, root, L, recRel, range, inChange, lastActivity, opts });
802
802
  }
803
803
 
804
804
  const prd = String(fm.prd_path || '').trim();
@@ -1274,7 +1274,7 @@ function computeWorkflowDerived(id, opts) {
1274
1274
  * existence and their order fail CI for a non-legacy record). Review, approval and handoff are never
1275
1275
  * CI-blocking in either track: none can be true before the PR exists.
1276
1276
  */
1277
- function pmLadder({ id, fm, lane, effLane, knownLane, legacy, steps, root, L, recRel, range, inChange, lastActivity }) {
1277
+ function pmLadder({ id, fm, lane, effLane, knownLane, legacy, steps, root, L, recRel, range, inChange, lastActivity, opts }) {
1278
1278
  const declared = String(fm.track || '').trim().toLowerCase();
1279
1279
  const track = declared === 'light' || declared === 'governed' ? declared
1280
1280
  : (laneDefs.laneDef('pm-artifacts', effLane) || laneDefs.laneDef('pm-artifacts', 'prd')).track;
@@ -1348,10 +1348,15 @@ function pmLadder({ id, fm, lane, effLane, knownLane, legacy, steps, root, L, re
1348
1348
  const blockers = steps.filter((x) => x.status === 'blocked');
1349
1349
  const gateFailures = blockers.filter((x) => x.blocking);
1350
1350
  const next = steps.find((x) => x.status === 'blocked') || steps.find((x) => x.status === 'pending' && x.next) || null;
1351
+ // status-from-artifacts: a document repository gets the one fact that applies to it — the brief or the deliverable
1352
+ // changed after the PM accepted it. Coverage and the status-line check are product-code facts and stay null here.
1353
+ const facts = (opts && opts.facts === false) ? {}
1354
+ : statusFacts.computeArtifacts({ root, L, fm, accepted });
1351
1355
  return {
1352
1356
  ok: true, id, lane, laneKnown: knownLane, legacy, in_change: inChange,
1353
1357
  last_activity: lastActivity, accepted: accepted.length > 0, stage, record: recRel,
1354
1358
  title: fm.title || id, anchor: fm.parent_kr || null, profile: 'pm-artifacts', track,
1359
+ ...facts,
1355
1360
  steps, blockers: blockers.map((x) => x.key), gate_failures: gateFailures.map((x) => x.rule || x.key),
1356
1361
  next: next ? { step: next.key, why: next.evidence, action: next.next || next.evidence } : null,
1357
1362
  range: range.ok ? { base: range.base, commits: range.shas.length, whole_history: !!range.wholeHistory } : { error: range.reason },