@mindrian_os/install 1.13.0-beta.12 → 1.13.0-beta.14

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 (123) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +57 -10
  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 +2 -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 +2 -1
  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 +2 -1
  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 +8 -3
  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/active-plugin-root.cjs +71 -6
  92. package/lib/core/brain-client.cjs +451 -36
  93. package/lib/core/cache-prune.cjs +208 -0
  94. package/lib/core/feynman/ROOM.md +25 -0
  95. package/lib/core/feynman/timeline-renderer.cjs +197 -0
  96. package/lib/core/feynman/timeline-runner.cjs +281 -0
  97. package/lib/core/navigation/edges.cjs +86 -0
  98. package/lib/core/navigation/insights.cjs +37 -0
  99. package/lib/core/navigation/memory-events.cjs +56 -1
  100. package/lib/core/navigation/neighborhood.cjs +5 -4
  101. package/lib/core/navigation/packet.cjs +176 -10
  102. package/lib/core/navigation/projections.cjs +201 -0
  103. package/lib/core/navigation.cjs +31 -0
  104. package/lib/core/resolve-brain-key.cjs +201 -0
  105. package/lib/mcp/larry-server-instructions.md +1 -1
  106. package/lib/memory/brain-cypher-chain-slice.test.cjs +368 -0
  107. package/lib/memory/f-selector-ranker.test.cjs +593 -0
  108. package/lib/memory/navigation-projections.test.cjs +241 -0
  109. package/lib/memory/navigation-write-edge.test.cjs +206 -0
  110. package/lib/memory/packet-chain-hint.test.cjs +407 -0
  111. package/lib/memory/packet-schema-validation.test.cjs +317 -0
  112. package/lib/memory/per-command-jtbd-derivation.test.cjs +130 -0
  113. package/lib/memory/per-command-teaching.test.cjs +110 -0
  114. package/lib/memory/run-feynman-tests.cjs +121 -0
  115. package/lib/memory/security-trifecta.test.cjs +23 -6
  116. package/lib/memory/selector-decisions.test.cjs +417 -0
  117. package/lib/memory/selector-miss.test.cjs +290 -0
  118. package/lib/workflow/f-selector-ranker.cjs +420 -0
  119. package/lib/workflow/selector-decisions.cjs +368 -0
  120. package/package.json +4 -1
  121. package/references/design/email-template-standard.md +1 -1
  122. package/references/user-research/2026-04-05-leah-lawrence-session.md +3 -3
  123. package/skills/brain-connector/SKILL.md +9 -3
package/commands/room.md CHANGED
@@ -5,6 +5,7 @@ argument-hint: [overview|<section>]
5
5
  body_shape_overview: B (Semantic Tree)
6
6
  body_shape_section: C (Room Card)
7
7
  serves_jtbd: ["audit-room"]
8
+ teaching: "When you need to view or launch the active Data Room, /mos:room opens the room view with its current state. The default entry point for room navigation."
8
9
  ui_reference: skills/ui-system/SKILL.md
9
10
  allowed-tools:
10
11
  - Read
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
@@ -142,7 +143,7 @@ Remove `neo4j-brain` and `pinecone-brain` from `.mcp.json` if present.
142
143
 
143
144
  Ask the user:
144
145
 
145
- > "Do you have a Brain API key? If not, request one at mindrianos-jsagirs-projects.vercel.app/brain-access -- you'll get it within 24 hours."
146
+ > "Do you have a Brain API key? If not, request one at mindrianos.vercel.app/brain-access -- you'll get it within 24 hours."
146
147
 
147
148
  If the user provides a key:
148
149
 
@@ -163,9 +164,13 @@ if [ -f ~/.mindrian.env ] && grep -q "MINDRIAN_BRAIN_KEY" ~/.mindrian.env; then
163
164
  else
164
165
  echo "MINDRIAN_BRAIN_KEY=<their-key>" >> ~/.mindrian.env
165
166
  fi
