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,453 @@
|
|
|
1
|
+
// Spec editing tools: heyspex_regen_task, regen_tasks_for_epic, backfill_epic,
|
|
2
|
+
// edit_file, propose_metadata, list_known_tags, canvas_inbox. Extracted from cli.mjs under
|
|
3
|
+
// [[HS-BLNF-6W61-DIAG-cli-mjs-has-grown-into-a-13-665-line]] (no behavior change).
|
|
4
|
+
|
|
5
|
+
import { executeCanvasInbox } from './canvas-inbox.mjs';
|
|
6
|
+
import { consoleFetch, controlError, controlOk } from './console-transport.mjs';
|
|
7
|
+
import { detectTagDrift } from './plan-ground.mjs';
|
|
8
|
+
import { consoleErrorMessage } from './console-fetch.mjs';
|
|
9
|
+
import { detectAgentIdentity, getConsoleUrl } from './mcp-config.mjs';
|
|
10
|
+
import { resolveVaultWriteMode } from './read-path.mjs';
|
|
11
|
+
import { deferredPayloadFromError, splitRawSpec, updateSpecViaConsole } from './cloud-vault-writes.mjs';
|
|
12
|
+
import { writeFileSync } from 'node:fs';
|
|
13
|
+
import { executeShipEpicCloudAware } from './plan-push.mjs';
|
|
14
|
+
import { executeRegenTask, executeRegenTasksForEpic } from './regen-tasks.mjs';
|
|
15
|
+
import { loadTask } from './vault-io.mjs';
|
|
16
|
+
import { InvalidListCursorError, fitPageToByteBudget, resolvePageLimit } from './list-tools.mjs';
|
|
17
|
+
import { DEFAULT_TOOL_BUDGET_BYTES } from './response-budget.mjs';
|
|
18
|
+
|
|
19
|
+
export function handleRegenTask(vaultRoot, args = {}) {
|
|
20
|
+
const result = executeRegenTask(vaultRoot, {
|
|
21
|
+
task_id: args.task_id,
|
|
22
|
+
commit: args.commit === true || args.commit === 'true',
|
|
23
|
+
new_body: args.new_body,
|
|
24
|
+
retire_reason: args.retire_reason,
|
|
25
|
+
loadTask: (ref) => loadTask(vaultRoot, ref),
|
|
26
|
+
});
|
|
27
|
+
return {
|
|
28
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
29
|
+
isError: result.ok === false,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function handleRegenTasksForEpic(vaultRoot, args = {}) {
|
|
34
|
+
const result = executeRegenTasksForEpic(vaultRoot, {
|
|
35
|
+
epic_id: args.epic_id,
|
|
36
|
+
commit: args.commit === true || args.commit === 'true',
|
|
37
|
+
task_specs: args.task_specs,
|
|
38
|
+
loadTask: (ref) => loadTask(vaultRoot, ref),
|
|
39
|
+
});
|
|
40
|
+
return {
|
|
41
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
42
|
+
isError: result.ok === false,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* [[HS-Y00G-EPIC]] — Safe file edit: claim → write → release.
|
|
48
|
+
* Acquires the file claim, writes content to disk, and releases the claim.
|
|
49
|
+
* If claim fails (already claimed by another agent), returns the conflict info.
|
|
50
|
+
*
|
|
51
|
+
* WHERE THE WRITE GOES ([[HS-BYU4-CWPX-TASK-promotion-writes-go-through-the-console]]).
|
|
52
|
+
* The write mode is resolved ONCE here, at the handler entry, and threaded to
|
|
53
|
+
* step 2 below:
|
|
54
|
+
*
|
|
55
|
+
* - `local` — byte-identical to the original behavior: `writeFileSync` to
|
|
56
|
+
* `args.file_path`, and the success payload carries exactly the fields it
|
|
57
|
+
* always did.
|
|
58
|
+
* - `cloud` — the mutated markdown is split back into the
|
|
59
|
+
* `{ frontmatter, body }` pair `PUT /api/specs/:id` takes and sent through
|
|
60
|
+
* {@link updateSpecViaConsole}. Nothing touches this machine's disk: a
|
|
61
|
+
* markdown file written in cloud reliance is a split brain, a document on a
|
|
62
|
+
* machine the console's index has never heard of (and that may hold no
|
|
63
|
+
* vault clone at all).
|
|
64
|
+
*
|
|
65
|
+
* The spec id comes from the POST-EDIT content's own `id:` frontmatter — the
|
|
66
|
+
* caller has already produced the finished document, so its frontmatter is the
|
|
67
|
+
* authority on which spec this edit replaces. Content with no resolvable `id:`
|
|
68
|
+
* (a non-spec file, or a spec fragment) has NO cloud route: there is no console
|
|
69
|
+
* verb for "write these bytes to this arbitrary path". That degrades explicitly
|
|
70
|
+
* as `CLOUD_MODE_UNSUPPORTED_WRITE` naming the file — never a silent fallback
|
|
71
|
+
* to the disk write the mode just forbade.
|
|
72
|
+
*
|
|
73
|
+
* @param {object} args
|
|
74
|
+
* @param {object} [deps] Injection seam so tests drive every branch without a
|
|
75
|
+
* socket: `consoleFetch` (defaults to the real transport), `consoleUrl`
|
|
76
|
+
* (defaults to `getConsoleUrl()`), `resolveWriteMode` (defaults to
|
|
77
|
+
* `resolveVaultWriteMode`).
|
|
78
|
+
*/
|
|
79
|
+
export async function handleEditFile(args = {}, deps = {}) {
|
|
80
|
+
if (!args.file_path || typeof args.file_path !== 'string') {
|
|
81
|
+
return {
|
|
82
|
+
isError: true,
|
|
83
|
+
content: [{ type: 'text', text: JSON.stringify({ error: 'INVALID_ARGS', message: 'file_path is required' }) }],
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
if (typeof args.content !== 'string') {
|
|
87
|
+
return {
|
|
88
|
+
isError: true,
|
|
89
|
+
content: [{ type: 'text', text: JSON.stringify({ error: 'INVALID_ARGS', message: 'content is required and must be a string' }) }],
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const identity = detectAgentIdentity(args.agent, args.session_id);
|
|
94
|
+
const fetchDep = deps.consoleFetch ?? consoleFetch;
|
|
95
|
+
// Resolved ONCE per handler entry, then threaded to step 2. `?? 'local'`
|
|
96
|
+
// keeps every pre-existing caller on exactly the disk write it had.
|
|
97
|
+
const writeMode = (await (deps.resolveWriteMode ?? resolveVaultWriteMode)()) ?? 'local';
|
|
98
|
+
|
|
99
|
+
// Step 1: Acquire claim
|
|
100
|
+
const claimBody = {
|
|
101
|
+
file_path: args.file_path,
|
|
102
|
+
agent: identity.agent,
|
|
103
|
+
session_id: identity.sessionId,
|
|
104
|
+
ttl_ms: 300000, // 5-minute default for file claims
|
|
105
|
+
};
|
|
106
|
+
if (args.working_spec_id && typeof args.working_spec_id === 'string') {
|
|
107
|
+
claimBody.working_spec_id = args.working_spec_id;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const claimResult = await fetchDep('/api/file-claims/acquire', { method: 'POST', body: claimBody });
|
|
111
|
+
|
|
112
|
+
if (claimResult.status === 409) {
|
|
113
|
+
const conflict = claimResult.body && typeof claimResult.body === 'object' ? claimResult.body : {};
|
|
114
|
+
return {
|
|
115
|
+
isError: true,
|
|
116
|
+
content: [{ type: 'text', text: JSON.stringify({
|
|
117
|
+
error: 'FILE_CLAIMED',
|
|
118
|
+
message: `CONFLICT: file "${args.file_path}" is claimed by another agent.`,
|
|
119
|
+
claimed_by: conflict.existing?.agent ?? 'unknown',
|
|
120
|
+
claimed_session: conflict.existing?.sessionId ?? null,
|
|
121
|
+
expires_at: conflict.existing?.expiresAt ?? null,
|
|
122
|
+
conflict: conflict,
|
|
123
|
+
}, null, 2) }],
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (!claimResult.ok) {
|
|
128
|
+
return controlError(claimResult, 'EDIT_FILE_CLAIM_FAILED', { file_path: args.file_path });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Always release before returning a failure, so a rejected write never leaves
|
|
132
|
+
// the file locked for the claim's full TTL.
|
|
133
|
+
const releaseClaim = () =>
|
|
134
|
+
fetchDep('/api/file-claims/release', { method: 'DELETE', body: { file_path: args.file_path } });
|
|
135
|
+
|
|
136
|
+
// Step 2: Write — to disk in local mode, to `PUT /api/specs/:id` in cloud mode.
|
|
137
|
+
let cloudWrite = null;
|
|
138
|
+
if (writeMode === 'cloud') {
|
|
139
|
+
const consoleUrl = deps.consoleUrl ?? getConsoleUrl();
|
|
140
|
+
const { frontmatter, body } = splitRawSpec(args.content);
|
|
141
|
+
const specId = typeof frontmatter?.id === 'string' ? frontmatter.id.trim() : '';
|
|
142
|
+
if (!specId) {
|
|
143
|
+
await releaseClaim();
|
|
144
|
+
return {
|
|
145
|
+
isError: true,
|
|
146
|
+
content: [{ type: 'text', text: JSON.stringify({
|
|
147
|
+
error: 'CLOUD_MODE_UNSUPPORTED_WRITE',
|
|
148
|
+
message:
|
|
149
|
+
`heyspex_edit_file cannot write "${args.file_path}" in cloud vault-write mode: the content carries no `
|
|
150
|
+
+ '`id:` frontmatter, and the console has no route that writes arbitrary bytes to an arbitrary path '
|
|
151
|
+
+ '(POST /api/specs creates a typed spec, PUT /api/specs/:id replaces one by id). NOTHING WAS WRITTEN — '
|
|
152
|
+
+ 'the local disk write is deliberately NOT used as a fallback, because a file written here would be '
|
|
153
|
+
+ 'invisible to the console index this session reads from.',
|
|
154
|
+
file_path: args.file_path,
|
|
155
|
+
write_mode: 'cloud',
|
|
156
|
+
console_url: consoleUrl,
|
|
157
|
+
on_disk: false,
|
|
158
|
+
}, null, 2) }],
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
try {
|
|
162
|
+
cloudWrite = await updateSpecViaConsole({
|
|
163
|
+
consoleFetch: fetchDep,
|
|
164
|
+
consoleUrl,
|
|
165
|
+
specId,
|
|
166
|
+
frontmatter,
|
|
167
|
+
body,
|
|
168
|
+
});
|
|
169
|
+
} catch (err) {
|
|
170
|
+
await releaseClaim();
|
|
171
|
+
// Outage: the edit was queued to the write outbox (truthful receipt, not an error).
|
|
172
|
+
const deferred = deferredPayloadFromError(err);
|
|
173
|
+
if (deferred) {
|
|
174
|
+
return {
|
|
175
|
+
content: [{ type: 'text', text: JSON.stringify({ ...deferred, file_path: args.file_path, spec_id: specId, write_mode: 'cloud', on_disk: false }, null, 2) }],
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
return {
|
|
179
|
+
isError: true,
|
|
180
|
+
content: [{ type: 'text', text: JSON.stringify({
|
|
181
|
+
error: 'CONSOLE_UNREACHABLE_CLOUD_MODE',
|
|
182
|
+
message: String(err?.message ?? err),
|
|
183
|
+
file_path: args.file_path,
|
|
184
|
+
spec_id: specId,
|
|
185
|
+
write_mode: 'cloud',
|
|
186
|
+
on_disk: false,
|
|
187
|
+
...(err?.notFound ? { not_found: true } : {}),
|
|
188
|
+
}, null, 2) }],
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
} else {
|
|
192
|
+
try {
|
|
193
|
+
writeFileSync(args.file_path, args.content, 'utf8');
|
|
194
|
+
} catch (err) {
|
|
195
|
+
// Release the claim on write failure so the file isn't locked forever
|
|
196
|
+
await releaseClaim();
|
|
197
|
+
return {
|
|
198
|
+
isError: true,
|
|
199
|
+
content: [{ type: 'text', text: JSON.stringify({
|
|
200
|
+
error: 'WRITE_FAILED',
|
|
201
|
+
message: `Failed to write file: ${err?.message ?? err}`,
|
|
202
|
+
file_path: args.file_path,
|
|
203
|
+
}) }],
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Step 3: Release claim
|
|
209
|
+
const releaseResult = await releaseClaim();
|
|
210
|
+
|
|
211
|
+
return controlOk({
|
|
212
|
+
written: true,
|
|
213
|
+
file_path: args.file_path,
|
|
214
|
+
released: releaseResult.ok ? releaseResult.body?.released ?? true : false,
|
|
215
|
+
agent: identity.agent,
|
|
216
|
+
session_id: identity.sessionId,
|
|
217
|
+
// Cloud-only additions — the local payload stays byte-identical to the
|
|
218
|
+
// shape every existing caller already parses.
|
|
219
|
+
...(cloudWrite
|
|
220
|
+
? {
|
|
221
|
+
write_mode: 'cloud',
|
|
222
|
+
on_disk: false,
|
|
223
|
+
spec_id: cloudWrite.specId,
|
|
224
|
+
rel_path: cloudWrite.relPath,
|
|
225
|
+
console_url: cloudWrite.consoleUrl,
|
|
226
|
+
}
|
|
227
|
+
: {}),
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Known-tags paging contract, mirrored from the console route
|
|
233
|
+
* (`spex-console/src/lib/scheduler/known-tags-query.ts`): the cursor is
|
|
234
|
+
* base64url JSON `{ c: count, t: tag }` over the `(count DESC, tag ASC)` sort
|
|
235
|
+
* key of the aggregate query. Encoded here only to RE-POINT the cursor when the
|
|
236
|
+
* byte budget trims a page — the console mints every cursor it hands out.
|
|
237
|
+
*/
|
|
238
|
+
export const KNOWN_TAGS_TOOL = Object.freeze({
|
|
239
|
+
default_limit: 100,
|
|
240
|
+
max_limit: 500,
|
|
241
|
+
// Measured on `content[0].text`; the dispatcher re-measures the whole envelope (escaped), so leave headroom.
|
|
242
|
+
byte_budget: Math.floor(DEFAULT_TOOL_BUDGET_BYTES * 0.8),
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
export function encodeKnownTagsCursor({ count, tag }) {
|
|
246
|
+
return Buffer.from(JSON.stringify({ c: Number(count) || 0, t: String(tag ?? '') }), 'utf8').toString('base64url');
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* `heyspex_list_known_tags` — the closed tag vocabulary for the focus-areas NLU
|
|
251
|
+
* path ([[HS-TASK-I0EK]] / [[HS-EPIC-7WJN]]). GET /api/scheduler/known-tags →
|
|
252
|
+
* ONE page `{ tags: [{ name, count }], next_cursor, page }` from an aggregate
|
|
253
|
+
* query paged with a keyset cursor over (count, tag)
|
|
254
|
+
* ([[HS-4H8T-8KK5-TASK-every-mcp-read-tool-declares-a-byte]], resolving
|
|
255
|
+
* [[HS-7QM6-1Q1C-DIAG-heyspex-list-known-tags-response-busts-t]] — the reply
|
|
256
|
+
* that once hit 138 KB). `limit` and `after` pass straight through to the
|
|
257
|
+
* console; the page is then fitted to the tool's byte budget, re-pointing
|
|
258
|
+
* `next_cursor` at the last tag kept so nothing is lost. Agents call this
|
|
259
|
+
* before proposing a focus pivot so they only suggest tags that exist.
|
|
260
|
+
* Requires a reachable console.
|
|
261
|
+
*
|
|
262
|
+
* Augments the console response with an additive `drift` array ([[HS-TASK-H0SI]]
|
|
263
|
+
* / [[HS-EPIC-OOPW]]): near-duplicate tag pairs within the page flagged by
|
|
264
|
+
* lexical string similarity (NOT embeddings) so the agent can propose a merge to
|
|
265
|
+
* keep the closed vocabulary clean. No tag is mutated here — `drift` is a
|
|
266
|
+
* suggestion signal only.
|
|
267
|
+
*/
|
|
268
|
+
export async function handleListKnownTags(args = {}, { consoleFetch: fetchDep = consoleFetch, byteBudget = KNOWN_TAGS_TOOL.byte_budget } = {}) {
|
|
269
|
+
const limit = resolvePageLimit(args.limit, KNOWN_TAGS_TOOL);
|
|
270
|
+
const params = new URLSearchParams({ limit: String(limit) });
|
|
271
|
+
if (typeof args.after === 'string' && args.after) params.set('after', args.after);
|
|
272
|
+
const result = await fetchDep(`/api/scheduler/known-tags?${params.toString()}`);
|
|
273
|
+
if (!result.ok) {
|
|
274
|
+
if (result.status === 400 && result.body?.error === 'invalid_cursor') {
|
|
275
|
+
return {
|
|
276
|
+
isError: true,
|
|
277
|
+
content: [{ type: 'text', text: JSON.stringify({ error: 'INVALID_CURSOR', message: new InvalidListCursorError().message, tool: 'heyspex_list_known_tags' }, null, 2) }],
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
return controlError(result, 'KNOWN_TAGS_FAILED');
|
|
281
|
+
}
|
|
282
|
+
const body = result.body && typeof result.body === 'object' ? result.body : {};
|
|
283
|
+
const tags = Array.isArray(body.tags) ? body.tags : [];
|
|
284
|
+
const base = {
|
|
285
|
+
...body,
|
|
286
|
+
tags,
|
|
287
|
+
next_cursor: typeof body.next_cursor === 'string' && body.next_cursor ? body.next_cursor : null,
|
|
288
|
+
page: { limit, returned: tags.length, byte_budget: byteBudget, budget_trimmed: false, ...(body.page ?? {}) },
|
|
289
|
+
};
|
|
290
|
+
const fitOpts = {
|
|
291
|
+
rows_key: 'tags',
|
|
292
|
+
// A page of tags that would fill the whole budget leaves no room for the
|
|
293
|
+
// drift signal the tool exists to surface: reserve a tenth for the
|
|
294
|
+
// strongest pairs. A page that needs no trim gets the whole budget.
|
|
295
|
+
byte_budget: byteBudget - Math.floor(byteBudget * 0.1),
|
|
296
|
+
cursorFor: (row) => encodeKnownTagsCursor({ count: row.count, tag: row.name }),
|
|
297
|
+
};
|
|
298
|
+
// 1. Fit the TAGS to the budget first (with an empty drift envelope in
|
|
299
|
+
// place so its bytes are accounted for) — the cursor must land on a tag
|
|
300
|
+
// the caller received, and drift over an un-returned tag is noise.
|
|
301
|
+
let current = fitPageToByteBudget({ ...base, drift: [] }, fitOpts).result;
|
|
302
|
+
// 2. Drift over the returned page only. It is pairwise (quadratic in page
|
|
303
|
+
// size) and sorted by similarity desc, so when it would bust the budget
|
|
304
|
+
// keep the strongest pairs that fit. Only when not even zero pairs fit
|
|
305
|
+
// (the truncation markers cost a few bytes) does one more tag go, and
|
|
306
|
+
// drift is recomputed over what remains.
|
|
307
|
+
for (;;) {
|
|
308
|
+
const fitted = fitKnownTagsDrift(current, detectTagDrift(current.tags), byteBudget);
|
|
309
|
+
if (fitted) return { content: [{ type: 'text', text: fitted.text }] };
|
|
310
|
+
if (current.tags.length <= 1) {
|
|
311
|
+
// A lone tag that cannot fit even with no drift: return it rather than
|
|
312
|
+
// an empty page with no way forward (the list-tools rule).
|
|
313
|
+
return { content: [{ type: 'text', text: JSON.stringify({ ...current, drift: [] }, null, 2) }] };
|
|
314
|
+
}
|
|
315
|
+
const kept = current.tags.slice(0, -1);
|
|
316
|
+
current = {
|
|
317
|
+
...current,
|
|
318
|
+
tags: kept,
|
|
319
|
+
next_cursor: fitOpts.cursorFor(kept[kept.length - 1]),
|
|
320
|
+
page: { ...current.page, returned: kept.length, budget_trimmed: true },
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Largest similarity-desc prefix of `drift` whose page fits `byteBudget`:
|
|
327
|
+
* the whole list untouched when it fits, else `page.drift_truncated: true` +
|
|
328
|
+
* `drift_dropped`. `null` when not even an empty drift list fits.
|
|
329
|
+
*/
|
|
330
|
+
function fitKnownTagsDrift(page, drift, byteBudget) {
|
|
331
|
+
const serialize = (payload) => JSON.stringify(payload, null, 2);
|
|
332
|
+
const fits = (text) => Buffer.byteLength(text, 'utf8') <= byteBudget;
|
|
333
|
+
const full = { ...page, drift };
|
|
334
|
+
const fullText = serialize(full);
|
|
335
|
+
if (fits(fullText)) return { payload: full, text: fullText };
|
|
336
|
+
let lo = 0;
|
|
337
|
+
let hi = drift.length - 1;
|
|
338
|
+
let best = null;
|
|
339
|
+
while (lo <= hi) {
|
|
340
|
+
const mid = (lo + hi) >>> 1;
|
|
341
|
+
const candidate = { ...page, drift: drift.slice(0, mid), page: { ...page.page, drift_truncated: true, drift_dropped: drift.length - mid } };
|
|
342
|
+
const text = serialize(candidate);
|
|
343
|
+
if (fits(text)) {
|
|
344
|
+
best = { payload: candidate, text };
|
|
345
|
+
lo = mid + 1;
|
|
346
|
+
} else {
|
|
347
|
+
hi = mid - 1;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return best;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* AI metadata proposal — [[HS-TASK-TAOD]] Phase 4 of [[HS-EPIC-Y9OJ]].
|
|
355
|
+
*
|
|
356
|
+
* Thin MCP wrapper around console `POST /api/ai/propose-metadata` (Haiku 4.5).
|
|
357
|
+
* One-shot at create time — agents must NOT call this on edit, status change,
|
|
358
|
+
* or update flows; that's the closed-loop bias the epic explicitly rejects.
|
|
359
|
+
*/
|
|
360
|
+
export async function handleProposeMetadata(args = {}) {
|
|
361
|
+
const name = typeof args?.name === 'string' ? args.name : '';
|
|
362
|
+
const body = typeof args?.body === 'string' ? args.body : '';
|
|
363
|
+
if (!name.trim() && !body.trim()) {
|
|
364
|
+
return {
|
|
365
|
+
isError: true,
|
|
366
|
+
content: [
|
|
367
|
+
{
|
|
368
|
+
type: 'text',
|
|
369
|
+
text: JSON.stringify(
|
|
370
|
+
{ ok: false, reason: 'invalid_request', message: 'name or body is required' },
|
|
371
|
+
null,
|
|
372
|
+
2,
|
|
373
|
+
),
|
|
374
|
+
},
|
|
375
|
+
],
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const payload = { action: 'propose', name, body };
|
|
380
|
+
if (typeof args?.specType === 'string') payload.specType = args.specType;
|
|
381
|
+
if (typeof args?.parentExcerpt === 'string') payload.parentExcerpt = args.parentExcerpt;
|
|
382
|
+
|
|
383
|
+
const result = await consoleFetch('/api/ai/propose-metadata', {
|
|
384
|
+
method: 'POST',
|
|
385
|
+
body: payload,
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
if (!result.ok) {
|
|
389
|
+
const reason =
|
|
390
|
+
result.errorKind === 'timeout'
|
|
391
|
+
? 'console_timeout'
|
|
392
|
+
: result.status === 0
|
|
393
|
+
? 'console_unreachable'
|
|
394
|
+
: 'console_error';
|
|
395
|
+
const errorPayload = {
|
|
396
|
+
ok: false,
|
|
397
|
+
reason,
|
|
398
|
+
message: consoleErrorMessage(
|
|
399
|
+
result,
|
|
400
|
+
`Console /api/ai/propose-metadata returned ${result.status}`,
|
|
401
|
+
),
|
|
402
|
+
console_response: result.body,
|
|
403
|
+
};
|
|
404
|
+
return {
|
|
405
|
+
isError: true,
|
|
406
|
+
content: [{ type: 'text', text: JSON.stringify(errorPayload, null, 2) }],
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
return {
|
|
411
|
+
content: [{ type: 'text', text: JSON.stringify(result.body, null, 2) }],
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Ground-truth dev-server status. The heavy logic lives in the repo's
|
|
416
|
+
// scripts/dev-server-status.mjs (the shared core for the hs CLI verb and the
|
|
417
|
+
// PreToolUse reminder hook too); we dynamic-import it from the vault root so
|
|
418
|
+
// the three faces stay in lockstep. Must never throw out of the tool call.
|
|
419
|
+
export async function handleCanvasInbox(vaultRoot, args = {}) {
|
|
420
|
+
// Session id: explicit arg wins; otherwise the ambient Claude Code session.
|
|
421
|
+
const explicit = typeof args.session_id === 'string' ? args.session_id.trim() : '';
|
|
422
|
+
const sessionId =
|
|
423
|
+
explicit ||
|
|
424
|
+
(process.env.CLAUDE_CODE_SESSION_ID || process.env.CLAUDE_SESSION_ID || '').trim();
|
|
425
|
+
|
|
426
|
+
const result = await executeCanvasInbox({ vaultRoot, sessionId });
|
|
427
|
+
return {
|
|
428
|
+
...(result.ok ? {} : { isError: true }),
|
|
429
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* `heyspex_ship_epic` / `backfill_epic`. Cloud-aware entry
|
|
435
|
+
* ([[HS-BYU4-CWPX-TASK-promotion-writes-go-through-the-console]] sweep):
|
|
436
|
+
* `executeShipEpicCloudAware` resolves `resolveVaultWriteMode()` ONCE and either
|
|
437
|
+
* runs the untouched synchronous disk path (`local`) or creates the epic, its
|
|
438
|
+
* tasks and the review doc through `POST /api/specs` in create-first/link-second
|
|
439
|
+
* order (`cloud`). The handler became `async` for that — awaiting is safe in
|
|
440
|
+
* both modes, since the local branch still returns the same plain object.
|
|
441
|
+
*/
|
|
442
|
+
export async function handleShipEpic(vaultRoot, args) {
|
|
443
|
+
const result = await executeShipEpicCloudAware(vaultRoot, args);
|
|
444
|
+
if (result.error) {
|
|
445
|
+
return {
|
|
446
|
+
isError: true,
|
|
447
|
+
content: [{ type: 'text', text: JSON.stringify(result) }],
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
return {
|
|
451
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
452
|
+
};
|
|
453
|
+
}
|
|
@@ -114,6 +114,26 @@ export function findReviewDocForSpec(vaultRoot, parentSpecId) {
|
|
|
114
114
|
*/
|
|
115
115
|
export const FILE_KEY_RE = /^(?=[0-9]*[A-Z])[A-Z0-9]{4,}$/;
|
|
116
116
|
|
|
117
|
+
/**
|
|
118
|
+
* `hs next CJV2` → `HS-CJV2` (owner ruling 2026-09-01): typing the bare file
|
|
119
|
+
* key is the natural gesture — the `HS-` prefix carries no information the
|
|
120
|
+
* CLI needs, so requiring it is friction. Accepts the long display form too
|
|
121
|
+
* (`CJV2-90JL`, `CJV2-90JL-EPIC`) by normalizing on the FIRST segment.
|
|
122
|
+
*
|
|
123
|
+
* Returns the `HS-`-prefixed uppercase id, or `null` when the token is not a
|
|
124
|
+
* spec-id shape at all (so the caller can keep its own "unexpected argument"
|
|
125
|
+
* error). A token that already starts with `HS-` comes back uppercased and
|
|
126
|
+
* otherwise untouched. Mirror: spex-console/src/lib/vault-id.ts
|
|
127
|
+
* `normalizeBareSpecIdToken`.
|
|
128
|
+
*/
|
|
129
|
+
export function normalizeBareSpecIdToken(input) {
|
|
130
|
+
const token = String(input ?? '').trim().toUpperCase();
|
|
131
|
+
if (!token) return null;
|
|
132
|
+
if (token.startsWith('HS-')) return token;
|
|
133
|
+
const head = token.split('-')[0];
|
|
134
|
+
return FILE_KEY_RE.test(head) ? `HS-${token}` : null;
|
|
135
|
+
}
|
|
136
|
+
|
|
117
137
|
/**
|
|
118
138
|
* File key carried by a spec id/basename in EITHER ordering, or null for
|
|
119
139
|
* legacy no-file-key names. The file key is the first title-tail token when it
|
|
@@ -202,14 +222,36 @@ export function allocateFileKey(occupied, options = {}) {
|
|
|
202
222
|
}
|
|
203
223
|
|
|
204
224
|
/**
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
*
|
|
225
|
+
* Mint a FILE KEY against an ALREADY-RESOLVED occupancy set. `randomUnit` is
|
|
226
|
+
* injectable for tests. `occupied` must be the UNION of file keys AND family
|
|
227
|
+
* keys: era-3 ids are bare file keys, so a file key that equals any family key
|
|
228
|
+
* makes a bare `HS-XXXX` citation ambiguous between one file and a whole
|
|
229
|
+
* family ([[HS-SJOT-AEP1-DIAG]]).
|
|
230
|
+
*
|
|
231
|
+
* TAKES THE OCCUPANCY, NOT A VAULT ROOT ([[HS-FJWL]]). This used to be
|
|
232
|
+
* `mintFileKey(vaultRoot)` and resolved occupancy itself, by walking
|
|
233
|
+
* `specs/**` — correct on the disk tier, blind on the cloud tier, where a
|
|
234
|
+
* filed spec has a Neon row and no local `.md` and the stale local clone still
|
|
235
|
+
* answers with a large, plausible-looking set. File keys are the unique
|
|
236
|
+
* per-file identity under Naming V3, so a blind scan there re-issues a live
|
|
237
|
+
* id. Callers now resolve occupancy for THEIR tier —
|
|
238
|
+
* `collectOccupiedKeysEitherKindViaTier` (occupied-keys.mjs) on a tier-aware
|
|
239
|
+
* path, `collectOccupiedKeysEitherKind` below on a path that is disk-only by
|
|
240
|
+
* construction — and hand the set in.
|
|
241
|
+
*
|
|
242
|
+
* The `Set` check is the migration's fail-loud: a call site left passing a
|
|
243
|
+
* vaultRoot string would otherwise mint against `collectOccupiedKeysEitherKind`
|
|
244
|
+
* of nothing, i.e. against an empty set, i.e. duplicates. Refuse instead.
|
|
210
245
|
*/
|
|
211
|
-
export function mintFileKey(
|
|
212
|
-
|
|
246
|
+
export function mintFileKey(occupied, options = {}) {
|
|
247
|
+
if (!(occupied instanceof Set)) {
|
|
248
|
+
throw new TypeError(
|
|
249
|
+
'mintFileKey(occupied, options) takes a resolved occupancy Set, not a vaultRoot ' +
|
|
250
|
+
'([[HS-FJWL]]). Use collectOccupiedKeysEitherKindViaTier(vaultRoot, { tier, consoleUrl }) ' +
|
|
251
|
+
'on a tier-aware path, or collectOccupiedKeysEitherKind(vaultRoot) on a disk-only one.',
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
return allocateFileKey(occupied, options);
|
|
213
255
|
}
|
|
214
256
|
|
|
215
257
|
/**
|
|
@@ -267,6 +309,20 @@ export function collectOccupiedKeysEitherKind(vaultRoot) {
|
|
|
267
309
|
* machinery as {@link allocateFileKey} (4 chars, at least one letter),
|
|
268
310
|
* checked against {@link collectOccupiedFamilyKeysFromVault} so a fresh mint
|
|
269
311
|
* doesn't accidentally collide with an existing unrelated family.
|
|
312
|
+
*
|
|
313
|
+
* DELIBERATELY STILL DISK-ONLY — do not "fix" this into a network call
|
|
314
|
+
* ([[HS-FJWL]]). {@link mintFileKey} was made tier-aware because a file key is
|
|
315
|
+
* the unique per-file identity: re-issuing one is a duplicate id. A FAMILY key
|
|
316
|
+
* is the opposite — any number of specs may legally share one (V3 shared-
|
|
317
|
+
* surname model, see {@link collectOccupiedFamilyKeysFromVault}), so this
|
|
318
|
+
* occupancy scan is a "pick something fresh" heuristic, NOT a uniqueness gate.
|
|
319
|
+
* Missing a cloud-only family key costs at most a family name reused by an
|
|
320
|
+
* unrelated spec, which the model already permits; paying a paged console walk
|
|
321
|
+
* per family mint would buy nothing. The cross-namespace half
|
|
322
|
+
* ([[HS-SJOT-AEP1-DIAG]] — a family key must not equal a FILE key) is carried
|
|
323
|
+
* by the union below on the disk tier and, in cloud reliance, by the console's
|
|
324
|
+
* own `createSpec` mint, which resolves occupancy through `loadAll()` and is
|
|
325
|
+
* tier-correct by construction.
|
|
270
326
|
*/
|
|
271
327
|
export function mintFamilyKey(vaultRoot, options = {}) {
|
|
272
328
|
return allocateFileKey(collectOccupiedKeysEitherKind(vaultRoot), options);
|
|
@@ -553,21 +609,41 @@ export function isBareFamilyToken(input) {
|
|
|
553
609
|
}
|
|
554
610
|
|
|
555
611
|
/**
|
|
556
|
-
* Guard for mutating MCP/CLI ops: reject a `spec_id` that is a bare
|
|
557
|
-
*
|
|
558
|
-
* `{ error, message }` to surface to the caller, or `null` when the id
|
|
559
|
-
* unambiguous enough to act on. Read-only resolvers do NOT call this — they
|
|
560
|
-
* a family token out to a set or just miss.
|
|
612
|
+
* Guard for mutating MCP/CLI ops: reject a `spec_id` that is a bare `HS-XXXX`
|
|
613
|
+
* token the caller has not proven names exactly one spec. Returns an error
|
|
614
|
+
* object `{ error, message }` to surface to the caller, or `null` when the id
|
|
615
|
+
* is unambiguous enough to act on. Read-only resolvers do NOT call this — they
|
|
616
|
+
* fan a family token out to a set or just miss.
|
|
617
|
+
*
|
|
618
|
+
* WORDING ([[HS-05ZW-XHR4-DIAG-the-capture-dropped-it-warning-blamed]] follow-on):
|
|
619
|
+
* {@link isBareFamilyToken} is SHAPE-ONLY — `HS-05ZW` is exactly as likely to be
|
|
620
|
+
* an era-3 FILE key (the stored `id:` of every newly minted spec) as a family
|
|
621
|
+
* key, and nothing here can tell them apart without a lookup. So the default
|
|
622
|
+
* message states only what was actually established: the token could not be
|
|
623
|
+
* resolved to one spec from this machine, and it may be either. A caller that
|
|
624
|
+
* HAS verified the family reading (e.g. `classifyBareSpecId` found the token
|
|
625
|
+
* among the vault's occupied family keys) passes `confirmedFamily: true` to get
|
|
626
|
+
* the stronger, now-earned wording. The actionable hint is identical either way.
|
|
627
|
+
*
|
|
628
|
+
* @param {string|null|undefined} input
|
|
629
|
+
* @param {{ confirmedFamily?: boolean, detail?: string|null }} [opts]
|
|
561
630
|
*/
|
|
562
|
-
export function requireResolvableSpecId(input) {
|
|
631
|
+
export function requireResolvableSpecId(input, opts = {}) {
|
|
563
632
|
if (isBareFamilyToken(input)) {
|
|
564
633
|
const base = normalizeIdInput(input);
|
|
634
|
+
const hint =
|
|
635
|
+
`Pass a full basename that includes the type label ` +
|
|
636
|
+
`(e.g. ${base}-TASK-<slug> or HS-TASK-${base.replace(/^HS-/, '')}-<slug>; ` +
|
|
637
|
+
`the era-3 form is ${base}-<FAMILY>-<TYPE>-<slug>).`;
|
|
638
|
+
const claim = opts.confirmedFamily
|
|
639
|
+
? `"${base}" is a bare family key, which names a whole shared-suffix family ` +
|
|
640
|
+
`rather than one spec.`
|
|
641
|
+
: `"${base}" is a bare token that could not be resolved to exactly one spec ` +
|
|
642
|
+
`from this machine — it may be the file key of a spec with no local copy, ` +
|
|
643
|
+
`or a family key naming a whole shared-suffix family.`;
|
|
565
644
|
return {
|
|
566
645
|
error: 'AMBIGUOUS_SPEC_ID',
|
|
567
|
-
message:
|
|
568
|
-
`"${base}" is a bare family key, which names a whole shared-suffix family ` +
|
|
569
|
-
`rather than one spec. Pass a full basename that includes the type label ` +
|
|
570
|
-
`(e.g. ${base}-TASK-<slug> or HS-TASK-${base.replace(/^HS-/, '')}-<slug>).`,
|
|
646
|
+
message: opts.detail ? `${claim} ${opts.detail} ${hint}` : `${claim} ${hint}`,
|
|
571
647
|
};
|
|
572
648
|
}
|
|
573
649
|
return null;
|