@mindrian_os/install 1.13.0-beta.13 → 1.13.0-beta.16

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 (118) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +21 -11
  3. package/README.md +74 -572
  4. package/commands/act.md +1 -0
  5. package/commands/admin.md +1 -0
  6. package/commands/analyze-needs.md +1 -0
  7. package/commands/analyze-systems.md +1 -0
  8. package/commands/analyze-timing.md +1 -0
  9. package/commands/auto-explore.md +1 -0
  10. package/commands/beautiful-question.md +1 -0
  11. package/commands/brain-derive.md +1 -0
  12. package/commands/build-knowledge.md +1 -0
  13. package/commands/build-thesis.md +1 -0
  14. package/commands/causal.md +1 -0
  15. package/commands/challenge-assumptions.md +1 -0
  16. package/commands/compare-ventures.md +1 -0
  17. package/commands/dashboard.md +1 -0
  18. package/commands/deep-grade.md +1 -0
  19. package/commands/diagnose.md +1 -0
  20. package/commands/diagnostics.md +1 -0
  21. package/commands/doctor.md +1 -0
  22. package/commands/dominant-designs.md +1 -0
  23. package/commands/explain-decision.md +1 -0
  24. package/commands/explore-domains.md +1 -0
  25. package/commands/explore-futures.md +1 -0
  26. package/commands/explore-trends.md +1 -0
  27. package/commands/export.md +1 -0
  28. package/commands/feynman-timeline-refresh.md +78 -0
  29. package/commands/file-meeting.md +1 -0
  30. package/commands/find-analogies.md +1 -0
  31. package/commands/find-bottlenecks.md +1 -0
  32. package/commands/find-connections.md +1 -0
  33. package/commands/funding.md +1 -0
  34. package/commands/grade.md +1 -0
  35. package/commands/graph.md +1 -0
  36. package/commands/hat-briefing.md +1 -0
  37. package/commands/heal.md +1 -0
  38. package/commands/help.md +1 -0
  39. package/commands/hmi-status.md +1 -0
  40. package/commands/jtbd.md +1 -0
  41. package/commands/leadership.md +1 -0
  42. package/commands/lean-canvas.md +1 -0
  43. package/commands/macro-trends.md +1 -0
  44. package/commands/map-unknowns.md +1 -0
  45. package/commands/memory.md +1 -0
  46. package/commands/models.md +1 -0
  47. package/commands/mos-reason.md +1 -0
  48. package/commands/mullins.md +1 -0
  49. package/commands/new-project.md +1 -0
  50. package/commands/onboard.md +1 -0
  51. package/commands/operator.md +1 -0
  52. package/commands/opportunities.md +1 -0
  53. package/commands/organize.md +1 -0
  54. package/commands/persona.md +1 -0
  55. package/commands/pipeline.md +1 -0
  56. package/commands/present.md +1 -0
  57. package/commands/publish.md +1 -0
  58. package/commands/query.md +1 -0
  59. package/commands/radar.md +1 -0
  60. package/commands/reanalyze.md +1 -0
  61. package/commands/research.md +1 -0
  62. package/commands/room.md +1 -0
  63. package/commands/rooms.md +1 -0
  64. package/commands/root-cause.md +1 -0
  65. package/commands/rs-experts.md +1 -0
  66. package/commands/rs-explain.md +1 -0
  67. package/commands/rs-fetch.md +1 -0
  68. package/commands/rs-thesis.md +1 -0
  69. package/commands/scenario-plan.md +1 -0
  70. package/commands/scheduled-tasks.md +1 -0
  71. package/commands/score-innovation.md +1 -0
  72. package/commands/scout.md +1 -0
  73. package/commands/setup.md +1 -0
  74. package/commands/snapshot.md +1 -0
  75. package/commands/speakers.md +1 -0
  76. package/commands/splash.md +1 -0
  77. package/commands/status.md +1 -0
  78. package/commands/structure-argument.md +1 -0
  79. package/commands/suggest-next.md +1 -0
  80. package/commands/systems-thinking.md +1 -0
  81. package/commands/think-hats.md +1 -0
  82. package/commands/update.md +1 -0
  83. package/commands/user-needs.md +1 -0
  84. package/commands/validate.md +1 -0
  85. package/commands/value-proposition.md +1 -0
  86. package/commands/vault.md +1 -0
  87. package/commands/visualize.md +1 -0
  88. package/commands/whitespace.md +1 -0
  89. package/commands/wiki.md +1 -0
  90. package/lib/brain/framework-chain-slice.cjs +193 -0
  91. package/lib/core/cache-prune.cjs +114 -8
  92. package/lib/core/feynman/ROOM.md +25 -0
  93. package/lib/core/feynman/timeline-renderer.cjs +197 -0
  94. package/lib/core/feynman/timeline-runner.cjs +281 -0
  95. package/lib/core/install-state.cjs +242 -0
  96. package/lib/core/navigation/edges.cjs +86 -0
  97. package/lib/core/navigation/insights.cjs +37 -0
  98. package/lib/core/navigation/memory-events.cjs +39 -0
  99. package/lib/core/navigation/packet.cjs +89 -9
  100. package/lib/core/navigation/projections.cjs +201 -0
  101. package/lib/core/navigation.cjs +25 -0
  102. package/lib/mcp/larry-server-instructions.md +1 -1
  103. package/lib/memory/brain-cypher-chain-slice.test.cjs +368 -0
  104. package/lib/memory/f-selector-ranker.test.cjs +593 -0
  105. package/lib/memory/navigation-projections.test.cjs +241 -0
  106. package/lib/memory/navigation-write-edge.test.cjs +206 -0
  107. package/lib/memory/packet-chain-hint.test.cjs +407 -0
  108. package/lib/memory/packet-schema-validation.test.cjs +317 -0
  109. package/lib/memory/per-command-jtbd-derivation.test.cjs +130 -0
  110. package/lib/memory/per-command-teaching.test.cjs +110 -0
  111. package/lib/memory/run-feynman-tests.cjs +36 -0
  112. package/lib/memory/selector-decisions.test.cjs +417 -0
  113. package/lib/memory/selector-miss.test.cjs +290 -0
  114. package/lib/workflow/f-selector-ranker.cjs +420 -0
  115. package/lib/workflow/selector-decisions.cjs +368 -0
  116. package/package.json +1 -1
  117. package/references/design/email-template-standard.md +1 -1
  118. package/references/user-research/2026-04-05-leah-lawrence-session.md +3 -3
