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.
Files changed (37) hide show
  1. package/package.json +1 -1
  2. package/payload/KIT-VERSION +1 -1
  3. package/payload/mods/pmos-cockpit/.claude-plugin/plugin.json +1 -1
  4. package/payload/mods/pmos-cockpit/README.md +24 -2
  5. package/payload/mods/pmos-cockpit/hooks/lib/handover-run.mjs +33 -0
  6. package/payload/mods/pmos-cockpit/hooks/lib/handover.mjs +267 -0
  7. package/payload/mods/pmos-cockpit/hooks/lib/mode.mjs +45 -0
  8. package/payload/mods/pmos-cockpit/hooks/lib/review.mjs +117 -0
  9. package/payload/mods/pmos-cockpit/hooks/lib/unpushed.mjs +53 -0
  10. package/payload/mods/pmos-cockpit/hooks/register.tsx +283 -3
  11. package/payload/mods/pmos-cockpit/types/index.d.ts +17 -1
  12. package/payload/okf/core/concepts/frontmatter-format.md +6 -1
  13. package/payload/planning/evals/eval-calibration.md +1 -1
  14. package/payload/planning/okrs/health-budgets.md +1 -1
  15. package/payload/skills/SKILL-FORMAT.md +1 -0
  16. package/payload/state/discoveries.md +1 -1
  17. package/workspace/README.md +1 -1
  18. package/workspace/guide/initiatives-and-next-steps.md +1 -0
  19. package/workspace/guide/knowledge-graph.md +6 -1
  20. package/workspace/guide/layout.md +1 -0
  21. package/workspace/guide/proactive-engine.md +21 -3
  22. package/workspace/guide/scheduled-tasks-and-cli.md +1 -0
  23. package/workspace/guide/statuses.md +1 -0
  24. package/workspace/guide/validate-with-typesafe.md +1 -0
  25. package/workspace/lib/kg-cli.js +2 -1
  26. package/workspace/lib/kg-dangling-baseline.json +1 -0
  27. package/workspace/lib/kg-dangling-rules.json +1 -0
  28. package/workspace/lib/kg.js +20 -8
  29. package/workspace/lib/learn.js +280 -0
  30. package/workspace/lib/proactive-cli.js +49 -1
  31. package/workspace/lib/proactive.js +8 -7
  32. package/workspace/lib/session-flag.js +35 -0
  33. package/workspace/package-lock.json +1 -1
  34. package/workspace/package.json +3 -3
  35. package/workspace/public/pm/modes-live.js +52 -4
  36. package/workspace/server.js +108 -5
  37. 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. Nothing in this module opens it unasked (modes, M12, come later).
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: 'Opened the initiative slice. Esc closes it.' }
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-04T09:05:06Z"}
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-04T09:05:06Z"}
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-04T09:05:06Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-04T10:38:12Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -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; the last has no effect yet because nothing learns from the verdicts in this slice). 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`.
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: initiatives and next steps"
4
+ description: "What the initiative detail shows, top to bottom: status, lifecycle control, and the next step the workspace proposes."
4
5
  timestamp: 2026-10-03
5
6
  ---
6
7
 
@@ -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: layout"
4
+ description: "The three workspace columns: navigation, detail and tabs, and what each one holds."
4
5
  timestamp: 2026-10-03
5
6
  ---
6
7
 
@@ -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. In this release a verdict is only recorded. Nothing uses it to change the order yet.
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 | Saved for later. Nothing learns from your verdicts in this release, so it changes nothing today. |
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.
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  type: Guide
3
3
  title: "Guide: scheduled tasks and the command line"
4
+ description: "How scheduled tasks run a skill in a terminal session, their limits, and the command line that manages them."
4
5
  timestamp: 2026-10-03
5
6
  ---
6
7
 
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  type: Guide
3
3
  title: "Guide: statuses"
4
+ description: "The nine initiative statuses, who acts on each, and why a status is worked out again each time and never typed."
4
5
  timestamp: 2026-10-03
5
6
  ---
6
7