heyspex 0.1.0 → 0.2.0
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/README.md +21 -0
- package/bin/hs.mjs +931 -98
- 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 +1085 -103
- package/dist/cli/hs-claim.mjs +936 -63
- package/dist/cli/hs-connect.mjs +9786 -1910
- package/dist/cli/hs-defer.mjs +5267 -3053
- package/dist/cli/hs-dev.mjs +564 -0
- package/dist/cli/hs-env.mjs +385 -299
- package/dist/cli/hs-index-rebuild.mjs +7129 -0
- package/dist/cli/hs-index-reconcile.mjs +7852 -0
- package/dist/cli/hs-init.mjs +777 -138
- package/dist/cli/hs-install.mjs +6583 -485
- package/dist/cli/hs-join.mjs +10137 -2368
- package/dist/cli/hs-vault-pull.mjs +4659 -5882
- package/dist/cli/hs-vault.mjs +3793 -1537
- package/dist/cli/hs-workspace.mjs +2550 -524
- 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 +28 -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 +26 -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 +31 -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 +30 -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/mcp-client-config.md +259 -6
- package/package.json +231 -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/bin/launch-mcp.mjs +95 -0
- package/plugins/heyspex/bin/run-hook.mjs +62 -0
- package/plugins/heyspex/hooks/cursor-hooks.json +227 -0
- package/plugins/heyspex/hooks/hooks.json +361 -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 +410 -0
- package/scripts/claude-hooks/_intent-classifier-semantic.mjs +351 -0
- package/scripts/claude-hooks/_run.mjs +120 -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-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 +575 -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/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 +66 -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 +247 -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 +449 -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/wip-checkpoint.mjs +298 -0
- package/scripts/cursor-hooks/manifest.mjs +235 -0
- package/scripts/dev-server-status.mjs +34 -13
- package/scripts/dev-server-stop.mjs +313 -0
- package/scripts/heyspex-mode.mjs +268 -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 +333 -0
- package/scripts/hs-worktree.mjs +201 -0
- package/scripts/install-claude-code-hooks.mjs +270 -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 +197 -60
- package/scripts/lib/customer-kit.mjs +886 -0
- package/scripts/lib/dev-mode.mjs +244 -28
- package/scripts/lib/heyspex-home.mjs +72 -0
- package/scripts/lib/hs-command.mjs +277 -0
- package/scripts/lib/hs-companion-settings.mjs +472 -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 +11 -21
- package/scripts/lib/hs-source-push.mjs +107 -58
- 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/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 +441 -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/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 +274 -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/git-reflog-audit.mjs +294 -0
- package/spexharness/src/intent-classifier.mjs +194 -0
- package/spexharness/src/intent-directive.mjs +541 -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 +819 -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 +95 -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 +46 -5
- package/vendor/heyspex-mcp/capture-channel.mjs +55 -5
- package/vendor/heyspex-mcp/capture-console-client.mjs +80 -1
- package/vendor/heyspex-mcp/capture-focus.mjs +1035 -29
- package/vendor/heyspex-mcp/capture-outbox.mjs +5 -1
- package/vendor/heyspex-mcp/capture-tools.mjs +1219 -0
- package/vendor/heyspex-mcp/claim-tools.mjs +1124 -0
- package/vendor/heyspex-mcp/cli-verbs.mjs +2076 -0
- package/vendor/heyspex-mcp/cli.mjs +201 -15524
- package/vendor/heyspex-mcp/cloud-tool-surface.mjs +128 -0
- package/vendor/heyspex-mcp/cloud-vault-writes.mjs +412 -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 +657 -16
- package/vendor/heyspex-mcp/console-transport.mjs +166 -0
- package/vendor/heyspex-mcp/credentials.mjs +584 -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 +124 -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 +688 -0
- package/vendor/heyspex-mcp/fleet-handlers.mjs +37 -0
- package/vendor/heyspex-mcp/fleet-queue-tools.mjs +69 -14
- package/vendor/heyspex-mcp/focus-tools.mjs +190 -0
- package/vendor/heyspex-mcp/folder-trust-store.mjs +59 -0
- package/vendor/heyspex-mcp/http-transport.mjs +300 -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 +94 -0
- package/vendor/heyspex-mcp/memory-tools.mjs +523 -0
- package/vendor/heyspex-mcp/mvp-mode.mjs +53 -6
- package/vendor/heyspex-mcp/occupied-keys.mjs +141 -0
- package/vendor/heyspex-mcp/on-deck-brief.mjs +3360 -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 +906 -21
- 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 +325 -0
- package/vendor/heyspex-mcp/procedure-tools.mjs +540 -351
- package/vendor/heyspex-mcp/promote-capture.mjs +386 -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 +660 -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 +29 -9
- 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 +59 -0
- package/vendor/heyspex-mcp/research-focus.mjs +207 -4
- package/vendor/heyspex-mcp/research-handlers.mjs +623 -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/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 +108 -0
- package/vendor/heyspex-mcp/session-tools.mjs +152 -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 +446 -0
- package/vendor/heyspex-mcp/spec-id-lib.mjs +93 -17
- package/vendor/heyspex-mcp/split-research.mjs +301 -56
- package/vendor/heyspex-mcp/stdio-rpc.mjs +22 -0
- package/vendor/heyspex-mcp/submit-for-review.mjs +132 -6
- package/vendor/heyspex-mcp/summary-contract.mjs +105 -0
- package/vendor/heyspex-mcp/summary-write-tools.mjs +224 -0
- package/vendor/heyspex-mcp/symptom-query.mjs +146 -0
- package/vendor/heyspex-mcp/test-helpers.mjs +108 -1
- package/vendor/heyspex-mcp/tool-catalog.mjs +2813 -0
- package/vendor/heyspex-mcp/tool-dispatch.mjs +639 -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 +724 -0
- package/vendor/heyspex-mcp/vault-less-endpoints.mjs +52 -0
- package/vendor/heyspex-mcp/vault-resolve.mjs +366 -68
- package/vendor/heyspex-mcp/verify-reproduction.mjs +270 -30
- package/vendor/heyspex-mcp/whoami.mjs +91 -0
|
@@ -0,0 +1,688 @@
|
|
|
1
|
+
// Filing and plan tools: heyspex_file_spec, register_spec, push_plan, ground_plan,
|
|
2
|
+
// plan_review, plan_qc, validate_linkage, validate_user_skills. 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 { buildGatewayReport } from '../../scripts/gateway-skills-lib.mjs';
|
|
6
|
+
import { resolveSpecIdForMutation, wrapDesignResult } from './tool-helpers.mjs';
|
|
7
|
+
import { validateLinkage } from './validate-linkage.mjs';
|
|
8
|
+
import { runPlanQc, runPlanReview } from './plan-review.mjs';
|
|
9
|
+
import { injectNeonFocusPin, writeFocusSelectionVendor } from './focus-tools.mjs';
|
|
10
|
+
import { executeGroundPlan } from './plan-ground.mjs';
|
|
11
|
+
import { executePushPlanCloudAware } from './plan-push.mjs';
|
|
12
|
+
import { registerSpecViaMode } from './register-spec.mjs';
|
|
13
|
+
import { SUMMARY_REQUIRED_FILE_SPEC_TYPES, buildSummaryRequiredMcpError } from './summary-contract.mjs';
|
|
14
|
+
import { buildCaptureStageRefusal, decideCaptureStageGate } from './capture-stage-gate.mjs';
|
|
15
|
+
import { resolveLiveActiveCapture } from './capture-tools.mjs';
|
|
16
|
+
import { detectAgentIdentity } from './mcp-config.mjs';
|
|
17
|
+
import { randomUUID } from 'node:crypto';
|
|
18
|
+
import { consoleFetch, enrichConsoleFailurePayloadAsync } from './console-transport.mjs';
|
|
19
|
+
import { consoleErrorCode, consoleErrorMessage } from './console-fetch.mjs';
|
|
20
|
+
import { resolveReadTier } from './read-path.mjs';
|
|
21
|
+
import { join } from 'node:path';
|
|
22
|
+
import { statSync, existsSync } from 'node:fs';
|
|
23
|
+
import { importDevModeCore } from './dev-mode-core.mjs';
|
|
24
|
+
import { PACKAGE_ROOT } from './package-root.mjs';
|
|
25
|
+
import { resolveCodeRoot } from '../../scripts/lib/shared-path-helpers.mjs';
|
|
26
|
+
|
|
27
|
+
// scripts/lib/dev-mode.mjs is a CODE-repo artifact that ships with this
|
|
28
|
+
// package — since the repo split (2026-08-31) the vault root no longer holds
|
|
29
|
+
// it, so resolve it relative to THIS file's checkout first and fall back to
|
|
30
|
+
// the vault root only for pre-split layouts where they were the same repo.
|
|
31
|
+
// `HS_PKG_ROOT` wins when set (same precedence as dev-tools.mjs's
|
|
32
|
+
// CHECKOUT_ROOT / plan-review.mjs's resolveCodeAuditRoots).
|
|
33
|
+
const CHECKOUT_ROOT = resolveCodeRoot({ packageRoot: PACKAGE_ROOT });
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* heyspex_validate_linkage — scan the vault for orphan specs, broken wikilinks,
|
|
37
|
+
* missing manifest refs, and parent/child mismatches ([[HS-TASK-0U1F]]).
|
|
38
|
+
* Core lives in validate-linkage.mjs; this is the MCP face.
|
|
39
|
+
*/
|
|
40
|
+
export function handleValidateLinkage(vaultRoot, args = {}) {
|
|
41
|
+
const result = validateLinkage(vaultRoot, {
|
|
42
|
+
spec_id: typeof args.spec_id === 'string' && args.spec_id.trim() ? args.spec_id.trim() : null,
|
|
43
|
+
includeWarnings: args.include_warnings !== false,
|
|
44
|
+
});
|
|
45
|
+
return {
|
|
46
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* heyspex_validate_user_skills — validate user-authored gateway skills and
|
|
52
|
+
* their scope coverage (HS-WEC3). Core lives in scripts/gateway-skills-lib.mjs;
|
|
53
|
+
* this is the MCP face (CLI face: `hs gateway validate`; doctor face:
|
|
54
|
+
* `hs doctor --check-gateway`). Read-only by contract.
|
|
55
|
+
*/
|
|
56
|
+
export function handleValidateUserSkills(vaultRoot, args = {}) {
|
|
57
|
+
const report = buildGatewayReport(vaultRoot);
|
|
58
|
+
const slug = typeof args.skill === 'string' && args.skill.trim() ? args.skill.trim() : null;
|
|
59
|
+
const result = slug
|
|
60
|
+
? {
|
|
61
|
+
...report,
|
|
62
|
+
skills: { ...report.skills, skills: report.skills.skills.filter((s) => s.slug === slug) },
|
|
63
|
+
orphans: { ...report.orphans, orphans: report.orphans.orphans.filter((o) => o.slug === slug) },
|
|
64
|
+
}
|
|
65
|
+
: report;
|
|
66
|
+
if (slug && result.skills.skills.length === 0) {
|
|
67
|
+
return wrapDesignResult({ error: 'skill_not_found', message: `No user skill named "${slug}" under ${report.skills.root}.` });
|
|
68
|
+
}
|
|
69
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* heyspex_register_spec — link an orphan/mismatched spec into the roadmap
|
|
74
|
+
* hierarchy ([[HS-TASK-5K13]]). Core lives in register-spec.mjs; this is the
|
|
75
|
+
* MCP face. The fix counterpart to heyspex_validate_linkage.
|
|
76
|
+
*/
|
|
77
|
+
export async function handleRegisterSpec(vaultRoot, args = {}) {
|
|
78
|
+
const registerResolved = await resolveSpecIdForMutation(args.spec_id);
|
|
79
|
+
if (registerResolved.response) return registerResolved.response;
|
|
80
|
+
// Cloud-aware entry ([[HS-BYU4-CWPX-TASK-promotion-writes-go-through-the-console]]
|
|
81
|
+
// sweep): resolves `resolveVaultWriteMode()` ONCE and either runs the
|
|
82
|
+
// untouched synchronous disk path (`local`) or sends each touched spec
|
|
83
|
+
// through `PUT /api/specs/:id` (`cloud`). Awaiting is safe in both — the
|
|
84
|
+
// local branch delegates straight to `registerSpec` and returns the same
|
|
85
|
+
// plain object it always did.
|
|
86
|
+
const result = await registerSpecViaMode(vaultRoot, {
|
|
87
|
+
spec_id: registerResolved.specId,
|
|
88
|
+
parent_id: typeof args.parent_id === 'string' && args.parent_id.trim() ? args.parent_id.trim() : null,
|
|
89
|
+
roadmap_id: typeof args.roadmap_id === 'string' && args.roadmap_id.trim() ? args.roadmap_id.trim() : undefined,
|
|
90
|
+
position: typeof args.position === 'string' && args.position.trim() ? args.position.trim() : undefined,
|
|
91
|
+
dryRun: args.dry_run === true,
|
|
92
|
+
});
|
|
93
|
+
return {
|
|
94
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* File a NEW spec of any non-capture kind ([[HS-EPIC-1EM0]] / [[HS-TASK-B6DG]]).
|
|
100
|
+
* Thin forwarder to console `POST /api/specs` — the server owns id allocation,
|
|
101
|
+
* canonical-template frontmatter, parent-array linkage, and per-kind validation
|
|
102
|
+
* (all extended by [[HS-TASK-U13O]]). The client contract is preserved verbatim
|
|
103
|
+
* ({ type, name, frontmatter?, body? }); no kind/title rename. POST errors
|
|
104
|
+
* (invalid parent, `type: capture` rejection, etc.) surface verbatim so the
|
|
105
|
+
* agent sees the server's guidance (e.g. "use heyspex_active_capture").
|
|
106
|
+
*/
|
|
107
|
+
export function resolveFileSpecSummary(args = {}) {
|
|
108
|
+
const top = typeof args.summary === 'string' ? args.summary.trim() : '';
|
|
109
|
+
const fromFm =
|
|
110
|
+
args.frontmatter && typeof args.frontmatter.summary === 'string'
|
|
111
|
+
? args.frontmatter.summary.trim()
|
|
112
|
+
: '';
|
|
113
|
+
return top || fromFm;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* The only top-level `heyspex_file_spec` arguments the tool actually reads
|
|
118
|
+
* ([[tool-catalog.mjs]]'s declared inputSchema `properties`, kept in
|
|
119
|
+
* lockstep by `file-spec-artifact-field.test.mjs`). Parent/milestone linkage
|
|
120
|
+
* is NOT a top-level argument — it goes inside `frontmatter.parent` /
|
|
121
|
+
* `frontmatter.milestone` (the schema description says so), but nothing
|
|
122
|
+
* enforced that at runtime.
|
|
123
|
+
*/
|
|
124
|
+
export const FILE_SPEC_ACCEPTED_ARGS = [
|
|
125
|
+
'type',
|
|
126
|
+
'name',
|
|
127
|
+
'summary',
|
|
128
|
+
'frontmatter',
|
|
129
|
+
'body',
|
|
130
|
+
'recipe_context',
|
|
131
|
+
'checks',
|
|
132
|
+
'expect_tier',
|
|
133
|
+
];
|
|
134
|
+
|
|
135
|
+
/** Top-level keys a caller plausibly means for frontmatter linkage — used only to tailor the refusal message. */
|
|
136
|
+
const FILE_SPEC_FRONTMATTER_HINT_KEYS = new Set(['parent', 'milestone', 'pillar', 'severity', 'id']);
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Pure verdict on a `heyspex_file_spec` argument bag: refuses an unrecognized
|
|
140
|
+
* top-level key BY NAME instead of letting it silently vanish before
|
|
141
|
+
* `buildFileSpecPostBody` even runs — the same failure mode
|
|
142
|
+
* [[additionalproperties-false-in-an-mcp]] documents for
|
|
143
|
+
* `heyspex_update_capture` (`updateCaptureArgsVerdict`), and the exact shape
|
|
144
|
+
* of [[HS-XQMK-A1QE-DIAG-heyspex-file-spec-drops-the-parent]]: a top-level
|
|
145
|
+
* `parent:` argument passed the declared `additionalProperties: false`
|
|
146
|
+
* schema (never enforced at runtime by the MCP transport), then vanished
|
|
147
|
+
* silently because `buildFileSpecPostBody` never read `args.parent` — every
|
|
148
|
+
* filed task landed orphaned with no error anywhere. Call this at the
|
|
149
|
+
* handler's first hop, before any other validation or the POST.
|
|
150
|
+
*/
|
|
151
|
+
export function fileSpecArgsVerdict(args = {}) {
|
|
152
|
+
const unknown = Object.keys(args).filter((k) => !FILE_SPEC_ACCEPTED_ARGS.includes(k));
|
|
153
|
+
if (unknown.length > 0) {
|
|
154
|
+
const hinted = unknown.filter((k) => FILE_SPEC_FRONTMATTER_HINT_KEYS.has(k));
|
|
155
|
+
const hint =
|
|
156
|
+
hinted.length > 0
|
|
157
|
+
? ` Spec-kind linkage/overrides go inside frontmatter, not top-level: pass frontmatter: { ${hinted
|
|
158
|
+
.map((k) => `${k}: ...`)
|
|
159
|
+
.join(', ')} } instead.`
|
|
160
|
+
: '';
|
|
161
|
+
return {
|
|
162
|
+
ok: false,
|
|
163
|
+
error: {
|
|
164
|
+
error: 'UNKNOWN_FIELD',
|
|
165
|
+
message: `heyspex_file_spec does not accept: ${unknown.join(', ')}. NOTHING WAS FILED — re-send using only the accepted arguments.${hint}`,
|
|
166
|
+
field: unknown[0],
|
|
167
|
+
unknown_fields: unknown,
|
|
168
|
+
accepted_fields: FILE_SPEC_ACCEPTED_ARGS,
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
return { ok: true };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Build the POST /api/specs request body from heyspex_file_spec args. Pure and
|
|
177
|
+
* exported so the field-forwarding contract is unit-testable without a live
|
|
178
|
+
* console. `recipe_context` (the heyspex_start_recipe start context) is
|
|
179
|
+
* forwarded verbatim when it is a non-null object — the server reads recipe_id
|
|
180
|
+
* + tool_scope from it and stamps recipe provenance ([[HS-98G6-TASK]] /
|
|
181
|
+
* [[HS-5G0P-EPIC]] AC5); it is omitted entirely when absent so non-recipe
|
|
182
|
+
* filings are byte-identical to before.
|
|
183
|
+
*/
|
|
184
|
+
export function buildFileSpecPostBody(args = {}, actingAgent = null) {
|
|
185
|
+
const authoredSummary = resolveFileSpecSummary(args);
|
|
186
|
+
return {
|
|
187
|
+
type: args.type,
|
|
188
|
+
name: args.name,
|
|
189
|
+
...(authoredSummary ? { summary: authoredSummary } : {}),
|
|
190
|
+
...(args.frontmatter && typeof args.frontmatter === 'object'
|
|
191
|
+
? { frontmatter: args.frontmatter }
|
|
192
|
+
: {}),
|
|
193
|
+
...(typeof args.body === 'string' ? { body: args.body } : {}),
|
|
194
|
+
...(actingAgent ? { acting_agent: actingAgent } : {}),
|
|
195
|
+
...(args.recipe_context && typeof args.recipe_context === 'object' && !Array.isArray(args.recipe_context)
|
|
196
|
+
? { recipe_context: args.recipe_context }
|
|
197
|
+
: {}),
|
|
198
|
+
// The frozen checks manifest ([[checks-manifest-authoring]]). Forwarded only
|
|
199
|
+
// when the caller authored rows, so filings that pass none stay byte-identical
|
|
200
|
+
// to before and pick up the server's `no_checks_manifest` warning instead.
|
|
201
|
+
...(Array.isArray(args.checks) && args.checks.length > 0 ? { checks: args.checks } : {}),
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Local host names whose console owns THIS machine's disk. A write posted to
|
|
207
|
+
* one of these landed (or should have landed) in the local clone; anything
|
|
208
|
+
* else is a cloud console with no access to it.
|
|
209
|
+
*/
|
|
210
|
+
export const LOCAL_CONSOLE_HOSTS = new Set(['localhost', '127.0.0.1', '0.0.0.0', '[::1]', '::1']);
|
|
211
|
+
|
|
212
|
+
/** True when a console origin points at this machine. Never throws. */
|
|
213
|
+
export function isLocalConsoleOrigin(url) {
|
|
214
|
+
if (typeof url !== 'string' || !url) return false;
|
|
215
|
+
try {
|
|
216
|
+
return LOCAL_CONSOLE_HOSTS.has(new URL(url).hostname.toLowerCase());
|
|
217
|
+
} catch {
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* WHERE DID THE WRITE LAND — the pure verdict ([[HS-IOJW-5YDH-DIAG]]).
|
|
224
|
+
*
|
|
225
|
+
* The bug this closes: a vault write reported plain success whether the bytes
|
|
226
|
+
* reached a file in this clone or only a Neon row on another machine. Two
|
|
227
|
+
* sessions lost work to it on 2026-08-21 and recovered it only by re-reading
|
|
228
|
+
* the file out of habit.
|
|
229
|
+
*
|
|
230
|
+
* Inputs are all MEASURED by the caller, never assumed:
|
|
231
|
+
* consoleUrl the origin the POST actually went to (consoleFetch result.url)
|
|
232
|
+
* consoleWrite the console's own `write` block when it is new enough to send one
|
|
233
|
+
* relPath vault-relative path the console says it wrote
|
|
234
|
+
* vaultRoot this session's clone, or null when there is none
|
|
235
|
+
* absPath the absolute path actually written, when the console reports one
|
|
236
|
+
* localFileExists a real stat of that path; null when unmeasurable
|
|
237
|
+
* devMode { mode, reliance, source, updated_at } from ~/.heyspex/dev-mode.json
|
|
238
|
+
* expectTier the caller's declared expectation: "disk" | "cloud" | undefined
|
|
239
|
+
*
|
|
240
|
+
* Returns { landed, error } — `error` non-null means the tool must answer
|
|
241
|
+
* isError, WITH the spec id still attached so nobody re-files a duplicate.
|
|
242
|
+
*/
|
|
243
|
+
export function fileSpecLandingVerdict({
|
|
244
|
+
consoleUrl = null,
|
|
245
|
+
consoleWrite = null,
|
|
246
|
+
relPath = null,
|
|
247
|
+
vaultRoot = null,
|
|
248
|
+
localFileExists = null,
|
|
249
|
+
devMode = null,
|
|
250
|
+
expectTier = null,
|
|
251
|
+
absPath = null,
|
|
252
|
+
} = {}) {
|
|
253
|
+
// Tier: prefer what the console measured; fall back to the origin we posted
|
|
254
|
+
// to, so this still works against a deployed console that predates the
|
|
255
|
+
// `write` block.
|
|
256
|
+
let tier;
|
|
257
|
+
let reportedBy;
|
|
258
|
+
if (consoleWrite?.mode === 'local' || consoleWrite?.mode === 'cloud') {
|
|
259
|
+
tier = consoleWrite.mode === 'local' ? 'disk' : 'cloud';
|
|
260
|
+
reportedBy = 'console';
|
|
261
|
+
} else {
|
|
262
|
+
tier = isLocalConsoleOrigin(consoleUrl) ? 'disk' : 'cloud';
|
|
263
|
+
reportedBy = 'inferred_from_console_url';
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const landed = {
|
|
267
|
+
tier,
|
|
268
|
+
reported_by: reportedBy,
|
|
269
|
+
console_url: consoleUrl,
|
|
270
|
+
rel_path: relPath,
|
|
271
|
+
// The console's own absolute path wins. Its vault root is the SPECS dir,
|
|
272
|
+
// not the repo root, and `relPath` is relative to THAT — joining it onto
|
|
273
|
+
// the MCP's vaultRoot points at a file that never existed. Caught live on
|
|
274
|
+
// the first real filing through this code.
|
|
275
|
+
abs_path: absPath ?? (vaultRoot && relPath ? join(vaultRoot, relPath) : null),
|
|
276
|
+
in_local_vault: localFileExists,
|
|
277
|
+
vault_root: vaultRoot,
|
|
278
|
+
console_write: consoleWrite ?? null,
|
|
279
|
+
dev_mode: devMode
|
|
280
|
+
? {
|
|
281
|
+
mode: devMode.mode ?? null,
|
|
282
|
+
reliance: devMode.reliance ?? null,
|
|
283
|
+
set_by: devMode.source ?? null,
|
|
284
|
+
set_at: devMode.updated_at ?? null,
|
|
285
|
+
}
|
|
286
|
+
: null,
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
const modeNote = landed.dev_mode?.mode
|
|
290
|
+
? ` Mode is ${landed.dev_mode.mode}/${landed.dev_mode.reliance}, last set by ${landed.dev_mode.set_by ?? 'unknown'} at ${landed.dev_mode.set_at ?? 'unknown time'}.`
|
|
291
|
+
: '';
|
|
292
|
+
|
|
293
|
+
// 1. The caller said where it wanted the write and it went elsewhere.
|
|
294
|
+
if (expectTier && tier !== expectTier) {
|
|
295
|
+
return {
|
|
296
|
+
landed,
|
|
297
|
+
error: {
|
|
298
|
+
error: 'WRITE_TIER_MISMATCH',
|
|
299
|
+
message:
|
|
300
|
+
`The spec was created, but on ${tier} — you asked for ${expectTier}.${modeNote} ` +
|
|
301
|
+
'The spec id below is real; do NOT file it again. Switch the mode with ' +
|
|
302
|
+
'heyspex_dev_mode and re-check, or pass expect_tier to match where you want it.',
|
|
303
|
+
},
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// 2. Undeclared cloud landing while this session has a clone in front of it.
|
|
308
|
+
// This is the exact shape that lost work: agent believes it is writing
|
|
309
|
+
// files, the mode was flipped underneath it, nothing appears on disk.
|
|
310
|
+
if (!expectTier && tier === 'cloud' && vaultRoot) {
|
|
311
|
+
return {
|
|
312
|
+
landed,
|
|
313
|
+
error: {
|
|
314
|
+
error: 'WRITE_LANDED_OFF_DISK',
|
|
315
|
+
message:
|
|
316
|
+
`The spec was created in the cloud index, NOT as a file in this clone (${vaultRoot}).${modeNote} ` +
|
|
317
|
+
'The spec id below is real; do NOT file it again. Either switch back with ' +
|
|
318
|
+
'heyspex_dev_mode({ action: "set", mode: "local" }) and re-file, or pass ' +
|
|
319
|
+
'expect_tier: "cloud" to say you meant it.',
|
|
320
|
+
},
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// 3. It claims disk, and the file is not there. A write that reports success
|
|
325
|
+
// over a missing file is the defect itself.
|
|
326
|
+
if (tier === 'disk' && localFileExists === false) {
|
|
327
|
+
return {
|
|
328
|
+
landed,
|
|
329
|
+
error: {
|
|
330
|
+
error: 'WRITE_NOT_ON_DISK',
|
|
331
|
+
message:
|
|
332
|
+
`The console answered success but no file exists at ${landed.abs_path ?? relPath}.${modeNote} ` +
|
|
333
|
+
'The spec id below may still be real in the index — check it before re-filing, and ' +
|
|
334
|
+
'read the console logs for a swallowed disk error.',
|
|
335
|
+
},
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return { landed, error: null };
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Shell around {@link fileSpecLandingVerdict}: reads the machine-local dev-mode
|
|
344
|
+
* record and stats the file. Never throws — a landing report that fails must
|
|
345
|
+
* not fail the write it reports on.
|
|
346
|
+
*/
|
|
347
|
+
export async function measureFileSpecLanding({ consoleUrl, consoleBody, relPath, vaultRoot, expectTier }) {
|
|
348
|
+
let devMode = null;
|
|
349
|
+
try {
|
|
350
|
+
if (vaultRoot) {
|
|
351
|
+
// Bundler-safe loader (see dev-mode-core.mjs): the runtime paths first,
|
|
352
|
+
// then a static specifier. Without the fallback the bundled remote
|
|
353
|
+
// endpoint skipped the landing probe entirely.
|
|
354
|
+
const mod = await importDevModeCore([
|
|
355
|
+
join(CHECKOUT_ROOT, 'scripts', 'lib', 'dev-mode.mjs'),
|
|
356
|
+
join(vaultRoot, 'scripts', 'lib', 'dev-mode.mjs'),
|
|
357
|
+
]);
|
|
358
|
+
devMode = mod.readLocalDevModeRecord()?.record ?? null;
|
|
359
|
+
}
|
|
360
|
+
} catch {
|
|
361
|
+
devMode = null;
|
|
362
|
+
}
|
|
363
|
+
// Which absolute path to stat. The console's own `absPath` is authoritative
|
|
364
|
+
// when it sent one — its vault root is <repo>/specs, so `relPath` alone is
|
|
365
|
+
// NOT resolvable against the MCP's repo root. Without it, try both roots
|
|
366
|
+
// rather than guessing one and reporting a phantom missing file.
|
|
367
|
+
const consoleWrite = consoleBody?.write ?? null;
|
|
368
|
+
const candidates = [];
|
|
369
|
+
if (typeof consoleWrite?.absPath === 'string' && consoleWrite.absPath) {
|
|
370
|
+
candidates.push(consoleWrite.absPath);
|
|
371
|
+
} else if (vaultRoot && relPath) {
|
|
372
|
+
candidates.push(join(vaultRoot, relPath), join(vaultRoot, 'specs', relPath));
|
|
373
|
+
}
|
|
374
|
+
let localFileExists = candidates.length > 0 ? false : null;
|
|
375
|
+
let absPath = candidates[0] ?? null;
|
|
376
|
+
for (const candidate of candidates) {
|
|
377
|
+
try {
|
|
378
|
+
if (statSync(candidate).isFile()) {
|
|
379
|
+
localFileExists = true;
|
|
380
|
+
absPath = candidate;
|
|
381
|
+
break;
|
|
382
|
+
}
|
|
383
|
+
} catch {
|
|
384
|
+
// keep looking
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
return fileSpecLandingVerdict({
|
|
388
|
+
consoleUrl,
|
|
389
|
+
consoleWrite,
|
|
390
|
+
relPath,
|
|
391
|
+
vaultRoot,
|
|
392
|
+
localFileExists,
|
|
393
|
+
devMode,
|
|
394
|
+
expectTier,
|
|
395
|
+
absPath,
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
export async function handleFileSpec(args = {}, vaultRoot = null) {
|
|
400
|
+
// First hop: refuse an unrecognized top-level arg (e.g. a top-level
|
|
401
|
+
// `parent:`) BY NAME instead of letting it silently vanish before
|
|
402
|
+
// buildFileSpecPostBody even runs [[HS-XQMK-A1QE-DIAG-heyspex-file-spec-drops-the-parent]].
|
|
403
|
+
const argsVerdict = fileSpecArgsVerdict(args);
|
|
404
|
+
if (!argsVerdict.ok) {
|
|
405
|
+
return {
|
|
406
|
+
isError: true,
|
|
407
|
+
content: [{ type: 'text', text: JSON.stringify(argsVerdict.error, null, 2) }],
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
if (!args.type || typeof args.type !== 'string') {
|
|
411
|
+
return {
|
|
412
|
+
isError: true,
|
|
413
|
+
content: [{ type: 'text', text: JSON.stringify({ error: 'INVALID_ARGS', message: 'type is required' }) }],
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
if (!args.name || typeof args.name !== 'string') {
|
|
417
|
+
return {
|
|
418
|
+
isError: true,
|
|
419
|
+
content: [{ type: 'text', text: JSON.stringify({ error: 'INVALID_ARGS', message: 'name is required' }) }],
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
// Summary required at filing — planning phase (milestone/epic) or execution
|
|
423
|
+
// phase (task via What's Next `/hs-next` only). Never co-file epics and tasks.
|
|
424
|
+
if (SUMMARY_REQUIRED_FILE_SPEC_TYPES.has(args.type) && !resolveFileSpecSummary(args)) {
|
|
425
|
+
return {
|
|
426
|
+
isError: true,
|
|
427
|
+
content: [{
|
|
428
|
+
type: 'text',
|
|
429
|
+
text: JSON.stringify(buildSummaryRequiredMcpError(args.type), null, 2),
|
|
430
|
+
}],
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
// Capture-stage rule ([[capture-stage-gate]]): an open capture must not become
|
|
434
|
+
// an epic/task/milestone. Checked HERE, before the POST, because the hook that
|
|
435
|
+
// used to own this could not read the live pin and could be switched off by a
|
|
436
|
+
// file. Runs on its own resolution failure as "allow" — a gate that refuses on
|
|
437
|
+
// its own uncertainty is worse than no gate.
|
|
438
|
+
const captureStageVerdict = decideCaptureStageGate({
|
|
439
|
+
toolArgs: args,
|
|
440
|
+
activeCapture: vaultRoot ? await resolveLiveActiveCapture(vaultRoot) : null,
|
|
441
|
+
});
|
|
442
|
+
if (captureStageVerdict.decision === 'refuse') {
|
|
443
|
+
return {
|
|
444
|
+
isError: true,
|
|
445
|
+
content: [{ type: 'text', text: JSON.stringify(buildCaptureStageRefusal(captureStageVerdict), null, 2) }],
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Acting agent for `assisted_by` auto-stamp ([[HS-EPIC-IKIM-authorship-provenance-frontmatter-owner]]
|
|
450
|
+
// AC #2). Env-detected (same source as claim_spec); the console seeds
|
|
451
|
+
// `assisted_by: [acting_agent]` on the new spec, omit-when-empty if unset.
|
|
452
|
+
const actingAgent = detectAgentIdentity(args.agent, args.session_id).agent;
|
|
453
|
+
const body = buildFileSpecPostBody(args, actingAgent);
|
|
454
|
+
// Idempotency key ([[HS-70TD-DIAG]]): generated ONCE per logical file_spec call
|
|
455
|
+
// and reused across the transport's timeout-retry (the header lives on the
|
|
456
|
+
// shared `init`), so a slow-console retry replays the first create server-side
|
|
457
|
+
// instead of double-allocating a second byte-identical spec.
|
|
458
|
+
const idempotencyKey = randomUUID();
|
|
459
|
+
const result = await consoleFetch('/api/specs', {
|
|
460
|
+
method: 'POST',
|
|
461
|
+
body,
|
|
462
|
+
headers: { 'idempotency-key': idempotencyKey },
|
|
463
|
+
// [[HS-2FQY]] double-mint guard: against a REMOTE console the blind
|
|
464
|
+
// timeout-retry can land on a different serverless instance, where the
|
|
465
|
+
// in-memory idempotency registry cannot replay the first create — the
|
|
466
|
+
// retry mints a SECOND id for the same spec (observed: HS-2FQY + HS-K70K,
|
|
467
|
+
// identical content, one call). Loopback keeps the retry (single process,
|
|
468
|
+
// registry replays). On a remote timeout, check before re-filing (below).
|
|
469
|
+
retryOnTimeout: false,
|
|
470
|
+
});
|
|
471
|
+
if (!result.ok) {
|
|
472
|
+
const payload = await enrichConsoleFailurePayloadAsync(result, {
|
|
473
|
+
error: consoleErrorCode(result, 'FILE_SPEC_FAILED'),
|
|
474
|
+
message: consoleErrorMessage(result, `POST /api/specs returned ${result.status}`),
|
|
475
|
+
status: result.status,
|
|
476
|
+
url: result.url,
|
|
477
|
+
console_response: result.body,
|
|
478
|
+
attempted: { type: args.type, name: args.name },
|
|
479
|
+
...(result.errorKind === 'timeout'
|
|
480
|
+
? {
|
|
481
|
+
hint:
|
|
482
|
+
'The create may still have completed server-side after this client timed out. '
|
|
483
|
+
+ 'Search for a spec with this exact name before re-filing — a blind re-file can mint a duplicate id ([[HS-2FQY]]).',
|
|
484
|
+
}
|
|
485
|
+
: {}),
|
|
486
|
+
});
|
|
487
|
+
return {
|
|
488
|
+
isError: true,
|
|
489
|
+
content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
// POST returns { spec: { id, ... } }; surface the canonical { spec_id, path }
|
|
493
|
+
// contract alongside the full created spec for callers that want more.
|
|
494
|
+
const spec = result.body?.spec ?? null;
|
|
495
|
+
const relPath = spec?.relPath ?? spec?.path ?? null;
|
|
496
|
+
// [[HS-H227-MCPC-TASK]]: a caller that passed no `expect_tier` at all used
|
|
497
|
+
// to fall through to `WRITE_LANDED_OFF_DISK` the moment this session's own
|
|
498
|
+
// reliance is cloud (fileSpecLandingVerdict's rule 2 — "undeclared cloud
|
|
499
|
+
// landing while a clone is in front of us") even though a cloud-reliant
|
|
500
|
+
// session filing into the cloud index is exactly the intended shape, not a
|
|
501
|
+
// surprise. Default to "cloud" from the SAME tier verdict every other read
|
|
502
|
+
// in this file already trusts (`resolveReadTier`) — only when the caller
|
|
503
|
+
// passed nothing; an explicit `expect_tier` (including an explicit "disk")
|
|
504
|
+
// is never overridden, so local-reliance behavior is untouched.
|
|
505
|
+
let expectTier = args.expect_tier === 'disk' || args.expect_tier === 'cloud' ? args.expect_tier : null;
|
|
506
|
+
if (!expectTier) {
|
|
507
|
+
const tier = await resolveReadTier();
|
|
508
|
+
if (tier === 'neon') expectTier = 'cloud';
|
|
509
|
+
}
|
|
510
|
+
// WHERE IT LANDED ([[HS-IOJW-5YDH-DIAG]]) — measured, not assumed. A write
|
|
511
|
+
// that reports on the request instead of the result is the defect; this
|
|
512
|
+
// stats the file and names the console it went to.
|
|
513
|
+
const { landed, error: landingError } = await measureFileSpecLanding({
|
|
514
|
+
consoleUrl: result.url,
|
|
515
|
+
consoleBody: result.body,
|
|
516
|
+
relPath,
|
|
517
|
+
vaultRoot,
|
|
518
|
+
expectTier,
|
|
519
|
+
});
|
|
520
|
+
const payload = {
|
|
521
|
+
spec_id: spec?.id ?? null,
|
|
522
|
+
path: relPath,
|
|
523
|
+
landed,
|
|
524
|
+
spec,
|
|
525
|
+
console_response: result.body,
|
|
526
|
+
};
|
|
527
|
+
if (landingError) {
|
|
528
|
+
return {
|
|
529
|
+
isError: true,
|
|
530
|
+
content: [{ type: 'text', text: JSON.stringify({ ...landingError, ...payload }, null, 2) }],
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
// Warn rung of the capture-stage gate: the filing went through, and the reason
|
|
534
|
+
// it would otherwise have been refused rides along on the response.
|
|
535
|
+
if (captureStageVerdict.decision === 'warn') {
|
|
536
|
+
payload.capture_stage_warning = buildCaptureStageRefusal(captureStageVerdict);
|
|
537
|
+
}
|
|
538
|
+
// Non-coercive post-create suggestion ([[HS-TASK-FWPC]] / [[HS-EPIC-MNBD]]):
|
|
539
|
+
// a freshly-filed milestone has no epics yet, so suggest filing the first one.
|
|
540
|
+
// The agent decides whether to act — the MCP layer NEVER auto-executes the
|
|
541
|
+
// follow-on (CLAUDE.md autonomy boundary). Skipping leaves the milestone in
|
|
542
|
+
// `epics: []`, where MCEP/RCNB/VWSE surface it.
|
|
543
|
+
if (args.type === 'milestone' && spec?.id) {
|
|
544
|
+
payload.suggested_next_action = 'file_epic';
|
|
545
|
+
payload.suggested_next_action_args = { milestone_id: spec.id };
|
|
546
|
+
}
|
|
547
|
+
return {
|
|
548
|
+
content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
export function handlePlanReview(vaultRoot, args = {}) {
|
|
553
|
+
// Map the snake_case MCP args onto the executor's camelCase contract.
|
|
554
|
+
const result = runPlanReview({
|
|
555
|
+
vaultRoot,
|
|
556
|
+
planPath: typeof args.plan_path === 'string' ? args.plan_path : undefined,
|
|
557
|
+
planContent: typeof args.plan_content === 'string' ? args.plan_content : undefined,
|
|
558
|
+
scope: args.scope === 'breakdown' ? 'breakdown' : 'plan',
|
|
559
|
+
epicId: typeof args.epic_id === 'string' ? args.epic_id : undefined,
|
|
560
|
+
epicName: typeof args.epic_name === 'string' ? args.epic_name : undefined,
|
|
561
|
+
proposedTasks: Array.isArray(args.proposed_tasks) ? args.proposed_tasks : undefined,
|
|
562
|
+
qualityPass: args.quality_pass === true,
|
|
563
|
+
agentQualityFindings: Array.isArray(args.quality_findings) ? args.quality_findings : undefined,
|
|
564
|
+
});
|
|
565
|
+
return wrapDesignResult(result);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
export function handlePlanQc(vaultRoot, args = {}) {
|
|
569
|
+
const result = runPlanQc({
|
|
570
|
+
vaultRoot,
|
|
571
|
+
planPath: typeof args.plan_path === 'string' ? args.plan_path : undefined,
|
|
572
|
+
planContent: typeof args.plan_content === 'string' ? args.plan_content : undefined,
|
|
573
|
+
findingsSummary: typeof args.findings_summary === 'string' ? args.findings_summary : undefined,
|
|
574
|
+
axisBUserConfirmed: args.axis_b_user_confirmed === true,
|
|
575
|
+
axisBConfirmationNote:
|
|
576
|
+
typeof args.axis_b_confirmation_note === 'string' ? args.axis_b_confirmation_note : undefined,
|
|
577
|
+
});
|
|
578
|
+
return wrapDesignResult(result);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* `heyspex_push_plan`. `deps.writeFocusSelection` is the injection seam (idiom
|
|
583
|
+
* from fleet-queue-tools.mjs) so a test can drive the failed-pin path without a
|
|
584
|
+
* console; cli.mjs calls with two args and gets the real Neon writer.
|
|
585
|
+
*/
|
|
586
|
+
export async function handlePushPlan(vaultRoot, args, deps = {}) {
|
|
587
|
+
const { writeFocusSelection = writeFocusSelectionVendor } = deps;
|
|
588
|
+
// Cloud-aware entry ([[HS-BYU4-CWPX-TASK-promotion-writes-go-through-the-console]]
|
|
589
|
+
// sweep): resolves `resolveVaultWriteMode()` ONCE and either runs the
|
|
590
|
+
// untouched synchronous disk path (`local`) or files the epic through
|
|
591
|
+
// `POST /api/specs` (`cloud`). Awaiting is safe in both — the local branch
|
|
592
|
+
// still returns the same plain object it always did.
|
|
593
|
+
const result = await executePushPlanCloudAware(vaultRoot, args);
|
|
594
|
+
if (result.error) {
|
|
595
|
+
return {
|
|
596
|
+
isError: true,
|
|
597
|
+
content: [{ type: 'text', text: JSON.stringify(result) }],
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
// executePushPlan no longer writes focus itself (it is sync); do it here so the
|
|
601
|
+
// selection can reach Neon. Neon XOR file via `writeFocusSelectionVendor`.
|
|
602
|
+
//
|
|
603
|
+
// REPORT THE RESULT, NOT THE REQUEST ([[HS-IOJW-5YDH-DIAG]] sweep). The return
|
|
604
|
+
// boolean used to be discarded and `focus_written` came straight off the
|
|
605
|
+
// `on_deck` request flag, so a console that could not be reached — the one case
|
|
606
|
+
// `writeFocusSelectionVendor` is documented to fail in, with NO focus.yaml
|
|
607
|
+
// fallback — produced `success: true, focus_written: true` over a pin that was
|
|
608
|
+
// never stored, and sent the agent to /hs-next against nothing. The next
|
|
609
|
+
// session's entire run hangs off that pin.
|
|
610
|
+
if (result.on_deck && result.epic?.spec_path) {
|
|
611
|
+
const pinned = await writeFocusSelection(
|
|
612
|
+
vaultRoot,
|
|
613
|
+
result.epic.spec_path,
|
|
614
|
+
result.focus_reason,
|
|
615
|
+
);
|
|
616
|
+
result.focus_written = pinned === true;
|
|
617
|
+
if (!result.focus_written) {
|
|
618
|
+
// A failed pin is a LOUD FAILURE, never a field inside a success.
|
|
619
|
+
//
|
|
620
|
+
// It does NOT fail the whole push, and it must not: the epic markdown and
|
|
621
|
+
// the milestone mirror-update are already on disk and are correct. Erroring
|
|
622
|
+
// the push away would strand a filed epic behind a red tool call and invite
|
|
623
|
+
// a re-push that mints a SECOND epic for the same plan. So the filed facts
|
|
624
|
+
// are preserved verbatim in the payload (same shape as the `landingError`
|
|
625
|
+
// merge in handleFileSpec above) while `isError` + `success: false` stop any
|
|
626
|
+
// caller from reading this as a pinned epic. The only thing that failed is
|
|
627
|
+
// recoverable by re-running the pin — which is what `pin_recovery` says.
|
|
628
|
+
return {
|
|
629
|
+
isError: true,
|
|
630
|
+
content: [{
|
|
631
|
+
type: 'text',
|
|
632
|
+
text: JSON.stringify({
|
|
633
|
+
...result,
|
|
634
|
+
success: false,
|
|
635
|
+
error: 'FOCUS_PIN_NOT_WRITTEN',
|
|
636
|
+
epic_filed: true,
|
|
637
|
+
message:
|
|
638
|
+
`The epic WAS filed (${result.epic.spec_path}) but the on-deck focus pin was NOT written. ` +
|
|
639
|
+
'Neon is the only focus store (no .heyspex/focus.yaml fallback), so NOTHING is pinned — ' +
|
|
640
|
+
'do NOT run /hs-next expecting this epic, it will resolve to whatever was pinned before. ' +
|
|
641
|
+
'Do NOT re-run heyspex_push_plan: that would file a second epic for the same plan.',
|
|
642
|
+
pin_recovery: {
|
|
643
|
+
spec_path: result.epic.spec_path,
|
|
644
|
+
reason: result.focus_reason,
|
|
645
|
+
how: 'Check heyspex_dev_server_status, then re-pin: PUT /api/control/focus-selection ' +
|
|
646
|
+
'{ spec_path, kind: "epic", picked_by: "human", reason } — or re-run heyspex_push_plan ' +
|
|
647
|
+
'with on_deck once the console is reachable ONLY if the epic above was not filed.',
|
|
648
|
+
},
|
|
649
|
+
}, null, 2),
|
|
650
|
+
}],
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
return {
|
|
655
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
export async function handleGroundPlan(vaultRoot, args) {
|
|
660
|
+
// Resolve the Neon focus selection (layer-2 `spec_path`) at the async boundary
|
|
661
|
+
// and thread it into the sync grounding engine, so /hs-ground's focused-epic
|
|
662
|
+
// conflict scan reads the same Neon selection the console does — not the dormant
|
|
663
|
+
// file. `injectNeonFocusPin` adds `neon_selection_spec_path` (best-effort; an
|
|
664
|
+
// unreachable console / free-tier 402 leaves args untouched → file fallback).
|
|
665
|
+
// [[HS-TASK-FSEL]] residual / [[HS-TASK-FSKP]] session of [[HS-EPIC-FRTR]].
|
|
666
|
+
const resolvedArgs = await injectNeonFocusPin(args);
|
|
667
|
+
// Retrieval is remote and bounded since
|
|
668
|
+
// [[HS-YIR1-8KK5-TASK-planning-grounding-retrieves-through]] — the grounding
|
|
669
|
+
// engine issues `GET /api/search` calls of its own, so it is awaited here.
|
|
670
|
+
const result = await executeGroundPlan(vaultRoot, resolvedArgs);
|
|
671
|
+
if (result.error) {
|
|
672
|
+
return {
|
|
673
|
+
isError: true,
|
|
674
|
+
content: [{ type: 'text', text: JSON.stringify(result) }],
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
// Bounded report mode: emit the capped markdown directly so /hs-ground pastes it
|
|
678
|
+
// verbatim — no JSON envelope for the agent to unpack or over-read.
|
|
679
|
+
if (result._report) {
|
|
680
|
+
const footer = `\n\n---\n_grounded ${result.grounded_at} · ~${result.token_estimate}/${result.max_tokens} tok${result.truncated ? ' (truncated to fit)' : ''} · conflicts: ${result.conflict_count}${result.has_blocking_conflicts ? ' (BLOCKING — resolve first)' : ''}_`;
|
|
681
|
+
return {
|
|
682
|
+
content: [{ type: 'text', text: `${result.report_markdown}${footer}` }],
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
return {
|
|
686
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
687
|
+
};
|
|
688
|
+
}
|