package/commands/rooms.md CHANGED
@@ -4,6 +4,7 @@ description: List, switch, or archive project rooms
4
4
  argument-hint: [list|switch|archive|park]
5
5
  body_shape: B (Semantic Tree)
6
6
  serves_jtbd: ["audit-room"]
7
+ teaching: "When you have multiple venture rooms and need to switch, list, or archive them, /mos:rooms manages the registry. One person, many ventures."
7
8
  ui_reference: skills/ui-system/SKILL.md
8
9
  allowed-tools:
9
10
  - Read
@@ -2,6 +2,7 @@
2
2
  name: root-cause
3
3
  description: Trace root cause via 5-Whys, Fishbone, Fault Tree
4
4
  serves_jtbd: ["find-problem"]
5
+ teaching: "When the symptom keeps coming back, /mos:root-cause traces it via 5-Whys, Fishbone, and Fault Tree. Treats the cause, not the recurrence."
5
6
  # --- Phase 122 workflow-layer frontmatter ---
6
7
  kind: methodology
7
8
  frameworks: ["Root Cause Analysis"]
@@ -3,6 +3,7 @@ name: rs-experts
3
3
  description: Resolve the expert network for a topic via Aura Cypher MATCH
4
4
  body_shape: D (Comparison Matrix)
5
5
  serves_jtbd: ["find-bottleneck", "connect-domains"]
6
+ teaching: "When you need to know who in the world is working on a reverse salient you found, /mos:rs-experts resolves the expert network via Brain Cypher MATCH. Routes you to the people who already know."
6
7
  # --- Phase 122 workflow-layer frontmatter ---
7
8
  kind: methodology
8
9
  frameworks: ["Reverse Salient Analysis"]
@@ -3,6 +3,7 @@ name: rs-explain
3
3
  description: Bidirectional NL-Graph entry point. NL question to graph queries to Larry-voiced explanation.
4
4
  body_shape: E (Action Report)
5
5
  serves_jtbd: ["find-bottleneck"]
6
+ teaching: "When you have a question about a Reverse Salient discovery, /mos:rs-explain takes natural language in and returns a Larry-voiced explanation grounded in the graph. Bidirectional NL to graph and back."
6
7
  # --- Phase 122 workflow-layer frontmatter ---
7
8
  kind: methodology
8
9
  frameworks: ["Reverse Salient Analysis"]
@@ -3,6 +3,7 @@ name: rs-fetch
3
3
  description: Run the full Reverse Salient discovery pipeline for a topic
4
4
  body_shape: E (Action Report)
5
5
  serves_jtbd: ["find-bottleneck", "surface-contradiction"]
6
+ teaching: "When you need the full Reverse Salient pipeline run on a topic, /mos:rs-fetch executes the discovery end-to-end: corpus, math, cross-domain match, thesis. The complete sweep, not a sample."
6
7
  # --- Phase 122 workflow-layer frontmatter ---
7
8
  kind: methodology
8
9
  frameworks: ["Reverse Salient Analysis"]
@@ -3,6 +3,7 @@ name: rs-thesis
3
3
  description: Read the thesis for a prior Reverse Salient discovery
4
4
  body_shape: E (Action Report)
5
5
  serves_jtbd: ["find-bottleneck"]
6
+ teaching: "When you ran a Reverse Salient discovery earlier and want the thesis read back, /mos:rs-thesis surfaces the analytic conclusion in plain language. Best for revisiting old findings before a meeting."
6
7
  # --- Phase 122 workflow-layer frontmatter ---
7
8
  kind: methodology
8
9
  frameworks: ["Reverse Salient Analysis"]
@@ -2,6 +2,7 @@
2
2
  name: scenario-plan
3
3
  description: Build a 2x2 scenario matrix of plausible futures
4
4
  serves_jtbd: ["compare-options", "plan-execution"]
5
+ teaching: "When the future could go two different ways on two key uncertainties, /mos:scenario-plan builds the 2x2 matrix and names each quadrant. Forces you to plan for the world you do not expect."
5
6
  # --- Phase 122 workflow-layer frontmatter ---
6
7
  kind: methodology
7
8
  frameworks: ["Scenario Planning"]
@@ -3,6 +3,7 @@ name: scheduled-tasks
3
3
  description: Define Cowork scheduled tasks for the room
4
4
  body_shape: E (Action Report)
5
5
  serves_jtbd: ["plan-execution"]
6
+ teaching: "When you want Cowork to run something on a schedule against this room, /mos:scheduled-tasks defines the recurring job. Best for nightly grant sweeps or weekly meeting digests."
6
7
  ui_reference: skills/ui-system/SKILL.md
