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,325 @@
|
|
|
1
|
+
// Small cross-cutting helpers for tool handlers: spec-id guards, the design
|
|
2
|
+
// result wrapper, and the disk-landing measurement. 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 { statSync } from 'node:fs';
|
|
6
|
+
import { relative } from 'node:path';
|
|
7
|
+
import {
|
|
8
|
+
collectOccupiedFamilyKeysFromVault,
|
|
9
|
+
fileKeyFromBasename,
|
|
10
|
+
requireResolvableSpecId,
|
|
11
|
+
} from './spec-id-lib.mjs';
|
|
12
|
+
import { resolveSpecFileByFileKey } from './vault-io.mjs';
|
|
13
|
+
import { getConfig } from './mcp-config.mjs';
|
|
14
|
+
|
|
15
|
+
// Ambiguity guard for mutating ops ([[HS-EPIC-6SNQ-dual-read-resolver-grace-period-for-pipe]]
|
|
16
|
+
// AC#3). A bare family token (`HS-P2TR`) names a whole shared-suffix family, so a
|
|
17
|
+
// mutating op given one would act on an arbitrary family member.
|
|
18
|
+
//
|
|
19
|
+
// Era-3 escape ([[HS-0QVQ-RHIB-TASK]]): under era 3 a spec's `id:` IS a bare
|
|
20
|
+
// `HS-<fileKey>` token, which the shape check alone cannot tell apart from a
|
|
21
|
+
// family token. Before rejecting, try resolving the token as a FILE KEY — a
|
|
22
|
+
// unique hit that is NOT also an occupied family key names exactly one spec
|
|
23
|
+
// and is safe to act on. A token that matches BOTH namespaces (the
|
|
24
|
+
// [[HS-SJOT-AEP1-DIAG]] cross-namespace collision) fails closed, naming both
|
|
25
|
+
// interpretations — never silently pick one.
|
|
26
|
+
//
|
|
27
|
+
// Cloud reliance ([[HS-05ZW-XHR4-DIAG-the-capture-dropped-it-warning-blamed]]):
|
|
28
|
+
// that file-key escape is a LOCAL `readdirSync` scan, and a cloud-filed spec has
|
|
29
|
+
// no `.md` on this machine at all (`landed.onDisk: false`). So the scan could not
|
|
30
|
+
// tell "genuinely ambiguous" apart from "this machine has no copy", and every
|
|
31
|
+
// cloud-filed spec's own freshly minted id was refused by the very next tool
|
|
32
|
+
// called on it. `classifyBareSpecId` now separates those two verdicts, and the
|
|
33
|
+
// async `resolveSpecIdForMutation` asks the console before rejecting a token
|
|
34
|
+
// that is merely unresolvable FROM HERE.
|
|
35
|
+
|
|
36
|
+
/** How long the console disambiguation lookup may take before it is abandoned. */
|
|
37
|
+
export const SPEC_ID_CONSOLE_LOOKUP_TIMEOUT_MS = 8000;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Local-only verdict on a bare `HS-XXXX` spec id. One implementation shared by
|
|
41
|
+
* the synchronous guard and the async, console-aware resolver — never forked.
|
|
42
|
+
*
|
|
43
|
+
* @returns {{ kind: 'unambiguous' }
|
|
44
|
+
* | { kind: 'local_file_key', path: string }
|
|
45
|
+
* | { kind: 'collision', error: { error: string, message: string } }
|
|
46
|
+
* | { kind: 'local_family', error: { error: string, message: string } }
|
|
47
|
+
* | { kind: 'unresolved_locally', error: { error: string, message: string } }}
|
|
48
|
+
*/
|
|
49
|
+
export function classifyBareSpecId(vaultRoot, specId) {
|
|
50
|
+
const shapeError = requireResolvableSpecId(specId);
|
|
51
|
+
if (!shapeError) return { kind: 'unambiguous' };
|
|
52
|
+
|
|
53
|
+
const token = String(specId).trim().replace(/\.md$/i, '').replace(/^HS-/i, '').toUpperCase();
|
|
54
|
+
let byKey = null;
|
|
55
|
+
try {
|
|
56
|
+
byKey = resolveSpecFileByFileKey(vaultRoot, specId);
|
|
57
|
+
} catch {
|
|
58
|
+
byKey = null; // unreadable vault — the local scan could not answer
|
|
59
|
+
}
|
|
60
|
+
let isFamily = false;
|
|
61
|
+
try {
|
|
62
|
+
isFamily = collectOccupiedFamilyKeysFromVault(vaultRoot).has(token);
|
|
63
|
+
} catch {
|
|
64
|
+
isFamily = false;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (byKey && isFamily) {
|
|
68
|
+
return {
|
|
69
|
+
kind: 'collision',
|
|
70
|
+
error: {
|
|
71
|
+
error: 'AMBIGUOUS_SPEC_ID',
|
|
72
|
+
message:
|
|
73
|
+
`"${specId}" matches BOTH the unique file key of ${byKey} AND the family key of other ` +
|
|
74
|
+
`specs (cross-namespace collision) — refusing to guess. Pass the full basename of the ` +
|
|
75
|
+
`spec you mean.`,
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
if (byKey) return { kind: 'local_file_key', path: byKey }; // clean era-3 file-key hit
|
|
80
|
+
if (isFamily) {
|
|
81
|
+
// PROVEN a family key against this vault, so the strong wording is earned.
|
|
82
|
+
return {
|
|
83
|
+
kind: 'local_family',
|
|
84
|
+
error: requireResolvableSpecId(specId, { confirmedFamily: true }),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
// Neither namespace matched anything on disk. That is NOT proof of ambiguity —
|
|
88
|
+
// in cloud reliance it is the expected shape of a spec that lives only in Neon.
|
|
89
|
+
return { kind: 'unresolved_locally', error: shapeError };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Synchronous, local-only guard. Returns an error object to short-circuit on,
|
|
94
|
+
* or null when the id is safe to act on. Kept for callers (and tests) holding a
|
|
95
|
+
* vault root and no console; a tool handler should prefer
|
|
96
|
+
* {@link resolveSpecIdForMutation}, which additionally clears cloud-only ids.
|
|
97
|
+
*/
|
|
98
|
+
export function bareTokenSpecIdGuard(vaultRoot, specId) {
|
|
99
|
+
const verdict = classifyBareSpecId(vaultRoot, specId);
|
|
100
|
+
return verdict.error ?? null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function errorResponse(payload) {
|
|
104
|
+
return { isError: true, content: [{ type: 'text', text: JSON.stringify(payload) }] };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Local mirror of spec-id-lib's (unexported) id normalizer. */
|
|
108
|
+
function normalizeId(input) {
|
|
109
|
+
return String(input ?? '')
|
|
110
|
+
.trim()
|
|
111
|
+
.replace(/\.md$/i, '')
|
|
112
|
+
.toUpperCase();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** `{ error, message }` for a token nothing could verify, naming WHY not. */
|
|
116
|
+
function unverifiedResponse(specId, detail) {
|
|
117
|
+
return requireResolvableSpecId(specId, { detail });
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Lazily-bound console transport; injectable for tests, never imported eagerly. */
|
|
121
|
+
async function defaultConsoleFetch() {
|
|
122
|
+
const mod = await import('./console-transport.mjs');
|
|
123
|
+
return mod.consoleFetch;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Ask the console to resolve a bare token this machine cannot.
|
|
128
|
+
* `GET /api/specs/:id` resolves an era-3 file key server-side and answers with
|
|
129
|
+
* the full spec (including `basename`), or `404 { error: "ambiguous_id",
|
|
130
|
+
* candidates }` when the token really does name more than one spec.
|
|
131
|
+
*
|
|
132
|
+
* Never throws and never hangs: the transport carries an explicit timeout and
|
|
133
|
+
* every failure degrades to a rejection saying the id could not be VERIFIED
|
|
134
|
+
* from here. Silence is never upgraded into a claim about family keys.
|
|
135
|
+
*/
|
|
136
|
+
async function resolveBareSpecIdViaConsole(specId, deps = {}) {
|
|
137
|
+
let fetcher = deps.consoleFetch;
|
|
138
|
+
if (!fetcher) {
|
|
139
|
+
try {
|
|
140
|
+
fetcher = await defaultConsoleFetch();
|
|
141
|
+
} catch {
|
|
142
|
+
return {
|
|
143
|
+
error: unverifiedResponse(
|
|
144
|
+
specId,
|
|
145
|
+
'The console transport is unavailable in this process, so nothing was checked against the vault of record.',
|
|
146
|
+
),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
let res;
|
|
152
|
+
try {
|
|
153
|
+
res = await fetcher(`/api/specs/${encodeURIComponent(specId)}`, {
|
|
154
|
+
timeout: deps.timeoutMs ?? SPEC_ID_CONSOLE_LOOKUP_TIMEOUT_MS,
|
|
155
|
+
});
|
|
156
|
+
} catch (err) {
|
|
157
|
+
const why = err?.message ? String(err.message) : String(err);
|
|
158
|
+
return {
|
|
159
|
+
error: unverifiedResponse(
|
|
160
|
+
specId,
|
|
161
|
+
`The console lookup that would have resolved it failed (${why}).`,
|
|
162
|
+
),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const body = res?.body ?? null;
|
|
167
|
+
|
|
168
|
+
if (res?.ok) {
|
|
169
|
+
const spec = body?.spec ?? null;
|
|
170
|
+
const basename =
|
|
171
|
+
typeof spec?.basename === 'string' && spec.basename.trim() ? spec.basename.trim() : null;
|
|
172
|
+
if (!basename) {
|
|
173
|
+
return {
|
|
174
|
+
error: unverifiedResponse(
|
|
175
|
+
specId,
|
|
176
|
+
'The console answered without a basename, so the id could not be pinned to one spec.',
|
|
177
|
+
),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
// Do NOT trust a resolution that does not carry the token asked about. A
|
|
181
|
+
// live `heyspex_register_spec` dry run once resolved one spec's basename to
|
|
182
|
+
// an UNRELATED spec ([[HS-J6IE]]); this gate is why a server-side fuzzy
|
|
183
|
+
// match can never do that here.
|
|
184
|
+
const resolvedKey = fileKeyFromBasename(basename);
|
|
185
|
+
const token = String(specId).trim().replace(/^HS-/i, '').toUpperCase();
|
|
186
|
+
const idMatches = normalizeId(spec?.id ?? '') === normalizeId(specId);
|
|
187
|
+
if (resolvedKey?.toUpperCase() !== token && !idMatches) {
|
|
188
|
+
return {
|
|
189
|
+
error: {
|
|
190
|
+
error: 'AMBIGUOUS_SPEC_ID',
|
|
191
|
+
message:
|
|
192
|
+
`"${specId}" was answered by the console with "${basename}", whose file key does not ` +
|
|
193
|
+
`match the token asked about — refusing to act on a resolution that changed the id. ` +
|
|
194
|
+
`Pass the full basename of the spec you mean.`,
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
return { basename };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (body?.error === 'ambiguous_id') {
|
|
202
|
+
const candidates = Array.isArray(body.candidates) ? body.candidates : [];
|
|
203
|
+
return {
|
|
204
|
+
error: {
|
|
205
|
+
error: 'AMBIGUOUS_SPEC_ID',
|
|
206
|
+
message:
|
|
207
|
+
`"${specId}" names ${candidates.length || 'more than one'} specs in the vault of record` +
|
|
208
|
+
(candidates.length ? ` (${candidates.join(', ')})` : '') +
|
|
209
|
+
` — refusing to guess. Pass the full basename of the spec you mean.`,
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (res?.status === 404) {
|
|
215
|
+
return {
|
|
216
|
+
error: unverifiedResponse(
|
|
217
|
+
specId,
|
|
218
|
+
'The vault of record has no spec under that id either (console answered 404).',
|
|
219
|
+
),
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const status =
|
|
224
|
+
res?.authProblem
|
|
225
|
+
|| (res?.status === 0 || res?.status == null ? 'unreachable' : `HTTP ${res.status}`);
|
|
226
|
+
return {
|
|
227
|
+
error: unverifiedResponse(
|
|
228
|
+
specId,
|
|
229
|
+
`The console could not be asked (${status}), so nothing about this id was actually verified.`,
|
|
230
|
+
),
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* THE shared entry point for every mutating tool handler that takes a
|
|
236
|
+
* `spec_id`. Resolves the id to something unambiguous, or returns the MCP error
|
|
237
|
+
* response to short-circuit on.
|
|
238
|
+
*
|
|
239
|
+
* - full basename / already-unambiguous id → passes straight through
|
|
240
|
+
* - bare token the LOCAL vault resolves to one file → passes through
|
|
241
|
+
* - bare token only the CONSOLE can resolve → cleared, and rewritten to the
|
|
242
|
+
* full basename so the caller acts on an unambiguous id
|
|
243
|
+
* - genuinely ambiguous (either namespace, locally or server-side) → refused
|
|
244
|
+
* - console unreachable / not signed in / 404 → refused, saying plainly that
|
|
245
|
+
* the id could not be verified from this machine and why
|
|
246
|
+
*
|
|
247
|
+
* The console is consulted ONLY when the local scan came back empty, so the
|
|
248
|
+
* happy path costs exactly what it did before.
|
|
249
|
+
*
|
|
250
|
+
* `deps` is the test seam and nothing else: `{ vaultRoot }` overrides the
|
|
251
|
+
* resolved config, `{ consoleFetch }` replaces the transport (so no suite ever
|
|
252
|
+
* touches the live network), `{ timeoutMs }` shortens the lookup budget.
|
|
253
|
+
*
|
|
254
|
+
* @returns {Promise<{ specId: string, resolvedVia?: 'console' } | { response: object }>}
|
|
255
|
+
*/
|
|
256
|
+
export async function resolveSpecIdForMutation(specId, deps = {}) {
|
|
257
|
+
let verdict;
|
|
258
|
+
try {
|
|
259
|
+
verdict = classifyBareSpecId(deps.vaultRoot ?? getConfig().vaultRoot, specId);
|
|
260
|
+
} catch {
|
|
261
|
+
// Config unavailable — no local scan is possible, which is exactly the
|
|
262
|
+
// "could not answer from here" case, not proof of ambiguity.
|
|
263
|
+
const shapeError = requireResolvableSpecId(specId);
|
|
264
|
+
verdict = shapeError
|
|
265
|
+
? { kind: 'unresolved_locally', error: shapeError }
|
|
266
|
+
: { kind: 'unambiguous' };
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (verdict.kind === 'unambiguous' || verdict.kind === 'local_file_key') {
|
|
270
|
+
return { specId: typeof specId === 'string' ? specId.trim() : specId };
|
|
271
|
+
}
|
|
272
|
+
if (verdict.kind !== 'unresolved_locally') return { response: errorResponse(verdict.error) };
|
|
273
|
+
|
|
274
|
+
const viaConsole = await resolveBareSpecIdViaConsole(specId, deps);
|
|
275
|
+
if (viaConsole.basename) return { specId: viaConsole.basename, resolvedVia: 'console' };
|
|
276
|
+
return { response: errorResponse(viaConsole.error ?? verdict.error) };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Back-compat face of {@link resolveSpecIdForMutation} for handlers that need
|
|
281
|
+
* only the veto and not the resolved id. Same single implementation.
|
|
282
|
+
*/
|
|
283
|
+
export async function ambiguousSpecIdResponse(specId, deps = {}) {
|
|
284
|
+
const resolved = await resolveSpecIdForMutation(specId, deps);
|
|
285
|
+
return resolved.response ?? null;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Measure a direct-to-disk vault write ([[HS-IOJW-5YDH-DIAG]] sweep). Tools that
|
|
290
|
+
* call writeFileSync themselves cannot land in the cloud, but they still
|
|
291
|
+
* reported on the request rather than the result — this stats the file so the
|
|
292
|
+
* `landed` block means the bytes are really there. Never throws.
|
|
293
|
+
*/
|
|
294
|
+
export function measureDiskLanding(vaultRoot, absPath) {
|
|
295
|
+
let onDisk = false;
|
|
296
|
+
let bytes = null;
|
|
297
|
+
try {
|
|
298
|
+
const st = statSync(absPath);
|
|
299
|
+
onDisk = st.isFile();
|
|
300
|
+
bytes = onDisk ? st.size : null;
|
|
301
|
+
} catch {
|
|
302
|
+
onDisk = false;
|
|
303
|
+
bytes = null;
|
|
304
|
+
}
|
|
305
|
+
return {
|
|
306
|
+
tier: 'disk',
|
|
307
|
+
reported_by: 'measured',
|
|
308
|
+
abs_path: absPath,
|
|
309
|
+
rel_path: vaultRoot ? relative(vaultRoot, absPath) : null,
|
|
310
|
+
on_disk: onDisk,
|
|
311
|
+
bytes,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export function wrapDesignResult(result) {
|
|
316
|
+
if (result && result.error) {
|
|
317
|
+
return {
|
|
318
|
+
isError: true,
|
|
319
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
return {
|
|
323
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
324
|
+
};
|
|
325
|
+
}
|