@mindrian_os/cli 2.0.0-beta.29 → 2.0.0-beta.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +21 -0
- package/.mcp.json +14 -0
- package/CHANGELOG.md +41 -0
- package/agents/analogy-query-fetcher.md +119 -0
- package/agents/brain-query.md +91 -0
- package/agents/competitor-watch-fetcher.md +94 -0
- package/agents/framework-runner.md +258 -0
- package/agents/grading.md +216 -0
- package/agents/grant-reviewer.md +191 -0
- package/agents/investor.md +141 -0
- package/agents/larry-extended.md +215 -0
- package/agents/meeting-perspective-extractor.md +94 -0
- package/agents/opportunity-scanner.md +105 -0
- package/agents/persona-analyst.md +201 -0
- package/agents/research.md +103 -0
- package/agents/reverse-salient-agent.md +42 -0
- package/agents/vault-section-reviewer.md +107 -0
- package/assets/banner-showcase.html +537 -0
- package/assets/fonts/BebasNeue-Regular.ttf +0 -0
- package/assets/fonts/Inter-Bold.ttf +0 -0
- package/assets/fonts/Inter-Regular.ttf +0 -0
- package/assets/logo.svg +10 -0
- package/bin/local-chain-recommender.cjs +256 -0
- package/bin/mindrian-brain-mcp-client.cjs +319 -0
- package/bin/mindrian-mcp-server.cjs +445 -0
- package/bin/mindrian-mcp-shim.cjs +100 -0
- package/bin/mindrian-tools.cjs +784 -0
- package/commands/act.md +529 -0
- package/commands/admin.md +439 -0
- package/commands/agentshield.md +49 -0
- package/commands/analyze-needs.md +78 -0
- package/commands/analyze-systems.md +73 -0
- package/commands/analyze-timing.md +98 -0
- package/commands/auto-explore.md +85 -0
- package/commands/beautiful-question.md +76 -0
- package/commands/bono.md +152 -0
- package/commands/brain-derive.md +137 -0
- package/commands/build-knowledge.md +121 -0
- package/commands/build-thesis.md +94 -0
- package/commands/causal.md +269 -0
- package/commands/challenge-assumptions.md +78 -0
- package/commands/compare-ventures.md +117 -0
- package/commands/correct-reference-now.md +88 -0
- package/commands/dashboard.md +144 -0
- package/commands/deck.md +117 -0
- package/commands/deep-grade.md +214 -0
- package/commands/diagnose.md +131 -0
- package/commands/diagnostics.md +195 -0
- package/commands/dial-memory-refresh.md +113 -0
- package/commands/diffusion.md +129 -0
- package/commands/discover.md +221 -0
- package/commands/doctor.md +271 -0
- package/commands/dogfood-flush.md +130 -0
- package/commands/dominant-designs.md +76 -0
- package/commands/eureka.md +315 -0
- package/commands/explain-decision.md +135 -0
- package/commands/explore-domains.md +78 -0
- package/commands/explore-futures.md +67 -0
- package/commands/explore-opportunity.md +172 -0
- package/commands/explore-trends.md +78 -0
- package/commands/export.md +128 -0
- package/commands/feynman-timeline-refresh.md +102 -0
- package/commands/file-meeting.md +1195 -0
- package/commands/find-analogies.md +369 -0
- package/commands/find-bottlenecks.md +108 -0
- package/commands/find-connections.md +111 -0
- package/commands/funding.md +114 -0
- package/commands/futures.md +128 -0
- package/commands/grade-grant.md +297 -0
- package/commands/grade.md +257 -0
- package/commands/graph.md +218 -0
- package/commands/hat-briefing.md +163 -0
- package/commands/heal.md +72 -0
- package/commands/help.md +176 -0
- package/commands/hmi-status.md +75 -0
- package/commands/ignite.md +229 -0
- package/commands/ingest-methodology.md +128 -0
- package/commands/intel-pipeline.md +133 -0
- package/commands/jtbd.md +262 -0
- package/commands/leadership.md +113 -0
- package/commands/lean-canvas.md +74 -0
- package/commands/macro-trends.md +88 -0
- package/commands/map-unknowns.md +76 -0
- package/commands/memory-cortex-reach.md +79 -0
- package/commands/memory.md +213 -0
- package/commands/models.md +200 -0
- package/commands/mos-reason.md +489 -0
- package/commands/mos.md +162 -0
- package/commands/mullins.md +155 -0
- package/commands/mva-brief.md +86 -0
- package/commands/mva-option.md +119 -0
- package/commands/mva-report.md +76 -0
- package/commands/new-project.md +633 -0
- package/commands/new-surface.md +116 -0
- package/commands/onboard.md +489 -0
- package/commands/operator.md +168 -0
- package/commands/opportunities.md +177 -0
- package/commands/organize.md +74 -0
- package/commands/persona.md +276 -0
- package/commands/pipeline.md +167 -0
- package/commands/present.md +125 -0
- package/commands/publish.md +227 -0
- package/commands/pws-brain.md +154 -0
- package/commands/qualify-opportunity.md +113 -0
- package/commands/query.md +70 -0
- package/commands/radar.md +110 -0
- package/commands/reanalyze.md +124 -0
- package/commands/research.md +467 -0
- package/commands/room.md +385 -0
- package/commands/rooms.md +682 -0
- package/commands/root-cause.md +77 -0
- package/commands/rs-experts.md +153 -0
- package/commands/rs-explain.md +134 -0
- package/commands/rs-fetch.md +119 -0
- package/commands/rs-thesis.md +131 -0
- package/commands/scenario-plan.md +80 -0
- package/commands/scheduled-tasks.md +308 -0
- package/commands/score-innovation.md +78 -0
- package/commands/scout.md +379 -0
- package/commands/setup.md +658 -0
- package/commands/show.md +71 -0
- package/commands/skill.md +108 -0
- package/commands/snapshot.md +179 -0
- package/commands/speakers.md +118 -0
- package/commands/splash.md +55 -0
- package/commands/stance.md +91 -0
- package/commands/status.md +109 -0
- package/commands/structure-argument.md +82 -0
- package/commands/suggest-next.md +141 -0
- package/commands/systems-thinking.md +224 -0
- package/commands/think-hats.md +86 -0
- package/commands/trending-to-absurd.md +151 -0
- package/commands/update.md +256 -0
- package/commands/user-needs.md +77 -0
- package/commands/validate.md +77 -0
- package/commands/value-proposition.md +116 -0
- package/commands/vault.md +330 -0
- package/commands/visualize.md +71 -0
- package/commands/whitespace.md +541 -0
- package/commands/wiki.md +96 -0
- package/data/ROOM.md +47 -0
- package/data/brain-census.generated.json +2617 -0
- package/data/brain-orchestration-projection.json +4973 -0
- package/data/brain-packet-schema.json +545 -0
- package/data/brain-surface-contract.json +32 -0
- package/data/capability-ledger.json +391 -0
- package/data/capability-map-registry.json +23 -0
- package/data/command-registry.json +2433 -0
- package/data/connector-coverage-ledger.json +1618 -0
- package/data/connector-out-of-spine-allowlist.json +71 -0
- package/data/connector-registry.json +3637 -0
- package/data/corpus-stats-source.json +11 -0
- package/data/corpus-stats.generated.json +10 -0
- package/data/cross-domain-analogues.json +15 -0
- package/data/deck-aliases.json +6 -0
- package/data/deck-styles.json +85 -0
- package/data/deployment-surfaces.json +69 -0
- package/data/dispatch-framework-map.json +18 -0
- package/data/doctor-modules.json +206 -0
- package/data/dual-graph-health-baseline.json +12 -0
- package/data/eureka-critic-tags.json +32 -0
- package/data/first-reward-surfaces.json +66 -0
- package/data/first-touch-surfaces.json +60 -0
- package/data/flagship-floor-set.json +37 -0
- package/data/framework-names.json +121 -0
- package/data/grant-rubric-fixtures/early-stage-pre-seed.json +18 -0
- package/data/grant-rubric-fixtures/early-stage-round-a.json +18 -0
- package/data/grant-rubric-fixtures/early-stage-seed.json +18 -0
- package/data/grant-rubric-fixtures/hybrid-seed-incentive.json +18 -0
- package/data/grant-rubric-fixtures/rd-fund.json +18 -0
- package/data/grant-rubric-fixtures/regional-innovation-incubators.json +18 -0
- package/data/grant-rubric-fixtures/technology-incubators.json +18 -0
- package/data/grant-rubric-fixtures/tnufa.json +167 -0
- package/data/grant-rubric-schema.json +33 -0
- package/data/harness-fixtures/converged-room/.context/ROOM.md +13 -0
- package/data/harness-fixtures/converged-room/.intelligence/ROOM.md +13 -0
- package/data/harness-fixtures/converged-room/.snapshots/ROOM.md +13 -0
- package/data/harness-fixtures/converged-room/MINTO.md +19 -0
- package/data/harness-fixtures/converged-room/STATE.md +73 -0
- package/data/harness-fixtures/converged-room/USER.md +23 -0
- package/data/harness-fixtures/converged-room/assets/ROOM.md +13 -0
- package/data/harness-fixtures/converged-room/business-model/CONTEXT.md +32 -0
- package/data/harness-fixtures/converged-room/business-model/ROOM.md +36 -0
- package/data/harness-fixtures/converged-room/competitive-analysis/CONTEXT.md +33 -0
- package/data/harness-fixtures/converged-room/competitive-analysis/ROOM.md +35 -0
- package/data/harness-fixtures/converged-room/financial-model/CONTEXT.md +30 -0
- package/data/harness-fixtures/converged-room/financial-model/ROOM.md +35 -0
- package/data/harness-fixtures/converged-room/funding/CONTEXT.md +41 -0
- package/data/harness-fixtures/converged-room/funding/ROOM.md +35 -0
- package/data/harness-fixtures/converged-room/legal-ip/CONTEXT.md +30 -0
- package/data/harness-fixtures/converged-room/legal-ip/ROOM.md +35 -0
- package/data/harness-fixtures/converged-room/market-analysis/CONTEXT.md +32 -0
- package/data/harness-fixtures/converged-room/market-analysis/ROOM.md +35 -0
- package/data/harness-fixtures/converged-room/opportunity-bank/CONTEXT.md +39 -0
- package/data/harness-fixtures/converged-room/opportunity-bank/ROOM.md +36 -0
- package/data/harness-fixtures/converged-room/problem-definition/CONTEXT.md +32 -0
- package/data/harness-fixtures/converged-room/problem-definition/ROOM.md +36 -0
- package/data/harness-fixtures/converged-room/references/ROOM.md +13 -0
- package/data/harness-fixtures/converged-room/references/SECTION-SCHEMA.md +213 -0
- package/data/harness-fixtures/converged-room/references/SUB-SCHEMAS.md +91 -0
- package/data/harness-fixtures/converged-room/solution-design/CONTEXT.md +36 -0
- package/data/harness-fixtures/converged-room/solution-design/ROOM.md +36 -0
- package/data/harness-fixtures/converged-room/strategy/CONTEXT.md +35 -0
- package/data/harness-fixtures/converged-room/strategy/ROOM.md +35 -0
- package/data/harness-fixtures/converged-room/team/ROOM.md +13 -0
- package/data/harness-fixtures/converged-room/team-execution/CONTEXT.md +36 -0
- package/data/harness-fixtures/converged-room/team-execution/ROOM.md +35 -0
- package/data/harness-manifest.json +74 -0
- package/data/harness-policies/CONTEXT.md +55 -0
- package/data/harness-policies/_schema.json +42 -0
- package/data/harness-policies/contract-parity-larry.json +63 -0
- package/data/harness-policies/gate-card-fire.json +13 -0
- package/data/harness-policies/gate-graph-derive-health.json +13 -0
- package/data/harness-policies/gate-shape-declaration.json +13 -0
- package/data/harness-policies/gate-tool-honesty.json +13 -0
- package/data/harness-policies/gate-worktree-hygiene.json +13 -0
- package/data/harness-policies/memory-write-policy.json +28 -0
- package/data/harness-policies/registry-drift.json +13 -0
- package/data/harness-policies/release-payload-ceiling.json +13 -0
- package/data/harness-policies/voice-backend-noun-free.json +13 -0
- package/data/harness-policies/voice-fork-as-card.json +13 -0
- package/data/harness-policies/voice-glyph-present.json +13 -0
- package/data/harness-policies/voice-hyphens-only.json +13 -0
- package/data/help-groups.json +236 -0
- package/data/hitl-shape-backfill.json +777 -0
- package/data/hitl-shape-declaration-fixtures/invalid-bad-shape.json +5 -0
- package/data/hitl-shape-declaration-fixtures/invalid-missing-why.json +4 -0
- package/data/hitl-shape-declaration-fixtures/invalid-ranker-contradiction.json +6 -0
- package/data/hitl-shape-declaration-fixtures/invalid-skill-gap.json +3 -0
- package/data/hitl-shape-declaration-fixtures/valid-multi-stage.json +7 -0
- package/data/hitl-shape-declaration-fixtures/valid-single-shape.json +5 -0
- package/data/hitl-shape-declaration-fixtures/valid-skill-exempt.json +5 -0
- package/data/hitl-shape-declaration-schema.json +34 -0
- package/data/hitl-stages-fixtures/bono.json +10 -0
- package/data/hitl-stages-fixtures/pipeline-1.json +9 -0
- package/data/hitl-stages-fixtures/pipeline-2.json +9 -0
- package/data/hitl-stages-fixtures/pipeline-3.json +10 -0
- package/data/hitl-stages-fixtures/research.json +10 -0
- package/data/hitl-stages-fixtures/rs-convergence.json +10 -0
- package/data/hitl-stages-fixtures/rs-family.json +11 -0
- package/data/hitl-stages-fixtures/rs-reverse-salient.json +10 -0
- package/data/hitl-stages-fixtures/trending-to-absurd.json +11 -0
- package/data/hitl-stages-schema.json +26 -0
- package/data/mcp-tool-connectors.json +464 -0
- package/data/mva-agent-prompts.json +31 -0
- package/data/mva-deck-template.html +15 -0
- package/data/mva-heuristic-keywords.json +35 -0
- package/data/orchestration-command-ledger.json +576 -0
- package/data/orchestration-unwired-allowlist.json +6 -0
- package/data/portfolio-ahp-matrix.json +16 -0
- package/data/publish-needs.json +76 -0
- package/data/render-coverage-registry.json +1152 -0
- package/data/research-sources.json +58 -0
- package/data/roadmap-type-chains.json +28 -0
- package/data/room-blueprints.json +156 -0
- package/data/subagent-dispatch-grants.json +150 -0
- package/hooks/hooks.json +493 -0
- package/hooks/run-hook.cmd +64 -0
- package/lib/__init__.py +0 -0
- package/lib/__pycache__/__init__.cpython-312.pyc +0 -0
- package/lib/agents/auto-explore-agent.cjs +1163 -0
- package/lib/agents/mva/brain-classic-traps.cjs +55 -0
- package/lib/agents/mva/brain-cross-domain.cjs +79 -0
- package/lib/agents/mva/brain-similar-ventures.cjs +93 -0
- package/lib/agents/mva/dashboard-graph-neighborhood.cjs +72 -0
- package/lib/agents/mva/index.cjs +42 -0
- package/lib/agents/mva/six-hats-red-black.cjs +137 -0
- package/lib/agents/mva/tavily-funding-scan.cjs +147 -0
- package/lib/agents/mva/test-all-six-agents.cjs +467 -0
- package/lib/agents/reverse-salient-agent.cjs +757 -0
- package/lib/agents/tension-hook-agent.cjs +586 -0
- package/lib/brain/ROOM.md +44 -0
- package/lib/brain/chain-recommender.cjs +784 -0
- package/lib/brain/curation-batch.cjs +519 -0
- package/lib/brain/framework-chain-slice.cjs +252 -0
- package/lib/brain/hypergraph-event-schema.cjs +184 -0
- package/lib/chat/chat-context.js +185 -0
- package/lib/chat/chat-panel.js +721 -0
- package/lib/chat/fabric-chat.cjs +288 -0
- package/lib/chat/generative-tools.js +219 -0
- package/lib/conversation/ROOM.md +39 -0
- package/lib/conversation/classifier-rules.json +38 -0
- package/lib/conversation/classifier.cjs +264 -0
- package/lib/conversation/operator-epistemic-cap.test.cjs +156 -0
- package/lib/conversation/operator.cjs +492 -0
- package/lib/conversation/operator.test.cjs +160 -0
- package/lib/copy/115-spec-strings.cjs +55 -0
- package/lib/core/__init__.py +0 -0
- package/lib/core/__nav-stub.cjs +14 -0
- package/lib/core/__pycache__/__init__.cpython-312.pyc +0 -0
- package/lib/core/__pycache__/rs-math.cpython-312.pyc +0 -0
- package/lib/core/__pycache__/rs_cache.cpython-312.pyc +0 -0
- package/lib/core/__pycache__/rs_corpus.cpython-312.pyc +0 -0
- package/lib/core/__pycache__/rs_corpus_exclude.cpython-312.pyc +0 -0
- package/lib/core/__pycache__/rs_hybrid.cpython-312.pyc +0 -0
- package/lib/core/__pycache__/rs_math.cpython-312.pyc +0 -0
- package/lib/core/__pycache__/rs_rooms.cpython-312.pyc +0 -0
- package/lib/core/abstraction-gate.cjs +133 -0
- package/lib/core/act-jtbd-blurb.cjs +231 -0
- package/lib/core/artifact-id.cjs +148 -0
- package/lib/core/asset-ops.cjs +151 -0
- package/lib/core/auto-commit-throttle.cjs +129 -0
- package/lib/core/bearer-token.cjs +199 -0
- package/lib/core/behavioral/observation-schema.cjs +86 -0
- package/lib/core/bono/cell-fanout.cjs +305 -0
- package/lib/core/bono/debate-composition.cjs +495 -0
- package/lib/core/bono/hat-governance.cjs +367 -0
- package/lib/core/bono/persona-research.cjs +288 -0
- package/lib/core/bono/reviewer-governance.cjs +331 -0
- package/lib/core/brain-client.cjs +2365 -0
- package/lib/core/brain-derivation-prompts.cjs +331 -0
- package/lib/core/brain-derivation-queue.cjs +535 -0
- package/lib/core/brain-derivation.cjs +740 -0
- package/lib/core/brain-md-schema.cjs +528 -0
- package/lib/core/brain-md-staleness.cjs +357 -0
- package/lib/core/brain-response-sanitize.cjs +396 -0
- package/lib/core/breakthrough/canary.cjs +134 -0
- package/lib/core/breakthrough/canary.test.cjs +136 -0
- package/lib/core/breakthrough/detectors.cjs +359 -0
- package/lib/core/breakthrough/detectors.test.cjs +333 -0
- package/lib/core/breakthrough/ethics-fence.cjs +129 -0
- package/lib/core/breakthrough/ethics-fence.test.cjs +178 -0
- package/lib/core/breakthrough/resurfacing.cjs +150 -0
- package/lib/core/breakthrough/resurfacing.test.cjs +233 -0
- package/lib/core/breakthrough/review-queue.cjs +168 -0
- package/lib/core/breakthrough/review-queue.test.cjs +160 -0
- package/lib/core/breakthrough/scanner-d17-d18.test.cjs +229 -0
- package/lib/core/breakthrough/scanner.cjs +435 -0
- package/lib/core/breakthrough/scanner.test.cjs +267 -0
- package/lib/core/breakthrough/schema.cjs +180 -0
- package/lib/core/breakthrough/schema.test.cjs +256 -0
- package/lib/core/breakthrough/scoring.cjs +293 -0
- package/lib/core/breakthrough/scoring.test.cjs +423 -0
- package/lib/core/breakthrough/verb-dispatch.cjs +230 -0
- package/lib/core/breakthrough/verb-dispatch.test.cjs +185 -0
- package/lib/core/breakthrough/voice-scaffold.cjs +249 -0
- package/lib/core/breakthrough/voice-scaffold.test.cjs +251 -0
- package/lib/core/bridge-writer.cjs +477 -0
- package/lib/core/cache-prune.cjs +314 -0
- package/lib/core/card-fire-gate.cjs +107 -0
- package/lib/core/card-fire-sidechannel.cjs +518 -0
- package/lib/core/chain-executor.cjs +939 -0
- package/lib/core/chain-retry.cjs +214 -0
- package/lib/core/chain-step-dispatcher.cjs +397 -0
- package/lib/core/chat-context-builder.cjs +253 -0
- package/lib/core/check-plugin-enabled.cjs +145 -0
- package/lib/core/close-loop-writer.cjs +492 -0
- package/lib/core/command-registration-check.cjs +246 -0
- package/lib/core/correlation-label-index.cjs +230 -0
- package/lib/core/correlation.cjs +112 -0
- package/lib/core/coverage-monitor.cjs +183 -0
- package/lib/core/coverage-rollup.cjs +232 -0
- package/lib/core/cross-room-aggregator.cjs +1127 -0
- package/lib/core/cross-room-store.cjs +292 -0
- package/lib/core/cross-room-triggers.cjs +222 -0
- package/lib/core/daily-briefing.cjs +438 -0
- package/lib/core/decision-axes.cjs +178 -0
- package/lib/core/decision-capture.cjs +618 -0
- package/lib/core/deck-design-rules.cjs +211 -0
- package/lib/core/deep-links.cjs +82 -0
- package/lib/core/directive-envelope.cjs +217 -0
- package/lib/core/directive-envelope.test.cjs +225 -0
- package/lib/core/dispatch-optimizer.cjs +354 -0
- package/lib/core/doc-text-extractor.cjs +133 -0
- package/lib/core/doctor/capability-ledger-module.cjs +170 -0
- package/lib/core/doctor/card-fire-health-module.cjs +159 -0
- package/lib/core/doctor/cascade-rooms-active-module.cjs +127 -0
- package/lib/core/doctor/cascade-rooms-module.cjs +184 -0
- package/lib/core/doctor/class-m-brain-smoke.cjs +479 -0
- package/lib/core/doctor/class-m-brain-smoke.test.cjs +448 -0
- package/lib/core/doctor/class-s-eureka-smoke.cjs +380 -0
- package/lib/core/doctor/deployment-surfaces-module.cjs +408 -0
- package/lib/core/doctor/deprecated-usage-module.cjs +155 -0
- package/lib/core/doctor/eureka-fts-health-module.cjs +236 -0
- package/lib/core/doctor/graph-derive-heal-retrofit-module.cjs +148 -0
- package/lib/core/doctor/graph-derive-health-module.cjs +486 -0
- package/lib/core/doctor/install-incomplete-module.cjs +320 -0
- package/lib/core/doctor/install-state-module.cjs +503 -0
- package/lib/core/doctor/mcp-surface-module.cjs +369 -0
- package/lib/core/doctor/mode-select-checkpoint-module.cjs +113 -0
- package/lib/core/doctor/plugin-enabled-state-module.cjs +86 -0
- package/lib/core/doctor/room-graph-density-module.cjs +181 -0
- package/lib/core/doctor/room-md-module.cjs +198 -0
- package/lib/core/doctor/shared.cjs +555 -0
- package/lib/core/doctor/stale-first-touch-copy-module.cjs +83 -0
- package/lib/core/doctor/statusline-visibility-module.cjs +295 -0
- package/lib/core/doctor/ui-compliance-module.cjs +223 -0
- package/lib/core/doctor/umbilical-module.cjs +486 -0
- package/lib/core/doctor/verify-surface-module.cjs +82 -0
- package/lib/core/doctor/voice-style-log-module.cjs +146 -0
- package/lib/core/domain-insight-sweep.cjs +224 -0
- package/lib/core/drift-baseline.cjs +362 -0
- package/lib/core/drift-runtime-reachability.cjs +283 -0
- package/lib/core/dual-path-detector.cjs +84 -0
- package/lib/core/dual-path-detector.test.cjs +334 -0
- package/lib/core/enrichment-queue.cjs +636 -0
- package/lib/core/eureka/ahp-weights.cjs +232 -0
- package/lib/core/eureka/analogy-fitness.cjs +326 -0
- package/lib/core/eureka/compression-meter.cjs +165 -0
- package/lib/core/eureka/embedding-classifier.cjs +226 -0
- package/lib/core/eureka/embedding-spine.cjs +563 -0
- package/lib/core/eureka/entity-classifier.cjs +232 -0
- package/lib/core/eureka/entity-extractor.cjs +356 -0
- package/lib/core/eureka/eureka-enable.cjs +228 -0
- package/lib/core/eureka/eureka-offer.cjs +225 -0
- package/lib/core/eureka/eureka-reach-runner.cjs +430 -0
- package/lib/core/eureka/explore-chain.cjs +834 -0
- package/lib/core/eureka/explored-artifact.cjs +456 -0
- package/lib/core/eureka/fts-index-lifecycle.cjs +265 -0
- package/lib/core/eureka/grade-grant-examine.cjs +544 -0
- package/lib/core/eureka/grade-grant.cjs +596 -0
- package/lib/core/eureka/hybrid-retrieve.cjs +256 -0
- package/lib/core/eureka/lateral-engine-adapter.cjs +118 -0
- package/lib/core/eureka/lexical-overlap.cjs +98 -0
- package/lib/core/eureka/online-pattern-query.cjs +142 -0
- package/lib/core/eureka/opportunity-harvest.cjs +752 -0
- package/lib/core/eureka/opportunity-statement.cjs +519 -0
- package/lib/core/eureka/portfolio-dimensions.cjs +255 -0
- package/lib/core/eureka/qualify-opportunity.cjs +607 -0
- package/lib/core/eureka/reasoning-mode.cjs +481 -0
- package/lib/core/eureka/report-html.cjs +368 -0
- package/lib/core/eureka/research-filing.cjs +547 -0
- package/lib/core/eureka/room-native-substrate.cjs +343 -0
- package/lib/core/eureka/tail-quadrant.cjs +177 -0
- package/lib/core/eureka/tri-modal-index.cjs +564 -0
- package/lib/core/eureka/vector-store.cjs +414 -0
- package/lib/core/eureka-critic.cjs +733 -0
- package/lib/core/eureka-deps-resolver.cjs +179 -0
- package/lib/core/expert-genesis.cjs +437 -0
- package/lib/core/expert-library.cjs +303 -0
- package/lib/core/exports-log.cjs +79 -0
- package/lib/core/feynman/ROOM.md +25 -0
- package/lib/core/feynman/dial-memory-renderer.cjs +279 -0
- package/lib/core/feynman/dial-memory-runner.cjs +173 -0
- package/lib/core/feynman/feynman-seed-writer.cjs +187 -0
- package/lib/core/feynman/timeline-renderer.cjs +197 -0
- package/lib/core/feynman/timeline-runner.cjs +281 -0
- package/lib/core/feynman-minto-invariants.cjs +609 -0
- package/lib/core/findings-wirer.cjs +360 -0
- package/lib/core/first-touch-version-stamper.cjs +113 -0
- package/lib/core/folder-memory-async.cjs +424 -0
- package/lib/core/folder-memory-shared.cjs +890 -0
- package/lib/core/folder-memory.cjs +546 -0
- package/lib/core/framework-chain-composer.cjs +583 -0
- package/lib/core/frontmatter-schemas.cjs +468 -0
- package/lib/core/fusion-router.cjs +540 -0
- package/lib/core/futures/causal-cue.cjs +91 -0
- package/lib/core/futures/orchestrator.cjs +1391 -0
- package/lib/core/futures/subsystem-render.cjs +164 -0
- package/lib/core/gate-relevance.cjs +278 -0
- package/lib/core/git-ops.cjs +141 -0
- package/lib/core/graph-backfill.cjs +562 -0
- package/lib/core/graph-candidate-producer.cjs +235 -0
- package/lib/core/graph-derivation.cjs +546 -0
- package/lib/core/graph-derive-classifier.cjs +352 -0
- package/lib/core/graph-ops.cjs +274 -0
- package/lib/core/graph-refine-loop.cjs +194 -0
- package/lib/core/graph-self-heal.cjs +289 -0
- package/lib/core/greeting-intent-detector.cjs +201 -0
- package/lib/core/grill-engine.cjs +459 -0
- package/lib/core/hat-persistence.cjs +274 -0
- package/lib/core/hsi-engine.cjs +439 -0
- package/lib/core/hsi-lsa.cjs +246 -0
- package/lib/core/hsi-spectral.cjs +554 -0
- package/lib/core/hsi-to-graph.test.cjs +168 -0
- package/lib/core/icm-forest.cjs +703 -0
- package/lib/core/ignite-branch-gate.cjs +66 -0
- package/lib/core/index.cjs +60 -0
- package/lib/core/insight-sensors.cjs +1000 -0
- package/lib/core/install-state.cjs +242 -0
- package/lib/core/integration-registry.cjs +232 -0
- package/lib/core/intel-pipeline.cjs +545 -0
- package/lib/core/intelligence-cascade.cjs +852 -0
- package/lib/core/issue-tree.cjs +269 -0
- package/lib/core/larry-thinness-acknowledgment.cjs +64 -0
- package/lib/core/larry-thinness-acknowledgment.test.cjs +97 -0
- package/lib/core/lazygraph-ops.cjs +1453 -0
- package/lib/core/lens-engine.cjs +434 -0
- package/lib/core/leverage-scan.cjs +218 -0
- package/lib/core/llm-name-suggester.cjs +194 -0
- package/lib/core/llm-name-suggester.test.cjs +132 -0
- package/lib/core/lru-cache.cjs +139 -0
- package/lib/core/mcp-dep-heal.cjs +475 -0
- package/lib/core/mcp-dep-heal.test.cjs +253 -0
- package/lib/core/mcp-profiles.cjs +185 -0
- package/lib/core/meeting-ops.cjs +54 -0
- package/lib/core/memory/governance-candidate-raiser.cjs +205 -0
- package/lib/core/memory/governance-candidate.cjs +95 -0
- package/lib/core/memory/reconcile-memory-runner.cjs +653 -0
- package/lib/core/memory-ops.cjs +641 -0
- package/lib/core/meter/gate-density-reader.cjs +112 -0
- package/lib/core/meter/transfer-reader.cjs +276 -0
- package/lib/core/meter/two-gauge.cjs +245 -0
- package/lib/core/methodology-ingest.cjs +346 -0
- package/lib/core/metrics/larryreach-loop-health.cjs +142 -0
- package/lib/core/migration-snapshot.cjs +287 -0
- package/lib/core/migration-snapshot.test.cjs +174 -0
- package/lib/core/migrations/ROOM.md +33 -0
- package/lib/core/migrations/phase-109-nodes-provenance.cjs +400 -0
- package/lib/core/migrations/phase-109-session-focus.cjs +99 -0
- package/lib/core/migrations/phase-160-nodes-bitemporal.cjs +168 -0
- package/lib/core/migrations/phase-160-nodes-bitemporal.test.cjs +140 -0
- package/lib/core/migrations/phase-162-section-nodes.cjs +156 -0
- package/lib/core/migrations/phase-222-ranker-weights.cjs +62 -0
- package/lib/core/migrations/phase-224-edge-review-status.cjs +87 -0
- package/lib/core/mindrian-brain-shim.test.cjs +221 -0
- package/lib/core/mode-select-sidechannel.cjs +259 -0
- package/lib/core/model-profiles.cjs +246 -0
- package/lib/core/mullins-scaffold.cjs +258 -0
- package/lib/core/mullins-scaffold.test.cjs +128 -0
- package/lib/core/mva-agent-contract.cjs +170 -0
- package/lib/core/mva-agent-contract.test.cjs +169 -0
- package/lib/core/mva-budget.cjs +75 -0
- package/lib/core/mva-budget.test.cjs +68 -0
- package/lib/core/mva-classifier.cjs +392 -0
- package/lib/core/mva-classifier.test.cjs +248 -0
- package/lib/core/mva-deck-builder.cjs +452 -0
- package/lib/core/mva-deck-builder.test.cjs +287 -0
- package/lib/core/mva-detect.smoke.test.cjs +209 -0
- package/lib/core/mva-dispatcher.cjs +110 -0
- package/lib/core/mva-dispatcher.test.cjs +216 -0
- package/lib/core/mva-option-router.cjs +414 -0
- package/lib/core/mva-option-router.test.cjs +483 -0
- package/lib/core/mva-orchestrator.cjs +365 -0
- package/lib/core/mva-orchestrator.test.cjs +916 -0
- package/lib/core/mva-progressive-renderer.cjs +194 -0
- package/lib/core/mva-progressive-renderer.test.cjs +157 -0
- package/lib/core/mva-rule-linter.cjs +458 -0
- package/lib/core/mva-rule-linter.test.cjs +484 -0
- package/lib/core/mva-state.cjs +159 -0
- package/lib/core/mva-telemetry.cjs +58 -0
- package/lib/core/mva-telemetry.test.cjs +196 -0
- package/lib/core/mva-vercel-deploy.cjs +168 -0
- package/lib/core/mva-vercel-deploy.test.cjs +239 -0
- package/lib/core/nav-dial.cjs +440 -0
- package/lib/core/navigation/ROOM.md +15 -0
- package/lib/core/navigation/abstraction-claim.cjs +185 -0
- package/lib/core/navigation/calibration-gate.cjs +284 -0
- package/lib/core/navigation/calibration-log.cjs +141 -0
- package/lib/core/navigation/confirm-node.cjs +73 -0
- package/lib/core/navigation/dashboard-helpers.cjs +145 -0
- package/lib/core/navigation/edges.cjs +1150 -0
- package/lib/core/navigation/evidence-claim.cjs +186 -0
- package/lib/core/navigation/explanation.cjs +48 -0
- package/lib/core/navigation/file-evidence-readback.cjs +242 -0
- package/lib/core/navigation/focus.cjs +155 -0
- package/lib/core/navigation/get-domains-for-trends.cjs +277 -0
- package/lib/core/navigation/governance.cjs +101 -0
- package/lib/core/navigation/grant-rubric.cjs +218 -0
- package/lib/core/navigation/graph-export.cjs +325 -0
- package/lib/core/navigation/ingestion.cjs +109 -0
- package/lib/core/navigation/insights-stale.test.cjs +127 -0
- package/lib/core/navigation/insights.cjs +574 -0
- package/lib/core/navigation/lens-nodes.cjs +315 -0
- package/lib/core/navigation/memory-artifacts.cjs +437 -0
- package/lib/core/navigation/memory-cortex-packet.cjs +274 -0
- package/lib/core/navigation/memory-events.cjs +811 -0
- package/lib/core/navigation/neighborhood.cjs +79 -0
- package/lib/core/navigation/packet.cjs +420 -0
- package/lib/core/navigation/planning-artifacts.cjs +219 -0
- package/lib/core/navigation/projections.cjs +295 -0
- package/lib/core/navigation/ranker-weights.cjs +133 -0
- package/lib/core/navigation/reasoning-write.cjs +211 -0
- package/lib/core/navigation/reconcile-guard.cjs +98 -0
- package/lib/core/navigation/reified-claim.cjs +287 -0
- package/lib/core/navigation/research-preflight.cjs +156 -0
- package/lib/core/navigation/room-birth.cjs +1213 -0
- package/lib/core/navigation/room-context.cjs +380 -0
- package/lib/core/navigation/room-home.cjs +142 -0
- package/lib/core/navigation/spine-events.cjs +548 -0
- package/lib/core/navigation/synthetic-expert.cjs +177 -0
- package/lib/core/navigation/transitions.cjs +253 -0
- package/lib/core/navigation/typed-claim.cjs +208 -0
- package/lib/core/navigation/typed-domain.cjs +249 -0
- package/lib/core/navigation/typed-entity.cjs +236 -0
- package/lib/core/navigation/typed-frame.cjs +257 -0
- package/lib/core/navigation/typed-open-question.cjs +130 -0
- package/lib/core/navigation/typed-opportunity.cjs +418 -0
- package/lib/core/navigation-engine-offer.cjs +776 -0
- package/lib/core/navigation-engine-shared.cjs +256 -0
- package/lib/core/navigation-engine.cjs +1696 -0
- package/lib/core/navigation.cjs +686 -0
- package/lib/core/nl-graph-queries.cjs +164 -0
- package/lib/core/node-insert-epistemic.test.cjs +212 -0
- package/lib/core/node-insert-overrides.test.cjs +273 -0
- package/lib/core/node-insert.cjs +274 -0
- package/lib/core/npm-cli-resolve.cjs +151 -0
- package/lib/core/npm-cli-resolve.test.cjs +153 -0
- package/lib/core/npm-install-lock.cjs +326 -0
- package/lib/core/npm-install-lock.test.cjs +379 -0
- package/lib/core/numeric/sklearn-stopwords-v1.json +325 -0
- package/lib/core/numeric/svd.cjs +377 -0
- package/lib/core/numeric/tfidf.cjs +241 -0
- package/lib/core/offer-presenter.cjs +406 -0
- package/lib/core/opportunity-extractor.cjs +183 -0
- package/lib/core/opportunity-ops.cjs +1423 -0
- package/lib/core/orchestration-candidate-lift.cjs +284 -0
- package/lib/core/part8-egress-guard.cjs +536 -0
- package/lib/core/part8-egress-guard.test.cjs +194 -0
- package/lib/core/part8-egress-ontology.cjs +174 -0
- package/lib/core/part8-egress-ontology.test.cjs +133 -0
- package/lib/core/persona-ops.cjs +692 -0
- package/lib/core/persona-override.cjs +327 -0
- package/lib/core/persona-taxonomy.cjs +328 -0
- package/lib/core/pinecone-inference.cjs +176 -0
- package/lib/core/pitch-feedback-schemas.cjs +303 -0
- package/lib/core/planning/artifact-brain-packet.cjs +212 -0
- package/lib/core/planning/reconcile-runner.cjs +347 -0
- package/lib/core/platform-gates.cjs +120 -0
- package/lib/core/platform.cjs +257 -0
- package/lib/core/proactive-intelligence.cjs +528 -0
- package/lib/core/problem-type-router.cjs +315 -0
- package/lib/core/publish-needs-default-lane.cjs +110 -0
- package/lib/core/ralph-loop-gate.cjs +48 -0
- package/lib/core/reader/ab-harness.cjs +177 -0
- package/lib/core/reader/decide-projection-reader.cjs +358 -0
- package/lib/core/reasoning-ops.cjs +639 -0
- package/lib/core/recipe-maps.cjs +442 -0
- package/lib/core/recovery/case-file.cjs +358 -0
- package/lib/core/recovery/controller.cjs +1008 -0
- package/lib/core/recovery/dispatcher.cjs +533 -0
- package/lib/core/recovery/result-semantics.cjs +488 -0
- package/lib/core/recovery/stage-envelope.cjs +338 -0
- package/lib/core/refusal-messaging.cjs +569 -0
- package/lib/core/refusal-messaging.test.cjs +220 -0
- package/lib/core/repo-version.cjs +134 -0
- package/lib/core/research-cache.cjs +165 -0
- package/lib/core/research-cache.test.cjs +137 -0
- package/lib/core/research-context-extractor.cjs +283 -0
- package/lib/core/research-corpus.cjs +814 -0
- package/lib/core/research-corpus.test.cjs +280 -0
- package/lib/core/research-filing-selector.cjs +189 -0
- package/lib/core/resolve-active-room.cjs +602 -0
- package/lib/core/resolve-brain-key.cjs +273 -0
- package/lib/core/resolve-umbilical-target.cjs +288 -0
- package/lib/core/resolve-vercel-key.cjs +107 -0
- package/lib/core/resolve-vercel-key.test.cjs +137 -0
- package/lib/core/reverse-salient-persona-suffix.cjs +115 -0
- package/lib/core/room-auto-create.cjs +318 -0
- package/lib/core/room-auto-create.test.cjs +198 -0
- package/lib/core/room-chooser.cjs +244 -0
- package/lib/core/room-classifier-strict-mode.cjs +235 -0
- package/lib/core/room-db.cjs +348 -0
- package/lib/core/room-discard-cascade.cjs +238 -0
- package/lib/core/room-discard-cascade.test.cjs +135 -0
- package/lib/core/room-name-validator.cjs +132 -0
- package/lib/core/room-name-validator.test.cjs +156 -0
- package/lib/core/room-naming-selector.cjs +357 -0
- package/lib/core/room-naming-selector.test.cjs +277 -0
- package/lib/core/room-open.cjs +259 -0
- package/lib/core/room-ops-async.cjs +92 -0
- package/lib/core/room-ops-shared.cjs +64 -0
- package/lib/core/room-ops-sync.cjs +70 -0
- package/lib/core/room-ops.cjs +32 -0
- package/lib/core/room-receipt-emit.cjs +63 -0
- package/lib/core/room-root.cjs +93 -0
- package/lib/core/room-skeleton-scaffold.cjs +669 -0
- package/lib/core/room-skeleton-scaffold.test.cjs +296 -0
- package/lib/core/room-type-detector.cjs +386 -0
- package/lib/core/rs-backend-dispatch.cjs +64 -0
- package/lib/core/rs-brain-substrate-prompts.cjs +129 -0
- package/lib/core/rs-brain-substrate.cjs +570 -0
- package/lib/core/rs-breakthrough-scorer.cjs +255 -0
- package/lib/core/rs-canon-violations.cjs +82 -0
- package/lib/core/rs-chain-feeder.cjs +381 -0
- package/lib/core/rs-commercial-assessor.cjs +280 -0
- package/lib/core/rs-corpus-quality-gate.cjs +62 -0
- package/lib/core/rs-differential-scorer.cjs +629 -0
- package/lib/core/rs-domain-analyzer.cjs +385 -0
- package/lib/core/rs-egress-prompts.cjs +113 -0
- package/lib/core/rs-egress-telemetry.cjs +225 -0
- package/lib/core/rs-egress-violations.cjs +53 -0
- package/lib/core/rs-engine.cjs +597 -0
- package/lib/core/rs-expert-brain-projection.cjs +280 -0
- package/lib/core/rs-expert-mapper.cjs +467 -0
- package/lib/core/rs-fetcher-academic.cjs +697 -0
- package/lib/core/rs-fetcher-experts.cjs +314 -0
- package/lib/core/rs-fetcher-industry.cjs +731 -0
- package/lib/core/rs-fetcher-patents.cjs +564 -0
- package/lib/core/rs-innovation-classifier.cjs +194 -0
- package/lib/core/rs-math.cjs +458 -0
- package/lib/core/rs-mind-map.cjs +656 -0
- package/lib/core/rs-neo4j-writer.cjs +388 -0
- package/lib/core/rs-nl-to-query.cjs +435 -0
- package/lib/core/rs-pinecone-bridge.cjs +344 -0
- package/lib/core/rs-preprocessor.cjs +350 -0
- package/lib/core/rs-query-matrix.cjs +316 -0
- package/lib/core/rs-query-to-text.cjs +438 -0
- package/lib/core/rs-sqlite-mirror.cjs +495 -0
- package/lib/core/rs-thesis-generator.cjs +188 -0
- package/lib/core/rs_cache.py +635 -0
- package/lib/core/rs_corpus.py +562 -0
- package/lib/core/rs_corpus_exclude.py +62 -0
- package/lib/core/rs_hybrid.py +589 -0
- package/lib/core/rs_math.py +287 -0
- package/lib/core/rs_rooms.py +196 -0
- package/lib/core/salient-governance.cjs +315 -0
- package/lib/core/scheduled-scanner.cjs +658 -0
- package/lib/core/scratchpad-ops.cjs +316 -0
- package/lib/core/seam-liveness.cjs +191 -0
- package/lib/core/seam-liveness.test.cjs +177 -0
- package/lib/core/section-8-trace-schema.cjs +138 -0
- package/lib/core/section-registry.cjs +167 -0
- package/lib/core/security/agentshield-adapters.test.cjs +104 -0
- package/lib/core/security/agentshield-run.cjs +343 -0
- package/lib/core/security/agentshield-scanner.cjs +314 -0
- package/lib/core/security/agentshield-scanner.test.cjs +259 -0
- package/lib/core/sensors/hat-scoping-table.cjs +103 -0
- package/lib/core/sensors/sensor-circularity.cjs +278 -0
- package/lib/core/sensors/sensor-content-relevance.cjs +407 -0
- package/lib/core/sensors/sensor-diffusion-adoption.cjs +230 -0
- package/lib/core/sensors/sensor-eureka.cjs +208 -0
- package/lib/core/sensors/sensor-expert-skill.cjs +293 -0
- package/lib/core/sensors/sensor-external-fact.cjs +184 -0
- package/lib/core/sensors/sensor-gate-approach.cjs +104 -0
- package/lib/core/sensors/sensor-jtbd-reweight.cjs +141 -0
- package/lib/core/sensors/sensor-lagging-component.cjs +115 -0
- package/lib/core/sensors/sensor-memory-cortex.cjs +111 -0
- package/lib/core/sensors/sensor-methodology-decision.cjs +131 -0
- package/lib/core/sensors/sensor-opportunity-harvest.cjs +195 -0
- package/lib/core/sensors/sensor-perspective-lock.cjs +174 -0
- package/lib/core/sensors/sensor-priority.cjs +183 -0
- package/lib/core/sensors/sensor-recency.cjs +151 -0
- package/lib/core/sensors/sensor-roadmap-type.cjs +421 -0
- package/lib/core/sensors/sensor-room-pick.cjs +238 -0
- package/lib/core/sensors/sensor-show-share.cjs +215 -0
- package/lib/core/sensors/sensor-temporal-blindness.cjs +123 -0
- package/lib/core/sensors/sensor-types.cjs +294 -0
- package/lib/core/sensors/sensor-url-ingest.cjs +234 -0
- package/lib/core/session-binding.cjs +208 -0
- package/lib/core/session-presence.cjs +404 -0
- package/lib/core/session-register.cjs +138 -0
- package/lib/core/session-state.cjs +144 -0
- package/lib/core/shallow-doc-parser.cjs +353 -0
- package/lib/core/shallow-doc-parser.test.cjs +226 -0
- package/lib/core/skill-activation-router.cjs +284 -0
- package/lib/core/skillopt-schemas.cjs +509 -0
- package/lib/core/stale-copy-scanner.cjs +190 -0
- package/lib/core/stance-state.cjs +153 -0
- package/lib/core/state-aware-router.cjs +78 -0
- package/lib/core/state-ops.cjs +80 -0
- package/lib/core/state-version.cjs +350 -0
- package/lib/core/statusline-cache.cjs +266 -0
- package/lib/core/statusline-liveness-gate.cjs +105 -0
- package/lib/core/surface-fence.cjs +127 -0
- package/lib/core/synthesizers/comparison-matrix.cjs +82 -0
- package/lib/core/synthesizers/convergence-map.cjs +107 -0
- package/lib/core/synthesizers/domain-hierarchy.cjs +138 -0
- package/lib/core/synthesizers/source-comparison.cjs +56 -0
- package/lib/core/synthesizers/tension-map.cjs +119 -0
- package/lib/core/synthetic-expert-behavior-gate.cjs +99 -0
- package/lib/core/synthetic-expert-construction-gate.cjs +170 -0
- package/lib/core/telemetry/schema.cjs +168 -0
- package/lib/core/telemetry/schema.test.cjs +124 -0
- package/lib/core/telemetry/validator.cjs +200 -0
- package/lib/core/telemetry/validator.test.cjs +188 -0
- package/lib/core/telemetry/writer.cjs +141 -0
- package/lib/core/telemetry/writer.test.cjs +331 -0
- package/lib/core/temporal/date-sync-gate.cjs +256 -0
- package/lib/core/temporal/date-sync-gate.test.cjs +191 -0
- package/lib/core/temporal/dual-stamp.cjs +172 -0
- package/lib/core/temporal/dual-stamp.test.cjs +172 -0
- package/lib/core/temporal/point-in-time.cjs +76 -0
- package/lib/core/temporal/point-in-time.test.cjs +169 -0
- package/lib/core/temporal/recency-decay.cjs +119 -0
- package/lib/core/temporal/recency-decay.test.cjs +140 -0
- package/lib/core/temporal/reference-now.cjs +281 -0
- package/lib/core/temporal/reference-now.test.cjs +225 -0
- package/lib/core/temporal/resolve-relative-time.cjs +116 -0
- package/lib/core/temporal/resolve-relative-time.test.cjs +138 -0
- package/lib/core/temporal/supersession.cjs +250 -0
- package/lib/core/temporal/supersession.test.cjs +103 -0
- package/lib/core/terminal-capability.cjs +88 -0
- package/lib/core/token-estimator.cjs +348 -0
- package/lib/core/tone-filter.cjs +147 -0
- package/lib/core/trending-to-absurd/orchestrator.cjs +341 -0
- package/lib/core/trending-to-absurd/stage7-roadmap.cjs +317 -0
- package/lib/core/trending-to-absurd/variance.cjs +169 -0
- package/lib/core/unknowns/bandit.cjs +332 -0
- package/lib/core/unknowns/corpus-adapter.cjs +327 -0
- package/lib/core/unknowns/dsp.cjs +356 -0
- package/lib/core/unknowns/edge-writer.cjs +211 -0
- package/lib/core/unknowns/iface.cjs +212 -0
- package/lib/core/unknowns/orchestrator.cjs +375 -0
- package/lib/core/unknowns/pattern-miner.cjs +214 -0
- package/lib/core/unknowns/rumsfeld-matrix.cjs +155 -0
- package/lib/core/unknowns/verdict.cjs +254 -0
- package/lib/core/update-path.cjs +57 -0
- package/lib/core/url-ingest.cjs +568 -0
- package/lib/core/user-archetype.cjs +284 -0
- package/lib/core/user-md-ops.cjs +578 -0
- package/lib/core/venture-shape-nudge.cjs +178 -0
- package/lib/core/venture-shape-nudge.test.cjs +266 -0
- package/lib/core/verb-reach-affinity.cjs +299 -0
- package/lib/core/visual-ops.cjs +692 -0
- package/lib/core/voice-transition-detector.cjs +125 -0
- package/lib/core/watched-sources.cjs +237 -0
- package/lib/core/write-lock.cjs +149 -0
- package/lib/graph/canvas-graph.js +467 -0
- package/lib/graph/constellation-config.cjs +299 -0
- package/lib/graph/graph-detail-panel.js +165 -0
- package/lib/hmi/ROOM.md +47 -0
- package/lib/hmi/across-session-memory.cjs +669 -0
- package/lib/hmi/agentshield-drift-gate.cjs +142 -0
- package/lib/hmi/brain-review-packet.cjs +221 -0
- package/lib/hmi/cortex-reach-adapter.cjs +271 -0
- package/lib/hmi/cross-room-memory.cjs +618 -0
- package/lib/hmi/decoy-tier.cjs +395 -0
- package/lib/hmi/depth-state.cjs +164 -0
- package/lib/hmi/dial-label-composer.cjs +410 -0
- package/lib/hmi/dial-presenter.cjs +508 -0
- package/lib/hmi/dial-reach-orchestrator.cjs +377 -0
- package/lib/hmi/dial-selector.cjs +239 -0
- package/lib/hmi/f1-pick-capture-cli.cjs +131 -0
- package/lib/hmi/f3-depth-capture-cli.cjs +77 -0
- package/lib/hmi/f4-scope-capture-cli.cjs +86 -0
- package/lib/hmi/f8-action-capture-cli.cjs +145 -0
- package/lib/hmi/f8-capture.test.cjs +77 -0
- package/lib/hmi/f9-ordered-capture-cli.cjs +138 -0
- package/lib/hmi/harvest-scope-state.cjs +202 -0
- package/lib/hmi/hats-persona-cache.cjs +142 -0
- package/lib/hmi/jtbd-classifier.cjs +219 -0
- package/lib/hmi/jtbd-state.cjs +263 -0
- package/lib/hmi/jtbd-taxonomy.json +402 -0
- package/lib/hmi/mindrian-ascii-logo.cjs +96 -0
- package/lib/hmi/part8-egress-gate.cjs +149 -0
- package/lib/hmi/reach-component-map.json +47 -0
- package/lib/hmi/reach-relevance-gate.cjs +147 -0
- package/lib/hmi/selector-dispatcher.cjs +1201 -0
- package/lib/hmi/selector-dispatcher.test.cjs +141 -0
- package/lib/hmi/selector-telemetry.cjs +269 -0
- package/lib/hmi/shape-f0-renderer.cjs +145 -0
- package/lib/hmi/shape-f1-fallback.cjs +80 -0
- package/lib/hmi/shape-f1-renderer.cjs +219 -0
- package/lib/hmi/shape-f2-renderer.cjs +132 -0
- package/lib/hmi/shape-f3-parity.test.cjs +64 -0
- package/lib/hmi/shape-f3-renderer.cjs +66 -0
- package/lib/hmi/shape-f4-parity.test.cjs +72 -0
- package/lib/hmi/shape-f4-renderer.cjs +72 -0
- package/lib/hmi/shape-f5-renderer.cjs +155 -0
- package/lib/hmi/shape-f6-plan-review-renderer.cjs +324 -0
- package/lib/hmi/shape-f6-renderer.cjs +144 -0
- package/lib/hmi/shape-f7-breakthrough-renderer.cjs +234 -0
- package/lib/hmi/shape-f7-breakthrough-renderer.test.cjs +233 -0
- package/lib/hmi/shape-f8-renderer.cjs +150 -0
- package/lib/hmi/shape-f8-renderer.test.cjs +70 -0
- package/lib/hmi/shape-f9-renderer.cjs +187 -0
- package/lib/hmi/shape-f9-renderer.test.cjs +80 -0
- package/lib/hmi/shape-g-renderer.cjs +219 -0
- package/lib/hmi/shape-h-renderer.cjs +222 -0
- package/lib/hmi/tier-check.cjs +63 -0
- package/lib/hmi/turn-text.cjs +268 -0
- package/lib/hmi/voice-color-mark.cjs +318 -0
- package/lib/hmi/voice-style-log.cjs +276 -0
- package/lib/import/PRECONDITIONS.md +41 -0
- package/lib/import/branding.cjs +210 -0
- package/lib/import/branding.test.cjs +235 -0
- package/lib/import/classifications-sync.cjs +104 -0
- package/lib/import/classifications-sync.test.cjs +129 -0
- package/lib/import/enricher.cjs +296 -0
- package/lib/import/enricher.test.cjs +273 -0
- package/lib/import/integration.test.cjs +376 -0
- package/lib/import/manifest.cjs +129 -0
- package/lib/import/manifest.schema.json +185 -0
- package/lib/import/manifest.test.cjs +123 -0
- package/lib/import/meeting-detector.cjs +92 -0
- package/lib/import/meeting-detector.test.cjs +100 -0
- package/lib/import/person-detector.cjs +229 -0
- package/lib/import/person-detector.test.cjs +149 -0
- package/lib/import/report.cjs +186 -0
- package/lib/import/report.test.cjs +186 -0
- package/lib/import/room-md-scaffolder.cjs +49 -0
- package/lib/import/router.cjs +224 -0
- package/lib/import/router.test.cjs +356 -0
- package/lib/import/run-all-tests.cjs +36 -0
- package/lib/import/smoke-test.cjs +213 -0
- package/lib/import/smoke-test.test.cjs +148 -0
- package/lib/import/undo.test.cjs +199 -0
- package/lib/import/vault-scanner.cjs +105 -0
- package/lib/import/vault-scanner.test.cjs +67 -0
- package/lib/lens-engine/correlation-resolver.cjs +174 -0
- package/lib/lens-engine/source-lens-driver.cjs +736 -0
- package/lib/mcp/adapter-client.cjs +103 -0
- package/lib/mcp/app-html/dashboard.html +316 -0
- package/lib/mcp/app-html/graph.html +428 -0
- package/lib/mcp/app-html/mindrian-platform.html +1841 -0
- package/lib/mcp/app-html/wiki.html +383 -0
- package/lib/mcp/app-views.cjs +322 -0
- package/lib/mcp/brain-composition-census.cjs +184 -0
- package/lib/mcp/brain-router.cjs +530 -0
- package/lib/mcp/capability-registry.cjs +62 -0
- package/lib/mcp/contract-version.cjs +59 -0
- package/lib/mcp/daemon-lifecycle.cjs +324 -0
- package/lib/mcp/gate-dedup.cjs +120 -0
- package/lib/mcp/gate-ledger.cjs +145 -0
- package/lib/mcp/gate-render.cjs +483 -0
- package/lib/mcp/hook-adapter-audit.cjs +199 -0
- package/lib/mcp/larry-context.cjs +42 -0
- package/lib/mcp/mcp-first-flag.cjs +147 -0
- package/lib/mcp/no-instructions.test.cjs +270 -0
- package/lib/mcp/pipeline-state.cjs +424 -0
- package/lib/mcp/prompts.cjs +421 -0
- package/lib/mcp/register-core-tools.cjs +78 -0
- package/lib/mcp/resources.cjs +359 -0
- package/lib/mcp/runtime-instructions.cjs +38 -0
- package/lib/mcp/session-catchup.cjs +349 -0
- package/lib/mcp/session-registry.cjs +113 -0
- package/lib/mcp/session-room.cjs +124 -0
- package/lib/mcp/sse-event-bus.cjs +98 -0
- package/lib/mcp/stop-gate-handler.cjs +589 -0
- package/lib/mcp/surface-detect.cjs +186 -0
- package/lib/mcp/tool-router.cjs +2113 -0
- package/lib/mcp/tools/chain.cjs +674 -0
- package/lib/mcp/tools/claim.cjs +180 -0
- package/lib/mcp/tools/context.cjs +92 -0
- package/lib/mcp/tools/dual-path.cjs +79 -0
- package/lib/mcp/tools/gate.cjs +392 -0
- package/lib/mcp/tools/graph-reason.cjs +130 -0
- package/lib/mcp/tools/graph.cjs +321 -0
- package/lib/mcp/tools/identity.cjs +174 -0
- package/lib/mcp/tools/room.cjs +449 -0
- package/lib/mcp/tools/sensors.cjs +450 -0
- package/lib/mcp/tools/status.cjs +183 -0
- package/lib/mcp/tools/stop-gate.cjs +103 -0
- package/lib/mcp/tools/views.cjs +382 -0
- package/lib/mcp/tree-watcher.cjs +199 -0
- package/lib/memory/.mindrian/auto-explore-mat-test-001.json +11 -0
- package/lib/memory/aaak-compress.cjs +403 -0
- package/lib/memory/aaak-compress.test.cjs +288 -0
- package/lib/memory/async-artifact-auto-commit.test.cjs +223 -0
- package/lib/memory/bearer-token.test.cjs +315 -0
- package/lib/memory/body-shape-coverage.test.cjs +268 -0
- package/lib/memory/brain-cache-lru.test.cjs +259 -0
- package/lib/memory/brain-client-query-shape.test.cjs +160 -0
- package/lib/memory/brain-cypher-chain-slice.test.cjs +368 -0
- package/lib/memory/brain-derivation-graceful-degradation.test.cjs +1019 -0
- package/lib/memory/brain-derivation-queue.test.cjs +553 -0
- package/lib/memory/brain-derivation.test.cjs +639 -0
- package/lib/memory/brain-derive-command.test.cjs +534 -0
- package/lib/memory/brain-derive-curation.test.cjs +243 -0
- package/lib/memory/brain-md-invariants-validator.test.cjs +704 -0
- package/lib/memory/brain-md-schema.test.cjs +467 -0
- package/lib/memory/brain-md-staleness.test.cjs +525 -0
- package/lib/memory/brain-server-resolution.test.cjs +314 -0
- package/lib/memory/brain-suggestion-template.test.cjs +200 -0
- package/lib/memory/chain-recommender-canonical.test.cjs +208 -0
- package/lib/memory/chain-recommender.test.cjs +233 -0
- package/lib/memory/chat-context.test.cjs +128 -0
- package/lib/memory/command-registry.test.cjs +220 -0
- package/lib/memory/connector-registry.test.cjs +94 -0
- package/lib/memory/correlation-label-index.test.cjs +119 -0
- package/lib/memory/correlation.test.cjs +253 -0
- package/lib/memory/cross-room-aggregator.test.cjs +909 -0
- package/lib/memory/curation-132-03-dedup-held-rename.test.cjs +251 -0
- package/lib/memory/curation-132-05-pseudonymize.test.cjs +205 -0
- package/lib/memory/curation-batch.test.cjs +213 -0
- package/lib/memory/dashboard-server.test.cjs +256 -0
- package/lib/memory/debouncer-drain-at-prompt.test.cjs +389 -0
- package/lib/memory/decision-capture.test.cjs +632 -0
- package/lib/memory/decision-capture.worker.cjs +70 -0
- package/lib/memory/doctor-deprecation-surface.test.cjs +188 -0
- package/lib/memory/dual-graph-health.test.cjs +485 -0
- package/lib/memory/explain-decision-command.test.cjs +521 -0
- package/lib/memory/explain-decision-footer.test.cjs +316 -0
- package/lib/memory/explored-materials-store.cjs +392 -0
- package/lib/memory/f-selector-ranker.test.cjs +593 -0
- package/lib/memory/feynman-minto-guardian.test.cjs +863 -0
- package/lib/memory/feynman-minto-invariants.test.cjs +554 -0
- package/lib/memory/feynman-prompts-drift.test.cjs +144 -0
- package/lib/memory/feynman-prompts.cjs +151 -0
- package/lib/memory/feynman-prompts.test.cjs +96 -0
- package/lib/memory/first-touch-version.test.cjs +198 -0
- package/lib/memory/folder-memory-quadruple.test.cjs +548 -0
- package/lib/memory/folder-memory.test.cjs +503 -0
- package/lib/memory/framework-chain-composer.test.cjs +549 -0
- package/lib/memory/frontmatter-schema-validator.test.cjs +344 -0
- package/lib/memory/guardian-onstop-reaches-user.test.cjs +396 -0
- package/lib/memory/heal-command.test.cjs +604 -0
- package/lib/memory/help-coverage.test.cjs +108 -0
- package/lib/memory/help-renderer.test.cjs +145 -0
- package/lib/memory/huji-run-one-async-parity.test.cjs +242 -0
- package/lib/memory/hypergraph-event-schema.test.cjs +116 -0
- package/lib/memory/index-artifact-transaction.test.cjs +333 -0
- package/lib/memory/lazygraph-rs-discoveries-view.test.cjs +122 -0
- package/lib/memory/local-chain-recommender.test.cjs +172 -0
- package/lib/memory/mcp-input-validation.test.cjs +240 -0
- package/lib/memory/mcp-server-brain-deps.test.cjs +270 -0
- package/lib/memory/mcp-stack-fallback.test.cjs +433 -0
- package/lib/memory/minto-debounce-consumer-census.test.cjs +567 -0
- package/lib/memory/minto-debouncer.test.cjs +407 -0
- package/lib/memory/minto-debouncer.worker.cjs +46 -0
- package/lib/memory/minto-migration-v88.test.cjs +265 -0
- package/lib/memory/minto-schema-v88.test.cjs +390 -0
- package/lib/memory/mos-status-renderer.test.cjs +631 -0
- package/lib/memory/narrative-schema.cjs +376 -0
- package/lib/memory/narrative-schema.test.cjs +209 -0
- package/lib/memory/nav-dial.test.cjs +414 -0
- package/lib/memory/navigation-engine-core.test.cjs +712 -0
- package/lib/memory/navigation-engine-offer.test.cjs +404 -0
- package/lib/memory/navigation-hook-resolver.test.cjs +177 -0
- package/lib/memory/navigation-invariants.test.cjs +483 -0
- package/lib/memory/navigation-projections.test.cjs +241 -0
- package/lib/memory/navigation-write-edge.test.cjs +206 -0
- package/lib/memory/offer-closer.test.cjs +258 -0
- package/lib/memory/offer-presenter.test.cjs +554 -0
- package/lib/memory/on-stop-snapshot.test.cjs +404 -0
- package/lib/memory/orchestration-projection.test.cjs +706 -0
- package/lib/memory/packet-chain-hint.test.cjs +407 -0
- package/lib/memory/packet-schema-validation.test.cjs +317 -0
- package/lib/memory/palette-consistency.test.cjs +127 -0
- package/lib/memory/pending-tension-store.cjs +453 -0
- package/lib/memory/per-command-jtbd-derivation.test.cjs +130 -0
- package/lib/memory/per-command-teaching.test.cjs +110 -0
- package/lib/memory/post-compact-reinjection.test.cjs +854 -0
- package/lib/memory/post-write-triple.test.cjs +317 -0
- package/lib/memory/pre-compact-snapshot.test.cjs +495 -0
- package/lib/memory/precommit-real-commit.test.cjs +301 -0
- package/lib/memory/problem-type-router.test.cjs +656 -0
- package/lib/memory/query-efficiency-telemetry.test.cjs +612 -0
- package/lib/memory/recompile-room-references.test.cjs +392 -0
- package/lib/memory/recompile-room-references.worker.cjs +42 -0
- package/lib/memory/record-decision-dual-write.test.cjs +454 -0
- package/lib/memory/render-v2-disposition.test.cjs +199 -0
- package/lib/memory/room-classifier-strict-mode.test.cjs +430 -0
- package/lib/memory/room-minto-hook.test.cjs +398 -0
- package/lib/memory/rs-discovery-engine.test.cjs +323 -0
- package/lib/memory/run-feynman-tests.cjs +2101 -0
- package/lib/memory/security-trifecta.test.cjs +329 -0
- package/lib/memory/selector-alias-map.test.cjs +172 -0
- package/lib/memory/selector-decisions.test.cjs +417 -0
- package/lib/memory/selector-miss.test.cjs +290 -0
- package/lib/memory/session-start-brain-staleness.test.cjs +363 -0
- package/lib/memory/session-start-triple-injection.test.cjs +514 -0
- package/lib/memory/sessionstart-banner-formatter.cjs +318 -0
- package/lib/memory/sessionstart-coordinator.test.cjs +446 -0
- package/lib/memory/sessionstart-minto-banner.test.cjs +373 -0
- package/lib/memory/skill-activation-router.test.cjs +465 -0
- package/lib/memory/skill-vs-code-drift.test.cjs +257 -0
- package/lib/memory/soft-alias.test.cjs +144 -0
- package/lib/memory/spine-events-correlation.test.cjs +198 -0
- package/lib/memory/stale-copy-scanner.test.cjs +294 -0
- package/lib/memory/stamp-artifact-write.test.cjs +304 -0
- package/lib/memory/state-aware-router.test.cjs +90 -0
- package/lib/memory/statusline-active-room-write.test.cjs +245 -0
- package/lib/memory/statusline-active-room.test.cjs +315 -0
- package/lib/memory/statusline-minto-segment.test.cjs +292 -0
- package/lib/memory/statusline-two-row.test.cjs +338 -0
- package/lib/memory/suggest-next-workflow.test.cjs +176 -0
- package/lib/memory/sync-async-entry-points.test.cjs +204 -0
- package/lib/memory/terminal-capability.test.cjs +155 -0
- package/lib/memory/test-bridge-writer-enhanced.cjs +452 -0
- package/lib/memory/test-rs-brain-substrate-shape.cjs +529 -0
- package/lib/memory/test-rs-brain-substrate.cjs +636 -0
- package/lib/memory/test-rs-breakthrough-scorer.cjs +375 -0
- package/lib/memory/test-rs-canon-violations.cjs +218 -0
- package/lib/memory/test-rs-chain-feeder-core.cjs +344 -0
- package/lib/memory/test-rs-chain-feeder-skill-spawn.cjs +297 -0
- package/lib/memory/test-rs-commercial-assessor.cjs +385 -0
- package/lib/memory/test-rs-differential-scorer.cjs +480 -0
- package/lib/memory/test-rs-discovery-engine.cjs +648 -0
- package/lib/memory/test-rs-domain-analyzer.cjs +492 -0
- package/lib/memory/test-rs-egress-primitives.cjs +420 -0
- package/lib/memory/test-rs-expert-mapper.cjs +547 -0
- package/lib/memory/test-rs-explain-command.cjs +443 -0
- package/lib/memory/test-rs-fetcher-academic.cjs +848 -0
- package/lib/memory/test-rs-fetcher-experts.cjs +496 -0
- package/lib/memory/test-rs-fetcher-industry.cjs +702 -0
- package/lib/memory/test-rs-fetcher-patents.cjs +674 -0
- package/lib/memory/test-rs-innovation-classifier.cjs +301 -0
- package/lib/memory/test-rs-mind-map.cjs +646 -0
- package/lib/memory/test-rs-neo4j-writer.cjs +518 -0
- package/lib/memory/test-rs-nl-to-query.cjs +449 -0
- package/lib/memory/test-rs-pinecone-bridge.cjs +282 -0
- package/lib/memory/test-rs-preprocessor.cjs +433 -0
- package/lib/memory/test-rs-query-matrix.cjs +391 -0
- package/lib/memory/test-rs-query-to-text.cjs +551 -0
- package/lib/memory/test-rs-sqlite-mirror.cjs +649 -0
- package/lib/memory/test-rs-thesis-generator.cjs +360 -0
- package/lib/memory/triple-context-formatter.cjs +473 -0
- package/lib/memory/triple-context-formatter.test.cjs +442 -0
- package/lib/memory/user-md-persona.test.cjs +565 -0
- package/lib/memory/userpromptsubmit-integration.test.cjs +787 -0
- package/lib/memory/validators/README.md +157 -0
- package/lib/memory/validators/brain-md-invariants.cjs +475 -0
- package/lib/memory/validators/brain-substrate-invariants.cjs +285 -0
- package/lib/memory/validators/external-academic-invariants.cjs +249 -0
- package/lib/memory/validators/external-industry-invariants.cjs +271 -0
- package/lib/memory/validators/external-patents-invariants.cjs +266 -0
- package/lib/memory/validators/minto-invariants.cjs +62 -0
- package/lib/memory/validators/navigation-invariants.cjs +340 -0
- package/lib/memory/validators/queue-health.cjs +95 -0
- package/lib/memory/validators/snapshot-integrity.cjs +129 -0
- package/lib/memory/validators/stale-lifecycle.cjs +116 -0
- package/lib/memory/vault-section-minto-generator-atomic.test.cjs +556 -0
- package/lib/memory/vault-section-minto-generator-atomic.worker.cjs +73 -0
- package/lib/memory/workflow-layer-e2e.test.cjs +262 -0
- package/lib/memory/write-lock-atomic.test.cjs +137 -0
- package/lib/memory/write-lock-atomic.worker.cjs +55 -0
- package/lib/parity/check-parity.cjs +83 -0
- package/lib/presentation/presentation-server.cjs +101 -0
- package/lib/presentation/presentation-watcher.cjs +123 -0
- package/lib/quickview/hub-server.cjs +719 -0
- package/lib/quickview/server.cjs +533 -0
- package/lib/render/JTBD-PALETTES.md +145 -0
- package/lib/render/ROOM.md +111 -0
- package/lib/render/render-v2.cjs +532 -0
- package/lib/render/render-v2.test.cjs +267 -0
- package/lib/render/render.cjs +65 -0
- package/lib/sessionstart/budget-compressor.cjs +130 -0
- package/lib/sessionstart/contributor-interface.cjs +134 -0
- package/lib/sessionstart/contributor-isolator.cjs +128 -0
- package/lib/sessionstart/precedence-ladder.cjs +47 -0
- package/lib/state/ROOM.md +46 -0
- package/lib/state/state-md-parser.cjs +215 -0
- package/lib/statusline/ROOM.md +38 -0
- package/lib/statusline/banner-suppression.cjs +50 -0
- package/lib/statusline/cockpit-renderer.cjs +443 -0
- package/lib/statusline/cockpit-signals.cjs +312 -0
- package/lib/statusline/cockpit-telemetry.cjs +152 -0
- package/lib/statusline/ctx-window.cjs +135 -0
- package/lib/statusline/governing-thought-truncator.cjs +45 -0
- package/lib/statusline/next-move-cache.cjs +235 -0
- package/lib/statusline/ratification-next.cjs +164 -0
- package/lib/statusline/room-health-cache.cjs +200 -0
- package/lib/statusline/surface-detect.cjs +85 -0
- package/lib/statusline/two-row-renderer.cjs +207 -0
- package/lib/statusline/version-resolver.cjs +81 -0
- package/lib/ui/design-system.cjs +19 -0
- package/lib/update-bootstrap.sh.template +145 -0
- package/lib/vault/frontmatter-schema.cjs +297 -0
- package/lib/vault/room-scanner.cjs +365 -0
- package/lib/vault/wikilink-builder.cjs +231 -0
- package/lib/vault/wikilink-builder.test.cjs +182 -0
- package/lib/wiki/briefing.cjs +193 -0
- package/lib/wiki/editor-dist/wiki-editor.css +1 -0
- package/lib/wiki/editor-dist/wiki-editor.js +1327 -0
- package/lib/wiki/graph-links.cjs +281 -0
- package/lib/wiki/page-renderer.cjs +229 -0
- package/lib/wiki/room-home.cjs +353 -0
- package/lib/wiki/wiki-chat.cjs +81 -0
- package/lib/wiki/wiki-export.cjs +364 -0
- package/lib/wiki/wiki-layout.cjs +1143 -0
- package/lib/wiki/wiki-search.cjs +142 -0
- package/lib/wiki/wiki-server.cjs +748 -0
- package/lib/wiki/wiki-watcher.cjs +105 -0
- package/lib/workflow/ROOM.md +47 -0
- package/lib/workflow/chain-source.cjs +179 -0
- package/lib/workflow/command-resolver.cjs +155 -0
- package/lib/workflow/command-resolver.test.cjs +235 -0
- package/lib/workflow/cross-room-umbilical-closer.cjs +364 -0
- package/lib/workflow/dial-close-reach.cjs +405 -0
- package/lib/workflow/f-selector-ranker.cjs +1148 -0
- package/lib/workflow/f1-pick-consumer.cjs +232 -0
- package/lib/workflow/f3-depth-consumer.cjs +98 -0
- package/lib/workflow/f4-scope-consumer.cjs +112 -0
- package/lib/workflow/f8-consumer.test.cjs +82 -0
- package/lib/workflow/f8-fanout-consumer.cjs +208 -0
- package/lib/workflow/f9-consumer.test.cjs +71 -0
- package/lib/workflow/f9-ordered-consumer.cjs +199 -0
- package/lib/workflow/local-chain-recommender.cjs +330 -0
- package/lib/workflow/memory-cascade-f9-adapter.cjs +273 -0
- package/lib/workflow/memory-governance-closer.cjs +384 -0
- package/lib/workflow/offer-closer.cjs +407 -0
- package/lib/workflow/reach-hedge-ranker.cjs +809 -0
- package/lib/workflow/reach-reject-reader.cjs +372 -0
- package/lib/workflow/reconcile-f9-adapter.cjs +299 -0
- package/lib/workflow/selector-decisions.cjs +478 -0
- package/lib/workflow/session-binding-consumer.cjs +249 -0
- package/npm-shrinkwrap.json +1544 -0
- package/output-styles/destijl.md +67 -0
- package/package.json +22 -4
- package/pipelines/PWS_grading/01-deep-grade.md +48 -0
- package/pipelines/PWS_grading/02-mullins.md +50 -0
- package/pipelines/PWS_grading/03-build-thesis.md +60 -0
- package/pipelines/PWS_grading/04-structure-argument.md +77 -0
- package/pipelines/PWS_grading/CHAIN.md +79 -0
- package/pipelines/analogy/01-decompose.md +80 -0
- package/pipelines/analogy/02-abstract.md +87 -0
- package/pipelines/analogy/03-search.md +135 -0
- package/pipelines/analogy/04-transfer.md +101 -0
- package/pipelines/analogy/05-validate.md +106 -0
- package/pipelines/analogy/CHAIN.md +73 -0
- package/pipelines/discovery/01-explore-domains.md +44 -0
- package/pipelines/discovery/02-think-hats.md +50 -0
- package/pipelines/discovery/03-analyze-needs.md +54 -0
- package/pipelines/discovery/CHAIN.md +48 -0
- package/pipelines/thesis/01-structure-argument.md +45 -0
- package/pipelines/thesis/02-challenge-assumptions.md +48 -0
- package/pipelines/thesis/03-build-thesis.md +54 -0
- package/pipelines/thesis/CHAIN.md +48 -0
- package/references/brain/causal-directives.md +91 -0
- package/references/brain/causal-enrichment.cypher +165 -0
- package/references/brain/command-triggers-schema.md +15 -0
- package/references/brain/graph-architecture.md +317 -0
- package/references/brain/query-patterns.md +504 -0
- package/references/brain/room-hierarchy-schema.md +218 -0
- package/references/brain/schema.md +86 -0
- package/references/capability-radar/capabilities-index.md +269 -0
- package/references/capability-radar/changelog-cache.md +95 -0
- package/references/causal/causal-schema.md +103 -0
- package/references/design/email-template-standard.md +169 -0
- package/references/design/graph-visualization-standard.md +178 -0
- package/references/design/newsletter-email-template.html +307 -0
- package/references/document-generation.md +179 -0
- package/references/hsi/HSI-TOOLS-REFERENCE.md +222 -0
- package/references/import-config.md +141 -0
- package/references/integrations/detection-patterns.md +101 -0
- package/references/meeting/artifact-template.md +377 -0
- package/references/meeting/cross-meeting-intelligence.md +216 -0
- package/references/meeting/cross-relationship-patterns.md +202 -0
- package/references/meeting/extraction-perspectives.md +225 -0
- package/references/meeting/filing-protocol.md +222 -0
- package/references/meeting/knowledge-typing.md +242 -0
- package/references/meeting/live-join-interface.md +244 -0
- package/references/meeting/section-mapping.md +192 -0
- package/references/meeting/segment-classification.md +258 -0
- package/references/meeting/speaker-profile-template.md +219 -0
- package/references/meeting/summary-template.md +348 -0
- package/references/meeting/transcript-patterns.md +226 -0
- package/references/methodology/analyze-needs.md +135 -0
- package/references/methodology/analyze-systems.md +121 -0
- package/references/methodology/analyze-timing.md +149 -0
- package/references/methodology/beautiful-question.md +109 -0
- package/references/methodology/build-knowledge.md +161 -0
- package/references/methodology/build-thesis-scored.md +60 -0
- package/references/methodology/build-thesis.md +237 -0
- package/references/methodology/causal-loop-diagrams.md +65 -0
- package/references/methodology/challenge-assumptions.md +127 -0
- package/references/methodology/diagnose.md +169 -0
- package/references/methodology/dominant-designs.md +212 -0
- package/references/methodology/explore-domains.md +147 -0
- package/references/methodology/explore-futures.md +163 -0
- package/references/methodology/explore-trends.md +129 -0
- package/references/methodology/find-bottlenecks.md +131 -0
- package/references/methodology/grade.md +211 -0
- package/references/methodology/huji-stage-a-intake.md +246 -0
- package/references/methodology/index.md +34 -0
- package/references/methodology/leadership.md +200 -0
- package/references/methodology/lean-canvas.md +116 -0
- package/references/methodology/leverage-scan-signatures.md +83 -0
- package/references/methodology/macro-trends.md +192 -0
- package/references/methodology/map-unknowns.md +137 -0
- package/references/methodology/mullins-7-domains.md +104 -0
- package/references/methodology/problem-types.md +65 -0
- package/references/methodology/root-cause.md +178 -0
- package/references/methodology/rubric-huji.md +160 -0
- package/references/methodology/sapphire-encoding.md +355 -0
- package/references/methodology/scenario-plan.md +190 -0
- package/references/methodology/score-innovation.md +154 -0
- package/references/methodology/skillopt-judge-rubric.md +48 -0
- package/references/methodology/skillopt-queries-rubric.md +60 -0
- package/references/methodology/skillopt-refute-rubric.md +59 -0
- package/references/methodology/skillopt-review-rubric.md +72 -0
- package/references/methodology/skillopt-revise-rubric.md +48 -0
- package/references/methodology/structure-argument.md +158 -0
- package/references/methodology/systems-thinking.md +231 -0
- package/references/methodology/think-hats.md +147 -0
- package/references/methodology/triz-matrix.json +751 -0
- package/references/methodology/triz-principles.md +501 -0
- package/references/methodology/user-needs.md +199 -0
- package/references/methodology/validate.md +163 -0
- package/references/methodology/value-proposition.md +244 -0
- package/references/opportunities/funding-lifecycle.md +103 -0
- package/references/opportunities/grant-api-patterns.md +99 -0
- package/references/opportunities/opportunity-template.md +84 -0
- package/references/opportunities/tnufa-rubric.md +81 -0
- package/references/personality/assessment-philosophy.md +72 -0
- package/references/personality/lexicon.md +100 -0
- package/references/personality/persona-chains.md +56 -0
- package/references/personality/pws-lexicon-full.md +499 -0
- package/references/personality/voice-dna.md +156 -0
- package/references/personas/hat-perspectives.md +76 -0
- package/references/personas/persona-template.md +63 -0
- package/references/pipeline/act-output-contract.md +88 -0
- package/references/pipeline/chains-index.md +39 -0
- package/references/pws-profile-generation.md +79 -0
- package/references/reasoning/reasoning-schema.md +143 -0
- package/references/reasoning/reasoning-template.md +68 -0
- package/references/reasoning/run-template.md +38 -0
- package/references/research/RESEARCH_14_CLAUDE_CODE_SOURCE_ARCHITECTURE.md +209 -0
- package/references/research/RESEARCH_15_V1.8_OPTIMIZATION_JTBD.md +375 -0
- package/references/research/RESEARCH_16_NATIVE_FIRST_PLUGIN_ARCHITECTURE.md +575 -0
- package/references/research/RESEARCH_17_MCP_UI_FRAMEWORKS.md +272 -0
- package/references/security/agentshield-baseline.json +8 -0
- package/references/security/cve-db.json +220 -0
- package/references/taxonomy/TAXONOMY.md +192 -0
- package/references/templates/MINTO.md +36 -0
- package/references/user-research/2026-04-05-leah-lawrence-session.md +202 -0
- package/references/vault-kit/README.md +35 -0
- package/references/vault-kit/app.json +12 -0
- package/references/vault-kit/appearance.json +12 -0
- package/references/vault-kit/graph.json +35 -0
- package/references/vault-kit/snippets/mindrian-destijl.css +297 -0
- package/references/vault-kit/templates/new-artifact.md +37 -0
- package/references/vault-kit/templates/new-meeting-note.md +35 -0
- package/references/vault-kit/templates/new-team-profile.md +29 -0
- package/references/vault-kit/templates/new-xref.md +35 -0
- package/references/visual/ROOM.md +55 -0
- package/references/visual/palette.json +54 -0
- package/references/visual/symbol-system.md +151 -0
- package/scripts/189-plurai-gate-check.cjs +88 -0
- package/scripts/198-plurai-gate-check.cjs +118 -0
- package/scripts/198-rollback-rehearsal.cjs +233 -0
- package/scripts/83-scope-injection.test.cjs +648 -0
- package/scripts/PYTHON_GATES.md +78 -0
- package/scripts/__pycache__/compute-hsi.cpython-312.pyc +0 -0
- package/scripts/__pycache__/detect-reverse-salients.cpython-312.pyc +0 -0
- package/scripts/__pycache__/rs-engine.cpython-312.pyc +0 -0
- package/scripts/act-command.cjs +518 -0
- package/scripts/admin-brain-write.cjs +442 -0
- package/scripts/admin-command-gate.cjs +178 -0
- package/scripts/agentshield-scan-cli.cjs +226 -0
- package/scripts/agentshield-sessionstart-scan.cjs +282 -0
- package/scripts/analogy-fitness-report.cjs +266 -0
- package/scripts/analyze-room +1079 -0
- package/scripts/apply-teaching-content.cjs +298 -0
- package/scripts/async-artifact-auto-commit.cjs +485 -0
- package/scripts/audit-active-phase-resolver.cjs +160 -0
- package/scripts/audit-body-shape-coverage.cjs +213 -0
- package/scripts/auto-explore-drain.cjs +274 -0
- package/scripts/auto-explore-fingerprint.cjs +354 -0
- package/scripts/auto-explore-fire.cjs +359 -0
- package/scripts/backfill-correlation-id.cjs +425 -0
- package/scripts/backfill-hitl-shape.cjs +276 -0
- package/scripts/backfill-hitl-shape.test.cjs +307 -0
- package/scripts/backup-modifications +68 -0
- package/scripts/banner +182 -0
- package/scripts/brain-derivation-drain.cjs +291 -0
- package/scripts/brain-derive-command.cjs +846 -0
- package/scripts/brain-normalize-books.cypher +505 -0
- package/scripts/brain-normalize-final.cypher +1355 -0
- package/scripts/brain-normalize-problemtype.cypher +475 -0
- package/scripts/brain-normalize-supplement.cypher +449 -0
- package/scripts/brain-normalize-targeted.cypher +344 -0
- package/scripts/brain-normalize-v194.cypher +61 -0
- package/scripts/brain-normalize.cypher +530 -0
- package/scripts/brain-response-sanitize-hook.cjs +165 -0
- package/scripts/build-brain-census.cjs +957 -0
- package/scripts/build-brain-packet-schema.cjs +225 -0
- package/scripts/build-command-registry.cjs +587 -0
- package/scripts/build-connector-registry.cjs +1331 -0
- package/scripts/build-corpus-stats.cjs +408 -0
- package/scripts/build-dist-bundles.cjs +495 -0
- package/scripts/build-ecosystem-graph.cjs +490 -0
- package/scripts/build-graph +1462 -0
- package/scripts/build-graph-from-sqlite.cjs +97 -0
- package/scripts/build-harness-manifest.cjs +1090 -0
- package/scripts/build-jtbd-nudges +205 -0
- package/scripts/build-new-surface.cjs +805 -0
- package/scripts/build-orchestration-projection.cjs +1221 -0
- package/scripts/build-render-coverage.cjs +637 -0
- package/scripts/build-skill-mirrors.cjs +681 -0
- package/scripts/cache-hitrate-report.cjs +216 -0
- package/scripts/causal-to-graph.cjs +163 -0
- package/scripts/check-abstraction-fixture-neutral.cjs +160 -0
- package/scripts/check-admin-identity.cjs +212 -0
- package/scripts/check-brain-tool-liveness.cjs +502 -0
- package/scripts/check-card-fire.cjs +1809 -0
- package/scripts/check-cirs-declaration.cjs +279 -0
- package/scripts/check-deck-design.cjs +104 -0
- package/scripts/check-dual-graph-health.cjs +645 -0
- package/scripts/check-first-touch-drift.cjs +220 -0
- package/scripts/check-flagship-floor.cjs +391 -0
- package/scripts/check-framework-vocabulary-drift.cjs +563 -0
- package/scripts/check-gate-seam.cjs +178 -0
- package/scripts/check-graph-derive-health.cjs +208 -0
- package/scripts/check-graph-export-typemap.cjs +118 -0
- package/scripts/check-help-coverage.cjs +290 -0
- package/scripts/check-hitl-stages.cjs +219 -0
- package/scripts/check-hook-schema-compatibility.cjs +231 -0
- package/scripts/check-hsi-deps +42 -0
- package/scripts/check-kuzu-reintroduction.cjs +251 -0
- package/scripts/check-onboard +43 -0
- package/scripts/check-onboard-statusline.cjs +363 -0
- package/scripts/check-palette-consistency.cjs +106 -0
- package/scripts/check-pending-ambiguous.cjs +244 -0
- package/scripts/check-pending-breakthrough.cjs +189 -0
- package/scripts/check-pending-breakthrough.test.cjs +166 -0
- package/scripts/check-pending-naming-decision.cjs +97 -0
- package/scripts/check-plugin-path-anchoring.cjs +830 -0
- package/scripts/check-publish-needs.cjs +126 -0
- package/scripts/check-registry-drift.cjs +206 -0
- package/scripts/check-release-payload-ceiling.cjs +269 -0
- package/scripts/check-render-coverage.cjs +558 -0
- package/scripts/check-research-isomorphism.cjs +446 -0
- package/scripts/check-reward-before-investment.cjs +261 -0
- package/scripts/check-room-blueprints.cjs +213 -0
- package/scripts/check-schema-aliases.cjs +532 -0
- package/scripts/check-shape-declaration.cjs +958 -0
- package/scripts/check-shape-declaration.test.cjs +185 -0
- package/scripts/check-skill-spec.cjs +312 -0
- package/scripts/check-skill-vs-code-drift.cjs +178 -0
- package/scripts/check-substrate.cjs +513 -0
- package/scripts/check-tool-honesty.cjs +1601 -0
- package/scripts/check-update +60 -0
- package/scripts/check-version-and-sha.cjs +366 -0
- package/scripts/check-voice-style.cjs +184 -0
- package/scripts/check-worktree-hygiene.cjs +530 -0
- package/scripts/classify-insight +59 -0
- package/scripts/coherence-smoke-test.cjs +489 -0
- package/scripts/compute-bayesian-surprise.py +175 -0
- package/scripts/compute-blindspot-mass.py +209 -0
- package/scripts/compute-disruption-index.py +211 -0
- package/scripts/compute-element-novelty.py +254 -0
- package/scripts/compute-external-whitespace.py +563 -0
- package/scripts/compute-hsi.py +947 -0
- package/scripts/compute-meetings-intelligence +716 -0
- package/scripts/compute-opportunity-state +17 -0
- package/scripts/compute-state +393 -0
- package/scripts/compute-team +1112 -0
- package/scripts/compute-whitespace-embeddings.py +464 -0
- package/scripts/compute-whitespace-gaps.py +562 -0
- package/scripts/compute_topic_forest.py +684 -0
- package/scripts/consolidate-pinecone.py +265 -0
- package/scripts/context-monitor +943 -0
- package/scripts/create-speaker-profile +311 -0
- package/scripts/cross-room-detect.cjs +195 -0
- package/scripts/csv-to-idea-graph.cjs +755 -0
- package/scripts/ctxl-eval.cjs +846 -0
- package/scripts/curation-132-03-dedup-held-rename.cjs +395 -0
- package/scripts/curation-132-05-pseudonymize.cjs +189 -0
- package/scripts/derive-verb-reach-affinity.cjs +507 -0
- package/scripts/detect-reverse-salients.py +242 -0
- package/scripts/diagnostics-command.cjs +345 -0
- package/scripts/dial-memory-refresh-command.cjs +147 -0
- package/scripts/discover-analogy-whitespace.py +433 -0
- package/scripts/discover-hsi-whitespace.py +352 -0
- package/scripts/discover-rs-whitespace.py +327 -0
- package/scripts/discovery-cycle.cjs +625 -0
- package/scripts/disposition-render-v2.cjs +229 -0
- package/scripts/doctor-preflight-format.cjs +101 -0
- package/scripts/doctor.cjs +4327 -0
- package/scripts/dogfood-bridge/append-to-queue.sh +59 -0
- package/scripts/dogfood-bridge/path-filter.cjs +86 -0
- package/scripts/dogfood-derive.cjs +158 -0
- package/scripts/dogfood-emit.cjs +126 -0
- package/scripts/empathy-observation-emit.cjs +108 -0
- package/scripts/enrichment-queue-append.cjs +157 -0
- package/scripts/ensure-brain-baseline.cjs +117 -0
- package/scripts/entity-extract.cjs +1135 -0
- package/scripts/eureka-command.cjs +687 -0
- package/scripts/eureka-critic-run.cjs +662 -0
- package/scripts/eureka-portfolio-report.cjs +2038 -0
- package/scripts/eureka-room-report.cjs +419 -0
- package/scripts/explain-decision-command.cjs +499 -0
- package/scripts/export-pinecone-embeddings.cjs +516 -0
- package/scripts/extract-room-intelligence.cjs +714 -0
- package/scripts/fetch-brain-baseline.cjs +190 -0
- package/scripts/fetch-brain-baseline.py +360 -0
- package/scripts/feynman-minto-guardian.cjs +672 -0
- package/scripts/feynman-timeline-refresh-command.cjs +270 -0
- package/scripts/file-asset +253 -0
- package/scripts/fire-sensor-event.cjs +83 -0
- package/scripts/first-install-router.cjs +968 -0
- package/scripts/frontmatter-schema-validator.cjs +339 -0
- package/scripts/fts-index-drain.cjs +221 -0
- package/scripts/generate-chat-embed.cjs +179 -0
- package/scripts/generate-deck.cjs +322 -0
- package/scripts/generate-export.cjs +474 -0
- package/scripts/generate-hub.cjs +2553 -0
- package/scripts/generate-lobby.cjs +718 -0
- package/scripts/generate-presentation.cjs +988 -0
- package/scripts/generate-presentation.test.cjs +405 -0
- package/scripts/generate-section-intelligence.cjs +330 -0
- package/scripts/generate-snapshot.cjs +1410 -0
- package/scripts/generate-standalone +159 -0
- package/scripts/git-ops +211 -0
- package/scripts/graph-heal-pipeline.cjs +321 -0
- package/scripts/gsd-artifact-graph-hook.cjs +219 -0
- package/scripts/gsd-graph-derive-drain.cjs +571 -0
- package/scripts/gsd-graph-derive-sweep.cjs +195 -0
- package/scripts/heal-command.cjs +1365 -0
- package/scripts/hedge-refit-pipeline.cjs +207 -0
- package/scripts/help-renderer.cjs +303 -0
- package/scripts/hmi-compliance-poll.cjs +451 -0
- package/scripts/hmi-status-command.cjs +510 -0
- package/scripts/hooked-rescore-117.cjs +224 -0
- package/scripts/hooks/pre-commit +533 -0
- package/scripts/hooks/pre-commit-room-minto-guard.cmd +43 -0
- package/scripts/hooks/pre-commit-room-minto-guard.sh +533 -0
- package/scripts/hsi-to-graph.cjs +194 -0
- package/scripts/huji-batch.cjs +821 -0
- package/scripts/huji-eval.cjs +1158 -0
- package/scripts/huji-intake.cjs +563 -0
- package/scripts/huji-pin-smoketest.cjs +454 -0
- package/scripts/huji-run-one-async.cjs +305 -0
- package/scripts/huji-run-one.cjs +671 -0
- package/scripts/install-pre-commit.sh +113 -0
- package/scripts/intent-classifier +106 -0
- package/scripts/intent-classifier.cjs +3620 -0
- package/scripts/interpret-whitespace.cjs +691 -0
- package/scripts/jtbd-command.cjs +810 -0
- package/scripts/jtbd-update.cjs +299 -0
- package/scripts/label-topic-forest.cjs +667 -0
- package/scripts/learn-from-usage +168 -0
- package/scripts/lib/__pycache__/ensure_ml_deps.cpython-312.pyc +0 -0
- package/scripts/lib/ensure_ml_deps.py +97 -0
- package/scripts/load-embeddings-into-neo4j.cjs +569 -0
- package/scripts/memory-artifact-graph-hook.cjs +191 -0
- package/scripts/memory-command.cjs +687 -0
- package/scripts/memory-completion-detector.cjs +217 -0
- package/scripts/memory-lifecycle.cjs +620 -0
- package/scripts/memory-resume-nudge.cjs +233 -0
- package/scripts/methodology-ingest-local.sh +36 -0
- package/scripts/migrate-brain-mcp-from-http-to-stdio.cjs +190 -0
- package/scripts/migrate-brain-mcp-from-http-to-stdio.test.cjs +242 -0
- package/scripts/migrate-hats-to-roomdb.cjs +217 -0
- package/scripts/migrate-lazygraph.cjs +250 -0
- package/scripts/migrate-minto-schema-v88.cjs +296 -0
- package/scripts/migrate-plugin-root-refs.cjs +168 -0
- package/scripts/migrate-room-sections-v275.cjs +556 -0
- package/scripts/migrate-rooms +417 -0
- package/scripts/migrate-skill-frontmatter.cjs +472 -0
- package/scripts/migrate-stale-user-settings.cjs +296 -0
- package/scripts/migrate-telemetry-v1.cjs +384 -0
- package/scripts/migrate-telemetry-v1.test.cjs +356 -0
- package/scripts/minto-debouncer.cjs +428 -0
- package/scripts/mos-status.cjs +622 -0
- package/scripts/mva-detect.cjs +229 -0
- package/scripts/mva-run.cjs +36 -0
- package/scripts/on-agent-complete +163 -0
- package/scripts/on-cwd-changed +149 -0
- package/scripts/on-file-changed +68 -0
- package/scripts/on-stop +617 -0
- package/scripts/on-task-complete +194 -0
- package/scripts/operator-command.cjs +858 -0
- package/scripts/operator-update.cjs +308 -0
- package/scripts/part8-egress-guard-hook.cjs +229 -0
- package/scripts/pipeline-command.cjs +271 -0
- package/scripts/post-compact +383 -0
- package/scripts/post-update-activation.cjs +339 -0
- package/scripts/post-write +283 -0
- package/scripts/power-demo-prompt.md +621 -0
- package/scripts/pre-compact +273 -0
- package/scripts/preflight-auto-explore.cjs +314 -0
- package/scripts/preflight-doctor.cjs +225 -0
- package/scripts/preflight-release-drift.cjs +168 -0
- package/scripts/preflight-tension-surface.cjs +414 -0
- package/scripts/probe-brain-contract.cjs +502 -0
- package/scripts/publish-ops +346 -0
- package/scripts/query-efficiency-telemetry.cjs +364 -0
- package/scripts/query-semantic-scholar.cjs +545 -0
- package/scripts/reapply-modifications +49 -0
- package/scripts/reassign-primary.cjs +125 -0
- package/scripts/recompile-room-references.cjs +601 -0
- package/scripts/release-beta-preflight.sh +69 -0
- package/scripts/render-pdf +588 -0
- package/scripts/render-viz +190 -0
- package/scripts/research-speaker +239 -0
- package/scripts/resolve-room +204 -0
- package/scripts/restore-post-compact-context.cjs +320 -0
- package/scripts/room-auto-create-nudge.cjs +136 -0
- package/scripts/room-naming-selector.cjs +201 -0
- package/scripts/room-registry +767 -0
- package/scripts/rs-discovery-engine.cjs +725 -0
- package/scripts/rs-engine.py +2073 -0
- package/scripts/rs-experts-command.cjs +300 -0
- package/scripts/rs-explain-command.cjs +323 -0
- package/scripts/rs-fetch-command.cjs +170 -0
- package/scripts/rs-thesis-command.cjs +203 -0
- package/scripts/rs-vector-bridge.cjs +410 -0
- package/scripts/run-harness.cjs +890 -0
- package/scripts/scout-cadence-guard.cjs +323 -0
- package/scripts/scout-cadence-runner.cjs +509 -0
- package/scripts/scout-telemetry-aggregator.cjs +268 -0
- package/scripts/sealed-walker.py +96 -0
- package/scripts/seed-brain-commands.cjs +324 -0
- package/scripts/self-update +105 -0
- package/scripts/sentinel-deadline-monitor +241 -0
- package/scripts/sentinel-health-check +198 -0
- package/scripts/sentinel-snapshot +42 -0
- package/scripts/serve-dashboard +103 -0
- package/scripts/serve-dashboard-live +932 -0
- package/scripts/serve-presentation +97 -0
- package/scripts/serve-wiki +90 -0
- package/scripts/session-end-presence.cjs +79 -0
- package/scripts/session-start +1942 -0
- package/scripts/sessionstart-coordinator.cjs +249 -0
- package/scripts/sessionstart-npm-reconcile.cjs +111 -0
- package/scripts/sessionstart-post-update-preflight.cjs +303 -0
- package/scripts/sessionstart-reference-now-seed.cjs +137 -0
- package/scripts/setup-hooks.sh +112 -0
- package/scripts/skillopt-codereview.cjs +830 -0
- package/scripts/skillopt-eval.cjs +519 -0
- package/scripts/skillopt-funnel.cjs +1076 -0
- package/scripts/skillopt-genqueries.cjs +571 -0
- package/scripts/skillopt-inventory.cjs +319 -0
- package/scripts/skillopt-merge.cjs +656 -0
- package/scripts/skillopt-triggerloop.cjs +1126 -0
- package/scripts/soft-alias-runner.cjs +116 -0
- package/scripts/stamp-artifact-write.cjs +276 -0
- package/scripts/stamp-firing-block.cjs +325 -0
- package/scripts/state-write.cjs +97 -0
- package/scripts/statusline-fallback-echo.cjs +297 -0
- package/scripts/statusline-mos +116 -0
- package/scripts/statusline-mos-dispatch +127 -0
- package/scripts/suggest-next-command.cjs +416 -0
- package/scripts/sweep-skill-descriptions.cjs +263 -0
- package/scripts/sync-rooms-brain +438 -0
- package/scripts/sync-rooms-graph +264 -0
- package/scripts/telemetry-command-invocation.cjs +120 -0
- package/scripts/test-doctor-recovery.cjs +242 -0
- package/scripts/test-fresh-install.md +140 -0
- package/scripts/track-analytics +156 -0
- package/scripts/transcribe-audio +148 -0
- package/scripts/transcript-ingest.cjs +225 -0
- package/scripts/update-icm-index +153 -0
- package/scripts/v182-brain-optimize.cypher +1191 -0
- package/scripts/validate-model-profiles +93 -0
- package/scripts/vault-content-reformatter.cjs +581 -0
- package/scripts/vault-export-orchestrator.cjs +400 -0
- package/scripts/vault-footer-injector.cjs +315 -0
- package/scripts/vault-import.cjs +839 -0
- package/scripts/vault-integration-test.cjs +318 -0
- package/scripts/vault-regenerate-all.cjs +216 -0
- package/scripts/vault-regenerate-all.test.cjs +138 -0
- package/scripts/vault-rules-generator.cjs +220 -0
- package/scripts/vault-section-minto-generator.cjs +1642 -0
- package/scripts/vault-section-minto-generator.integration.test.cjs +576 -0
- package/scripts/vault-section-minto-generator.test.cjs +291 -0
- package/scripts/vault-section-state-generator.cjs +385 -0
- package/scripts/vault-welcome-generator.cjs +403 -0
- package/scripts/vault-wikilink-injector.cjs +449 -0
- package/scripts/verify-phase-132.cjs +338 -0
- package/scripts/verify-release +744 -0
- package/scripts/whitespace-command.cjs +831 -0
- package/scripts/whitespace-to-brain.cjs +383 -0
- package/scripts/whitespace-to-graph.cjs +220 -0
- package/scripts/wikilink-batch.cjs +102 -0
- package/scripts/wikilink-file.cjs +139 -0
- package/scripts/write-bridge-artifacts.cjs +140 -0
- package/scripts/write-scope-check +12 -0
- package/scripts/write-scope-check.cjs +461 -0
- package/scripts/write-whitespace-sections.cjs +483 -0
- package/settings.json +7 -0
- package/skills/act/SKILL.md +523 -0
- package/skills/admin/SKILL.md +437 -0
- package/skills/agentshield/SKILL.md +50 -0
- package/skills/analyze-needs/SKILL.md +74 -0
- package/skills/analyze-systems/SKILL.md +69 -0
- package/skills/analyze-timing/SKILL.md +94 -0
- package/skills/auto-explore/SKILL.md +84 -0
- package/skills/beautiful-question/SKILL.md +72 -0
- package/skills/bono/SKILL.md +146 -0
- package/skills/brain-connector/SKILL.md +206 -0
- package/skills/brain-derive/SKILL.md +140 -0
- package/skills/build-knowledge/SKILL.md +118 -0
- package/skills/build-thesis/SKILL.md +90 -0
- package/skills/causal/SKILL.md +265 -0
- package/skills/challenge-assumptions/SKILL.md +74 -0
- package/skills/client-discovery-interview/SKILL.md +94 -0
- package/skills/client-discovery-interview/discovery-scaffold.json +41 -0
- package/skills/client-discovery-interview/question-bank.md +149 -0
- package/skills/compare-ventures/SKILL.md +113 -0
- package/skills/context-engine/SKILL.md +169 -0
- package/skills/conversation-mode/SKILL.md +189 -0
- package/skills/correct-reference-now/SKILL.md +86 -0
- package/skills/dashboard/SKILL.md +142 -0
- package/skills/deck/SKILL.md +113 -0
- package/skills/deep-grade/SKILL.md +210 -0
- package/skills/diagnose/SKILL.md +128 -0
- package/skills/diagnostics/SKILL.md +193 -0
- package/skills/dial-memory-refresh/SKILL.md +116 -0
- package/skills/diffusion/SKILL.md +125 -0
- package/skills/discover/SKILL.md +216 -0
- package/skills/doctor/SKILL.md +269 -0
- package/skills/dogfood-flush/SKILL.md +133 -0
- package/skills/dominant-designs/SKILL.md +72 -0
- package/skills/eureka/SKILL.md +313 -0
- package/skills/explain-decision/SKILL.md +138 -0
- package/skills/explore-domains/SKILL.md +74 -0
- package/skills/explore-futures/SKILL.md +64 -0
- package/skills/explore-opportunity/SKILL.md +168 -0
- package/skills/explore-trends/SKILL.md +74 -0
- package/skills/export/SKILL.md +126 -0
- package/skills/feynman-timeline-refresh/SKILL.md +105 -0
- package/skills/file-meeting/SKILL.md +1190 -0
- package/skills/find-analogies/SKILL.md +360 -0
- package/skills/find-bottlenecks/SKILL.md +104 -0
- package/skills/find-connections/SKILL.md +108 -0
- package/skills/funding/SKILL.md +110 -0
- package/skills/futures/SKILL.md +124 -0
- package/skills/grade/SKILL.md +252 -0
- package/skills/grade-grant/SKILL.md +293 -0
- package/skills/graph/SKILL.md +216 -0
- package/skills/hat-briefing/SKILL.md +160 -0
- package/skills/heal/SKILL.md +69 -0
- package/skills/help/SKILL.md +172 -0
- package/skills/hmi-status/SKILL.md +73 -0
- package/skills/ignite/SKILL.md +225 -0
- package/skills/ingest-methodology/SKILL.md +125 -0
- package/skills/intel-pipeline/SKILL.md +128 -0
- package/skills/intelligence-orchestrator/SKILL.md +262 -0
- package/skills/jtbd/SKILL.md +261 -0
- package/skills/larry-personality/SKILL.md +570 -0
- package/skills/larry-personality/framework-chains.md +92 -0
- package/skills/larry-personality/mode-engine.md +185 -0
- package/skills/leadership/SKILL.md +109 -0
- package/skills/lean-canvas/SKILL.md +70 -0
- package/skills/macro-trends/SKILL.md +84 -0
- package/skills/map-unknowns/SKILL.md +72 -0
- package/skills/memory/SKILL.md +212 -0
- package/skills/memory-cortex-reach/SKILL.md +80 -0
- package/skills/models/SKILL.md +197 -0
- package/skills/mos/SKILL.md +160 -0
- package/skills/mos-deck-engine/SKILL.md +329 -0
- package/skills/mos-reason/SKILL.md +484 -0
- package/skills/mullins/SKILL.md +151 -0
- package/skills/mullins-scaffold/SKILL.md +132 -0
- package/skills/mullins-scaffold/scaffold.json +223 -0
- package/skills/mva-brief/SKILL.md +87 -0
- package/skills/mva-option/SKILL.md +120 -0
- package/skills/mva-pipeline/SKILL.md +158 -0
- package/skills/mva-report/SKILL.md +79 -0
- package/skills/new-project/SKILL.md +629 -0
- package/skills/new-surface/SKILL.md +112 -0
- package/skills/onboard/SKILL.md +485 -0
- package/skills/operator/SKILL.md +167 -0
- package/skills/opportunities/SKILL.md +171 -0
- package/skills/organize/SKILL.md +69 -0
- package/skills/persona/SKILL.md +272 -0
- package/skills/pipeline/SKILL.md +164 -0
- package/skills/present/SKILL.md +123 -0
- package/skills/publish/SKILL.md +224 -0
- package/skills/pws-brain/SKILL.md +152 -0
- package/skills/pws-methodology/SKILL.md +92 -0
- package/skills/qualify-opportunity/SKILL.md +111 -0
- package/skills/query/SKILL.md +68 -0
- package/skills/radar/SKILL.md +106 -0
- package/skills/reanalyze/SKILL.md +122 -0
- package/skills/research/SKILL.md +462 -0
- package/skills/room/SKILL.md +381 -0
- package/skills/room-passive/SKILL.md +174 -0
- package/skills/room-proactive/SKILL.md +344 -0
- package/skills/rooms/SKILL.md +678 -0
- package/skills/root-cause/SKILL.md +73 -0
- package/skills/rs-experts/SKILL.md +151 -0
- package/skills/rs-explain/SKILL.md +130 -0
- package/skills/rs-fetch/SKILL.md +115 -0
- package/skills/rs-thesis/SKILL.md +129 -0
- package/skills/scenario-plan/SKILL.md +76 -0
- package/skills/scheduled-tasks/SKILL.md +301 -0
- package/skills/score-innovation/SKILL.md +74 -0
- package/skills/scout/SKILL.md +370 -0
- package/skills/setup/SKILL.md +654 -0
- package/skills/show/SKILL.md +68 -0
- package/skills/skill/SKILL.md +104 -0
- package/skills/snapshot/SKILL.md +178 -0
- package/skills/speakers/SKILL.md +116 -0
- package/skills/splash/SKILL.md +54 -0
- package/skills/stance/SKILL.md +89 -0
- package/skills/status/SKILL.md +107 -0
- package/skills/structure-argument/SKILL.md +78 -0
- package/skills/suggest-next/SKILL.md +137 -0
- package/skills/systems-thinking/SKILL.md +220 -0
- package/skills/think-hats/SKILL.md +82 -0
- package/skills/trending-to-absurd/SKILL.md +100 -0
- package/skills/ui-system/SKILL.md +474 -0
- package/skills/ui-system/design-system/M-OS-DESIGN-SYSTEM.md +273 -0
- package/skills/ui-system/design-system/SPEC.md +72 -0
- package/skills/ui-system/design-system/image-prompt-style.md +29 -0
- package/skills/ui-system/design-system/mos-design-system.css +240 -0
- package/skills/ui-system/rules/design-system.md +60 -0
- package/skills/ui-system/rules/dual-palette.md +156 -0
- package/skills/ui-system/rules/glyph-disambiguation.md +171 -0
- package/skills/ui-system/rules/shape-f-zero-and-six.md +169 -0
- package/skills/update/SKILL.md +254 -0
- package/skills/user-needs/SKILL.md +73 -0
- package/skills/validate/SKILL.md +73 -0
- package/skills/value-proposition/SKILL.md +141 -0
- package/skills/vault/SKILL.md +326 -0
- package/skills/visualize/SKILL.md +69 -0
- package/skills/whitespace/SKILL.md +539 -0
- package/skills/wiki/SKILL.md +94 -0
- package/templates/_base.html +20 -0
- package/templates/chat-panel.html +205 -0
- package/templates/constellation-chat-hooks.js +89 -0
- package/templates/constellation.html +1272 -0
- package/templates/destijl-base.css +268 -0
- package/templates/discovery/discovery-brief-template.md +97 -0
- package/templates/due-diligence.html +49 -0
- package/templates/executive-summary.html +40 -0
- package/templates/icm/CLAUDE.md +31 -0
- package/templates/icm/GROUP-CLAUDE.md +33 -0
- package/templates/icm/INDEX.md +26 -0
- package/templates/import/stage-contracts/01-ingest.md +34 -0
- package/templates/import/stage-contracts/02-classify.md +48 -0
- package/templates/import/stage-contracts/03-route.md +42 -0
- package/templates/import/stage-contracts/04-enrich.md +40 -0
- package/templates/investment-thesis.html +25 -0
- package/templates/meeting-report.html +361 -0
- package/templates/presentation/dashboard.html +1150 -0
- package/templates/presentation/deck.html +540 -0
- package/templates/presentation/diagrams.html +437 -0
- package/templates/presentation/graph.html +391 -0
- package/templates/presentation/insights.html +548 -0
- package/templates/presentation/wiki.html +533 -0
- package/templates/profile.html +136 -0
- package/templates/room-context/last-session.md +56 -0
- package/templates/room-context/methodology-history.md +28 -0
- package/templates/room-context/rejection-log.md +25 -0
- package/templates/room-context/weekly-digest.md +54 -0
- package/templates/room-skeleton/MINTO.md.tmpl +19 -0
- package/templates/room-skeleton/ROOM.md.identity.tmpl +13 -0
- package/templates/room-skeleton/ROOM.md.section.tmpl +33 -0
- package/templates/room-skeleton/STATE.md.tmpl +33 -0
- package/templates/room-skeleton/USER.md.tmpl +23 -0
- package/templates/room-skeleton/references/.gitkeep +0 -0
- package/templates/room-skeleton/references/SECTION-SCHEMA.md +213 -0
- package/templates/room-skeleton/references/SUB-SCHEMAS.md +91 -0
- package/templates/room-skeleton/section-contracts/.gitkeep +0 -0
- package/templates/room-skeleton/section-contracts/business-model.md +32 -0
- package/templates/room-skeleton/section-contracts/competitive-analysis.md +33 -0
- package/templates/room-skeleton/section-contracts/financial-model.md +30 -0
- package/templates/room-skeleton/section-contracts/funding.md +41 -0
- package/templates/room-skeleton/section-contracts/legal-ip.md +30 -0
- package/templates/room-skeleton/section-contracts/market-analysis.md +32 -0
- package/templates/room-skeleton/section-contracts/opportunity-bank.md +39 -0
- package/templates/room-skeleton/section-contracts/problem-definition.md +32 -0
- package/templates/room-skeleton/section-contracts/solution-design.md +36 -0
- package/templates/room-skeleton/section-contracts/strategy.md +35 -0
- package/templates/room-skeleton/section-contracts/team-execution.md +36 -0
- package/templates/shared.css +427 -0
- package/templates/showcase/blueprint.html +432 -0
- package/templates/showcase/library.html +571 -0
- package/templates/showcase/narrative.html +361 -0
- package/templates/showcase/overview.html +468 -0
- package/templates/showcase/synthesis.html +473 -0
- package/templates/signature-footer.html +34 -0
|
@@ -0,0 +1,4327 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/*
|
|
3
|
+
* Copyright (c) 2026 Mindrian. BSL 1.1.
|
|
4
|
+
* Phase 93 D2 -- /mos:doctor diagnostic + recovery for install-cache drift.
|
|
5
|
+
*
|
|
6
|
+
* Detects whether the live install at ~/.claude/plugins/mindrian-os/ is
|
|
7
|
+
* out of sync with the marketplace cache at
|
|
8
|
+
* ~/.claude/plugins/cache/mindrian-marketplace/mos/<latest>/, and offers
|
|
9
|
+
* a one-shot recovery via --fix that backs up the stale install and
|
|
10
|
+
* replaces it with the latest cached version.
|
|
11
|
+
*
|
|
12
|
+
* MODES:
|
|
13
|
+
* doctor read-only diagnostic - exits with status code only
|
|
14
|
+
* doctor --fix runs backup-then-replace recovery if drift detected
|
|
15
|
+
* doctor --json machine-readable output (for hooks / regression tests)
|
|
16
|
+
*
|
|
17
|
+
* Exit codes:
|
|
18
|
+
* 0 -> healthy, no drift
|
|
19
|
+
* 1 -> drift detected (read-only mode)
|
|
20
|
+
* 2 -> drift detected and recovered (--fix mode)
|
|
21
|
+
* 3 -> internal error (cannot read directories, no marketplace cache, etc.)
|
|
22
|
+
*
|
|
23
|
+
* Phase 93 scope. See docs/autopsies/2026-04-28-install-cache-drift-incident.md.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
'use strict';
|
|
27
|
+
|
|
28
|
+
const fs = require('fs');
|
|
29
|
+
const path = require('path');
|
|
30
|
+
const os = require('os');
|
|
31
|
+
// Phase 126 Plan-02: semver package for numeric-aware prerelease ordering
|
|
32
|
+
// in cmpVersion (line ~194). semver@^7.7.4 is a PRODUCTION dependency in
|
|
33
|
+
// package.json (it was a devDependency until the debug session
|
|
34
|
+
// mcp-servers-cache-missing-node-modules audit found that classification
|
|
35
|
+
// was wrong -- doctor.cjs is a user-facing runtime script, so semver must
|
|
36
|
+
// be present on every install, including the vendored production tree).
|
|
37
|
+
// Canon Part 7 reuse: scripts/release.sh also uses semver for pre-release
|
|
38
|
+
// algebra; this is the same dep reused for scripts/doctor.cjs.
|
|
39
|
+
const semver = require('semver');
|
|
40
|
+
|
|
41
|
+
// -- Paths -----------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
// Phase 217 Plan-01 (D-02): shared constants + helpers + the pure class-A
|
|
44
|
+
// constituent readers live in the leaf module lib/core/doctor/shared.cjs. ONE
|
|
45
|
+
// destructuring require -- doctor.cjs -> shared.cjs is the ONLY direction (no
|
|
46
|
+
// back-require). C (the color table), HOME, PLUGIN_HOME, INSTALL_DIR,
|
|
47
|
+
// MARKETPLACE_CACHE_DIR, PLUGIN_ROOT, parseVersion, cmpVersion,
|
|
48
|
+
// resolveActivePluginRoot (re-exported by shared from
|
|
49
|
+
// lib/core/active-plugin-root.cjs), readRegistry,
|
|
50
|
+
// readInstalledPluginsVersion, checkInstallVersion, checkMarketplaceCache, and
|
|
51
|
+
// checkDevSourceConsistency all resolve here. INSTALL_DIR remains the source of
|
|
52
|
+
// truth for the class A check; new code MUST use resolveActivePluginRoot()
|
|
53
|
+
// instead of the hardcoded INSTALL_DIR constant.
|
|
54
|
+
const {
|
|
55
|
+
C,
|
|
56
|
+
HOME,
|
|
57
|
+
PLUGIN_HOME,
|
|
58
|
+
INSTALL_DIR,
|
|
59
|
+
MARKETPLACE_CACHE_DIR,
|
|
60
|
+
PLUGIN_ROOT,
|
|
61
|
+
parseVersion,
|
|
62
|
+
cmpVersion,
|
|
63
|
+
resolveActivePluginRoot,
|
|
64
|
+
readRegistry,
|
|
65
|
+
readInstalledPluginsVersion,
|
|
66
|
+
checkInstallVersion,
|
|
67
|
+
checkMarketplaceCache,
|
|
68
|
+
checkDevSourceConsistency,
|
|
69
|
+
// Phase 217 Plan 06: the class I/J shared readers moved to shared.cjs with the
|
|
70
|
+
// I/J migration. doctor.cjs still needs these five in the
|
|
71
|
+
// --report-registration-bug assembler (Canon Part 7 reuse); the rest are now
|
|
72
|
+
// internal to the install-state / deployment-surfaces runner modules.
|
|
73
|
+
collectVersionOfRecord,
|
|
74
|
+
computeVersionDivergences,
|
|
75
|
+
readLegacyConfigPin,
|
|
76
|
+
detectMarketplaceCacheInstall,
|
|
77
|
+
readInstalledPluginsInstallPath,
|
|
78
|
+
} = require(path.join(__dirname, '..', 'lib', 'core', 'doctor', 'shared.cjs'));
|
|
79
|
+
|
|
80
|
+
// Phase 217 Plan 06: class I (install-state) + class J (deployment-surfaces)
|
|
81
|
+
// migrated to registry-driven cadence:always runner modules. The accumulative
|
|
82
|
+
// engine owns their check + fix-then-recheck under the --install-state flag
|
|
83
|
+
// (both share it, exactly as the retired main() block). doctor.cjs imports the
|
|
84
|
+
// runners' RAW check entries ONLY for the --acceptance path (buildAcceptance
|
|
85
|
+
// checklist keys on the historical 'healthy' status); never a duplicate body
|
|
86
|
+
// (Pitfall 4). The runner check(ctx) surfaces map 'healthy' -> 'ok' for the
|
|
87
|
+
// engine's report.checks vocabulary.
|
|
88
|
+
const installStateModule = require(path.join(__dirname, '..', 'lib', 'core', 'doctor', 'install-state-module.cjs'));
|
|
89
|
+
const deploymentSurfacesModule = require(path.join(__dirname, '..', 'lib', 'core', 'doctor', 'deployment-surfaces-module.cjs'));
|
|
90
|
+
const checkInstallState = installStateModule.checkInstallState;
|
|
91
|
+
const checkDeploymentSurfaces = deploymentSurfacesModule.checkDeploymentSurfaces;
|
|
92
|
+
// Phase 217 Plan 04: the SINGLE umbilical/room target resolver + the .room-root
|
|
93
|
+
// walk-up + the section walk + generator spawn all moved WITH the class B/C/E
|
|
94
|
+
// checks into their registry runners (lib/core/doctor/cascade-rooms*-module.cjs,
|
|
95
|
+
// room-md-module.cjs). doctor.cjs no longer references any of them directly.
|
|
96
|
+
// Phase 139 Plan-02 (S2 accumulative-engine skeleton): the version-dispatch
|
|
97
|
+
// substrate. The hand-maintained module registry (data/doctor-modules.json,
|
|
98
|
+
// mirrors data/deployment-surfaces.json's "new entry = no code change" pattern)
|
|
99
|
+
// + doctor's OWN persisted applied-through watermark (readDoctorApplied /
|
|
100
|
+
// writeDoctorApplied at ~/.mindrian/doctor-applied.json -- NOT the session-start-
|
|
101
|
+
// overwritten ~/.mindrian-last-version, per the watermark_not_lastversion LOCKED
|
|
102
|
+
// decision). The selector (runAccumulativeEngine) generalizes the proven
|
|
103
|
+
// lib/core/install-state.cjs::migrateIfNeeded semver-dispatch + future-version-
|
|
104
|
+
// DEFER + additive-idempotency shape: it runs every module whose
|
|
105
|
+
// introduced_version is in (applied_through, running], idempotently.
|
|
106
|
+
const DOCTOR_MODULES_PATH = path.join(__dirname, '..', 'data', 'doctor-modules.json');
|
|
107
|
+
const { readDoctorApplied, writeDoctorApplied } = require(path.join(__dirname, '..', 'lib', 'core', 'migration-snapshot.cjs'));
|
|
108
|
+
// Phase 95.6 D-09 / R-01: the install-receipt const (INSTALL_RECEIPT_JSON) moved
|
|
109
|
+
// into lib/core/doctor/install-incomplete-module.cjs with the class H check
|
|
110
|
+
// (Phase 217 Plan 05).
|
|
111
|
+
|
|
112
|
+
// -- Argument parsing ------------------------------------------------
|
|
113
|
+
|
|
114
|
+
function parseArgs(argv) {
|
|
115
|
+
const flags = {
|
|
116
|
+
fix: false, json: false, verbose: false,
|
|
117
|
+
cascadeRooms: false, verifySurface: false, roomMd: false, uiCompliance: false,
|
|
118
|
+
statuslineVisibility: false,
|
|
119
|
+
// Phase 217 Plan-02 (D-05): class card-fire-health -- reports the health of
|
|
120
|
+
// the scripts/check-card-fire.cjs self-diagnostic instrument (intercept log
|
|
121
|
+
// exists/valid-JSONL/fresh, classifier/counter seams intact, session store
|
|
122
|
+
// parseable). cadence:always module, so a --card-fire-health-only run is a
|
|
123
|
+
// class-flag run (exit-0 invariant, no once-heal pass); also set by --all.
|
|
124
|
+
cardFireHealth: false,
|
|
125
|
+
installState: false,
|
|
126
|
+
// Phase 121.5-05 Sub-plan F (SEED-007 absorption): class K (stale-first-
|
|
127
|
+
// touch-copy) checks every first-touch surface declared in
|
|
128
|
+
// data/first-touch-surfaces.json for two regression patterns:
|
|
129
|
+
// (1) stale_version_hardcode -- a vX.Y.Z literal older than the running
|
|
130
|
+
// plugin version (the SEED-007 trigger);
|
|
131
|
+
// (2) em_dash_violation -- any U+2014 EM DASH in a surface flagged with
|
|
132
|
+
// em_dash_check: true (the feedback_no_emdashes hard rule).
|
|
133
|
+
// Per data/first-touch-surfaces.json each surface declares both invariants
|
|
134
|
+
// and an allowlist of doc-example version strings. Renamed from class H
|
|
135
|
+
// in 121.5-05-PLAN.md (class H is install-incomplete; deviation Rule 3).
|
|
136
|
+
staleFirstTouch: false,
|
|
137
|
+
// Phase 121.5-08 Sub-plan J (D-09 final clause): class L
|
|
138
|
+
// (deprecated-commands-you-still-use). Scans recent
|
|
139
|
+
// ~/.claude/projects/.../*.jsonl session transcripts (last 7 days)
|
|
140
|
+
// for /mos:<deprecated> patterns where the deprecated name maps to a
|
|
141
|
+
// canonical /mos:<new> per the v1.13.0-beta.16+ rename table:
|
|
142
|
+
// heal -> doctor --heal-room
|
|
143
|
+
// query -> graph
|
|
144
|
+
// organize -> rooms organize
|
|
145
|
+
// hmi-status -> doctor --ui-compliance
|
|
146
|
+
// visualize -> dashboard --mermaid
|
|
147
|
+
// diagnostics -> fingerprint
|
|
148
|
+
// MINDRIAN_DOCTOR_TRANSCRIPTS_DIR overrides the default scan dir for
|
|
149
|
+
// hermetic tests. Activated by --deprecated-usage AND --all.
|
|
150
|
+
// Pure LOCAL scan; zero network surface (Canon Part 8).
|
|
151
|
+
// Letter L chosen because A..K are already assigned (A install-cache,
|
|
152
|
+
// B+C cascade-rooms, D verify-surface, E room-md, F ui-compliance,
|
|
153
|
+
// G statusline-visibility, H install-incomplete, I install-state,
|
|
154
|
+
// J deployment-surfaces, K stale-first-touch-copy).
|
|
155
|
+
deprecatedUsage: false,
|
|
156
|
+
// Phase 127-02 BRAIN-MCP-127-08: class M (Brain end-to-end smoke).
|
|
157
|
+
// 6-layer probe (plugin root resolver, key resolver, HTTPS schema probe,
|
|
158
|
+
// MCP stdio handshake, e2e brain_schema via the shim, store identity).
|
|
159
|
+
// Diagnostic-only; class-flag invariant applies (exit 0 even on FAIL).
|
|
160
|
+
// Letter M is the next free letter after L (CONTEXT D4 text reads "K"
|
|
161
|
+
// but K is taken).
|
|
162
|
+
brainSmoke: false,
|
|
163
|
+
// quick(260706-13z) D14: class S (Eureka local-embedding-stack smoke).
|
|
164
|
+
// 4-layer probe (deps present, vec backend, model cache, graceful degrade).
|
|
165
|
+
// Diagnostic-only; class-flag invariant applies (exit 0 even on FAIL). Never
|
|
166
|
+
// downloads a model unless MINDRIAN_EUREKA_SMOKE_ALLOW_DOWNLOAD=1. Letter S
|
|
167
|
+
// is the next free letter after R (runtime-reachability, Phase 185).
|
|
168
|
+
eurekaSmoke: false,
|
|
169
|
+
// Phase 123 Plan-04 (HARNESS-123-11 + HARNESS-123-12): release-gate-as-a-
|
|
170
|
+
// command. --acceptance runs the 7-point checklist; --pre-tag filters to
|
|
171
|
+
// the 5 pre-tag-applicable points; --light-npx skips the mktemp HOME
|
|
172
|
+
// override sandbox in the npx-roundtrip point (operator opt-in for slow
|
|
173
|
+
// networks / hermetic CI). --acceptance has its OWN exit-code contract
|
|
174
|
+
// (0 = all points passed; 1 = any point failed) and is NOT a class flag,
|
|
175
|
+
// so the class-flag-always-exit-0 invariant does NOT apply.
|
|
176
|
+
//
|
|
177
|
+
// Phase 126.1 hotfix (2026-05-15): added `preFlight` tier. release.sh
|
|
178
|
+
// Step 6.6 calls --pre-tag AFTER Steps 3-6 intentionally bump
|
|
179
|
+
// plugin.json + package.json + CHANGELOG, so verify-release-clean-tree
|
|
180
|
+
// cannot live in 'pre-tag'. The new `pre-flight` tier is a strict
|
|
181
|
+
// subset of pre-tag MINUS in-flight-incompatible checks; release.sh
|
|
182
|
+
// Step 2.5 calls --pre-flight BEFORE Step 3 mutates anything, restoring
|
|
183
|
+
// the clean-tree gate. Mutually exclusive with --pre-tag; if both
|
|
184
|
+
// pass, --pre-tag wins.
|
|
185
|
+
acceptance: false, preTag: false, preFlight: false, lightNpx: false,
|
|
186
|
+
// Phase 127.2-03 Task 2 (Finding F1 + F7): --check-rs-engine pre-flights
|
|
187
|
+
// Python deps reachable from scripts/rs-engine.py (Engine 1 Act 1 surface
|
|
188
|
+
// for /mos:find-bottlenecks et al.). ADD-ONLY -- not a class flag; has
|
|
189
|
+
// its OWN dispatch + exit-code contract (0 = deps present; 1 = deps
|
|
190
|
+
// missing with actionable fix line). NOT in --all because --all is for
|
|
191
|
+
// class A-M drift detection, not for environment readiness probes.
|
|
192
|
+
// Closes the Windows tester 2026-05-23 silent-failure class.
|
|
193
|
+
checkRsEngine: false,
|
|
194
|
+
// Phase 127.2 Plan 04 Instance #7: --post-update routes through
|
|
195
|
+
// scripts/post-update-activation.cjs (atomic-swap delegate to the
|
|
196
|
+
// existing --fix pipeline + touch-file write so the next session's
|
|
197
|
+
// sessionstart-post-update-preflight.cjs verifies activation reached
|
|
198
|
+
// the wire). ADD-ONLY -- not a class flag; has its OWN dispatch +
|
|
199
|
+
// exit-code contract (0 = activated or already on latest; 1 = swap
|
|
200
|
+
// attempted and failed). Composes with --fix when called as
|
|
201
|
+
// `doctor --fix --post-update` from /mos:update Step 7.
|
|
202
|
+
postUpdate: false,
|
|
203
|
+
// Phase 146 Plan 04 (ACPT-01..05) + Phase 150.5-03 (ACPT-06):
|
|
204
|
+
// --dogfood-acceptance is the LOOP-FIRES gate -- the Phase 146 milestone
|
|
205
|
+
// gate. It runs the 6 ACPT dogfood drivers
|
|
206
|
+
// (tests/test-acpt-0{1,2,3,4,5,6}-*.cjs) as child processes and aggregates
|
|
207
|
+
// their exit codes. It has its OWN exit-code contract (0 = every ACPT
|
|
208
|
+
// hermetic leg passed; non-zero = a leg failed), separate from the existing
|
|
209
|
+
// release --acceptance, and mirroring how --acceptance is "NOT a class flag"
|
|
210
|
+
// with its own contract -- the class-flag-always-exit-0 invariant does NOT
|
|
211
|
+
// apply. Per the ROADMAP Phase 146 mandate: exit 0 = the milestone ships as
|
|
212
|
+
// "Larry Reaches"; non-zero = the milestone is renamed. The ACPT-05 driver's
|
|
213
|
+
// LIVE mode_a arm self-skips when Brain is unreachable (Plan 03), so the gate
|
|
214
|
+
// passes on the hermetic legs WITHOUT a reachable Brain -- the gate must NOT
|
|
215
|
+
// require Brain (Canon Part 8: --dogfood-acceptance adds zero network surface
|
|
216
|
+
// in doctor itself; the spawned drivers are LOCAL/hermetic).
|
|
217
|
+
dogfoodAcceptance: false,
|
|
218
|
+
// Phase 150-08 (MEM-09 / D-09): --claims is the CLAIM HARNESS gate -- the
|
|
219
|
+
// public-site-claim acceptance gate. It shells tests/claim-harness/run-all-
|
|
220
|
+
// claims.sh (the C1..C7 falsifiable drivers + the gates) and returns its OWN
|
|
221
|
+
// self-contained exit code (0 = every claim is true by instrumentation;
|
|
222
|
+
// non-zero = a claim failed), a SIBLING of --acceptance, NOT folded into the
|
|
223
|
+
// class roster (the class-flag-always-exit-0 invariant does NOT apply). Canon
|
|
224
|
+
// Part 8: the harness is LOCAL/hermetic; the Brain LIVE arms self-skip via
|
|
225
|
+
// class-m-brain-smoke, so the gate is CI-green without a reachable Brain. The
|
|
226
|
+
// DOCTOR_CLAIM_FAIL_POINT env hook (mirroring DOCTOR_TEST_FAIL_POINT) makes
|
|
227
|
+
// the gate's own failure path testable.
|
|
228
|
+
claims: false,
|
|
229
|
+
// Phase 150.9 Plan-02: --drift activates Class P (prose-vs-code, report-only)
|
|
230
|
+
// + Class Q (gsd-record drift) + the DRIFT.md writer. OPT-IN ONLY (D-04):
|
|
231
|
+
// NOT added to the --all block, NOT in the default session-start roster.
|
|
232
|
+
drift: false,
|
|
233
|
+
all: false, simulateWrite: null,
|
|
234
|
+
scanCommandsDir: null, scanScriptsDir: null,
|
|
235
|
+
// Phase 194-07 (PSB-14): --bind-check <roomDir> local room-health job.
|
|
236
|
+
bindCheck: false, bindCheckDir: null,
|
|
237
|
+
// Quick task 260705-jeq (D-01, D-02): --report-registration-bug is a NEW
|
|
238
|
+
// read-only sibling mode (like --check-rs-engine) that proves every locally-
|
|
239
|
+
// checkable cause of "valid install yet a command did not register" is CLEAN
|
|
240
|
+
// and assembles a paste-ready Anthropic bug report. Diagnostic-only: it never
|
|
241
|
+
// claims a "healthy"/"fixed" status and is NOT a --fix action. NOT in --all
|
|
242
|
+
// because --all is the class A-M drift roster, not an escalation reporter.
|
|
243
|
+
// Exit 0 whenever the report assembles (even offline); non-zero only if the
|
|
244
|
+
// assembler itself throws. Report goes to stdout only (never a file).
|
|
245
|
+
reportRegistrationBug: false,
|
|
246
|
+
// Phase 233 Plan-01 (RCA 4d): graph-derive-health. Reports whether a room's
|
|
247
|
+
// SEMANTIC edge layer ever landed -- BELONGS_TO edges present with ZERO
|
|
248
|
+
// cascade edges means the derivation never succeeded (the ~16-damaged-room
|
|
249
|
+
// shape). Scopes to the active room by default; --cascade-rooms widens it to
|
|
250
|
+
// every registered room, exactly as classes B and C already share that flag.
|
|
251
|
+
// --heal-room is the operator-facing sugar for `--graph-derive-health --fix`
|
|
252
|
+
// and is the literal flag the v1.13.0-beta.16 rename table
|
|
253
|
+
// ('heal' -> 'doctor --heal-room') has advertised since before it existed;
|
|
254
|
+
// this is where that documented mapping becomes real. Pure LOCAL room.db
|
|
255
|
+
// reads through the read-only navigation door + a LOCAL queue write; zero
|
|
256
|
+
// network surface (Canon Part 8).
|
|
257
|
+
graphDeriveHealth: false,
|
|
258
|
+
};
|
|
259
|
+
for (const arg of argv) {
|
|
260
|
+
if (arg === '--fix') flags.fix = true;
|
|
261
|
+
else if (arg === '--json') flags.json = true;
|
|
262
|
+
else if (arg === '--verbose' || arg === '-v') flags.verbose = true;
|
|
263
|
+
else if (arg === '--cascade-rooms') flags.cascadeRooms = true;
|
|
264
|
+
else if (arg === '--verify-surface') flags.verifySurface = true;
|
|
265
|
+
else if (arg === '--room-md') flags.roomMd = true;
|
|
266
|
+
else if (arg === '--ui-compliance') flags.uiCompliance = true;
|
|
267
|
+
else if (arg === '--statusline-visibility') flags.statuslineVisibility = true;
|
|
268
|
+
else if (arg === '--card-fire-health') flags.cardFireHealth = true;
|
|
269
|
+
else if (arg === '--install-state') flags.installState = true;
|
|
270
|
+
else if (arg === '--stale-first-touch') flags.staleFirstTouch = true;
|
|
271
|
+
else if (arg === '--deprecated-usage') flags.deprecatedUsage = true;
|
|
272
|
+
else if (arg === '--graph-derive-health') flags.graphDeriveHealth = true;
|
|
273
|
+
// --heal-room is sugar, not a second code path: it activates the SAME class
|
|
274
|
+
// and turns on --fix, so the heal runs through the engine's one
|
|
275
|
+
// fix-then-recheck flow (never a bespoke heal dispatch).
|
|
276
|
+
else if (arg === '--heal-room') { flags.graphDeriveHealth = true; flags.fix = true; }
|
|
277
|
+
else if (arg === '--brain-smoke') flags.brainSmoke = true;
|
|
278
|
+
else if (arg === '--eureka-smoke') flags.eurekaSmoke = true;
|
|
279
|
+
else if (arg === '--drift') flags.drift = true;
|
|
280
|
+
else if (arg === '--acceptance') flags.acceptance = true;
|
|
281
|
+
else if (arg === '--pre-tag') flags.preTag = true;
|
|
282
|
+
else if (arg === '--pre-flight') flags.preFlight = true;
|
|
283
|
+
else if (arg === '--light-npx') flags.lightNpx = true;
|
|
284
|
+
else if (arg === '--check-rs-engine') flags.checkRsEngine = true;
|
|
285
|
+
// Quick task 260705-jeq: sibling reporting mode; deliberately NOT added to
|
|
286
|
+
// the --all activation block below (same exclusion rationale as
|
|
287
|
+
// --check-rs-engine: --all is class A-M drift detection, not escalation).
|
|
288
|
+
else if (arg === '--report-registration-bug') flags.reportRegistrationBug = true;
|
|
289
|
+
else if (arg === '--post-update') flags.postUpdate = true;
|
|
290
|
+
else if (arg === '--dogfood-acceptance') flags.dogfoodAcceptance = true;
|
|
291
|
+
else if (arg === '--claims') flags.claims = true;
|
|
292
|
+
else if (arg === '--dry-run') flags.dryRun = true;
|
|
293
|
+
// Phase 194-07 (PSB-14 / D-10): --bind-check <roomDir> is a SIBLING flag (like
|
|
294
|
+
// --acceptance) with its OWN never-block exit-0 contract. It runs a LIGHTWEIGHT
|
|
295
|
+
// LOCAL structural room-health check + registers this session's presence at
|
|
296
|
+
// BIND-TIME (never per-turn, never a Brain call).
|
|
297
|
+
else if (arg === '--bind-check') flags.bindCheck = true;
|
|
298
|
+
else if (arg.startsWith('--bind-check=')) { flags.bindCheck = true; flags.bindCheckDir = arg.slice('--bind-check='.length); }
|
|
299
|
+
else if (arg === '--all') flags.all = true;
|
|
300
|
+
else if (arg.startsWith('--simulate-write=')) flags.simulateWrite = arg.slice('--simulate-write='.length);
|
|
301
|
+
else if (arg.startsWith('--scan-commands=')) flags.scanCommandsDir = arg.slice('--scan-commands='.length);
|
|
302
|
+
else if (arg.startsWith('--scan-scripts=')) flags.scanScriptsDir = arg.slice('--scan-scripts='.length);
|
|
303
|
+
else if (arg === '--help' || arg === '-h') {
|
|
304
|
+
console.log(usageText());
|
|
305
|
+
process.exit(0);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
// --all activates every class flag.
|
|
309
|
+
if (flags.all) {
|
|
310
|
+
flags.cascadeRooms = true;
|
|
311
|
+
flags.verifySurface = true;
|
|
312
|
+
flags.roomMd = true;
|
|
313
|
+
flags.uiCompliance = true;
|
|
314
|
+
flags.statuslineVisibility = true;
|
|
315
|
+
flags.installState = true;
|
|
316
|
+
flags.staleFirstTouch = true;
|
|
317
|
+
flags.deprecatedUsage = true;
|
|
318
|
+
flags.brainSmoke = true;
|
|
319
|
+
flags.cardFireHealth = true;
|
|
320
|
+
flags.graphDeriveHealth = true;
|
|
321
|
+
}
|
|
322
|
+
// --pre-tag implies --acceptance (convenience; running --pre-tag standalone
|
|
323
|
+
// is meaningless).
|
|
324
|
+
if (flags.preTag) flags.acceptance = true;
|
|
325
|
+
// --pre-flight implies --acceptance for the same reason. If both --pre-tag
|
|
326
|
+
// and --pre-flight are passed, --pre-tag wins downstream (the mode resolver
|
|
327
|
+
// checks preTag first); --pre-flight is the strict-subset tier release.sh
|
|
328
|
+
// Step 2.5 uses BEFORE Step 3 mutates the tree.
|
|
329
|
+
if (flags.preFlight) flags.acceptance = true;
|
|
330
|
+
// Phase 194-07: capture the space-separated `--bind-check <roomDir>` value (the
|
|
331
|
+
// `=` form is captured inline above). The value is the arg following the flag,
|
|
332
|
+
// provided it is not itself a flag.
|
|
333
|
+
if (flags.bindCheck && !flags.bindCheckDir) {
|
|
334
|
+
const bcIdx = argv.indexOf('--bind-check');
|
|
335
|
+
if (bcIdx !== -1 && typeof argv[bcIdx + 1] === 'string' && !argv[bcIdx + 1].startsWith('--')) {
|
|
336
|
+
flags.bindCheckDir = argv[bcIdx + 1];
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return flags;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function usageText() {
|
|
343
|
+
return `Usage: doctor.cjs [flags]
|
|
344
|
+
|
|
345
|
+
Default (no flag) runs class A install-cache check + class N plugin-enabled-state
|
|
346
|
+
(the silent-disable detector -- runs on a bare doctor run because the plugin's own
|
|
347
|
+
SessionStart hooks cannot fire while the plugin is DISABLED).
|
|
348
|
+
|
|
349
|
+
Class flags (combine freely; --all activates them all):
|
|
350
|
+
--cascade-rooms class B (.room-root sentinel) + class C (active-room guard silence)
|
|
351
|
+
--graph-derive-health graph-derive-health (RCA 4d): did this room's semantic edge layer ever
|
|
352
|
+
land? Reports FAIL when BELONGS_TO edges exist with zero cascade edges,
|
|
353
|
+
WARN on a derive queue entry stuck past 3 days or a recorded failure log.
|
|
354
|
+
Active room by default; add --cascade-rooms to sweep every room.
|
|
355
|
+
--heal-room sugar for --graph-derive-health --fix: re-enqueues every WARN/FAIL room
|
|
356
|
+
through the existing dedup-by-roomDir derive sweep (idempotent).
|
|
357
|
+
--verify-surface class D (live cascade end-to-end against test fixture)
|
|
358
|
+
--room-md class E (ROOM.md/MINTO.md presence under .room-root)
|
|
359
|
+
--ui-compliance class F (UI Ruling System scan)
|
|
360
|
+
--statusline-visibility class G (user-settings drift, plugin install integrity, statusline-mos isolated execution)
|
|
361
|
+
+ class H (install-incomplete: missing statusLine block and/or a halted .install-receipt.json)
|
|
362
|
+
--card-fire-health card-fire-health (D-05): health of the check-card-fire.cjs self-diagnostic
|
|
363
|
+
instrument -- intercept log exists/valid-JSONL/fresh (~/.mindrian/card-fire-
|
|
364
|
+
intercepts.log), classifier/counter library seams intact, render-coverage
|
|
365
|
+
registry parses, session store readable. cadence:always, LOCAL-only, no fix.
|
|
366
|
+
--install-state class I (install-state + topology + 6-way version-of-record consistency)
|
|
367
|
+
+ class J (deployment-surface manifest reconciliation)
|
|
368
|
+
--stale-first-touch class K (stale-first-touch-copy: stale version literals + em-dash violations
|
|
369
|
+
in greeting surfaces declared by data/first-touch-surfaces.json; SEED-007 absorption)
|
|
370
|
+
--deprecated-usage class L (deprecated commands you still use: scans last-7-days session
|
|
371
|
+
transcripts at ~/.claude/projects/.../*.jsonl for /mos:<deprecated>
|
|
372
|
+
patterns; surfaces a per-command "use /mos:<new> instead" hint.
|
|
373
|
+
Phase 121.5-08 Sub-plan J. LOCAL-only, zero network.)
|
|
374
|
+
--brain-smoke class M (Brain end-to-end smoke: 6-layer probe -- plugin root,
|
|
375
|
+
key resolver, HTTPS schema, MCP stdio handshake, e2e brain_schema,
|
|
376
|
+
store identity (stale-replica detection, quick 260819-c9b).
|
|
377
|
+
Diagnostic-only; reports the exact failing layer. Phase 127-02.)
|
|
378
|
+
--eureka-smoke class S (Eureka local-embedding-stack smoke: 4-layer probe --
|
|
379
|
+
deps present, vec backend, model cache, graceful degrade.
|
|
380
|
+
NON-cascading; never downloads a model unless
|
|
381
|
+
MINDRIAN_EUREKA_SMOKE_ALLOW_DOWNLOAD=1; rolls into --acceptance.
|
|
382
|
+
Diagnostic-only; reports the exact stopping layer. quick 260706-13z.)
|
|
383
|
+
class N plugin-enabled-state (DRIFT-12 / silent-disable detector): reads
|
|
384
|
+
~/.claude/settings.json enabledPlugins + installed_plugins.json.
|
|
385
|
+
installed && enabled===false -> CRITICAL (re-enable hint); enabled
|
|
386
|
+
===true or absent -> OK. Runs on a bare doctor run AND under --all (NOT
|
|
387
|
+
behind a flag -- a disabled plugin cannot run its own hooks). LOCAL
|
|
388
|
+
read only, never writes settings.json.
|
|
389
|
+
--drift class P (prose-vs-code drift, REPORT-ONLY -- wraps
|
|
390
|
+
check-skill-vs-code-drift + check-first-touch-drift; never
|
|
391
|
+
edits prose, even under --fix) + class Q (gsd-record drift --
|
|
392
|
+
shells out to gsd-tools validate health --raw, parses W007
|
|
393
|
+
ROADMAP gaps + I001 missing SUMMARYs). With --fix, writes the
|
|
394
|
+
DRIFT.md baseline (root index + per-folder detail) and stubs
|
|
395
|
+
missing SUMMARYs (mechanical heal); prose stays report-only.
|
|
396
|
+
OPT-IN ONLY: independent of --all, NOT in the session-start
|
|
397
|
+
roster. Release/CI invokes it explicitly. LOCAL-only, zero
|
|
398
|
+
network. Phase 150.9 (FIX-13).
|
|
399
|
+
+ class R (runtime-reachability drift -- the RUNTIME
|
|
400
|
+
assertion P/Q lack: FAILS NON-ZERO when a capability is
|
|
401
|
+
WIRED in the connector registry but UNREACHABLE by
|
|
402
|
+
decide() at runtime via the Phase-184 projection reader;
|
|
403
|
+
deterministic, LOCAL-only, zero network. Phase 185
|
|
404
|
+
DRIFT-01).
|
|
405
|
+
--all activate all class flags
|
|
406
|
+
|
|
407
|
+
Bind-time lifecycle job (Phase 194-07 -- separate from class flags):
|
|
408
|
+
--bind-check <roomDir> PSB-14 / D-10: a LIGHTWEIGHT LOCAL room-health check run
|
|
409
|
+
at BIND-TIME (never per-turn, never a Brain call). Verifies
|
|
410
|
+
the room dir exists + carries a .room-root sentinel +
|
|
411
|
+
.mindrian/room.db opens with nodes/edges tables, and on a
|
|
412
|
+
green report registers this session in the room's presence
|
|
413
|
+
ledger (lib/core/session-presence.cjs). Rides the stale-reap
|
|
414
|
+
cadence (dead-pid or mtime>5m presence files are reaped; a
|
|
415
|
+
live pid never). NEVER-BLOCK: an unhealthy room degrades to an
|
|
416
|
+
advisory and STILL exits 0 (binding is never hard-blocked).
|
|
417
|
+
Also appends a WATCH-only WARN finding row (Phase 225-02) when
|
|
418
|
+
the bundled SQLite is inside the upstream WAL-reset corruption
|
|
419
|
+
window (< 3.51.3, fixed upstream in commit 298a1c84) AND a live
|
|
420
|
+
co-session is present in this room -- detect-only, an
|
|
421
|
+
environment condition the user cannot repair locally; never
|
|
422
|
+
touches report.healthy or the exit code (still never-block).
|
|
423
|
+
|
|
424
|
+
Release-gate runner (Phase 123 Plan-04 -- separate from class flags):
|
|
425
|
+
--acceptance run the 7-point release-gate checklist (install-state +
|
|
426
|
+
deployment-surfaces + version-of-record-repo + verify-release +
|
|
427
|
+
doctor-all + version-of-record-published + npx-roundtrip).
|
|
428
|
+
Hard abort (exit non-zero) on any sub-check failure.
|
|
429
|
+
NO --allow override -- release infra is the one gate you cannot skip.
|
|
430
|
+
--pre-tag with --acceptance: filter to the 5 pre-tag-applicable points
|
|
431
|
+
(skips version-of-record-published + npx-roundtrip which are
|
|
432
|
+
false until AFTER the tag + npm publish). --pre-tag IMPLIES
|
|
433
|
+
--acceptance. release.sh Step 6.6 runs this BEFORE git tag.
|
|
434
|
+
--pre-flight with --acceptance: filter to pre-flight-applicable points
|
|
435
|
+
(strict subset of --pre-tag minus in-flight-incompatible
|
|
436
|
+
checks like verify-release-clean-tree). Runs BEFORE release.sh
|
|
437
|
+
mutates plugin.json / package.json / CHANGELOG. Mutually
|
|
438
|
+
exclusive with --pre-tag; if both pass, --pre-tag wins.
|
|
439
|
+
--light-npx with --acceptance (full): skip the mktemp HOME-override sandbox
|
|
440
|
+
in the npx-roundtrip point; resolve npx --no-install --help
|
|
441
|
+
instead of doing a live install. Operator opt-in.
|
|
442
|
+
|
|
443
|
+
Environment readiness probes (Phase 127.2 Plan 03 -- separate from class flags):
|
|
444
|
+
--check-rs-engine pre-flight Python deps reachable from scripts/rs-engine.py
|
|
445
|
+
(the Engine 1 Act 1 surface for /mos:find-bottlenecks et al.).
|
|
446
|
+
Probes top-level imports against the runtime's python (python3
|
|
447
|
+
preferred, python fallback). On missing deps emits an actionable
|
|
448
|
+
fix line: "Run: pip install -r requirements-hsi.txt --user".
|
|
449
|
+
Closes the Windows tester 2026-05-23 silent-failure class.
|
|
450
|
+
Exit 0 = deps present; exit 1 = deps missing (own contract,
|
|
451
|
+
NOT the class-flag-always-exit-0 invariant).
|
|
452
|
+
--report-registration-bug (Quick task 260705-jeq) READ-ONLY escalation reporter:
|
|
453
|
+
proves every locally-checkable cause of "valid install yet a
|
|
454
|
+
command did not register" is CLEAN (install-cache drift,
|
|
455
|
+
enabledPlugins silent-disable, legacy config.json pin,
|
|
456
|
+
marketplace clone git-dirt, version-of-record legs, on-disk
|
|
457
|
+
command count, static precondition sweep), then assembles a
|
|
458
|
+
paste-ready Anthropic bug report. Diagnostic-only: never a
|
|
459
|
+
--fix, never claims a status; the root cause is a confirmed
|
|
460
|
+
host-side core bug. stdout only; --json for the machine shape.
|
|
461
|
+
Exit 0 whenever the report assembles (even offline); non-zero
|
|
462
|
+
only if the assembler itself throws. NOT in --all.
|
|
463
|
+
--post-update (Phase 127.2 Plan 04 Instance #7) atomically activate
|
|
464
|
+
freshly-landed cache-staging bytes via scripts/post-update-
|
|
465
|
+
activation.cjs (delegates to --fix pipeline + writes the
|
|
466
|
+
~/.mindrian/post-update-restart-pending touch-file). Wired
|
|
467
|
+
into /mos:update Step 7 + composable as --fix --post-update.
|
|
468
|
+
Exit 0 = activated or already on latest; exit 1 = swap failed.
|
|
469
|
+
|
|
470
|
+
Loop-fires gate (Phase 146 -- the milestone gate; separate from class flags):
|
|
471
|
+
--dogfood-acceptance run the 6 ACPT dogfood drivers (engine-fires,
|
|
472
|
+
websearch-hat-scoped, first-material-explore,
|
|
473
|
+
filing-cascade-surfaces, brain-derive-tier-rise,
|
|
474
|
+
dial-atomic-emission) as
|
|
475
|
+
child processes and aggregate their exit codes. This is
|
|
476
|
+
the Phase 146 loop-fires gate: it certifies the loop
|
|
477
|
+
FIRES across the dogfood criteria. It has its OWN
|
|
478
|
+
exit-code contract (0 = all 6 ACPT hermetic legs
|
|
479
|
+
passed; non-zero = a leg failed) and is NOT a class
|
|
480
|
+
flag -- the class-flag-always-exit-0 invariant does NOT
|
|
481
|
+
apply. Per the ROADMAP Phase 146 mandate: exit 0 = the
|
|
482
|
+
milestone ships as "Larry Reaches"; non-zero = the
|
|
483
|
+
milestone is renamed. The ACPT-05 LIVE mode_a arm
|
|
484
|
+
self-skips when Brain is unreachable, so the gate is
|
|
485
|
+
green on the hermetic legs WITHOUT a reachable Brain --
|
|
486
|
+
the gate never requires Brain/web/Vercel. Composes with
|
|
487
|
+
tests/run-all-146.sh (the full-surface aggregator).
|
|
488
|
+
|
|
489
|
+
Claim harness gate (Phase 150 -- the public-site-claim gate; separate from class flags):
|
|
490
|
+
--claims run the claim harness (tests/claim-harness/run-all-
|
|
491
|
+
claims.sh): the C1..C7 falsifiable public-site-claim
|
|
492
|
+
drivers + the gates. A SIBLING of --acceptance: it has
|
|
493
|
+
its OWN exit-code contract (0 = every claim is true by
|
|
494
|
+
instrumentation; non-zero = a claim failed) and is NOT a
|
|
495
|
+
class flag -- the class-flag-always-exit-0 invariant does
|
|
496
|
+
NOT apply. Canon Part 8: the harness is LOCAL/hermetic;
|
|
497
|
+
the Brain LIVE arms self-skip via class-m-brain-smoke, so
|
|
498
|
+
the gate is green WITHOUT a reachable Brain. The semantic
|
|
499
|
+
claims (C2-good, C4-relevance) are carved to the Part-10
|
|
500
|
+
human empathy gate, never machine-faked. The
|
|
501
|
+
DOCTOR_CLAIM_FAIL_POINT env hook synthesizes a failure of
|
|
502
|
+
the gate for its own self-test.
|
|
503
|
+
|
|
504
|
+
Behavior flags:
|
|
505
|
+
--fix attempt auto-recovery for each class that supports it
|
|
506
|
+
(class H: re-stamps the canonical statusLine block when missing, idempotently)
|
|
507
|
+
--json machine-readable output (for hooks / regression tests)
|
|
508
|
+
--verbose, -v extra diagnostic detail
|
|
509
|
+
--simulate-write=<p> simulate a PWD write at <p> for class C active-room mismatch
|
|
510
|
+
--scan-commands=<d> override commands/ scan dir for class F (default: ./commands)
|
|
511
|
+
--scan-scripts=<d> override scripts/ scan dir for class F (default: ./scripts)
|
|
512
|
+
--help, -h print this usage
|
|
513
|
+
|
|
514
|
+
Exit codes:
|
|
515
|
+
0 healthy, no drift across activated classes; --acceptance: all points passed
|
|
516
|
+
1 drift detected (read-only mode); --acceptance: any point failed (hard abort)
|
|
517
|
+
2 drift detected and recovered (--fix mode)
|
|
518
|
+
3 internal error
|
|
519
|
+
`;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// -- Comparable semver helpers + diagnostic checks -------------------
|
|
523
|
+
// parseVersion, cmpVersion, checkInstallVersion, checkMarketplaceCache, and
|
|
524
|
+
// checkDevSourceConsistency moved to lib/core/doctor/shared.cjs (Phase 217
|
|
525
|
+
// Plan-01 D-02) and are imported at the top of this file.
|
|
526
|
+
|
|
527
|
+
// -- Recovery --------------------------------------------------------
|
|
528
|
+
// Phase 95.2 D-01..D-04: atomic-swap recovery.
|
|
529
|
+
// Pattern: cp -> verify -> two-step rename. Destructive ops only after
|
|
530
|
+
// non-destructive verification has passed. POSIX rename(2) is atomic on
|
|
531
|
+
// the same filesystem (~/.claude/plugins/ is single-mount in practice).
|
|
532
|
+
// For cross-fs (EXDEV) we fall back to cpSync+rmSync per safeRename().
|
|
533
|
+
|
|
534
|
+
function performRecoveryAtomic(installVersion, latestCacheVersion) {
|
|
535
|
+
const sourceDir = path.join(MARKETPLACE_CACHE_DIR, latestCacheVersion);
|
|
536
|
+
if (!fs.existsSync(sourceDir)) {
|
|
537
|
+
return { status: 'error', detail: `marketplace cache for ${latestCacheVersion} not found at ${sourceDir}`, stage: 'precheck' };
|
|
538
|
+
}
|
|
539
|
+
const installNewDir = INSTALL_DIR + '.new';
|
|
540
|
+
const ts = new Date().toISOString().replace(/[:.]/g, '').replace(/T/, '-').slice(0, 15);
|
|
541
|
+
// Finding F: 'missing' is more truthful than 'unknown' when the install dir doesn't exist.
|
|
542
|
+
const backupTag = installVersion || (fs.existsSync(INSTALL_DIR) ? 'unknown' : 'missing');
|
|
543
|
+
const backupDir = path.join(PLUGIN_HOME, `mindrian-os.stale-${backupTag}-${ts}`);
|
|
544
|
+
|
|
545
|
+
// Phase 1: prepare install.new (cleanup any stale install.new from a prior failed run -- recover-the-recoverer logic)
|
|
546
|
+
try {
|
|
547
|
+
if (fs.existsSync(installNewDir)) fs.rmSync(installNewDir, { recursive: true, force: true });
|
|
548
|
+
} catch (err) {
|
|
549
|
+
return { status: 'error', detail: `failed to clean prior install.new: ${err.message}`, stage: 'cleanup-installnew' };
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// Finding E: hermetic test injection point.
|
|
553
|
+
if (process.env.MOS_TEST_FORCE_FAIL === 'copy') {
|
|
554
|
+
return { status: 'error', detail: 'MOS_TEST_FORCE_FAIL=copy injection', stage: 'cp', installNewDir };
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// Phase 2: cp via fs.cpSync (Finding A; replaces the prior shell-out copy -- Windows-functional, no exec)
|
|
558
|
+
try {
|
|
559
|
+
fs.cpSync(sourceDir, installNewDir, { recursive: true, dereference: false, preserveTimestamps: true });
|
|
560
|
+
} catch (err) {
|
|
561
|
+
// install untouched; install.new may be incomplete -- leave for inspection.
|
|
562
|
+
return { status: 'error', detail: `cp failed: ${err.message}`, stage: 'cp', installNewDir };
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// Phase 3: structural version verify (D-02). Read plugin.json from install.new ONLY; do not load any plugin code.
|
|
566
|
+
if (process.env.MOS_TEST_FORCE_FAIL === 'verify') {
|
|
567
|
+
return { status: 'error', detail: 'MOS_TEST_FORCE_FAIL=verify injection', stage: 'verify', installNewDir };
|
|
568
|
+
}
|
|
569
|
+
try {
|
|
570
|
+
const pj = JSON.parse(fs.readFileSync(path.join(installNewDir, '.claude-plugin/plugin.json'), 'utf8'));
|
|
571
|
+
if (pj.version !== latestCacheVersion) {
|
|
572
|
+
return { status: 'error', detail: `verify failed: install.new version is ${pj.version}, expected ${latestCacheVersion}`, stage: 'verify', installNewDir };
|
|
573
|
+
}
|
|
574
|
+
} catch (err) {
|
|
575
|
+
return { status: 'error', detail: `verify failed: ${err.message}`, stage: 'verify', installNewDir };
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// Phase 4: two-step atomic-swap.
|
|
579
|
+
// Phase 4a: mv install -> install.stale-X (only if install exists). Finding D safeRename for cross-fs.
|
|
580
|
+
let backedUp = false;
|
|
581
|
+
if (fs.existsSync(INSTALL_DIR)) {
|
|
582
|
+
if (process.env.MOS_TEST_FORCE_FAIL === 'rename-old') {
|
|
583
|
+
return { status: 'error', detail: 'MOS_TEST_FORCE_FAIL=rename-old injection', stage: 'backup-mv', installNewDir };
|
|
584
|
+
}
|
|
585
|
+
try {
|
|
586
|
+
safeRename(INSTALL_DIR, backupDir);
|
|
587
|
+
backedUp = true;
|
|
588
|
+
} catch (err) {
|
|
589
|
+
// install untouched (rename didn't complete); install.new left for inspection.
|
|
590
|
+
return { status: 'error', detail: `backup mv failed: ${err.message}`, stage: 'backup-mv', installNewDir };
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// Phase 4b: mv install.new -> install. The atomic moment.
|
|
595
|
+
if (process.env.MOS_TEST_FORCE_FAIL === 'commit') {
|
|
596
|
+
// Simulate post-backup commit failure -> trigger D-03 rollback path.
|
|
597
|
+
if (backedUp) {
|
|
598
|
+
try { safeRename(backupDir, INSTALL_DIR); } catch (_) { /* double failure; user must recover manually */ }
|
|
599
|
+
}
|
|
600
|
+
return { status: 'error', detail: 'recovery failed -- live install restored from backup; investigate manually', stage: 'commit-rollback', exitCode: 4 };
|
|
601
|
+
}
|
|
602
|
+
try {
|
|
603
|
+
safeRename(installNewDir, INSTALL_DIR);
|
|
604
|
+
} catch (err) {
|
|
605
|
+
// D-03 rollback: restore from backup so live install is recovered to pre-recovery state.
|
|
606
|
+
if (backedUp) {
|
|
607
|
+
try { safeRename(backupDir, INSTALL_DIR); }
|
|
608
|
+
catch (_) { /* double failure; backup remains on disk for manual recovery */ }
|
|
609
|
+
}
|
|
610
|
+
return { status: 'error', detail: `recovery failed -- live install restored from backup; investigate manually (root: ${err.message})`, stage: 'commit-rollback', exitCode: 4 };
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// Phase 5: post-swap verification (cheap re-check; should-never-fail given Phase 3 already passed).
|
|
614
|
+
const after = checkInstallVersion();
|
|
615
|
+
if (after.status !== 'ok' || after.version !== latestCacheVersion) {
|
|
616
|
+
return { status: 'error', detail: `post-swap verify mismatch: install version is ${after.version || 'unknown'}, expected ${latestCacheVersion}`, backup: backedUp ? backupDir : null };
|
|
617
|
+
}
|
|
618
|
+
return { status: 'ok', backup: backedUp ? backupDir : null, recoveredVersion: latestCacheVersion };
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// Finding D: rename(2) is atomic on the same filesystem; cpSync+rmSync fallback for EXDEV.
|
|
622
|
+
function safeRename(src, dst) {
|
|
623
|
+
try {
|
|
624
|
+
fs.renameSync(src, dst);
|
|
625
|
+
} catch (err) {
|
|
626
|
+
if (err && err.code === 'EXDEV') {
|
|
627
|
+
// Cross-filesystem move; copy + delete (NOT atomic, but only path that works across mounts).
|
|
628
|
+
fs.cpSync(src, dst, { recursive: true, dereference: false, preserveTimestamps: true });
|
|
629
|
+
fs.rmSync(src, { recursive: true, force: true });
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
throw err;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// readRegistry moved to lib/core/doctor/shared.cjs (Phase 217 Plan-01 D-02) and
|
|
637
|
+
// is imported at the top of this file. The class B/C/E section-walk helpers
|
|
638
|
+
// (SKIP_DIRS, listSubdirs) + the generator spawn (invokeGenerator) + findRoomRoot
|
|
639
|
+
// moved WITH those checks into their registry runners in Phase 217 Plan 04.
|
|
640
|
+
|
|
641
|
+
// -- Class D / G / H bodies removed (Phase 217 Plan 05) -----------------------
|
|
642
|
+
//
|
|
643
|
+
// checkSurfaceVerification (class D), checkStatuslineVisibility +
|
|
644
|
+
// performStatuslineFix (class G), and checkInstallIncomplete + performClassHFix
|
|
645
|
+
// (class H) -- plus their private helpers (STALE_STATUSLINE_PATH_REGEX,
|
|
646
|
+
// stripAnsi, CLASS_H_CANONICAL_STEPS, classHActionString,
|
|
647
|
+
// userSettingsHasStatusLine) and the INSTALL_RECEIPT_JSON const -- moved into
|
|
648
|
+
// lib/core/doctor/verify-surface-module.cjs, statusline-visibility-module.cjs,
|
|
649
|
+
// and install-incomplete-module.cjs. The accumulative engine owns their
|
|
650
|
+
// dispatch + fix-then-recheck flow via the registry (data/doctor-modules.json).
|
|
651
|
+
|
|
652
|
+
// -- Class I + Class J: install-state + topology + version-of-record ---
|
|
653
|
+
// + deployment-surface manifest reconciliation (Phase 123 Plan-03).
|
|
654
|
+
//
|
|
655
|
+
// Class I (HARNESS-123-07): install-state record present + internally
|
|
656
|
+
// consistent; topology classification (Bug 7 fix); 6-way version-of-record
|
|
657
|
+
// equality across installed_plugins.json <-> record.active_version <->
|
|
658
|
+
// record.statusline_renders_version <-> record.last_version_file_value
|
|
659
|
+
// <-> record.path_bin_version <-> ~/.mindrian-last-version. STRING
|
|
660
|
+
// EQUALITY tolerates the 4-component non-semver 1.12.5.1 case.
|
|
661
|
+
//
|
|
662
|
+
// Class J (HARNESS-123-08): walks data/deployment-surfaces.json; per
|
|
663
|
+
// surface, checks expected vs observed per check_kind (marker | exact-
|
|
664
|
+
// value | observed-only); honors topology_scope='dev-clone' (skipped on
|
|
665
|
+
// non-dev-clone) and reconcile='never' (excluded from auto-fix); honors
|
|
666
|
+
// the schema extension `path_within_file` (added in Plan-03 to fix the
|
|
667
|
+
// live-dev-box settings.json whole-file-comparison bug).
|
|
668
|
+
//
|
|
669
|
+
// Aggressive --fix (HARNESS-123-09): missing record (spawn session-start);
|
|
670
|
+
// wrong ~/.mindrian-last-version (rewrite); legacy-clone migration
|
|
671
|
+
// (backup-verify-remove with the dev-clone safety belt -- NEVER touches
|
|
672
|
+
// a dev clone); conservative installed_plugins.json repair (back up
|
|
673
|
+
// first; repoint at newest valid marketplace-cache dir; restart note);
|
|
674
|
+
// flag-only for topology=='not-found' / vanished PATH-bin / wrong
|
|
675
|
+
// statusline_renders_version.
|
|
676
|
+
//
|
|
677
|
+
// Class-flag invariant: when --install-state (or --all) is set, exit is
|
|
678
|
+
// always 0 unless an explicit --fix attempt failed.
|
|
679
|
+
//
|
|
680
|
+
// Canon Part 8: all of class I + class J is LOCAL-only. Zero fetch / http /
|
|
681
|
+
// curl / brain.mindrian / tavily in any of these functions.
|
|
682
|
+
//
|
|
683
|
+
// Phase 217 Plan 06: the class I helpers + checkInstallState + performClassIFix
|
|
684
|
+
// moved to lib/core/doctor/install-state-module.cjs; the class J helpers +
|
|
685
|
+
// checkDeploymentSurfaces + performClassJFix moved to
|
|
686
|
+
// lib/core/doctor/deployment-surfaces-module.cjs; the shared readers
|
|
687
|
+
// (readLegacyConfigPin, detectMarketplaceCacheInstall, collectVersionOfRecord,
|
|
688
|
+
// computeVersionDivergences, ...) moved to lib/core/doctor/shared.cjs. Both are
|
|
689
|
+
// now registry-driven cadence:always runners under the --install-state flag
|
|
690
|
+
// (install-state before deployment-surfaces so class J reads class I's
|
|
691
|
+
// same-invocation result via ctx.checks). doctor.cjs imports only the runners'
|
|
692
|
+
// RAW check entries (checkInstallState / checkDeploymentSurfaces, bound near the
|
|
693
|
+
// shared.cjs require) for the --acceptance path.
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
// -- Acceptance: release-gate-as-a-command (Phase 123 Plan-04) -------
|
|
697
|
+
//
|
|
698
|
+
// `mindrian-os doctor --acceptance` runs a 7-point checklist that asserts the
|
|
699
|
+
// release is consistent end-to-end. `--pre-tag` filters to the 5 points true
|
|
700
|
+
// BEFORE the tag + npm publish lands. Both are HARD ABORTS -- no --allow
|
|
701
|
+
// override (per CONTEXT D-16: release infra is the one gate you cannot skip).
|
|
702
|
+
//
|
|
703
|
+
// THIS BLOCK IS NOT A CLASS FLAG. It has its own exit-code contract:
|
|
704
|
+
// 0 = all (filtered) points passed
|
|
705
|
+
// 1 = any point failed
|
|
706
|
+
// The class-flag-always-exit-0 graceful-degradation invariant does NOT apply.
|
|
707
|
+
//
|
|
708
|
+
// Canon Part 8 (LOCAL-only invariant): the npx-roundtrip + version-of-record-
|
|
709
|
+
// published points are the ONE network touch in this phase. They run ONLY
|
|
710
|
+
// during a release (when release.sh invokes --acceptance after the push).
|
|
711
|
+
// --pre-tag has ZERO network -- it stays Part-8 clean for in-session use.
|
|
712
|
+
//
|
|
713
|
+
// Test-mode env hooks (honored only when DOCTOR_TEST_MODE=1):
|
|
714
|
+
// DOCTOR_TEST_STUB_POST_PUBLISH=1 post-publish points throw if invoked
|
|
715
|
+
// (asserts the --pre-tag filter works)
|
|
716
|
+
// DOCTOR_TEST_FAIL_POINT=<point_id> synthesize a failure of the named point
|
|
717
|
+
// DOCTOR_VERIFY_RELEASE_PATH=<path> override scripts/verify-release path
|
|
718
|
+
|
|
719
|
+
// evaluateMarketplaceSourcePin(source, ver) -- pure helper, exported for
|
|
720
|
+
// hermetic tests (Phase 341 D-01/D-06/T-341-17). Compares a marketplace
|
|
721
|
+
// plugins[0].source object against the released version. Accepts EITHER
|
|
722
|
+
// shape during the git-to-npm transition:
|
|
723
|
+
// - npm shape (source.version present): source.source must be 'npm',
|
|
724
|
+
// source.package must be '@mindrian_os/cli', source.version must equal
|
|
725
|
+
// ver exactly (no v prefix), and NEITHER a residual 'ref' NOR a
|
|
726
|
+
// residual 'url' key may be present -- a stale ref/url sitting beside a
|
|
727
|
+
// correct version is exactly the half-migrated state D-01 warns about.
|
|
728
|
+
// - legacy git shape (source.ref present, no source.version): source.ref
|
|
729
|
+
// must equal 'v' + ver. Accepted only until the marketplace catalog's
|
|
730
|
+
// first post-341 release flips it to the npm shape (release.sh Step 4).
|
|
731
|
+
// Neither shape recognized (no version, no ref) is a failure.
|
|
732
|
+
function evaluateMarketplaceSourcePin(source, ver) {
|
|
733
|
+
if (!source || typeof source !== 'object') {
|
|
734
|
+
return { ok: false, finding: 'marketplace plugins[0].source is missing or not an object', detail: { source: source } };
|
|
735
|
+
}
|
|
736
|
+
const hasVersion = Object.prototype.hasOwnProperty.call(source, 'version');
|
|
737
|
+
const hasRef = Object.prototype.hasOwnProperty.call(source, 'ref');
|
|
738
|
+
const hasUrl = Object.prototype.hasOwnProperty.call(source, 'url');
|
|
739
|
+
|
|
740
|
+
if (hasVersion) {
|
|
741
|
+
// npm shape (or a half-migrated attempt at it).
|
|
742
|
+
if (hasRef || hasUrl) {
|
|
743
|
+
return {
|
|
744
|
+
ok: false,
|
|
745
|
+
finding: 'marketplace source carries a residual ref/url key alongside version (half-migrated D-01 state): ' + JSON.stringify(source),
|
|
746
|
+
detail: { source: source, residualRef: source.ref, residualUrl: source.url },
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
if (source.source !== 'npm' || source.package !== '@mindrian_os/cli') {
|
|
750
|
+
return {
|
|
751
|
+
ok: false,
|
|
752
|
+
finding: 'marketplace source is not the npm shape {source:npm, package:@mindrian_os/cli}: got ' + JSON.stringify(source),
|
|
753
|
+
detail: { source: source },
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
if (source.version !== ver) {
|
|
757
|
+
return { ok: false, finding: 'marketplace source.version is ' + source.version + ', expected ' + ver, detail: { version: source.version, expected: ver } };
|
|
758
|
+
}
|
|
759
|
+
return { ok: true, finding: null, detail: { shape: 'npm', version: source.version } };
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
if (hasRef) {
|
|
763
|
+
// Legacy git shape, accepted during the transition (the live
|
|
764
|
+
// marketplace has not flipped yet as of Phase 341 Plan 05).
|
|
765
|
+
const expectedRef = 'v' + ver;
|
|
766
|
+
if (source.ref !== expectedRef) {
|
|
767
|
+
return { ok: false, finding: 'marketplace source.ref is ' + source.ref + ', expected ' + expectedRef, detail: { ref: source.ref, expected: expectedRef } };
|
|
768
|
+
}
|
|
769
|
+
return { ok: true, finding: null, detail: { shape: 'git-legacy', ref: source.ref } };
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
return { ok: false, finding: 'marketplace source has neither a version nor a ref key: ' + JSON.stringify(source), detail: { source: source } };
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
function buildAcceptanceChecklist(ctx) {
|
|
776
|
+
const home = ctx.home;
|
|
777
|
+
const pluginRoot = ctx.pluginRoot;
|
|
778
|
+
const flagLightNpx = ctx.flagLightNpx;
|
|
779
|
+
const inTestMode = process.env.DOCTOR_TEST_MODE === '1';
|
|
780
|
+
return [
|
|
781
|
+
{
|
|
782
|
+
id: 'install-state',
|
|
783
|
+
label: 'install-state record present + snapshot matches a live spot-check',
|
|
784
|
+
severity: 'blocker',
|
|
785
|
+
applies_to: ['pre-tag', 'full'],
|
|
786
|
+
run: async function () {
|
|
787
|
+
if (inTestMode && process.env.DOCTOR_TEST_FAIL_POINT === 'install-state') {
|
|
788
|
+
return { ok: false, finding: 'install-state synthesized failure (test mode)', detail: {} };
|
|
789
|
+
}
|
|
790
|
+
const c = checkInstallState({ home: home });
|
|
791
|
+
// status: 'healthy' | 'warn' | 'error'. We allow 'healthy' through; anything
|
|
792
|
+
// else fails the gate. (Findings drive the finding string for the operator.)
|
|
793
|
+
const ok = c.status === 'healthy';
|
|
794
|
+
let finding = null;
|
|
795
|
+
if (!ok) {
|
|
796
|
+
if (Array.isArray(c.findings) && c.findings.length) {
|
|
797
|
+
finding = c.findings[0].finding || c.findings[0].id || ('install-state status: ' + c.status);
|
|
798
|
+
} else {
|
|
799
|
+
finding = 'install-state status: ' + c.status;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
return { ok: ok, finding: finding, detail: { status: c.status, findings: c.findings, topology: c.topology } };
|
|
803
|
+
},
|
|
804
|
+
},
|
|
805
|
+
{
|
|
806
|
+
id: 'deployment-surfaces',
|
|
807
|
+
label: 'every owned deployment surface reconciled',
|
|
808
|
+
severity: 'blocker',
|
|
809
|
+
applies_to: ['pre-tag', 'full'],
|
|
810
|
+
run: async function () {
|
|
811
|
+
if (inTestMode && process.env.DOCTOR_TEST_FAIL_POINT === 'deployment-surfaces') {
|
|
812
|
+
return { ok: false, finding: 'deployment-surfaces synthesized failure (test mode)', detail: {} };
|
|
813
|
+
}
|
|
814
|
+
// Reuse install-state for the topology + active root/version handles.
|
|
815
|
+
const s = checkInstallState({ home: home });
|
|
816
|
+
const activeRoot = (s.record && s.record.active_root)
|
|
817
|
+
|| (s.resolver && s.resolver.root)
|
|
818
|
+
|| null;
|
|
819
|
+
const activeVersion = (s.record && s.record.active_version)
|
|
820
|
+
|| (function () { try { return readInstalledPluginsVersion(home); } catch (_) { return null; } })();
|
|
821
|
+
const c = checkDeploymentSurfaces({
|
|
822
|
+
home: home,
|
|
823
|
+
topology: s.topology || 'not-found',
|
|
824
|
+
activeRoot: activeRoot,
|
|
825
|
+
activeVersion: activeVersion,
|
|
826
|
+
});
|
|
827
|
+
const ok = c.status === 'healthy' || c.status === 'skipped';
|
|
828
|
+
const failedSurfaces = Array.isArray(c.surfaces)
|
|
829
|
+
? c.surfaces.filter(function (x) { return x.ok === false; })
|
|
830
|
+
: [];
|
|
831
|
+
const finding = ok ? null : (failedSurfaces.length + ' surface(s) drifted');
|
|
832
|
+
return { ok: ok, finding: finding, detail: { status: c.status, surfaces: c.surfaces } };
|
|
833
|
+
},
|
|
834
|
+
},
|
|
835
|
+
{
|
|
836
|
+
id: 'version-of-record-repo',
|
|
837
|
+
label: 'plugin.json / package.json / CHANGELOG top entry consistent (repo-file half of 5-way)',
|
|
838
|
+
severity: 'blocker',
|
|
839
|
+
applies_to: ['pre-tag', 'full'],
|
|
840
|
+
run: async function () {
|
|
841
|
+
if (inTestMode && process.env.DOCTOR_TEST_FAIL_POINT === 'version-of-record-repo') {
|
|
842
|
+
return { ok: false, finding: 'version-of-record-repo synthesized failure (test mode)', detail: {} };
|
|
843
|
+
}
|
|
844
|
+
try {
|
|
845
|
+
const pjPath = path.join(pluginRoot, '.claude-plugin', 'plugin.json');
|
|
846
|
+
const pkPath = path.join(pluginRoot, 'package.json');
|
|
847
|
+
const chPath = path.join(pluginRoot, 'CHANGELOG.md');
|
|
848
|
+
const pj = JSON.parse(fs.readFileSync(pjPath, 'utf8'));
|
|
849
|
+
const pk = JSON.parse(fs.readFileSync(pkPath, 'utf8'));
|
|
850
|
+
const ch = fs.readFileSync(chPath, 'utf8');
|
|
851
|
+
const chMatch = ch.match(/^## \[([^\]]+)\]/m);
|
|
852
|
+
const chTop = chMatch ? chMatch[1] : null;
|
|
853
|
+
const pjVer = pj.version;
|
|
854
|
+
const pkVer = pk.version;
|
|
855
|
+
// Accept "Unreleased" CHANGELOG heading (work in progress); the
|
|
856
|
+
// sub-check passes if pj==pk AND CHANGELOG either matches OR is
|
|
857
|
+
// an [Unreleased] heading naming the same version in its body
|
|
858
|
+
// (the release.sh Step 6 finalizes [Unreleased] -> [vN] before
|
|
859
|
+
// Step 6.6 calls us, so by then chTop is the version).
|
|
860
|
+
const allMatch = pjVer === pkVer && (pjVer === chTop || chTop === 'Unreleased');
|
|
861
|
+
const finding = allMatch ? null : ('version mismatch: plugin.json=' + pjVer + ', package.json=' + pkVer + ', CHANGELOG top=' + chTop);
|
|
862
|
+
return { ok: !!allMatch, finding: finding, detail: { pluginJson: pjVer, packageJson: pkVer, changelogTop: chTop } };
|
|
863
|
+
} catch (e) {
|
|
864
|
+
return { ok: false, finding: 'version-of-record-repo read failed: ' + e.message, detail: {} };
|
|
865
|
+
}
|
|
866
|
+
},
|
|
867
|
+
},
|
|
868
|
+
{
|
|
869
|
+
id: 'verify-release',
|
|
870
|
+
label: 'scripts/verify-release passes',
|
|
871
|
+
severity: 'blocker',
|
|
872
|
+
// PRE-TAG ONLY. Excluded from 'full' (post-publish) on purpose:
|
|
873
|
+
// release.sh Step 7.5 (Commit B) bumps the working tree to the next dev
|
|
874
|
+
// pre-release (beta.N+1) while the marketplace correctly stays at the
|
|
875
|
+
// published beta.N. scripts/verify-release Check 3 is a STRICT
|
|
876
|
+
// plugin==marketplace equality, so under 'full' it would compare
|
|
877
|
+
// beta.N+1 vs beta.N and false-fail (the Step 9.8 false abort, RCA
|
|
878
|
+
// 260605-h3p). Post-publish version state is OWNED by the
|
|
879
|
+
// 'version-of-record-published' module below (tag + marketplace
|
|
880
|
+
// source.ref + npm view, keyed off the LAST SHIPPED tag, not the dev
|
|
881
|
+
// placeholder). scripts/verify-release stays the strict PRE-tag gate
|
|
882
|
+
// (release.sh Step 2 standalone + the --pre-tag acceptance tier).
|
|
883
|
+
// Precedent: the 'verify-release-clean-tree' sibling was tier-moved
|
|
884
|
+
// from ['pre-tag','full'] to ['full'] for the mirror-image reason
|
|
885
|
+
// (see tests/test-doctor-acceptance-preflight-checks.cjs:233).
|
|
886
|
+
applies_to: ['pre-tag'],
|
|
887
|
+
run: async function () {
|
|
888
|
+
if (inTestMode && process.env.DOCTOR_TEST_FAIL_POINT === 'verify-release') {
|
|
889
|
+
return { ok: false, finding: 'verify-release synthesized failure (test mode)', detail: {} };
|
|
890
|
+
}
|
|
891
|
+
// Override path only honored in test mode (the test harness sets a logging shim).
|
|
892
|
+
const verifyPath = (inTestMode && process.env.DOCTOR_VERIFY_RELEASE_PATH)
|
|
893
|
+
? process.env.DOCTOR_VERIFY_RELEASE_PATH
|
|
894
|
+
: path.join(pluginRoot, 'scripts', 'verify-release');
|
|
895
|
+
const cp = require('child_process');
|
|
896
|
+
const r = cp.spawnSync('bash', [verifyPath], { encoding: 'utf8', timeout: 60000 });
|
|
897
|
+
const ok = r.status === 0;
|
|
898
|
+
const finding = ok ? null : ('verify-release exited ' + r.status);
|
|
899
|
+
return { ok: ok, finding: finding, detail: { status: r.status, stdoutTail: (r.stdout || '').slice(-500), stderrTail: (r.stderr || '').slice(-500) } };
|
|
900
|
+
},
|
|
901
|
+
},
|
|
902
|
+
{
|
|
903
|
+
id: 'version-of-record-published',
|
|
904
|
+
label: 'git tag exists + marketplace source.version pinned + npm view returns the version',
|
|
905
|
+
severity: 'blocker',
|
|
906
|
+
applies_to: ['full'],
|
|
907
|
+
run: async function () {
|
|
908
|
+
if (inTestMode && process.env.DOCTOR_TEST_STUB_POST_PUBLISH === '1') {
|
|
909
|
+
throw new Error('post-publish stub invoked under --pre-tag -- bug in runner');
|
|
910
|
+
}
|
|
911
|
+
if (inTestMode && process.env.DOCTOR_TEST_FAIL_POINT === 'version-of-record-published') {
|
|
912
|
+
return { ok: false, finding: 'version-of-record-published synthesized failure (test mode)', detail: {} };
|
|
913
|
+
}
|
|
914
|
+
const cp = require('child_process');
|
|
915
|
+
try {
|
|
916
|
+
// Phase 127.2 Plan 02 Finding B: resolve "version of record" via
|
|
917
|
+
// last shipped git tag, NOT plugin.json. release.sh Commit B
|
|
918
|
+
// bumps main HEAD to the next pre-release placeholder (e.g.
|
|
919
|
+
// beta.27 after beta.26 ships), which by definition has no tag
|
|
920
|
+
// yet. The gate must verify the LAST SHIPPED version is live
|
|
921
|
+
// across all three records (tag + marketplace source + npm), not
|
|
922
|
+
// the placeholder.
|
|
923
|
+
const tagProbe = cp.spawnSync('git', ['-C', pluginRoot, 'describe', '--tags', '--abbrev=0', '--match=v*'], { encoding: 'utf8' });
|
|
924
|
+
let ver;
|
|
925
|
+
if (tagProbe.status === 0 && tagProbe.stdout && tagProbe.stdout.trim()) {
|
|
926
|
+
ver = tagProbe.stdout.trim().replace(/^v/, '');
|
|
927
|
+
} else {
|
|
928
|
+
// Fresh repo edge case (no tags yet): fall back to plugin.json.
|
|
929
|
+
const pj = JSON.parse(fs.readFileSync(path.join(pluginRoot, '.claude-plugin', 'plugin.json'), 'utf8'));
|
|
930
|
+
ver = pj.version;
|
|
931
|
+
}
|
|
932
|
+
// (a) git tag exists, reachable from origin/main.
|
|
933
|
+
const t = cp.spawnSync('git', ['-C', pluginRoot, 'rev-parse', '--verify', 'refs/tags/v' + ver], { encoding: 'utf8' });
|
|
934
|
+
if (t.status !== 0) return { ok: false, finding: 'git tag v' + ver + ' not found', detail: { stderr: (t.stderr || '').slice(-200) } };
|
|
935
|
+
// (b) marketplace source.version pinned to <ver> (D-01/D-06, npm
|
|
936
|
+
// source; no v prefix). During the git-to-npm transition this also
|
|
937
|
+
// accepts the legacy source.ref === 'v'+ver shape (the LIVE
|
|
938
|
+
// marketplace has not flipped yet as of this phase), via the pure
|
|
939
|
+
// helper below so a fixture-driven unit test can exercise every
|
|
940
|
+
// branch without touching the real marketplace.json.
|
|
941
|
+
const mpPath = path.join(home, 'mindrian-marketplace', '.claude-plugin', 'marketplace.json');
|
|
942
|
+
let mp;
|
|
943
|
+
try { mp = JSON.parse(fs.readFileSync(mpPath, 'utf8')); }
|
|
944
|
+
catch (e) { return { ok: false, finding: 'marketplace.json unreadable: ' + e.message, detail: { mpPath: mpPath } }; }
|
|
945
|
+
const source = mp.plugins && mp.plugins[0] && mp.plugins[0].source;
|
|
946
|
+
const sourceCheck = evaluateMarketplaceSourcePin(source, ver);
|
|
947
|
+
if (!sourceCheck.ok) return { ok: false, finding: sourceCheck.finding, detail: sourceCheck.detail };
|
|
948
|
+
// (c) npm view -- THIS IS THE ONE NETWORK CALL in this point.
|
|
949
|
+
const n = cp.spawnSync('npm', ['view', '@mindrian_os/cli@' + ver, 'version'], { encoding: 'utf8', timeout: 30000 });
|
|
950
|
+
if (n.status !== 0) return { ok: false, finding: 'npm view failed: ' + (n.stderr || '').slice(-200), detail: { status: n.status } };
|
|
951
|
+
const npmVer = (n.stdout || '').trim();
|
|
952
|
+
const ok = npmVer === ver;
|
|
953
|
+
return { ok: ok, finding: ok ? null : ('npm view returned ' + npmVer + ', expected ' + ver), detail: { npmVer: npmVer, expectedVer: ver } };
|
|
954
|
+
} catch (e) {
|
|
955
|
+
return { ok: false, finding: 'version-of-record-published threw: ' + e.message, detail: {} };
|
|
956
|
+
}
|
|
957
|
+
},
|
|
958
|
+
},
|
|
959
|
+
{
|
|
960
|
+
id: 'npx-roundtrip',
|
|
961
|
+
label: 'npx @mindrian_os/cli round-trip resolves cleanly',
|
|
962
|
+
severity: 'blocker',
|
|
963
|
+
applies_to: ['full'],
|
|
964
|
+
run: async function () {
|
|
965
|
+
if (inTestMode && process.env.DOCTOR_TEST_STUB_POST_PUBLISH === '1') {
|
|
966
|
+
throw new Error('post-publish stub invoked under --pre-tag -- bug in runner');
|
|
967
|
+
}
|
|
968
|
+
if (inTestMode && process.env.DOCTOR_TEST_FAIL_POINT === 'npx-roundtrip') {
|
|
969
|
+
return { ok: false, finding: 'npx-roundtrip synthesized failure (test mode)', detail: {} };
|
|
970
|
+
}
|
|
971
|
+
const cp = require('child_process');
|
|
972
|
+
// Phase 127.2 Plan 02 Finding B (mirrored): resolve target version
|
|
973
|
+
// via last shipped tag, not plugin.json placeholder. Same logic as
|
|
974
|
+
// version-of-record-published.
|
|
975
|
+
const tagProbe = cp.spawnSync('git', ['-C', pluginRoot, 'describe', '--tags', '--abbrev=0', '--match=v*'], { encoding: 'utf8' });
|
|
976
|
+
let ver;
|
|
977
|
+
if (tagProbe.status === 0 && tagProbe.stdout && tagProbe.stdout.trim()) {
|
|
978
|
+
ver = tagProbe.stdout.trim().replace(/^v/, '');
|
|
979
|
+
} else {
|
|
980
|
+
const pj = JSON.parse(fs.readFileSync(path.join(pluginRoot, '.claude-plugin', 'plugin.json'), 'utf8'));
|
|
981
|
+
ver = pj.version;
|
|
982
|
+
}
|
|
983
|
+
// RCA fix (release-step-9.7-npx-self-test-false-alarm, recommendation A,
|
|
984
|
+
// 2026-06-02): assert the published package INSTALLS + its bin is present
|
|
985
|
+
// and parseable, NOT the PATH-sensitive npx-by-name runtime exit. The
|
|
986
|
+
// launcher shells out to claude/git absent in the bare sandbox, and npm
|
|
987
|
+
// 10.9.7 npx-by-name does not reliably link the package bin -- so a
|
|
988
|
+
// perfectly healthy publish exits non-zero for an environment reason
|
|
989
|
+
// (the false alarm that blocked every cut since beta.37). `npm install`
|
|
990
|
+
// is the path that actually works; bin presence + `node --check` + the
|
|
991
|
+
// .bin symlink is the true publishable+installable signal.
|
|
992
|
+
const sandbox = fs.mkdtempSync(path.join(os.tmpdir(), 'mos-acceptance-'));
|
|
993
|
+
try {
|
|
994
|
+
const env = Object.assign({}, process.env, {
|
|
995
|
+
HOME: sandbox,
|
|
996
|
+
USERPROFILE: sandbox,
|
|
997
|
+
npm_config_cache: path.join(sandbox, '.npm'),
|
|
998
|
+
});
|
|
999
|
+
if (flagLightNpx) {
|
|
1000
|
+
// Light path: metadata-only -- assert the published bin is declared. No install.
|
|
1001
|
+
const v = cp.spawnSync('npm', ['view', '@mindrian_os/cli@' + ver, 'bin.mindrian-os'], { encoding: 'utf8', env: env, timeout: 30000 });
|
|
1002
|
+
const declared = (v.stdout || '').trim();
|
|
1003
|
+
const ok = v.status === 0 && declared.length > 0;
|
|
1004
|
+
return { ok: ok, finding: ok ? null : ('npm view bin.mindrian-os empty/failed (' + v.status + ')'), detail: { declared: declared, status: v.status } };
|
|
1005
|
+
}
|
|
1006
|
+
cp.spawnSync('npm', ['init', '-y'], { cwd: sandbox, env: env, encoding: 'utf8', timeout: 30000 });
|
|
1007
|
+
const inst = cp.spawnSync('npm', ['install', '--prefer-online', '@mindrian_os/cli@' + ver], { cwd: sandbox, env: env, encoding: 'utf8', timeout: 120000 });
|
|
1008
|
+
if (inst.status !== 0) {
|
|
1009
|
+
return { ok: false, finding: ('npm install @mindrian_os/cli@' + ver + ' failed (' + inst.status + ')'), detail: { status: inst.status, stderrTail: (inst.stderr || '').slice(-300) } };
|
|
1010
|
+
}
|
|
1011
|
+
const binFile = path.join(sandbox, 'node_modules', '@mindrian_os', 'cli', 'bin', 'cli.js');
|
|
1012
|
+
const binLink = path.join(sandbox, 'node_modules', '.bin', 'mindrian-os');
|
|
1013
|
+
const present = fs.existsSync(binFile);
|
|
1014
|
+
const parses = present && cp.spawnSync(process.execPath, ['--check', binFile], { encoding: 'utf8' }).status === 0;
|
|
1015
|
+
const linked = fs.existsSync(binLink);
|
|
1016
|
+
const ok = present && parses && linked;
|
|
1017
|
+
return {
|
|
1018
|
+
ok: ok,
|
|
1019
|
+
finding: ok ? null : ('published package install verification failed (present=' + present + ' parses=' + parses + ' linked=' + linked + ')'),
|
|
1020
|
+
detail: { present: present, parses: parses, linked: linked },
|
|
1021
|
+
};
|
|
1022
|
+
} finally {
|
|
1023
|
+
try { fs.rmSync(sandbox, { recursive: true, force: true }); } catch (_) { /* best-effort */ }
|
|
1024
|
+
}
|
|
1025
|
+
},
|
|
1026
|
+
},
|
|
1027
|
+
{
|
|
1028
|
+
id: 'doctor-all',
|
|
1029
|
+
label: 'doctor --all exits 0',
|
|
1030
|
+
severity: 'blocker',
|
|
1031
|
+
applies_to: ['pre-tag', 'full'],
|
|
1032
|
+
run: async function () {
|
|
1033
|
+
if (inTestMode && process.env.DOCTOR_TEST_FAIL_POINT === 'doctor-all') {
|
|
1034
|
+
return { ok: false, finding: 'doctor-all synthesized failure (test mode)', detail: {} };
|
|
1035
|
+
}
|
|
1036
|
+
// Spawn ourselves with --all (NOT --acceptance, to avoid recursion).
|
|
1037
|
+
// Scrub DOCTOR_TEST_FAIL_POINT so a top-level test fail-point env var
|
|
1038
|
+
// doesn't propagate into our self-spawn and re-trigger the synthesis
|
|
1039
|
+
// there (the synthesis above already covers this point).
|
|
1040
|
+
const cp = require('child_process');
|
|
1041
|
+
const childEnv = Object.assign({}, process.env, { DOCTOR_TEST_FAIL_POINT: '' });
|
|
1042
|
+
const r = cp.spawnSync('node', [path.join(pluginRoot, 'scripts', 'doctor.cjs'), '--all', '--json'], { encoding: 'utf8', timeout: 60000, env: childEnv });
|
|
1043
|
+
let j = null;
|
|
1044
|
+
try {
|
|
1045
|
+
if (r.stdout) {
|
|
1046
|
+
const start = r.stdout.indexOf('{');
|
|
1047
|
+
if (start !== -1) j = JSON.parse(r.stdout.slice(start));
|
|
1048
|
+
}
|
|
1049
|
+
} catch (_) { /* leave null */ }
|
|
1050
|
+
const driftCount = j && j.summary && typeof j.summary.drift === 'number' ? j.summary.drift : 0;
|
|
1051
|
+
const ok = r.status === 0 && driftCount === 0;
|
|
1052
|
+
let finding = null;
|
|
1053
|
+
if (r.status !== 0) finding = 'doctor --all exited ' + r.status;
|
|
1054
|
+
else if (driftCount > 0) finding = 'doctor --all reports ' + driftCount + ' drift item(s)';
|
|
1055
|
+
return { ok: ok, finding: finding, detail: { exit: r.status, summary: j && j.summary } };
|
|
1056
|
+
},
|
|
1057
|
+
},
|
|
1058
|
+
{
|
|
1059
|
+
// Phase 172-13 (Canon Part 11 R2/R9/INV-10 step 3): the born-wired coverage
|
|
1060
|
+
// gate as a release acceptance point. Runs BOTH --check gates -- the
|
|
1061
|
+
// connector registry (a surface neither WIRED nor EXCLUDED is a hard fail)
|
|
1062
|
+
// and the orchestration projection (a bare command counterpart neither
|
|
1063
|
+
// ranked nor excluded is a hard fail). A release with an accidental-dark
|
|
1064
|
+
// surface aborts here. Canon Part 8: both --check gates regenerate in
|
|
1065
|
+
// memory from LOCAL sources; zero Brain / network.
|
|
1066
|
+
id: 'coverage-gate',
|
|
1067
|
+
label: 'connector + orchestration-projection + framework-vocabulary + render-coverage + skill-mirrors gates pass (no dark surface); shape-declaration and tool-honesty are advisory (WARNs inline, never blocks)',
|
|
1068
|
+
severity: 'blocker',
|
|
1069
|
+
applies_to: ['pre-tag', 'full'],
|
|
1070
|
+
run: async function () {
|
|
1071
|
+
if (inTestMode && process.env.DOCTOR_TEST_FAIL_POINT === 'coverage-gate') {
|
|
1072
|
+
return { ok: false, finding: 'coverage-gate synthesized failure (test mode)', detail: {} };
|
|
1073
|
+
}
|
|
1074
|
+
const cp = require('child_process');
|
|
1075
|
+
const gates = [
|
|
1076
|
+
{ id: 'connector', script: 'build-connector-registry.cjs' },
|
|
1077
|
+
{ id: 'projection', script: 'build-orchestration-projection.cjs' },
|
|
1078
|
+
// Phase 254 Plan 03 (WIRE-04): the framework-vocabulary-drift gate rides
|
|
1079
|
+
// the SAME coverage-gate organ. A silent divergence between the three
|
|
1080
|
+
// committed framework vocabularies (KNOWN_FRAMEWORKS, the registry's
|
|
1081
|
+
// declared frameworks, and the projection's framework nodes) is the
|
|
1082
|
+
// measured root cause of the one-step-chain defect Plans 01/02 of Phase
|
|
1083
|
+
// 254 fix; this gate makes any FUTURE undeclared divergence a blocker
|
|
1084
|
+
// too. A declared entry with a stated reason never fails this gate.
|
|
1085
|
+
{ id: 'framework-vocabulary', script: 'check-framework-vocabulary-drift.cjs' },
|
|
1086
|
+
// Phase 178-03 (Canon Part 11 render twin, C-3): the render-coverage gate
|
|
1087
|
+
// rides the SAME doctor --acceptance organ as the two CIRS gates. A render
|
|
1088
|
+
// GAP (a reachable Decision-Gate surface not routed through the SEED-020
|
|
1089
|
+
// card-emission door) is a blocker here too -- HARD-FAIL, never WARN (R-5).
|
|
1090
|
+
{ id: 'render', script: 'check-render-coverage.cjs' },
|
|
1091
|
+
// Quick task 260705-sy9: the skill-mirror staleness gate rides the SAME
|
|
1092
|
+
// coverage-gate organ (the Phase 178-03 render-gate precedent). A stale
|
|
1093
|
+
// skills/<name>/SKILL.md mirror ships a wrong command body to machines
|
|
1094
|
+
// hit by the Claude Code registration bug (260705-ob7). Blocker semantics:
|
|
1095
|
+
// the script exits 1 on any missing/stale mirror or a deleted/reverted
|
|
1096
|
+
// SKIP_LIST skill. Canon Part 8: local byte compares only.
|
|
1097
|
+
{ id: 'skill-mirrors', script: 'build-skill-mirrors.cjs' },
|
|
1098
|
+
// Phase 190-04 (SFD-04/SFD-05, Canon Part 11 R16), softened by Phase 210
|
|
1099
|
+
// (item 210-A, navigator decision, CONTEXT addendum 1): the born-declared-shape
|
|
1100
|
+
// gate is ADVISORY. The script's --check exits 0 by default even on a
|
|
1101
|
+
// violating tree (its WARN lines enumerate every violation in stderrTail),
|
|
1102
|
+
// so the r.status === 0 ok test below no longer blocks on shape-declaration
|
|
1103
|
+
// drift; --strict on the script restores the pre-210 hard-fail if a future
|
|
1104
|
+
// phase re-hardens. The other three gates above KEEP blocker semantics.
|
|
1105
|
+
// The gate still enumerates the tree at run time; no hardcoded count.
|
|
1106
|
+
{ id: 'shape-declaration', script: 'check-shape-declaration.cjs' },
|
|
1107
|
+
// Quick task 260903-ljj: the MCP tool-honesty description-vs-behavior
|
|
1108
|
+
// scanner rides the SAME advisory-by-default posture as the
|
|
1109
|
+
// shape-declaration entry above. Its --check exits 0 by design even
|
|
1110
|
+
// on a tree carrying HIGH RISK findings (it WARNs and enumerates
|
|
1111
|
+
// each one in stderrTail instead of failing), so the r.status === 0
|
|
1112
|
+
// ok test below never fails the roll-up on an advisory finding;
|
|
1113
|
+
// --strict on the script restores hard-fail once the live findings
|
|
1114
|
+
// are triaged and the gate is deliberately re-hardened. The gate
|
|
1115
|
+
// still enumerates every server.tool() registration at run time;
|
|
1116
|
+
// no hardcoded tool/branch/command count.
|
|
1117
|
+
{ id: 'tool-honesty', script: 'check-tool-honesty.cjs' },
|
|
1118
|
+
];
|
|
1119
|
+
const results = [];
|
|
1120
|
+
for (const g of gates) {
|
|
1121
|
+
const scriptPath = path.join(pluginRoot, 'scripts', g.script);
|
|
1122
|
+
if (!fs.existsSync(scriptPath)) {
|
|
1123
|
+
results.push({ id: g.id, ok: false, status: null, note: 'script missing: ' + g.script });
|
|
1124
|
+
continue;
|
|
1125
|
+
}
|
|
1126
|
+
const r = cp.spawnSync('node', [scriptPath, '--check'], { encoding: 'utf8', timeout: 60000 });
|
|
1127
|
+
results.push({
|
|
1128
|
+
id: g.id,
|
|
1129
|
+
ok: r.status === 0,
|
|
1130
|
+
status: r.status,
|
|
1131
|
+
stderrTail: (r.stderr || '').slice(-400),
|
|
1132
|
+
});
|
|
1133
|
+
}
|
|
1134
|
+
const failed = results.filter(function (x) { return !x.ok; });
|
|
1135
|
+
const ok = failed.length === 0;
|
|
1136
|
+
const finding = ok ? null : (failed.map(function (x) { return x.id + ' gate exited ' + x.status; }).join('; '));
|
|
1137
|
+
return { ok: ok, finding: finding, detail: { gates: results } };
|
|
1138
|
+
},
|
|
1139
|
+
},
|
|
1140
|
+
// -- Phase 126 Plan 05: 5 release-flight pre-flight hot-patches --
|
|
1141
|
+
//
|
|
1142
|
+
// Phase 123's release cut (v1.13.0-beta.13) surfaced 5 hot-patches during
|
|
1143
|
+
// Plan-06 pre-flight. The operator applied them MANUALLY before tagging
|
|
1144
|
+
// the release. That tribal knowledge is release risk; Plan 05 absorbs each
|
|
1145
|
+
// hot-patch as a checklist entry so the gate enforces them automatically.
|
|
1146
|
+
// All 5 are pre-tag-applicable (they verify pre-release state). Each
|
|
1147
|
+
// honors the DOCTOR_TEST_FAIL_POINT injection (mirrors the established
|
|
1148
|
+
// pattern from Phase 123). Canon Part 6 (dog-fooding): each manually-
|
|
1149
|
+
// applied operator check is promoted to a structured gate.
|
|
1150
|
+
{
|
|
1151
|
+
id: 'session-start-active-version',
|
|
1152
|
+
label: 'session-start derives active_version correctly from installed_plugins.json',
|
|
1153
|
+
severity: 'blocker',
|
|
1154
|
+
applies_to: ['pre-tag', 'full'],
|
|
1155
|
+
run: async function () {
|
|
1156
|
+
if (inTestMode && process.env.DOCTOR_TEST_FAIL_POINT === 'session-start-active-version') {
|
|
1157
|
+
return { ok: false, finding: 'session-start-active-version synthesized failure (test mode)', detail: {} };
|
|
1158
|
+
}
|
|
1159
|
+
try {
|
|
1160
|
+
// Read installed_plugins.json + extract the mos@mindrian-marketplace
|
|
1161
|
+
// active version. Mirror the same name/structure scan as session-start
|
|
1162
|
+
// lines 178-186 (accept both "mos" and "mindrian-os" prefixes).
|
|
1163
|
+
const ipPath = path.join(home, '.claude', 'plugins', 'installed_plugins.json');
|
|
1164
|
+
if (!fs.existsSync(ipPath)) {
|
|
1165
|
+
return { ok: false, finding: 'installed_plugins.json absent', detail: { ipPath: ipPath } };
|
|
1166
|
+
}
|
|
1167
|
+
const ip = JSON.parse(fs.readFileSync(ipPath, 'utf8'));
|
|
1168
|
+
const plugins = (ip && ip.plugins) || {};
|
|
1169
|
+
let recordedVersion = null;
|
|
1170
|
+
for (const k of Object.keys(plugins)) {
|
|
1171
|
+
const name = String(k).split('@')[0];
|
|
1172
|
+
if (name !== 'mos' && name !== 'mindrian-os') continue;
|
|
1173
|
+
let entry = plugins[k];
|
|
1174
|
+
if (Array.isArray(entry)) entry = entry[0];
|
|
1175
|
+
if (entry && entry.version) { recordedVersion = entry.version; break; }
|
|
1176
|
+
}
|
|
1177
|
+
if (!recordedVersion) {
|
|
1178
|
+
return { ok: false, finding: 'no mos@mindrian-marketplace entry in installed_plugins.json', detail: {} };
|
|
1179
|
+
}
|
|
1180
|
+
// Resolve the active plugin root and compare its plugin.json version
|
|
1181
|
+
// to the recorded installed_plugins.json version. session-start's
|
|
1182
|
+
// line-202 derivation uses resolver-root-basename first, then this
|
|
1183
|
+
// value -- a mismatch means the two derivation paths disagree.
|
|
1184
|
+
const r = resolveActivePluginRoot({});
|
|
1185
|
+
if (!r || !r.root) {
|
|
1186
|
+
return { ok: false, finding: 'active plugin root unresolvable', detail: { source: r && r.source } };
|
|
1187
|
+
}
|
|
1188
|
+
const pjPath = path.join(r.root, '.claude-plugin', 'plugin.json');
|
|
1189
|
+
if (!fs.existsSync(pjPath)) {
|
|
1190
|
+
return { ok: false, finding: 'plugin.json missing in active root', detail: { pjPath: pjPath } };
|
|
1191
|
+
}
|
|
1192
|
+
const pj = JSON.parse(fs.readFileSync(pjPath, 'utf8'));
|
|
1193
|
+
const ok = pj.version === recordedVersion;
|
|
1194
|
+
return {
|
|
1195
|
+
ok: ok,
|
|
1196
|
+
finding: ok ? null : ('plugin.json says ' + pj.version + ' but installed_plugins.json says ' + recordedVersion),
|
|
1197
|
+
detail: { pluginJson: pj.version, recordedVersion: recordedVersion, activeRoot: r.root, topology: r.topology },
|
|
1198
|
+
};
|
|
1199
|
+
} catch (e) {
|
|
1200
|
+
return { ok: false, finding: 'session-start-active-version threw: ' + e.message, detail: {} };
|
|
1201
|
+
}
|
|
1202
|
+
},
|
|
1203
|
+
},
|
|
1204
|
+
{
|
|
1205
|
+
id: 'verify-release-clean-tree',
|
|
1206
|
+
label: 'verify-release Step 12 reports clean git tree (tracked files only)',
|
|
1207
|
+
severity: 'blocker',
|
|
1208
|
+
// Phase 126 Plan 05 absorbed this check from the Phase 123 cut pre-flight.
|
|
1209
|
+
// The 2026-05-14 beta.16 cut attempt surfaced that --pre-tag is the wrong
|
|
1210
|
+
// mode: release.sh Step 6.6 runs --pre-tag AFTER Steps 3-6 intentionally
|
|
1211
|
+
// bump package.json + plugin.json + CHANGELOG (3 expected tracked-file
|
|
1212
|
+
// mods), and this strict clean-tree check trips on them. The check is a
|
|
1213
|
+
// PRE-FLIGHT meant to run OUT-OF-BAND with release.sh (operator runs it
|
|
1214
|
+
// manually before kicking off the cut, OR release.sh runs it BEFORE
|
|
1215
|
+
// Step 3 -- which would be a release.sh refactor). For now: drop from
|
|
1216
|
+
// 'pre-tag', keep in 'full' (post-tag, everything should be committed).
|
|
1217
|
+
// Operators can still invoke doctor --acceptance manually pre-flight by
|
|
1218
|
+
// running it without --pre-tag (which runs the 'full' suite).
|
|
1219
|
+
//
|
|
1220
|
+
// Phase 126.1 hotfix (2026-05-15): added 'pre-flight' tier. release.sh
|
|
1221
|
+
// Step 2.5 calls --pre-flight BEFORE Step 3 mutates the tree, so this
|
|
1222
|
+
// check runs on a clean working tree before the cut starts bumping.
|
|
1223
|
+
// 'pre-tag' stays excluded because Step 6.6 still runs post-bump.
|
|
1224
|
+
applies_to: ['pre-flight', 'full'],
|
|
1225
|
+
run: async function () {
|
|
1226
|
+
if (inTestMode && process.env.DOCTOR_TEST_FAIL_POINT === 'verify-release-clean-tree') {
|
|
1227
|
+
return { ok: false, finding: 'verify-release-clean-tree synthesized failure (test mode)', detail: {} };
|
|
1228
|
+
}
|
|
1229
|
+
const cp = require('child_process');
|
|
1230
|
+
try {
|
|
1231
|
+
// Mirror verify-release Step 12 line 296: --untracked-files=no so
|
|
1232
|
+
// untracked files are allowed; tracked-dirty is the failure mode.
|
|
1233
|
+
const r = cp.spawnSync('git', ['-C', pluginRoot, 'status', '--porcelain', '--untracked-files=no'], { encoding: 'utf8', timeout: 10000 });
|
|
1234
|
+
if (r.status !== 0) {
|
|
1235
|
+
return { ok: false, finding: 'git status failed (exit ' + r.status + ')', detail: { stderr: (r.stderr || '').slice(-200) } };
|
|
1236
|
+
}
|
|
1237
|
+
const dirty = (r.stdout || '').trim();
|
|
1238
|
+
const ok = dirty === '';
|
|
1239
|
+
const dirtyCount = dirty ? dirty.split('\n').length : 0;
|
|
1240
|
+
return {
|
|
1241
|
+
ok: ok,
|
|
1242
|
+
finding: ok ? null : ('tracked-file drift: ' + dirtyCount + ' file(s)'),
|
|
1243
|
+
detail: { dirtyCount: dirtyCount, dirtyTail: dirty.slice(0, 500) },
|
|
1244
|
+
};
|
|
1245
|
+
} catch (e) {
|
|
1246
|
+
return { ok: false, finding: 'verify-release-clean-tree threw: ' + e.message, detail: {} };
|
|
1247
|
+
}
|
|
1248
|
+
},
|
|
1249
|
+
},
|
|
1250
|
+
{
|
|
1251
|
+
id: 'frontmatter-yaml-validity',
|
|
1252
|
+
label: 'commands/operator.md + commands/doctor.md frontmatter YAML parses cleanly',
|
|
1253
|
+
severity: 'blocker',
|
|
1254
|
+
applies_to: ['pre-tag', 'full'],
|
|
1255
|
+
run: async function () {
|
|
1256
|
+
if (inTestMode && process.env.DOCTOR_TEST_FAIL_POINT === 'frontmatter-yaml-validity') {
|
|
1257
|
+
return { ok: false, finding: 'frontmatter-yaml-validity synthesized failure (test mode)', detail: {} };
|
|
1258
|
+
}
|
|
1259
|
+
try {
|
|
1260
|
+
const targets = [
|
|
1261
|
+
path.join(pluginRoot, 'commands', 'operator.md'),
|
|
1262
|
+
path.join(pluginRoot, 'commands', 'doctor.md'),
|
|
1263
|
+
];
|
|
1264
|
+
const failures = [];
|
|
1265
|
+
for (const t of targets) {
|
|
1266
|
+
if (!fs.existsSync(t)) { failures.push(t + ' (absent)'); continue; }
|
|
1267
|
+
const content = fs.readFileSync(t, 'utf8');
|
|
1268
|
+
// Frontmatter pattern: file starts with `---\n` and the next `---\n`
|
|
1269
|
+
// closes the block. We do not require a leading BOM.
|
|
1270
|
+
const m = content.match(/^---\n([\s\S]*?)\n---\n/);
|
|
1271
|
+
if (!m) { failures.push(t + ' (no YAML frontmatter)'); continue; }
|
|
1272
|
+
const yaml = m[1];
|
|
1273
|
+
const lines = yaml.split('\n');
|
|
1274
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1275
|
+
const ln = lines[i];
|
|
1276
|
+
if (ln.trim() === '') continue;
|
|
1277
|
+
if (ln.indexOf('\t') !== -1) { failures.push(t + ' line ' + (i + 1) + ' contains tab'); break; }
|
|
1278
|
+
// Allowed shapes: `<key>: <value>`, ` <key>: <value>` (nested),
|
|
1279
|
+
// `- <item>` (list item), `# comment`. Anything else is drift.
|
|
1280
|
+
const isKeyValue = /^[A-Za-z_][A-Za-z0-9_-]*:/.test(ln);
|
|
1281
|
+
const isIndented = ln.startsWith(' ');
|
|
1282
|
+
const isListItem = ln.startsWith('-');
|
|
1283
|
+
const isComment = ln.startsWith('#');
|
|
1284
|
+
if (!isKeyValue && !isIndented && !isListItem && !isComment) {
|
|
1285
|
+
failures.push(t + ' line ' + (i + 1) + ' not key:value');
|
|
1286
|
+
break;
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
const ok = failures.length === 0;
|
|
1291
|
+
return {
|
|
1292
|
+
ok: ok,
|
|
1293
|
+
finding: ok ? null : ('YAML drift: ' + failures.join('; ')),
|
|
1294
|
+
detail: { failures: failures },
|
|
1295
|
+
};
|
|
1296
|
+
} catch (e) {
|
|
1297
|
+
return { ok: false, finding: 'frontmatter-yaml-validity threw: ' + e.message, detail: {} };
|
|
1298
|
+
}
|
|
1299
|
+
},
|
|
1300
|
+
},
|
|
1301
|
+
{
|
|
1302
|
+
id: 'release-dry-run-output',
|
|
1303
|
+
label: 'release.sh --dry-run produces all expected step names',
|
|
1304
|
+
severity: 'blocker',
|
|
1305
|
+
applies_to: ['pre-tag', 'full'],
|
|
1306
|
+
run: async function () {
|
|
1307
|
+
if (inTestMode && process.env.DOCTOR_TEST_FAIL_POINT === 'release-dry-run-output') {
|
|
1308
|
+
return { ok: false, finding: 'release-dry-run-output synthesized failure (test mode)', detail: {} };
|
|
1309
|
+
}
|
|
1310
|
+
const cp = require('child_process');
|
|
1311
|
+
try {
|
|
1312
|
+
// Pass an explicit bump mode ('patch') so this self-test is robust to
|
|
1313
|
+
// the CURRENT version state. A bare '--dry-run' defaults to --prerelease
|
|
1314
|
+
// only when the current version carries a '-beta.N' suffix; during a
|
|
1315
|
+
// --finalize run this self-test executes AFTER Step 3 has bumped the
|
|
1316
|
+
// version to a clean X.Y.Z, where release.sh correctly requires an
|
|
1317
|
+
// explicit mode and a bare dry-run exits 1 (the finalize-path self-test
|
|
1318
|
+
// bug, fixed 2026-06-02). 'patch' is valid on both clean and suffixed
|
|
1319
|
+
// versions and emits the same mode-independent step names this check asserts.
|
|
1320
|
+
const r = cp.spawnSync('bash', [path.join(pluginRoot, 'scripts', 'release.sh'), 'patch', '--dry-run'], { encoding: 'utf8', timeout: 30000 });
|
|
1321
|
+
if (r.status !== 0) {
|
|
1322
|
+
return { ok: false, finding: 'release.sh --dry-run exited ' + r.status, detail: { stderr: (r.stderr || '').slice(-200) } };
|
|
1323
|
+
}
|
|
1324
|
+
const out = r.stdout || '';
|
|
1325
|
+
// NOTE: Plan 04 (Phase 126 Wave 3) patched this list to add Step 5.5
|
|
1326
|
+
// / 9.7 / 9.8 and to reorder Step 9.6 -> 9.8 for the rename. The new
|
|
1327
|
+
// Step 9.6 is the install-minisite HARD lockstep; Step 9.7 is the
|
|
1328
|
+
// npx-publish self-test; Step 9.8 is the post-publish full
|
|
1329
|
+
// --acceptance gate (was Step 9.6 in Wave 2's scaffold). If you edit
|
|
1330
|
+
// this list, make sure release.sh --dry-run output still matches in
|
|
1331
|
+
// order. Phase 126.1 hotfix (2026-05-15) added Step 2.5 (the new
|
|
1332
|
+
// --pre-flight clean-tree gate that runs BEFORE Step 3 mutates).
|
|
1333
|
+
const expectedSteps = ['Step 2', 'Step 2.5', 'Step 3', 'Step 4', 'Step 5', 'Step 5b', 'Step 5.5', 'Step 6', 'Step 6.5', 'Step 6.6', 'Step 7', 'Step 9.5', 'Step 9.6', 'Step 9.7', 'Step 9.8'];
|
|
1334
|
+
const missing = expectedSteps.filter(function (s) { return out.indexOf(s) === -1; });
|
|
1335
|
+
const ok = missing.length === 0;
|
|
1336
|
+
return {
|
|
1337
|
+
ok: ok,
|
|
1338
|
+
finding: ok ? null : ('missing step names: ' + missing.join(', ')),
|
|
1339
|
+
detail: { missing: missing, expectedCount: expectedSteps.length },
|
|
1340
|
+
};
|
|
1341
|
+
} catch (e) {
|
|
1342
|
+
return { ok: false, finding: 'release-dry-run-output threw: ' + e.message, detail: {} };
|
|
1343
|
+
}
|
|
1344
|
+
},
|
|
1345
|
+
},
|
|
1346
|
+
{
|
|
1347
|
+
id: 'working-tree-housekeeping',
|
|
1348
|
+
label: 'no orphan .tmp/.bak/.swp files in tracked directories',
|
|
1349
|
+
severity: 'blocker',
|
|
1350
|
+
applies_to: ['pre-tag', 'full'],
|
|
1351
|
+
run: async function () {
|
|
1352
|
+
if (inTestMode && process.env.DOCTOR_TEST_FAIL_POINT === 'working-tree-housekeeping') {
|
|
1353
|
+
return { ok: false, finding: 'working-tree-housekeeping synthesized failure (test mode)', detail: {} };
|
|
1354
|
+
}
|
|
1355
|
+
const cp = require('child_process');
|
|
1356
|
+
try {
|
|
1357
|
+
const r = cp.spawnSync('git', ['-C', pluginRoot, 'ls-files'], { encoding: 'utf8', timeout: 10000 });
|
|
1358
|
+
if (r.status !== 0) {
|
|
1359
|
+
return { ok: false, finding: 'git ls-files failed (exit ' + r.status + ')', detail: { stderr: (r.stderr || '').slice(-200) } };
|
|
1360
|
+
}
|
|
1361
|
+
const tracked = (r.stdout || '').split('\n').filter(Boolean);
|
|
1362
|
+
const orphans = tracked.filter(function (p) { return /\.(tmp|bak|swp|swo)$/.test(p); });
|
|
1363
|
+
const ok = orphans.length === 0;
|
|
1364
|
+
const orphanSample = orphans.slice(0, 5).join(', ') + (orphans.length > 5 ? ' (+' + (orphans.length - 5) + ' more)' : '');
|
|
1365
|
+
return {
|
|
1366
|
+
ok: ok,
|
|
1367
|
+
finding: ok ? null : ('orphan tracked tmp/bak files: ' + orphanSample),
|
|
1368
|
+
detail: { orphanCount: orphans.length, orphans: orphans.slice(0, 20) },
|
|
1369
|
+
};
|
|
1370
|
+
} catch (e) {
|
|
1371
|
+
return { ok: false, finding: 'working-tree-housekeeping threw: ' + e.message, detail: {} };
|
|
1372
|
+
}
|
|
1373
|
+
},
|
|
1374
|
+
},
|
|
1375
|
+
{
|
|
1376
|
+
// QUICK-260906-t3s -- catches unregistered checkout directories piling
|
|
1377
|
+
// up under .claude/worktrees/: agent-session worktrees whose linked-
|
|
1378
|
+
// worktree admin record has gone stale (a moved repo root, a deleted
|
|
1379
|
+
// branch, a crashed cleanup) so `git worktree prune` can no longer see
|
|
1380
|
+
// them and they sit on disk forever. On 2026-09-06 this reached 46
|
|
1381
|
+
// orphans / 3.9GB, and an unscoped version search walked into one of
|
|
1382
|
+
// them and returned a months-old answer as if it were current (see
|
|
1383
|
+
// docs/autopsies/2026-09-06-worktree-version-contamination-incident.md).
|
|
1384
|
+
// BLOCKER, not advisory (DD-4 of that quick task's plan): the clean
|
|
1385
|
+
// state is exactly zero and the fix is a single command, so unlike the
|
|
1386
|
+
// large-disputed-violation-set advisory gates (shape-declaration,
|
|
1387
|
+
// tool-honesty), a hard fail here is honest and cheap. `--advisory` on
|
|
1388
|
+
// the underlying script exists for a release train that ever needs to
|
|
1389
|
+
// move past this anyway; it is not wired here on purpose.
|
|
1390
|
+
// Canon Part 8: every check this entry runs is a local filesystem read
|
|
1391
|
+
// or a local `git` plumbing call, spawned in-process. Zero network.
|
|
1392
|
+
id: 'worktree-hygiene',
|
|
1393
|
+
label: 'no unregistered checkout directories under .claude/worktrees/',
|
|
1394
|
+
severity: 'blocker',
|
|
1395
|
+
applies_to: ['pre-tag', 'full'],
|
|
1396
|
+
run: async function () {
|
|
1397
|
+
if (inTestMode && process.env.DOCTOR_TEST_FAIL_POINT === 'worktree-hygiene') {
|
|
1398
|
+
return { ok: false, finding: 'worktree-hygiene synthesized failure (test mode)', detail: {} };
|
|
1399
|
+
}
|
|
1400
|
+
const cp = require('child_process');
|
|
1401
|
+
const scriptPath = path.join(pluginRoot, 'scripts', 'check-worktree-hygiene.cjs');
|
|
1402
|
+
if (!fs.existsSync(scriptPath)) {
|
|
1403
|
+
return { ok: false, finding: 'worktree-hygiene: script missing at ' + scriptPath, detail: { note: 'script missing' } };
|
|
1404
|
+
}
|
|
1405
|
+
try {
|
|
1406
|
+
const r = cp.spawnSync('node', [scriptPath, '--check', '--json'], { encoding: 'utf8', timeout: 30000, cwd: pluginRoot });
|
|
1407
|
+
let parsed;
|
|
1408
|
+
try {
|
|
1409
|
+
parsed = JSON.parse(r.stdout || '{}');
|
|
1410
|
+
} catch (e) {
|
|
1411
|
+
return { ok: false, finding: 'worktree-hygiene: could not parse script output: ' + e.message, detail: { stdout: (r.stdout || '').slice(-500), stderr: (r.stderr || '').slice(-500) } };
|
|
1412
|
+
}
|
|
1413
|
+
const nonRegistered = Array.isArray(parsed.nonRegistered) ? parsed.nonRegistered : [];
|
|
1414
|
+
const ok = r.status === 0;
|
|
1415
|
+
const finding = ok
|
|
1416
|
+
? null
|
|
1417
|
+
: nonRegistered.length + ' unregistered worktree checkout(s) under .claude/worktrees/; fix with: node scripts/check-worktree-hygiene.cjs --prune --confirm';
|
|
1418
|
+
return {
|
|
1419
|
+
ok: ok,
|
|
1420
|
+
finding: finding,
|
|
1421
|
+
detail: { orphanCount: nonRegistered.length, review: nonRegistered.filter(function (x) { return x.status !== 'safe-orphan'; }) },
|
|
1422
|
+
};
|
|
1423
|
+
} catch (e) {
|
|
1424
|
+
return { ok: false, finding: 'worktree-hygiene threw: ' + e.message, detail: {} };
|
|
1425
|
+
}
|
|
1426
|
+
},
|
|
1427
|
+
},
|
|
1428
|
+
{
|
|
1429
|
+
// Phase 298 Plan 15 (SEED-032, Harness-as-Code) -- the final rung on
|
|
1430
|
+
// the placement ladder: standalone (`run-harness --check`) becomes
|
|
1431
|
+
// embedded here (a doctor acceptance point) and chained (the widened
|
|
1432
|
+
// pre-commit guard). Wired LAST, only after slice 1 was confirmed
|
|
1433
|
+
// green (`node scripts/run-harness.cjs --check --json` exit 0 on a
|
|
1434
|
+
// clean tree), per the D-4 precedent from quick 260906-t3s: every
|
|
1435
|
+
// `buildAcceptanceChecklist` entry is `severity: 'blocker'` and
|
|
1436
|
+
// `--acceptance` hard-aborts with no `--allow` override, so a
|
|
1437
|
+
// blocker's first run must be a true green.
|
|
1438
|
+
//
|
|
1439
|
+
// WHY BLOCKER, NOT ADVISORY: a declared harness policy that stops
|
|
1440
|
+
// running is a silent governance hole -- every acceptance entry here
|
|
1441
|
+
// is a blocker by construction (see `eureka-fts-index-visible`'s own
|
|
1442
|
+
// comment above for the general reasoning); informational reporting
|
|
1443
|
+
// for this same surface already lives in the `voice-style-log`
|
|
1444
|
+
// doctor MODULE (plan 298-08), which is the advisory sibling.
|
|
1445
|
+
//
|
|
1446
|
+
// Canon Part 8: the only I/O below is a local filesystem existence
|
|
1447
|
+
// check plus one local `spawnSync('node', [...])` of a repo-relative
|
|
1448
|
+
// script, argv array (never a shell string). Zero network.
|
|
1449
|
+
id: 'harness-policies',
|
|
1450
|
+
label: 'every declared harness policy runs and no blocking policy failed',
|
|
1451
|
+
severity: 'blocker',
|
|
1452
|
+
applies_to: ['pre-tag', 'full'],
|
|
1453
|
+
run: async function () {
|
|
1454
|
+
if (inTestMode && process.env.DOCTOR_TEST_FAIL_POINT === 'harness-policies') {
|
|
1455
|
+
return { ok: false, finding: 'harness-policies synthesized failure (test mode)', detail: {} };
|
|
1456
|
+
}
|
|
1457
|
+
const cp = require('child_process');
|
|
1458
|
+
const scriptPath = path.join(pluginRoot, 'scripts', 'run-harness.cjs');
|
|
1459
|
+
if (!fs.existsSync(scriptPath)) {
|
|
1460
|
+
return { ok: false, finding: 'harness-policies: script missing at ' + scriptPath, detail: { note: 'script missing' } };
|
|
1461
|
+
}
|
|
1462
|
+
try {
|
|
1463
|
+
const r = cp.spawnSync('node', [scriptPath, '--check', '--json'], { encoding: 'utf8', timeout: 30000, cwd: pluginRoot });
|
|
1464
|
+
let parsed;
|
|
1465
|
+
try {
|
|
1466
|
+
parsed = JSON.parse(r.stdout || '{}');
|
|
1467
|
+
} catch (e) {
|
|
1468
|
+
return { ok: false, finding: 'harness-policies: could not parse script output: ' + e.message, detail: { stdout: (r.stdout || '').slice(-500), stderr: (r.stderr || '').slice(-500) } };
|
|
1469
|
+
}
|
|
1470
|
+
const counts = parsed.counts || {};
|
|
1471
|
+
const policies = Array.isArray(parsed.policies) ? parsed.policies : [];
|
|
1472
|
+
const failedIds = policies.filter(function (p) { return p.verdict === 'fail'; }).map(function (p) { return p.id; });
|
|
1473
|
+
const ok = r.status === 0;
|
|
1474
|
+
// The 2026-07-11 printer-must-match-counter lesson: the failing
|
|
1475
|
+
// finding echoes the SAME 'Totals: ...' verdict string
|
|
1476
|
+
// run-harness.cjs's own printReport() prints in text mode
|
|
1477
|
+
// (scripts/run-harness.cjs:413-416), built here from the same
|
|
1478
|
+
// parsed counts object, never a separately-computed tally, plus
|
|
1479
|
+
// the exact recovery command.
|
|
1480
|
+
const totalsLine = 'Totals: ' + (counts.pass || 0) + ' pass, ' + (counts.fail || 0) + ' fail, ' +
|
|
1481
|
+
(counts.ghost || 0) + ' ghost, ' + (counts.declared || 0) + ' declared, ' + (counts.total || 0) + ' total.';
|
|
1482
|
+
const finding = ok
|
|
1483
|
+
? null
|
|
1484
|
+
: (failedIds.length + ' failing harness polic' + (failedIds.length === 1 ? 'y' : 'ies') +
|
|
1485
|
+
(failedIds.length ? ' (' + failedIds.join(', ') + ')' : '') + '; ' + totalsLine +
|
|
1486
|
+
' Fix with: node scripts/run-harness.cjs --check');
|
|
1487
|
+
return {
|
|
1488
|
+
ok: ok,
|
|
1489
|
+
finding: finding,
|
|
1490
|
+
detail: { counts: counts, failedIds: failedIds },
|
|
1491
|
+
};
|
|
1492
|
+
} catch (e) {
|
|
1493
|
+
return { ok: false, finding: 'harness-policies threw: ' + e.message, detail: {} };
|
|
1494
|
+
}
|
|
1495
|
+
},
|
|
1496
|
+
},
|
|
1497
|
+
{
|
|
1498
|
+
// Phase 127.2 Plan 04 Instance #7 -- Class N acceptance point:
|
|
1499
|
+
// "activation reached the wire."
|
|
1500
|
+
//
|
|
1501
|
+
// Asserts that the live MCP server's version matches the
|
|
1502
|
+
// package.json version-of-record. Catches the silent-activation gap
|
|
1503
|
+
// where /mos:update lands bytes in cache + the active install is not
|
|
1504
|
+
// swapped, so the user thinks they are on beta.N+1 while every Brain
|
|
1505
|
+
// probe quietly hits beta.N. Before Class N, this defect could ship
|
|
1506
|
+
// a beta where the published bytes never activate on testers'
|
|
1507
|
+
// machines -- breaking the entire dog-fooding feedback loop because
|
|
1508
|
+
// tester "I tested beta.N" reports were filed against the previous
|
|
1509
|
+
// beta.
|
|
1510
|
+
//
|
|
1511
|
+
// applies_to: ['full'] only. The wire probe requires a live MCP
|
|
1512
|
+
// server reachable + the published version on npm/marketplace; this
|
|
1513
|
+
// is FALSE before tag + publish, so the pre-tag and pre-flight modes
|
|
1514
|
+
// skip this point. Step 9.8 of release.sh runs the full --acceptance
|
|
1515
|
+
// gate AFTER publish, which is where this point gates.
|
|
1516
|
+
id: 'activation-reached-the-wire',
|
|
1517
|
+
label: 'L4 MCP server version matches package.json version-of-record',
|
|
1518
|
+
severity: 'blocker',
|
|
1519
|
+
applies_to: ['full'],
|
|
1520
|
+
run: async function () {
|
|
1521
|
+
if (inTestMode && process.env.DOCTOR_TEST_FAIL_POINT === 'activation-reached-the-wire') {
|
|
1522
|
+
return { ok: false, finding: 'activation-reached-the-wire synthesized failure (test mode)', detail: {} };
|
|
1523
|
+
}
|
|
1524
|
+
// Defensive: opt-out for hermetic CI / offline mode. release.sh
|
|
1525
|
+
// Step 9.8 runs against a live network; CI smoke tests can set
|
|
1526
|
+
// DOCTOR_SKIP_ACTIVATION_GATE=1 to mark this point ok-as-skipped.
|
|
1527
|
+
if (process.env.DOCTOR_SKIP_ACTIVATION_GATE === '1') {
|
|
1528
|
+
return { ok: true, finding: null, detail: { skipped: true, reason: 'DOCTOR_SKIP_ACTIVATION_GATE=1' } };
|
|
1529
|
+
}
|
|
1530
|
+
const cp = require('child_process');
|
|
1531
|
+
try {
|
|
1532
|
+
// 1. Read version-of-record (package.json) from pluginRoot.
|
|
1533
|
+
const pkPath = path.join(pluginRoot, 'package.json');
|
|
1534
|
+
if (!fs.existsSync(pkPath)) {
|
|
1535
|
+
return { ok: false, finding: 'package.json not found at ' + pkPath, detail: {} };
|
|
1536
|
+
}
|
|
1537
|
+
const recordVersion = JSON.parse(fs.readFileSync(pkPath, 'utf8')).version;
|
|
1538
|
+
if (!recordVersion) {
|
|
1539
|
+
return { ok: false, finding: 'package.json contains no version field', detail: {} };
|
|
1540
|
+
}
|
|
1541
|
+
// 2. Spawn doctor --brain-smoke --json against ourselves to probe
|
|
1542
|
+
// the live MCP server. Reuses the Phase 127-02 Class M smoke
|
|
1543
|
+
// test (6-layer probe at lib/core/doctor/class-m-brain-smoke.cjs).
|
|
1544
|
+
const doctorPath = path.join(__dirname, 'doctor.cjs');
|
|
1545
|
+
const r = cp.spawnSync('node', [doctorPath, '--brain-smoke', '--json'], {
|
|
1546
|
+
encoding: 'utf8', timeout: 30000,
|
|
1547
|
+
});
|
|
1548
|
+
if (!r || typeof r.stdout !== 'string') {
|
|
1549
|
+
return { ok: false, finding: 'brain-smoke spawn produced no stdout', detail: { stderr: (r && r.stderr || '').slice(-200) } };
|
|
1550
|
+
}
|
|
1551
|
+
let payload = null;
|
|
1552
|
+
try { payload = JSON.parse(r.stdout); }
|
|
1553
|
+
catch (_) { return { ok: false, finding: 'brain-smoke stdout not parseable as JSON', detail: { stdoutTail: r.stdout.slice(-200) } }; }
|
|
1554
|
+
if (!payload || !Array.isArray(payload.layers)) {
|
|
1555
|
+
return { ok: false, finding: 'brain-smoke payload missing layers array', detail: { payloadKeys: payload ? Object.keys(payload) : [] } };
|
|
1556
|
+
}
|
|
1557
|
+
const L4 = payload.layers.find(function (l) { return l && (l.id === 'mcp_stdio_handshake' || l.name === 'L4 MCP stdio handshake'); });
|
|
1558
|
+
if (!L4) {
|
|
1559
|
+
return { ok: false, finding: 'L4 mcp_stdio_handshake layer absent', detail: { layerIds: payload.layers.map(function (l) { return l && l.id; }) } };
|
|
1560
|
+
}
|
|
1561
|
+
if (!L4.ok) {
|
|
1562
|
+
return { ok: false, finding: 'L4 mcp_stdio_handshake did not succeed: ' + (L4.reason || 'unknown'), detail: { L4: L4 } };
|
|
1563
|
+
}
|
|
1564
|
+
// 3. Parse server=mindrian-brain vX.Y.Z[-PR] from L4 reason.
|
|
1565
|
+
const match = String(L4.reason || '').match(/server=mindrian-brain v(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)/);
|
|
1566
|
+
if (!match) {
|
|
1567
|
+
return { ok: false, finding: 'L4 reason missing server-version pattern: ' + L4.reason, detail: { L4: L4 } };
|
|
1568
|
+
}
|
|
1569
|
+
const wireVersion = match[1];
|
|
1570
|
+
// 4. Compare. Match = pass, drift = activation-gap.
|
|
1571
|
+
const ok = wireVersion === recordVersion;
|
|
1572
|
+
const finding = ok ? null : ('activation gap detected: live install serves v' + wireVersion + ' but version-of-record is v' + recordVersion);
|
|
1573
|
+
// 5. Store-identity guard (quick task 260819-c9b, WS-E1). Reuses
|
|
1574
|
+
// the SAME payload already parsed above -- no second spawn.
|
|
1575
|
+
// Two things wire-lock a release here: the store_identity
|
|
1576
|
+
// layer must not silently disappear from the payload (absent
|
|
1577
|
+
// is a not-ok finding), and a release must never be cut while
|
|
1578
|
+
// the wire is pointed at the frozen stale-replica signature
|
|
1579
|
+
// (also not-ok, quoting the layer's own named reason). Any
|
|
1580
|
+
// OTHER store-identity failure -- a transient count dip, an
|
|
1581
|
+
// unreachable Brain -- is carried into detail as information
|
|
1582
|
+
// only and does NOT flip ok: an absolute count gate would
|
|
1583
|
+
// brick the release train on ordinary graph churn, the same
|
|
1584
|
+
// reasoning that keeps the dual-graph gate in report-only
|
|
1585
|
+
// mode (scripts/check-dual-graph-health.cjs DEFAULT_MODE).
|
|
1586
|
+
const storeIdentity = payload.layers.find(function (l) { return l && l.id === 'store_identity'; });
|
|
1587
|
+
if (!storeIdentity) {
|
|
1588
|
+
return {
|
|
1589
|
+
ok: false,
|
|
1590
|
+
finding: 'store_identity layer absent from brain-smoke payload',
|
|
1591
|
+
detail: { wireVersion: wireVersion, recordVersion: recordVersion, layerIds: payload.layers.map(function (l) { return l && l.id; }) },
|
|
1592
|
+
};
|
|
1593
|
+
}
|
|
1594
|
+
if (!storeIdentity.ok && /stale_replica_signature/.test(String(storeIdentity.reason || ''))) {
|
|
1595
|
+
return {
|
|
1596
|
+
ok: false,
|
|
1597
|
+
finding: 'release is being cut against the stale replica: ' + storeIdentity.reason,
|
|
1598
|
+
detail: { wireVersion: wireVersion, recordVersion: recordVersion, storeIdentity: storeIdentity },
|
|
1599
|
+
};
|
|
1600
|
+
}
|
|
1601
|
+
return { ok: ok, finding: finding, detail: { wireVersion: wireVersion, recordVersion: recordVersion, storeIdentity: storeIdentity } };
|
|
1602
|
+
} catch (e) {
|
|
1603
|
+
return { ok: false, finding: 'activation-reached-the-wire threw: ' + e.message, detail: {} };
|
|
1604
|
+
}
|
|
1605
|
+
},
|
|
1606
|
+
},
|
|
1607
|
+
{
|
|
1608
|
+
// Phase 199-05 (AS-08) -- Class O acceptance point:
|
|
1609
|
+
// "AgentShield reports zero flagged findings across all 5 plugin surfaces."
|
|
1610
|
+
//
|
|
1611
|
+
// Runs the shipped 199-04 orchestrator (runAgentShieldScan) over the SAME
|
|
1612
|
+
// tree doctor.cjs lives in (dog-food, Canon Part 6 / AS-04) and fails the
|
|
1613
|
+
// release gate if any of the five non-Brain config surfaces (MCP tool
|
|
1614
|
+
// descriptions, hook commands, skill files, CLAUDE.md permissions,
|
|
1615
|
+
// package.json deps) carries an unremediated FLAGGED finding. This catches
|
|
1616
|
+
// a poisoned tool-description / hook / skill BEFORE the plugin ships,
|
|
1617
|
+
// closing the born-wired security loop for the /mos:agentshield surface.
|
|
1618
|
+
//
|
|
1619
|
+
// ok is keyed ONLY on totalFlagged === 0. An `ambiguous` verdict WARNS via
|
|
1620
|
+
// the finding string but does NOT trip the blocker (mirrors the
|
|
1621
|
+
// warn-vs-blocker distinction used elsewhere in this file). The scan opens
|
|
1622
|
+
// NO network wire and starts NO MCP server (Part 8): every gatherer reads
|
|
1623
|
+
// plugin-local files READ-ONLY.
|
|
1624
|
+
//
|
|
1625
|
+
// applies_to: ['pre-tag','full'] -- the surfaces are all repo-local and
|
|
1626
|
+
// present before tag, so this gates in both the pre-tag and full modes.
|
|
1627
|
+
// ADD-ONLY: no existing checklist entry's logic is touched.
|
|
1628
|
+
id: 'agentshield-all-surfaces-clean',
|
|
1629
|
+
label: 'Class O: AgentShield reports zero flagged findings across all 5 plugin surfaces',
|
|
1630
|
+
severity: 'blocker',
|
|
1631
|
+
applies_to: ['pre-tag', 'full'],
|
|
1632
|
+
run: async function () {
|
|
1633
|
+
if (inTestMode && process.env.DOCTOR_TEST_FAIL_POINT === 'agentshield-all-surfaces-clean') {
|
|
1634
|
+
return { ok: false, finding: 'agentshield-all-surfaces-clean synthesized failure (test mode)', detail: {} };
|
|
1635
|
+
}
|
|
1636
|
+
try {
|
|
1637
|
+
const runnerPath = path.join(__dirname, '..', 'lib', 'core', 'security', 'agentshield-run.cjs');
|
|
1638
|
+
const { runAgentShieldScan } = require(runnerPath);
|
|
1639
|
+
const result = runAgentShieldScan();
|
|
1640
|
+
const ok = result.totalFlagged === 0;
|
|
1641
|
+
let finding = null;
|
|
1642
|
+
if (!ok) {
|
|
1643
|
+
const first = Array.isArray(result.findings) && result.findings.length > 0
|
|
1644
|
+
? result.findings[0]
|
|
1645
|
+
: null;
|
|
1646
|
+
finding = 'AgentShield flagged ' + result.totalFlagged + ' finding(s); first: ' +
|
|
1647
|
+
(first
|
|
1648
|
+
? (first.surface + '/' + first.id + ' (' + (first.ruleId || first.reason) + ')')
|
|
1649
|
+
: 'unknown');
|
|
1650
|
+
} else if (result.totalAmbiguous > 0) {
|
|
1651
|
+
// WARN-not-fail: ambiguous surfaces surface via the finding string
|
|
1652
|
+
// but the blocker stays green (only flagged findings fail the gate).
|
|
1653
|
+
finding = 'WARN: ' + result.totalAmbiguous +
|
|
1654
|
+
' ambiguous finding(s) across surfaces (no flagged; blocker not tripped)';
|
|
1655
|
+
}
|
|
1656
|
+
return {
|
|
1657
|
+
ok: ok,
|
|
1658
|
+
finding: finding,
|
|
1659
|
+
detail: {
|
|
1660
|
+
totalFlagged: result.totalFlagged,
|
|
1661
|
+
totalAmbiguous: result.totalAmbiguous,
|
|
1662
|
+
surfaces: result.surfaces,
|
|
1663
|
+
},
|
|
1664
|
+
};
|
|
1665
|
+
} catch (e) {
|
|
1666
|
+
return { ok: false, finding: 'agentshield-all-surfaces-clean threw: ' + e.message, detail: {} };
|
|
1667
|
+
}
|
|
1668
|
+
},
|
|
1669
|
+
},
|
|
1670
|
+
{
|
|
1671
|
+
// quick(260706-13z) D14(e) -- Class S acceptance point.
|
|
1672
|
+
// Phase 341 Plan 03 (D-11) split the probe into a blocker half (L1-L4:
|
|
1673
|
+
// "is the capability reachable") and an advisory half (L5
|
|
1674
|
+
// model_installed: "is the model actually installed"). Spawns
|
|
1675
|
+
// doctor --eureka-smoke --json against ourselves and gates on
|
|
1676
|
+
// payload.ok === true with all 5 layers present -- payload.ok already
|
|
1677
|
+
// excludes the advisory layer from its own reduction (checkEurekaSmoke),
|
|
1678
|
+
// so a slim install with the model absent still passes this gate
|
|
1679
|
+
// honestly instead of failing it or being skipped. The probe NEVER
|
|
1680
|
+
// downloads a model (cache miss is a graceful PASS, and the Pitfall-6
|
|
1681
|
+
// un-awaited isModelCached bug that made a cold cache download inside
|
|
1682
|
+
// this very gate is fixed as of this plan), so this is safe inside a
|
|
1683
|
+
// release gate on airgapped CI.
|
|
1684
|
+
//
|
|
1685
|
+
// applies_to: ['pre-tag','full'] -- the stack is entirely repo-local
|
|
1686
|
+
// (node_modules + local model cache), present before tag, no publish
|
|
1687
|
+
// needed. ADD-ONLY: no existing checklist entry's logic is touched.
|
|
1688
|
+
// DOCTOR_SKIP_EUREKA_SMOKE=1 remains the hermetic-CI opt-out (mirrors
|
|
1689
|
+
// DOCTOR_SKIP_ACTIVATION_GATE) for a tree where the optional eureka deps
|
|
1690
|
+
// were intentionally not installed; per D-11 it is never needed on the
|
|
1691
|
+
// shipped artifact itself, because the slim install now passes on its
|
|
1692
|
+
// own (the advisory layer never blocks it).
|
|
1693
|
+
id: 'eureka-smoke-stack-ready',
|
|
1694
|
+
label: 'Class S: local eureka capability reachable (blocker) + embedding model installed (advisory)',
|
|
1695
|
+
severity: 'blocker',
|
|
1696
|
+
applies_to: ['pre-tag', 'full'],
|
|
1697
|
+
run: async function () {
|
|
1698
|
+
if (inTestMode && process.env.DOCTOR_TEST_FAIL_POINT === 'eureka-smoke-stack-ready') {
|
|
1699
|
+
return { ok: false, finding: 'eureka-smoke-stack-ready synthesized failure (test mode)', detail: {} };
|
|
1700
|
+
}
|
|
1701
|
+
if (process.env.DOCTOR_SKIP_EUREKA_SMOKE === '1') {
|
|
1702
|
+
return { ok: true, finding: null, detail: { skipped: true, reason: 'DOCTOR_SKIP_EUREKA_SMOKE=1' } };
|
|
1703
|
+
}
|
|
1704
|
+
const cp = require('child_process');
|
|
1705
|
+
try {
|
|
1706
|
+
const doctorPath = path.join(__dirname, 'doctor.cjs');
|
|
1707
|
+
const r = cp.spawnSync('node', [doctorPath, '--eureka-smoke', '--json'], {
|
|
1708
|
+
encoding: 'utf8', timeout: 30000,
|
|
1709
|
+
});
|
|
1710
|
+
if (!r || typeof r.stdout !== 'string') {
|
|
1711
|
+
return { ok: false, finding: 'eureka-smoke spawn produced no stdout', detail: { stderr: (r && r.stderr || '').slice(-200) } };
|
|
1712
|
+
}
|
|
1713
|
+
let payload = null;
|
|
1714
|
+
try { payload = JSON.parse(r.stdout); }
|
|
1715
|
+
catch (_) { return { ok: false, finding: 'eureka-smoke stdout not parseable as JSON', detail: { stdoutTail: r.stdout.slice(-200) } }; }
|
|
1716
|
+
if (!payload || !Array.isArray(payload.layers)) {
|
|
1717
|
+
return { ok: false, finding: 'eureka-smoke payload missing layers array', detail: { payloadKeys: payload ? Object.keys(payload) : [] } };
|
|
1718
|
+
}
|
|
1719
|
+
if (payload.layers.length !== 5) {
|
|
1720
|
+
return { ok: false, finding: 'eureka-smoke expected 5 layers, got ' + payload.layers.length, detail: { layerIds: payload.layers.map(function (l) { return l && l.id; }) } };
|
|
1721
|
+
}
|
|
1722
|
+
if (payload.ok !== true) {
|
|
1723
|
+
// A non-advisory (blocker) failure -- capability itself is unreachable.
|
|
1724
|
+
const firstFail = payload.layers.find(function (l) { return l && !l.ok && !l.advisory; });
|
|
1725
|
+
return {
|
|
1726
|
+
ok: false,
|
|
1727
|
+
finding: 'eureka stack not ready: ' + (firstFail ? (firstFail.name + ' -- ' + firstFail.reason) : 'unknown layer'),
|
|
1728
|
+
detail: { layers: payload.layers },
|
|
1729
|
+
};
|
|
1730
|
+
}
|
|
1731
|
+
// Passing path (D-10 "never a silent no-op" applied to the gate
|
|
1732
|
+
// itself): the advisory layer's reason is INCLUDED here even though
|
|
1733
|
+
// it never blocked the gate, so "model not installed" is always
|
|
1734
|
+
// visible to the operator, never hidden behind a green result.
|
|
1735
|
+
return {
|
|
1736
|
+
ok: true,
|
|
1737
|
+
finding: null,
|
|
1738
|
+
detail: {
|
|
1739
|
+
layers: payload.layers.map(function (l) {
|
|
1740
|
+
const entry = { id: l.id, ok: l.ok };
|
|
1741
|
+
if (l.advisory) entry.reason = l.reason;
|
|
1742
|
+
return entry;
|
|
1743
|
+
}),
|
|
1744
|
+
},
|
|
1745
|
+
};
|
|
1746
|
+
} catch (e) {
|
|
1747
|
+
return { ok: false, finding: 'eureka-smoke-stack-ready threw: ' + e.message, detail: {} };
|
|
1748
|
+
}
|
|
1749
|
+
},
|
|
1750
|
+
},
|
|
1751
|
+
{
|
|
1752
|
+
// Phase 244 Plan 06 (closing ROADMAP SC1 / RESEARCH BLOCKER B-2):
|
|
1753
|
+
// "a presence/freshness check added to `node scripts/doctor.cjs
|
|
1754
|
+
// --acceptance`, so an unbuilt index is visible rather than silently
|
|
1755
|
+
// producing zero fires." This point calls the health module's check()
|
|
1756
|
+
// DIRECTLY in-process (a sqlite_master read is cheap; unlike Class S it
|
|
1757
|
+
// needs no separate process for model loading, so it never spawns).
|
|
1758
|
+
//
|
|
1759
|
+
// SEVERITY REASONING, stated explicitly so a future reader never assumes
|
|
1760
|
+
// this was careless: EVERY entry in this checklist carries severity
|
|
1761
|
+
// 'blocker' and --acceptance hard-aborts on any failed point. Absent is
|
|
1762
|
+
// the CORRECT DEFAULT state of every room today (verified live against
|
|
1763
|
+
// the 8.4 MB `rethinking-mindrianos` room.db, zero eureka_fts rows) --
|
|
1764
|
+
// failing on absent would fail every release until every registered
|
|
1765
|
+
// room has been rebuilt. Stale (rows pointing at nodes that no longer
|
|
1766
|
+
// exist) is a genuine correctness defect that produces ghost triggers,
|
|
1767
|
+
// so failing on stale is right. The LOGIC below, not a lowered severity,
|
|
1768
|
+
// is what makes this point visibility rather than a blanket gate: it
|
|
1769
|
+
// reports the full per-room census in `detail` on the PASSING path too
|
|
1770
|
+
// (absent/empty/ok all pass), and fails ONLY when a room is classified
|
|
1771
|
+
// index_stale.
|
|
1772
|
+
//
|
|
1773
|
+
// applies_to: ['pre-tag', 'full'] -- purely LOCAL sqlite_master reads
|
|
1774
|
+
// against registered rooms, present before tag, no publish needed.
|
|
1775
|
+
// DOCTOR_SKIP_EUREKA_FTS_HEALTH=1 is the hermetic-CI opt-out (mirrors
|
|
1776
|
+
// DOCTOR_SKIP_EUREKA_SMOKE) for a tree with no registered room registry.
|
|
1777
|
+
id: 'eureka-fts-index-visible',
|
|
1778
|
+
label: 'local lexical trigger index (eureka_fts) is present and not stale, per registered room',
|
|
1779
|
+
severity: 'blocker',
|
|
1780
|
+
applies_to: ['pre-tag', 'full'],
|
|
1781
|
+
run: async function () {
|
|
1782
|
+
if (inTestMode && process.env.DOCTOR_TEST_FAIL_POINT === 'eureka-fts-index-visible') {
|
|
1783
|
+
return { ok: false, finding: 'eureka-fts-index-visible synthesized failure (test mode)', detail: {} };
|
|
1784
|
+
}
|
|
1785
|
+
if (process.env.DOCTOR_SKIP_EUREKA_FTS_HEALTH === '1') {
|
|
1786
|
+
return { ok: true, finding: null, detail: { skipped: true, reason: 'DOCTOR_SKIP_EUREKA_FTS_HEALTH=1' } };
|
|
1787
|
+
}
|
|
1788
|
+
try {
|
|
1789
|
+
const ftsHealthMod = require(path.join(__dirname, '..', 'lib', 'core', 'doctor', 'eureka-fts-health-module.cjs'));
|
|
1790
|
+
const result = ftsHealthMod.check({});
|
|
1791
|
+
const rooms = Array.isArray(result && result.rooms) ? result.rooms : [];
|
|
1792
|
+
const staleRoom = rooms.find(function (r) { return r && r.state === 'index_stale'; });
|
|
1793
|
+
if (staleRoom) {
|
|
1794
|
+
return {
|
|
1795
|
+
ok: false,
|
|
1796
|
+
finding: 'eureka_fts stale in room "' + staleRoom.room + '" ('
|
|
1797
|
+
+ staleRoom.orphan_rows + ' orphan row(s) pointing at deleted nodes)',
|
|
1798
|
+
detail: { status: result.status, totals: result.totals, rooms: rooms },
|
|
1799
|
+
};
|
|
1800
|
+
}
|
|
1801
|
+
return {
|
|
1802
|
+
ok: true,
|
|
1803
|
+
finding: null,
|
|
1804
|
+
detail: { status: (result && result.status) || 'skip', totals: result && result.totals, rooms: rooms },
|
|
1805
|
+
};
|
|
1806
|
+
} catch (e) {
|
|
1807
|
+
return { ok: false, finding: 'eureka-fts-index-visible threw: ' + e.message, detail: {} };
|
|
1808
|
+
}
|
|
1809
|
+
},
|
|
1810
|
+
},
|
|
1811
|
+
{
|
|
1812
|
+
// Phase 265 Plan 01 (RADAR-01/RADAR-02): "node scripts/doctor.cjs
|
|
1813
|
+
// --acceptance surfaces ledger staleness through a registered doctor
|
|
1814
|
+
// module" (must_have truth). Calls the capability-ledger module's
|
|
1815
|
+
// check() DIRECTLY in-process (a single JSON read + one bounded
|
|
1816
|
+
// `claude --version` subprocess; no spawn of doctor.cjs itself).
|
|
1817
|
+
//
|
|
1818
|
+
// SEVERITY REASONING: unlike eureka-fts-index-visible's
|
|
1819
|
+
// visibility-not-gate design, this point IS a hard gate. T-265-02
|
|
1820
|
+
// (Repudiation: stale ledger presenting false assurance) names this
|
|
1821
|
+
// exact wiring as its mitigation -- "warn on unknown version, never
|
|
1822
|
+
// ok; wired to doctor --acceptance so it runs on the release gate
|
|
1823
|
+
// path." A ledger that has fallen behind the installed Claude Code
|
|
1824
|
+
// version by more than MINDRIAN_LEDGER_MAX_VERSION_LAG, or a version
|
|
1825
|
+
// the check could not read at all, must not let a release ship
|
|
1826
|
+
// silently -- that silent-pass is the exact rot pattern Phase 138's
|
|
1827
|
+
// markdown ledger died from.
|
|
1828
|
+
//
|
|
1829
|
+
// applies_to: ['pre-tag', 'full'] -- purely LOCAL (one JSON read, one
|
|
1830
|
+
// bounded local subprocess; Canon Part 8 zero network). Present before
|
|
1831
|
+
// tag, no publish needed. DOCTOR_SKIP_CAPABILITY_LEDGER=1 is the
|
|
1832
|
+
// hermetic-CI / offline-claude-binary opt-out, mirroring the sibling
|
|
1833
|
+
// DOCTOR_SKIP_* escapes above.
|
|
1834
|
+
id: 'capability-ledger-fresh',
|
|
1835
|
+
label: 'capability ledger freshness: data/capability-ledger.json tracks the installed claude --version',
|
|
1836
|
+
severity: 'blocker',
|
|
1837
|
+
applies_to: ['pre-tag', 'full'],
|
|
1838
|
+
run: async function () {
|
|
1839
|
+
if (inTestMode && process.env.DOCTOR_TEST_FAIL_POINT === 'capability-ledger-fresh') {
|
|
1840
|
+
return { ok: false, finding: 'capability-ledger-fresh synthesized failure (test mode)', detail: {} };
|
|
1841
|
+
}
|
|
1842
|
+
if (process.env.DOCTOR_SKIP_CAPABILITY_LEDGER === '1') {
|
|
1843
|
+
return { ok: true, finding: null, detail: { skipped: true, reason: 'DOCTOR_SKIP_CAPABILITY_LEDGER=1' } };
|
|
1844
|
+
}
|
|
1845
|
+
try {
|
|
1846
|
+
const ledgerMod = require(path.join(__dirname, '..', 'lib', 'core', 'doctor', 'capability-ledger-module.cjs'));
|
|
1847
|
+
const result = ledgerMod.check({});
|
|
1848
|
+
if (!result || result.status !== 'ok') {
|
|
1849
|
+
return {
|
|
1850
|
+
ok: false,
|
|
1851
|
+
finding: 'capability-ledger-fresh: ' + (result && result.detail || 'check() returned no detail'),
|
|
1852
|
+
detail: { status: result && result.status },
|
|
1853
|
+
};
|
|
1854
|
+
}
|
|
1855
|
+
return { ok: true, finding: null, detail: { status: result.status, detail: result.detail } };
|
|
1856
|
+
} catch (e) {
|
|
1857
|
+
return { ok: false, finding: 'capability-ledger-fresh threw: ' + e.message, detail: {} };
|
|
1858
|
+
}
|
|
1859
|
+
},
|
|
1860
|
+
},
|
|
1861
|
+
{
|
|
1862
|
+
// Phase 265 Plan 23 (RADAR-30, R-7): surfaces the MCP surface doctor
|
|
1863
|
+
// organ (lib/core/doctor/mcp-surface-module.cjs) on the acceptance
|
|
1864
|
+
// path, mirroring the capability-ledger-fresh point immediately
|
|
1865
|
+
// above. Calls the organ's check() DIRECTLY in-process; the organ
|
|
1866
|
+
// itself owns both stdio spawns (Canon Part 8: zero network).
|
|
1867
|
+
//
|
|
1868
|
+
// SEVERITY REASONING: 'error' (a zero-tool tools/list response or a
|
|
1869
|
+
// wedged/crashed server) fails this gate -- that is the exact
|
|
1870
|
+
// 2.1.128 signal ("connected with 0 tools") this organ exists to
|
|
1871
|
+
// catch, and it must not ship silently. 'warn' (the measured
|
|
1872
|
+
// combined surface exceeds TOTAL_SURFACE_TOKEN_BUDGET) does NOT fail
|
|
1873
|
+
// this gate -- per the organ's own contract it is a measurement
|
|
1874
|
+
// warning, not a tool-count cap (the Brain's 10-15 figure does not
|
|
1875
|
+
// transfer to a 36-tool server; see the module's header comment).
|
|
1876
|
+
// The finding/detail carries the warn text either way so it is
|
|
1877
|
+
// visible in acceptance output without blocking a release on it.
|
|
1878
|
+
id: 'mcp-surface-tool-count',
|
|
1879
|
+
label: 'MCP surface: live per-server tool count for both servers, fails on zero tools',
|
|
1880
|
+
severity: 'blocker',
|
|
1881
|
+
applies_to: ['pre-tag', 'full'],
|
|
1882
|
+
run: async function () {
|
|
1883
|
+
if (inTestMode && process.env.DOCTOR_TEST_FAIL_POINT === 'mcp-surface-tool-count') {
|
|
1884
|
+
return { ok: false, finding: 'mcp-surface-tool-count synthesized failure (test mode)', detail: {} };
|
|
1885
|
+
}
|
|
1886
|
+
if (process.env.DOCTOR_SKIP_MCP_SURFACE === '1') {
|
|
1887
|
+
return { ok: true, finding: null, detail: { skipped: true, reason: 'DOCTOR_SKIP_MCP_SURFACE=1' } };
|
|
1888
|
+
}
|
|
1889
|
+
try {
|
|
1890
|
+
const mcpSurfaceMod = require(path.join(__dirname, '..', 'lib', 'core', 'doctor', 'mcp-surface-module.cjs'));
|
|
1891
|
+
const result = mcpSurfaceMod.check({});
|
|
1892
|
+
if (!result || result.status === 'error') {
|
|
1893
|
+
return {
|
|
1894
|
+
ok: false,
|
|
1895
|
+
finding: 'mcp-surface-tool-count: ' + (result && result.detail || 'check() returned no detail'),
|
|
1896
|
+
detail: { status: result && result.status },
|
|
1897
|
+
};
|
|
1898
|
+
}
|
|
1899
|
+
return { ok: true, finding: null, detail: { status: result.status, detail: result.detail } };
|
|
1900
|
+
} catch (e) {
|
|
1901
|
+
return { ok: false, finding: 'mcp-surface-tool-count threw: ' + e.message, detail: {} };
|
|
1902
|
+
}
|
|
1903
|
+
},
|
|
1904
|
+
},
|
|
1905
|
+
];
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
async function runAcceptance(opts) {
|
|
1909
|
+
const home = opts.home;
|
|
1910
|
+
const pluginRoot = opts.pluginRoot;
|
|
1911
|
+
const flagPreTag = opts.flagPreTag;
|
|
1912
|
+
const flagPreFlight = opts.flagPreFlight;
|
|
1913
|
+
const flagLightNpx = opts.flagLightNpx;
|
|
1914
|
+
const checklist = buildAcceptanceChecklist({ home: home, pluginRoot: pluginRoot, flagLightNpx: flagLightNpx });
|
|
1915
|
+
// Phase 126.1 hotfix (2026-05-15): tri-state mode. --pre-tag wins when both
|
|
1916
|
+
// --pre-tag and --pre-flight are passed; --pre-flight is the strict-subset
|
|
1917
|
+
// tier release.sh Step 2.5 uses BEFORE Step 3 mutates the tree.
|
|
1918
|
+
const mode = flagPreTag ? 'pre-tag' : (flagPreFlight ? 'pre-flight' : 'full');
|
|
1919
|
+
const filtered = checklist.filter(function (p) { return p.applies_to.indexOf(mode) !== -1; });
|
|
1920
|
+
const results = [];
|
|
1921
|
+
const failed = [];
|
|
1922
|
+
for (const p of filtered) {
|
|
1923
|
+
let r;
|
|
1924
|
+
try { r = await p.run(); }
|
|
1925
|
+
catch (e) { r = { ok: false, finding: 'point ' + p.id + ' threw: ' + e.message, detail: {} }; }
|
|
1926
|
+
results.push({ id: p.id, label: p.label, ok: !!r.ok, finding: r.finding || null, detail: r.detail || null });
|
|
1927
|
+
if (!r.ok) failed.push(p.id);
|
|
1928
|
+
}
|
|
1929
|
+
return {
|
|
1930
|
+
mode: mode,
|
|
1931
|
+
points: results,
|
|
1932
|
+
failed_points: failed,
|
|
1933
|
+
summary: { total: results.length, passed: results.length - failed.length, failed: failed.length },
|
|
1934
|
+
};
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
// -- Loop-fires gate: --dogfood-acceptance (Phase 146 Plan 04) -------
|
|
1938
|
+
//
|
|
1939
|
+
// The Phase 146 milestone gate. Composes the 6 ACPT dogfood drivers (Phase 146
|
|
1940
|
+
// Plans 01-03: ACPT-01 engine-fires, ACPT-02 websearch-hat-scoped, ACPT-03
|
|
1941
|
+
// first-material-explore, ACPT-04 filing-cascade-surfaces, ACPT-05
|
|
1942
|
+
// brain-derive-tier-rise; Phase 150.5-03: ACPT-06 dial-atomic-emission)
|
|
1943
|
+
// by spawning each as a child `node <driver>` process and aggregating
|
|
1944
|
+
// their exit codes. Modeled on runAcceptance (the checklist-point shape +
|
|
1945
|
+
// the spawnSync child-process model from the verify-release point), but with
|
|
1946
|
+
// its OWN exit-code contract: 0 = every ACPT hermetic leg passed; non-zero =
|
|
1947
|
+
// a leg failed. NOT a class flag -- the class-flag-always-exit-0 invariant
|
|
1948
|
+
// does NOT apply (per the ROADMAP mandate: exit 0 = milestone ships as "Larry
|
|
1949
|
+
// Reaches", non-zero = the milestone is renamed).
|
|
1950
|
+
//
|
|
1951
|
+
// Canon Part 8: --dogfood-acceptance adds ZERO network surface in doctor
|
|
1952
|
+
// itself -- it only spawns LOCAL node drivers. The ACPT-05 driver's LIVE
|
|
1953
|
+
// mode_a arm self-skips when Brain is unreachable (Plan 03), so the gate is
|
|
1954
|
+
// green on the hermetic legs WITHOUT a reachable Brain. The gate requires the
|
|
1955
|
+
// hermetic mode_b arm, NEVER the live mode_a arm (T-146-14): a down Brain can
|
|
1956
|
+
// never make the gate fake mode_a.
|
|
1957
|
+
//
|
|
1958
|
+
// Each ACPT point: { id, label, run: () -> { ok, finding, detail } } where run
|
|
1959
|
+
// spawns `node <driver>` via child_process.spawnSync with a 120000ms timeout
|
|
1960
|
+
// and ok = (status === 0).
|
|
1961
|
+
|
|
1962
|
+
const ACPT_DRIVERS = [
|
|
1963
|
+
{ id: 'ACPT-01', file: 'test-acpt-01-engine-fires.cjs', label: 'navigation engine fires (routing_source legacy->engine on a real fired sensor)' },
|
|
1964
|
+
{ id: 'ACPT-02', file: 'test-acpt-02-websearch-hat-scoped.cjs', label: 'WebSearch is hat-scoped (the external-tool affordance respects the hat boundary)' },
|
|
1965
|
+
{ id: 'ACPT-03', file: 'test-acpt-03-first-material-explore.cjs', label: 'first material auto-explores (room non-empty by turn 2)' },
|
|
1966
|
+
{ id: 'ACPT-04', file: 'test-acpt-04-filing-cascade-surfaces.cjs', label: 'filing surfaces a cross-relationship cascade mid-session' },
|
|
1967
|
+
{ id: 'ACPT-05', file: 'test-acpt-05-brain-derive-tier-rise.cjs', label: 'BRAIN.md derive raises tier_mode above tier_0 (hermetic; live mode_a self-skips)' },
|
|
1968
|
+
{ id: 'ACPT-06', file: 'test-acpt-06-dial-atomic-emission.cjs', label: 'dial text + card contract emit atomically on the engine arm (real sensor fired from the production turn shape); legacy emits neither' },
|
|
1969
|
+
];
|
|
1970
|
+
|
|
1971
|
+
async function runDogfoodAcceptance(opts) {
|
|
1972
|
+
opts = opts || {};
|
|
1973
|
+
const pluginRoot = opts.pluginRoot || PLUGIN_ROOT;
|
|
1974
|
+
const cp = require('child_process');
|
|
1975
|
+
const testsDir = path.join(pluginRoot, 'tests');
|
|
1976
|
+
|
|
1977
|
+
const points = ACPT_DRIVERS.map(function (d) {
|
|
1978
|
+
return {
|
|
1979
|
+
id: d.id,
|
|
1980
|
+
label: d.label,
|
|
1981
|
+
run: function () {
|
|
1982
|
+
const driverPath = path.join(testsDir, d.file);
|
|
1983
|
+
if (!fs.existsSync(driverPath)) {
|
|
1984
|
+
return { ok: false, finding: 'driver missing: ' + d.file, detail: { file: d.file } };
|
|
1985
|
+
}
|
|
1986
|
+
const r = cp.spawnSync('node', [driverPath], { encoding: 'utf8', timeout: 120000 });
|
|
1987
|
+
const ok = r.status === 0;
|
|
1988
|
+
const finding = ok ? null : ('driver ' + d.file + ' exited ' + r.status + (r.signal ? ' (signal ' + r.signal + ')' : ''));
|
|
1989
|
+
return {
|
|
1990
|
+
ok: ok,
|
|
1991
|
+
finding: finding,
|
|
1992
|
+
detail: {
|
|
1993
|
+
file: d.file,
|
|
1994
|
+
status: r.status,
|
|
1995
|
+
signal: r.signal || null,
|
|
1996
|
+
stdoutTail: (r.stdout || '').slice(-500),
|
|
1997
|
+
stderrTail: (r.stderr || '').slice(-500),
|
|
1998
|
+
},
|
|
1999
|
+
};
|
|
2000
|
+
},
|
|
2001
|
+
};
|
|
2002
|
+
});
|
|
2003
|
+
|
|
2004
|
+
const results = [];
|
|
2005
|
+
const failed = [];
|
|
2006
|
+
for (const p of points) {
|
|
2007
|
+
let r;
|
|
2008
|
+
try { r = p.run(); }
|
|
2009
|
+
catch (e) { r = { ok: false, finding: 'point ' + p.id + ' threw: ' + e.message, detail: {} }; }
|
|
2010
|
+
results.push({ id: p.id, label: p.label, ok: !!r.ok, finding: r.finding || null, detail: r.detail || null });
|
|
2011
|
+
if (!r.ok) failed.push(p.id);
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
return {
|
|
2015
|
+
gate: 'dogfood-acceptance',
|
|
2016
|
+
points: results,
|
|
2017
|
+
failed_points: failed,
|
|
2018
|
+
summary: { total: results.length, passed: results.length - failed.length, failed: failed.length },
|
|
2019
|
+
};
|
|
2020
|
+
}
|
|
2021
|
+
|
|
2022
|
+
// -- Phase 150-08 (MEM-09 / D-09): the claim harness gate (doctor --claims) -----
|
|
2023
|
+
//
|
|
2024
|
+
// A SIBLING of runAcceptance: it shells tests/claim-harness/run-all-claims.sh
|
|
2025
|
+
// (the C1..C7 falsifiable public-site-claim drivers + the gates) and returns its
|
|
2026
|
+
// OWN self-contained exit code, NOT folded into the class roster. Canon Part 8:
|
|
2027
|
+
// the harness is LOCAL/hermetic; the Brain LIVE arms self-skip via
|
|
2028
|
+
// class-m-brain-smoke, so the gate is CI-green without a reachable Brain.
|
|
2029
|
+
//
|
|
2030
|
+
// Self-test hook (mirrors DOCTOR_TEST_FAIL_POINT): when DOCTOR_CLAIM_FAIL_POINT
|
|
2031
|
+
// is set (any non-empty value) AND the in-test mode is active, the gate
|
|
2032
|
+
// synthesizes a failure of the harness so its own failure path is testable
|
|
2033
|
+
// WITHOUT corrupting the real harness. The accepted token is 'run-all-claims'
|
|
2034
|
+
// (the single point this gate owns); any other non-empty value also trips, so a
|
|
2035
|
+
// test can simply set DOCTOR_CLAIM_FAIL_POINT=run-all-claims.
|
|
2036
|
+
function runClaims(opts) {
|
|
2037
|
+
opts = opts || {};
|
|
2038
|
+
const pluginRoot = opts.pluginRoot || PLUGIN_ROOT;
|
|
2039
|
+
const cp = require('child_process');
|
|
2040
|
+
const harness = path.join(pluginRoot, 'tests', 'claim-harness', 'run-all-claims.sh');
|
|
2041
|
+
|
|
2042
|
+
// DOCTOR_CLAIM_FAIL_POINT self-test (mirrors the DOCTOR_TEST_FAIL_POINT
|
|
2043
|
+
// precedent at runAcceptance). inTestMode mirrors the acceptance self-test
|
|
2044
|
+
// gate: it is on when the env var is set (the test owns the process).
|
|
2045
|
+
const failPoint = process.env.DOCTOR_CLAIM_FAIL_POINT;
|
|
2046
|
+
if (typeof failPoint === 'string' && failPoint.length > 0) {
|
|
2047
|
+
return {
|
|
2048
|
+
gate: 'claims',
|
|
2049
|
+
ok: false,
|
|
2050
|
+
synthesized: true,
|
|
2051
|
+
fail_point: failPoint,
|
|
2052
|
+
status: null,
|
|
2053
|
+
finding: 'DOCTOR_CLAIM_FAIL_POINT synthesized a failure of the claim harness (' + failPoint + ')',
|
|
2054
|
+
summary: { total: 1, passed: 0, failed: 1 },
|
|
2055
|
+
};
|
|
2056
|
+
}
|
|
2057
|
+
|
|
2058
|
+
if (!fs.existsSync(harness)) {
|
|
2059
|
+
return {
|
|
2060
|
+
gate: 'claims',
|
|
2061
|
+
ok: false,
|
|
2062
|
+
synthesized: false,
|
|
2063
|
+
status: null,
|
|
2064
|
+
finding: 'claim harness missing: tests/claim-harness/run-all-claims.sh',
|
|
2065
|
+
summary: { total: 1, passed: 0, failed: 1 },
|
|
2066
|
+
};
|
|
2067
|
+
}
|
|
2068
|
+
|
|
2069
|
+
const r = cp.spawnSync('bash', [harness], { encoding: 'utf8', timeout: 300000 });
|
|
2070
|
+
const ok = r.status === 0;
|
|
2071
|
+
return {
|
|
2072
|
+
gate: 'claims',
|
|
2073
|
+
ok: ok,
|
|
2074
|
+
synthesized: false,
|
|
2075
|
+
status: r.status,
|
|
2076
|
+
signal: r.signal || null,
|
|
2077
|
+
finding: ok ? null : ('claim harness exited ' + r.status + (r.signal ? ' (signal ' + r.signal + ')' : '')),
|
|
2078
|
+
stdoutTail: (r.stdout || '').slice(-800),
|
|
2079
|
+
stderrTail: (r.stderr || '').slice(-400),
|
|
2080
|
+
summary: { total: 1, passed: ok ? 1 : 0, failed: ok ? 0 : 1 },
|
|
2081
|
+
};
|
|
2082
|
+
}
|
|
2083
|
+
|
|
2084
|
+
|
|
2085
|
+
// -- Renderers -------------------------------------------------------
|
|
2086
|
+
|
|
2087
|
+
// Phase 95.1-04 retrofit: 4-zone Shape E (Action Report) per skills/ui-system/SKILL.md.
|
|
2088
|
+
// Zone 1 header literal '-- MindrianOS -- doctor -- {stage} --' (compact form when
|
|
2089
|
+
// projected lines > 30 per D-16). Zone 2 body holds per-class drift findings using
|
|
2090
|
+
// only the 12-glyph vocabulary. Zone 3 reserved for room-proactive signals (D-17;
|
|
2091
|
+
// always empty in Phase 95.1, room-proactive integration deferred). F.1 Next Move
|
|
2092
|
+
// selector renders before Zone 4 when drift detected without --fix (D-18; structural
|
|
2093
|
+
// marker only, canonical AskUserQuestion implementation deferred to Phase 88.2 -- see
|
|
2094
|
+
// .planning/phases/95.1-mos-doctor-drift-detection-and-self-heal/f1-selector-deferred.md).
|
|
2095
|
+
// Zone 4 action footer always present (D-12). Renderer is purely structural; Larry
|
|
2096
|
+
// handles narrative interpretation per D-19.
|
|
2097
|
+
function renderHumanReport(report) {
|
|
2098
|
+
const lines = [];
|
|
2099
|
+
|
|
2100
|
+
// Compute stage label for Zone 1 per D-12 stage values.
|
|
2101
|
+
let stage = 'no-drift';
|
|
2102
|
+
if (report.fixRequested && report.classARecovered) stage = 'recovered';
|
|
2103
|
+
else if (report.fixRequested) stage = 'recovering';
|
|
2104
|
+
else if (report.drift && report.drift.detected) stage = 'drift-detected';
|
|
2105
|
+
// Additional class-driven drift detection added by 95.1 plans 05+06 will populate
|
|
2106
|
+
// report.checks; iterate and aggregate stage accordingly.
|
|
2107
|
+
|
|
2108
|
+
// Compute body rows BEFORE deciding header density (D-16).
|
|
2109
|
+
const bodyRows = [];
|
|
2110
|
+
|
|
2111
|
+
// Class A -- install-cache (existing logic preserved, glyphs swapped to vocabulary).
|
|
2112
|
+
//
|
|
2113
|
+
// Phase 126 Plan-01 fix: when classARecovered is set (recovery succeeded),
|
|
2114
|
+
// the renderer MUST emit BOTH `✓ recovered to <version>` AND `backup <path>`
|
|
2115
|
+
// lines per the commands/doctor.md Step 3 contract -- INCLUDING the
|
|
2116
|
+
// missing-install case (installResult.status === 'missing') where the
|
|
2117
|
+
// pre-fix renderer fell into the `cannot read state` branch and never
|
|
2118
|
+
// emitted the contract lines (2026-05-13 Windows dogfood finding 3a).
|
|
2119
|
+
// Recovery-succeeded takes precedence over status-shape branching.
|
|
2120
|
+
if (report.classARecovered) {
|
|
2121
|
+
// Drift was detected (either status==='ok' + cmp<0 OR status==='missing'
|
|
2122
|
+
// with available cache; see drift detection at line ~2634-2649) and was
|
|
2123
|
+
// closed by performRecoveryAtomic. Use the drift-detected layout for
|
|
2124
|
+
// continuity with the live-stale recovery case.
|
|
2125
|
+
bodyRows.push(` ${C.yellow}■${C.reset} install-cache ${C.yellow}⚠${C.reset} drift detected`);
|
|
2126
|
+
// Show the live -> recovered transition. For the missing-install case
|
|
2127
|
+
// report.install.version is undefined; show `missing` instead so the
|
|
2128
|
+
// line is still informative.
|
|
2129
|
+
const liveLabel = report.install.version || (report.install.status === 'missing' ? 'missing' : 'unknown');
|
|
2130
|
+
bodyRows.push(` ${C.dim}live${C.reset} ${C.yellow}${liveLabel}${C.reset} ${C.dim}→${C.reset} ${C.green}${report.cache.latest || report.classARecovered.recoveredVersion}${C.reset}`);
|
|
2131
|
+
bodyRows.push(` ${C.green}✓${C.reset} recovered to ${C.green}${report.classARecovered.recoveredVersion}${C.reset}`);
|
|
2132
|
+
// Backup line: omit when backup is null (missing-install case -- nothing
|
|
2133
|
+
// existed to rename; performRecoveryAtomic returned backup=null at
|
|
2134
|
+
// line ~370 of this file). The contract example specifies the line is
|
|
2135
|
+
// emitted WHEN a backup exists, not unconditionally.
|
|
2136
|
+
if (report.classARecovered.backup) {
|
|
2137
|
+
bodyRows.push(` ${C.dim}backup ${report.classARecovered.backup}${C.reset}`);
|
|
2138
|
+
}
|
|
2139
|
+
} else if (report.install.status === 'ok' && report.cache.status === 'ok') {
|
|
2140
|
+
if (report.drift.detected) {
|
|
2141
|
+
bodyRows.push(` ${C.yellow}■${C.reset} install-cache ${C.yellow}⚠${C.reset} drift detected`);
|
|
2142
|
+
bodyRows.push(` ${C.dim}live${C.reset} ${C.yellow}${report.install.version}${C.reset} ${C.dim}→${C.reset} ${C.green}${report.cache.latest}${C.reset}`);
|
|
2143
|
+
if (report.fixRequested) {
|
|
2144
|
+
// RCA doctor-marketplace-cache-drift-deadlock (2026-06-12): three-way
|
|
2145
|
+
// branch replaces the single `recovery failed: <err || 'unknown'>`
|
|
2146
|
+
// line. The literal 'unknown' rendered whenever recovery was never
|
|
2147
|
+
// ATTEMPTED (gated/skipped) -- classARecovered and recoveryError both
|
|
2148
|
+
// null is not a failure, it is an un-run.
|
|
2149
|
+
if (report.recoveryError) {
|
|
2150
|
+
bodyRows.push(` ${C.red}⚠${C.reset} recovery failed: ${report.recoveryError}`);
|
|
2151
|
+
} else if (report.recoverySkipped) {
|
|
2152
|
+
bodyRows.push(` ${C.yellow}⚠${C.reset} recovery skipped (topology marketplace-cache -- legacy install dir is vestigial)`);
|
|
2153
|
+
} else {
|
|
2154
|
+
bodyRows.push(` ${C.dim}recovery not attempted (gated)${C.reset}`);
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
} else {
|
|
2158
|
+
bodyRows.push(` ${C.green}■${C.reset} install-cache ${C.green}✓${C.reset} healthy ${C.dim}(${report.install.version})${C.reset}`);
|
|
2159
|
+
}
|
|
2160
|
+
} else {
|
|
2161
|
+
bodyRows.push(` ${C.red}■${C.reset} install-cache ${C.red}⚠${C.reset} cannot read state`);
|
|
2162
|
+
if (report.install.status !== 'ok') bodyRows.push(` ${C.dim}install: ${report.install.detail || report.install.status}${C.reset}`);
|
|
2163
|
+
if (report.cache.status !== 'ok') bodyRows.push(` ${C.dim}cache: ${report.cache.detail || report.cache.status}${C.reset}`);
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2166
|
+
// Dev source consistency.
|
|
2167
|
+
if (report.dev.status === 'ok') {
|
|
2168
|
+
bodyRows.push(` ${C.green}■${C.reset} dev-source ${C.green}✓${C.reset} consistent ${C.dim}(${report.dev.pluginJson})${C.reset}`);
|
|
2169
|
+
} else if (report.dev.status === 'mismatch') {
|
|
2170
|
+
bodyRows.push(` ${C.yellow}■${C.reset} dev-source ${C.yellow}⚠${C.reset} version mismatch`);
|
|
2171
|
+
bodyRows.push(` ${C.dim}plugin.json ${report.dev.pluginJson}${C.reset}`);
|
|
2172
|
+
bodyRows.push(` ${C.dim}package.json ${report.dev.packageJson}${C.reset}`);
|
|
2173
|
+
}
|
|
2174
|
+
// status=skip silently omitted (acceptable for end users without dev clone).
|
|
2175
|
+
|
|
2176
|
+
// Slot for class B/C/D/E/F/G rows -- populated by Plans 95.1-05 + 95.1-06
|
|
2177
|
+
// and Plan 106-03 via report.checks.* extension. Each new check reads from
|
|
2178
|
+
// report.checks[name] and pushes rows in the same Shape-E format:
|
|
2179
|
+
// filled-square + class label + status glyph + detail. Glyphs match the
|
|
2180
|
+
// existing class A pattern: green check for ok, yellow warn for warn,
|
|
2181
|
+
// red warn for error (mirrors line 1067 'cannot read state'), dim slash
|
|
2182
|
+
// for skip. Class F UI compliance scanner approves these glyphs (U+2717
|
|
2183
|
+
// BALLOT X is forbidden; U+26A0 WARN and U+2298 CIRCLED DIVISION SLASH
|
|
2184
|
+
// are approved per the 12-glyph vocabulary).
|
|
2185
|
+
if (report.checks) {
|
|
2186
|
+
// Class G (statusline-visibility) + class H (install-incomplete) hand-coded
|
|
2187
|
+
// render branches RETIRED (Phase 217 Plan 05): both rows -- and class H's
|
|
2188
|
+
// hint sub-line -- now come from the generic loop below + the runner's
|
|
2189
|
+
// action_lines[]. After this plan the ONLY non-generic render path left is
|
|
2190
|
+
// class A (install-cache), the sanctioned carve-out (Plan 06).
|
|
2191
|
+
for (const [name, check] of Object.entries(report.checks)) {
|
|
2192
|
+
if (name === 'install-cache') continue; // already handled above.
|
|
2193
|
+
// Skip entries are omitted, matching the plugin-enabled-state precedent
|
|
2194
|
+
// above and computeSummary's skip omission, so visible rows stay in exact
|
|
2195
|
+
// parity with the Summary tally.
|
|
2196
|
+
if (!check || check.status === 'skip') continue;
|
|
2197
|
+
let glyph;
|
|
2198
|
+
let color;
|
|
2199
|
+
if (check.status === 'ok') { glyph = '✓'; color = C.green; }
|
|
2200
|
+
else if (check.status === 'warn') { glyph = '⚠'; color = C.yellow; }
|
|
2201
|
+
else if (check.status === 'error') { glyph = '⚠'; color = C.red; }
|
|
2202
|
+
else { glyph = '⊘'; color = C.dim; }
|
|
2203
|
+
bodyRows.push(' ' + color + '■' + C.reset + ' ' + name.padEnd(28) + color + glyph + C.reset + ' ' + (check.detail || check.status));
|
|
2204
|
+
// Phase 217 Plan-01: action_lines sub-line support. The structural home
|
|
2205
|
+
// for the hand-coded hint sub-lines (class H/K/N) that later migration
|
|
2206
|
+
// plans delete: a cadence:always module returns action_lines[] and the
|
|
2207
|
+
// generic renderer prints one dim `-> ` sub-line per entry.
|
|
2208
|
+
if (Array.isArray(check.action_lines)) {
|
|
2209
|
+
for (const line of check.action_lines) {
|
|
2210
|
+
bodyRows.push(' ' + C.dim + '-> ' + line + C.reset);
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
}
|
|
2215
|
+
|
|
2216
|
+
// Render --fix outcomes so a fix attempt is never invisible. Every --fix
|
|
2217
|
+
// dispatch pushes its result into report.recovered; class A recovery already
|
|
2218
|
+
// renders via classARecovered above, so the install-cache tool is guarded to
|
|
2219
|
+
// avoid a double line.
|
|
2220
|
+
if (report.fixRequested && Array.isArray(report.recovered) && report.recovered.length > 0) {
|
|
2221
|
+
for (const entry of report.recovered) {
|
|
2222
|
+
if (!entry) continue;
|
|
2223
|
+
if (entry.status === 'skip') continue; // skip entries omitted.
|
|
2224
|
+
if (entry.tool === 'install-cache') continue; // already rendered via classARecovered.
|
|
2225
|
+
let glyph;
|
|
2226
|
+
let color;
|
|
2227
|
+
if (entry.status === 'ok') { glyph = '✓'; color = C.green; }
|
|
2228
|
+
else if (entry.status === 'partial') { glyph = '⚠'; color = C.yellow; }
|
|
2229
|
+
else if (entry.status === 'error') { glyph = '⚠'; color = C.red; }
|
|
2230
|
+
else { glyph = '⊘'; color = C.dim; }
|
|
2231
|
+
bodyRows.push(' ' + color + glyph + C.reset + ' fix ' + (entry.tool || 'unknown-tool') + ': ' + (entry.detail || entry.status));
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2235
|
+
// Summary line. Aggregate counts from install/dev/drift signals plus any
|
|
2236
|
+
// report.checks.* rows that Plans 05+06 will register.
|
|
2237
|
+
const summary = computeSummary(report);
|
|
2238
|
+
bodyRows.push('');
|
|
2239
|
+
bodyRows.push(` ${C.dim}Summary: ${C.green}${summary.healthy}${C.dim} healthy / ${C.yellow}${summary.drift}${C.dim} drift / ${C.red}${summary.warnings}${C.dim} warnings${C.reset}`);
|
|
2240
|
+
|
|
2241
|
+
// F.1 Next Move selector (D-18) -- only when drift AND !fix. Phase 95.1 ships
|
|
2242
|
+
// a STRUCTURAL marker. Larry handles conversational selection per D-19.
|
|
2243
|
+
// Phase 88.2 will replace this with the canonical AskUserQuestion primitive.
|
|
2244
|
+
// See .planning/phases/95.1-.../f1-selector-deferred.md for the deferral note.
|
|
2245
|
+
if (!report.fixRequested && (report.drift.detected || summary.drift > 0)) {
|
|
2246
|
+
bodyRows.push('');
|
|
2247
|
+
bodyRows.push(` ${C.cyan}[F.1 Next Move]${C.reset}`);
|
|
2248
|
+
bodyRows.push(` ${C.cyan}▶${C.reset} Run ${C.cyan}/mos:doctor --fix${C.reset}`);
|
|
2249
|
+
bodyRows.push(` ${C.dim}▷${C.reset} Defer`);
|
|
2250
|
+
bodyRows.push(` ${C.dim}▷${C.reset} Free-Text`);
|
|
2251
|
+
}
|
|
2252
|
+
|
|
2253
|
+
// Zone 4 action footer -- ALWAYS present (D-12 mandate). 2-3 grounded /mos:
|
|
2254
|
+
// commands. Primary action uses U+25B6, alternatives use U+25B7.
|
|
2255
|
+
bodyRows.push('');
|
|
2256
|
+
if (report.drift.detected || summary.drift > 0) {
|
|
2257
|
+
bodyRows.push(` ${C.cyan}▶ /mos:doctor --fix --all${C.reset} ${C.dim}# auto-recover all drift classes${C.reset}`);
|
|
2258
|
+
bodyRows.push(` ${C.dim}▷${C.reset} ${C.cyan}/mos:rooms${C.reset} ${C.dim}# inspect known rooms${C.reset}`);
|
|
2259
|
+
bodyRows.push(` ${C.dim}▷${C.reset} ${C.cyan}/mos:doctor --json${C.reset} ${C.dim}# machine-readable output${C.reset}`);
|
|
2260
|
+
} else {
|
|
2261
|
+
bodyRows.push(` ${C.cyan}▶ /mos:status${C.reset} ${C.dim}# room state overview${C.reset}`);
|
|
2262
|
+
bodyRows.push(` ${C.dim}▷${C.reset} ${C.cyan}/mos:doctor --all${C.reset} ${C.dim}# re-run all classes${C.reset}`);
|
|
2263
|
+
bodyRows.push(` ${C.dim}▷${C.reset} ${C.cyan}/mos:doctor --json${C.reset} ${C.dim}# machine-readable output${C.reset}`);
|
|
2264
|
+
}
|
|
2265
|
+
|
|
2266
|
+
// Zone 1 header (decided AFTER body rows for density rule, D-16).
|
|
2267
|
+
// Zone 3 (Intelligence Strip) stays empty in 95.1 per D-17 -- room-proactive
|
|
2268
|
+
// integration deferred. Default behavior: omit Zone 3 entirely.
|
|
2269
|
+
const projectedLineCount = bodyRows.length + 4; // header + blank + body + trailing blank.
|
|
2270
|
+
if (projectedLineCount > 30) {
|
|
2271
|
+
// Compact header per D-16 density rule.
|
|
2272
|
+
const classCount = Object.keys(report.checks || { 'install-cache': 1 }).length;
|
|
2273
|
+
lines.push(`${C.dim}-- doctor: ${classCount} drift classes checked --${C.reset}`);
|
|
2274
|
+
} else {
|
|
2275
|
+
// Standard Zone 1 header per D-12. Literal prefix '-- MindrianOS -- doctor'
|
|
2276
|
+
// is asserted by tests/test-doctor-ui-self-compliant.cjs scenario 3.
|
|
2277
|
+
lines.push(`${C.dim}-- MindrianOS -- doctor -- ${stage} --${C.reset}`);
|
|
2278
|
+
}
|
|
2279
|
+
lines.push('');
|
|
2280
|
+
for (const row of bodyRows) lines.push(row);
|
|
2281
|
+
lines.push('');
|
|
2282
|
+
|
|
2283
|
+
return lines.join('\n');
|
|
2284
|
+
}
|
|
2285
|
+
|
|
2286
|
+
// Helper for Zone 2 summary aggregation. Walks install/dev/drift signals plus
|
|
2287
|
+
// any report.checks.* rows that Plans 05+06 will register and returns
|
|
2288
|
+
// healthy / drift / warning totals.
|
|
2289
|
+
function computeSummary(report) {
|
|
2290
|
+
let healthy = 0;
|
|
2291
|
+
let drift = 0;
|
|
2292
|
+
let warnings = 0;
|
|
2293
|
+
// Class A -- install-cache + drift signal.
|
|
2294
|
+
//
|
|
2295
|
+
// Phase 126 Plan-01 fix: after a successful classA recovery, the drift was
|
|
2296
|
+
// RESOLVED -- the summary must NOT report `1 drift` while the header says
|
|
2297
|
+
// `recovered` (Option A: decrement-after-recovery, minimum surface delta
|
|
2298
|
+
// per Canon Part 7). 2026-05-13 Windows dogfood finding 3a flagged the
|
|
2299
|
+
// `0 healthy / 1 drift / 0 warnings` line as semantically inconsistent
|
|
2300
|
+
// with the recovered header. After recovery the install is on the cache's
|
|
2301
|
+
// latest version; classify as healthy.
|
|
2302
|
+
if (report.classARecovered) {
|
|
2303
|
+
healthy += 1;
|
|
2304
|
+
} else if (report.install.status === 'ok' && report.cache.status === 'ok' && !report.drift.detected) {
|
|
2305
|
+
healthy += 1;
|
|
2306
|
+
} else if (report.drift.detected) {
|
|
2307
|
+
drift += 1;
|
|
2308
|
+
} else {
|
|
2309
|
+
warnings += 1;
|
|
2310
|
+
}
|
|
2311
|
+
// Dev source.
|
|
2312
|
+
if (report.dev.status === 'ok') healthy += 1;
|
|
2313
|
+
else if (report.dev.status === 'mismatch') warnings += 1;
|
|
2314
|
+
// Future class B/C/D/E/F: walk report.checks.
|
|
2315
|
+
if (report.checks) {
|
|
2316
|
+
for (const [name, check] of Object.entries(report.checks)) {
|
|
2317
|
+
if (name === 'install-cache') continue;
|
|
2318
|
+
if (check && check.status === 'ok') healthy += 1;
|
|
2319
|
+
else if (check && (check.status === 'warn' || check.status === 'error')) drift += 1;
|
|
2320
|
+
// status='skip' omitted from totals.
|
|
2321
|
+
}
|
|
2322
|
+
}
|
|
2323
|
+
return { healthy, drift, warnings };
|
|
2324
|
+
}
|
|
2325
|
+
|
|
2326
|
+
// -- Accumulative engine (Phase 139-02, S2) --------------------------
|
|
2327
|
+
//
|
|
2328
|
+
// The version-dispatch substrate that converts doctor's frozen Phase-95 class
|
|
2329
|
+
// roster into a forward-healing engine. Generalizes the proven
|
|
2330
|
+
// lib/core/install-state.cjs::migrateIfNeeded shape: iterate the hand-maintained
|
|
2331
|
+
// module registry (data/doctor-modules.json), run every module whose
|
|
2332
|
+
// introduced_version is in the (applied_through, running] window, DEFER any
|
|
2333
|
+
// future-version module (introduced > running), and never re-run an
|
|
2334
|
+
// already-applied one (the persisted watermark at ~/.mindrian/doctor-applied.json
|
|
2335
|
+
// never regresses -> re-running is a no-op).
|
|
2336
|
+
//
|
|
2337
|
+
// In THIS plan the registry is seeded EMPTY of organ modules, so the loop runs
|
|
2338
|
+
// zero modules -- but the window math + runner-dispatch wiring is complete, so
|
|
2339
|
+
// Plan 03 only adds a registry entry + a runner file, no engine code.
|
|
2340
|
+
//
|
|
2341
|
+
// Canon Part 8: LOCAL file reads only. Zero network surface; runs no Brain query.
|
|
2342
|
+
|
|
2343
|
+
// Normalize a version string for semver comparison. Prefer semver.valid (which
|
|
2344
|
+
// PRESERVES prerelease labels so 1.13.1-beta.4 vs 1.13.1-beta.3 compare
|
|
2345
|
+
// correctly) and fall back to semver.coerce only for sloppy input that is not
|
|
2346
|
+
// already valid semver. Returns null when neither resolves.
|
|
2347
|
+
function _normalizeVersion(v) {
|
|
2348
|
+
if (typeof v !== 'string' || !v) return null;
|
|
2349
|
+
const valid = semver.valid(v);
|
|
2350
|
+
if (valid) return valid;
|
|
2351
|
+
const coerced = semver.coerce(v);
|
|
2352
|
+
return coerced ? coerced.version : null;
|
|
2353
|
+
}
|
|
2354
|
+
|
|
2355
|
+
/**
|
|
2356
|
+
* Run the accumulative-engine selector.
|
|
2357
|
+
*
|
|
2358
|
+
* @param {object} [flags] CLI flags (fix, dryRun, json, flags, classFlagsActive)
|
|
2359
|
+
* OR a test opts seam ({ home, running, registry, dryRun }). The test
|
|
2360
|
+
* seam keys take precedence so the test never depends on the live
|
|
2361
|
+
* registry. `flags` is the full parsed CLI flags object and
|
|
2362
|
+
* `classFlagsActive` the class-flag-mode boolean; both drive the
|
|
2363
|
+
* Phase 217 cadence/flag gates (default {} + false => baseWanted true,
|
|
2364
|
+
* keeping every pre-217 caller byte-compatible).
|
|
2365
|
+
* @returns {{
|
|
2366
|
+
* status: 'ok'|'skip',
|
|
2367
|
+
* detail?: string,
|
|
2368
|
+
* running: string|null,
|
|
2369
|
+
* applied_through: string|null,
|
|
2370
|
+
* selected: Array<{id, introduced_version}>,
|
|
2371
|
+
* deferred: Array<{id, introduced_version}>,
|
|
2372
|
+
* findings: Array<object>,
|
|
2373
|
+
* advanced: boolean,
|
|
2374
|
+
* checks: Object<string, object>,
|
|
2375
|
+
* recovered: Array<object>
|
|
2376
|
+
* }}
|
|
2377
|
+
*/
|
|
2378
|
+
// resolveAlwaysRunner(mod): resolve the check/fix pair for a cadence:always
|
|
2379
|
+
// module. Test seams (mod._check / mod._fix) win; otherwise require the runner
|
|
2380
|
+
// file relative to the plugin root and read its exported check/fix. Never
|
|
2381
|
+
// throws (a require failure leaves the fn null -> the caller records an error
|
|
2382
|
+
// finding), so one bad module cannot crash the always-pass (T-217-01).
|
|
2383
|
+
function resolveAlwaysRunner(mod) {
|
|
2384
|
+
let checkFn = (typeof mod._check === 'function') ? mod._check : null;
|
|
2385
|
+
let fixFn = (typeof mod._fix === 'function') ? mod._fix : null;
|
|
2386
|
+
if ((!checkFn || !fixFn) && mod.runner) {
|
|
2387
|
+
try {
|
|
2388
|
+
const runnerPath = path.isAbsolute(mod.runner)
|
|
2389
|
+
? mod.runner
|
|
2390
|
+
: path.join(PLUGIN_ROOT, mod.runner);
|
|
2391
|
+
const runnerMod = require(runnerPath);
|
|
2392
|
+
if (!checkFn && typeof runnerMod.check === 'function') checkFn = runnerMod.check;
|
|
2393
|
+
if (!fixFn && typeof runnerMod.fix === 'function') fixFn = runnerMod.fix;
|
|
2394
|
+
} catch (_) { /* leave null; the caller soft-fails to an error finding */ }
|
|
2395
|
+
}
|
|
2396
|
+
return { checkFn, fixFn };
|
|
2397
|
+
}
|
|
2398
|
+
|
|
2399
|
+
function runAccumulativeEngine(flags) {
|
|
2400
|
+
const opts = flags || {};
|
|
2401
|
+
const home = opts.home || process.env.HOME || process.env.USERPROFILE || HOME;
|
|
2402
|
+
const dryRun = !!opts.dryRun;
|
|
2403
|
+
const wantFix = !!opts.fix;
|
|
2404
|
+
// Phase 217 Plan-01: the full parsed CLI flags object + the class-flag-mode
|
|
2405
|
+
// boolean, both carried so the engine can reproduce the exact gate that
|
|
2406
|
+
// today lives at the main() call site. Defaults keep every pre-217 caller
|
|
2407
|
+
// (the hermetic selector tests pass only { home, running, registry })
|
|
2408
|
+
// byte-compatible: flags {} + classFlagsActive false => baseWanted true.
|
|
2409
|
+
const callerFlags = (opts.flags && typeof opts.flags === 'object') ? opts.flags : {};
|
|
2410
|
+
const classFlagsActive = !!opts.classFlagsActive;
|
|
2411
|
+
// baseWanted is the class-A/N "runs on a bare / --all / --fix run, but a
|
|
2412
|
+
// class-flag-only run (e.g. --room-md) skips it" gate -- the exact predicate
|
|
2413
|
+
// that used to guard the engine call in main() (line ~5534).
|
|
2414
|
+
const baseWanted = !!(callerFlags.all || callerFlags.fix || !classFlagsActive);
|
|
2415
|
+
|
|
2416
|
+
// 1. Resolve the RUNNING version of record. Test seam: opts.running wins.
|
|
2417
|
+
// Otherwise read the active plugin.json via checkInstallVersion().
|
|
2418
|
+
let runningRaw = (typeof opts.running !== 'undefined') ? opts.running : null;
|
|
2419
|
+
if (runningRaw === null && typeof opts.running === 'undefined') {
|
|
2420
|
+
try {
|
|
2421
|
+
const iv = checkInstallVersion();
|
|
2422
|
+
runningRaw = iv && iv.version ? iv.version : null;
|
|
2423
|
+
} catch (_) {
|
|
2424
|
+
runningRaw = null;
|
|
2425
|
+
}
|
|
2426
|
+
// Phase 217 Plan 05/06: when checkInstallVersion cannot resolve the install
|
|
2427
|
+
// (e.g. a hermetic test that overrides HOME so ~/.claude/plugins is not
|
|
2428
|
+
// where checkInstallVersion looked, or an install loaded from an unexpected
|
|
2429
|
+
// path) OR resolves a NON-CLEAN-SEMVER version (a 4-component ship version
|
|
2430
|
+
// like 1.12.5.1 that only survives via semver.coerce, which DROPS the 4th
|
|
2431
|
+
// component -- 1.12.5.1 -> 1.12.5 -- and would then place the install BELOW
|
|
2432
|
+
// a diagnostic's introduced_version on the deferred-guard number line, so a
|
|
2433
|
+
// cadence:always check that ships WITH this code would be wrongly DEFERRED),
|
|
2434
|
+
// fall back to the version of record of the CODE that is actually running --
|
|
2435
|
+
// PLUGIN_ROOT/.claude-plugin/plugin.json. That path is HOME-independent, so
|
|
2436
|
+
// the accumulative engine never goes dark (the Pitfall-1 silence the
|
|
2437
|
+
// always-cadence design exists to prevent, and the regression class I test
|
|
2438
|
+
// i.6 pins: a 1.12.5.1 install must still surface report.checks
|
|
2439
|
+
// ['install-state']). Real installs with CLEAN semver resolve via
|
|
2440
|
+
// checkInstallVersion first, so this is a no-op for them. Only reached when
|
|
2441
|
+
// opts.running was undefined (the real CLI path); an EXPLICIT opts.running:
|
|
2442
|
+
// null still yields the soft-fail skip. semver.valid rejects 4-component and
|
|
2443
|
+
// accepts prereleases (1.13.0-beta.13 stays as-resolved), which is exactly
|
|
2444
|
+
// the clean-vs-coerced distinction the deferred-guard needs.
|
|
2445
|
+
if (!semver.valid(typeof runningRaw === 'string' ? runningRaw : '')) {
|
|
2446
|
+
try {
|
|
2447
|
+
const devPluginJson = path.join(PLUGIN_ROOT, '.claude-plugin', 'plugin.json');
|
|
2448
|
+
if (fs.existsSync(devPluginJson)) {
|
|
2449
|
+
const j = JSON.parse(fs.readFileSync(devPluginJson, 'utf8'));
|
|
2450
|
+
runningRaw = j && j.version ? j.version : null;
|
|
2451
|
+
}
|
|
2452
|
+
} catch (_) {
|
|
2453
|
+
runningRaw = null;
|
|
2454
|
+
}
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
2457
|
+
const running = _normalizeVersion(runningRaw);
|
|
2458
|
+
if (!running) {
|
|
2459
|
+
return {
|
|
2460
|
+
status: 'skip',
|
|
2461
|
+
detail: 'running version unresolved',
|
|
2462
|
+
running: null,
|
|
2463
|
+
applied_through: null,
|
|
2464
|
+
selected: [],
|
|
2465
|
+
deferred: [],
|
|
2466
|
+
findings: [],
|
|
2467
|
+
advanced: false,
|
|
2468
|
+
checks: {},
|
|
2469
|
+
recovered: [],
|
|
2470
|
+
};
|
|
2471
|
+
}
|
|
2472
|
+
|
|
2473
|
+
// 2. Read the registry (soft-fail to { modules: [] } on parse error).
|
|
2474
|
+
// Test seam: opts.registry wins.
|
|
2475
|
+
let registry;
|
|
2476
|
+
if (opts.registry && typeof opts.registry === 'object') {
|
|
2477
|
+
registry = opts.registry;
|
|
2478
|
+
} else {
|
|
2479
|
+
try {
|
|
2480
|
+
registry = JSON.parse(fs.readFileSync(DOCTOR_MODULES_PATH, 'utf8'));
|
|
2481
|
+
} catch (_) {
|
|
2482
|
+
registry = { modules: [] };
|
|
2483
|
+
}
|
|
2484
|
+
}
|
|
2485
|
+
const modules = Array.isArray(registry.modules) ? registry.modules : [];
|
|
2486
|
+
|
|
2487
|
+
// 2b. Split registry modules by CADENCE (Phase 217 Plan-01, Pitfall 1):
|
|
2488
|
+
// - cadence:'always' = a per-invocation DIAGNOSTIC. Its check() runs on
|
|
2489
|
+
// EVERY doctor invocation, immune to the watermark. This is what stops
|
|
2490
|
+
// a migrated D-01 check from going permanently silent after one run.
|
|
2491
|
+
// - cadence:'once' (or ABSENT, back-compat) = a watermark-gated HEAL
|
|
2492
|
+
// (umbilical's existing semantics). The Plan 02 contract test forces the
|
|
2493
|
+
// registry file itself to declare cadence explicitly.
|
|
2494
|
+
const onceModules = [];
|
|
2495
|
+
const alwaysModules = [];
|
|
2496
|
+
for (const mod of modules) {
|
|
2497
|
+
if (!mod || typeof mod.id !== 'string') continue;
|
|
2498
|
+
if (mod.cadence === 'always') alwaysModules.push(mod);
|
|
2499
|
+
else onceModules.push(mod);
|
|
2500
|
+
}
|
|
2501
|
+
|
|
2502
|
+
// 3. Read the watermark. null applied_through means "first run; window is
|
|
2503
|
+
// (null, running] = everything <= running". Read unconditionally so the
|
|
2504
|
+
// return shape reports applied_through even when the once-pass is gated off.
|
|
2505
|
+
const wm = readDoctorApplied(home);
|
|
2506
|
+
const appliedThrough = wm.applied_through; // semver string | null
|
|
2507
|
+
|
|
2508
|
+
const selected = [];
|
|
2509
|
+
const deferred = [];
|
|
2510
|
+
const findings = [];
|
|
2511
|
+
const ranModuleIds = [];
|
|
2512
|
+
let anyHardError = false;
|
|
2513
|
+
let advanced = false;
|
|
2514
|
+
|
|
2515
|
+
// 4. ONCE pass (watermark-gated heals; umbilical). Behavior byte-identical to
|
|
2516
|
+
// pre-217 with ONE relocation: the entire once-pass (window selection +
|
|
2517
|
+
// runner invocation + watermark advance) is now gated behind baseWanted --
|
|
2518
|
+
// the exact `flags.all || flags.fix || !classFlagsActive` predicate that
|
|
2519
|
+
// used to live at the main() call site. A class-flag-only run (--room-md)
|
|
2520
|
+
// therefore never runs the once-heals and never advances the watermark,
|
|
2521
|
+
// preserving the pre-217 invariant. When the gate is false the once-pass
|
|
2522
|
+
// yields selected:[], advanced:false.
|
|
2523
|
+
if (baseWanted) {
|
|
2524
|
+
// SELECT once modules where introduced_version is in (applied_through, running].
|
|
2525
|
+
// Lower bound EXCLUSIVE (already-applied), upper bound INCLUSIVE (current).
|
|
2526
|
+
// A future-version module (introduced > running) is DEFERRED, never run --
|
|
2527
|
+
// the future-version-DEFER shape from install-state.migrateIfNeeded.
|
|
2528
|
+
const lower = appliedThrough || '0.0.0';
|
|
2529
|
+
for (const mod of onceModules) {
|
|
2530
|
+
const introduced = _normalizeVersion(mod.introduced_version);
|
|
2531
|
+
if (!introduced) {
|
|
2532
|
+
// Malformed introduced_version -> skip (cannot place in the window).
|
|
2533
|
+
continue;
|
|
2534
|
+
}
|
|
2535
|
+
const aboveLower = semver.gt(introduced, _normalizeVersion(lower) || '0.0.0');
|
|
2536
|
+
const atOrBelowRunning = semver.lte(introduced, running);
|
|
2537
|
+
if (!atOrBelowRunning) {
|
|
2538
|
+
deferred.push(mod);
|
|
2539
|
+
continue;
|
|
2540
|
+
}
|
|
2541
|
+
if (aboveLower) {
|
|
2542
|
+
selected.push(mod);
|
|
2543
|
+
}
|
|
2544
|
+
// introduced <= applied_through -> already applied -> skip silently.
|
|
2545
|
+
}
|
|
2546
|
+
|
|
2547
|
+
// 5. Run each selected module's runner, wrapped in try/catch so one bad
|
|
2548
|
+
// module cannot crash doctor (soft-fail -> error finding, loop continues).
|
|
2549
|
+
// --fix runs the module fixer; read-only runs the checker only.
|
|
2550
|
+
for (const mod of selected) {
|
|
2551
|
+
try {
|
|
2552
|
+
// Resolve the runner. Test seam: an in-test runner is attached as
|
|
2553
|
+
// mod._check / mod._fix. Otherwise require the runner file relative to
|
|
2554
|
+
// the plugin root and invoke its exported check(opts)/fix(opts) entry.
|
|
2555
|
+
let entryResult;
|
|
2556
|
+
if (wantFix && typeof mod._fix === 'function') {
|
|
2557
|
+
entryResult = mod._fix({ home, running, dryRun });
|
|
2558
|
+
} else if (typeof mod._check === 'function') {
|
|
2559
|
+
entryResult = mod._check({ home, running, dryRun });
|
|
2560
|
+
} else if (mod.runner) {
|
|
2561
|
+
const runnerPath = path.isAbsolute(mod.runner)
|
|
2562
|
+
? mod.runner
|
|
2563
|
+
: path.join(PLUGIN_ROOT, mod.runner);
|
|
2564
|
+
const runnerMod = require(runnerPath);
|
|
2565
|
+
const entry = (wantFix && typeof runnerMod.fix === 'function')
|
|
2566
|
+
? runnerMod.fix
|
|
2567
|
+
: runnerMod.check;
|
|
2568
|
+
if (typeof entry !== 'function') {
|
|
2569
|
+
throw new Error('runner ' + mod.runner + ' exposes no ' + (wantFix ? 'fix/check' : 'check') + ' function');
|
|
2570
|
+
}
|
|
2571
|
+
entryResult = entry({ home, running, dryRun, fix: wantFix });
|
|
2572
|
+
} else {
|
|
2573
|
+
throw new Error('module ' + mod.id + ' has no runner');
|
|
2574
|
+
}
|
|
2575
|
+
findings.push(Object.assign({ id: mod.id, status: 'ok' }, entryResult && typeof entryResult === 'object' ? entryResult : {}));
|
|
2576
|
+
ranModuleIds.push(mod.id);
|
|
2577
|
+
} catch (err) {
|
|
2578
|
+
anyHardError = true;
|
|
2579
|
+
findings.push({ id: mod.id, status: 'error', detail: (err && err.message) || 'runner threw' });
|
|
2580
|
+
}
|
|
2581
|
+
}
|
|
2582
|
+
|
|
2583
|
+
// 6. Idempotency: advance the watermark to running ONLY on a non-dry-run with
|
|
2584
|
+
// no hard runner error. Re-running with the watermark already at running
|
|
2585
|
+
// selects ZERO modules (window (running, running] is empty) -> no-op.
|
|
2586
|
+
// An EMPTY window (zero selected) on a clean run still advances so the
|
|
2587
|
+
// ledger records the heal-through point.
|
|
2588
|
+
if (!dryRun && !anyHardError) {
|
|
2589
|
+
advanced = writeDoctorApplied(home, { appliedThrough: running, ranModuleIds });
|
|
2590
|
+
}
|
|
2591
|
+
}
|
|
2592
|
+
|
|
2593
|
+
// 7. ALWAYS pass (Phase 217 Plan-01, Pitfall 1). For each cadence:always
|
|
2594
|
+
// module in registry array order: flag-gate, deferred-guard (upper bound
|
|
2595
|
+
// only -- NO watermark lower bound, that is the whole point), run check()
|
|
2596
|
+
// on EVERY invocation, optional fix-then-recheck. Results land ONLY in
|
|
2597
|
+
// alwaysChecks (never in the once-path findings[]), so the selector-test
|
|
2598
|
+
// assertions on findings stay valid and Pitfall 3 is impossible.
|
|
2599
|
+
const alwaysChecks = {};
|
|
2600
|
+
const recovered = [];
|
|
2601
|
+
for (const mod of alwaysModules) {
|
|
2602
|
+
// a. Flag gate. null/undefined flag => the class-A/N baseWanted gate (runs
|
|
2603
|
+
// under bare / --all / --fix when no class flag is active). A named flag
|
|
2604
|
+
// => the module runs only when that parseArgs flag is set.
|
|
2605
|
+
const flagKey = mod.flag;
|
|
2606
|
+
const wanted = (flagKey === null || flagKey === undefined)
|
|
2607
|
+
? baseWanted
|
|
2608
|
+
: !!callerFlags[flagKey];
|
|
2609
|
+
if (!wanted) continue; // no report entry -- matches today's absent-when-not-requested behavior.
|
|
2610
|
+
|
|
2611
|
+
// b. Deferred guard (registry/code-skew safety ONLY): a module introduced in
|
|
2612
|
+
// a FUTURE version is deferred. There is NO lower-bound / watermark check
|
|
2613
|
+
// for always modules -- that immunity is the point (Pitfall 1).
|
|
2614
|
+
const introduced = _normalizeVersion(mod.introduced_version);
|
|
2615
|
+
if (introduced && semver.gt(introduced, running)) {
|
|
2616
|
+
deferred.push(mod);
|
|
2617
|
+
continue;
|
|
2618
|
+
}
|
|
2619
|
+
|
|
2620
|
+
// c. Build ctx. `checks` is the accumulating always-result map so a later
|
|
2621
|
+
// module can read an earlier same-invocation result (Plan 06 reads
|
|
2622
|
+
// ctx.checks['install-state']).
|
|
2623
|
+
const ctx = { home, running, dryRun, fix: wantFix, flags: callerFlags, checks: alwaysChecks };
|
|
2624
|
+
|
|
2625
|
+
// d. Resolve + run check() inside try/catch (soft-fail -> error row,
|
|
2626
|
+
// self-DoS mitigation T-217-01).
|
|
2627
|
+
const { checkFn, fixFn } = resolveAlwaysRunner(mod);
|
|
2628
|
+
let result;
|
|
2629
|
+
try {
|
|
2630
|
+
if (typeof checkFn !== 'function') {
|
|
2631
|
+
throw new Error('module ' + mod.id + ' exposes no check function');
|
|
2632
|
+
}
|
|
2633
|
+
result = checkFn(ctx);
|
|
2634
|
+
} catch (err) {
|
|
2635
|
+
result = { status: 'error', detail: (err && err.message) || 'runner threw' };
|
|
2636
|
+
}
|
|
2637
|
+
|
|
2638
|
+
// e. Fix-then-recheck flow (mirrors the ad-hoc class E/G/H pattern). Only
|
|
2639
|
+
// when --fix was requested, the module declares fix_supported === true,
|
|
2640
|
+
// a fixer exists, the check surfaced a warn/error, and the result is not
|
|
2641
|
+
// explicitly marked unrecoverable.
|
|
2642
|
+
if (wantFix && mod.fix_supported === true && typeof fixFn === 'function'
|
|
2643
|
+
&& result && (result.status === 'warn' || result.status === 'error')
|
|
2644
|
+
&& result.recoverable !== false) {
|
|
2645
|
+
let fixRes;
|
|
2646
|
+
try {
|
|
2647
|
+
fixRes = fixFn(Object.assign({}, ctx, { check_result: result }));
|
|
2648
|
+
} catch (err) {
|
|
2649
|
+
fixRes = { status: 'error', detail: (err && err.message) || 'fixer threw' };
|
|
2650
|
+
}
|
|
2651
|
+
// Re-run the check so the reported result reflects the post-fix state.
|
|
2652
|
+
try {
|
|
2653
|
+
result = checkFn(ctx);
|
|
2654
|
+
} catch (err) {
|
|
2655
|
+
result = { status: 'error', detail: (err && err.message) || 'runner threw' };
|
|
2656
|
+
}
|
|
2657
|
+
result.fix_result = fixRes;
|
|
2658
|
+
// Recovered plumbing. Object.assign order matters: the fixer's own `tool`
|
|
2659
|
+
// field wins when present (preserves class E's existing recovery record
|
|
2660
|
+
// shape, e.g. tool:'generate-section-intelligence').
|
|
2661
|
+
if (fixRes && Array.isArray(fixRes.recoveries)) {
|
|
2662
|
+
for (const rec of fixRes.recoveries) {
|
|
2663
|
+
recovered.push(Object.assign({ tool: mod.id }, rec));
|
|
2664
|
+
}
|
|
2665
|
+
} else {
|
|
2666
|
+
recovered.push(Object.assign({ tool: mod.id }, fixRes));
|
|
2667
|
+
}
|
|
2668
|
+
}
|
|
2669
|
+
|
|
2670
|
+
// f. Always results go ONLY into alwaysChecks, NEVER the once-path findings[].
|
|
2671
|
+
alwaysChecks[mod.id] = result;
|
|
2672
|
+
}
|
|
2673
|
+
|
|
2674
|
+
return {
|
|
2675
|
+
status: 'ok',
|
|
2676
|
+
running,
|
|
2677
|
+
applied_through: appliedThrough,
|
|
2678
|
+
selected: selected.map((m) => ({ id: m.id, introduced_version: m.introduced_version })),
|
|
2679
|
+
deferred: deferred.map((m) => ({ id: m.id, introduced_version: m.introduced_version })),
|
|
2680
|
+
findings,
|
|
2681
|
+
advanced: !!advanced,
|
|
2682
|
+
checks: alwaysChecks,
|
|
2683
|
+
recovered,
|
|
2684
|
+
};
|
|
2685
|
+
}
|
|
2686
|
+
|
|
2687
|
+
// -- Phase 150.9 Class Q / heal-arm helpers --------------------------
|
|
2688
|
+
//
|
|
2689
|
+
// resolveGsdTools(): locate gsd-tools.cjs mirroring the workflows/health.md:52
|
|
2690
|
+
// _GSD_TOOLS resolver order. NEVER hardcodes a single path. Honors the
|
|
2691
|
+
// MINDRIAN_DOCTOR_GSD_TOOLS env override FIRST (hermetic-test injection +
|
|
2692
|
+
// operator override). Returns an absolute path or null (-> Class Q emits a
|
|
2693
|
+
// 'skip' finding; the run never crashes). Canon Part 8: locates a LOCAL tool;
|
|
2694
|
+
// no network.
|
|
2695
|
+
function resolveGsdTools() {
|
|
2696
|
+
const fsLocal = require('fs');
|
|
2697
|
+
// 0. Explicit override (tests + operators).
|
|
2698
|
+
const override = process.env.MINDRIAN_DOCTOR_GSD_TOOLS;
|
|
2699
|
+
if (override) {
|
|
2700
|
+
return fsLocal.existsSync(override) ? override : null;
|
|
2701
|
+
}
|
|
2702
|
+
const SHIM = 'gsd-tools.cjs';
|
|
2703
|
+
const candidates = [];
|
|
2704
|
+
// 1. RUNTIME_DIR / repo-root gsd-core.
|
|
2705
|
+
const runtimeRoot = process.env.RUNTIME_DIR || path.resolve(__dirname, '..');
|
|
2706
|
+
candidates.push(path.join(runtimeRoot, 'gsd-core', 'bin', SHIM));
|
|
2707
|
+
candidates.push(path.join(runtimeRoot, '.claude', 'gsd-core', 'bin', SHIM));
|
|
2708
|
+
// 2. HOME-installed gsd-core (the common dev/CI topology).
|
|
2709
|
+
const home = process.env.HOME || process.env.USERPROFILE;
|
|
2710
|
+
if (home) candidates.push(path.join(home, '.claude', 'gsd-core', 'bin', SHIM));
|
|
2711
|
+
for (const c of candidates) {
|
|
2712
|
+
try { if (fsLocal.existsSync(c)) return c; } catch (_e) { /* keep looking */ }
|
|
2713
|
+
}
|
|
2714
|
+
return null;
|
|
2715
|
+
}
|
|
2716
|
+
|
|
2717
|
+
// extractPhaseToken(message): pull a phase label (digits + dots, e.g. "150.7")
|
|
2718
|
+
// out of a W007 ROADMAP-gap message. Returns the token or null.
|
|
2719
|
+
function extractPhaseToken(message) {
|
|
2720
|
+
const s = String(message || '');
|
|
2721
|
+
// "Phase 150.7 exists on disk but not in ROADMAP.md"
|
|
2722
|
+
let m = s.match(/Phase\s+([0-9][0-9.]*)\b/i);
|
|
2723
|
+
if (m) return m[1];
|
|
2724
|
+
// Fallback: a leading "<NN>-<name>" directory token.
|
|
2725
|
+
m = s.match(/\b([0-9][0-9.]*)-[a-z]/i);
|
|
2726
|
+
return m ? m[1] : null;
|
|
2727
|
+
}
|
|
2728
|
+
|
|
2729
|
+
// extractPlanRef(message): from an I001 "NNN-PP-PLAN.md has no SUMMARY.md"
|
|
2730
|
+
// message recover { phase, planFile, planLabel }. Returns null when no
|
|
2731
|
+
// "<plan>-PLAN.md" token is present.
|
|
2732
|
+
function extractPlanRef(message) {
|
|
2733
|
+
const s = String(message || '');
|
|
2734
|
+
const m = s.match(/([0-9][0-9.]*-[0-9A-Za-z.]+-PLAN\.md)/);
|
|
2735
|
+
if (!m) return null;
|
|
2736
|
+
const planFile = m[1];
|
|
2737
|
+
const planLabel = planFile.replace(/-PLAN\.md$/i, '');
|
|
2738
|
+
const phaseMatch = planLabel.match(/^([0-9][0-9.]*)-/);
|
|
2739
|
+
const phase = phaseMatch ? phaseMatch[1] : null;
|
|
2740
|
+
return { phase, planFile, planLabel };
|
|
2741
|
+
}
|
|
2742
|
+
|
|
2743
|
+
// findPhaseDir(repoRoot, phaseToken): locate the on-disk phase directory under
|
|
2744
|
+
// .planning/phases/ whose name starts with "<phase>-". Returns absolute path or
|
|
2745
|
+
// null. Used only by the --fix heal arm to place a SUMMARY stub.
|
|
2746
|
+
function findPhaseDir(repoRoot, phaseToken) {
|
|
2747
|
+
if (!phaseToken) return null;
|
|
2748
|
+
const fsLocal = require('fs');
|
|
2749
|
+
const phasesDir = path.join(repoRoot, '.planning', 'phases');
|
|
2750
|
+
let entries;
|
|
2751
|
+
try { entries = fsLocal.readdirSync(phasesDir, { withFileTypes: true }); }
|
|
2752
|
+
catch (_e) { return null; }
|
|
2753
|
+
const prefix = phaseToken + '-';
|
|
2754
|
+
for (const ent of entries) {
|
|
2755
|
+
if (ent.isDirectory() && ent.name.indexOf(prefix) === 0) {
|
|
2756
|
+
return path.join(phasesDir, ent.name);
|
|
2757
|
+
}
|
|
2758
|
+
}
|
|
2759
|
+
return null;
|
|
2760
|
+
}
|
|
2761
|
+
|
|
2762
|
+
// -- Main ------------------------------------------------------------
|
|
2763
|
+
|
|
2764
|
+
// ---------------------------------------------------------------------------
|
|
2765
|
+
// Quick task 260705-jeq: --report-registration-bug assembler (D-01, D-02).
|
|
2766
|
+
//
|
|
2767
|
+
// A NEW read-only reporting mode. Every FACT comes from an EXISTING evidence
|
|
2768
|
+
// collector (Canon Part 7 -- reuse, never reimplement): checkPluginEnabled,
|
|
2769
|
+
// readInstalledPluginsVersion, readLegacyConfigPin, detectMarketplaceCacheInstall,
|
|
2770
|
+
// collectVersionOfRecord, computeVersionDivergences, resolveActivePluginRoot, and
|
|
2771
|
+
// Task 1's sweepCommandRegistration. The mode proves each locally-checkable cause
|
|
2772
|
+
// of "valid install yet a command did not register" is CLEAN, so what remains is
|
|
2773
|
+
// the confirmed host-side core bug -- nothing to fix locally, escalate to
|
|
2774
|
+
// Anthropic. Diagnostic-only: the output NEVER carries a "healthy"/"fixed" status.
|
|
2775
|
+
// Canon Part 8: LOCAL reads + two guarded best-effort child probes (git, claude
|
|
2776
|
+
// --version), each via execFileSync with an argument array (never a shell string).
|
|
2777
|
+
// ---------------------------------------------------------------------------
|
|
2778
|
+
|
|
2779
|
+
// Best-effort Claude Code version; null (never a throw) when unobtainable, so the
|
|
2780
|
+
// report still assembles offline (RESEARCH open question 2 resolved).
|
|
2781
|
+
function probeClaudeVersion() {
|
|
2782
|
+
try {
|
|
2783
|
+
const cp = require('child_process');
|
|
2784
|
+
const out = cp.execFileSync('claude', ['--version'], {
|
|
2785
|
+
encoding: 'utf8', timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'],
|
|
2786
|
+
});
|
|
2787
|
+
const v = String(out || '').trim();
|
|
2788
|
+
return v || null;
|
|
2789
|
+
} catch (_) { return null; }
|
|
2790
|
+
}
|
|
2791
|
+
|
|
2792
|
+
// Guarded git-dirty probe of the marketplace clone dir. Swallow-and-note when git
|
|
2793
|
+
// or the dir is absent (the report must assemble anyway). execFileSync with an
|
|
2794
|
+
// argument array only (T-jeq-01: never a string-interpolated shell command).
|
|
2795
|
+
function probeMarketplaceCloneGit(home) {
|
|
2796
|
+
const dir = path.join(home, '.claude', 'plugins', 'marketplaces', 'mindrian-marketplace');
|
|
2797
|
+
try {
|
|
2798
|
+
if (!fs.existsSync(dir)) {
|
|
2799
|
+
return { available: false, dir, note: 'clone dir absent' };
|
|
2800
|
+
}
|
|
2801
|
+
const cp = require('child_process');
|
|
2802
|
+
const out = cp.execFileSync('git', ['-C', dir, 'status', '--porcelain'], {
|
|
2803
|
+
encoding: 'utf8', timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'],
|
|
2804
|
+
});
|
|
2805
|
+
return { available: true, dir, dirty: String(out || '').trim().length > 0 };
|
|
2806
|
+
} catch (e) {
|
|
2807
|
+
return { available: false, dir, note: 'git probe unavailable (' + ((e && e.message) || 'error') + ')' };
|
|
2808
|
+
}
|
|
2809
|
+
}
|
|
2810
|
+
|
|
2811
|
+
function countCommandFilesAt(root) {
|
|
2812
|
+
try {
|
|
2813
|
+
const dir = path.join(root, 'commands');
|
|
2814
|
+
return fs.readdirSync(dir).filter((f) => f.endsWith('.md')).length;
|
|
2815
|
+
} catch (_) { return 0; }
|
|
2816
|
+
}
|
|
2817
|
+
|
|
2818
|
+
// Assemble the full report data object (all facts, no rendering). Pure gather.
|
|
2819
|
+
function assembleRegistrationBugData() {
|
|
2820
|
+
const home = process.env.HOME || process.env.USERPROFILE || HOME;
|
|
2821
|
+
|
|
2822
|
+
let resolver = { root: null, topology: 'not-found', source: 'not-found' };
|
|
2823
|
+
try { resolver = resolveActivePluginRoot(); } catch (_) { /* degrade to not-found */ }
|
|
2824
|
+
const rootUsed = (resolver && resolver.root) || PLUGIN_ROOT;
|
|
2825
|
+
|
|
2826
|
+
let record = null;
|
|
2827
|
+
try { record = JSON.parse(fs.readFileSync(path.join(home, '.mindrian', 'install-state.json'), 'utf8')); } catch (_) { /* absent */ }
|
|
2828
|
+
|
|
2829
|
+
const versions = collectVersionOfRecord(home, resolver, record);
|
|
2830
|
+
const divergences = computeVersionDivergences(versions);
|
|
2831
|
+
const IP = readInstalledPluginsVersion(home);
|
|
2832
|
+
const pluginEnabled = require(path.join(__dirname, '..', 'lib', 'core', 'check-plugin-enabled.cjs')).checkPluginEnabled();
|
|
2833
|
+
const legacyPin = readLegacyConfigPin(home);
|
|
2834
|
+
const marketplaceCache = detectMarketplaceCacheInstall(home);
|
|
2835
|
+
const installPath = readInstalledPluginsInstallPath(home);
|
|
2836
|
+
const gitProbe = probeMarketplaceCloneGit(home);
|
|
2837
|
+
const commandCount = countCommandFilesAt(rootUsed);
|
|
2838
|
+
const claudeVersion = probeClaudeVersion();
|
|
2839
|
+
const { sweepCommandRegistration } = require(path.join(__dirname, '..', 'lib', 'core', 'command-registration-check.cjs'));
|
|
2840
|
+
const sweep = sweepCommandRegistration(path.join(rootUsed, 'commands'));
|
|
2841
|
+
|
|
2842
|
+
// The 7 ruled-out causes, each CLEAN or FLAGGED (fix-locally-first).
|
|
2843
|
+
const ruledOut = [];
|
|
2844
|
+
|
|
2845
|
+
// (a) install-cache drift (class A).
|
|
2846
|
+
(function () {
|
|
2847
|
+
let status = 'CLEAN';
|
|
2848
|
+
let detail = 'active install matches the newest marketplace cache';
|
|
2849
|
+
if (marketplaceCache && IP !== 'unknown' && String(marketplaceCache.version) !== String(IP)) {
|
|
2850
|
+
status = 'FLAGGED';
|
|
2851
|
+
detail = 'installed_plugins.json is ' + IP + ' but the newest marketplace cache is ' + marketplaceCache.version;
|
|
2852
|
+
} else if (!marketplaceCache) {
|
|
2853
|
+
detail = 'no marketplace cache present to compare against (nothing to drift)';
|
|
2854
|
+
}
|
|
2855
|
+
ruledOut.push({ key: 'install-cache-drift', label: 'install-cache drift', status, detail });
|
|
2856
|
+
}());
|
|
2857
|
+
|
|
2858
|
+
// (b) enabledPlugins silent-disable (class N / DRIFT-12).
|
|
2859
|
+
(function () {
|
|
2860
|
+
let status = 'CLEAN';
|
|
2861
|
+
let detail = pluginEnabled.enabled === true
|
|
2862
|
+
? 'plugin key ' + (pluginEnabled.key || '(unresolved)') + ' is enabled'
|
|
2863
|
+
: 'enabledPlugins key absent (legacy / unknown -- not the silent-disable bug)';
|
|
2864
|
+
if (pluginEnabled.enabled === false) {
|
|
2865
|
+
status = 'FLAGGED';
|
|
2866
|
+
detail = 'enabledPlugins[' + pluginEnabled.key + '] is false (DRIFT-12 silent-disable); run: claude plugin enable ' + pluginEnabled.key;
|
|
2867
|
+
}
|
|
2868
|
+
ruledOut.push({ key: 'enabled-plugins-silent-disable', label: 'enabledPlugins silent-disable', status, detail });
|
|
2869
|
+
}());
|
|
2870
|
+
|
|
2871
|
+
// (c) legacy config.json pin vs installed_plugins.json (F11 class).
|
|
2872
|
+
(function () {
|
|
2873
|
+
let status = 'CLEAN';
|
|
2874
|
+
let detail = legacyPin
|
|
2875
|
+
? 'legacy plugins/config.json pin (' + legacyPin.version + ') agrees with installed_plugins.json (' + IP + ')'
|
|
2876
|
+
: 'no legacy plugins/config.json pin present (fresh-install N/A)';
|
|
2877
|
+
if (legacyPin && IP !== 'unknown' && String(legacyPin.version) !== String(IP)) {
|
|
2878
|
+
status = 'FLAGGED';
|
|
2879
|
+
detail = 'legacy plugins/config.json pins ' + legacyPin.version + ' but installed_plugins.json is ' + IP + ' (F11 legacy-pin drift can poison command registration)';
|
|
2880
|
+
}
|
|
2881
|
+
ruledOut.push({ key: 'legacy-config-pin', label: 'legacy config.json pin', status, detail });
|
|
2882
|
+
}());
|
|
2883
|
+
|
|
2884
|
+
// (d) marketplace clone git-dirty state.
|
|
2885
|
+
(function () {
|
|
2886
|
+
let status = 'CLEAN';
|
|
2887
|
+
let detail;
|
|
2888
|
+
if (!gitProbe.available) {
|
|
2889
|
+
detail = 'marketplace clone git state not probeable (' + gitProbe.note + ') -- treated as not-applicable';
|
|
2890
|
+
} else if (gitProbe.dirty) {
|
|
2891
|
+
status = 'FLAGGED';
|
|
2892
|
+
detail = 'marketplace clone at ' + gitProbe.dir + ' has a dirty working tree (uncommitted local edits can shadow the shipped command files)';
|
|
2893
|
+
} else {
|
|
2894
|
+
detail = 'marketplace clone working tree is clean (' + gitProbe.dir + ')';
|
|
2895
|
+
}
|
|
2896
|
+
ruledOut.push({ key: 'marketplace-clone-git-dirty', label: 'marketplace clone git-dirty', status, detail });
|
|
2897
|
+
}());
|
|
2898
|
+
|
|
2899
|
+
// (e) version-of-record legs all agree.
|
|
2900
|
+
(function () {
|
|
2901
|
+
let status = 'CLEAN';
|
|
2902
|
+
let detail = 'all known version-of-record legs agree';
|
|
2903
|
+
if (divergences && divergences.length) {
|
|
2904
|
+
status = 'FLAGGED';
|
|
2905
|
+
detail = divergences.length + ' version leg disagreement(s): '
|
|
2906
|
+
+ divergences.map((d) => d.from + '(' + d.fromValue + ') vs ' + d.to + '(' + d.toValue + ')').join('; ');
|
|
2907
|
+
}
|
|
2908
|
+
ruledOut.push({ key: 'version-of-record-legs', label: 'version-of-record legs', status, detail });
|
|
2909
|
+
}());
|
|
2910
|
+
|
|
2911
|
+
// (f) command files present on disk.
|
|
2912
|
+
(function () {
|
|
2913
|
+
const status = commandCount > 0 ? 'CLEAN' : 'FLAGGED';
|
|
2914
|
+
const detail = commandCount > 0
|
|
2915
|
+
? commandCount + ' command files present on disk at the active root'
|
|
2916
|
+
: 'no command files found at the active root (' + rootUsed + ')';
|
|
2917
|
+
ruledOut.push({ key: 'command-files-present', label: 'command files present', status, detail });
|
|
2918
|
+
}());
|
|
2919
|
+
|
|
2920
|
+
// (g) static precondition sweep (Task 1 helper) zero FAILs.
|
|
2921
|
+
(function () {
|
|
2922
|
+
const status = sweep.pass ? 'CLEAN' : 'FLAGGED';
|
|
2923
|
+
const detail = sweep.pass
|
|
2924
|
+
? 'static precondition sweep passed (' + sweep.scanned + ' scanned, 0 FAIL)'
|
|
2925
|
+
: sweep.failures.length + ' precondition FAIL(s): ' + sweep.failures.map((f) => f.file + '[' + f.rule + ']').join(', ');
|
|
2926
|
+
ruledOut.push({ key: 'static-precondition-sweep', label: 'static precondition sweep', status, detail });
|
|
2927
|
+
}());
|
|
2928
|
+
|
|
2929
|
+
return {
|
|
2930
|
+
mode: 'report-registration-bug',
|
|
2931
|
+
escalation: 'diagnostic only - escalate to Anthropic, nothing to fix locally',
|
|
2932
|
+
environment: {
|
|
2933
|
+
plugin_version_legs: versions,
|
|
2934
|
+
divergences,
|
|
2935
|
+
installed_plugins_version: IP,
|
|
2936
|
+
install_path: installPath,
|
|
2937
|
+
active_root: rootUsed,
|
|
2938
|
+
topology: (resolver && resolver.topology) || 'not-found',
|
|
2939
|
+
platform: process.platform,
|
|
2940
|
+
os_release: os.release(),
|
|
2941
|
+
claude_version: claudeVersion,
|
|
2942
|
+
command_count: commandCount,
|
|
2943
|
+
manifest_note: 'plugin.json declares NO `commands` field -- the standard auto-discovery contract every surveyed marketplace plugin uses; the failure is in the host registration of that contract, not the manifest',
|
|
2944
|
+
},
|
|
2945
|
+
ruled_out: ruledOut,
|
|
2946
|
+
control_test: 'a control test against another installed marketplace plugin\'s commands reproduced the identical failure -- the defect is not specific to this plugin',
|
|
2947
|
+
provenance: [
|
|
2948
|
+
'.planning/debug/every-mos-command-unknown.md',
|
|
2949
|
+
'.planning/debug/windows-install-update-ux.md',
|
|
2950
|
+
],
|
|
2951
|
+
};
|
|
2952
|
+
}
|
|
2953
|
+
|
|
2954
|
+
// Render the paste-ready human report from the data object.
|
|
2955
|
+
function renderRegistrationBugText(data) {
|
|
2956
|
+
const env = data.environment;
|
|
2957
|
+
const lines = [];
|
|
2958
|
+
lines.push('=== MindrianOS command-registration -- paste-ready Anthropic bug report ===');
|
|
2959
|
+
lines.push('');
|
|
2960
|
+
lines.push(data.escalation.toUpperCase());
|
|
2961
|
+
lines.push('This is a plugin command-registration subsystem report. Root cause is a');
|
|
2962
|
+
lines.push('confirmed Claude Code host-side core bug: a valid install whose commands do');
|
|
2963
|
+
lines.push('not register. Every locally-checkable cause below is ruled out first.');
|
|
2964
|
+
lines.push('');
|
|
2965
|
+
lines.push('-- Environment facts --');
|
|
2966
|
+
const legs = env.plugin_version_legs || {};
|
|
2967
|
+
lines.push(' version legs: IP=' + legs.IP + ' AV=' + legs.AV + ' SR=' + legs.SR + ' LV=' + legs.LV + ' PB=' + legs.PB);
|
|
2968
|
+
lines.push(' version divergences: ' + (env.divergences && env.divergences.length ? env.divergences.length : 'none'));
|
|
2969
|
+
lines.push(' platform: ' + env.platform + ' / ' + env.os_release);
|
|
2970
|
+
lines.push(' Claude Code version: ' + (env.claude_version || 'fill in: run claude --version'));
|
|
2971
|
+
lines.push(' active plugin root: ' + env.active_root + ' (topology: ' + env.topology + ')');
|
|
2972
|
+
lines.push(' command files on disk: ' + env.command_count);
|
|
2973
|
+
lines.push(' manifest: ' + env.manifest_note);
|
|
2974
|
+
lines.push('');
|
|
2975
|
+
lines.push('-- Ruled out (each local cause proven CLEAN, or FLAGGED fix-locally-first) --');
|
|
2976
|
+
for (const r of data.ruled_out) {
|
|
2977
|
+
let line = ' [' + r.status + '] ' + r.label + ' -- ' + r.detail;
|
|
2978
|
+
if (r.status === 'FLAGGED') line += ' (fix locally first - this occurrence may not be the core bug)';
|
|
2979
|
+
lines.push(line);
|
|
2980
|
+
}
|
|
2981
|
+
lines.push('');
|
|
2982
|
+
lines.push('-- Control test --');
|
|
2983
|
+
lines.push(' ' + data.control_test);
|
|
2984
|
+
lines.push('');
|
|
2985
|
+
lines.push('-- Provenance (ruled-out SIBLING causes, distinct from this core bug) --');
|
|
2986
|
+
for (const p of data.provenance) lines.push(' ' + p);
|
|
2987
|
+
lines.push('');
|
|
2988
|
+
return lines.join('\n');
|
|
2989
|
+
}
|
|
2990
|
+
|
|
2991
|
+
// Sibling dispatch entry. Always exits 0 when the report assembles (D-02);
|
|
2992
|
+
// non-zero only if the assembler itself throws. stdout only (D-01).
|
|
2993
|
+
function reportRegistrationBug(flags) {
|
|
2994
|
+
try {
|
|
2995
|
+
const data = assembleRegistrationBugData();
|
|
2996
|
+
if (flags.json) {
|
|
2997
|
+
process.stdout.write(JSON.stringify(data, null, 2) + '\n');
|
|
2998
|
+
} else {
|
|
2999
|
+
process.stdout.write(renderRegistrationBugText(data) + '\n');
|
|
3000
|
+
}
|
|
3001
|
+
return 0;
|
|
3002
|
+
} catch (e) {
|
|
3003
|
+
process.stderr.write('report-registration-bug assembler threw: ' + ((e && e.message) || 'unknown') + '\n');
|
|
3004
|
+
return 1;
|
|
3005
|
+
}
|
|
3006
|
+
}
|
|
3007
|
+
|
|
3008
|
+
// Phase 225-02 (REQ-4, PD-2, Pitfall 2): compare two dotted numeric version
|
|
3009
|
+
// strings by NUMERIC segments (not lexicographically -- '3.51.10' must NOT sort
|
|
3010
|
+
// before '3.51.3'). Returns true when a < b. Zero new dependencies (CLAUDE.md:
|
|
3011
|
+
// CJS only, node built-ins only; this deliberately does NOT reuse the `semver`
|
|
3012
|
+
// import above because the SQLite version literal is a plain numeric triple and
|
|
3013
|
+
// a hand-rolled segment compare has no prerelease/caret semantics to get wrong).
|
|
3014
|
+
// Any non-string / unparseable input returns false, so a garbage version can
|
|
3015
|
+
// never fire the advisory.
|
|
3016
|
+
function _sqliteVersionLt(a, b) {
|
|
3017
|
+
if (typeof a !== 'string' || typeof b !== 'string') return false;
|
|
3018
|
+
const pa = a.split('.');
|
|
3019
|
+
const pb = b.split('.');
|
|
3020
|
+
const n = Math.max(pa.length, pb.length);
|
|
3021
|
+
for (let i = 0; i < n; i++) {
|
|
3022
|
+
const na = parseInt(pa[i], 10);
|
|
3023
|
+
const nb = parseInt(pb[i], 10);
|
|
3024
|
+
const va = Number.isFinite(na) ? na : 0;
|
|
3025
|
+
const vb = Number.isFinite(nb) ? nb : 0;
|
|
3026
|
+
if (va < vb) return true;
|
|
3027
|
+
if (va > vb) return false;
|
|
3028
|
+
}
|
|
3029
|
+
return false;
|
|
3030
|
+
}
|
|
3031
|
+
|
|
3032
|
+
// Phase 225-02 (REQ-4, PD-2): the WAL-reset corruption advisory. Fires ONLY when
|
|
3033
|
+
// the bundled SQLite is inside the upstream WAL-reset corruption window
|
|
3034
|
+
// (< 3.51.3, fixed upstream in commit 298a1c84) AND a live co-session is present
|
|
3035
|
+
// in this room (the 2-connection precondition the race needs). This is
|
|
3036
|
+
// detect-only by design: the two Phase-218 SQLite issues split as (a) SQLITE_BUSY
|
|
3037
|
+
// contention -- already FIXED by 218 D-05 (busy_timeout + synchronous=NORMAL) --
|
|
3038
|
+
// and (b) the WAL-reset checkpoint race, which is UPSTREAM and code-unfixable here
|
|
3039
|
+
// because Node bundles SQLite (Pitfall 2). PD-2: no extraction-worker guard ships
|
|
3040
|
+
// this phase (the drain worker files are Phase 224's in-flight surface); the
|
|
3041
|
+
// honest move is a cheap doctor advisory, never a block.
|
|
3042
|
+
//
|
|
3043
|
+
// opts { roomDir, sessionId, home, sqliteVersion, hasCoSession } -- sqliteVersion
|
|
3044
|
+
// and hasCoSession are INJECTABLE test seams. Production defaults resolve inside
|
|
3045
|
+
// the single try/catch: sqliteVersion via an in-memory node:sqlite probe (zero
|
|
3046
|
+
// user bytes), hasCoSession via a lazy require of session-presence.cjs called
|
|
3047
|
+
// with { roomDir, sessionId } so the CURRENT binding session excludes itself as
|
|
3048
|
+
// its own co-session. Returns a WARN finding string on fire; returns null on
|
|
3049
|
+
// no-fire OR on ANY fault (probe throws, node:sqlite absent, presence read
|
|
3050
|
+
// fails). The whole body is one try/catch returning null in catch -- the advisory
|
|
3051
|
+
// can never crash or block a bind (never-block, T-194-19).
|
|
3052
|
+
function _walResetAdvisory(opts) {
|
|
3053
|
+
try {
|
|
3054
|
+
opts = opts || {};
|
|
3055
|
+
let version = opts.sqliteVersion;
|
|
3056
|
+
if (typeof version !== 'string') {
|
|
3057
|
+
const { DatabaseSync } = require('node:sqlite');
|
|
3058
|
+
version = new DatabaseSync(':memory:').prepare('select sqlite_version() as v').get().v;
|
|
3059
|
+
}
|
|
3060
|
+
if (!_sqliteVersionLt(version, '3.51.3')) return null;
|
|
3061
|
+
|
|
3062
|
+
let hasCo = opts.hasCoSession;
|
|
3063
|
+
let coPresent;
|
|
3064
|
+
if (typeof hasCo === 'function') {
|
|
3065
|
+
coPresent = hasCo({ roomDir: opts.roomDir, sessionId: opts.sessionId, home: opts.home });
|
|
3066
|
+
} else {
|
|
3067
|
+
const presence = require(path.join(__dirname, '..', 'lib', 'core', 'session-presence.cjs'));
|
|
3068
|
+
coPresent = presence.hasCoSession({ roomDir: opts.roomDir, sessionId: opts.sessionId, home: opts.home });
|
|
3069
|
+
}
|
|
3070
|
+
if (!coPresent) return null;
|
|
3071
|
+
|
|
3072
|
+
return 'WARN: bundled SQLite ' + version + ' is inside the WAL-reset corruption ' +
|
|
3073
|
+
'window (fixed in 3.51.3) and a live co-session is present in this room; ' +
|
|
3074
|
+
'concurrent checkpoints can corrupt room.db (Phase 218 finding, commit 298a1c84)';
|
|
3075
|
+
} catch (_) {
|
|
3076
|
+
// Never-block, never-crash: any fault degrades to no-finding.
|
|
3077
|
+
return null;
|
|
3078
|
+
}
|
|
3079
|
+
}
|
|
3080
|
+
|
|
3081
|
+
function main() {
|
|
3082
|
+
const flags = parseArgs(process.argv.slice(2));
|
|
3083
|
+
|
|
3084
|
+
// Phase 194-07 (PSB-14 / PSB-13 / D-10): --bind-check <roomDir> runs the
|
|
3085
|
+
// BIND-TIME local room-health job and rides the stale-reap cadence. Its
|
|
3086
|
+
// contract is NEVER-BLOCK: it ALWAYS exits 0 (an unhealthy room degrades to an
|
|
3087
|
+
// advisory; binding is never hard-blocked -- T-194-19). It composes
|
|
3088
|
+
// session-presence.runBindCheck (LOCAL structural check + presence
|
|
3089
|
+
// registerPresence on green) and session-presence.reap (dead/stale presence
|
|
3090
|
+
// sweep). ZERO Brain call (Part 8): this is dispatched BEFORE the class-flag
|
|
3091
|
+
// block and BEFORE --acceptance so the Brain-smoke never runs on a bind.
|
|
3092
|
+
if (flags.bindCheck) {
|
|
3093
|
+
const presence = require(path.join(__dirname, '..', 'lib', 'core', 'session-presence.cjs'));
|
|
3094
|
+
const roomDir = flags.bindCheckDir;
|
|
3095
|
+
const sessionId = process.env.CLAUDE_SESSION_ID || ('cli-' + process.pid);
|
|
3096
|
+
const home = process.env.MINDRIAN_ROOMS_HOME || process.env.MINDRIAN_ROOMS_ROOT || undefined;
|
|
3097
|
+
let report;
|
|
3098
|
+
try {
|
|
3099
|
+
report = presence.runBindCheck({ sessionId: sessionId, roomDir: roomDir, home: home });
|
|
3100
|
+
} catch (_) {
|
|
3101
|
+
report = { healthy: false, bound: [], primary: null, onScope: false, findings: ['bind-check-error'] };
|
|
3102
|
+
}
|
|
3103
|
+
// Quick-task 20260702: WRITE the LOCAL room-health cache the shipped
|
|
3104
|
+
// cockpit-signals reader consumes, so the statusline health glyph goes LIVE
|
|
3105
|
+
// (closes the 192-04 room-health NAMED DEBT: the READ path shipped, the WRITE
|
|
3106
|
+
// path did not). Maps this bind-check report to sound|drift|broken. LOCAL only
|
|
3107
|
+
// (Part 8), fully guarded so a persist failure never affects the never-block
|
|
3108
|
+
// bind contract. Written to HOME/.mindrian (the reader's path), NOT the rooms
|
|
3109
|
+
// home, so the hot-path reader picks it up unchanged.
|
|
3110
|
+
try {
|
|
3111
|
+
const roomHealthCache = require(path.join(__dirname, '..', 'lib', 'statusline', 'room-health-cache.cjs'));
|
|
3112
|
+
roomHealthCache.persistRoomHealth(roomHealthCache.statusFromBindReport(report));
|
|
3113
|
+
} catch (_healthErr) { /* graceful: reader keeps returning the byte-stable default */ }
|
|
3114
|
+
// Phase 225-02 (REQ-4, PD-2): WAL-reset corruption advisory. Runs AFTER the
|
|
3115
|
+
// room-health-cache persist so the statusline glyph maps the UNMODIFIED
|
|
3116
|
+
// report. This is a findings-row WATCH item only: it NEVER touches
|
|
3117
|
+
// report.healthy (the WAL window is an environment condition the user cannot
|
|
3118
|
+
// repair locally, so flipping healthy would wrongly degrade the room-health
|
|
3119
|
+
// glyph) and NEVER changes the exit code (the block still ends in the
|
|
3120
|
+
// unconditional process.exit(0) below -- never-block, T-194-19).
|
|
3121
|
+
try {
|
|
3122
|
+
const walFinding = _walResetAdvisory({ roomDir: roomDir, sessionId: sessionId, home: home });
|
|
3123
|
+
if (typeof walFinding === 'string' && walFinding.length > 0) {
|
|
3124
|
+
if (!Array.isArray(report.findings)) report.findings = [];
|
|
3125
|
+
report.findings.push(walFinding);
|
|
3126
|
+
}
|
|
3127
|
+
} catch (_walErr) { /* never-block: the advisory can never affect the bind */ }
|
|
3128
|
+
if (flags.json) {
|
|
3129
|
+
console.log(JSON.stringify(report));
|
|
3130
|
+
} else {
|
|
3131
|
+
console.log((report.healthy ? 'OK' : 'ADVISORY') + ' doctor --bind-check: ' + (roomDir || '(no room dir)'));
|
|
3132
|
+
for (const f of (report.findings || [])) console.log(' - ' + f);
|
|
3133
|
+
if (report.healthy) console.log(' presence registered for session ' + sessionId);
|
|
3134
|
+
}
|
|
3135
|
+
// Stale-reap on the bind-time cadence (D-08 half + T-194-21): drop dead-pid
|
|
3136
|
+
// or mtime>5m presence files for this room. A live pid is never reaped. This
|
|
3137
|
+
// is keyed to bind-time + reconcile, NEVER per-turn.
|
|
3138
|
+
try {
|
|
3139
|
+
if (roomDir) presence.reap({ roomDir: roomDir });
|
|
3140
|
+
} catch (_) {}
|
|
3141
|
+
process.exit(0); // never-block: an unhealthy bind still exits 0 (advisory).
|
|
3142
|
+
return;
|
|
3143
|
+
}
|
|
3144
|
+
|
|
3145
|
+
// Phase 123 Plan-04: release-gate runner. --acceptance has its own exit-
|
|
3146
|
+
// code contract (0 = all points passed; 1 = any point failed); HARD ABORT
|
|
3147
|
+
// -- no --allow override (per CONTEXT D-16: release infra is the one gate
|
|
3148
|
+
// you cannot skip). The class-flag-always-exit-0 invariant does NOT apply.
|
|
3149
|
+
// Dispatched BEFORE the class-flag block so the existing per-class drift
|
|
3150
|
+
// detectors do not run twice (the doctor-all sub-check spawns ourselves
|
|
3151
|
+
// with --all for that, in a child process).
|
|
3152
|
+
if (flags.acceptance) {
|
|
3153
|
+
const home = process.env.HOME || process.env.USERPROFILE || HOME;
|
|
3154
|
+
runAcceptance({
|
|
3155
|
+
home: home,
|
|
3156
|
+
pluginRoot: PLUGIN_ROOT,
|
|
3157
|
+
flagPreTag: flags.preTag,
|
|
3158
|
+
flagPreFlight: flags.preFlight,
|
|
3159
|
+
flagLightNpx: flags.lightNpx,
|
|
3160
|
+
}).then(function (result) {
|
|
3161
|
+
// Per-point status lines (human-readable).
|
|
3162
|
+
for (const p of result.points) {
|
|
3163
|
+
const tag = p.ok ? 'PASS' : 'FAIL';
|
|
3164
|
+
const findingSuffix = p.finding ? ' -- ' + p.finding : '';
|
|
3165
|
+
console.log(tag + ' ' + p.id + ': ' + p.label + findingSuffix);
|
|
3166
|
+
}
|
|
3167
|
+
console.log('');
|
|
3168
|
+
const failSuffix = result.failed_points.length
|
|
3169
|
+
? '; failed: ' + result.failed_points.join(', ')
|
|
3170
|
+
: '';
|
|
3171
|
+
console.log('Acceptance ' + result.mode + ': ' + result.summary.passed + '/' + result.summary.total + ' points passed' + failSuffix + '.');
|
|
3172
|
+
// Phase 126 Plan 03: persist last_acceptance_run into install-state v2
|
|
3173
|
+
// (additive; best-effort). The write happens BEFORE the final exit so
|
|
3174
|
+
// even on failed --acceptance the state captures the run. Failure to
|
|
3175
|
+
// write (e.g., install-state.json absent OR migrator throws) emits a
|
|
3176
|
+
// stderr note but does NOT crash --acceptance. Plan 07's v2 schema
|
|
3177
|
+
// carries the `last_acceptance_run: { timestamp, passed, failed }`
|
|
3178
|
+
// field; migrateIfNeeded promotes v1 -> v2 transparently if needed.
|
|
3179
|
+
// Canon Part 8: LOCAL file I/O only ($HOME/.mindrian/). Zero network.
|
|
3180
|
+
try {
|
|
3181
|
+
const installStateMod = require(path.join(__dirname, '..', 'lib', 'core', 'install-state.cjs'));
|
|
3182
|
+
// Migration is idempotent: v1 -> v2 additive; v2 no-op; future-version
|
|
3183
|
+
// returns futureVersion:true without touching the file (Plan 07 D3).
|
|
3184
|
+
installStateMod.migrateIfNeeded({ home: home });
|
|
3185
|
+
const current = installStateMod.readInstallState({ home: home });
|
|
3186
|
+
if (current) {
|
|
3187
|
+
const updated = Object.assign({}, current, {
|
|
3188
|
+
last_acceptance_run: {
|
|
3189
|
+
timestamp: new Date().toISOString(),
|
|
3190
|
+
passed: result.summary.passed,
|
|
3191
|
+
failed: result.summary.failed,
|
|
3192
|
+
},
|
|
3193
|
+
});
|
|
3194
|
+
installStateMod.writeInstallState({ home: home, state: updated });
|
|
3195
|
+
}
|
|
3196
|
+
// No-op when install-state.json is absent (readInstallState returns
|
|
3197
|
+
// null) -- the record is session-start's responsibility to create.
|
|
3198
|
+
} catch (err) {
|
|
3199
|
+
process.stderr.write('[doctor --acceptance] failed to persist last_acceptance_run: ' + (err && err.message) + '\n');
|
|
3200
|
+
}
|
|
3201
|
+
if (flags.json) console.log(JSON.stringify(result, null, 2));
|
|
3202
|
+
process.exit(result.failed_points.length === 0 ? 0 : 1);
|
|
3203
|
+
}).catch(function (e) {
|
|
3204
|
+
console.error('acceptance runner threw: ' + (e && e.message));
|
|
3205
|
+
if (flags.json) console.log(JSON.stringify({ mode: flags.preTag ? 'pre-tag' : (flags.preFlight ? 'pre-flight' : 'full'), error: e && e.message, points: [], failed_points: ['__runner__'], summary: { total: 0, passed: 0, failed: 1 } }, null, 2));
|
|
3206
|
+
process.exit(1);
|
|
3207
|
+
});
|
|
3208
|
+
return;
|
|
3209
|
+
}
|
|
3210
|
+
|
|
3211
|
+
// Phase 146 Plan 04: --dogfood-acceptance loop-fires gate. Has its OWN
|
|
3212
|
+
// exit-code contract (0 = all 6 ACPT hermetic legs passed; non-zero = a leg
|
|
3213
|
+
// failed) -- per the ROADMAP Phase 146 mandate, exit 0 = the milestone ships
|
|
3214
|
+
// as "Larry Reaches", non-zero = the milestone is renamed. NOT a class flag;
|
|
3215
|
+
// the class-flag-always-exit-0 invariant does NOT apply. Dispatched BEFORE the
|
|
3216
|
+
// class-flag block so the own-contract exit code wins. Canon Part 8: spawns
|
|
3217
|
+
// LOCAL ACPT drivers only; zero network surface in doctor itself; the gate
|
|
3218
|
+
// never requires a reachable Brain (ACPT-05 live mode_a self-skips).
|
|
3219
|
+
// Phase 150-08 (MEM-09 / D-09): --claims is the CLAIM HARNESS gate, a SIBLING
|
|
3220
|
+
// of --acceptance with its OWN exit-code contract (0 = every public-site claim
|
|
3221
|
+
// is true by instrumentation; non-zero = a claim failed). Dispatched BEFORE the
|
|
3222
|
+
// class-flag block so the own-contract exit code wins; NOT a class flag, so the
|
|
3223
|
+
// class-flag-always-exit-0 invariant does NOT apply. Canon Part 8: the harness
|
|
3224
|
+
// is LOCAL/hermetic; the Brain LIVE arms self-skip via class-m-brain-smoke, so
|
|
3225
|
+
// the gate never requires a reachable Brain. The DOCTOR_CLAIM_FAIL_POINT env
|
|
3226
|
+
// hook synthesizes the failure path for the gate's own self-test.
|
|
3227
|
+
if (flags.claims) {
|
|
3228
|
+
const result = runClaims({ pluginRoot: PLUGIN_ROOT });
|
|
3229
|
+
if (result.ok) {
|
|
3230
|
+
console.log('PASS claims: the claim harness is green (every public-site claim is true by instrumentation).');
|
|
3231
|
+
} else {
|
|
3232
|
+
console.log('FAIL claims: ' + (result.finding || 'the claim harness failed') + '.');
|
|
3233
|
+
if (result.stdoutTail) console.log(result.stdoutTail);
|
|
3234
|
+
}
|
|
3235
|
+
if (flags.json) console.log(JSON.stringify(result, null, 2));
|
|
3236
|
+
process.exit(result.ok ? 0 : 1);
|
|
3237
|
+
return;
|
|
3238
|
+
}
|
|
3239
|
+
|
|
3240
|
+
if (flags.dogfoodAcceptance) {
|
|
3241
|
+
runDogfoodAcceptance({ pluginRoot: PLUGIN_ROOT }).then(function (result) {
|
|
3242
|
+
for (const p of result.points) {
|
|
3243
|
+
const tag = p.ok ? 'PASS' : 'FAIL';
|
|
3244
|
+
const findingSuffix = p.finding ? ' -- ' + p.finding : '';
|
|
3245
|
+
console.log(tag + ' ' + p.id + ': ' + p.label + findingSuffix);
|
|
3246
|
+
}
|
|
3247
|
+
console.log('');
|
|
3248
|
+
const failSuffix = result.failed_points.length
|
|
3249
|
+
? '; failed: ' + result.failed_points.join(', ')
|
|
3250
|
+
: '';
|
|
3251
|
+
console.log('Dogfood acceptance (loop-fires gate): ' + result.summary.passed + '/' + result.summary.total + ' ACPT points passed' + failSuffix + '.');
|
|
3252
|
+
if (result.failed_points.length === 0) {
|
|
3253
|
+
console.log('Exit 0: the milestone ships as "Larry Reaches".');
|
|
3254
|
+
} else {
|
|
3255
|
+
console.log('Non-zero: the milestone is renamed (a loop-fires leg failed).');
|
|
3256
|
+
}
|
|
3257
|
+
if (flags.json) console.log(JSON.stringify(result, null, 2));
|
|
3258
|
+
process.exit(result.summary.failed > 0 ? 1 : 0);
|
|
3259
|
+
}).catch(function (e) {
|
|
3260
|
+
console.error('dogfood-acceptance gate threw: ' + (e && e.message));
|
|
3261
|
+
if (flags.json) console.log(JSON.stringify({ gate: 'dogfood-acceptance', error: e && e.message, points: [], failed_points: ['__runner__'], summary: { total: 0, passed: 0, failed: 1 } }, null, 2));
|
|
3262
|
+
process.exit(1);
|
|
3263
|
+
});
|
|
3264
|
+
return;
|
|
3265
|
+
}
|
|
3266
|
+
|
|
3267
|
+
// Phase 127.2-03 Task 2 (Findings F1 + F7): --check-rs-engine pre-flight.
|
|
3268
|
+
//
|
|
3269
|
+
// Probes top-level Python imports reachable from scripts/rs-engine.py
|
|
3270
|
+
// (the Engine 1 Act 1 surface for /mos:find-bottlenecks, /mos:whitespace,
|
|
3271
|
+
// /mos:find-connections, /mos:find-analogies, /mos:score-innovation). The
|
|
3272
|
+
// Windows tester 2026-05-23 silent-failure root cause is `requests` (a
|
|
3273
|
+
// transitive dep via lib/core/rs_corpus.py) missing from the runtime
|
|
3274
|
+
// python. This dispatch lands BEFORE the class A-M flow so the gate is
|
|
3275
|
+
// standalone -- it has its own exit-code contract (0 = ready, 1 = missing
|
|
3276
|
+
// deps), distinct from the class-flag-always-exit-0 invariant.
|
|
3277
|
+
//
|
|
3278
|
+
// SCOPE GUARD (CONTEXT): ADD-ONLY -- this dispatcher does NOT touch any
|
|
3279
|
+
// existing acceptance point, UI-compliance check, statusline check, or
|
|
3280
|
+
// session-start logic. It is a sibling of --brain-smoke (class M) in
|
|
3281
|
+
// shape but not in classification (not a class flag).
|
|
3282
|
+
if (flags.checkRsEngine) {
|
|
3283
|
+
runCheckRsEngine(flags).then(function (code) {
|
|
3284
|
+
process.exit(code);
|
|
3285
|
+
}).catch(function (e) {
|
|
3286
|
+
// Defensive: any uncaught error inside the probe surfaces as exit 1
|
|
3287
|
+
// with a hint so /mos:doctor's other gates are never crashed by a
|
|
3288
|
+
// broken probe implementation.
|
|
3289
|
+
console.error('check-rs-engine probe threw: ' + (e && e.message));
|
|
3290
|
+
if (flags.json) {
|
|
3291
|
+
console.log(JSON.stringify({ ok: false, error: (e && e.message) || 'unknown', fix: 'Run: pip install -r requirements-hsi.txt --user (use python -m pip if pip is not in PATH)' }, null, 2));
|
|
3292
|
+
}
|
|
3293
|
+
process.exit(1);
|
|
3294
|
+
});
|
|
3295
|
+
return;
|
|
3296
|
+
}
|
|
3297
|
+
|
|
3298
|
+
// Quick task 260705-jeq: --report-registration-bug dispatch (D-01, D-02).
|
|
3299
|
+
//
|
|
3300
|
+
// Sibling of --check-rs-engine in SHAPE (own dispatch, own exit contract) but
|
|
3301
|
+
// a diagnostic REPORTER, not a probe: it assembles a paste-ready Anthropic bug
|
|
3302
|
+
// report proving every locally-checkable registration cause is CLEAN. Exit 0
|
|
3303
|
+
// whenever the report assembles (even offline); non-zero only if the assembler
|
|
3304
|
+
// throws. Honors flags.json inline. NOT in --all (see parseArgs rationale).
|
|
3305
|
+
//
|
|
3306
|
+
// SCOPE GUARD: ADD-ONLY -- touches no existing acceptance point, class check,
|
|
3307
|
+
// or session-start logic. Every fact reuses an existing collector (Part 7).
|
|
3308
|
+
if (flags.reportRegistrationBug) {
|
|
3309
|
+
process.exit(reportRegistrationBug(flags));
|
|
3310
|
+
return;
|
|
3311
|
+
}
|
|
3312
|
+
|
|
3313
|
+
// Phase 127.2 Plan 04 Instance #7: --post-update dispatch.
|
|
3314
|
+
//
|
|
3315
|
+
// Delegates to scripts/post-update-activation.cjs which (a) detects the
|
|
3316
|
+
// freshly-landed cache-staging dir, (b) atomically swaps via the existing
|
|
3317
|
+
// --fix pipeline (Phase 95.2; three autopsies of hardening), (c) writes
|
|
3318
|
+
// the ~/.mindrian/post-update-restart-pending touch-file so the next
|
|
3319
|
+
// session's sessionstart-post-update-preflight.cjs can verify activation
|
|
3320
|
+
// reached the wire (Class N acceptance, see buildAcceptanceChecklist).
|
|
3321
|
+
//
|
|
3322
|
+
// SCOPE GUARD: ADD-ONLY -- does not touch any existing acceptance point,
|
|
3323
|
+
// UI-compliance check, or session-start hook. Composable with --fix: when
|
|
3324
|
+
// both flags pass we spawn post-update-activation.cjs which itself spawns
|
|
3325
|
+
// doctor.cjs --fix --json (the existing chain) so the atomic-swap logic
|
|
3326
|
+
// is exercised through ONE canonical code path. Closes Canon Part 7
|
|
3327
|
+
// (reuse before build) on the activation-reached-the-wire surface.
|
|
3328
|
+
if (flags.postUpdate) {
|
|
3329
|
+
try {
|
|
3330
|
+
const activator = require(path.join(__dirname, 'post-update-activation.cjs'));
|
|
3331
|
+
activator.activatePostUpdate({}).then(function (result) {
|
|
3332
|
+
if (flags.json) {
|
|
3333
|
+
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
3334
|
+
} else {
|
|
3335
|
+
process.stdout.write(activator.renderActionReport(result));
|
|
3336
|
+
}
|
|
3337
|
+
process.exit(result.ok ? 0 : 1);
|
|
3338
|
+
}).catch(function (e) {
|
|
3339
|
+
if (flags.json) {
|
|
3340
|
+
console.log(JSON.stringify({ ok: false, error: (e && e.message) || 'unknown' }, null, 2));
|
|
3341
|
+
} else {
|
|
3342
|
+
console.error('post-update activation threw: ' + (e && e.message));
|
|
3343
|
+
}
|
|
3344
|
+
process.exit(1);
|
|
3345
|
+
});
|
|
3346
|
+
} catch (e) {
|
|
3347
|
+
if (flags.json) {
|
|
3348
|
+
console.log(JSON.stringify({ ok: false, error: 'failed to load post-update-activation: ' + (e && e.message) }, null, 2));
|
|
3349
|
+
} else {
|
|
3350
|
+
console.error('failed to load post-update-activation: ' + (e && e.message));
|
|
3351
|
+
}
|
|
3352
|
+
process.exit(1);
|
|
3353
|
+
}
|
|
3354
|
+
return;
|
|
3355
|
+
}
|
|
3356
|
+
|
|
3357
|
+
// Phase 127-02 BRAIN-MCP-127-08: --brain-smoke (class M) dispatch.
|
|
3358
|
+
//
|
|
3359
|
+
// Phase 217 carve-out: brain-smoke (class M) stays special-cased as an async
|
|
3360
|
+
// dispatch block and is NOT migrated to a registry runner. The accumulative
|
|
3361
|
+
// engine loop is SYNCHRONOUS (it calls check(ctx) and consumes the return
|
|
3362
|
+
// value inline); class M is an async 6-layer network probe (Promise-based,
|
|
3363
|
+
// with the Windows-safe process.exitCode teardown below). Migrating it would
|
|
3364
|
+
// require an async engine pass, deliberately not taken during the 211-217
|
|
3365
|
+
// release window (217-RESEARCH Pitfall 2, CONTEXT discretion clause A2:
|
|
3366
|
+
// lower-risk call under release pressure).
|
|
3367
|
+
//
|
|
3368
|
+
// The 6-layer probe is dispatched here BEFORE the class A-L flow so the
|
|
3369
|
+
// output is the canonical { class:"M", ok, layers:[6], overall_ms } shape
|
|
3370
|
+
// (consumed by tests/test-127-02-doctor-class-m.sh). When combined with
|
|
3371
|
+
// --all, this branch still owns the output; the other class checks run
|
|
3372
|
+
// under it via a parallel promise chain, but for the standalone case
|
|
3373
|
+
// (which is the primary user surface) the doctor exits after Class M
|
|
3374
|
+
// alone. Class-flag invariant applies: exit 0 even on FAIL.
|
|
3375
|
+
if (flags.brainSmoke && !flags.eurekaSmoke && !flags.cascadeRooms && !flags.verifySurface
|
|
3376
|
+
&& !flags.roomMd && !flags.uiCompliance && !flags.statuslineVisibility
|
|
3377
|
+
&& !flags.installState && !flags.staleFirstTouch && !flags.deprecatedUsage
|
|
3378
|
+
&& !flags.all) {
|
|
3379
|
+
// Debug session doctor-brain-smoke-win-crash (2026-05-30): a synchronous
|
|
3380
|
+
// process.exit() here tore down a still-open libuv handle (the undici
|
|
3381
|
+
// keep-alive TLS socket left un-released by the L3 schema probe on a
|
|
3382
|
+
// 401/403 key-rejected path) MID-CLOSE, which asserts on Windows in
|
|
3383
|
+
// src/win/async.c (UV_HANDLE_CLOSING) and surfaces to the Claude Code Bash
|
|
3384
|
+
// wrapper as "Exit code 127". Setting process.exitCode and returning lets
|
|
3385
|
+
// the event loop drain every outstanding handle the OS-safe way, then the
|
|
3386
|
+
// process exits naturally with the requested code. This covers ALL handle
|
|
3387
|
+
// variants (socket, timer, child) and is correct on Windows/macOS/Linux.
|
|
3388
|
+
// The class-flag invariant (exit 0 even on per-layer FAIL) is preserved:
|
|
3389
|
+
// classMBrainSmoke() returns 0, asserted by tests/test-127-02-doctor-class-m.sh T3.
|
|
3390
|
+
classMBrainSmoke(flags).then(function (code) {
|
|
3391
|
+
process.exitCode = code;
|
|
3392
|
+
}).catch(function (e) {
|
|
3393
|
+
console.error('class-m smoke threw: ' + (e && e.message));
|
|
3394
|
+
if (flags.json) console.log(JSON.stringify({ class: 'M', ok: false, error: e && e.message, layers: [], overall_ms: 0 }, null, 2));
|
|
3395
|
+
// Class-flag invariant: exit 0 even on internal error.
|
|
3396
|
+
process.exitCode = 0;
|
|
3397
|
+
});
|
|
3398
|
+
return;
|
|
3399
|
+
}
|
|
3400
|
+
|
|
3401
|
+
// Phase 217 carve-out: eureka-smoke (class S) stays special-cased as an async
|
|
3402
|
+
// dispatch block and is NOT migrated to a registry runner, for the same reason
|
|
3403
|
+
// as class M: the accumulative engine loop is synchronous and class S is an
|
|
3404
|
+
// async 4-layer local-embedding-stack probe. Migrating it requires an async
|
|
3405
|
+
// engine pass, deliberately not taken during the 211-217 release window
|
|
3406
|
+
// (217-RESEARCH Pitfall 2, CONTEXT discretion clause A2).
|
|
3407
|
+
//
|
|
3408
|
+
// quick(260706-13z) D14: --eureka-smoke (class S) standalone dispatch. Mirrors
|
|
3409
|
+
// the class-M block exactly, including the process.exitCode-not-process.exit
|
|
3410
|
+
// Windows-safe teardown (2026-05-30 RCA) and the class-flag invariant (exit 0
|
|
3411
|
+
// even on per-layer FAIL). The !flags.brainSmoke guard lets a combined
|
|
3412
|
+
// --brain-smoke --eureka-smoke run fall through to the --all-style merge.
|
|
3413
|
+
if (flags.eurekaSmoke && !flags.brainSmoke && !flags.cascadeRooms && !flags.verifySurface
|
|
3414
|
+
&& !flags.roomMd && !flags.uiCompliance && !flags.statuslineVisibility
|
|
3415
|
+
&& !flags.installState && !flags.staleFirstTouch && !flags.deprecatedUsage
|
|
3416
|
+
&& !flags.all) {
|
|
3417
|
+
classSEurekaSmoke(flags).then(function (code) {
|
|
3418
|
+
process.exitCode = code;
|
|
3419
|
+
}).catch(function (e) {
|
|
3420
|
+
console.error('class-s smoke threw: ' + (e && e.message));
|
|
3421
|
+
if (flags.json) console.log(JSON.stringify({ class: 'S', ok: false, error: e && e.message, layers: [], overall_ms: 0 }, null, 2));
|
|
3422
|
+
// Class-flag invariant: exit 0 even on internal error.
|
|
3423
|
+
process.exitCode = 0;
|
|
3424
|
+
});
|
|
3425
|
+
return;
|
|
3426
|
+
}
|
|
3427
|
+
|
|
3428
|
+
const installResult = checkInstallVersion();
|
|
3429
|
+
const cacheResult = checkMarketplaceCache();
|
|
3430
|
+
const devResult = checkDevSourceConsistency();
|
|
3431
|
+
|
|
3432
|
+
// Detect whether any class B/C/D/E/F/G/I/J flag was activated -- when YES the
|
|
3433
|
+
// doctor run is in "class-flag mode" (graceful degradation per Canon Part
|
|
3434
|
+
// 8 invariant): class A install/cache issues become informational, never
|
|
3435
|
+
// hard exits, and warnings from new checks return 0 unless --fix-failed.
|
|
3436
|
+
const classFlagsActive = flags.cascadeRooms || flags.verifySurface
|
|
3437
|
+
|| flags.roomMd || flags.uiCompliance || flags.statuslineVisibility
|
|
3438
|
+
|| flags.cardFireHealth
|
|
3439
|
+
|| flags.installState || flags.staleFirstTouch || flags.deprecatedUsage
|
|
3440
|
+
|| flags.brainSmoke || flags.eurekaSmoke || flags.drift
|
|
3441
|
+
|| flags.graphDeriveHealth;
|
|
3442
|
+
// ^ Phase 150.9 Plan-02: registering flags.drift here honors the
|
|
3443
|
+
// graceful-degradation exit-0 invariant. This is what preserves the
|
|
3444
|
+
// marketplace-cache-drift-deadlock carve-out automatically: that carve-out
|
|
3445
|
+
// lives in Class A's checkInstallVersion() which --drift never touches, and
|
|
3446
|
+
// classFlagsActive forces _finalizeAndExit to exit 0 (so a detected install
|
|
3447
|
+
// drift never flips a --drift run to exit 1). Per D-04 + the CONTEXT
|
|
3448
|
+
// discretion ruling.
|
|
3449
|
+
|
|
3450
|
+
const report = {
|
|
3451
|
+
install: installResult,
|
|
3452
|
+
cache: cacheResult,
|
|
3453
|
+
dev: devResult,
|
|
3454
|
+
drift: { detected: false },
|
|
3455
|
+
fixRequested: flags.fix,
|
|
3456
|
+
classARecovered: null, // class A install-cache recovery result (single object)
|
|
3457
|
+
recoveryError: null,
|
|
3458
|
+
recoverySkipped: null, // set when --fix drift recovery is skipped BY DESIGN
|
|
3459
|
+
// (marketplace-cache topology; RCA
|
|
3460
|
+
// doctor-marketplace-cache-drift-deadlock). Stable
|
|
3461
|
+
// JSON shape for consumers (post-update activator).
|
|
3462
|
+
checks: {}, // class B/C/D/E/F results land here.
|
|
3463
|
+
recovered: [], // unified array of recovery records across all classes.
|
|
3464
|
+
};
|
|
3465
|
+
|
|
3466
|
+
// Drift detection (class A) -- widened in Phase 95.2 D-05/D-06.
|
|
3467
|
+
if (installResult.status === 'ok' && cacheResult.status === 'ok') {
|
|
3468
|
+
if (installResult.parsed && cacheResult.latestParsed) {
|
|
3469
|
+
const cmp = cmpVersion(installResult.parsed, cacheResult.latestParsed);
|
|
3470
|
+
report.drift = {
|
|
3471
|
+
detected: cmp < 0,
|
|
3472
|
+
compare: cmp,
|
|
3473
|
+
};
|
|
3474
|
+
}
|
|
3475
|
+
} else if (installResult.status === 'missing'
|
|
3476
|
+
&& cacheResult.status === 'ok'
|
|
3477
|
+
&& cacheResult.versions
|
|
3478
|
+
&& cacheResult.versions.length > 0
|
|
3479
|
+
&& resolveActivePluginRoot().topology !== 'marketplace-cache') {
|
|
3480
|
+
// Phase 95.2 D-05: missing install with available cache is a recoverable drift case.
|
|
3481
|
+
// Topology guard (debug session doctor-class-a-drift-topology-blind-false-positive,
|
|
3482
|
+
// 2026-05-31): installResult is computed against the hardcoded legacy INSTALL_DIR
|
|
3483
|
+
// (line 55, ~/.claude/plugins/mindrian-os). Under marketplace-cache topology that
|
|
3484
|
+
// legacy dir is CORRECTLY absent -- Claude Code loads the plugin from the cache
|
|
3485
|
+
// path recorded in installed_plugins.json -- so a missing legacy dir is the healthy
|
|
3486
|
+
// state, not drift. resolveActivePluginRoot() only returns a marketplace-cache root
|
|
3487
|
+
// when that root passed isPluginDir() (a valid .claude-plugin/plugin.json), so the
|
|
3488
|
+
// marketplace-cache topology already implies a healthy resolved active root. This
|
|
3489
|
+
// guard makes class A agree with the topology-aware class I gate (--install-state)
|
|
3490
|
+
// while preserving the legacy-topology recovery path: a genuinely-missing legacy
|
|
3491
|
+
// active root (topology !== marketplace-cache) still reports install-missing drift.
|
|
3492
|
+
report.drift = { detected: true, reason: 'install-missing' };
|
|
3493
|
+
}
|
|
3494
|
+
|
|
3495
|
+
// Phase 95.2 D-07: install.recoverable (pure addition; preserves byte-stability of existing fields).
|
|
3496
|
+
// True iff the cache holds at least one valid version. Lets automation distinguish
|
|
3497
|
+
// 'can fix automatically' from 'needs manual intervention' without parsing cache.versions[].
|
|
3498
|
+
report.install.recoverable = (cacheResult.status === 'ok' && cacheResult.versions && cacheResult.versions.length > 0);
|
|
3499
|
+
|
|
3500
|
+
// Phase 217 carve-out: class A (install-cache) stays special-cased in main()
|
|
3501
|
+
// and is NOT migrated to a registry runner. Its results live OUTSIDE
|
|
3502
|
+
// report.checks (report.install / report.cache / report.drift /
|
|
3503
|
+
// report.classARecovered), its recovery is the atomic-swap path, and
|
|
3504
|
+
// _finalizeAndExit(flags, report, classFlagsActive, cacheResult, installResult)
|
|
3505
|
+
// consumes cacheResult + installResult POSITIONALLY -- the exit-code contract
|
|
3506
|
+
// is structurally coupled to main(). Forcing it into the uniform module shape
|
|
3507
|
+
// would restructure the most-tested path days before the 211-217 release. Its
|
|
3508
|
+
// three pure constituent readers (checkInstallVersion, checkMarketplaceCache,
|
|
3509
|
+
// checkDevSourceConsistency) already left the monolith into
|
|
3510
|
+
// lib/core/doctor/shared.cjs in 217-01. Full migration is future work, per
|
|
3511
|
+
// 217-RESEARCH Open Question 3.
|
|
3512
|
+
//
|
|
3513
|
+
// Class A recovery (existing behavior preserved; result also pushed onto
|
|
3514
|
+
// the unified report.recovered array for class B/C/D/E/F co-existence).
|
|
3515
|
+
// Phase 95.2 D-01..D-04: now uses performRecoveryAtomic with two-step atomic-swap.
|
|
3516
|
+
// Phase 123 Plan-03 carve-out: when --install-state (class I) is active,
|
|
3517
|
+
// class A's `--fix` is suppressed. Class I owns the legacy-migration
|
|
3518
|
+
// semantics (backup-then-verify-then-remove with the dev-clone safety belt
|
|
3519
|
+
// and the dirty/unpushed refuse checks per D-13). Without this carve-out
|
|
3520
|
+
// class A would migrate the legacy dir AHEAD of class I, defeating the
|
|
3521
|
+
// dirty-legacy refuse contract.
|
|
3522
|
+
// Resolve the active topology ONCE for both the recovery gate and the
|
|
3523
|
+
// skipped-by-design branch (RCA doctor-marketplace-cache-drift-deadlock:
|
|
3524
|
+
// the prior double-call pattern made the gate and the drift detector read
|
|
3525
|
+
// two different sources of truth).
|
|
3526
|
+
const activeTopologyAtGate = resolveActivePluginRoot().topology;
|
|
3527
|
+
if (flags.fix && report.drift.detected && !flags.installState
|
|
3528
|
+
&& activeTopologyAtGate !== 'marketplace-cache') {
|
|
3529
|
+
// Defense-in-depth (debug session doctor-class-a-drift-topology-blind-false-positive,
|
|
3530
|
+
// 2026-05-31): with the Change-1 topology guard above, drift.detected is already
|
|
3531
|
+
// false for the healthy marketplace-cache case so this gate is not reached for it.
|
|
3532
|
+
// This redundant topology check ensures a future regression in the drift branch
|
|
3533
|
+
// cannot re-enable performRecoveryAtomic recreating the legacy ~/.claude/plugins/
|
|
3534
|
+
// mindrian-os dir under marketplace-cache topology (which class I would then flag
|
|
3535
|
+
// as a migration candidate -- the two subsystems must never contradict).
|
|
3536
|
+
const result = performRecoveryAtomic(installResult.version, cacheResult.latest);
|
|
3537
|
+
if (result.status === 'ok') {
|
|
3538
|
+
report.classARecovered = result;
|
|
3539
|
+
report.recovered.push({ tool: 'install-cache', status: 'ok', version: result.recoveredVersion, backup: result.backup });
|
|
3540
|
+
} else {
|
|
3541
|
+
report.recoveryError = result.detail;
|
|
3542
|
+
// Phase 95.2 D-03: distinguish rollback exit (code 4) from regular failure (code 1).
|
|
3543
|
+
if (result.exitCode === 4 || result.stage === 'commit-rollback') {
|
|
3544
|
+
report.recoveryRolledBack = true;
|
|
3545
|
+
}
|
|
3546
|
+
}
|
|
3547
|
+
} else if (flags.fix && report.drift.detected && !flags.installState
|
|
3548
|
+
&& activeTopologyAtGate === 'marketplace-cache') {
|
|
3549
|
+
// RCA doctor-marketplace-cache-drift-deadlock (2026-06-12): the topology
|
|
3550
|
+
// check is what blocked recovery. Record the skip instead of silently
|
|
3551
|
+
// falling through -- a silent skip left classARecovered AND recoveryError
|
|
3552
|
+
// both null, which rendered as the useless 'recovery failed: unknown' and
|
|
3553
|
+
// tripped the unconditional drift exit-1 in _finalizeAndExit (the
|
|
3554
|
+
// permanent /mos:update Step 7 deadlock). Skipped-by-design is NOT
|
|
3555
|
+
// failure: the renderer and the exit finalizer both read this field.
|
|
3556
|
+
report.recoverySkipped = {
|
|
3557
|
+
reason: 'topology-marketplace-cache',
|
|
3558
|
+
detail: 'recovery skipped by design: active topology is marketplace-cache; the legacy install dir is vestigial and class A will not touch it (class I owns legacy migration)',
|
|
3559
|
+
};
|
|
3560
|
+
}
|
|
3561
|
+
|
|
3562
|
+
// Class B + C: cascade-rooms (sentinel + active-room guard silence) migrated to
|
|
3563
|
+
// registry-driven cadence:always runners (Phase 217 Plan 04). The accumulative
|
|
3564
|
+
// engine now owns cascade-rooms + cascade-rooms-active under the --cascade-rooms
|
|
3565
|
+
// flag; class B's --fix (never wired pre-217) lives in the runner's fix(ctx).
|
|
3566
|
+
|
|
3567
|
+
// Class D (verify-surface), class G (statusline-visibility), and class H
|
|
3568
|
+
// (install-incomplete) migrated to registry-driven cadence:always runners
|
|
3569
|
+
// (Phase 217 Plan 05). The accumulative engine now owns:
|
|
3570
|
+
// - verify-surface under --verify-surface (check-only, fix_supported false);
|
|
3571
|
+
// - statusline-visibility + install-incomplete under --statusline-visibility
|
|
3572
|
+
// (H shares G's flag, today's exact behavior: --statusline-visibility
|
|
3573
|
+
// surfaces both, --all surfaces both);
|
|
3574
|
+
// and the engine's fix-then-recheck flow owns what the old hand-wired G/H --fix
|
|
3575
|
+
// dispatches did (G gate: warn AND recoverable !== false; H's stricter
|
|
3576
|
+
// recoverable === true gate is enforced runner-internally in its fix(ctx)).
|
|
3577
|
+
|
|
3578
|
+
// Class I (install-state) + Class J (deployment-surfaces) migrated to
|
|
3579
|
+
// registry-driven cadence:always runners (Phase 217 Plan 06). The
|
|
3580
|
+
// accumulative engine now owns both under the --install-state flag (they share
|
|
3581
|
+
// it, exactly as this block did): install-state runs first so class J reads
|
|
3582
|
+
// its same-invocation result via ctx.checks['install-state']; each fix's
|
|
3583
|
+
// recoveries[] land on report.recovered through the engine glue. The class-A
|
|
3584
|
+
// BUG 7 reinterpretation that used to close this block (it reads class I's
|
|
3585
|
+
// topology to downgrade an expected-absent legacy clone) is relocated to AFTER
|
|
3586
|
+
// the engine populates report.checks['install-state'] (see below).
|
|
3587
|
+
|
|
3588
|
+
|
|
3589
|
+
// Class P: prose-vs-code / product-claims drift (Phase 150.9 Plan-02;
|
|
3590
|
+
// Fable v1.13.1 audit track 1). REPORT-ONLY (D-03): NEVER edits any prose
|
|
3591
|
+
// .md file, even under --fix. Wraps the two shipped checkers (Canon Part 7
|
|
3592
|
+
// reuse -- repoints, does NOT reimplement):
|
|
3593
|
+
// - check-skill-vs-code-drift.cjs : require().check() (clean, no exit)
|
|
3594
|
+
// - check-first-touch-drift.cjs : spawned as a SUBPROCESS (Pitfall 1 --
|
|
3595
|
+
// it is CLI-only and self-exits, so a
|
|
3596
|
+
// require() would kill doctor mid-run).
|
|
3597
|
+
// OPT-IN ONLY (D-04): gated strictly behind flags.drift, NEVER behind
|
|
3598
|
+
// !classFlagsActive. Pure LOCAL: file reads + a LOCAL node subprocess; zero
|
|
3599
|
+
// network, zero Brain, zero telemetry (Canon Part 8). Soft-fail: a throw
|
|
3600
|
+
// records an error finding, never crashes doctor.
|
|
3601
|
+
if (flags.drift) {
|
|
3602
|
+
try {
|
|
3603
|
+
// (a) skill-vs-code drift -- require-clean.
|
|
3604
|
+
let skillResult = null;
|
|
3605
|
+
let skillErr = null;
|
|
3606
|
+
try {
|
|
3607
|
+
const { check } = require(path.join(__dirname, 'check-skill-vs-code-drift.cjs'));
|
|
3608
|
+
skillResult = check();
|
|
3609
|
+
} catch (e) {
|
|
3610
|
+
skillErr = (e && e.message) || 'skill-vs-code check threw';
|
|
3611
|
+
}
|
|
3612
|
+
|
|
3613
|
+
// (b) first-touch drift -- SUBPROCESS (Pitfall 1). Capture exit code
|
|
3614
|
+
// (1 = drift, 0 = clean) + parse the "DRIFT:" stdout lines. We pass the
|
|
3615
|
+
// child the SAME process.env so a test's MOS_TEST_SCAN_DIR fixture flows
|
|
3616
|
+
// through. Fixed arg array (no shell string).
|
|
3617
|
+
const ftPath = path.join(__dirname, 'check-first-touch-drift.cjs');
|
|
3618
|
+
const cp = require('child_process');
|
|
3619
|
+
const ft = cp.spawnSync('node', [ftPath], { encoding: 'utf8', timeout: 30000 });
|
|
3620
|
+
const ftStdout = (ft && ft.stdout) || '';
|
|
3621
|
+
const firstTouchHits = ftStdout
|
|
3622
|
+
.split('\n')
|
|
3623
|
+
.filter((ln) => ln.indexOf('DRIFT:') === 0).length;
|
|
3624
|
+
const firstTouchDrift = ft && ft.status === 1;
|
|
3625
|
+
|
|
3626
|
+
// Aggregate. skill drift = !valid; first-touch drift = exit 1 / hits > 0.
|
|
3627
|
+
const skillDrift = skillResult ? skillResult.valid === false : false;
|
|
3628
|
+
const anyDrift = skillDrift || firstTouchDrift;
|
|
3629
|
+
|
|
3630
|
+
const detailParts = [];
|
|
3631
|
+
if (skillErr) detailParts.push('skill-vs-code check error: ' + skillErr);
|
|
3632
|
+
else if (skillDrift) detailParts.push('skill-vs-code drift (SKILL.md disagrees with shipped code)');
|
|
3633
|
+
if (firstTouchDrift) detailParts.push('first-touch drift: ' + firstTouchHits + ' hit(s)');
|
|
3634
|
+
if (!anyDrift && !skillErr) detailParts.push('prose-vs-code surfaces clean');
|
|
3635
|
+
|
|
3636
|
+
report.checks['prose-vs-code'] = {
|
|
3637
|
+
class: 'P',
|
|
3638
|
+
// REPORT-ONLY: status is ok|warn (never a --fix that rewrites prose).
|
|
3639
|
+
// A checker that errored is surfaced as warn-with-detail, not a false ok.
|
|
3640
|
+
status: (anyDrift || skillErr) ? 'warn' : 'ok',
|
|
3641
|
+
detail: detailParts.join('; '),
|
|
3642
|
+
skill_findings: skillResult || null,
|
|
3643
|
+
skill_error: skillErr,
|
|
3644
|
+
first_touch_hits: firstTouchHits,
|
|
3645
|
+
first_touch_drift: !!firstTouchDrift,
|
|
3646
|
+
report_only: true,
|
|
3647
|
+
};
|
|
3648
|
+
} catch (err) {
|
|
3649
|
+
report.checks['prose-vs-code'] = {
|
|
3650
|
+
class: 'P',
|
|
3651
|
+
status: 'error',
|
|
3652
|
+
detail: (err && err.message) || 'class P threw',
|
|
3653
|
+
first_touch_hits: 0,
|
|
3654
|
+
report_only: true,
|
|
3655
|
+
};
|
|
3656
|
+
}
|
|
3657
|
+
}
|
|
3658
|
+
|
|
3659
|
+
// Class Q: GSD execution-record drift (Phase 150.9 Plan-02; the W007/I001
|
|
3660
|
+
// class Fable ran by hand). SHELLS OUT to `gsd-tools validate health --raw`
|
|
3661
|
+
// (D-02 -- single source of truth; does NOT reimplement the ROADMAP/SUMMARY
|
|
3662
|
+
// scan) and parses W007 (ROADMAP drift) + I001 (missing SUMMARYs). OPT-IN
|
|
3663
|
+
// ONLY (D-04): gated behind flags.drift. Pure LOCAL: spawns a LOCAL node
|
|
3664
|
+
// subprocess (gsd-tools), zero network / zero Brain (Canon Part 8 -- the
|
|
3665
|
+
// spawnSync('node', [...'validate','health']) is a local subprocess, not a
|
|
3666
|
+
// network call). Pitfalls handled: 2 (shape-assert before reading; error not
|
|
3667
|
+
// false-clean), 3 (cwd = repo root, treat broken/error health as
|
|
3668
|
+
// indeterminate). Soft-fail + locator-skip: never crashes doctor.
|
|
3669
|
+
if (flags.drift) {
|
|
3670
|
+
try {
|
|
3671
|
+
const driftRepoRoot = path.resolve(__dirname, '..');
|
|
3672
|
+
const gsdToolsPath = resolveGsdTools();
|
|
3673
|
+
if (!gsdToolsPath) {
|
|
3674
|
+
report.checks['gsd-record-drift'] = {
|
|
3675
|
+
class: 'Q',
|
|
3676
|
+
status: 'skip',
|
|
3677
|
+
detail: 'gsd-health unavailable (gsd-tools.cjs not found via the resolver order) -- W007/I001 scan skipped',
|
|
3678
|
+
w007: [],
|
|
3679
|
+
i001: [],
|
|
3680
|
+
};
|
|
3681
|
+
} else {
|
|
3682
|
+
const cp = require('child_process');
|
|
3683
|
+
// T-150.9-03 mitigation: FIXED arg array (never a shell string); no
|
|
3684
|
+
// interpolation of untrusted input. cwd = repo root (Pitfall 3).
|
|
3685
|
+
const hr = cp.spawnSync('node', [gsdToolsPath, 'validate', 'health', '--raw'], {
|
|
3686
|
+
cwd: driftRepoRoot,
|
|
3687
|
+
encoding: 'utf8',
|
|
3688
|
+
timeout: 60000,
|
|
3689
|
+
});
|
|
3690
|
+
let health = null;
|
|
3691
|
+
let parseErr = null;
|
|
3692
|
+
try {
|
|
3693
|
+
health = JSON.parse((hr && hr.stdout) || '');
|
|
3694
|
+
} catch (e) {
|
|
3695
|
+
parseErr = (e && e.message) || 'JSON parse failed';
|
|
3696
|
+
}
|
|
3697
|
+
|
|
3698
|
+
if (parseErr || !health || typeof health !== 'object'
|
|
3699
|
+
|| !Array.isArray(health.warnings) || !Array.isArray(health.info)) {
|
|
3700
|
+
// Pitfall 2: shape unrecognized -> error, NOT a false clean.
|
|
3701
|
+
report.checks['gsd-record-drift'] = {
|
|
3702
|
+
class: 'Q',
|
|
3703
|
+
status: 'error',
|
|
3704
|
+
detail: 'gsd-health output shape unrecognized -- W007/I001 parse skipped'
|
|
3705
|
+
+ (parseErr ? ' (' + parseErr + ')' : ''),
|
|
3706
|
+
w007: [],
|
|
3707
|
+
i001: [],
|
|
3708
|
+
};
|
|
3709
|
+
} else {
|
|
3710
|
+
const w007 = health.warnings.filter((w) => w && w.code === 'W007');
|
|
3711
|
+
const i001 = health.info.filter((i) => i && i.code === 'I001');
|
|
3712
|
+
// Pitfall 3: a broken/error top-level status is indeterminate -- surface
|
|
3713
|
+
// it rather than reporting clean when there are no parsed findings.
|
|
3714
|
+
const topStatus = String(health.status || '');
|
|
3715
|
+
const indeterminate = (topStatus === 'broken' || topStatus === 'error');
|
|
3716
|
+
let qStatus;
|
|
3717
|
+
if (w007.length || i001.length) qStatus = 'warn';
|
|
3718
|
+
else if (indeterminate) qStatus = 'error';
|
|
3719
|
+
else qStatus = 'ok';
|
|
3720
|
+
report.checks['gsd-record-drift'] = {
|
|
3721
|
+
class: 'Q',
|
|
3722
|
+
status: qStatus,
|
|
3723
|
+
detail: qStatus === 'warn'
|
|
3724
|
+
? ('gsd-record drift: ' + w007.length + ' W007 (ROADMAP gap) + ' + i001.length + ' I001 (missing SUMMARY)')
|
|
3725
|
+
: (qStatus === 'error'
|
|
3726
|
+
? ('gsd-health status "' + topStatus + '" -- record-drift indeterminate')
|
|
3727
|
+
: 'gsd execution records clean (no W007 / I001)'),
|
|
3728
|
+
health_status: topStatus,
|
|
3729
|
+
w007,
|
|
3730
|
+
i001,
|
|
3731
|
+
};
|
|
3732
|
+
}
|
|
3733
|
+
}
|
|
3734
|
+
} catch (err) {
|
|
3735
|
+
report.checks['gsd-record-drift'] = {
|
|
3736
|
+
class: 'Q',
|
|
3737
|
+
status: 'error',
|
|
3738
|
+
detail: (err && err.message) || 'class Q threw',
|
|
3739
|
+
w007: [],
|
|
3740
|
+
i001: [],
|
|
3741
|
+
};
|
|
3742
|
+
}
|
|
3743
|
+
}
|
|
3744
|
+
|
|
3745
|
+
// Class R: RUNTIME-REACHABILITY drift (Phase 185, DRIFT-01). The assertion
|
|
3746
|
+
// Classes P + Q lack: P/Q do MERGE-TIME marking (prose-vs-code + gsd-record)
|
|
3747
|
+
// only -- neither checks whether a WIRED capability is actually REACHABLE by
|
|
3748
|
+
// decide() at runtime. Class R closes that gap (CIRS R9 -- doctor --drift is
|
|
3749
|
+
// the SCHEDULED reconciliation surface): it FAILS when a capability is WIRED
|
|
3750
|
+
// in the connector registry but the Phase-184 decide()-time projection READER
|
|
3751
|
+
// can never surface it. Unlike P (report-only prose) this is a HARD signal --
|
|
3752
|
+
// a runtime-unreachable WIRED capability flips doctor --drift to non-zero (the
|
|
3753
|
+
// narrow exit branch in _finalizeAndExit keys on this check's 'warn' status).
|
|
3754
|
+
//
|
|
3755
|
+
// Pure LOCAL (Canon Part 8): reads the connector registry + the orchestration
|
|
3756
|
+
// projection (both committed LOCAL artifacts) via the Phase-184 reader; zero
|
|
3757
|
+
// network egress. Reachability is computed DETERMINISTICALLY in code,
|
|
3758
|
+
// never by an LLM-judge. Soft-fail: a throw records an 'error' finding (exit
|
|
3759
|
+
// stays 0 -- an indeterminate computation is not a drift FAIL), never crashes
|
|
3760
|
+
// doctor. Hermetic test seams: MOS_TEST_REACHABILITY_REGISTRY /
|
|
3761
|
+
// MOS_TEST_REACHABILITY_PROJECTION inject fixture artifact paths.
|
|
3762
|
+
if (flags.drift) {
|
|
3763
|
+
try {
|
|
3764
|
+
const { checkRuntimeReachability } = require(
|
|
3765
|
+
path.join(__dirname, '..', 'lib', 'core', 'drift-runtime-reachability.cjs'));
|
|
3766
|
+
const rrOpts = {};
|
|
3767
|
+
if (process.env.MOS_TEST_REACHABILITY_REGISTRY) {
|
|
3768
|
+
rrOpts.registryPath = process.env.MOS_TEST_REACHABILITY_REGISTRY;
|
|
3769
|
+
}
|
|
3770
|
+
if (process.env.MOS_TEST_REACHABILITY_PROJECTION) {
|
|
3771
|
+
rrOpts.projectionPath = process.env.MOS_TEST_REACHABILITY_PROJECTION;
|
|
3772
|
+
}
|
|
3773
|
+
const rr = checkRuntimeReachability(rrOpts);
|
|
3774
|
+
report.checks['runtime-reachability'] = Object.assign({ class: 'R' }, rr);
|
|
3775
|
+
} catch (err) {
|
|
3776
|
+
report.checks['runtime-reachability'] = {
|
|
3777
|
+
class: 'R',
|
|
3778
|
+
status: 'error',
|
|
3779
|
+
detail: (err && err.message) || 'class R threw',
|
|
3780
|
+
wired_eligible: 0,
|
|
3781
|
+
reachable: 0,
|
|
3782
|
+
unreachable: [],
|
|
3783
|
+
report_only: false,
|
|
3784
|
+
};
|
|
3785
|
+
}
|
|
3786
|
+
}
|
|
3787
|
+
|
|
3788
|
+
// --drift --fix HEAL ARM (D-03 heal-where-safe split). MECHANICAL drift
|
|
3789
|
+
// auto-heals: write the DRIFT.md baseline (root index + per-folder detail)
|
|
3790
|
+
// from the Class P + Class Q findings, and stub a missing SUMMARY for each
|
|
3791
|
+
// I001 record. JUDGMENT-CALL drift stays REPORT-ONLY: Class P prose findings
|
|
3792
|
+
// are recorded, NEVER auto-fixed. This arm MUST NOT call process.exit (exit
|
|
3793
|
+
// is _finalizeAndExit-only -- preserves the exit-0 invariant + the deadlock
|
|
3794
|
+
// carve-out). Soft-fail: a throw records a heal error, never crashes doctor.
|
|
3795
|
+
if (flags.drift && flags.fix) {
|
|
3796
|
+
try {
|
|
3797
|
+
const driftRepoRoot = path.resolve(__dirname, '..');
|
|
3798
|
+
const planningDir = path.join(driftRepoRoot, '.planning');
|
|
3799
|
+
const {
|
|
3800
|
+
writeDriftBaseline,
|
|
3801
|
+
stubMissingSummary,
|
|
3802
|
+
} = require(path.join(__dirname, '..', 'lib', 'core', 'drift-baseline.cjs'));
|
|
3803
|
+
|
|
3804
|
+
// Build the finding set from Class P + Class Q results.
|
|
3805
|
+
const findings = [];
|
|
3806
|
+
const qRes = report.checks['gsd-record-drift'];
|
|
3807
|
+
if (qRes && Array.isArray(qRes.w007)) {
|
|
3808
|
+
for (const w of qRes.w007) {
|
|
3809
|
+
const phase = extractPhaseToken(w && w.message);
|
|
3810
|
+
findings.push({
|
|
3811
|
+
finding_id: 'W007-' + (phase || 'unknown'),
|
|
3812
|
+
track: 'gsd_record',
|
|
3813
|
+
severity: 'warn',
|
|
3814
|
+
status: 'open',
|
|
3815
|
+
source_class: 'Q',
|
|
3816
|
+
detail: (w && w.message) || 'ROADMAP gap',
|
|
3817
|
+
phase: phase,
|
|
3818
|
+
per_folder: !!phase,
|
|
3819
|
+
});
|
|
3820
|
+
}
|
|
3821
|
+
}
|
|
3822
|
+
const summaryStubs = [];
|
|
3823
|
+
if (qRes && Array.isArray(qRes.i001)) {
|
|
3824
|
+
for (const i of qRes.i001) {
|
|
3825
|
+
const planRef = extractPlanRef(i && i.message);
|
|
3826
|
+
const phase = planRef ? planRef.phase : null;
|
|
3827
|
+
findings.push({
|
|
3828
|
+
finding_id: 'I001-' + (planRef ? planRef.planLabel : 'unknown'),
|
|
3829
|
+
track: 'gsd_record',
|
|
3830
|
+
severity: 'info',
|
|
3831
|
+
status: 'open',
|
|
3832
|
+
source_class: 'Q',
|
|
3833
|
+
detail: (i && i.message) || 'missing SUMMARY',
|
|
3834
|
+
phase: phase,
|
|
3835
|
+
per_folder: !!phase,
|
|
3836
|
+
});
|
|
3837
|
+
if (planRef) summaryStubs.push(planRef);
|
|
3838
|
+
}
|
|
3839
|
+
}
|
|
3840
|
+
// Class P prose findings are recorded REPORT-ONLY in the root index; they
|
|
3841
|
+
// carry per_folder:false so the writer renders a (report-only) pointer and
|
|
3842
|
+
// never writes a prose file.
|
|
3843
|
+
const pRes = report.checks['prose-vs-code'];
|
|
3844
|
+
if (pRes && (pRes.status === 'warn')) {
|
|
3845
|
+
findings.push({
|
|
3846
|
+
finding_id: 'P-prose-vs-code',
|
|
3847
|
+
track: 'prose_vs_code',
|
|
3848
|
+
severity: 'high',
|
|
3849
|
+
status: 'open',
|
|
3850
|
+
source_class: 'P',
|
|
3851
|
+
detail: pRes.detail || 'prose-vs-code drift (report-only)',
|
|
3852
|
+
phase: null,
|
|
3853
|
+
per_folder: false,
|
|
3854
|
+
});
|
|
3855
|
+
}
|
|
3856
|
+
|
|
3857
|
+
const healResult = { drift_md: null, summary_stubs: [], errors: [] };
|
|
3858
|
+
try {
|
|
3859
|
+
const wr = writeDriftBaseline({
|
|
3860
|
+
planningDir,
|
|
3861
|
+
findings,
|
|
3862
|
+
meta: {
|
|
3863
|
+
audit_date: new Date().toISOString().slice(0, 10),
|
|
3864
|
+
auditor: 'doctor --drift --fix (Class P + Q)',
|
|
3865
|
+
status: (qRes && qRes.health_status) || 'unknown',
|
|
3866
|
+
process_note: 'Bound per-agent scope, fan wider (Fable 600s watchdog lesson).',
|
|
3867
|
+
},
|
|
3868
|
+
});
|
|
3869
|
+
healResult.drift_md = { rootPath: wr.rootPath, wrote: wr.wrote };
|
|
3870
|
+
} catch (e) {
|
|
3871
|
+
healResult.errors.push('drift-baseline write: ' + ((e && e.message) || e));
|
|
3872
|
+
}
|
|
3873
|
+
// Stub missing SUMMARYs (I001 mechanical auto-heal).
|
|
3874
|
+
for (const ref of summaryStubs) {
|
|
3875
|
+
try {
|
|
3876
|
+
const phaseDir = findPhaseDir(driftRepoRoot, ref.phase);
|
|
3877
|
+
if (!phaseDir) {
|
|
3878
|
+
healResult.errors.push('no phase dir for ' + ref.phase + ' (' + ref.planFile + ')');
|
|
3879
|
+
continue;
|
|
3880
|
+
}
|
|
3881
|
+
const st = stubMissingSummary({ phaseDir, planFile: ref.planFile });
|
|
3882
|
+
healResult.summary_stubs.push({ path: st.path, created: st.created });
|
|
3883
|
+
} catch (e) {
|
|
3884
|
+
healResult.errors.push('summary stub ' + ref.planFile + ': ' + ((e && e.message) || e));
|
|
3885
|
+
}
|
|
3886
|
+
}
|
|
3887
|
+
report.checks['drift-heal'] = {
|
|
3888
|
+
class: 'Q',
|
|
3889
|
+
status: healResult.errors.length ? 'warn' : 'ok',
|
|
3890
|
+
detail: 'DRIFT.md baseline written; ' + healResult.summary_stubs.filter((s) => s.created).length
|
|
3891
|
+
+ ' SUMMARY stub(s) created'
|
|
3892
|
+
+ (healResult.errors.length ? '; ' + healResult.errors.length + ' heal error(s)' : ''),
|
|
3893
|
+
drift_md: healResult.drift_md,
|
|
3894
|
+
summary_stubs: healResult.summary_stubs,
|
|
3895
|
+
errors: healResult.errors,
|
|
3896
|
+
report_only_prose: true,
|
|
3897
|
+
};
|
|
3898
|
+
} catch (err) {
|
|
3899
|
+
report.checks['drift-heal'] = {
|
|
3900
|
+
class: 'Q',
|
|
3901
|
+
status: 'error',
|
|
3902
|
+
detail: (err && err.message) || 'drift heal arm threw',
|
|
3903
|
+
summary_stubs: [],
|
|
3904
|
+
errors: [(err && err.message) || 'threw'],
|
|
3905
|
+
};
|
|
3906
|
+
}
|
|
3907
|
+
}
|
|
3908
|
+
|
|
3909
|
+
// Phase 127-02 BRAIN-MCP-127-08: Class M (Brain end-to-end smoke) under
|
|
3910
|
+
// combined runs (e.g. --all). The standalone --brain-smoke path is handled
|
|
3911
|
+
// earlier with its own canonical output shape; here we attach the result
|
|
3912
|
+
// into report.checks['brain-smoke'] so --all sees all classes in one
|
|
3913
|
+
// report. Async-to-sync bridge: run-and-finalize via promise chain.
|
|
3914
|
+
// Phase 139 Plan-02 (S2): the accumulative-engine selector. It is the ENGINE,
|
|
3915
|
+
// not a class -- so it is NOT gated behind a class A-M flag. It runs under
|
|
3916
|
+
// --all (every doctor sweep heals the install forward) and standalone (a bare
|
|
3917
|
+
// `doctor` run). With the registry seeded EMPTY of organ modules it selects
|
|
3918
|
+
// zero modules and stays a no-op; the wiring is what Plan 03 builds on.
|
|
3919
|
+
// Soft-fail: a throw here records an error finding but never crashes doctor.
|
|
3920
|
+
// Phase 217 Plan-01: the engine call is now UNCONDITIONAL (cadence:always
|
|
3921
|
+
// modules must run on every invocation via their own flag gates). The engine
|
|
3922
|
+
// itself reproduces the old class-A/N gate internally for the once-pass +
|
|
3923
|
+
// self-report row via opts.classFlagsActive. main() spreads the engine's
|
|
3924
|
+
// per-module always results into report.checks (so the generic renderer +
|
|
3925
|
+
// computeSummary treat each as a top-level row) and appends any recovered
|
|
3926
|
+
// records; the self-report row is attached ONLY under the old gate so a
|
|
3927
|
+
// --room-md-only run does not grow a new accumulative-engine row.
|
|
3928
|
+
try {
|
|
3929
|
+
const engineResult = runAccumulativeEngine({
|
|
3930
|
+
fix: flags.fix,
|
|
3931
|
+
dryRun: flags.dryRun,
|
|
3932
|
+
flags,
|
|
3933
|
+
classFlagsActive,
|
|
3934
|
+
});
|
|
3935
|
+
const { checks: engineChecks, recovered: engineRecovered, ...engineSelf } = engineResult;
|
|
3936
|
+
Object.assign(report.checks, engineChecks || {});
|
|
3937
|
+
if (Array.isArray(engineRecovered)) {
|
|
3938
|
+
for (const rec of engineRecovered) report.recovered.push(rec);
|
|
3939
|
+
}
|
|
3940
|
+
if (flags.all || flags.fix || !classFlagsActive) {
|
|
3941
|
+
report.checks['accumulative-engine'] = engineSelf;
|
|
3942
|
+
}
|
|
3943
|
+
} catch (err) {
|
|
3944
|
+
report.checks['accumulative-engine'] = { status: 'error', detail: (err && err.message) || 'engine threw', selected: [], deferred: [], findings: [], advanced: false };
|
|
3945
|
+
}
|
|
3946
|
+
|
|
3947
|
+
// BUG 7 reinterpretation (relocated from the retired class I/J main() block,
|
|
3948
|
+
// Phase 217 Plan 06): class A (install-cache) x class I (install-state)
|
|
3949
|
+
// interplay. When class A reports install status:'missing' AND class I
|
|
3950
|
+
// resolves topology=='marketplace-cache', the legacy clone path is EXPECTED
|
|
3951
|
+
// absent (Claude Code loads from the cache), so downgrade class A to a note
|
|
3952
|
+
// and neutralize an install-missing-based drift. Class I now runs inside the
|
|
3953
|
+
// accumulative engine, so this reads the engine-populated
|
|
3954
|
+
// report.checks['install-state']. Gated on flags.installState (the flag that
|
|
3955
|
+
// activates class I), matching the pre-migration guard.
|
|
3956
|
+
if (flags.installState) {
|
|
3957
|
+
const stateAfter = report.checks['install-state'];
|
|
3958
|
+
if (report.install && report.install.status === 'missing'
|
|
3959
|
+
&& stateAfter && stateAfter.topology === 'marketplace-cache') {
|
|
3960
|
+
report.install = Object.assign({}, report.install, {
|
|
3961
|
+
status: 'ok',
|
|
3962
|
+
note: 'legacy clone path expected absent on a marketplace-cache install (Bug 7 fix)',
|
|
3963
|
+
bug7_fix: true,
|
|
3964
|
+
});
|
|
3965
|
+
if (report.drift && report.drift.reason === 'install-missing') {
|
|
3966
|
+
report.drift = { detected: false, reason: 'bug7-fix-marketplace-cache' };
|
|
3967
|
+
}
|
|
3968
|
+
}
|
|
3969
|
+
}
|
|
3970
|
+
|
|
3971
|
+
|
|
3972
|
+
if (flags.brainSmoke) {
|
|
3973
|
+
const { checkBrainSmoke } = require(path.join(__dirname, '..', 'lib', 'core', 'doctor', 'class-m-brain-smoke.cjs'));
|
|
3974
|
+
checkBrainSmoke().then(function (result) {
|
|
3975
|
+
report.checks['brain-smoke'] = Object.assign({ class: 'M' }, result);
|
|
3976
|
+
_finalizeAndExit(flags, report, classFlagsActive, cacheResult, installResult);
|
|
3977
|
+
}).catch(function (err) {
|
|
3978
|
+
report.checks['brain-smoke'] = { class: 'M', status: 'error', detail: err && err.message };
|
|
3979
|
+
_finalizeAndExit(flags, report, classFlagsActive, cacheResult, installResult);
|
|
3980
|
+
});
|
|
3981
|
+
return;
|
|
3982
|
+
}
|
|
3983
|
+
|
|
3984
|
+
// quick(260706-13z) D14: Class S (Eureka smoke) under combined runs (e.g. a
|
|
3985
|
+
// --brain-smoke --eureka-smoke pair, or --all). The standalone --eureka-smoke
|
|
3986
|
+
// path is handled earlier with its own canonical output; here we attach the
|
|
3987
|
+
// result into report.checks['eureka-smoke'] beside the brain-smoke block.
|
|
3988
|
+
if (flags.eurekaSmoke) {
|
|
3989
|
+
const { checkEurekaSmoke } = require(path.join(__dirname, '..', 'lib', 'core', 'doctor', 'class-s-eureka-smoke.cjs'));
|
|
3990
|
+
checkEurekaSmoke().then(function (result) {
|
|
3991
|
+
report.checks['eureka-smoke'] = Object.assign({ class: 'S' }, result);
|
|
3992
|
+
_finalizeAndExit(flags, report, classFlagsActive, cacheResult, installResult);
|
|
3993
|
+
}).catch(function (err) {
|
|
3994
|
+
report.checks['eureka-smoke'] = { class: 'S', status: 'error', detail: err && err.message };
|
|
3995
|
+
_finalizeAndExit(flags, report, classFlagsActive, cacheResult, installResult);
|
|
3996
|
+
});
|
|
3997
|
+
return;
|
|
3998
|
+
}
|
|
3999
|
+
|
|
4000
|
+
_finalizeAndExit(flags, report, classFlagsActive, cacheResult, installResult);
|
|
4001
|
+
}
|
|
4002
|
+
|
|
4003
|
+
// -- --check-rs-engine probe (Phase 127.2 Plan 03, Findings F1 + F7) ---------
|
|
4004
|
+
//
|
|
4005
|
+
// Pre-flights Python deps reachable from scripts/rs-engine.py for the Engine 1
|
|
4006
|
+
// Act 1 surface. Probes:
|
|
4007
|
+
// - `requests` (transitive via lib/core/rs_corpus.py; the Windows 2026-05-23
|
|
4008
|
+
// silent-failure root cause)
|
|
4009
|
+
// - `numpy` (direct top-level import in rs-engine.py + rs_math)
|
|
4010
|
+
// Plus best-effort umbrella probe of `sentence_transformers` + `sklearn`
|
|
4011
|
+
// (declared in requirements-hsi.txt; used by sibling Engine 1 surfaces).
|
|
4012
|
+
//
|
|
4013
|
+
// Python resolution: `python3` preferred (POSIX convention + Aryeh's tester
|
|
4014
|
+
// machine has both py3.13 + py3.14 reachable via `python3`); falls back to
|
|
4015
|
+
// `python` (Windows / older installs). Honors MINDRIAN_PYTHON env override
|
|
4016
|
+
// for tester / CI use, matching lib/agents/reverse-salient-agent.cjs:181.
|
|
4017
|
+
//
|
|
4018
|
+
// JSON exit (always emits on --json): { ok, ready, missing, python, fix }.
|
|
4019
|
+
// Human exit: actionable fix line on FAIL; concise OK line on PASS.
|
|
4020
|
+
//
|
|
4021
|
+
// Phase-134-python-elimination-false-complete RCA, Change 1 (2026-08-27):
|
|
4022
|
+
// `--fix` composes with `--check-rs-engine` (same convention as `--drift --fix`
|
|
4023
|
+
// and `--graph-derive-health --fix` / `--heal-room`) to actually install the
|
|
4024
|
+
// missing deps via `python -m pip install --user`, instead of only printing
|
|
4025
|
+
// the manual instruction. Explicit `--fix` invocation IS the consent gate --
|
|
4026
|
+
// same precedent as every other `--fix` composition in this file. Without
|
|
4027
|
+
// `--fix`, behavior is unchanged: probe-only, printed instruction.
|
|
4028
|
+
async function runCheckRsEngine(flags) {
|
|
4029
|
+
const cp = require('node:child_process');
|
|
4030
|
+
// Critical deps (rs-engine.py path -- failure here = /mos:find-bottlenecks down).
|
|
4031
|
+
const critical = ['requests', 'numpy'];
|
|
4032
|
+
// Umbrella deps (broader Engine 1 Act 1 surface; missing -> warn but not fail).
|
|
4033
|
+
const umbrella = ['sentence_transformers', 'sklearn'];
|
|
4034
|
+
const all = critical.concat(umbrella);
|
|
4035
|
+
const fix = 'Run: pip install -r requirements-hsi.txt --user (use python -m pip if pip is not in PATH), or `/mos:doctor --check-rs-engine --fix` to install automatically';
|
|
4036
|
+
// import-name -> pip-package-name (they differ for two of the four deps).
|
|
4037
|
+
const pipName = {
|
|
4038
|
+
requests: 'requests',
|
|
4039
|
+
numpy: 'numpy',
|
|
4040
|
+
sentence_transformers: 'sentence-transformers',
|
|
4041
|
+
sklearn: 'scikit-learn',
|
|
4042
|
+
};
|
|
4043
|
+
|
|
4044
|
+
// Resolve python interpreter: MINDRIAN_PYTHON > python3 > python.
|
|
4045
|
+
// Each candidate gets a probe; first one that responds wins.
|
|
4046
|
+
function probePythonAvailable(bin) {
|
|
4047
|
+
try {
|
|
4048
|
+
const r = cp.spawnSync(bin, ['-c', 'import sys; print(sys.version)'], {
|
|
4049
|
+
encoding: 'utf8',
|
|
4050
|
+
timeout: 5000,
|
|
4051
|
+
});
|
|
4052
|
+
return r && r.status === 0;
|
|
4053
|
+
} catch (_e) {
|
|
4054
|
+
return false;
|
|
4055
|
+
}
|
|
4056
|
+
}
|
|
4057
|
+
let python = process.env.MINDRIAN_PYTHON || null;
|
|
4058
|
+
if (!python || !probePythonAvailable(python)) {
|
|
4059
|
+
if (probePythonAvailable('python3')) python = 'python3';
|
|
4060
|
+
else if (probePythonAvailable('python')) python = 'python';
|
|
4061
|
+
else python = null;
|
|
4062
|
+
}
|
|
4063
|
+
|
|
4064
|
+
if (!python) {
|
|
4065
|
+
const payload = {
|
|
4066
|
+
ok: false,
|
|
4067
|
+
ready: false,
|
|
4068
|
+
missing: all.slice(),
|
|
4069
|
+
python: null,
|
|
4070
|
+
fix: 'Python 3 is not on PATH. Install Python 3.10+ then: ' + fix,
|
|
4071
|
+
};
|
|
4072
|
+
if (flags.json) {
|
|
4073
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
4074
|
+
} else {
|
|
4075
|
+
console.log('Engine 1 Act 1 (/mos:find-bottlenecks et al.) cannot start: Python is not on PATH.');
|
|
4076
|
+
console.log(payload.fix);
|
|
4077
|
+
}
|
|
4078
|
+
return 1;
|
|
4079
|
+
}
|
|
4080
|
+
|
|
4081
|
+
// Per-dep probe via `python -c "import <name>"`. Each probe is timeout-fenced
|
|
4082
|
+
// so a hung interpreter cannot stall /mos:doctor. Status code 0 = present.
|
|
4083
|
+
function probeImport(dep) {
|
|
4084
|
+
try {
|
|
4085
|
+
const r = cp.spawnSync(python, ['-c', 'import ' + dep], {
|
|
4086
|
+
encoding: 'utf8',
|
|
4087
|
+
timeout: 10000,
|
|
4088
|
+
});
|
|
4089
|
+
const ok = r && r.status === 0;
|
|
4090
|
+
// Capture stderr tail on failure for diagnostic surface (mirror F2 pattern).
|
|
4091
|
+
const stderrTail = (!ok && r && r.stderr) ? String(r.stderr).slice(-200) : '';
|
|
4092
|
+
return { dep: dep, ok: ok, stderrTail: stderrTail };
|
|
4093
|
+
} catch (e) {
|
|
4094
|
+
return { dep: dep, ok: false, stderrTail: String((e && e.message) || '').slice(-200) };
|
|
4095
|
+
}
|
|
4096
|
+
}
|
|
4097
|
+
|
|
4098
|
+
let results = all.map(probeImport);
|
|
4099
|
+
let missingCritical = results.filter(function (r) {
|
|
4100
|
+
return critical.indexOf(r.dep) !== -1 && !r.ok;
|
|
4101
|
+
}).map(function (r) { return r.dep; });
|
|
4102
|
+
let missingUmbrella = results.filter(function (r) {
|
|
4103
|
+
return umbrella.indexOf(r.dep) !== -1 && !r.ok;
|
|
4104
|
+
}).map(function (r) { return r.dep; });
|
|
4105
|
+
let ok = missingCritical.length === 0;
|
|
4106
|
+
|
|
4107
|
+
// Self-remediation (--fix): attempt `python -m pip install --user` for
|
|
4108
|
+
// every currently-missing dep, then re-probe. Explicit `--fix` is the
|
|
4109
|
+
// consent gate (matches --drift --fix / --heal-room precedent elsewhere
|
|
4110
|
+
// in this file) -- no prompt, no dry-run-first, same posture as the
|
|
4111
|
+
// already-shipped scripts/lib/ensure_ml_deps.py auto-installer.
|
|
4112
|
+
let remediation = null;
|
|
4113
|
+
if (!ok || missingUmbrella.length > 0) {
|
|
4114
|
+
if (flags.fix) {
|
|
4115
|
+
const toInstall = missingCritical.concat(missingUmbrella);
|
|
4116
|
+
const pipArgs = ['-m', 'pip', 'install', '--user', '--quiet']
|
|
4117
|
+
.concat(toInstall.map(function (dep) { return pipName[dep] || dep; }));
|
|
4118
|
+
if (!flags.json) {
|
|
4119
|
+
console.log('Engine 1 Act 1: installing missing Python deps (' + toInstall.join(', ') + ')...');
|
|
4120
|
+
}
|
|
4121
|
+
let installResult;
|
|
4122
|
+
try {
|
|
4123
|
+
installResult = cp.spawnSync(python, pipArgs, { encoding: 'utf8', timeout: 300000 });
|
|
4124
|
+
} catch (e) {
|
|
4125
|
+
installResult = { status: -1, error: e };
|
|
4126
|
+
}
|
|
4127
|
+
const installOk = installResult && installResult.status === 0;
|
|
4128
|
+
// Re-probe regardless of the reported install exit code -- the ground
|
|
4129
|
+
// truth is whether the interpreter can import the module afterward,
|
|
4130
|
+
// not pip's exit status (mirrors ensure_ml_deps.py's own re-check).
|
|
4131
|
+
results = all.map(probeImport);
|
|
4132
|
+
missingCritical = results.filter(function (r) {
|
|
4133
|
+
return critical.indexOf(r.dep) !== -1 && !r.ok;
|
|
4134
|
+
}).map(function (r) { return r.dep; });
|
|
4135
|
+
missingUmbrella = results.filter(function (r) {
|
|
4136
|
+
return umbrella.indexOf(r.dep) !== -1 && !r.ok;
|
|
4137
|
+
}).map(function (r) { return r.dep; });
|
|
4138
|
+
ok = missingCritical.length === 0;
|
|
4139
|
+
remediation = {
|
|
4140
|
+
attempted: true,
|
|
4141
|
+
installed: toInstall,
|
|
4142
|
+
pip_exit_code: installResult ? installResult.status : null,
|
|
4143
|
+
resolved: installOk && missingCritical.concat(missingUmbrella).length === 0,
|
|
4144
|
+
still_missing: missingCritical.concat(missingUmbrella),
|
|
4145
|
+
stderr_tail: (installResult && installResult.stderr) ? String(installResult.stderr).slice(-200) : '',
|
|
4146
|
+
};
|
|
4147
|
+
} else {
|
|
4148
|
+
remediation = { attempted: false };
|
|
4149
|
+
}
|
|
4150
|
+
}
|
|
4151
|
+
|
|
4152
|
+
if (flags.json) {
|
|
4153
|
+
const payload = {
|
|
4154
|
+
ok: ok,
|
|
4155
|
+
ready: ok,
|
|
4156
|
+
python: python,
|
|
4157
|
+
probes: results.map(function (r) { return { dep: r.dep, ok: r.ok }; }),
|
|
4158
|
+
missing: missingCritical.concat(missingUmbrella),
|
|
4159
|
+
missing_critical: missingCritical,
|
|
4160
|
+
missing_umbrella: missingUmbrella,
|
|
4161
|
+
fix: ok ? null : fix,
|
|
4162
|
+
remediation: remediation,
|
|
4163
|
+
};
|
|
4164
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
4165
|
+
} else {
|
|
4166
|
+
if (ok) {
|
|
4167
|
+
console.log('Engine 1 Act 1 ready -- Python (' + python + ') has all critical deps: ' + critical.join(', ') + '.');
|
|
4168
|
+
if (remediation && remediation.attempted) {
|
|
4169
|
+
console.log('Auto-installed: ' + remediation.installed.join(', ') + '.');
|
|
4170
|
+
}
|
|
4171
|
+
if (missingUmbrella.length > 0) {
|
|
4172
|
+
console.log('Note: umbrella deps missing (' + missingUmbrella.join(', ') + '); affects sibling Engine 1 surfaces. ' + fix);
|
|
4173
|
+
}
|
|
4174
|
+
} else if (remediation && remediation.attempted) {
|
|
4175
|
+
console.log('Engine 1 Act 1 (/mos:find-bottlenecks et al.) still needs Python deps after auto-install attempt.');
|
|
4176
|
+
console.log('Still missing: ' + missingCritical.join(', ') + '.');
|
|
4177
|
+
if (remediation.stderr_tail) console.log('pip stderr (tail): ' + remediation.stderr_tail);
|
|
4178
|
+
console.log('Manual fallback -- pip install -r requirements-hsi.txt --user (use python -m pip if pip is not in PATH)');
|
|
4179
|
+
} else {
|
|
4180
|
+
console.log('Engine 1 Act 1 (/mos:find-bottlenecks et al.) needs Python deps. Missing: ' + missingCritical.join(', ') + '.');
|
|
4181
|
+
console.log(fix);
|
|
4182
|
+
}
|
|
4183
|
+
}
|
|
4184
|
+
|
|
4185
|
+
return ok ? 0 : 1;
|
|
4186
|
+
}
|
|
4187
|
+
|
|
4188
|
+
// -- Class M dispatcher (Phase 127-02) ---------------------------------------
|
|
4189
|
+
//
|
|
4190
|
+
// Mirrors the Class I dispatcher shape (scripts/doctor.cjs Class I precedent).
|
|
4191
|
+
// Returns the process exit code; per the class-flag invariant (line 1447) this
|
|
4192
|
+
// is 0 even on probe FAIL. The standalone --brain-smoke caller awaits this and
|
|
4193
|
+
// exits with whatever this returns; the --all combined caller goes through
|
|
4194
|
+
// _finalizeAndExit instead.
|
|
4195
|
+
async function classMBrainSmoke(flags) {
|
|
4196
|
+
const { checkBrainSmoke } = require(path.join(__dirname, '..', 'lib', 'core', 'doctor', 'class-m-brain-smoke.cjs'));
|
|
4197
|
+
const result = await checkBrainSmoke();
|
|
4198
|
+
if (flags.json) {
|
|
4199
|
+
console.log(JSON.stringify(Object.assign({ class: 'M' }, result), null, 2));
|
|
4200
|
+
} else {
|
|
4201
|
+
console.log('Class M -- Brain end-to-end smoke (6-layer probe)');
|
|
4202
|
+
console.log(' Overall: ' + (result.ok ? 'PASS' : 'FAIL') + ' (' + result.overall_ms + 'ms)');
|
|
4203
|
+
for (const layer of result.layers) {
|
|
4204
|
+
const marker = layer.ok ? 'PASS' : 'FAIL';
|
|
4205
|
+
console.log(' [' + marker + '] ' + layer.name + ' -- ' + layer.reason + ' (' + layer.ms + 'ms)');
|
|
4206
|
+
// Handoff section 7 item g: report which endpoint the wire resolved
|
|
4207
|
+
// to and whether it is canon. Guarded on payload being present so
|
|
4208
|
+
// layers 1-5 (which never carry one) print exactly as before.
|
|
4209
|
+
if (layer.payload) {
|
|
4210
|
+
const p = layer.payload;
|
|
4211
|
+
console.log(' endpoint=' + p.endpoint + ' node_count=' + p.node_count
|
|
4212
|
+
+ ' canon=' + p.canon + (p.override ? ' override=true' : ''));
|
|
4213
|
+
if (p.stamp) {
|
|
4214
|
+
const stampParts = [];
|
|
4215
|
+
if (p.stamp.schema_version != null) stampParts.push('schema_version=' + p.stamp.schema_version);
|
|
4216
|
+
if (p.stamp.last_reconciled != null) stampParts.push('last_reconciled=' + p.stamp.last_reconciled);
|
|
4217
|
+
if (p.stamp.refreshed_at != null) stampParts.push('refreshed_at=' + p.stamp.refreshed_at);
|
|
4218
|
+
console.log(' GraphRagMeta stamp: ' + stampParts.join(' '));
|
|
4219
|
+
}
|
|
4220
|
+
}
|
|
4221
|
+
}
|
|
4222
|
+
}
|
|
4223
|
+
// Class-flag invariant: exit 0 even on per-layer FAIL.
|
|
4224
|
+
return 0;
|
|
4225
|
+
}
|
|
4226
|
+
|
|
4227
|
+
// -- Class S dispatcher (quick 260706-13z) -----------------------------------
|
|
4228
|
+
//
|
|
4229
|
+
// Mirrors the Class M dispatcher shape. Returns the process exit code; per the
|
|
4230
|
+
// class-flag invariant this is 0 even on probe FAIL. The standalone
|
|
4231
|
+
// --eureka-smoke caller awaits this; the combined caller goes through
|
|
4232
|
+
// _finalizeAndExit instead.
|
|
4233
|
+
async function classSEurekaSmoke(flags) {
|
|
4234
|
+
const { checkEurekaSmoke } = require(path.join(__dirname, '..', 'lib', 'core', 'doctor', 'class-s-eureka-smoke.cjs'));
|
|
4235
|
+
const result = await checkEurekaSmoke();
|
|
4236
|
+
if (flags.json) {
|
|
4237
|
+
console.log(JSON.stringify(Object.assign({ class: 'S' }, result), null, 2));
|
|
4238
|
+
} else {
|
|
4239
|
+
console.log('Class S -- Eureka local-embedding-stack smoke (4-layer probe)');
|
|
4240
|
+
console.log(' Overall: ' + (result.ok ? 'PASS' : 'FAIL') + ' (' + result.overall_ms + 'ms)');
|
|
4241
|
+
for (const layer of result.layers) {
|
|
4242
|
+
const marker = layer.ok ? 'PASS' : 'FAIL';
|
|
4243
|
+
console.log(' [' + marker + '] ' + layer.name + ' -- ' + layer.reason + ' (' + layer.ms + 'ms)');
|
|
4244
|
+
}
|
|
4245
|
+
}
|
|
4246
|
+
// Class-flag invariant: exit 0 even on per-layer FAIL.
|
|
4247
|
+
return 0;
|
|
4248
|
+
}
|
|
4249
|
+
|
|
4250
|
+
// -- Output + exit finalizer (extracted Phase 127-02 to allow async injection)
|
|
4251
|
+
function _finalizeAndExit(flags, report, classFlagsActive, cacheResult, installResult) {
|
|
4252
|
+
// Output
|
|
4253
|
+
if (flags.json) {
|
|
4254
|
+
console.log(JSON.stringify(report, null, 2));
|
|
4255
|
+
} else {
|
|
4256
|
+
console.log(renderHumanReport(report));
|
|
4257
|
+
}
|
|
4258
|
+
|
|
4259
|
+
// Exit code.
|
|
4260
|
+
// Phase 185 (DRIFT-01): runtime-reachability is the ONE --drift check that
|
|
4261
|
+
// produces a HARD non-zero signal. A capability WIRED in the connector
|
|
4262
|
+
// registry but UNREACHABLE by decide() at runtime is a CIRS R9 drift FAIL, not
|
|
4263
|
+
// a report-only warning -- so a 'warn' here flips doctor --drift to exit 1.
|
|
4264
|
+
// This is evaluated BEFORE the classFlagsActive graceful-degradation exit-0,
|
|
4265
|
+
// but it is narrowly scoped: report.checks['runtime-reachability'] is
|
|
4266
|
+
// populated ONLY under flags.drift, so this branch can never alter the exit
|
|
4267
|
+
// code of any other class-flag run (--all does not set --drift), and it leaves
|
|
4268
|
+
// the marketplace-cache-drift-deadlock carve-out (Class A) untouched. A clean
|
|
4269
|
+
// reachability check (status 'ok') OR an indeterminate one ('error') falls
|
|
4270
|
+
// through to the normal exit-0 invariant, so the real registry+projection
|
|
4271
|
+
// (fully reachable today) keeps doctor --drift at exit 0 -- no regression.
|
|
4272
|
+
const reach = report.checks && report.checks['runtime-reachability'];
|
|
4273
|
+
if (reach && reach.status === 'warn') {
|
|
4274
|
+
process.exit(1);
|
|
4275
|
+
}
|
|
4276
|
+
// When class flags are active we honor the graceful-degradation invariant
|
|
4277
|
+
// (Canon Part 8): the doctor run NEVER aborts with non-zero unless an
|
|
4278
|
+
// explicit --fix attempt failed. Tests rely on this for hermetic scratch-
|
|
4279
|
+
// registry runs that have no real install directory.
|
|
4280
|
+
if (classFlagsActive) {
|
|
4281
|
+
process.exit(0);
|
|
4282
|
+
}
|
|
4283
|
+
// Phase 95.2 exit-code chain (additive; preserves existing 0/1/2/3 semantics, adds 4).
|
|
4284
|
+
// 0 = healthy (no drift detected).
|
|
4285
|
+
// 1 = drift detected (read-only mode OR --fix didn't run).
|
|
4286
|
+
// 2 = drift detected and recovered via --fix.
|
|
4287
|
+
// 3 = internal error (cache unreadable, or install in 'error' state -- not 'missing').
|
|
4288
|
+
// 4 = recovery attempted but rolled back to backup state (D-03).
|
|
4289
|
+
// 0 = drift detected but recovery skipped by design (marketplace-cache
|
|
4290
|
+
// topology; report.recoverySkipped set -- only ever under --fix, so
|
|
4291
|
+
// read-only drift keeps exiting 1). RCA:
|
|
4292
|
+
// doctor-marketplace-cache-drift-deadlock (2026-06-12).
|
|
4293
|
+
if (cacheResult.status !== 'ok') process.exit(3);
|
|
4294
|
+
if (installResult.status === 'error') process.exit(3);
|
|
4295
|
+
if (report.recoveryRolledBack) process.exit(4);
|
|
4296
|
+
if (report.classARecovered) process.exit(2);
|
|
4297
|
+
if (report.drift.detected && !report.recoverySkipped) process.exit(1);
|
|
4298
|
+
// Class N (DRIFT-12): the plugin-DISABLED silent-disable state is a drift
|
|
4299
|
+
// signal on a bare / non-class-flag run. enabled===false -> exit 1 so
|
|
4300
|
+
// `mindrian-os doctor` returns non-zero for the one failure mode that no
|
|
4301
|
+
// SessionStart hook can ever report (the hooks do not run while disabled).
|
|
4302
|
+
// ok / null / skip leave the exit at 0. Under class-flag mode the run already
|
|
4303
|
+
// returned 0 above (graceful-degradation invariant) -- the finding is still
|
|
4304
|
+
// visible in report.checks for --all consumers.
|
|
4305
|
+
const pen = report.checks && report.checks['plugin-enabled-state'];
|
|
4306
|
+
if (pen && pen.status === 'warn') process.exit(1);
|
|
4307
|
+
process.exit(0);
|
|
4308
|
+
}
|
|
4309
|
+
|
|
4310
|
+
// Phase 139 Plan-02: export the accumulative-engine selector for hermetic unit
|
|
4311
|
+
// testing (tests/test-doctor-module-selector.cjs requires this module and calls
|
|
4312
|
+
// runAccumulativeEngine with an injected registry + watermark HOME). Guard the
|
|
4313
|
+
// CLI entry so `require('doctor.cjs')` does NOT run main(); the script still
|
|
4314
|
+
// runs main() when invoked directly or spawned as a subprocess (the standard
|
|
4315
|
+
// `node scripts/doctor.cjs ...` usage, where require.main === module).
|
|
4316
|
+
// renderHumanReport + computeSummary are additively exported so
|
|
4317
|
+
// tests/test-doctor-fix-renderer.cjs can drive them as hermetic unit sub-tests
|
|
4318
|
+
// (warn-row visibility, Summary parity, recovered-line rendering) without
|
|
4319
|
+
// spawning a subprocess. The require.main guard below still prevents main().
|
|
4320
|
+
// Phase 225-02 (REQ-4): additive export of the WAL-reset advisory helpers for
|
|
4321
|
+
// hermetic unit testing via injected seams (tests/test-225-wal-advisory.cjs).
|
|
4322
|
+
// Additive-only append -- existing keys are never reordered.
|
|
4323
|
+
module.exports = { runAccumulativeEngine, renderHumanReport, computeSummary, _walResetAdvisory, _sqliteVersionLt, evaluateMarketplaceSourcePin };
|
|
4324
|
+
|
|
4325
|
+
if (require.main === module) {
|
|
4326
|
+
main();
|
|
4327
|
+
}
|