7
8
  surface: cowork
8
9
  allowed-tools:
@@ -2,6 +2,7 @@
2
2
  name: score-innovation
3
3
  description: Score cross-domain innovation via HSI
4
4
  serves_jtbd: ["compare-options", "validate-idea"]
5
+ teaching: "When you are choosing between cross-domain innovation candidates, /mos:score-innovation runs HSI scoring to rank them by semantic surprise. The math reveals which idea is actually novel."
5
6
  # --- Phase 122 workflow-layer frontmatter ---
6
7
  kind: methodology
7
8
  frameworks: ["HSI Semantic Surprise Analysis Assistant"]
package/commands/scout.md CHANGED
@@ -3,6 +3,7 @@ name: scout
3
3
  description: Run sentinel scans across the room
4
4
  body_shape: E (Action Report)
5
5
  serves_jtbd: ["explore", "understand-market"]
6
+ teaching: "When you want background scans running across the room without driving them yourself, /mos:scout fires the sentinel checks. The proactive layer, not the reactive one."
6
7
  ui_reference: skills/ui-system/SKILL.md
7
8
  allowed-tools:
8
9
  - Read
package/commands/setup.md CHANGED
@@ -3,6 +3,7 @@ name: setup
3
3
  description: Configure optional integrations (Brain, Velma)
4
4
  argument-hint: [brain|velma|graph]
5
5
  serves_jtbd: ["explore"]
6
+ teaching: "When you want to wire optional integrations like Brain or Velma, /mos:setup walks you through configuration. MindrianOS works without them; they make it work harder."
6
7
  allowed-tools:
7
8
  - Read
8
9
  - Write
@@ -3,6 +3,7 @@ name: snapshot
3
3
  description: Package a Data Room snapshot for sharing
4
4
  argument-hint: '[<room-path>] [--open]'
5
5
  serves_jtbd: ["prepare-pitch", "audit-room"]
6
+ teaching: "When you need a frozen Data Room artifact to share with someone outside the team, /mos:snapshot packages everything into a portable bundle. Read-only by design."
6
7
  disable-model-invocation: true
7
8
  usage: /mos:snapshot [ROOM_PATH] [--output PATH] [--open]
8
9
  category: export
@@ -4,6 +4,7 @@ description: Show who spoke in your meetings and their roles
4
4
  body_shape: C (Room Card)
5
5
  body_shape_detail: Each speaker as a card with role, expertise, meeting count
6
6
  serves_jtbd: ["file-meeting"]
7
+ teaching: "When you have a meeting filed and want to know who said what, /mos:speakers shows the participants with their roles, attendance, and contribution patterns. The people layer of meeting intelligence."
7
8
  ui_reference: skills/ui-system/SKILL.md
8
9
  allowed-tools:
9
10
  - Read
@@ -3,6 +3,7 @@ name: splash
3
3
  description: Display the MindrianOS Mondrian banner
4
4
  body_shape: raw
5
5
  serves_jtbd: ["explore"]
6
+ teaching: "When you want the MindrianOS Mondrian banner, /mos:splash displays it. Mostly decorative; useful for screenshots and demo openings."
6
7
  allowed-tools:
7
8
  - Bash
8
9
  ---
@@ -4,6 +4,7 @@ description: Show governing thought per section + health glyphs
4
4
  argument-hint: "[section] [--stale-only]"
5
5
  body_shape: E (Action Report)
6
6
  serves_jtbd: ["audit-room", "explore"]
7
+ teaching: "When you need a fast read on the room's current state, /mos:status shows the governing thought per section plus health glyphs. The 10-second status check."
7
8
  ui_reference: skills/ui-system/SKILL.md
8
9
  allowed-tools:
9
10
  - Bash(node scripts/mos-status.cjs:*)
@@ -2,6 +2,7 @@
2
2
  name: structure-argument
3
3
  description: Structure an argument with Minto + SCQA + MECE
4
4
  serves_jtbd: ["validate-idea", "explore"]
5
+ teaching: "When an argument is muddled and you cannot say why, /mos:structure-argument restructures it with Minto pyramid, SCQA, and MECE. The right structure usually surfaces the missing premise."
5
6
  # --- Phase 122 workflow-layer frontmatter ---
6
7
  kind: methodology
7
8
  frameworks: ["The Pyramid Principle", "MECE (Mutually Exclusive, Collectively Exhaustive)"]
@@ -2,6 +2,7 @@
2
2
  name: suggest-next
3
3
  description: Suggest the next move using the room graph
4
4
  serves_jtbd: ["plan-execution", "explore"]
5
+ teaching: "When you finish a step and want Larry to recommend the next move, /mos:suggest-next reads the room graph and proposes 3-5 options with reasons. The Navigation Engine made visible."
5
6
  # --- Phase 122 workflow-layer frontmatter ---
6
7
  kind: meta
7
8
  frameworks: []
@@ -2,6 +2,7 @@
2
2
  name: systems-thinking
3
3
  description: Map feedback loops, stocks, and flows
4
4
  serves_jtbd: ["find-bottleneck"]
5
+ teaching: "When the dynamics matter more than the parts, /mos:systems-thinking maps feedback loops, stocks, and flows. Surfaces where the leverage actually lives in the system."
5
6
  # --- Phase 122 workflow-layer frontmatter ---
6
7
  kind: methodology
7
8
  frameworks: ["Systems Thinking"]
@@ -2,6 +2,7 @@
2
2
  name: think-hats
3
3
  description: Rotate through De Bono's Six Thinking Hats
4
4
  serves_jtbd: ["explore", "compare-options"]