167
+ # SEC-02 (Phase 123 Plan-07): lock down permissions on POSIX (no-op on Windows).
168
+ # Without this, lib/core/resolve-brain-key.cjs refuses to load the key from a
169
+ # group/world-readable file and session-start shows "Brain: NOT loaded".
170
+ chmod 600 "$HOME/.mindrian.env" 2>/dev/null || true
166
171
  ```
167
172
 
168
- Tell the user: "Key saved to both your project `.env` and `~/.mindrian.env` (global backup). Brain will connect from any directory now."
173
+ Tell the user: "Key saved to both your project `.env` and `~/.mindrian.env` (global backup, chmod 600). Brain will connect from any directory now."
169
174
 
170
175
  ### 4. Test Connection
171
176
 
@@ -206,7 +211,7 @@ curl -s -w "\n%{http_code}" --max-time 15 \
206
211
  > "Brain connected and verified. Larry just got smarter. Your existing commands now have graph intelligence behind them. Try `/mos:suggest-next`."
207
212
 
208
213
  **On health OK but key auth failure (401):**
209
- > "Brain server is up, but your key was rejected. Double-check the key you received, or request a new one at mindrianos-jsagirs-projects.vercel.app/brain-access"
214
+ > "Brain server is up, but your key was rejected. Double-check the key you received, or request a new one at mindrianos.vercel.app/brain-access"
210
215
 
211
216
  **On health OK but key verification timeout:**
212
217
  > "Brain server is up and your key is saved. Verification timed out but that is normal on first connect. Try `/mos:suggest-next` to confirm it works."
@@ -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
+ };
@@ -116,19 +116,84 @@ function fromLegacyClone(home) {
116
116
  return isPluginDir(legacy) ? legacy : null;
117
117
  }
118
118
 
119
+ // Phase 123 Plan-02 (HARNESS-123-05): classify the resolved plugin root into one
120
+ // of the 4 known topologies. RESEARCH Pitfall 4: a legacy-clone dir can carry an
121
+ // unrelated origin remote (the dogfood box's ~/.claude/plugins/mindrian-os/ has
122
+ // its origin pointed at mindrian-agno-backend.git) so a naive "has origin ->
123
+ // dev-clone" heuristic mis-classifies it. The order below is precedence-locked:
124
+ // env override and explicit legacy path win before the structural git probe.
125
+ //
126
+ // Returns one of: 'marketplace-cache' | 'dev-clone' | 'legacy' | 'not-found'.
127
+ function classifyTopology(root, source) {
128
+ if (!root) return 'not-found';
129
+ // env override -> always treated as a dev clone (tests, dev boxes, hand clones).
130
+ if (source === 'MINDRIAN_OS_ROOT') return 'dev-clone';
131
+
132
+ const home = os.homedir();
133
+ // Explicit legacy path wins before the structural check -- some legacy clones
134
+ // carry an unrelated origin remote (Pitfall 4).
135
+ try {
136
+ if (path.resolve(root) === path.resolve(home, '.claude', 'plugins', 'mindrian-os')) {
137
+ return 'legacy';
138
+ }
139
+ } catch { /* ignore */ }
140
+
141
+ // Marketplace cache: under ~/.claude/plugins/cache/<mp>/{mos|mindrian-os}/<version>/.
142
+ try {
143
+ const cacheBase = path.resolve(home, '.claude', 'plugins', 'cache');
144
+ const resolved = path.resolve(root);
145
+ if (resolved.startsWith(cacheBase + path.sep)) {
146
+ return 'marketplace-cache';
147
+ }
148
+ } catch { /* ignore */ }
149
+
150
+ // Dev clone structural probe: a .git + install.sh + an origin remote URL that
151
+ // mentions the plugin repo name. execSync is wrapped in try/catch so a missing
152
+ // git binary or a non-git dir falls through cleanly.
153
+ try {
154
+ if (fs.existsSync(path.join(root, '.git')) && fs.existsSync(path.join(root, 'install.sh'))) {
155
+ const cp = require('node:child_process');
156
+ let originUrl = '';
157
+ try {
158
+ originUrl = cp.execSync('git -C ' + JSON.stringify(root) + ' remote get-url origin', {
159
+ stdio: ['ignore', 'pipe', 'ignore'],
160
+ encoding: 'utf8',
161
+ timeout: 1500,
162
+ }).trim();
163
+ } catch { /* no origin remote, swallow */ }
164
+ if (/mindrian-os-plugin/i.test(originUrl)) return 'dev-clone';
165
+ }
166
+ } catch { /* ignore */ }
167
+
168
+ // Defensive defaults: respect the resolver's source classification.
169
+ if (source === 'legacy-clone') return 'legacy';
170
+ // A resolved plugin dir that is not under a recognized cache path and not the
171
+ // legacy fixed path and does not pass the dev-clone probe -- treat as
172
+ // marketplace-cache (the bin/cli.js + installed_plugins.json path).
173
+ return 'marketplace-cache';
174
+ }
175
+
119
176
  function resolveActivePluginRoot() {
120
177
  const envRoot = process.env.MINDRIAN_OS_ROOT;
121
- if (envRoot) return { root: envRoot, source: 'MINDRIAN_OS_ROOT' };
178
+ if (envRoot) {
179
+ return { root: envRoot, source: 'MINDRIAN_OS_ROOT', topology: classifyTopology(envRoot, 'MINDRIAN_OS_ROOT') };
180
+ }
122
181
 
123
182
  const home = os.homedir();
124
183
  let r;
125
- if ((r = fromInstalledPlugins(home))) return { root: r, source: 'installed_plugins.json' };
126
- if ((r = fromMarketplaceCache(home))) return { root: r, source: 'marketplace-cache' };
127
- if ((r = fromLegacyClone(home))) return { root: r, source: 'legacy-clone' };
128
- return { root: null, source: 'not-found' };
184
+ if ((r = fromInstalledPlugins(home))) {
185
+ return { root: r, source: 'installed_plugins.json', topology: classifyTopology(r, 'installed_plugins.json') };
186
+ }
187
+ if ((r = fromMarketplaceCache(home))) {
188
+ return { root: r, source: 'marketplace-cache', topology: classifyTopology(r, 'marketplace-cache') };
189
+ }
190
+ if ((r = fromLegacyClone(home))) {
191
+ return { root: r, source: 'legacy-clone', topology: classifyTopology(r, 'legacy-clone') };
192
+ }
193
+ return { root: null, source: 'not-found', topology: 'not-found' };
129
194
  }
130
195
 
131
- module.exports = { resolveActivePluginRoot };
196
+ module.exports = { resolveActivePluginRoot, classifyTopology };
132
197
 
133
198
  // CLI entry point.
134
199
  if (require.main === module) {