heyspex 0.1.0 → 0.2.1
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/marketplace.json +14 -0
- package/README.md +21 -0
- package/bin/hs.mjs +1089 -104
- package/bin/spexcode.mjs +262 -0
- package/dev-factory/engine/worktree.mjs +740 -0
- package/dev-factory/lib/code-repo-root.mjs +67 -0
- package/dev-factory/lib/context-budget.mjs +84 -0
- package/dev-factory/lib/epic-detect.mjs +190 -0
- package/dev-factory/lib/fleet-queue-file.mjs +846 -0
- package/dev-factory/lib/fleet-queue.mjs +426 -0
- package/dev-factory/lib/fleet-state.mjs +14 -18
- package/dev-factory/lib/model-catalog.mjs +338 -0
- package/dev-factory/lib/seat-context.mjs +199 -0
- package/dev-factory/lib/spec-io.mjs +93 -0
- package/dev-factory/lib/sufficiency-score.mjs +8 -2
- package/dev-factory/lib/worktree-allocator.mjs +744 -0
- package/dev-factory/model-catalog.json +222 -0
- package/dist/cli/hs-check.mjs +1127 -103
- package/dist/cli/hs-claim.mjs +981 -63
- package/dist/cli/hs-connect.mjs +10059 -1729
- package/dist/cli/hs-defer.mjs +5507 -3072
- package/dist/cli/hs-dev.mjs +576 -0
- package/dist/cli/hs-env.mjs +416 -302
- package/dist/cli/hs-index-rebuild.mjs +7334 -0
- package/dist/cli/hs-index-reconcile.mjs +8138 -0
- package/dist/cli/hs-init.mjs +2840 -149
- package/dist/cli/hs-install.mjs +7214 -594
- package/dist/cli/hs-join.mjs +10640 -2421
- package/dist/cli/hs-vault-pull.mjs +8157 -6370
- package/dist/cli/hs-vault-push.mjs +6433 -0
- package/dist/cli/hs-vault.mjs +4029 -1555
- package/dist/cli/hs-workspace.mjs +2702 -538
- package/dist/cli-templates/customer-vault/specs/entity-model.md +594 -601
- package/dist/cli-templates/customer-vault/specs/protocols/canonical-frontmatter-templates.md +110 -177
- package/dist/cli-templates/customer-vault/specs/protocols/destructive-action-checklist.md +122 -0
- package/dist/cli-templates/customer-vault/specs/protocols/memory-protocol.md +13 -15
- package/dist/cli-templates/customer-vault/specs/protocols/session-close-protocol.md +91 -0
- package/dist/cli-templates/customer-vault/specs/protocols/spec-hierarchy.md +150 -168
- package/dist/cli-templates/customer-vault/specs/protocols/spec-summary-authoring.md +83 -0
- package/dist/cli-templates/harness-commands/claude/hs-auto.md +36 -0
- package/dist/cli-templates/harness-commands/claude/hs-backfill.md +28 -0
- package/dist/cli-templates/harness-commands/claude/hs-capture.md +62 -0
- package/dist/cli-templates/harness-commands/claude/hs-defer.md +29 -0
- package/dist/cli-templates/harness-commands/claude/hs-dev.md +20 -0
- package/dist/cli-templates/harness-commands/claude/hs-doctor.md +32 -0
- package/dist/cli-templates/harness-commands/claude/hs-ground.md +30 -0
- package/dist/cli-templates/harness-commands/claude/hs-handoff.md +33 -0
- package/dist/cli-templates/harness-commands/claude/hs-log.md +27 -0
- package/dist/cli-templates/harness-commands/claude/hs-next.md +29 -0
- package/dist/cli-templates/harness-commands/claude/hs-promote.md +70 -0
- package/dist/cli-templates/harness-commands/claude/hs-push.md +35 -0
- package/dist/cli-templates/harness-commands/claude/hs-qc.md +27 -0
- package/dist/cli-templates/harness-commands/claude/hs-res.md +63 -0
- package/dist/cli-templates/harness-commands/claude/hs-resolve.md +23 -0
- package/dist/cli-templates/harness-commands/claude/hs-review.md +23 -0
- package/dist/cli-templates/harness-commands/claude/hs-view.md +28 -0
- package/dist/cli-templates/harness-commands/claude/kill-ports.md +32 -0
- package/dist/cli-templates/harness-commands/claude/prototype.md +91 -0
- package/dist/cli-templates/harness-commands/cursor/hs-auto.md +40 -0
- package/dist/cli-templates/harness-commands/cursor/hs-backfill.md +31 -0
- package/dist/cli-templates/harness-commands/cursor/hs-capture.md +65 -0
- package/dist/cli-templates/harness-commands/cursor/hs-defer.md +32 -0
- package/dist/cli-templates/harness-commands/cursor/hs-dev.md +24 -0
- package/dist/cli-templates/harness-commands/cursor/hs-doctor.md +36 -0
- package/dist/cli-templates/harness-commands/cursor/hs-ground.md +33 -0
- package/dist/cli-templates/harness-commands/cursor/hs-handoff.md +37 -0
- package/dist/cli-templates/harness-commands/cursor/hs-log.md +31 -0
- package/dist/cli-templates/harness-commands/cursor/hs-next.md +32 -0
- package/dist/cli-templates/harness-commands/cursor/hs-promote.md +73 -0
- package/dist/cli-templates/harness-commands/cursor/hs-push.md +38 -0
- package/dist/cli-templates/harness-commands/cursor/hs-qc.md +30 -0
- package/dist/cli-templates/harness-commands/cursor/hs-res.md +67 -0
- package/dist/cli-templates/harness-commands/cursor/hs-resolve.md +26 -0
- package/dist/cli-templates/harness-commands/cursor/hs-review.md +26 -0
- package/dist/cli-templates/harness-commands/cursor/hs-view.md +32 -0
- package/dist/cli-templates/harness-commands/cursor/kill-ports.md +35 -0
- package/dist/cli-templates/harness-commands/cursor/prototype.md +95 -0
- package/docs/copilot-plugin-install.md +162 -0
- package/docs/driving-heyspex-from-copilot.md +165 -0
- package/docs/mcp-client-config.md +259 -6
- package/package.json +249 -124
- package/plugins/heyspex/.claude-plugin/plugin.json +12 -0
- package/plugins/heyspex/.cursor-plugin/plugin.json +15 -0
- package/plugins/heyspex/.mcp.json +12 -0
- package/plugins/heyspex/README.md +309 -0
- package/plugins/heyspex/bin/launch-mcp.mjs +95 -0
- package/plugins/heyspex/bin/run-hook.mjs +80 -0
- package/plugins/heyspex/hooks/cursor-hooks.json +308 -0
- package/plugins/heyspex/hooks/hooks.json +418 -0
- package/plugins/heyspex/mcp.json +13 -0
- package/scripts/check-mcp-freshness.mjs +17 -9
- package/scripts/claude-hooks/_env-vault-events.mjs +68 -0
- package/scripts/claude-hooks/_guardrail-events.mjs +96 -0
- package/scripts/claude-hooks/_guardrail-loader.mjs +494 -0
- package/scripts/claude-hooks/_intent-classifier-router.mjs +456 -0
- package/scripts/claude-hooks/_intent-classifier-semantic.mjs +356 -0
- package/scripts/claude-hooks/_run.mjs +413 -0
- package/scripts/claude-hooks/_transcript-text.mjs +60 -0
- package/scripts/claude-hooks/_vault-retrieval.mjs +322 -0
- package/scripts/claude-hooks/agent-spawn-delegation-gate.mjs +133 -0
- package/scripts/claude-hooks/capture-execution-gate.mjs +51 -0
- package/scripts/claude-hooks/capture-workflow-gate.mjs +11 -0
- package/scripts/claude-hooks/check-auto-memory-budget.mjs +396 -0
- package/scripts/claude-hooks/check-claude-md-budget.mjs +327 -0
- package/scripts/claude-hooks/check-mcp-freshness-hook.mjs +14 -0
- package/scripts/claude-hooks/claim-before-code-gate.mjs +11 -0
- package/scripts/claude-hooks/claim-liveness-shared.mjs +577 -0
- package/scripts/claude-hooks/close-terminal-on-ship.mjs +64 -0
- package/scripts/claude-hooks/coo-denylist-gate.mjs +11 -0
- package/scripts/claude-hooks/coo-guard-check.mjs +11 -0
- package/scripts/claude-hooks/deep-research-cost-gate.mjs +11 -0
- package/scripts/claude-hooks/dev-server-task-reminder.mjs +15 -0
- package/scripts/claude-hooks/diagnose-capture-reminder.mjs +11 -0
- package/scripts/claude-hooks/directive-obligation-gate.mjs +11 -0
- package/scripts/claude-hooks/emit-claim-status.mjs +41 -0
- package/scripts/claude-hooks/enforce-session-sequence.mjs +11 -0
- package/scripts/claude-hooks/enforce-vault-naming.mjs +11 -0
- package/scripts/claude-hooks/env-vault-guard.mjs +11 -0
- package/scripts/claude-hooks/epic-milestone-phrasing-gate.mjs +11 -0
- package/scripts/claude-hooks/git-identity-guard.mjs +11 -0
- package/scripts/claude-hooks/git-reflog-audit.mjs +15 -0
- package/scripts/claude-hooks/git-stash-gate.mjs +11 -0
- package/scripts/claude-hooks/guard-auto-memory-writes.mjs +137 -0
- package/scripts/claude-hooks/id-display-format-gate.mjs +11 -0
- package/scripts/claude-hooks/inject-communication-style.mjs +86 -0
- package/scripts/claude-hooks/inject-coo-seat.mjs +78 -0
- package/scripts/claude-hooks/inject-dev-server-awareness.mjs +11 -0
- package/scripts/claude-hooks/inject-doctrine.mjs +13 -0
- package/scripts/claude-hooks/inject-vault-context.mjs +14 -0
- package/scripts/claude-hooks/intent-classifier-trigger.mjs +279 -0
- package/scripts/claude-hooks/log-session-activity.mjs +341 -0
- package/scripts/claude-hooks/naughty-list-gate.mjs +11 -0
- package/scripts/claude-hooks/notify-status-flip.mjs +72 -0
- package/scripts/claude-hooks/plan-exit-ingest-gate.mjs +170 -0
- package/scripts/claude-hooks/plan-ingest-close-reminder.mjs +66 -0
- package/scripts/claude-hooks/plan-review-gate.mjs +11 -0
- package/scripts/claude-hooks/preflight-reminder.mjs +262 -0
- package/scripts/claude-hooks/protected-action-intent.mjs +221 -0
- package/scripts/claude-hooks/research-method-guard.mjs +398 -0
- package/scripts/claude-hooks/research-protocol-gate.mjs +11 -0
- package/scripts/claude-hooks/session-end.mjs +451 -0
- package/scripts/claude-hooks/settings-write-gate.mjs +11 -0
- package/scripts/claude-hooks/settlement-check-gate.mjs +39 -0
- package/scripts/claude-hooks/status-flip-detect.mjs +17 -0
- package/scripts/claude-hooks/terminal-close-shared.mjs +426 -0
- package/scripts/claude-hooks/terminal-close-watcher.mjs +122 -0
- package/scripts/claude-hooks/vault-mirror-catchup.mjs +11 -0
- package/scripts/claude-hooks/vault-write-tier-guard.mjs +11 -0
- package/scripts/claude-hooks/wip-checkpoint.mjs +302 -0
- package/scripts/cursor-hooks/manifest.mjs +281 -0
- package/scripts/dev-server-status.mjs +34 -13
- package/scripts/dev-server-stop.mjs +313 -0
- package/scripts/heyspex-mode.mjs +288 -0
- package/scripts/hs-auto-resolve.mjs +240 -0
- package/scripts/hs-auto-source.mjs +398 -0
- package/scripts/hs-auto.mjs +134 -0
- package/scripts/hs-capture-cli.mjs +326 -0
- package/scripts/hs-classifier.mjs +136 -0
- package/scripts/hs-companion.mjs +88 -0
- package/scripts/hs-gateway.mjs +115 -0
- package/scripts/hs-mode.mjs +125 -0
- package/scripts/hs-next.mjs +441 -0
- package/scripts/hs-settings.mjs +338 -0
- package/scripts/hs-view.mjs +496 -0
- package/scripts/hs-worktree.mjs +201 -0
- package/scripts/install-claude-code-hooks.mjs +272 -0
- package/scripts/install-cursor-hooks.mjs +471 -0
- package/scripts/lib/agent-env-markers.mjs +38 -0
- package/scripts/lib/console-url-resolver.mjs +199 -61
- package/scripts/lib/customer-kit.mjs +1203 -0
- package/scripts/lib/dev-mode.mjs +249 -32
- package/scripts/lib/directive-obligations.mjs +349 -0
- package/scripts/lib/heyspex-home.mjs +72 -0
- package/scripts/lib/hook-dispositions.mjs +210 -0
- package/scripts/lib/hook-vault-header.mjs +30 -0
- package/scripts/lib/hs-command.mjs +277 -0
- package/scripts/lib/hs-companion-settings.mjs +472 -0
- package/scripts/lib/hs-source-branch.mjs +196 -0
- package/scripts/lib/hs-source-commit.mjs +59 -5
- package/scripts/lib/hs-source-identity.mjs +526 -0
- package/scripts/lib/hs-source-log.mjs +13 -22
- package/scripts/lib/hs-source-preflight.mjs +18 -4
- package/scripts/lib/hs-source-push.mjs +386 -307
- package/scripts/lib/hs-worktree-args.mjs +115 -0
- package/scripts/lib/shared-path-helpers.mjs +134 -0
- package/scripts/lib/standalone-doctor.mjs +540 -3
- package/scripts/lib/subagent-git-lock.mjs +109 -0
- package/scripts/lib/template-pin.mjs +34 -0
- package/scripts/lib/window-placement.mjs +109 -0
- package/scripts/provenance-lib.mjs +91 -0
- package/scripts/stamp-provenance-on-edit.mjs +222 -0
- package/scripts/wip-ledger-health.mjs +653 -0
- package/spexharness/doctrine/baseline/SPEXCODE-DOCTRINE.baseline.json +29 -0
- package/spexharness/src/adapters/audit.mjs +197 -0
- package/spexharness/src/adapters/claude-code.mjs +449 -0
- package/spexharness/src/adapters/copilot.mjs +236 -0
- package/spexharness/src/adapters/detect-harness.mjs +34 -0
- package/spexharness/src/chat/tui/ansi.mjs +448 -0
- package/spexharness/src/chat/tui/palette.mjs +22 -0
- package/spexharness/src/chat/tui/select.mjs +208 -0
- package/spexharness/src/config-root.mjs +54 -0
- package/spexharness/src/context-budget.mjs +141 -0
- package/spexharness/src/dev-server-control.mjs +389 -0
- package/spexharness/src/doctrine/behaviour.mjs +371 -0
- package/spexharness/src/doctrine/corpus.mjs +733 -0
- package/spexharness/src/doctrine/inject.mjs +338 -0
- package/spexharness/src/doctrine/merge.mjs +184 -0
- package/spexharness/src/doctrine/sources.mjs +252 -0
- package/spexharness/src/doctrine/synthesize.mjs +401 -0
- package/spexharness/src/doctrine/vault-source.mjs +574 -0
- package/spexharness/src/doctrine/writer.mjs +176 -0
- package/spexharness/src/events.mjs +84 -0
- package/spexharness/src/gates/_shell-write-targets.mjs +77 -0
- package/spexharness/src/gates/capture-workflow-gate.mjs +202 -0
- package/spexharness/src/gates/claim-before-code-gate.decide.mjs +33 -0
- package/spexharness/src/gates/claim-before-code-gate.mjs +129 -0
- package/spexharness/src/gates/deep-research-cost-gate.mjs +275 -0
- package/spexharness/src/gates/diagnose-capture-reminder.mjs +112 -0
- package/spexharness/src/gates/directive-obligation-gate.decide.mjs +132 -0
- package/spexharness/src/gates/directive-obligation-gate.mjs +183 -0
- package/spexharness/src/gates/enforce-session-sequence.mjs +895 -0
- package/spexharness/src/gates/enforce-vault-naming.mjs +215 -0
- package/spexharness/src/gates/epic-milestone-phrasing-gate.mjs +233 -0
- package/spexharness/src/gates/git-identity-guard.decide.mjs +436 -0
- package/spexharness/src/gates/git-identity-guard.mjs +61 -0
- package/spexharness/src/gates/git-reflog-audit.mjs +151 -0
- package/spexharness/src/gates/git-stash-gate.decide.mjs +361 -0
- package/spexharness/src/gates/git-stash-gate.mjs +159 -0
- package/spexharness/src/gates/id-display-format-gate.mjs +190 -0
- package/spexharness/src/gates/inject-doctrine-gate.mjs +89 -0
- package/spexharness/src/gates/inject-vault-context.mjs +239 -0
- package/spexharness/src/gates/naughty-list-gate.mjs +213 -0
- package/spexharness/src/gates/plan-review-gate.mjs +233 -0
- package/spexharness/src/gates/preflight-reminder-gate.mjs +265 -0
- package/spexharness/src/gates/research-protocol-gate.mjs +175 -0
- package/spexharness/src/gates/settings-write-gate.decide.mjs +96 -0
- package/spexharness/src/gates/settings-write-gate.mjs +104 -0
- package/spexharness/src/gates/vault-mirror-catchup.mjs +124 -0
- package/spexharness/src/gates/vault-write-tier-guard.decide.mjs +125 -0
- package/spexharness/src/gates/vault-write-tier-guard.mjs +42 -0
- package/spexharness/src/git-reflog-audit.mjs +294 -0
- package/spexharness/src/intent-classifier.mjs +196 -0
- package/spexharness/src/intent-directive.mjs +626 -0
- package/spexharness/src/profile/learn.mjs +551 -0
- package/spexharness/src/profile/proposals.mjs +357 -0
- package/spexharness/src/profile/vault-inclusion.mjs +386 -0
- package/spexharness/src/redact.mjs +151 -0
- package/spexharness/src/rule-table.mjs +849 -0
- package/spexharness/src/seat-context.mjs +122 -0
- package/spexharness/src/session-bootstrap.mjs +512 -0
- package/spexharness/src/session-record.mjs +815 -0
- package/spexharness/src/token-inventory.mjs +207 -0
- package/vendor/heyspex-mcp/account-switch.mjs +96 -0
- package/vendor/heyspex-mcp/alignment-gate.mjs +315 -1
- package/vendor/heyspex-mcp/blocker-resolver.mjs +37 -2
- package/vendor/heyspex-mcp/breakdown-ladder.mjs +119 -4
- package/vendor/heyspex-mcp/breakdown-source.mjs +271 -0
- package/vendor/heyspex-mcp/breakdown-writer.mjs +23 -3
- package/vendor/heyspex-mcp/capture-buffer-patches.mjs +77 -8
- package/vendor/heyspex-mcp/capture-channel.mjs +55 -5
- package/vendor/heyspex-mcp/capture-console-client.mjs +117 -3
- package/vendor/heyspex-mcp/capture-focus.mjs +1061 -31
- package/vendor/heyspex-mcp/capture-outbox.mjs +15 -5
- package/vendor/heyspex-mcp/capture-sources.mjs +195 -0
- package/vendor/heyspex-mcp/capture-tools.mjs +1264 -0
- package/vendor/heyspex-mcp/claim-tools.mjs +1147 -0
- package/vendor/heyspex-mcp/cli-verbs.mjs +2074 -0
- package/vendor/heyspex-mcp/cli.mjs +207 -15524
- package/vendor/heyspex-mcp/close-branch-report.mjs +102 -0
- package/vendor/heyspex-mcp/cloud-tool-surface.mjs +128 -0
- package/vendor/heyspex-mcp/cloud-vault-writes.mjs +602 -0
- package/vendor/heyspex-mcp/config-seed.yaml +15 -0
- package/vendor/heyspex-mcp/config-settings.mjs +290 -7
- package/vendor/heyspex-mcp/console-fetch.mjs +822 -17
- package/vendor/heyspex-mcp/console-transport.mjs +172 -0
- package/vendor/heyspex-mcp/credentials.mjs +585 -0
- package/vendor/heyspex-mcp/defer-spec.mjs +44 -4
- package/vendor/heyspex-mcp/deferred-reclaim.mjs +29 -3
- package/vendor/heyspex-mcp/design-handlers.mjs +31 -0
- package/vendor/heyspex-mcp/design-tools.mjs +4 -1
- package/vendor/heyspex-mcp/dev-factory-bridge.mjs +4 -4
- package/vendor/heyspex-mcp/dev-mode-core.mjs +45 -0
- package/vendor/heyspex-mcp/dev-tools.mjs +131 -0
- package/vendor/heyspex-mcp/diagnose-handlers.mjs +54 -0
- package/vendor/heyspex-mcp/events-tools.mjs +207 -0
- package/vendor/heyspex-mcp/filing-tools.mjs +723 -0
- package/vendor/heyspex-mcp/fleet-handlers.mjs +37 -0
- package/vendor/heyspex-mcp/fleet-queue-tools.mjs +71 -15
- package/vendor/heyspex-mcp/focus-tools.mjs +190 -0
- package/vendor/heyspex-mcp/folder-trust-store.mjs +60 -0
- package/vendor/heyspex-mcp/http-transport.mjs +302 -0
- package/vendor/heyspex-mcp/list-tools.mjs +257 -0
- package/vendor/heyspex-mcp/login-handoff.mjs +15 -1
- package/vendor/heyspex-mcp/mcp-config.mjs +107 -0
- package/vendor/heyspex-mcp/memory-tools.mjs +523 -0
- package/vendor/heyspex-mcp/mvp-mode.mjs +53 -6
- package/vendor/heyspex-mcp/obligation-backstop.mjs +103 -0
- package/vendor/heyspex-mcp/occupied-keys.mjs +142 -0
- package/vendor/heyspex-mcp/on-deck-brief.mjs +3377 -0
- package/vendor/heyspex-mcp/on-deck-scheduler.mjs +1708 -0
- package/vendor/heyspex-mcp/package-root.mjs +32 -0
- package/vendor/heyspex-mcp/peer-presence.mjs +8 -1
- package/vendor/heyspex-mcp/plan-ground.mjs +621 -652
- package/vendor/heyspex-mcp/plan-push.mjs +908 -22
- package/vendor/heyspex-mcp/plan-review.mjs +124 -30
- package/vendor/heyspex-mcp/preflight-gate.mjs +0 -0
- package/vendor/heyspex-mcp/procedure-handlers.mjs +327 -0
- package/vendor/heyspex-mcp/procedure-tools.mjs +1181 -970
- package/vendor/heyspex-mcp/promote-capture.mjs +388 -69
- package/vendor/heyspex-mcp/promote-diagnose.mjs +74 -0
- package/vendor/heyspex-mcp/promote-research-to-diagnose.mjs +9 -2
- package/vendor/heyspex-mcp/promote-research.mjs +666 -53
- package/vendor/heyspex-mcp/promotion-helpers.mjs +251 -0
- package/vendor/heyspex-mcp/prompts.mjs +1460 -0
- package/vendor/heyspex-mcp/read-path.mjs +191 -15
- package/vendor/heyspex-mcp/receipt-shape.mjs +146 -0
- package/vendor/heyspex-mcp/recipe-handlers.mjs +117 -0
- package/vendor/heyspex-mcp/regen-tasks.mjs +210 -3
- package/vendor/heyspex-mcp/register-spec.mjs +266 -54
- package/vendor/heyspex-mcp/request-context.mjs +83 -0
- package/vendor/heyspex-mcp/research-focus.mjs +207 -4
- package/vendor/heyspex-mcp/research-handlers.mjs +625 -0
- package/vendor/heyspex-mcp/research-tools.mjs +4 -4
- package/vendor/heyspex-mcp/response-budget.mjs +348 -0
- package/vendor/heyspex-mcp/review-focus.mjs +25 -1
- package/vendor/heyspex-mcp/roadmap-default.mjs +70 -0
- package/vendor/heyspex-mcp/search-specs.mjs +280 -0
- package/vendor/heyspex-mcp/seat-block-ledger.mjs +113 -0
- package/vendor/heyspex-mcp/seat-identity.mjs +2 -6
- package/vendor/heyspex-mcp/session-owner.mjs +109 -0
- package/vendor/heyspex-mcp/session-tools.mjs +162 -0
- package/vendor/heyspex-mcp/settings-registry.mjs +232 -0
- package/vendor/heyspex-mcp/settings-tools.mjs +85 -12
- package/vendor/heyspex-mcp/spec-edit-tools.mjs +453 -0
- package/vendor/heyspex-mcp/spec-id-lib.mjs +93 -17
- package/vendor/heyspex-mcp/split-research.mjs +304 -56
- package/vendor/heyspex-mcp/stdio-rpc.mjs +22 -0
- package/vendor/heyspex-mcp/submit-for-review.mjs +150 -7
- package/vendor/heyspex-mcp/summary-contract.mjs +105 -0
- package/vendor/heyspex-mcp/summary-write-tools.mjs +228 -0
- package/vendor/heyspex-mcp/symptom-query.mjs +146 -0
- package/vendor/heyspex-mcp/test-helpers.mjs +123 -1
- package/vendor/heyspex-mcp/tool-catalog.mjs +2819 -0
- package/vendor/heyspex-mcp/tool-dispatch.mjs +657 -0
- package/vendor/heyspex-mcp/tool-helpers.mjs +325 -0
- package/vendor/heyspex-mcp/validate-linkage.mjs +414 -27
- package/vendor/heyspex-mcp/vault-io.mjs +720 -0
- package/vendor/heyspex-mcp/vault-less-endpoints.mjs +52 -0
- package/vendor/heyspex-mcp/vault-resolve.mjs +554 -68
- package/vendor/heyspex-mcp/verify-reproduction.mjs +270 -30
- package/vendor/heyspex-mcp/whoami.mjs +92 -0
- package/vendor/heyspex-mcp/write-outbox.mjs +512 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve `/hs-auto [filename]` to a dev-factory dispatch target.
|
|
3
|
+
*/
|
|
4
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
5
|
+
import { basename, isAbsolute, join, normalize, relative } from 'node:path';
|
|
6
|
+
import { findSpecFile } from '../dev-factory/lib/spec-io.mjs';
|
|
7
|
+
// Epic/parent-shaped detection is the shared source of truth in dev-factory/lib —
|
|
8
|
+
// scripts/ already imports findSpecFile from there, so this import direction is
|
|
9
|
+
// established (2026-07-11 consolidation of the previously-triplicated predicate).
|
|
10
|
+
import { specFacts, isEpicSpec } from '../dev-factory/lib/epic-detect.mjs';
|
|
11
|
+
import {
|
|
12
|
+
fmWikilinkList,
|
|
13
|
+
isPhase1Artifact,
|
|
14
|
+
isSourcePlanningSpec,
|
|
15
|
+
readSpecMetaFull,
|
|
16
|
+
resolvePhase1Spec,
|
|
17
|
+
resolveSourcePlanningSpec,
|
|
18
|
+
} from './hs-auto-source.mjs';
|
|
19
|
+
|
|
20
|
+
const FRONTMATTER = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/;
|
|
21
|
+
const HS_ID = /^HS-[A-Z0-9]+-(?:TASK|EPIC|MIL|CAP|RES|REV|DIAG)(?:-|$)/i;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param {string} repoRoot
|
|
25
|
+
* @param {string} raw
|
|
26
|
+
*/
|
|
27
|
+
function toRepoRelative(repoRoot, raw) {
|
|
28
|
+
const abs = isAbsolute(raw) ? normalize(raw) : normalize(join(repoRoot, raw));
|
|
29
|
+
const rel = relative(repoRoot, abs).replace(/\\/g, '/');
|
|
30
|
+
if (rel.startsWith('..')) return raw.replace(/\\/g, '/');
|
|
31
|
+
return rel;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @param {string} fm
|
|
36
|
+
* @param {string} key
|
|
37
|
+
*/
|
|
38
|
+
function fmScalar(fm, key) {
|
|
39
|
+
const m = fm.match(new RegExp(`^${key}:\\s*(.+)$`, 'm'));
|
|
40
|
+
if (!m) return null;
|
|
41
|
+
return m[1].trim().replace(/^['"]|['"]$/g, '');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @param {string} filePath
|
|
46
|
+
*/
|
|
47
|
+
function readSpecMeta(filePath) {
|
|
48
|
+
const meta = readSpecMetaFull(filePath);
|
|
49
|
+
return {
|
|
50
|
+
id: meta.id,
|
|
51
|
+
type: meta.type,
|
|
52
|
+
status: meta.status,
|
|
53
|
+
target: meta.target,
|
|
54
|
+
spec_path: filePath,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @param {string} specsDir
|
|
60
|
+
* @param {string} targetNeedle repo-relative, forward slashes
|
|
61
|
+
*/
|
|
62
|
+
function findTaskByTargetSync(specsDir, targetNeedle) {
|
|
63
|
+
const tasksDir = join(specsDir, 'tasks');
|
|
64
|
+
if (!existsSync(tasksDir)) return null;
|
|
65
|
+
/** @type {string[]} */
|
|
66
|
+
const files = [];
|
|
67
|
+
const walk = (dir) => {
|
|
68
|
+
for (const entry of readdirSync(dir)) {
|
|
69
|
+
const full = join(dir, entry);
|
|
70
|
+
if (statSync(full).isDirectory()) walk(full);
|
|
71
|
+
else if (entry.endsWith('.md')) files.push(full);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
walk(tasksDir);
|
|
75
|
+
|
|
76
|
+
const norm = targetNeedle.replace(/\\/g, '/');
|
|
77
|
+
for (const file of files) {
|
|
78
|
+
const meta = readSpecMeta(file);
|
|
79
|
+
if (!meta.target) continue;
|
|
80
|
+
const t = meta.target.replace(/\\/g, '/');
|
|
81
|
+
if (t === norm || norm.endsWith(t) || t.endsWith(norm)) {
|
|
82
|
+
return meta;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* @param {{ id: string|null, type: string|null, target: string|null }} meta
|
|
90
|
+
* @param {string} rel
|
|
91
|
+
* @param {string} repoRoot
|
|
92
|
+
*/
|
|
93
|
+
function resolveExecutableSpec(meta, rel, repoRoot) {
|
|
94
|
+
if (isSourcePlanningSpec(meta.type, meta.id)) {
|
|
95
|
+
const full = readSpecMetaFull(join(repoRoot, rel));
|
|
96
|
+
return resolveSourcePlanningSpec(repoRoot, full, rel);
|
|
97
|
+
}
|
|
98
|
+
// HS-5Q41: a capture (raw or mid-stage) resolves to the Phase-1 runner with a start-at-step hint,
|
|
99
|
+
// instead of dead-ending at MISSING_TARGET. Placed before the epic/target branches — a capture is
|
|
100
|
+
// neither an epic nor a dispatchable task.
|
|
101
|
+
if (isPhase1Artifact(meta.type, meta.id)) {
|
|
102
|
+
const full = readSpecMetaFull(join(repoRoot, rel));
|
|
103
|
+
return resolvePhase1Spec(full, rel);
|
|
104
|
+
}
|
|
105
|
+
// Epic/parent backstop (2026-07-11 idle-fleet incident): an epic is detected by
|
|
106
|
+
// type:, by id — including ids ENDING in "-EPIC", which the old /-EPIC-/ regex
|
|
107
|
+
// missed — by living under specs/epics/, or by carrying a children: frontmatter
|
|
108
|
+
// key at all (parent-shaped). Any of these is STRUCTURALLY blocked from
|
|
109
|
+
// `mode: dispatch`: an epic with children: [] (or with children) can only ever
|
|
110
|
+
// classify as breakdown, even when it carries a target: of its own. The dispatch
|
|
111
|
+
// branch below is unreachable for it by construction, and controller.mjs's
|
|
112
|
+
// dispatch gate (lib/dispatch-gate.mjs) backstops any caller that bypasses this.
|
|
113
|
+
// Detection is the shared source of truth (dev-factory/lib/epic-detect.mjs); this
|
|
114
|
+
// classifier additionally routes ANY parent-shaped spec (a children: key present,
|
|
115
|
+
// even with children) to breakdown.
|
|
116
|
+
let facts = { id: meta.id ?? '', type: (meta.type ?? '').toLowerCase(), path: rel, hasChildrenKey: false, childCount: 0, target: meta.target };
|
|
117
|
+
let childCount = 0;
|
|
118
|
+
try {
|
|
119
|
+
const raw = readFileSync(join(repoRoot, rel), 'utf8');
|
|
120
|
+
facts = specFacts({ raw, specPath: rel, specId: meta.id });
|
|
121
|
+
const fm = raw.match(FRONTMATTER)?.[1] ?? '';
|
|
122
|
+
if (facts.hasChildrenKey) childCount = fmWikilinkList(fm, 'children').length;
|
|
123
|
+
} catch { /* unreadable file → fall through to the target checks below */ }
|
|
124
|
+
const isEpic = isEpicSpec(facts) || facts.hasChildrenKey;
|
|
125
|
+
if (isEpic) {
|
|
126
|
+
return {
|
|
127
|
+
mode: 'breakdown',
|
|
128
|
+
spec_id: meta.id,
|
|
129
|
+
spec_path: rel,
|
|
130
|
+
children_count: childCount,
|
|
131
|
+
dispatch_blocked:
|
|
132
|
+
'An epic (or a parent-shaped spec with a children: key) is never dispatchable as a single target — breakdown first, then dispatch the FILED child tasks across the fleet.',
|
|
133
|
+
breakdown_command: `node dev-factory/controller.mjs --breakdown-epic ${meta.id}`,
|
|
134
|
+
epic_pipeline_command: `node dev-factory/dispatch-epic.mjs --epic ${meta.id}`,
|
|
135
|
+
filing_helper: 'node dev-factory/lib/breakdown-to-filing.mjs --draft <draft.json> --task-id HS-....-TASK [--target path]',
|
|
136
|
+
coo_contract:
|
|
137
|
+
'COO orchestration only — run preflight, then breakdown_command (or epic_pipeline_command, which chains breakdown → owner approval → filed-children fan-out via backlog-runner --concurrent). Review draft JSON, run filing_helper per task row, file via heyspex_file_spec with ## Builder brief, re-resolve, dispatch. NEVER controller.mjs --spec <epic> --target — the dispatch gate refuses it. Do NOT implement in Cursor.',
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
if (!meta.target) {
|
|
141
|
+
return {
|
|
142
|
+
error: 'MISSING_TARGET',
|
|
143
|
+
message: `${meta.id ?? rel} has no target: frontmatter — add target before dispatch.`,
|
|
144
|
+
spec_id: meta.id ?? undefined,
|
|
145
|
+
spec_path: rel,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
mode: 'dispatch',
|
|
150
|
+
spec_id: meta.id,
|
|
151
|
+
spec_path: rel,
|
|
152
|
+
target: meta.target.replace(/\\/g, '/'),
|
|
153
|
+
dispatch_command: buildDispatchCommand(rel, meta.target),
|
|
154
|
+
coo_contract:
|
|
155
|
+
'COO orchestration only — run preflight, then dispatch_command. Do NOT implement in Cursor unless the gate fails after fleet retries.',
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* @param {string} repoRoot
|
|
161
|
+
* @param {string | undefined | null} filename
|
|
162
|
+
*/
|
|
163
|
+
export function resolveHsAutoEntry(repoRoot, filename) {
|
|
164
|
+
const specsDir = join(repoRoot, 'specs');
|
|
165
|
+
const trimmed = (filename ?? '').trim().replace(/^["']|['"]$/g, '');
|
|
166
|
+
if (!trimmed) {
|
|
167
|
+
return {
|
|
168
|
+
mode: 'discover',
|
|
169
|
+
message: 'No filename — run backlog discovery after pre-flight.',
|
|
170
|
+
coo_contract:
|
|
171
|
+
'COO orchestration only — run preflight, discover_command, pick a ready task with target:, re-resolve. Do NOT implement in Cursor.',
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const idToken = trimmed.match(HS_ID)?.[0] ?? (
|
|
176
|
+
basename(trimmed).match(HS_ID)?.[0] ?? null
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
if (idToken) {
|
|
180
|
+
const specPath = findSpecFile(specsDir, idToken);
|
|
181
|
+
if (!specPath) {
|
|
182
|
+
return { error: 'SPEC_NOT_FOUND', message: `No vault spec found for ${idToken}` };
|
|
183
|
+
}
|
|
184
|
+
const meta = readSpecMeta(specPath);
|
|
185
|
+
const rel = relative(repoRoot, specPath).replace(/\\/g, '/');
|
|
186
|
+
return resolveExecutableSpec(meta, rel, repoRoot);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const relPath = toRepoRelative(repoRoot, trimmed);
|
|
190
|
+
const absPath = join(repoRoot, relPath);
|
|
191
|
+
if (relPath.endsWith('.md') && existsSync(absPath)) {
|
|
192
|
+
const meta = readSpecMeta(absPath);
|
|
193
|
+
return resolveExecutableSpec(meta, relPath, repoRoot);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (existsSync(absPath)) {
|
|
197
|
+
const byTarget = findTaskByTargetSync(specsDir, relPath);
|
|
198
|
+
if (byTarget?.id && byTarget.target) {
|
|
199
|
+
const specRel = relative(repoRoot, byTarget.spec_path).replace(/\\/g, '/');
|
|
200
|
+
return {
|
|
201
|
+
mode: 'dispatch',
|
|
202
|
+
spec_id: byTarget.id,
|
|
203
|
+
spec_path: specRel,
|
|
204
|
+
target: byTarget.target.replace(/\\/g, '/'),
|
|
205
|
+
matched_via: 'target_frontmatter',
|
|
206
|
+
dispatch_command: buildDispatchCommand(specRel, byTarget.target),
|
|
207
|
+
coo_contract:
|
|
208
|
+
'COO orchestration only — run preflight, then dispatch_command. Do NOT implement in Cursor unless the gate fails after fleet retries.',
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
return {
|
|
212
|
+
error: 'NO_TASK_FOR_FILE',
|
|
213
|
+
message: `No vault task lists target: ${relPath}. File a task with target: or pass a spec path/id.`,
|
|
214
|
+
file: relPath,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
error: 'FILE_NOT_FOUND',
|
|
220
|
+
message: `Path not found: ${trimmed}`,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* @param {string} specPath repo-relative
|
|
226
|
+
* @param {string} target
|
|
227
|
+
*/
|
|
228
|
+
export function buildDispatchCommand(specPath, target) {
|
|
229
|
+
const specId = basename(specPath, '.md');
|
|
230
|
+
const checkpoint = `dev-factory/state/checkpoint-${specId}.json`;
|
|
231
|
+
return [
|
|
232
|
+
'node dev-factory/controller.mjs',
|
|
233
|
+
`--spec ${specPath}`,
|
|
234
|
+
`--target ${target.replace(/\\/g, '/')}`,
|
|
235
|
+
'--use-scheduler',
|
|
236
|
+
'--quality-pipeline',
|
|
237
|
+
'--n 3',
|
|
238
|
+
`--checkpoint ${checkpoint}`,
|
|
239
|
+
].join(' ');
|
|
240
|
+
}
|
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Source-spec pipeline helpers for `/hs-auto` — diagnose + research are planning
|
|
3
|
+
* inputs (like capture/research in Phase 1). They are NOT dispatchable until a
|
|
4
|
+
* remediation epic + tasks with `target:` exist.
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
7
|
+
import { join, relative, basename } from 'node:path';
|
|
8
|
+
|
|
9
|
+
import { findSpecFile } from '../dev-factory/lib/spec-io.mjs';
|
|
10
|
+
|
|
11
|
+
const FRONTMATTER = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/;
|
|
12
|
+
|
|
13
|
+
/** @typedef {{ id: string|null, type: string|null, status: string|null, target: string|null, parent: string|null, spec_path: string, body: string }} SpecMeta */
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {string} fm
|
|
17
|
+
* @param {string} key
|
|
18
|
+
*/
|
|
19
|
+
export function fmScalar(fm, key) {
|
|
20
|
+
const m = fm.match(new RegExp(`^${key}:\\s*(.+)$`, 'm'));
|
|
21
|
+
if (!m) return null;
|
|
22
|
+
return m[1].trim().replace(/^['"]|['"]$/g, '');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {string} filePath
|
|
27
|
+
* @returns {SpecMeta}
|
|
28
|
+
*/
|
|
29
|
+
export function readSpecMetaFull(filePath) {
|
|
30
|
+
const raw = readFileSync(filePath, 'utf8');
|
|
31
|
+
const m = raw.match(FRONTMATTER);
|
|
32
|
+
const fm = m?.[1] ?? '';
|
|
33
|
+
const body = m ? raw.slice(m[0].length) : raw;
|
|
34
|
+
return {
|
|
35
|
+
id: fmScalar(fm, 'id'),
|
|
36
|
+
type: fmScalar(fm, 'type'),
|
|
37
|
+
status: fmScalar(fm, 'status'),
|
|
38
|
+
target: fmScalar(fm, 'target'),
|
|
39
|
+
parent: fmScalar(fm, 'parent'),
|
|
40
|
+
spec_path: filePath,
|
|
41
|
+
body,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @param {string} fm
|
|
47
|
+
* @param {string} key
|
|
48
|
+
* @returns {string[]}
|
|
49
|
+
*/
|
|
50
|
+
export function fmWikilinkList(fm, key) {
|
|
51
|
+
const block = fm.match(new RegExp(`^${key}:\\s*\\n((?:\\s*-\\s+.+\\n?)*)`, 'm'));
|
|
52
|
+
if (!block) {
|
|
53
|
+
const scalar = fmScalar(fm, key);
|
|
54
|
+
if (!scalar) return [];
|
|
55
|
+
const ids = scalar.match(/HS-[A-Z0-9]+-(?:TASK|EPIC|MIL|DIAG|RES|CAP|REV)(?:-[A-Z0-9]+)?/gi);
|
|
56
|
+
return ids ?? [];
|
|
57
|
+
}
|
|
58
|
+
return block[1]
|
|
59
|
+
.split(/\r?\n/)
|
|
60
|
+
.map((line) => line.match(/^\s*-\s+(.+?)\s*$/)?.[1])
|
|
61
|
+
.filter(Boolean)
|
|
62
|
+
.flatMap((entry) => {
|
|
63
|
+
const ids = entry.match(/HS-[A-Z0-9]+-(?:TASK|EPIC|MIL|DIAG|RES|CAP|REV)(?:-[A-Z0-9]+)?/gi);
|
|
64
|
+
return ids ?? [];
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* @param {string | null | undefined} type
|
|
70
|
+
* @param {string | null | undefined} id
|
|
71
|
+
*/
|
|
72
|
+
export function isSourcePlanningSpec(type, id) {
|
|
73
|
+
const t = (type ?? '').toLowerCase();
|
|
74
|
+
if (t === 'diagnose' || t === 'research') return true;
|
|
75
|
+
const token = id ?? '';
|
|
76
|
+
return /-DIAG(?:-|$)/i.test(token) || /-RES(?:-|$)/i.test(token);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ── Phase-1 resolver (HS-5Q41): detect a capture's Phase-1 stage so runPhase1 can enter at the
|
|
80
|
+
// matching step instead of restarting from scratch. A capture (-CAP / type: capture) is a Phase-1
|
|
81
|
+
// pipeline artifact; DIAG/RES keep their own remediation flow (isSourcePlanningSpec) untouched. ──
|
|
82
|
+
|
|
83
|
+
/** The Phase-1 pipeline stages, ordered raw → plan-draft. */
|
|
84
|
+
export const PHASE1_STAGES = Object.freeze([
|
|
85
|
+
'raw',
|
|
86
|
+
'expanded',
|
|
87
|
+
'research-stub',
|
|
88
|
+
'research-doc',
|
|
89
|
+
'plan-draft',
|
|
90
|
+
]);
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Stage → runPhase1 entry step (name + index; mirrors dev-factory/pipeline/phase1.mjs STEP).
|
|
94
|
+
* A later stage skips the earlier steps; at run time the recorded checkpoint still wins over the
|
|
95
|
+
* hint so a re-pinned artifact never rewinds or restarts.
|
|
96
|
+
*/
|
|
97
|
+
export const PHASE1_STAGE_TO_STEP = Object.freeze({
|
|
98
|
+
raw: { step: 'expand', index: 0 },
|
|
99
|
+
expanded: { step: 'classify', index: 2 },
|
|
100
|
+
'research-stub': { step: 'research_l1', index: 3 },
|
|
101
|
+
// research-doc has its research done but no plan authored yet → enter at the plan-author step
|
|
102
|
+
// (index 6) so the plan is actually authored before filing (HS-KMBT-EPIC). plan-draft already
|
|
103
|
+
// carries a plan → enter at file (now index 7, after PLAN_AUTHOR was inserted at 6).
|
|
104
|
+
'research-doc': { step: 'plan_author', index: 6 },
|
|
105
|
+
'plan-draft': { step: 'file', index: 7 },
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* A spec is a Phase-1 artifact when it is a capture (type: capture or an -CAP id). DIAG/RES route
|
|
110
|
+
* through isSourcePlanningSpec instead, so this stays orthogonal to the remediation flow.
|
|
111
|
+
* @param {string | null | undefined} type
|
|
112
|
+
* @param {string | null | undefined} id
|
|
113
|
+
*/
|
|
114
|
+
export function isPhase1Artifact(type, id) {
|
|
115
|
+
if ((type ?? '').toLowerCase() === 'capture') return true;
|
|
116
|
+
return /-CAP(?:-|$)/i.test(id ?? '');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Deterministic Phase-1 stage detection for a capture (frontmatter + section presence, never
|
|
121
|
+
* semantic). Precedence: (1) explicit `phase1-stage`/`phase1_stage` frontmatter; (2) `capture_status`
|
|
122
|
+
* (promoted → plan-draft, expanded → at-least-expanded); (3) body section presence. Returns one of
|
|
123
|
+
* PHASE1_STAGES; unknown/empty inputs resolve to 'raw' (the safe full-pipeline entry).
|
|
124
|
+
* @param {SpecMeta} meta
|
|
125
|
+
* @returns {string}
|
|
126
|
+
*/
|
|
127
|
+
export function detectPhase1Stage(meta) {
|
|
128
|
+
const raw = readFileSync(meta.spec_path, 'utf8');
|
|
129
|
+
const fmMatch = raw.match(FRONTMATTER);
|
|
130
|
+
const fm = fmMatch?.[1] ?? '';
|
|
131
|
+
const body = meta.body ?? (fmMatch ? raw.slice(fmMatch[0].length) : raw);
|
|
132
|
+
|
|
133
|
+
// 1) An explicit stage field is authoritative — the caller/pipeline stamped it deliberately.
|
|
134
|
+
const explicit = (fmScalar(fm, 'phase1-stage') ?? fmScalar(fm, 'phase1_stage'))?.toLowerCase();
|
|
135
|
+
if (explicit && PHASE1_STAGES.includes(explicit)) return explicit;
|
|
136
|
+
|
|
137
|
+
// 2) Genuine research artifacts override the lifecycle field UPWARD. These headings are never in
|
|
138
|
+
// the raw capture template (which seeds only Core Idea / Notes / Source Material / Summary — a bare
|
|
139
|
+
// `# Core Idea` heading is NOT proof of expansion), so their presence means real added content:
|
|
140
|
+
// - a completed research doc carries BOTH `## Sources` and `## Executive Summary` → research-doc
|
|
141
|
+
// - a research stub carries `## Sources` or a `## Research` / `## Findings` heading → research-stub
|
|
142
|
+
const hasSources = /^##\s+Sources\b/im.test(body);
|
|
143
|
+
const hasExecSummary = /^##\s+Executive Summary\b/im.test(body);
|
|
144
|
+
const hasResearchHeading = /^##\s+(Research|Findings)\b/im.test(body);
|
|
145
|
+
if (hasSources && hasExecSummary) return 'research-doc';
|
|
146
|
+
if (hasSources || hasResearchHeading) return 'research-stub';
|
|
147
|
+
|
|
148
|
+
// 3) `capture_status` is the maintained lifecycle field — authoritative for raw/expanded/promoted.
|
|
149
|
+
// (Heading presence is deliberately NOT used to infer expanded: the capture template seeds a
|
|
150
|
+
// `# Core Idea` heading even for raw captures, so inferring expansion from it misreads raw as
|
|
151
|
+
// expanded — a real defect found driving live captures through the resolver, 2026-07-09.)
|
|
152
|
+
const capStatus = (fmScalar(fm, 'capture_status') ?? '').toLowerCase();
|
|
153
|
+
if (capStatus === 'promoted') return 'plan-draft';
|
|
154
|
+
if (capStatus === 'expanded') return 'expanded';
|
|
155
|
+
|
|
156
|
+
// 4) Default: raw (the safe full-pipeline entry) — including when capture_status is absent/unknown.
|
|
157
|
+
return 'raw';
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Resolve a capture to a `mode: phase1` dispatch plan with the detected stage + start-at-step hint,
|
|
162
|
+
* so `/hs-auto` on a capture (or a mid-stage capture) no longer dead-ends at MISSING_TARGET.
|
|
163
|
+
* @param {SpecMeta} meta full meta (spec_path + body populated)
|
|
164
|
+
* @param {string} relPath repo-relative capture path
|
|
165
|
+
*/
|
|
166
|
+
export function resolvePhase1Spec(meta, relPath) {
|
|
167
|
+
const stage = detectPhase1Stage(meta);
|
|
168
|
+
const entry = PHASE1_STAGE_TO_STEP[stage] ?? PHASE1_STAGE_TO_STEP.raw;
|
|
169
|
+
return {
|
|
170
|
+
mode: 'phase1',
|
|
171
|
+
spec_id: meta.id,
|
|
172
|
+
spec_path: relPath,
|
|
173
|
+
stage,
|
|
174
|
+
entry_step: entry.step,
|
|
175
|
+
entry_step_index: entry.index,
|
|
176
|
+
phase1_command: `node dev-factory/pipeline/phase1.mjs --capture ${relPath} --stage ${stage}`,
|
|
177
|
+
coo_contract: [
|
|
178
|
+
'COO orchestration only — Phase-1 resolver (start from any state).',
|
|
179
|
+
`Run runPhase1 entering at the "${entry.step}" step (stage: ${stage}).`,
|
|
180
|
+
'Invariant: re-pinning a mid-stage artifact resumes from its recorded checkpoint — never restarts.',
|
|
181
|
+
'Per-front reach: Handheld advances one checkpoint; Assisted carries to end-of-Phase-1 (milestone + epics filed) then a review gate; Autonomous carries into Phase 2.',
|
|
182
|
+
'Do NOT implement in Cursor.',
|
|
183
|
+
].join('\n'),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* @param {SpecMeta} meta
|
|
189
|
+
* @param {string} sourceId
|
|
190
|
+
*/
|
|
191
|
+
export function referencesSourceId(meta, sourceId) {
|
|
192
|
+
const raw = readFileSync(meta.spec_path, 'utf8');
|
|
193
|
+
const m = raw.match(FRONTMATTER);
|
|
194
|
+
const fm = m?.[1] ?? '';
|
|
195
|
+
const haystack = [
|
|
196
|
+
fmScalar(fm, 'source'),
|
|
197
|
+
fmScalar(fm, 'remediation_of'),
|
|
198
|
+
fmScalar(fm, 'diagnose'),
|
|
199
|
+
fmScalar(fm, 'research'),
|
|
200
|
+
fmScalar(fm, 'parent'),
|
|
201
|
+
meta.body,
|
|
202
|
+
]
|
|
203
|
+
.filter(Boolean)
|
|
204
|
+
.join('\n');
|
|
205
|
+
return haystack.includes(sourceId);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* @param {string} specsDir
|
|
210
|
+
*/
|
|
211
|
+
function walkMarkdown(specsDir) {
|
|
212
|
+
/** @type {string[]} */
|
|
213
|
+
const files = [];
|
|
214
|
+
const walk = (dir) => {
|
|
215
|
+
if (!existsSync(dir)) return;
|
|
216
|
+
for (const entry of readdirSync(dir)) {
|
|
217
|
+
const full = join(dir, entry);
|
|
218
|
+
if (statSync(full).isDirectory()) walk(full);
|
|
219
|
+
else if (entry.endsWith('.md')) files.push(full);
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
walk(specsDir);
|
|
223
|
+
return files;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* @param {string} repoRoot
|
|
228
|
+
* @param {string} sourceId
|
|
229
|
+
* @returns {SpecMeta | null}
|
|
230
|
+
*/
|
|
231
|
+
export function findRemediationEpic(repoRoot, sourceId) {
|
|
232
|
+
const epicsDir = join(repoRoot, 'specs', 'epics');
|
|
233
|
+
for (const file of walkMarkdown(epicsDir)) {
|
|
234
|
+
const meta = readSpecMetaFull(file);
|
|
235
|
+
if (referencesSourceId(meta, sourceId)) return meta;
|
|
236
|
+
}
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* @param {string} repoRoot
|
|
242
|
+
* @param {string} sourceId
|
|
243
|
+
* @param {string | null} epicId
|
|
244
|
+
* @returns {SpecMeta[]}
|
|
245
|
+
*/
|
|
246
|
+
export function findLinkedTasks(repoRoot, sourceId, epicId) {
|
|
247
|
+
const tasksDir = join(repoRoot, 'specs', 'tasks');
|
|
248
|
+
/** @type {SpecMeta[]} */
|
|
249
|
+
const out = [];
|
|
250
|
+
for (const file of walkMarkdown(tasksDir)) {
|
|
251
|
+
const meta = readSpecMetaFull(file);
|
|
252
|
+
const linked =
|
|
253
|
+
referencesSourceId(meta, sourceId)
|
|
254
|
+
|| (epicId && (meta.parent?.includes(epicId) ?? false));
|
|
255
|
+
if (linked) out.push(meta);
|
|
256
|
+
}
|
|
257
|
+
return out;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const DISPATCHABLE_STATUSES = new Set(['open', 'in_progress']);
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* @param {SpecMeta[]} tasks
|
|
264
|
+
* @returns {SpecMeta | null}
|
|
265
|
+
*/
|
|
266
|
+
export function pickDispatchableTask(tasks) {
|
|
267
|
+
const ready = tasks.filter(
|
|
268
|
+
(t) => t.target && t.status && DISPATCHABLE_STATUSES.has(t.status),
|
|
269
|
+
);
|
|
270
|
+
ready.sort((a, b) => a.spec_path.localeCompare(b.spec_path));
|
|
271
|
+
return ready[0] ?? null;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* @param {string} repoRoot
|
|
276
|
+
* @param {string} epicId
|
|
277
|
+
* @returns {number}
|
|
278
|
+
*/
|
|
279
|
+
export function countEpicChildren(repoRoot, epicId) {
|
|
280
|
+
const epicPath = findSpecFile(join(repoRoot, 'specs'), epicId);
|
|
281
|
+
if (!epicPath) return 0;
|
|
282
|
+
const raw = readFileSync(epicPath, 'utf8');
|
|
283
|
+
const fm = raw.match(FRONTMATTER)?.[1] ?? '';
|
|
284
|
+
return fmWikilinkList(fm, 'children').length;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* @param {string} specPath repo-relative
|
|
289
|
+
* @param {string} target
|
|
290
|
+
*/
|
|
291
|
+
function buildDispatchCommand(specPath, target) {
|
|
292
|
+
const specId = basename(specPath, '.md');
|
|
293
|
+
const checkpoint = `dev-factory/state/checkpoint-${specId}.json`;
|
|
294
|
+
return [
|
|
295
|
+
'node dev-factory/controller.mjs',
|
|
296
|
+
`--spec ${specPath}`,
|
|
297
|
+
`--target ${target.replace(/\\/g, '/')}`,
|
|
298
|
+
'--use-scheduler',
|
|
299
|
+
'--n 3',
|
|
300
|
+
`--checkpoint ${checkpoint}`,
|
|
301
|
+
].join(' ');
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* @param {string} repoRoot
|
|
306
|
+
* @param {{ id: string|null, type: string|null, spec_path: string }} sourceMeta
|
|
307
|
+
* @param {string} relPath repo-relative source path
|
|
308
|
+
*/
|
|
309
|
+
export function resolveSourcePlanningSpec(repoRoot, sourceMeta, relPath) {
|
|
310
|
+
const sourceId = sourceMeta.id;
|
|
311
|
+
if (!sourceId) {
|
|
312
|
+
return {
|
|
313
|
+
error: 'SOURCE_ID_MISSING',
|
|
314
|
+
message: `${relPath} has no id: frontmatter.`,
|
|
315
|
+
spec_path: relPath,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const sourceKind = (sourceMeta.type ?? '').toLowerCase() === 'research' ? 'research' : 'diagnose';
|
|
320
|
+
const epic = findRemediationEpic(repoRoot, sourceId);
|
|
321
|
+
const tasks = findLinkedTasks(repoRoot, sourceId, epic?.id ?? null);
|
|
322
|
+
const dispatchTask = pickDispatchableTask(tasks);
|
|
323
|
+
|
|
324
|
+
if (dispatchTask?.id && dispatchTask.target) {
|
|
325
|
+
const taskRel = relative(repoRoot, dispatchTask.spec_path).replace(/\\/g, '/');
|
|
326
|
+
return {
|
|
327
|
+
mode: 'dispatch',
|
|
328
|
+
spec_id: dispatchTask.id,
|
|
329
|
+
spec_path: taskRel,
|
|
330
|
+
target: dispatchTask.target.replace(/\\/g, '/'),
|
|
331
|
+
source_spec_id: sourceId,
|
|
332
|
+
source_spec_path: relPath,
|
|
333
|
+
source_kind: sourceKind,
|
|
334
|
+
matched_via: 'source_remediation_task',
|
|
335
|
+
dispatch_command: buildDispatchCommand(taskRel, dispatchTask.target),
|
|
336
|
+
coo_contract:
|
|
337
|
+
'COO orchestration only — run preflight, then dispatch_command. Do NOT implement in Cursor unless the gate fails after fleet retries.',
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (epic?.id) {
|
|
342
|
+
const childCount = countEpicChildren(repoRoot, epic.id);
|
|
343
|
+
const epicRel = relative(repoRoot, epic.spec_path).replace(/\\/g, '/');
|
|
344
|
+
if (childCount === 0) {
|
|
345
|
+
return {
|
|
346
|
+
mode: 'breakdown',
|
|
347
|
+
spec_id: epic.id,
|
|
348
|
+
spec_path: epicRel,
|
|
349
|
+
source_spec_id: sourceId,
|
|
350
|
+
source_spec_path: relPath,
|
|
351
|
+
source_kind: sourceKind,
|
|
352
|
+
remediation_epic_id: epic.id,
|
|
353
|
+
breakdown_kind: 'decompose',
|
|
354
|
+
breakdown_command: `node dev-factory/engine/breakdown.mjs --decompose --epic ${epic.id}`,
|
|
355
|
+
coo_contract:
|
|
356
|
+
'COO orchestration only — run preflight, then breakdown_command. Review the JSON draft, file tasks via heyspex_file_spec (each with target:), register linkage, re-run hs auto on the source spec, then dispatch. Do NOT implement in Cursor.',
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
return {
|
|
360
|
+
mode: 'breakdown',
|
|
361
|
+
spec_id: epic.id,
|
|
362
|
+
spec_path: epicRel,
|
|
363
|
+
source_spec_id: sourceId,
|
|
364
|
+
source_spec_path: relPath,
|
|
365
|
+
source_kind: sourceKind,
|
|
366
|
+
remediation_epic_id: epic.id,
|
|
367
|
+
breakdown_kind: 'incremental',
|
|
368
|
+
breakdown_command: `node dev-factory/controller.mjs --breakdown-epic ${epic.id}`,
|
|
369
|
+
linked_tasks_without_target: tasks.filter((t) => !t.target).map((t) => t.id),
|
|
370
|
+
coo_contract:
|
|
371
|
+
'COO orchestration only — run preflight, then breakdown_command. Review draft, file any missing tasks with target:, re-run hs auto on the source spec, then dispatch. Do NOT implement in Cursor.',
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return {
|
|
376
|
+
mode: 'source_plan',
|
|
377
|
+
source_kind: sourceKind,
|
|
378
|
+
spec_id: sourceId,
|
|
379
|
+
spec_path: relPath,
|
|
380
|
+
linked_tasks: tasks.map((t) => ({ id: t.id, status: t.status, target: t.target })),
|
|
381
|
+
file_epic_intent: 'file_epic',
|
|
382
|
+
file_task_intent: 'file_task',
|
|
383
|
+
decompose_command: 'node dev-factory/engine/breakdown.mjs --decompose --epic <REMEDIATION_EPIC_ID>',
|
|
384
|
+
re_resolve_command: `node bin/hs.mjs auto ${sourceId} --json`,
|
|
385
|
+
coo_contract: [
|
|
386
|
+
'COO orchestration only — do NOT implement in Cursor.',
|
|
387
|
+
'1) Run preflight from JSON.',
|
|
388
|
+
'2) heyspex_preflight({ intent: "file_epic" }) → heyspex_file_spec a remediation epic (no milestone required). Body: synthesize Decision, Fix slices, Deferred, and Acceptance from the source spec. Frontmatter source: wikilink to this diagnose/research.',
|
|
389
|
+
'3) heyspex_register_spec(epic_id, parent_id) — parent_id null/omit when no milestone.',
|
|
390
|
+
'4) node dev-factory/engine/breakdown.mjs --decompose --epic <EPIC_ID> — fleet breakdown seat drafts tasks.',
|
|
391
|
+
'5) COO reviews draft → heyspex_file_spec each task with target: + acceptance criteria → heyspex_register_spec per task.',
|
|
392
|
+
'6) Re-run node bin/hs.mjs auto on the source spec → expect mode dispatch.',
|
|
393
|
+
'7) Run dispatch_command; review gate verdict only.',
|
|
394
|
+
].join('\n'),
|
|
395
|
+
message:
|
|
396
|
+
`No remediation epic linked to ${sourceId}. File epic + tasks from the source spec, then re-resolve for fleet dispatch.`,
|
|
397
|
+
};
|
|
398
|
+
}
|