5
+ teaching: "When the team keeps wearing the same hat and missing perspectives, /mos:think-hats rotates them through de Bono's six. The discomfort is the point; that is where the new thought lives."
5
6
  # --- Phase 122 workflow-layer frontmatter ---
6
7
  kind: methodology
7
8
  frameworks: ["Six Thinking Hats"]
@@ -3,6 +3,7 @@ name: update
3
3
  description: Check for MindrianOS updates and install via Claude Code's native plugin loader
4
4
  argument-hint: [check|reapply|force]
5
5
  serves_jtbd: ["audit-room"]
6
+ teaching: "When you suspect MindrianOS has a newer version waiting, /mos:update checks and installs via Claude Code's native plugin loader. Stale plugins quietly diverge from the docs."
6
7
  allowed-tools:
7
8
  - Bash
8
9
  - Read
@@ -2,6 +2,7 @@
2
2
  name: user-needs
3
3
  description: Map user needs with importance vs satisfaction
4
4
  serves_jtbd: ["find-problem"]
5
+ teaching: "When you need to map what users actually want versus what they say they want, /mos:user-needs plots importance against satisfaction. The gap is where the opportunity lives."
5
6
  # --- Phase 122 workflow-layer frontmatter ---
6
7
  kind: methodology
7
8
  frameworks: ["Jobs to Be Done (JTBD)"]
@@ -2,6 +2,7 @@
2
2
  name: validate
3
3
  description: Validate ideas via importance-satisfaction scoring
4
4
  serves_jtbd: ["validate-idea"]
5
+ teaching: "When an idea needs a real test before more investment, /mos:validate runs importance-satisfaction scoring against the customer segment. Validation is a measurement, not a feeling."
5
6
  # --- Phase 122 workflow-layer frontmatter ---
6
7
  kind: methodology
7
8
  frameworks: ["Jobs to Be Done (JTBD)"]
@@ -2,6 +2,7 @@
2
2
  name: validate-proposition
3
3
  description: Score your value proposition against 3 VP gates
4
4
  serves_jtbd: ["validate-idea", "prepare-pitch"]
5
+ teaching: "When you have a value proposition but no proof it holds, /mos:validate-proposition scores it against the three PWS VP gates with sequential math. A clean gate failure beats a vague pass."
5
6
  # --- Phase 122 workflow-layer frontmatter ---
6
7
  kind: methodology
7
8
  frameworks: ["PWS Value Proposition"]
package/commands/vault.md CHANGED
@@ -5,6 +5,7 @@ argument-hint: '[<room-name>] [--path <dir>]'
5
5
  disable-model-invocation: true
6
6
  body_shape_overview: E (Mini Report)
7
7
  serves_jtbd: ["prepare-pitch"]
8
+ teaching: "When you want the Data Room available in Obsidian for offline reading, /mos:vault exports it as a nested vault with wikilinks intact. Graph view comes free."
8
9
  ui_reference: skills/ui-system/SKILL.md
9
10
  allowed-tools:
10
11
  - Read
@@ -4,6 +4,7 @@ description: Open room diagrams in the browser
4
4
  argument-hint: [structure|graph|chart]
5
5
  body_shape: D (Document View)
6
6
  serves_jtbd: ["audit-room", "prepare-pitch"]
7
+ teaching: "When you want to see the room as diagrams in a browser, /mos:visualize opens the visual layer. Best when a stakeholder needs the picture, not the prose."
7
8
  ui_reference: skills/ui-system/SKILL.md
8
9
  allowed-tools:
9
10
  - Bash
@@ -3,6 +3,7 @@ name: whitespace
3
3
  description: Detect whitespace gaps in the room's coverage
4
4
  body_shape: varies
5
5
  serves_jtbd: ["connect-domains", "find-problem"]
6
+ teaching: "When you suspect a gap exists in the room's coverage of a domain, /mos:whitespace runs HSI scoring across the artifact corpus to find under-explored zones. Best after the room has 20+ entries."
6
7
  # --- Phase 122 workflow-layer frontmatter ---
7
8
  kind: methodology
8
9
  frameworks: ["HSI Semantic Surprise Analysis Assistant"]
package/commands/wiki.md CHANGED
@@ -3,6 +3,7 @@ name: wiki
3
3
  description: Open the Data Room wiki of room sections
4
4
  body_shape: D (Document View)
5
5
  serves_jtbd: ["audit-room", "prepare-pitch"]
6
+ teaching: "When you want to read the Data Room as linked wiki pages, /mos:wiki opens the wiki view. Section by section, with cross-references rendered as hyperlinks."
6
7
  ui_reference: skills/ui-system/SKILL.md
7
8
  allowed-tools:
8
9
  - Bash
