create-pmos 0.45.0 → 0.47.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/payload/KIT-VERSION +1 -1
- package/payload/mods/pmos-cockpit/.claude-plugin/plugin.json +1 -1
- package/payload/mods/pmos-cockpit/README.md +24 -2
- package/payload/mods/pmos-cockpit/hooks/lib/handover-run.mjs +33 -0
- package/payload/mods/pmos-cockpit/hooks/lib/handover.mjs +267 -0
- package/payload/mods/pmos-cockpit/hooks/lib/mode.mjs +45 -0
- package/payload/mods/pmos-cockpit/hooks/lib/review.mjs +117 -0
- package/payload/mods/pmos-cockpit/hooks/lib/unpushed.mjs +53 -0
- package/payload/mods/pmos-cockpit/hooks/register.tsx +283 -3
- package/payload/mods/pmos-cockpit/types/index.d.ts +17 -1
- package/payload/okf/core/concepts/frontmatter-format.md +6 -1
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/skills/SKILL-FORMAT.md +1 -0
- package/payload/state/discoveries.md +1 -1
- package/workspace/README.md +1 -1
- package/workspace/guide/initiatives-and-next-steps.md +1 -0
- package/workspace/guide/knowledge-graph.md +6 -1
- package/workspace/guide/layout.md +1 -0
- package/workspace/guide/proactive-engine.md +21 -3
- package/workspace/guide/scheduled-tasks-and-cli.md +1 -0
- package/workspace/guide/statuses.md +1 -0
- package/workspace/guide/validate-with-typesafe.md +1 -0
- package/workspace/lib/kg-cli.js +2 -1
- package/workspace/lib/kg-dangling-baseline.json +1 -0
- package/workspace/lib/kg-dangling-rules.json +1 -0
- package/workspace/lib/kg.js +20 -8
- package/workspace/lib/learn.js +280 -0
- package/workspace/lib/proactive-cli.js +49 -1
- package/workspace/lib/proactive.js +8 -7
- package/workspace/lib/session-flag.js +35 -0
- package/workspace/package-lock.json +1 -1
- package/workspace/package.json +3 -3
- package/workspace/public/pm/modes-live.js +52 -4
- package/workspace/server.js +108 -5
- package/workspace/skills/morning-brief.skill +1 -0
|
@@ -13,6 +13,11 @@ import { PROCESS_LIMIT_MS, isFresh, parseFactsOutput, sliceRows, summarize } fro
|
|
|
13
13
|
import { clean, formatStatus } from './lib/status-line.mjs'
|
|
14
14
|
import { SECTION_ID, buildSection } from './lib/prompt-section.mjs'
|
|
15
15
|
import { PROBES, aheadProbe, interpretGit } from './lib/probes.mjs'
|
|
16
|
+
import { describeMode, detectMode, slicePolicy } from './lib/mode.mjs'
|
|
17
|
+
import { UNPUSHED_PROBES, cloudBandText, interpretUnpushed } from './lib/unpushed.mjs'
|
|
18
|
+
import { HANDOVER_PROBES, buildPlan, countSince, interpretSince, notePaths, parseNote, planStillGood, renderNote, resumeText, sinceProbes, subjectsProbe } from './lib/handover.mjs'
|
|
19
|
+
import { carryOut } from './lib/handover-run.mjs'
|
|
20
|
+
import { REVIEWER_AGENT, REVIEWER_PROMPT, REVIEWER_TOOLS, REVIEWER_TYPE, buildReviewPrompt, offerText, parseVerdict, pickReviewerModel, planEval } from './lib/review.mjs'
|
|
16
21
|
|
|
17
22
|
const status = atom({ plugin: 'pmos-cockpit', key: 'status' } as const, null)
|
|
18
23
|
const isHidden = atom({ plugin: 'pmos-cockpit', key: 'isHidden' } as const, false)
|
|
@@ -22,6 +27,19 @@ const offer = atom({ plugin: 'pmos-cockpit', key: 'offer' } as const, null)
|
|
|
22
27
|
// of the ladder. `factsCache` holds the last ladder read, for one initiative at one commit.
|
|
23
28
|
const view = atom({ plugin: 'pmos-cockpit', key: 'view' } as const, null)
|
|
24
29
|
const factsCache = atom({ plugin: 'pmos-cockpit', key: 'factsCache' } as const, null)
|
|
30
|
+
// Where this session runs (M12). Read from two environment variables when the mod loads and kept here;
|
|
31
|
+
// nothing a tool call does afterwards moves it. It changes what is drawn, never what is guarded.
|
|
32
|
+
const modeAtom = atom({ plugin: 'pmos-cockpit', key: 'mode' } as const, null)
|
|
33
|
+
// Continuity (M13). The plan `/pmos-handover` showed, waiting for a person's confirmation; it holds the
|
|
34
|
+
// facts of the checkout it was made for. The resume summary read at session start, and whether the
|
|
35
|
+
// person dismissed it. None of the three is ever given to the model.
|
|
36
|
+
const handoverPlan = atom({ plugin: 'pmos-cockpit', key: 'handoverPlan' } as const, null)
|
|
37
|
+
const resume = atom({ plugin: 'pmos-cockpit', key: 'resume' } as const, null)
|
|
38
|
+
const resumeGone = atom({ plugin: 'pmos-cockpit', key: 'resumeGone' } as const, false)
|
|
39
|
+
// The independent evaluator (M14). `review` is the one reviewer in flight; `reviewOffer` is its answer,
|
|
40
|
+
// held until a person presses Record or types /pmos-record-review. Neither is ever written to a ledger.
|
|
41
|
+
const review = atom({ plugin: 'pmos-cockpit', key: 'review' } as const, null)
|
|
42
|
+
const reviewOffer = atom({ plugin: 'pmos-cockpit', key: 'reviewOffer' } as const, null)
|
|
25
43
|
// The width the person's terminal draws the band at, seen at the last render. Lost on a reload, which
|
|
26
44
|
// only returns the status line to its default width.
|
|
27
45
|
let lastColumns = 0
|
|
@@ -52,6 +70,14 @@ async function gitFacts($: any, root: string) {
|
|
|
52
70
|
return { branch, defaultBranch, commitsAhead }
|
|
53
71
|
}
|
|
54
72
|
|
|
73
|
+
// Read the two variables once per load. The names are literals, as the engine requires.
|
|
74
|
+
async function readMode($: any): Promise<{ mode: string; signal: string | null }> {
|
|
75
|
+
const get = async (name: 'CLAUDE_CODE_REMOTE' | 'PMOS_WORKSPACE'): Promise<string | undefined> => {
|
|
76
|
+
try { return name === 'CLAUDE_CODE_REMOTE' ? await $.env.get('CLAUDE_CODE_REMOTE') : await $.env.get('PMOS_WORKSPACE') } catch { return undefined }
|
|
77
|
+
}
|
|
78
|
+
return detectMode({ remote: await get('CLAUDE_CODE_REMOTE'), workspace: await get('PMOS_WORKSPACE') })
|
|
79
|
+
}
|
|
80
|
+
|
|
55
81
|
const isPerson = (e: any): boolean => e?.origin?.kind === 'composer' || e?.origin?.kind === 'bridge'
|
|
56
82
|
|
|
57
83
|
async function readText($: any, path: string): Promise<string | null> {
|
|
@@ -308,6 +334,142 @@ async function repair($: any): Promise<CockpitStatus> {
|
|
|
308
334
|
return gather($, done, error)
|
|
309
335
|
}
|
|
310
336
|
|
|
337
|
+
// ── continuity (M13) ─────────────────────────────────────────────────────────────────────────────
|
|
338
|
+
// What the checkout looks like to a handover: git, the initiative the branch names, and what is changed
|
|
339
|
+
// or unpushed. Read-only; every process has the 5 second limit.
|
|
340
|
+
async function readCheckout($: any, root: string, layout: any) {
|
|
341
|
+
const run = (argv: string[], limit = PROCESS_LIMIT_MS) => sh($, argv, { cwd: root, timeoutMs: limit })
|
|
342
|
+
const r: Record<string, any> = {}
|
|
343
|
+
for (const k of ['inside', 'branch', 'sha', 'dirs', 'defaultBranch'] as const) r[k] = await run(PROBES[k])
|
|
344
|
+
const g = interpretGit(r)
|
|
345
|
+
const idGuess = g.branch?.startsWith('initiative/') ? g.branch.slice('initiative/'.length) : null
|
|
346
|
+
const rp = idGuess ? recordPath(layout, idGuess) : null
|
|
347
|
+
const initiative = resolveInitiative({ branch: g.noGit ? null : g.branch, recordExists: rp ? await $.fs.exists(rp) : null, commitsAhead: null })
|
|
348
|
+
let porcelain = ''
|
|
349
|
+
let hasOrigin = false
|
|
350
|
+
let unpushed = { unpushed: false }
|
|
351
|
+
if (!g.noGit) {
|
|
352
|
+
const st = await run(HANDOVER_PROBES.status as string[])
|
|
353
|
+
porcelain = st.exitCode === 0 ? st.stdout : ''
|
|
354
|
+
hasOrigin = (await run(HANDOVER_PROBES.origin as string[])).exitCode === 0
|
|
355
|
+
const u: Record<string, any> = {}
|
|
356
|
+
for (const [k, argv] of Object.entries(UNPUSHED_PROBES)) u[k] = await run(argv as string[])
|
|
357
|
+
unpushed = interpretUnpushed(u)
|
|
358
|
+
}
|
|
359
|
+
return { run, g, initiative, porcelain, hasOrigin, unpushed }
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// What the plan, and the confirmation that follows it, compare: the facts a plan was made for.
|
|
363
|
+
const keyOf = (c: any, words: string) => ({ branch: c.g.branch, sha: c.g.sha, porcelain: c.porcelain, words })
|
|
364
|
+
|
|
365
|
+
async function planHandover($: any, words: string): Promise<string> {
|
|
366
|
+
const { root, layout } = await rootAndLayout($)
|
|
367
|
+
const c = await readCheckout($, root, layout)
|
|
368
|
+
const p = buildPlan({ layout, root, initiative: c.initiative, git: c.g, porcelain: c.porcelain, hasOrigin: c.hasOrigin, unpushed: c.unpushed, words, nowMs: await $.clock.now() })
|
|
369
|
+
if (!p.ok) { await update($, handoverPlan, () => null); return p.text }
|
|
370
|
+
await update($, handoverPlan, () => p.plan)
|
|
371
|
+
return p.text
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Called only from a person's typed command or press. The plan must still match the checkout.
|
|
375
|
+
async function confirmHandover($: any, push: boolean): Promise<string> {
|
|
376
|
+
const plan: any = await read($, handoverPlan)
|
|
377
|
+
const { root, layout } = await rootAndLayout($)
|
|
378
|
+
const c = await readCheckout($, root, layout)
|
|
379
|
+
const now: number = await $.clock.now()
|
|
380
|
+
const good = planStillGood(plan, plan ? keyOf(c, plan.words) : { branch: null, sha: null, porcelain: '', words: '' }, now)
|
|
381
|
+
if (!good.ok) {
|
|
382
|
+
await update($, handoverPlan, () => null)
|
|
383
|
+
return `Nothing was written. ${good.why}${plan ? ' Run /pmos-handover again to see a new plan.' : ''}`
|
|
384
|
+
}
|
|
385
|
+
// the note's facts, read fresh: the ladder for this initiative, and how far the branch is from the default
|
|
386
|
+
await refreshView($, true)
|
|
387
|
+
const v: any = await read($, view)
|
|
388
|
+
const def: string = c.g.defaultBranch ?? 'main'
|
|
389
|
+
const ahead = await c.run(['git', 'rev-list', '--count', `${def}..HEAD`])
|
|
390
|
+
const subj = await c.run(subjectsProbe(def) as string[])
|
|
391
|
+
const noteText = renderNote({
|
|
392
|
+
id: plan.id, branch: plan.branch, sha: c.g.sha, summary: v?.initiative?.id === plan.id ? v.summary : null,
|
|
393
|
+
commits: ahead.exitCode === 0 && /^\d{1,9}$/.test(out(ahead)) ? Number(out(ahead)) : 0, defaultBranch: def,
|
|
394
|
+
subjects: subj.exitCode === 0 ? out(subj).split('\n').filter(Boolean) : [], paths: plan.paths, words: plan.words, atIso: new Date(now).toISOString(),
|
|
395
|
+
})
|
|
396
|
+
// a note path that is a link, or that sits behind one, is never written through
|
|
397
|
+
const rootReal = (await $.fs.stat(root, { resolve: true }).catch(() => undefined))?.realPath ?? root
|
|
398
|
+
const noteDir = plan.noteRel.split('/').slice(0, -1).join('/')
|
|
399
|
+
const write = async (path: string, text: string) => {
|
|
400
|
+
for (const [p, expect] of [[path, `${rootReal}/${plan.noteRel}`], [`${root}/${noteDir}`, `${rootReal}/${noteDir}`]] as const) {
|
|
401
|
+
const st = await $.fs.stat(p, { resolve: true }).catch(() => undefined)
|
|
402
|
+
if (st && (st.isLink || (st.realPath && st.realPath !== expect))) throw new Error(`${p} is, or sits behind, a link`)
|
|
403
|
+
}
|
|
404
|
+
await $.fs.write(path, text)
|
|
405
|
+
}
|
|
406
|
+
const r = await carryOut({ plan, push, noteText, run: c.run, write })
|
|
407
|
+
await update($, handoverPlan, () => null)
|
|
408
|
+
await update($, resume, () => null)
|
|
409
|
+
await refreshView($, true)
|
|
410
|
+
return r.text
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
async function pressHandover($: any, push: boolean) {
|
|
414
|
+
try { $.ui.toast(`pmos-cockpit: ${await confirmHandover($, push)}`) } catch (e: any) { $.ui.toast(`pmos-cockpit: the handover did not run. ${String(e?.message ?? e)}`) }
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// At session start: the newest note for this branch's initiative, and how far the branch has moved.
|
|
418
|
+
async function loadResume($: any): Promise<void> {
|
|
419
|
+
try {
|
|
420
|
+
const { root, layout } = await rootAndLayout($)
|
|
421
|
+
const v: any = await read($, view)
|
|
422
|
+
const id: string | null = v?.initiative?.state === 'ok' ? v.initiative.id : null
|
|
423
|
+
const np = id === null ? null : notePaths(layout, root, id)
|
|
424
|
+
const text = np === null ? null : await readText($, np.abs)
|
|
425
|
+
const note = text === null ? null : parseNote(text)
|
|
426
|
+
if (id === null || np === null || note === null || note.id !== id || note.headSha === null) { await update($, resume, () => null); return }
|
|
427
|
+
const run = (argv: string[]) => sh($, argv, { cwd: root, timeoutMs: PROCESS_LIMIT_MS })
|
|
428
|
+
const pr = sinceProbes(note, np.rel) as any
|
|
429
|
+
const r: Record<string, any> = { ancestor: await run(pr.ancestor) }
|
|
430
|
+
if (r.ancestor.exitCode === 0) {
|
|
431
|
+
r.handoverCommit = await run(pr.handoverCommit)
|
|
432
|
+
const h = out(r.handoverCommit)
|
|
433
|
+
r.count = await run(countSince(/^[0-9a-f]{40,64}$/.test(h) ? h : note.headSha) as string[])
|
|
434
|
+
}
|
|
435
|
+
const line = resumeText(note, id, interpretSince(r))
|
|
436
|
+
await update($, resume, () => (line === null ? null : { text: line }))
|
|
437
|
+
} catch {
|
|
438
|
+
await update($, resume, () => null)
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Recording a reviewer's verdict: called only from a person's press or typed command. The offer is
|
|
443
|
+
// cleared before the writer runs, so a second press records nothing.
|
|
444
|
+
async function recordReview($: any): Promise<{ ok: boolean; text: string }> {
|
|
445
|
+
const o: any = await read($, reviewOffer)
|
|
446
|
+
if (!o) return { ok: false, text: 'There is no reviewer verdict waiting to be recorded.' }
|
|
447
|
+
await update($, reviewOffer, () => null)
|
|
448
|
+
const { root, layout } = await rootAndLayout($)
|
|
449
|
+
const plan = planEval({ layout, root, review: o, atIso: new Date(await $.clock.now()).toISOString() })
|
|
450
|
+
if (!plan.ok) return { ok: false, text: plan.reason }
|
|
451
|
+
const r = await sh($, plan.argv, { cwd: root, env: plan.env, timeoutMs: PROCESS_LIMIT_MS })
|
|
452
|
+
if (r.exitCode !== 0) return { ok: false, text: `Not recorded. ${(r.stderr || out(r)).trim()}` }
|
|
453
|
+
return { ok: true, text: `Recorded ${o.verdict} ${o.score} for ${o.run_id}, evaluator ${o.evaluator_model}.${plan.storesFindings ? '' : ' The findings are not stored in a kit: copy them from above if you want them kept.'}` }
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// The reviewer's answer: parsed, shown, and held. This writes nothing.
|
|
457
|
+
async function takeReviewerAnswer($: any, e: any): Promise<void> {
|
|
458
|
+
try {
|
|
459
|
+
const pending: any = await read($, review)
|
|
460
|
+
if (!pending || e.agentId !== pending.agentId) return
|
|
461
|
+
await update($, review, () => null)
|
|
462
|
+
const v = parseVerdict(e.answer)
|
|
463
|
+
if (v.ok) {
|
|
464
|
+
const o = { run_id: pending.run_id, initiative_id: pending.initiative_id, rubric_path: pending.rubric_path, verdict: v.verdict, score: v.score, evaluator_model: pending.model, findings: v.findings }
|
|
465
|
+
await update($, reviewOffer, () => o)
|
|
466
|
+
$.ui.toast(`pmos-cockpit: ${offerText(o)} Press Record above the prompt, or type /pmos-record-review.`)
|
|
467
|
+
} else {
|
|
468
|
+
$.ui.toast(`pmos-cockpit: the reviewer's answer had no verdict, so nothing is offered. ${v.reason}.`)
|
|
469
|
+
}
|
|
470
|
+
} catch { /* bookkeeping for a convenience: never reaches the turn */ }
|
|
471
|
+
}
|
|
472
|
+
|
|
311
473
|
const baseName = (p: string): string => p.split('/').filter(Boolean).pop() ?? p
|
|
312
474
|
|
|
313
475
|
// The session view (M11): one read of the checkout and, at most once a minute for each commit, one read
|
|
@@ -363,6 +525,14 @@ async function refreshView($: any, force = false): Promise<void> {
|
|
|
363
525
|
links = { record: rp ? clean(rp.replace(`${root}/`, ''), 80) : null, prd: field('prd_path'), rubric: field('rubric_path') }
|
|
364
526
|
}
|
|
365
527
|
|
|
528
|
+
// cloud only: what would be lost with the session. Read-only git; the band says it, a person acts.
|
|
529
|
+
let cloud: string | null = null
|
|
530
|
+
if (((await read($, modeAtom)) as any)?.mode === 'cloud' && !g.noGit) {
|
|
531
|
+
const u: Record<string, any> = {}
|
|
532
|
+
for (const [k, argv] of Object.entries(UNPUSHED_PROBES)) u[k] = await run(argv as string[])
|
|
533
|
+
cloud = cloudBandText(interpretUnpushed(u), g.branch)
|
|
534
|
+
}
|
|
535
|
+
|
|
366
536
|
const text = formatStatus({
|
|
367
537
|
repo: baseName(root),
|
|
368
538
|
worktree: g.linked ? baseName(root) : null,
|
|
@@ -374,7 +544,7 @@ async function refreshView($: any, force = false): Promise<void> {
|
|
|
374
544
|
})
|
|
375
545
|
$.ui.status(text)
|
|
376
546
|
const stateRel = layout.stateRoot ? layout.stateRoot.replace(`${root}/`, '') : null
|
|
377
|
-
await update($, view, () => ({ text, initiative, summary, unavailable, links, sha: g.sha, at: started, stateRel, noGit: g.noGit }))
|
|
547
|
+
await update($, view, () => ({ text, initiative, summary, unavailable, links, sha: g.sha, at: started, stateRel, noGit: g.noGit, cloud }))
|
|
378
548
|
} catch {
|
|
379
549
|
/* the view is a convenience: on any failure the last view stays, and the turn is not told */
|
|
380
550
|
}
|
|
@@ -415,10 +585,20 @@ export const register: Register = (on) => {
|
|
|
415
585
|
await $.command.register({ name: 'pmos-repair-skills', description: 'Link the kit skills that are not registered in .claude/skills' })
|
|
416
586
|
await $.command.register({ name: 'pmos-grant', description: 'Mint one single-use grant: /pmos-grant merge:<PR> | publish:<artifact> [minutes] [your words]' })
|
|
417
587
|
await $.command.register({ name: 'pmos-slice', description: 'Open the initiative slice: the ladder, step by step, for the branch you are on' })
|
|
588
|
+
await $.command.register({ name: 'pmos-handover', description: 'Hand the branch over: /pmos-handover [your words] shows a plan; /pmos-handover confirm [local] commits and pushes it' })
|
|
418
589
|
await $.command.register({ name: 'pmos-accept', description: 'Record your Acceptance of a run: /pmos-accept <run_id> pass|fail <note>' })
|
|
590
|
+
await $.command.register({ name: 'pmos-review', description: 'Ask a read-only reviewer on another model to judge the newest run: /pmos-review [model]' })
|
|
591
|
+
await $.command.register({ name: 'pmos-record-review', description: 'Record the reviewer verdict that is waiting (the same as pressing Record)' })
|
|
592
|
+
try {
|
|
593
|
+
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] })
|
|
594
|
+
} catch { /* the review command says so when it cannot start the reviewer */ }
|
|
595
|
+
const m = await readMode($)
|
|
596
|
+
await update($, modeAtom, () => m)
|
|
419
597
|
const s = await gather($)
|
|
420
598
|
await update($, status, () => s)
|
|
421
599
|
await refreshView($)
|
|
600
|
+
await update($, resumeGone, () => false)
|
|
601
|
+
await loadResume($)
|
|
422
602
|
if (s.skills && s.skills.missing.length > 0) {
|
|
423
603
|
$.ui.toast(`PMOS: ${s.skills.missing.length} kit skill(s) are not registered. Run /pmos-repair-skills.`)
|
|
424
604
|
}
|
|
@@ -431,16 +611,24 @@ export const register: Register = (on) => {
|
|
|
431
611
|
await refreshView($, true)
|
|
432
612
|
const v: any = await read($, view)
|
|
433
613
|
const lines = [summary(s)]
|
|
614
|
+
const m: any = await read($, modeAtom)
|
|
615
|
+
if (m) lines.push(describeMode(m))
|
|
616
|
+
const rs: any = await read($, resume)
|
|
617
|
+
if (rs?.text) lines.push(`resume ${String(rs.text).replace(/^PMOS: /, '')}`)
|
|
618
|
+
try { lines.push(`surfaces ${(await $.session.surfaces()).join(', ') || 'none reported'}`) } catch { lines.push('surfaces unavailable') }
|
|
434
619
|
if (v?.summary) lines.push(`stage ${v.summary.stage}${v.summary.next ? ` (next: ${v.summary.next.key})` : ''}${v.summary.gateFailures.length ? `; blocking: ${v.summary.gateFailures.join(', ')}` : ''}`)
|
|
435
620
|
else if (v?.unavailable) lines.push(`stage unavailable: ${v.unavailable}`)
|
|
436
621
|
return { text: lines.join('\n') }
|
|
437
622
|
})
|
|
438
623
|
|
|
439
|
-
// The slice opens only when asked
|
|
624
|
+
// The slice opens only when asked, and not at all when the workspace already shows it (hosted).
|
|
440
625
|
on('command.run', { command: 'pmos-slice' }, async ($) => {
|
|
626
|
+
const m: any = await read($, modeAtom)
|
|
627
|
+
const policy = slicePolicy(m?.mode ?? 'local')
|
|
628
|
+
if (!policy.opens) return { text: policy.text }
|
|
441
629
|
await refreshView($)
|
|
442
630
|
await $.ui.open({ id: 'pmos-slice', title: 'PMOS initiative' })
|
|
443
|
-
return { text:
|
|
631
|
+
return { text: policy.text }
|
|
444
632
|
})
|
|
445
633
|
|
|
446
634
|
on('ui.render', { component: 'Pane', requestId: 'pmos-slice' }, async ($, e) => {
|
|
@@ -479,6 +667,7 @@ export const register: Register = (on) => {
|
|
|
479
667
|
|
|
480
668
|
// The status line follows the branch: refreshed at the end of each turn, never per tool call.
|
|
481
669
|
on('turn.complete', async ($, e, next) => {
|
|
670
|
+
await takeReviewerAnswer($, e)
|
|
482
671
|
await refreshView($)
|
|
483
672
|
return next(e)
|
|
484
673
|
})
|
|
@@ -540,6 +729,43 @@ export const register: Register = (on) => {
|
|
|
540
729
|
return v
|
|
541
730
|
})
|
|
542
731
|
|
|
732
|
+
// ── the independent evaluator (M14) ─────────────────────────────────────────────────────────
|
|
733
|
+
// The builder's model never sees the reviewer in its list of agents: only the person starts it.
|
|
734
|
+
on('agent.offer', { agent: REVIEWER_TYPE }, () => ({ isOffered: false }))
|
|
735
|
+
|
|
736
|
+
on('command.run', { command: 'pmos-review' }, async ($, e) => {
|
|
737
|
+
if (!isPerson(e)) return { text: 'A review is started only by your own typed command. This run did not come from you, so nothing was started.' }
|
|
738
|
+
if (await read($, review)) return { text: 'A review is already running. Its verdict will appear above the prompt.' }
|
|
739
|
+
try {
|
|
740
|
+
const { root, layout } = await rootAndLayout($)
|
|
741
|
+
if (layout.kind === 'none') return { text: 'No review started. There is no PMOS state here.' }
|
|
742
|
+
const facts = await gitFacts($, root)
|
|
743
|
+
const id = facts.branch?.startsWith('initiative/') ? facts.branch.slice('initiative/'.length) : null
|
|
744
|
+
if (id === null) return { text: 'No review started. The branch names no initiative.' }
|
|
745
|
+
const run = (await runsOf($, layout)).filter((r) => r.initiative_id === id).pop()
|
|
746
|
+
if (!run) return { text: `No review started. Initiative ${id} has no run in ${layout.stateRoot ?? 'the state root'}.` }
|
|
747
|
+
const rp = recordPath(layout, id)
|
|
748
|
+
const rec = rp ? await readText($, rp) : null
|
|
749
|
+
const m = rec === null ? null : /^rubric_path:[ \t]*(\S[^\n]*)$/m.exec(rec)
|
|
750
|
+
const rubricPath = m ? m[1].replace(/^["']|["']$/g, '').trim() : ''
|
|
751
|
+
const model = pickReviewerModel(await $.session.model(), String(e.args ?? '').trim())
|
|
752
|
+
if (!model.ok) return { text: `No review started. ${model.reason}` }
|
|
753
|
+
const prompt = buildReviewPrompt({ initiativeId: id, runId: run.run_id, rubricPath })
|
|
754
|
+
if (!prompt.ok) return { text: `No review started. ${prompt.reason}.` }
|
|
755
|
+
const sp: any = await $.agent.spawn({ subagentType: REVIEWER_TYPE, prompt: prompt.text, description: `review ${run.run_id}`, model: model.model })
|
|
756
|
+
if (sp?.deny || typeof sp?.agentId !== 'string') return { text: `No review started. ${sp?.deny ?? 'the reviewer could not be started.'}` }
|
|
757
|
+
await update($, review, () => ({ agentId: sp.agentId, run_id: run.run_id, initiative_id: id, rubric_path: rubricPath, model: model.model }))
|
|
758
|
+
return { text: `Started a read-only reviewer on ${model.model} (builder family ${model.builderFamily}) for run ${run.run_id}. Its verdict will appear above the prompt, and nothing is recorded until you press Record.` }
|
|
759
|
+
} catch (err: any) {
|
|
760
|
+
return { text: `No review started. ${String(err?.message ?? err).slice(0, 200)}` }
|
|
761
|
+
}
|
|
762
|
+
})
|
|
763
|
+
|
|
764
|
+
on('command.run', { command: 'pmos-record-review' }, async ($, e) => {
|
|
765
|
+
if (!isPerson(e)) return { text: 'A verdict is recorded only by your own typed command or press. This run did not come from you, so nothing was written.' }
|
|
766
|
+
return { text: (await recordReview($)).text }
|
|
767
|
+
})
|
|
768
|
+
|
|
543
769
|
// ── commands that are a person's act ────────────────────────────────────────────────────────
|
|
544
770
|
on('command.run', { command: 'pmos-grant' }, async ($, e) => {
|
|
545
771
|
if (!isPerson(e)) return { text: 'A grant is minted only by your own typed command or press. This run did not come from you, so nothing was written.' }
|
|
@@ -550,6 +776,18 @@ export const register: Register = (on) => {
|
|
|
550
776
|
return { text: r.exitCode === 0 ? `Granted ${plan.scope.kind}:${plan.scope.target}, single use, ${plan.ttl} minutes. ${out(r).split('\n').pop()}` : `No grant written. ${(r.stderr || out(r)).trim()}` }
|
|
551
777
|
})
|
|
552
778
|
|
|
779
|
+
// A handover is a person's act: a plan first, and only a typed confirm or a press carries it out.
|
|
780
|
+
on('command.run', { command: 'pmos-handover' }, async ($, e) => {
|
|
781
|
+
if (!isPerson(e)) return { text: 'A handover commits and pushes only on your own typed command or press. This run did not come from you, so nothing was done.' }
|
|
782
|
+
const args = String(e.args ?? '').trim()
|
|
783
|
+
const m = /^confirm(?:[ \t]+(local))?$/.exec(args)
|
|
784
|
+
try {
|
|
785
|
+
return { text: m ? await confirmHandover($, m[1] !== 'local') : await planHandover($, args) }
|
|
786
|
+
} catch (err: any) {
|
|
787
|
+
return { text: `Nothing was done. The handover could not run: ${String(err?.message ?? err)}` }
|
|
788
|
+
}
|
|
789
|
+
})
|
|
790
|
+
|
|
553
791
|
on('command.run', { command: 'pmos-accept' }, async ($, e) => {
|
|
554
792
|
if (!isPerson(e)) return { text: 'An acceptance is recorded only by your own typed command or press. This run did not come from you, so nothing was written.' }
|
|
555
793
|
const a = parseAccept(e.args)
|
|
@@ -584,6 +822,29 @@ export const register: Register = (on) => {
|
|
|
584
822
|
</Box>
|
|
585
823
|
)
|
|
586
824
|
}
|
|
825
|
+
const hp: any = await read($, handoverPlan)
|
|
826
|
+
if (hp && !e.props.hasSurvey) {
|
|
827
|
+
const { Box, Button, Text } = $.ui.resolve(e)
|
|
828
|
+
return (
|
|
829
|
+
<Box>
|
|
830
|
+
<Text>PMOS: handover plan for {hp.id}, {hp.paths.length} path(s){hp.push.run ? `, then ${hp.push.text}` : ', no push'}. </Text>
|
|
831
|
+
{hp.push.run && <Button key="go" label="Commit and push" variant="primary" onPress={() => pressHandover($, true)} />}
|
|
832
|
+
<Button key="local" label="Commit only" onPress={() => pressHandover($, false)} />
|
|
833
|
+
<Button key="cancel" label="Cancel" onPress={() => update($, handoverPlan, () => null)} />
|
|
834
|
+
</Box>
|
|
835
|
+
)
|
|
836
|
+
}
|
|
837
|
+
const rv: any = await read($, reviewOffer)
|
|
838
|
+
if (rv && !e.props.hasSurvey) {
|
|
839
|
+
const { Box, Button, Text } = $.ui.resolve(e)
|
|
840
|
+
return (
|
|
841
|
+
<Box>
|
|
842
|
+
<Text>PMOS: {offerText(rv)} </Text>
|
|
843
|
+
<Button key="record" label="Record" variant="primary" onPress={async () => { const r = await recordReview($); $.ui.toast(`pmos-cockpit: ${r.text}`) }} />
|
|
844
|
+
<Button key="later" label="Not now" onPress={() => update($, reviewOffer, () => null)} />
|
|
845
|
+
</Box>
|
|
846
|
+
)
|
|
847
|
+
}
|
|
587
848
|
if (e.props.hasSurvey || (await read($, isHidden))) return next(e)
|
|
588
849
|
// one line, only when something blocks, in this order: skill drift, a blocking ladder step, a branch
|
|
589
850
|
// with commits and no initiative record. The text comes from checked fields, never from a record.
|
|
@@ -608,6 +869,25 @@ export const register: Register = (on) => {
|
|
|
608
869
|
</Box>
|
|
609
870
|
)
|
|
610
871
|
}
|
|
872
|
+
if (!hasDrift(s) && v?.cloud) {
|
|
873
|
+
const { Box, Button, Text } = $.ui.resolve(e)
|
|
874
|
+
return (
|
|
875
|
+
<Box>
|
|
876
|
+
<Text>{clean(v.cloud, 300)} </Text>
|
|
877
|
+
<Button key="hide" label="Hide" onPress={() => update($, isHidden, () => true)} />
|
|
878
|
+
</Box>
|
|
879
|
+
)
|
|
880
|
+
}
|
|
881
|
+
const rs: any = await read($, resume)
|
|
882
|
+
if (!hasDrift(s) && rs?.text && !(await read($, resumeGone))) {
|
|
883
|
+
const { Box, Button, Text } = $.ui.resolve(e)
|
|
884
|
+
return (
|
|
885
|
+
<Box>
|
|
886
|
+
<Text>{clean(rs.text, 400)} </Text>
|
|
887
|
+
<Button key="dismiss" label="Dismiss" onPress={() => update($, resumeGone, () => true)} />
|
|
888
|
+
</Box>
|
|
889
|
+
)
|
|
890
|
+
}
|
|
611
891
|
if (!hasDrift(s)) return next(e)
|
|
612
892
|
const { Box, Button, Text } = $.ui.resolve(e)
|
|
613
893
|
const k = s!.skills!
|
|
@@ -49,12 +49,28 @@ export type ViewState = {
|
|
|
49
49
|
at: number
|
|
50
50
|
stateRel: string | null
|
|
51
51
|
noGit: boolean
|
|
52
|
+
cloud: string | null
|
|
52
53
|
}
|
|
53
54
|
|
|
54
55
|
export type FactsCache = { id: string | null; sha: string; at: number; summary: NonNullable<ViewState['summary']> }
|
|
55
56
|
|
|
57
|
+
export type HandoverPlan = {
|
|
58
|
+
id: string
|
|
59
|
+
branch: string
|
|
60
|
+
noteRel: string
|
|
61
|
+
noteAbs: string
|
|
62
|
+
paths: string[]
|
|
63
|
+
addPaths: string[]
|
|
64
|
+
push: { run: boolean; argv: string[] | null; text: string }
|
|
65
|
+
message: string
|
|
66
|
+
words: string
|
|
67
|
+
key: string
|
|
68
|
+
at: number
|
|
69
|
+
sha: string | null
|
|
70
|
+
}
|
|
71
|
+
|
|
56
72
|
declare module 'claude-code' {
|
|
57
73
|
interface PluginState {
|
|
58
|
-
'pmos-cockpit': { status: CockpitStatus | null; isHidden: boolean; offer: { run_id: string; initiative_id: string; message: string } | null; view: ViewState | null; factsCache: FactsCache | null }
|
|
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 }
|
|
59
75
|
}
|
|
60
76
|
}
|
|
@@ -46,9 +46,10 @@ OKF's reserved filenames carry **no** frontmatter and are skipped by both the mi
|
|
|
46
46
|
The enforced list lives in [`scripts/frontmatter_registry.txt`](/scripts/frontmatter_registry.txt) — the single source `frontmatter_lint.py` reads. A `type:` value not in it fails CI (this catches typos).
|
|
47
47
|
|
|
48
48
|
- **OKF core:** `Concept`, `ADR`, `Template`, `Playbook`, `Reference`
|
|
49
|
-
- **Planning:** `Initiative`, `PRD`, `Story`, `Rubric`, `OKR`, `Radar`, `Checklist`, `Calibration`, `Research`, `Design`, `Prototype`
|
|
49
|
+
- **Planning:** `Initiative`, `PRD`, `Story`, `Rubric`, `OKR`, `Radar`, `Checklist`, `Calibration`, `Research`, `Design`, `Prototype`, `Handover`
|
|
50
50
|
- `Initiative` is the repo-native initiative record in [`planning/initiatives/`](/planning/initiatives/), read by the [Initiative Gate](/.github/scripts/initiative-gate.sh). Its D56 **work-type lane** rides a separate `lane:` key — not `type:` — so the lane vocabulary and this registry never collide.
|
|
51
51
|
- `Story` is a **user-story document** — the decomposition of a spec into the units of work handed to whoever builds them. It is the one type in this registry that is also **checked positionally** (see below), and it may carry an optional `jira:` / `confluence:` / `google_doc:` key naming the tracker record it corresponds to. That key is a *declaration* the workspace displays, never a lookup: PMOS holds no credential and makes no outbound request.
|
|
52
|
+
- `Handover` is the note the cockpit mod's `/pmos-handover` writes (`planning/handovers/<id>.md`, or `pmos/state/handovers/<id>.md` in a kit): one per initiative, replaced on each handover, with checked fields (`id`, `head_sha`, `branch`, `stage`, `next`, `commits`, `files`, `handover_at`) read back by the resume summary ([D81](/okf/products/pmos/adr/d81-cockpit-mod-and-grants.md)).
|
|
52
53
|
- **Root governance:** `Decision`, `Principle`, `Debt`, `Discovery`, `Guide`
|
|
53
54
|
|
|
54
55
|
### When the path decides the type
|
|
@@ -72,6 +73,10 @@ Add **one line** to `scripts/frontmatter_registry.txt` and a matching row above.
|
|
|
72
73
|
- [`scripts/frontmatter_lint.py`](/scripts/frontmatter_lint.py) — deterministic, no secrets; asserts the three required keys (and `type` ∈ registry) on every non-reserved tracked `.md`. Run in `.github/workflows/frontmatter-ci.yml`.
|
|
73
74
|
- [`scripts/backfill_frontmatter.py`](/scripts/backfill_frontmatter.py) — the one-time migration: prepend-only, idempotent; reuses a file's already-declared `type`/`date` when present, else infers from path (`type`), the first H1 (`title`), and the last git-commit date (`timestamp`).
|
|
74
75
|
|
|
76
|
+
## Typed relation keys (opt-in)
|
|
77
|
+
|
|
78
|
+
A document may carry `supersedes`, `amended_by`, `cites` and `initiative` as one-line lists of paths to `.md` files. The knowledge graph (`pmos kg`) reads them as stated edges of that type. The keys are optional and are not backfilled. See [the knowledge-graph guide](/workspace/guide/knowledge-graph.md).
|
|
79
|
+
|
|
75
80
|
## Tenant repositories follow OKF v0.2 (D73)
|
|
76
81
|
|
|
77
82
|
Since [D74](/okf/products/pmos/adr/d74-pmos-self-drops-timestamp.md) PMOS's own repository follows OKF v0.2 too: the Frontmatter Gate requires `type` and `title`, `timestamp` is optional and still checked when present, and `generated`, `sources`, `status`, `verified` and `stale_after` are accepted. Nothing was backfilled. The documents PMOS and its agents write into a **tenant** repository, such as a PM's, follow [OKF v0.2](https://github.com/GoogleCloudPlatform/knowledge-catalog/blob/main/okf/SPEC.md) instead: `type`, `title` and `description`, then `generated: {by, at}` in place of `timestamp`, `sources` in place of a body `# Citations` list, and `status` (`draft | stable | deprecated`), `verified` and `stale_after` as they apply. Every key sits on **one line**, because five of PMOS's frontmatter readers stop at the first indented line. Actors are `claude-code/<skill>` for an agent run, `human:<id>` for a person and `process:<name>` for a tool.
|
|
@@ -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-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-04T10:38:12Z"}
|
|
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-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-04T10:38:12Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
type: Reference
|
|
3
3
|
title: "SKILL-FORMAT.md — Required Format for PMOS Skills"
|
|
4
|
+
description: "The required structure of every .skill file in PMOS, and why it keeps skills cheap to keep available and expensive to load only when used."
|
|
4
5
|
timestamp: 2026-07-23
|
|
5
6
|
---
|
|
6
7
|
|
|
@@ -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-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-04T10:38:12Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
package/workspace/README.md
CHANGED
|
@@ -76,7 +76,7 @@ After the session ends, `GET /api/next-step/summary` returns the files the branc
|
|
|
76
76
|
|
|
77
77
|
## Today: what is waiting for you (`proactive-engine`, slice D1)
|
|
78
78
|
|
|
79
|
-
`lib/proactive.js` is a pure core: it reads the records the workspace already derives and finds five conditions: a need waiting on the PM, an initiative returned at acceptance, an ended next-step session with no review, a scheduled task whose last run failed, and an active initiative with no agent run for the stalled threshold (5 days by default). The ranking is fixed and stated in the guide. `GET /api/proactive/today` computes the list on each request. A scan (`POST /api/proactive/scan`, and a 10-minute automatic tick inside the existing 60-second interval) appends new items once a day to `signals.jsonl`. Feedback (`useful`, `not-useful`, `wrong-priority`, `missed` with a note, and the events `opened`, `dismissed`) is appended to `proactive-feedback.jsonl`; a dismissed item is hidden for 7 days. Settings are in `proactive-settings.json` (engine on or off, one trigger off, the stalled threshold, quiet hours, pause learning
|
|
79
|
+
`lib/proactive.js` is a pure core: it reads the records the workspace already derives and finds five conditions: a need waiting on the PM, an initiative returned at acceptance, an ended next-step session with no review, a scheduled task whose last run failed, and an active initiative with no agent run for the stalled threshold (5 days by default). The ranking is fixed and stated in the guide. `GET /api/proactive/today` computes the list on each request. A scan (`POST /api/proactive/scan`, and a 10-minute automatic tick inside the existing 60-second interval) appends new items once a day to `signals.jsonl`. Feedback (`useful`, `not-useful`, `wrong-priority`, `missed` with a note, and the events `opened`, `dismissed`) is appended to `proactive-feedback.jsonl`; a dismissed item is hidden for 7 days. Settings are in `proactive-settings.json` (engine on or off, one trigger off, the stalled threshold, quiet hours, pause learning). The same actions are in `pmos proactive today|scan|settings|feedback` (`lib/proactive-cli.js`), a thin client of those routes. The list is drawn in the navigation column by the `today` drawer item. It runs only while the server runs, reads no mail or calendar, and starts no session, sends nothing and changes no record of an initiative (`proactive.test.js` E25-E26 read the source for that). Tests: `proactive.test.js` (pure core, routes, CLI) and `pm-r22-test.js` (the page). The guide page is `guide/proactive-engine.md`. **Learning (D4).** `lib/learn.js` is a second pure core. It joins the PM's verdicts, events and step reviews to items through the candidate id in `signals.jsonl` (which now records the initiative's lane), moves three bounded weights (per trigger, per lane, per trigger and time of day; caps 8, 4, 3 and 15 in all, no cell before 5 events) that `rank` adds to the fixed score, reports a 7-day rollup with every count and a watermelon check, leaves a trigger off Today when it was rated useful under 30% of the time over 10 or more ratings in 28 days, and proposes lessons from four templates. A lesson is approved, rejected or retired only by the PM's press; an approved lesson is read by the morning-brief session through `pmos proactive learn lessons` and goes into no other prompt, so the C1 prompt and plan hash are unchanged. The weekly review runs from the existing 10-minute proactive tick and does not catch up. needs and returned are never lowered or narrowed. Pause learning gives zero weights and no review. Ledgers: `learn-events.jsonl`, `lessons.jsonl`, `proactive-rollups.jsonl`. Routes `GET /api/proactive/learn` and `POST /api/proactive/learn/{run,lesson,restore,reset}`; commands `pmos proactive learn …`. Tests: `learn.test.js` and `pm-r26-test.js`.
|
|
80
80
|
|
|
81
81
|
## The morning brief (`proactive-engine`, slice D2)
|
|
82
82
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
type: Guide
|
|
3
3
|
title: "Guide: find knowledge without reading whole files"
|
|
4
|
+
description: "How to use pmos kg to find one card, section or decision without reading whole files, and the limits of the map."
|
|
4
5
|
timestamp: 2026-10-03
|
|
5
6
|
---
|
|
6
7
|
|
|
@@ -18,7 +19,7 @@ The map has an **edge** only where a file states a relation:
|
|
|
18
19
|
|---|---|
|
|
19
20
|
| link | A Markdown link to a tracked file. |
|
|
20
21
|
| frontmatter | A path in `prd_path`, `rubric_path`, `explainer_path` or `sources`. |
|
|
21
|
-
| supersedes, amended_by, cites | A document that chooses to carry one of these keys. |
|
|
22
|
+
| supersedes, amended_by, cites, initiative | A document that chooses to carry one of these keys. |
|
|
22
23
|
| initiative | An `initiative_id` that names an initiative record. |
|
|
23
24
|
| anchor | A `parent_kr` of `health:<id>` or a KR id. |
|
|
24
25
|
| cites (mention) | A `D<nn>` in the text, such as `D65`, with two or more digits. |
|
|
@@ -27,6 +28,10 @@ A link that points at a missing file is **dangling**. It is listed and never bec
|
|
|
27
28
|
|
|
28
29
|
It does not guess. A file that talks about a topic without a link, a path or a `D<nn>` is not connected to it. It does not use embeddings or a model, and it does not use the network.
|
|
29
30
|
|
|
31
|
+
## Typed keys
|
|
32
|
+
|
|
33
|
+
A document may carry `supersedes`, `amended_by`, `cites` and `initiative` in its frontmatter. Each key is one line and holds one or more paths to `.md` files, separated by commas. The map reads each path as a stated edge of that type. A path to a file that does not exist is listed as dangling and never becomes an edge. Nobody fills these keys in for existing documents; add them to new or edited ones. `pmos kg stats` shows how many edges are typed and how many are inferred from a `D<nn>` in the text.
|
|
34
|
+
|
|
30
35
|
## Commands
|
|
31
36
|
|
|
32
37
|
| Command | What it does |
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
type: Guide
|
|
3
3
|
title: "Guide: Today and the proactive engine"
|
|
4
|
+
description: "What the Today item shows, how the morning brief is made, and how the workspace works out what is waiting for you."
|
|
4
5
|
timestamp: 2026-10-03
|
|
5
6
|
---
|
|
6
7
|
|
|
@@ -39,6 +40,22 @@ From a Today item that names an initiative, you can ask for the plan and approve
|
|
|
39
40
|
|
|
40
41
|
The states of an entry are queued, started, skipped, expired and cancelled.
|
|
41
42
|
|
|
43
|
+
## What it learns
|
|
44
|
+
|
|
45
|
+
Today learns from you, and only from you. The evidence is your own records on this computer: the verdict you give an item, what you do with it (opened, acted, dismissed, ignored), and your accept or send-back on a prepared session. The workspace makes no model call to do this. The weights are arithmetic, and the lessons are written from fixed templates filled with counts.
|
|
46
|
+
|
|
47
|
+
- **Weights.** Three adjustments are added to an item's place in the order: one for its trigger (at most 8 points), one for its lane (at most 4) and one for its trigger at this time of day (at most 3). Time of day is morning 05-12, afternoon 12-17, evening 17-22 and night, in your local time. A cell moves only after 5 events, and moves less than the raw average. The total is at most 15, so learning reorders items that are close and cannot turn the stated order upside down. An item shows its adjustment and the counts behind it.
|
|
48
|
+
- **What it never does.** It never lowers or hides what blocks on you: the needs and returned triggers get no negative adjustment and are never narrowed. Items of the morning brief teach no weight, because their order is fixed. Pause learning makes every weight zero and stops the weekly review, the proposals and the narrowing; feedback is still recorded.
|
|
49
|
+
- **The weekly review.** About every 7 days, only while the workspace server runs, the workspace takes one snapshot. It does not catch up after a gap: it takes one snapshot of the last 7 days. **run the weekly review now** does the same on demand and is refused while learning is paused. Quiet hours hold scans, not this review.
|
|
50
|
+
- **The rollup.** For the last 7 days: the hit rate (useful and acted, over useful, acted, not useful, wrong priority and dismissed), the used-as-is rate (accepted over accepted and sent back), corrections per prepared run (sends-back over every session started from a step, reviewed or not), and the items you reported as missed. Every rate shows its count. With fewer than 5 rated items, or 3 runs, it says there is not enough evidence and shows no rate.
|
|
51
|
+
- **The watermelon check.** It looks only when the hit rate is 0.6 or more. It then names any of these: 2 or more items reported as missed, a used-as-is rate under 0.5 over 3 or more reviewed runs, or 0.5 or more corrections per prepared run over 3 or more runs. A good hit rate with one of these under it is not a good week.
|
|
52
|
+
- **Lessons.** The review proposes lessons from four templates: a trigger that was useful under 40% or at least 80% of the time (5 or more ratings), items you reported as missed (2 or more), and a trigger that did badly in one part of the day only. A lesson is a proposal. It is approved, rejected or retired only when you press the button. A rejected lesson is not proposed again for 28 days. At most 10 lessons are approved at once.
|
|
53
|
+
- **Where an approved lesson goes.** Into the state folder, and from there to the morning-brief session, which reads it with `pmos proactive learn lessons`. It goes into no other prompt, so the prompt and the plan hash of a prepared step do not change, and no file of any repository is written. To keep a lesson in repository memory, copy it there yourself.
|
|
54
|
+
- **Narrowing.** A trigger other than needs and returned that you rated useful under 30% of the time over 10 or more ratings in 28 days is left off Today. Today says so, with the numbers and a **restore** button. The trigger is still scanned. A restore holds until 10 new ratings of that trigger have arrived.
|
|
55
|
+
- **forget what it learned** makes the workspace ignore all earlier feedback from then on. Nothing is deleted.
|
|
56
|
+
|
|
57
|
+
The records are `learn-events.jsonl` (restore and reset), `lessons.jsonl` and `proactive-rollups.jsonl`, in the workspace state folder. They are not committed.
|
|
58
|
+
|
|
42
59
|
## What it shows
|
|
43
60
|
|
|
44
61
|
Today scans for five conditions. Each one becomes one item with the reason and the evidence.
|
|
@@ -60,7 +77,7 @@ The order is fixed and you can read it: what blocks on you first, then an unrevi
|
|
|
60
77
|
- dismiss 7 days hides the item for 7 days.
|
|
61
78
|
- something was missed records a note about what Today should have shown. It needs a note.
|
|
62
79
|
|
|
63
|
-
A verdict never changes a status, a lifecycle record or an acceptance.
|
|
80
|
+
A verdict never changes a status, a lifecycle record or an acceptance. It is recorded, and it is evidence for what Today learns, as described above.
|
|
64
81
|
|
|
65
82
|
## Settings
|
|
66
83
|
|
|
@@ -74,7 +91,7 @@ A verdict never changes a status, a lifecycle record or an acceptance. In this r
|
|
|
74
91
|
| name | Used in the greeting. |
|
|
75
92
|
| stalled after N days | The stalled threshold, from 1 to 90 days. |
|
|
76
93
|
| quiet hours | A start and an end time. No automatic scan runs between them. |
|
|
77
|
-
| pause learning |
|
|
94
|
+
| pause learning | Stops all learning: weights are zero, and there is no weekly review, no proposal and no narrowing. Feedback is still recorded. |
|
|
78
95
|
|
|
79
96
|
## The commands
|
|
80
97
|
|
|
@@ -83,12 +100,13 @@ A verdict never changes a status, a lifecycle record or an acceptance. In this r
|
|
|
83
100
|
- `pmos proactive settings` shows the settings. Add `--enabled`, `--trigger NAME=on|off`, `--stalled-days`, `--quiet HH:MM-HH:MM|none`, `--pause-learning`, `--background-prepare on|off`, `--brief-source NAME=on|off`, `--brief-scoring on|off` or `--name` to change one.
|
|
84
101
|
- `pmos proactive brief` shows today's brief. `pmos proactive brief submit --file PATH` submits one. `pmos proactive brief schedule` makes the 06:45 schedule for the current repository.
|
|
85
102
|
- `pmos proactive prepare` shows the queue. `pmos proactive prepare plan --initiative ID` shows a plan and its hash. `pmos proactive prepare queue --initiative ID --item ITEM --sha HASH` approves it. `pmos proactive prepare cancel ID` cancels an entry. `pmos proactive prepare run` looks at the queue now.
|
|
103
|
+
- `pmos proactive learn` shows the rollup, the weights, the narrowed triggers and the lessons. `pmos proactive learn run` takes the weekly review now. `pmos proactive learn lessons` lists the approved lessons. `pmos proactive learn approve ID`, `reject ID` and `retire ID` decide a lesson. `pmos proactive learn restore TRIGGER` brings back a narrowed trigger. `pmos proactive learn reset` makes it ignore earlier feedback.
|
|
86
104
|
- `pmos proactive feedback <id> useful|not-useful|wrong-priority|opened|dismissed` records feedback. Use `--missed "text"` for a missed item.
|
|
87
105
|
|
|
88
106
|
## Limits
|
|
89
107
|
|
|
90
108
|
- It runs only while the workspace server runs. It does not catch up for time the server was off. A sleeping computer gets no scan.
|
|
91
109
|
- The items from the records come from this computer. The workspace itself does not read your mail or your calendar. Only the morning-brief session does, and only the sources you switched on.
|
|
92
|
-
- It does not start a session, push, send, post, accept or merge. It only shows items and records your feedback. The one exception is background preparation, which starts a session only for a plan you approved, as described above.
|
|
110
|
+
- It does not start a session, push, send, post, accept or merge. It only shows items and records your feedback, and learns from it as described above. The one exception is background preparation, which starts a session only for a plan you approved, as described above.
|
|
93
111
|
- An unreadable repository is listed as not read. An empty list is not proof that nothing waits there.
|
|
94
112
|
- The records stay in the workspace state folder on this computer. They are not committed.
|