@@ -0,0 +1,193 @@
1
+ 'use strict';
2
+ // Phase 125-02 -- Brain Cypher slice query (parameterized 1-3 hop FEEDS_INTO; LIMIT 50).
3
+ // CONTEXT.md Scope IN section B item 4 -- the LOCKED Cypher. Async wrapper over
4
+ // brain-client.query that produces the framework_chain_hint shape Plan 03 stitches
5
+ // into the packet's local_graph_summary.
6
+ //
7
+ // Canon Part 8: only $active_frameworks (array of generic framework name strings,
8
+ // each passed through sanitizeCypherInput) + $max_hops (1|2|3 int) cross the wire.
9
+ // No user content. No artifact text. No room identifiers in the query.
10
+ //
11
+ // Graceful degradation: any failure path (Brain unreachable, query throws, invalid
12
+ // max_hops, empty active_frameworks, all-rejected-by-sanitizer, non-array result)
13
+ // returns a degraded result envelope rather than throwing. Tier 0 stays functional.
14
+
15
+ const crypto = require('node:crypto');
16
+ const defaultBrainClient = require('../core/brain-client.cjs');
17
+
18
+ // LOCKED Cypher template per CONTEXT.md Scope IN section B item 4.
19
+ // READ-ONLY (MATCH/RETURN/LIMIT only -- no MERGE/CREATE/DELETE). The two $-bound
20
+ // params ($active_frameworks, $max_hops) are the ONLY values that cross the wire;
21
+ // every framework name in $active_frameworks is sanitized via the brain-client
22
+ // sanitizeCypherInput whitelist before binding (Canon Part 8 invariant).
23
+ const FRAMEWORK_CHAIN_SLICE_CYPHER =
24
+ 'MATCH (f:Framework)-[r:FEEDS_INTO*1..$max_hops]->(g:Framework) ' +
25
+ 'WHERE f.name IN $active_frameworks ' +
26
+ 'RETURN f.name AS from, g.name AS to, ' +
27
+ ' r.confidence AS confidence, ' +
28
+ ' r.transform_description AS transform_description, ' +
29
+ ' length(r) AS hop_distance ' +
30
+ 'LIMIT 50';
31
+
32
+ function _nowIso() {
33
+ return new Date().toISOString();
34
+ }
35
+
36
+ function _sha256Hex(s) {
37
+ return crypto.createHash('sha256').update(String(s), 'utf8').digest('hex');
38
+ }
39
+
40
+ function _degraded(opts) {
41
+ const max_hops = (opts && (opts.max_hops === 1 || opts.max_hops === 2 || opts.max_hops === 3))
42
+ ? opts.max_hops
43
+ : 3;
44
+ return {
45
+ edges: [],
46
+ slice_scope: max_hops,
47
+ slice_rationale: (opts && opts.rationale) ? String(opts.rationale) : 'degraded',
48
+ brain_snapshot_id: null,
49
+ fetched_at: _nowIso(),
50
+ };
51
+ }
52
+
53
+ // Normalize result rows from brain-client.query. The shipped brain-client.query
54
+ // returns { records: Array<row> } for the brain_query path; tests + the
55
+ // CONTEXT.md spec describe the inner array of rows. Accept BOTH shapes so we
56
+ // stay compatible whether the caller stubs query() with an Array directly OR
57
+ // the live brain-client wraps in { records }. Any other shape degrades cleanly.
58
+ function _unwrapRows(result) {
59
+ if (Array.isArray(result)) return result;
60
+ if (result && Array.isArray(result.records)) return result.records;
61
+ return null;
62
+ }
63
+
64
+ /**
65
+ * Fetch a parameterized 1-3 hop FEEDS_INTO slice from the live Brain.
66
+ *
67
+ * @param {object} opts
68
+ * @param {string[]} opts.active_frameworks - Array of framework name strings.
69
+ * Empty array short-circuits to a degraded envelope.
70
+ * @param {1|2|3} opts.max_hops - Hop depth (must be exactly 1, 2, or 3).
71
+ * Any other value short-circuits to a degraded envelope.
72
+ * @param {object} [opts.brainClient] - Test seam; defaults to live brain-client.
73
+ * @returns {Promise<{
74
+ * edges: Array<{from:string|null, to:string|null, confidence:number|null,
75
+ * transform_description:string|null, hop_distance:number|null}>,
76
+ * slice_scope: 1|2|3,
77
+ * slice_rationale: string,
78
+ * brain_snapshot_id: string|null,
79
+ * fetched_at: string
80
+ * }>}
81
+ */
82
+ async function fetchFrameworkChainSlice(opts) {
83
+ const o = opts || {};
84
+ const active_frameworks = Array.isArray(o.active_frameworks) ? o.active_frameworks : [];
85
+ const max_hops = (o.max_hops === 1 || o.max_hops === 2 || o.max_hops === 3) ? o.max_hops : null;
86
+ const brainClient = o.brainClient || defaultBrainClient;
87
+
88
+ // Gate 1: max_hops must be 1, 2, or 3. Anything else (0, 4, undefined,
89
+ // string, null) returns a degraded envelope with slice_scope defaulted to 3.
90
+ if (max_hops === null) {
91
+ return _degraded({ max_hops: 3, rationale: 'invalid_max_hops; expected 1|2|3' });
92
+ }
93
+
94
+ // Gate 2: empty active_frameworks short-circuits before any Brain contact.
95
+ // The Cypher would match nothing anyway; the degraded envelope is honest.
96
+ if (active_frameworks.length === 0) {
97
+ return _degraded({ max_hops: max_hops, rationale: 'empty active_frameworks; no slice fetched' });
98
+ }
99
+
100
+ // Gate 3: Brain reachability check before issuing the Cypher query. The
101
+ // live brain-client.isAvailable() is sync + cheap (an API-key resolve).
102
+ if (typeof brainClient.isAvailable === 'function' && !brainClient.isAvailable()) {
103
+ return _degraded({ max_hops: max_hops, rationale: 'brain_unreachable' });
104
+ }
105
+
106
+ // Canon Part 8 sanitization. brain-client._test.sanitizeCypherInput is the
107
+ // shipped whitelist (Phase 110-05 D-04 invariant). Each framework name is
108
+ // sanitized BEFORE binding; values that scrub down to empty are dropped.
109
+ // Fallback no-op only when the seam is genuinely absent (the live
110
+ // brain-client always exposes it; tests may stub a minimal client).
111
+ const sanitizer = (brainClient._test && typeof brainClient._test.sanitizeCypherInput === 'function')
112
+ ? brainClient._test.sanitizeCypherInput
113
+ : function (s) { return s; };
114
+ const sanitized = active_frameworks
115
+ .filter(function (n) { return typeof n === 'string' && n.length > 0; })
116
+ .map(function (n) { return sanitizer(n); })
117
+ .filter(function (n) { return typeof n === 'string' && n.length > 0; });
118
+
119
+ if (sanitized.length === 0) {
120
+ return _degraded({
121
+ max_hops: max_hops,
122
+ rationale: 'all_active_frameworks_rejected_by_sanitizer',
123
+ });
124
+ }
125
+
126
+ // Issue the parameterized Cypher. The ONLY $-bound values are the sanitized
127
+ // framework name array + the max_hops integer. The Cypher string itself is
128
+ // a frozen constant -- no template interpolation, no string concatenation
129
+ // with caller-derived data (Canon Part 8 enforced by inspection).
130
+ let result;
131
+ try {
132
+ result = await brainClient.query(FRAMEWORK_CHAIN_SLICE_CYPHER, {
133
+ active_frameworks: sanitized,
134
+ max_hops: max_hops,
135
+ });
136
+ } catch (e) {
137
+ const msg = (e && e.message) ? String(e.message) : 'unknown';
138
+ return _degraded({
139
+ max_hops: max_hops,
140
+ rationale: 'brain_query_failed: ' + msg.slice(0, 60),
141
+ });
142
+ }
143
+
144
+ const rows = _unwrapRows(result);
145
+ if (rows === null) {
146
+ return _degraded({ max_hops: max_hops, rationale: 'brain_returned_non_array' });
147
+ }
148
+
149
+ // Map raw rows to the framework_chain_hint edge shape. Null-tolerant per
150
+ // G-05: confidence and transform_description may be null on the live graph
151
+ // and we preserve null explicitly (no defaults). hop_distance comes from
152
+ // length(r) which is always a number when the row exists.
153
+ // Defensive: hard-cap at 50 even though the Cypher already enforces LIMIT 50.
154
+ const edges = rows.slice(0, 50).map(function (r) {
155
+ return {
156
+ from: (r && typeof r.from === 'string') ? r.from : null,
157
+ to: (r && typeof r.to === 'string') ? r.to : null,
158
+ confidence: (r && typeof r.confidence === 'number') ? r.confidence : null,
159
+ transform_description: (r && typeof r.transform_description === 'string')
160
+ ? r.transform_description
161
+ : null,
162
+ hop_distance: (r && typeof r.hop_distance === 'number') ? r.hop_distance : null,
163
+ };
164
+ });
165
+
166
+ // brain_snapshot_id: per CONTEXT.md Open Question #4, "Brain commit_id if
167
+ // exposed, else SHA of Cypher result." The shipped brain_query does NOT
168
+ // expose a commit_id; we derive a deterministic content hash of the raw
169
+ // rows so callers can detect slice changes without holding the rows
170
+ // themselves (Plan 03 + downstream packet validator never sees them).
171
+ const snapshotId = _sha256Hex(JSON.stringify(rows));
172
+
173
+ return {
174
+ edges: edges,
175
+ slice_scope: max_hops,
176
+ slice_rationale: 'brain_reachable; ' + edges.length + ' edges fetched; '
177
+ + sanitized.length + ' active_frameworks; max_hops=' + max_hops,
178
+ brain_snapshot_id: snapshotId,
179
+ fetched_at: _nowIso(),
180
+ };
181
+ }
182
+
183
+ module.exports = {
184
+ fetchFrameworkChainSlice: fetchFrameworkChainSlice,
185
+ FRAMEWORK_CHAIN_SLICE_CYPHER: FRAMEWORK_CHAIN_SLICE_CYPHER,
186
+ // Test seam -- helpers exposed for invariant tests; not part of the
187
+ // public API. Plan 03 (packet builder) consumes only the two above.
188
+ _test: {
189
+ _degraded: _degraded,
190
+ _sha256Hex: _sha256Hex,
191
+ _unwrapRows: _unwrapRows,
192
+ },
193
+ };
@@ -31,10 +31,33 @@
31
31
  *
32
32
  * If installed_plugins.json is unreadable (ENOENT, parse error, or no
33
33
  * mos@mindrian-marketplace entry), the function returns
34
- * { kept: [], removed: [], skipped: true, reason: '...' }
34
+ * { kept: [], removed: [], removedBackups: [], skipped: true, reason: '...' }
35
35
  * with NO mutation. Never guesses; never deletes anything in the absence
36
36
  * of an authoritative active-version answer.
37
37
  *
38
+ * Phase 126 Plan-06 extension -- STALE BACKUP DIR PRUNING.
39
+ * In addition to the marketplace-cache version dirs, this function ALSO
40
+ * prunes Phase 95.2's atomic-swap backup directories at
41
+ * ~/.claude/plugins/mindrian-os.stale-<tag>-<timestamp>/
42
+ * that are older than MOS_CACHE_PRUNE_AGE_DAYS (default 30 days).
43
+ *
44
+ * Background: Phase 95.2 wraps `claude plugin update` in an atomic-swap
45
+ * recovery flow. Each recovery leaves a backup of the prior install at
46
+ * the sibling path above. By Phase 95.2 contract, backups are retained
47
+ * "indefinitely" (after 24h the user can delete manually). Long-running
48
+ * tester installs (Lawrence, Gary, etc) never see manual cleanup, so
49
+ * backups accumulate. 30 days is the next operational threshold beyond
50
+ * the 24h user-driven cleanup window.
51
+ *
52
+ * Pattern match is LITERAL prefix `mindrian-os.stale-` (regex
53
+ * /^mindrian-os\.stale-/). Does NOT match `mindrian-os/` (the live
54
+ * install) or unrelated siblings -- the period after `mindrian-os` is
55
+ * what gates the match.
56
+ *
57
+ * Window override: set process.env.MOS_CACHE_PRUNE_AGE_DAYS to any
58
+ * positive integer. Invalid values (non-numeric, negative) fall back
59
+ * to the 30-day default. v2 may move this to .mos/config.json.
60
+ *
38
61
  * Signature:
39
62
  * pruneMarketplaceCache({
40
63
  * home = os.homedir(),
@@ -42,14 +65,22 @@
42
65
  * retainCount = 2,
43
66
  * dryRun = false,
44
67
  * } = {}) => {
45
- * kept: string[], // version basenames kept on disk (incl. active)
46
- * removed: string[], // version basenames removed (or would be, in dryRun)
47
- * skipped: boolean, // true if we declined to act (see `reason`)
48
- * reason: string|null // human-readable explanation, or null on success
68
+ * kept: string[], // version basenames kept on disk (incl. active)
69
+ * removed: string[], // version basenames removed (or would be, in dryRun)
70
+ * removedBackups: string[], // absolute paths of stale backup dirs removed (Phase 126)
71
+ * skipped: boolean, // true if we declined the marketplace-cache prune
72
+ * reason: string|null, // human-readable explanation, or null on success
73
+ * ageDays: number, // the active stale-backup age window in days (Phase 126)
49
74
  * }
50
75
  *
76
+ * Backward compat: the original Phase 123 consumer at scripts/doctor.cjs
77
+ * line ~2100 reads r.removed.length + r.kept only; both unchanged here.
78
+ * `removedBackups` and `ageDays` are ADDITIVE fields.
79
+ *
51
80
  * Canon Part 8 sanity check (cp.6): the forbidden-network-token grep
52
- * exits 1 (no match) on this source file. Test cp.6 enforces it.
81
+ * exits 1 (no match) on this source file. Test cp.6 enforces it. The
82
+ * Phase 126 extension reads only local fs (stat + readdir + rmSync on
83
+ * scratch / on-disk backup dirs); zero network surface.
53
84
  */
54
85
 
55
86
  const fs = require('node:fs');
@@ -137,10 +168,20 @@ function pruneMarketplaceCache(opts) {
137
168
  const retainCount = (typeof o.retainCount === 'number' && o.retainCount >= 0) ? o.retainCount : 2;
138
169
  const dryRun = !!o.dryRun;
139
170
 
171
+ // Phase 126 Plan-06: stale-backup age window. Read once at the top so the
172
+ // returned `ageDays` field reflects the value that gated the prune.
173
+ // Env override is integer-only; invalid values fall back to the default.
174
+ const ageDaysRaw = process.env.MOS_CACHE_PRUNE_AGE_DAYS;
175
+ const ageDays = (typeof ageDaysRaw === 'string' && /^\d+$/.test(ageDaysRaw))
176
+ ? parseInt(ageDaysRaw, 10)
177
+ : 30;
178
+
140
179
  // Step 1: read active version. Skip entirely if unavailable -- never guess.
180
+ // (When skipped, we also decline the Phase-126 stale-backup pass, mirroring
181
+ // "no mutation in the absence of authoritative state".)
141
182
  const active = readActiveVersion(home);
142
183
  if (active.skipped) {
143
- return { kept: [], removed: [], skipped: true, reason: active.reason };
184
+ return { kept: [], removed: [], removedBackups: [], skipped: true, reason: active.reason, ageDays };
144
185
  }
145
186
  const activeVersion = active.activeVersion;
146
187
 
@@ -149,7 +190,9 @@ function pruneMarketplaceCache(opts) {
149
190
  if (names.length === 0) {
150
191
  // No cache dir to prune. The active version is still "kept" conceptually
151
192
  // (it lives in the cache when it exists; absent cache = nothing to do).
152
- return { kept: [activeVersion], removed: [], skipped: false, reason: 'no cache dir to prune' };
193
+ // Phase 126: still run the stale-backup pass below (returns at end).
194
+ const removedBackups = pruneStaleBackups(home, ageDays, dryRun);
195
+ return { kept: [activeVersion], removed: [], removedBackups, skipped: false, reason: 'no cache dir to prune', ageDays };
153
196
  }
154
197
 
155
198
  // Step 3: build the keep-set.
@@ -191,18 +234,81 @@ function pruneMarketplaceCache(opts) {
191
234
  }
192
235
  }
193
236
 
237
+ // Phase 126 Plan-06: stale-backup pass. ADDITIVE to the cache prune above;
238
+ // walks PLUGIN_HOME (sibling of cache/) for mindrian-os.stale-* dirs older
239
+ // than `ageDays`. Failures are swallowed per-entry (best-effort, mirrors the
240
+ // existing per-version rmSync error path).
241
+ const removedBackups = pruneStaleBackups(home, ageDays, dryRun);
242
+
194
243
  return {
195
244
  kept: Array.from(keep),
196
245
  removed,
246
+ removedBackups,
197
247
  skipped: false,
198
248
  reason: null,
249
+ ageDays,
199
250
  };
200
251
  }
201
252
 
253
+ // ---------------------------------------------------------------------------
254
+ // Phase 126 Plan-06 helper -- prune stale backup directories.
255
+ //
256
+ // Walks <home>/.claude/plugins/ for entries whose basename matches the
257
+ // LITERAL prefix /^mindrian-os\.stale-/ (the period after `mindrian-os`
258
+ // is what gates the match -- does NOT match `mindrian-os/` live install
259
+ // or unrelated siblings such as `mindrian-marketplace/`).
260
+ //
261
+ // Returns an array of absolute paths that were removed (or that would
262
+ // have been removed in dryRun mode). On error (unreadable dir, stat
263
+ // failure, rm failure), the entry is silently skipped -- best-effort
264
+ // semantics mirror the in-loop pattern of the cache-version prune above.
265
+ // ---------------------------------------------------------------------------
266
+ function pruneStaleBackups(home, ageDays, dryRun) {
267
+ const removedBackups = [];
268
+ const pluginsDir = path.join(home, '.claude', 'plugins');
269
+ const cutoffMs = Date.now() - (ageDays * 86400000);
270
+
271
+ let entries = [];
272
+ try {
273
+ entries = fs.readdirSync(pluginsDir, { withFileTypes: true });
274
+ } catch (_) {
275
+ // pluginsDir absent: nothing to prune. Return empty list.
276
+ return removedBackups;
277
+ }
278
+
279
+ for (const e of entries) {
280
+ if (!e.isDirectory()) continue;
281
+ if (!/^mindrian-os\.stale-/.test(e.name)) continue;
282
+ const fullPath = path.join(pluginsDir, e.name);
283
+ let mtimeMs;
284
+ try {
285
+ mtimeMs = fs.statSync(fullPath).mtimeMs;
286
+ } catch (_) {
287
+ continue; // stat failure: leave for inspection
288
+ }
289
+ if (mtimeMs >= cutoffMs) continue; // recent enough; retain
290
+
291
+ if (dryRun) {
292
+ removedBackups.push(fullPath);
293
+ continue;
294
+ }
295
+ try {
296
+ fs.rmSync(fullPath, { recursive: true, force: true });
297
+ removedBackups.push(fullPath);
298
+ } catch (_) {
299
+ // Best-effort: a failed removal does not abort the whole prune.
300
+ // (Mirrors the existing pattern in the cache-version prune above.)
301
+ }
302
+ }
303
+
304
+ return removedBackups;
305
+ }
306
+
202
307
  module.exports = {
203
308
  pruneMarketplaceCache,
204
309
  // Internal helpers exposed for testability + future reuse.
205
310
  readActiveVersion,
206
311
  listCacheVersions,
207
312
  sortByMtimeDesc,
313
+ pruneStaleBackups,
208
314
  };
@@ -0,0 +1,25 @@
1
+ # lib/core/feynman/
2
+
3
+ Phase 124 (FEYNMAN.md temporal awareness) renderer + runner ship here.
4
+
5
+ Contains (after Plans 124-01 and 124-02 land):
6
+ - `timeline-renderer.cjs` -- pure function `renderTimeline(db, sectionSlug, opts) -> { markdown_body, summary_stats }`. Reads ONLY via `lib/core/navigation.cjs` (the Phase 109 closed chokepoint): `findRecentChanges`, `findStaleDecisions`, and the new `firstCapturedLastTouchedBySection` (added as the 15th re-export in Plan 124-01, mirroring the Phase 110-03 `logMemoryEvent` re-export idiom). ZERO filesystem reads. ZERO Brain calls. ZERO LLM calls.
7
+ - `timeline-runner.cjs` -- `refreshAll(roomDir)` + `refreshSection(roomDir, sectionSlug)`. Walks the room's section folders, finds each `FEYNMAN.md` with the sentinel pair (creates the pair if absent on first run), reads the surrounding body, calls the renderer, writes the file back with the body byte-preserved and the sentinel-bounded section replaced, sets `timeline_last_rendered: <ISO>` frontmatter (second-resolution). Each refresh logs a `memory_event` of type `feynman_timeline_refreshed`. Idempotent (re-run -> byte-identical output).
8
+
9
+ Sentinel pair (D-02 hard invariant; bytes outside the pair are byte-preserved across regeneration):
10
+ - `<!-- TIMELINE_AUTO_START -->`
11
+ - `<!-- TIMELINE_AUTO_END -->`
12
+
13
+ Stale thresholds (D-06; overridable via `process.env.MINDRIAN_TIMELINE_THRESHOLDS_JSON` for tests):
14
+ - recent < 7 days
15
+ - quiet 7-30 days
16
+ - stale 30-90 days
17
+ - dormant > 90 days
18
+
19
+ Owner: Phase 124 FEYNMAN.md Temporal Awareness.
20
+ Canon: Part 9 (the Larry-explains face of memory_event). Renderer reads ONLY room.db via navigation.cjs (D-03). Writes ONLY FEYNMAN.md inside the sentinels (D-02 hard invariant).
21
+ Boundary: NO Brain calls (Canon Part 8); NO filesystem reads outside the FEYNMAN.md being written and the room.db family (allow-list pattern enforced by `tests/test-feynman-timeline-canon-part-9-invariant.cjs`).
22
+
23
+ Upstream: `lib/core/navigation.cjs` (Phase 109 chokepoint), `lib/core/navigation/memory-events.cjs` (EVENT_TYPES +2 in Plan 124-02), `lib/core/navigation/insights.cjs` (source_section provenance).
24
+
25
+ See: `.planning/phases/124-feynman-temporal-awareness/124-CONTEXT.md`.