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
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* `/api/specs/:id/status` is SSOT (coercion, rollup, spec events). [[HS-DIAG-DOSH]]
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { parseAcceptanceSubsections } from './alignment-gate.mjs';
|
|
8
|
+
import { extractSection, hasHeading, parseAcceptanceSubsections } from './alignment-gate.mjs';
|
|
9
|
+
import { gatherCloseBranchReport } from './close-branch-report.mjs';
|
|
9
10
|
import { readMvpModeState } from './mvp-mode.mjs';
|
|
10
11
|
import { isBelowReviewLevel, reviewLevelForGrain } from './review-rollup.mjs';
|
|
11
12
|
|
|
@@ -18,12 +19,87 @@ export function resolveActiveReviewLevel(vaultRoot) {
|
|
|
18
19
|
return reviewLevelForGrain(mvp?.review_grain);
|
|
19
20
|
}
|
|
20
21
|
|
|
22
|
+
const REVIEW_LEVELS = new Set(['task', 'epic', 'milestone']);
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Reconcile the grain this machine read with the grain the console reports it
|
|
26
|
+
* gated under ([[HS-A53F]]). The console's `review_level` wins when present —
|
|
27
|
+
* the status route decides the coercion, so its grain is the one that produced
|
|
28
|
+
* `effective_status`; the client's own read is kept beside it as
|
|
29
|
+
* `client_review_level` and the two are compared explicitly. An older console
|
|
30
|
+
* that reports no grain leaves the client's value in place (`source: client`).
|
|
31
|
+
*/
|
|
32
|
+
export function reviewLevelReport(clientReviewLevel, consoleBody) {
|
|
33
|
+
const serverLevel = consoleBody?.review_level;
|
|
34
|
+
const hasServer = typeof serverLevel === 'string' && REVIEW_LEVELS.has(serverLevel);
|
|
35
|
+
const reviewLevel = hasServer ? serverLevel : clientReviewLevel;
|
|
36
|
+
return {
|
|
37
|
+
review_level: reviewLevel,
|
|
38
|
+
review_level_source: hasServer ? (consoleBody?.review_level_source ?? 'server') : 'client',
|
|
39
|
+
client_review_level: clientReviewLevel,
|
|
40
|
+
review_level_mismatch: hasServer && serverLevel !== clientReviewLevel,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
21
44
|
/** Count acceptance criteria the same way the close-out gate expects (flat bullets). */
|
|
22
45
|
export function countAcceptanceCriteria(body) {
|
|
23
46
|
const { sections } = parseAcceptanceSubsections(body ?? '');
|
|
24
47
|
return sections.reduce((n, s) => n + (s.bullets?.length ?? 0), 0);
|
|
25
48
|
}
|
|
26
49
|
|
|
50
|
+
// Section anchors, mirrored from `parseAcceptanceSubsections` (alignment-gate.mjs)
|
|
51
|
+
// so the checkbox reader below looks at EXACTLY the criteria that function counts.
|
|
52
|
+
// The regexes are duplicated rather than imported because they are module-private
|
|
53
|
+
// there; `submit-for-review.test.mjs` pins the two against each other over the
|
|
54
|
+
// shared fixtures, so a drift is a red test rather than a silent disagreement.
|
|
55
|
+
const ACCEPTANCE_HEADING_TITLE = /^acceptance(?:\s+criteria)?$/i;
|
|
56
|
+
const ACCEPTANCE_HEADING_TITLE_ANY = /^acceptance/i;
|
|
57
|
+
const DONE_WHEN_HEADING_TITLE_ALIAS = /^done\s+when\b/i;
|
|
58
|
+
// Group 1 is the checkbox content when the item renders one; undefined otherwise.
|
|
59
|
+
const CHECKBOX_LIST_ITEM = /^\s*(?:[-*]\s+(?:\[([ xX])\]\s+)?|\d+[.)]\s+)(.+?)\s*$/;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Read the acceptance criteria's rendered checkboxes as REAL STATE
|
|
63
|
+
* ([[HS-LBNJ-6AT2-DIAG]] AC3).
|
|
64
|
+
*
|
|
65
|
+
* Before this, `[ ]` and `[x]` were matched and stripped identically by every
|
|
66
|
+
* parser on both runtimes — only a COUNT survived — so "progress" was always the
|
|
67
|
+
* agent's self-reported `acProgress` integer and never the document. A spec could
|
|
68
|
+
* render every box unchecked and still pass the AC gate. A rendered `- [ ]` that
|
|
69
|
+
* no code reads is worse than no checkbox at all, because every human and agent
|
|
70
|
+
* reads it as a verification signal.
|
|
71
|
+
*
|
|
72
|
+
* Returns `{ present, total, checked, unchecked }` where `unchecked` lists the
|
|
73
|
+
* 1-based positions still showing `[ ]`. `present: false` (no criterion renders a
|
|
74
|
+
* box — the 241 specs using numbered criteria) means the caller keeps its prior
|
|
75
|
+
* behavior exactly: this is additive, not a new requirement on every spec.
|
|
76
|
+
*/
|
|
77
|
+
export function readAcceptanceCheckboxes(body) {
|
|
78
|
+
const text = body ?? '';
|
|
79
|
+
let section = extractSection(text, ACCEPTANCE_HEADING_TITLE);
|
|
80
|
+
if (!section && !hasHeading(text, ACCEPTANCE_HEADING_TITLE_ANY)) {
|
|
81
|
+
section = extractSection(text, DONE_WHEN_HEADING_TITLE_ALIAS);
|
|
82
|
+
}
|
|
83
|
+
if (!section) return { present: false, total: 0, checked: 0, unchecked: [] };
|
|
84
|
+
|
|
85
|
+
let index = 0;
|
|
86
|
+
let total = 0;
|
|
87
|
+
let checked = 0;
|
|
88
|
+
const unchecked = [];
|
|
89
|
+
for (const line of section.split(/\r?\n/)) {
|
|
90
|
+
const m = line.match(CHECKBOX_LIST_ITEM);
|
|
91
|
+
if (!m || !m[2].trim()) continue;
|
|
92
|
+
index += 1;
|
|
93
|
+
// An item with no checkbox makes no claim either way — it can neither
|
|
94
|
+
// satisfy nor block the gate, so it is counted as a criterion but not as a box.
|
|
95
|
+
if (m[1] === undefined) continue;
|
|
96
|
+
total += 1;
|
|
97
|
+
if (m[1].toLowerCase() === 'x') checked += 1;
|
|
98
|
+
else unchecked.push(index);
|
|
99
|
+
}
|
|
100
|
+
return { present: total > 0, total, checked, unchecked };
|
|
101
|
+
}
|
|
102
|
+
|
|
27
103
|
/**
|
|
28
104
|
* Post-close vault steps still required after the status API flip (HS-REV vs seed).
|
|
29
105
|
*/
|
|
@@ -67,7 +143,13 @@ function findClaimAcProgress(claimsBody, specId) {
|
|
|
67
143
|
/**
|
|
68
144
|
* @param {{ vaultRoot: string, consoleFetch: Function, postClaimEndpoint?: Function, args?: Record<string, unknown> }} ctx
|
|
69
145
|
*/
|
|
70
|
-
export async function executeSubmitForReview({
|
|
146
|
+
export async function executeSubmitForReview({
|
|
147
|
+
vaultRoot,
|
|
148
|
+
consoleFetch,
|
|
149
|
+
postClaimEndpoint,
|
|
150
|
+
args = {},
|
|
151
|
+
gatherBranchReport = gatherCloseBranchReport,
|
|
152
|
+
}) {
|
|
71
153
|
const specId = typeof args.spec_id === 'string' ? args.spec_id.trim() : '';
|
|
72
154
|
if (!specId) {
|
|
73
155
|
return { error: 'INVALID_ARGS', message: 'spec_id is required' };
|
|
@@ -75,6 +157,12 @@ export async function executeSubmitForReview({ vaultRoot, consoleFetch, postClai
|
|
|
75
157
|
|
|
76
158
|
const skipAcCheck = args.skip_ac_check === true;
|
|
77
159
|
const releaseClaim = args.release_claim !== false;
|
|
160
|
+
// Historical token calibration (HS-HBD2): forward the caller's real
|
|
161
|
+
// context-window spend, when known, so the console pairs it with the
|
|
162
|
+
// pre-build estimate on the build-complete spec event. Only a genuine
|
|
163
|
+
// finite number is forwarded — the console PATCH route is the source of
|
|
164
|
+
// truth for range validation (>= 0), this just avoids sending junk types.
|
|
165
|
+
const hasActualTokens = typeof args.actual_tokens === 'number' && Number.isFinite(args.actual_tokens);
|
|
78
166
|
|
|
79
167
|
const specRes = await consoleFetch(`/api/specs/${encodeURIComponent(specId)}`);
|
|
80
168
|
if (!specRes.ok) {
|
|
@@ -115,8 +203,28 @@ export async function executeSubmitForReview({ vaultRoot, consoleFetch, postClai
|
|
|
115
203
|
claimInfo = findClaimAcProgress(claimsRes.body, specId);
|
|
116
204
|
}
|
|
117
205
|
|
|
206
|
+
const checkboxes = readAcceptanceCheckboxes(spec.body);
|
|
207
|
+
|
|
118
208
|
if (!skipAcCheck && criteriaCount > 0) {
|
|
119
209
|
const progress = claimInfo.found ? claimInfo.acProgress : undefined;
|
|
210
|
+
// Checkbox-authoritative ([[HS-LBNJ-6AT2-DIAG]] AC3): where the spec renders
|
|
211
|
+
// criteria checkboxes, THEY are the state — an unchecked box refuses the
|
|
212
|
+
// hand-off no matter what the self-reported integer says. Where it renders
|
|
213
|
+
// none, the acProgress rule below is unchanged.
|
|
214
|
+
if (checkboxes.present && checkboxes.unchecked.length > 0) {
|
|
215
|
+
return {
|
|
216
|
+
error: 'AC_CHECKBOXES_UNCHECKED',
|
|
217
|
+
message:
|
|
218
|
+
`Acceptance criteria ${checkboxes.unchecked.join(', ')} are still rendered unchecked ` +
|
|
219
|
+
`(${checkboxes.checked} of ${checkboxes.total} boxes ticked in ${specId}). ` +
|
|
220
|
+
`Tick the boxes you actually verified — the document is the record, not the progress count — ` +
|
|
221
|
+
`or pass skip_ac_check: true with a reason in notes.`,
|
|
222
|
+
criteria_count: criteriaCount,
|
|
223
|
+
ac_progress: progress ?? null,
|
|
224
|
+
checkboxes,
|
|
225
|
+
claim_found: claimInfo.found,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
120
228
|
if (progress === undefined || progress < criteriaCount) {
|
|
121
229
|
return {
|
|
122
230
|
error: 'AC_PROGRESS_INCOMPLETE',
|
|
@@ -124,6 +232,7 @@ export async function executeSubmitForReview({ vaultRoot, consoleFetch, postClai
|
|
|
124
232
|
`Acceptance criteria progress is ${progress ?? 'unknown'} of ${criteriaCount}. Call heyspex_report_progress after each criterion, or pass skip_ac_check: true with a reason in notes.`,
|
|
125
233
|
criteria_count: criteriaCount,
|
|
126
234
|
ac_progress: progress ?? null,
|
|
235
|
+
checkboxes,
|
|
127
236
|
claim_found: claimInfo.found,
|
|
128
237
|
};
|
|
129
238
|
}
|
|
@@ -140,16 +249,32 @@ export async function executeSubmitForReview({ vaultRoot, consoleFetch, postClai
|
|
|
140
249
|
|
|
141
250
|
const statusRes = await consoleFetch(`/api/specs/${encodeURIComponent(specId)}/status`, {
|
|
142
251
|
method: 'PATCH',
|
|
143
|
-
body: {
|
|
252
|
+
body: {
|
|
253
|
+
status: 'in_review',
|
|
254
|
+
...(hasActualTokens ? { actual_tokens: args.actual_tokens } : {}),
|
|
255
|
+
},
|
|
144
256
|
});
|
|
145
257
|
|
|
146
258
|
if (!statusRes.ok) {
|
|
259
|
+
// The server names the grain it gated under ([[HS-A53F]]); put it beside the
|
|
260
|
+
// grain THIS machine read so a 409 under a disagreeing grain is legible as a
|
|
261
|
+
// resolver mismatch, not as "the agent tried to close".
|
|
262
|
+
const grainReport = reviewLevelReport(reviewLevel, statusRes.body);
|
|
147
263
|
return {
|
|
148
264
|
error: 'STATUS_PATCH_FAILED',
|
|
149
265
|
message: `PATCH /api/specs/${specId}/status returned ${statusRes.status}`,
|
|
150
266
|
status: statusRes.status,
|
|
151
267
|
console_response: statusRes.body,
|
|
152
268
|
attempted: { requested_status: 'in_review' },
|
|
269
|
+
...grainReport,
|
|
270
|
+
...(grainReport.review_level_mismatch
|
|
271
|
+
? {
|
|
272
|
+
hint:
|
|
273
|
+
`This machine's config.yaml says review_grain "${reviewLevel}" but the console ` +
|
|
274
|
+
`decided "${grainReport.review_level}" (source: ${grainReport.review_level_source}). ` +
|
|
275
|
+
`The server is authoritative; align the cloud setting via PUT /api/mvp-mode { review_grain }.`,
|
|
276
|
+
}
|
|
277
|
+
: {}),
|
|
153
278
|
};
|
|
154
279
|
}
|
|
155
280
|
|
|
@@ -158,6 +283,11 @@ export async function executeSubmitForReview({ vaultRoot, consoleFetch, postClai
|
|
|
158
283
|
transition.spec?.frontmatter?.status ?? transition.spec?.status ?? 'in_review',
|
|
159
284
|
).toLowerCase();
|
|
160
285
|
const coercedTo = transition.coerced_to ?? transition.coercedTo ?? null;
|
|
286
|
+
const grainReport = reviewLevelReport(reviewLevel, transition);
|
|
287
|
+
// The SERVER's grain drives the close-out guidance when it was reported — the
|
|
288
|
+
// server is what actually coerced (or did not coerce) the status.
|
|
289
|
+
const effectiveReviewLevel = grainReport.review_level;
|
|
290
|
+
const effectiveIsReviewUnit = !isBelowReviewLevel(specKind, effectiveReviewLevel);
|
|
161
291
|
|
|
162
292
|
let claimReleased;
|
|
163
293
|
if (releaseClaim && typeof postClaimEndpoint === 'function') {
|
|
@@ -167,20 +297,33 @@ export async function executeSubmitForReview({ vaultRoot, consoleFetch, postClai
|
|
|
167
297
|
|
|
168
298
|
const guidance = buildCloseOutGuidance({
|
|
169
299
|
specKind,
|
|
170
|
-
reviewLevel,
|
|
300
|
+
reviewLevel: effectiveReviewLevel,
|
|
171
301
|
effectiveStatus,
|
|
172
|
-
isReviewUnit,
|
|
302
|
+
isReviewUnit: effectiveIsReviewUnit,
|
|
173
303
|
});
|
|
174
304
|
|
|
305
|
+
// Name the branch each half of the session landed on, and whether they
|
|
306
|
+
// match ([[HS-PPX7-CWPX-TASK-session-close-names-the-branch-each]]).
|
|
307
|
+
// Best-effort: a failure here never blocks the close-out.
|
|
308
|
+
try {
|
|
309
|
+
const report = await gatherBranchReport({ consoleFetch });
|
|
310
|
+
guidance.branch_report = report;
|
|
311
|
+
} catch {
|
|
312
|
+
guidance.branch_report = { status: 'unknown', matches: false, lines: ['Branch report unavailable.'] };
|
|
313
|
+
}
|
|
314
|
+
|
|
175
315
|
return {
|
|
176
316
|
spec_id: specId,
|
|
177
317
|
requested_status: 'in_review',
|
|
178
318
|
effective_status: effectiveStatus,
|
|
179
319
|
coerced_to: coercedTo,
|
|
180
|
-
|
|
181
|
-
is_review_unit:
|
|
320
|
+
...grainReport,
|
|
321
|
+
is_review_unit: effectiveIsReviewUnit,
|
|
182
322
|
criteria_count: criteriaCount,
|
|
183
323
|
ac_progress: claimInfo.acProgress ?? null,
|
|
324
|
+
// Reported on success too, so a caller can see WHICH signal cleared the gate
|
|
325
|
+
// — the document's boxes or the unverified self-report count.
|
|
326
|
+
checkboxes,
|
|
184
327
|
rolled_up: transition.rolledUp ?? transition.rolled_up ?? undefined,
|
|
185
328
|
propagated: transition.propagated ?? undefined,
|
|
186
329
|
claim_released: claimReleased,
|
|
@@ -160,3 +160,108 @@ ${readStep}
|
|
|
160
160
|
|
|
161
161
|
Make only this one change; ask only if the spec body is too thin to summarize honestly.`;
|
|
162
162
|
}
|
|
163
|
+
|
|
164
|
+
/** Target lengths from the contract above — soft, surfaced as warnings by the writer. */
|
|
165
|
+
export const SUMMARY_OPENING_SENTENCE_TARGET_CHARS = 160;
|
|
166
|
+
export const SUMMARY_TOTAL_TARGET_CHARS = 400;
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Validate an authored `summary` value BEFORE it is written anywhere (the
|
|
170
|
+
* console row via `heyspex_author_summary`, or a vault file). One validator
|
|
171
|
+
* for both destinations so the cloud write is held to exactly the rules the
|
|
172
|
+
* on-disk `summary: |` block is. Pure.
|
|
173
|
+
*
|
|
174
|
+
* Hard failures (nothing is written): not a string, empty after trim, a
|
|
175
|
+
* pasted YAML field (`summary: |` / `summary:` prefix — the writer adds the
|
|
176
|
+
* field itself), or a frontmatter fence. Soft findings come back as
|
|
177
|
+
* `warnings` — the contract's ≤160 / ≤400 targets are targets, not gates.
|
|
178
|
+
*
|
|
179
|
+
* @param {unknown} raw
|
|
180
|
+
* @returns {{ ok: true, summary: string, warnings: string[] } | { ok: false, error: string, message: string }}
|
|
181
|
+
*/
|
|
182
|
+
export function validateAuthoredSummary(raw) {
|
|
183
|
+
if (typeof raw !== 'string') {
|
|
184
|
+
return { ok: false, error: 'INVALID_SUMMARY', message: '`summary` must be a string of plain-language prose.' };
|
|
185
|
+
}
|
|
186
|
+
const normalized = raw.replace(/\r\n?/g, '\n').trim();
|
|
187
|
+
if (!normalized) {
|
|
188
|
+
return { ok: false, error: 'INVALID_SUMMARY', message: '`summary` is empty — author the plain-language summary per spec-summary-authoring, then pass it.' };
|
|
189
|
+
}
|
|
190
|
+
if (/^summary\s*:/i.test(normalized)) {
|
|
191
|
+
return {
|
|
192
|
+
ok: false,
|
|
193
|
+
error: 'INVALID_SUMMARY',
|
|
194
|
+
message: 'Pass the summary PROSE only — no `summary:` / `summary: |` YAML prefix. The writer stores it as the frontmatter field itself.',
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
if (/^---\s*$/m.test(normalized)) {
|
|
198
|
+
return { ok: false, error: 'INVALID_SUMMARY', message: '`summary` must not contain a frontmatter fence (`---`).' };
|
|
199
|
+
}
|
|
200
|
+
const warnings = [];
|
|
201
|
+
const opening = (normalized.match(/^[\s\S]*?[.!?](?=\s|$)/) || [normalized])[0].trim();
|
|
202
|
+
if (opening.length > SUMMARY_OPENING_SENTENCE_TARGET_CHARS) {
|
|
203
|
+
warnings.push(
|
|
204
|
+
`opening sentence is ${opening.length} chars (target ≤${SUMMARY_OPENING_SENTENCE_TARGET_CHARS}) — list rows and cards render only this sentence.`,
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
if (normalized.length > SUMMARY_TOTAL_TARGET_CHARS) {
|
|
208
|
+
warnings.push(`summary is ${normalized.length} chars (target ≤${SUMMARY_TOTAL_TARGET_CHARS} total).`);
|
|
209
|
+
}
|
|
210
|
+
return { ok: true, summary: normalized, warnings };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Cloud-reliance twin of {@link buildSummaryFillInPrompt}
|
|
215
|
+
* ([[HS-T41O-NZ8S-DIAG]]): when the console's Neon row is the source of record
|
|
216
|
+
* (prod / cloud dev-mode reliance, or a spec that exists only in Neon and not
|
|
217
|
+
* in this machine's vault clone), the agent must NOT edit a vault file — the
|
|
218
|
+
* console never reads it back. This prompt names the console write instead:
|
|
219
|
+
* `heyspex_author_summary { spec_id, summary }` → `PUT /api/specs/:id`.
|
|
220
|
+
* It deliberately carries NO on-disk path and no "open the file" step.
|
|
221
|
+
*
|
|
222
|
+
* @param {{ specId: string, name: string, type: string,
|
|
223
|
+
* parent?: { id: string, kind?: string, title?: string, goal?: string|null } | null,
|
|
224
|
+
* consoleUrl?: string|null }} input
|
|
225
|
+
* @returns {string}
|
|
226
|
+
*/
|
|
227
|
+
export function buildSummaryConsoleWritePrompt({ specId, name, type, parent = null, consoleUrl = null }) {
|
|
228
|
+
const parentKind = (parent && parent.kind) || 'parent';
|
|
229
|
+
const parentBlock =
|
|
230
|
+
parent && parent.id
|
|
231
|
+
? `
|
|
232
|
+
|
|
233
|
+
## Parent context
|
|
234
|
+
This ${type} rolls up to ${parentKind} **${parent.id}**${parent.title ? ` — ${parent.title}` : ''}.
|
|
235
|
+
${
|
|
236
|
+
parent.goal && String(parent.goal).trim()
|
|
237
|
+
? `Its goal, in the parent's own words:\n> ${String(parent.goal).replace(/\s+/g, ' ').trim()}`
|
|
238
|
+
: `The parent has no \`summary:\` yet — read its body from the console (\`GET /api/specs/${parent.id}\`, or \`heyspex_next_on_deck { spec_id: "${parent.id}", summary: true }\`) to state the parent's goal in plain words.`
|
|
239
|
+
}
|
|
240
|
+
Your opening sentence MUST fuse that goal with what THIS ${type} delivers toward it, in whichever order reads naturally when said out loud (no rigid "So that …, this …" template, no long purpose clause stacked in front of the subject). Keep it plain enough for a twelve-year-old who's never heard of this project.`
|
|
241
|
+
: '';
|
|
242
|
+
const where = consoleUrl ? ` (${consoleUrl})` : '';
|
|
243
|
+
const toolArgs = `{ spec_id: "${specId}", summary: "<your summary>" }`;
|
|
244
|
+
return `You are a coding agent working in the HeySpex workflow. This session is CLOUD-reliant: the console's record for the spec${where} is the source of record. Do NOT edit a vault file on disk for this — your clone may not hold the spec, and the console never reads that file back.
|
|
245
|
+
|
|
246
|
+
## Goal
|
|
247
|
+
The spec **${specId}** (${name}) is missing a \`summary:\` field. Author it, then write it through the console.
|
|
248
|
+
|
|
249
|
+
## Target spec
|
|
250
|
+
- **Spec ID:** ${specId}
|
|
251
|
+
- **Title:** ${name}
|
|
252
|
+
- **Entity type:** ${type}
|
|
253
|
+
- **Source of record:** the console row (\`GET /api/specs/${specId}\`) — not a file
|
|
254
|
+
- **Missing field:** \`summary\`${parentBlock}
|
|
255
|
+
|
|
256
|
+
## What good looks like
|
|
257
|
+
${SUMMARY_CONTRACT}
|
|
258
|
+
- For the console write, pass the PROSE only as the \`summary\` argument — no \`summary: |\` YAML wrapper; the console stores it as the frontmatter field.
|
|
259
|
+
|
|
260
|
+
## What to do
|
|
261
|
+
1. Read the spec's scope from the brief above (or \`GET /api/specs/${specId}\` on the console)${parent && parent.id ? ', and review the parent context above so your opening sentence carries the parent\'s goal' : ''}.
|
|
262
|
+
2. Author a \`summary\` that satisfies the contract above.
|
|
263
|
+
3. Write it through the console: \`heyspex_load_tools { names: ["heyspex_author_summary"] }\` (it is a deferred tool), then \`heyspex_author_summary ${toolArgs}\`. CLI fallback: \`hs mcp call heyspex_author_summary --args '{"spec_id":"${specId}","summary":"<your summary>"}' --json\`. The tool changes ONLY \`summary\` (\`PUT /api/specs/:id\` with the row's other frontmatter and body preserved).
|
|
264
|
+
4. Re-run \`heyspex_next_on_deck { summary: true }\` — the brief now shows the summary and asks to claim.
|
|
265
|
+
|
|
266
|
+
Make only this one change; ask only if the spec body is too thin to summarize honestly.`;
|
|
267
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `heyspex_author_summary` — write a spec's plain-language `summary:` field
|
|
3
|
+
* where the session's source of record actually lives ([[HS-T41O-NZ8S-DIAG]]).
|
|
4
|
+
*
|
|
5
|
+
* The `/hs-next` brief's `author_summary_then_claim` phase used to tell every
|
|
6
|
+
* agent to `Edit` the vault file on disk. In cloud reliance (prod, or dev-mode
|
|
7
|
+
* `reliance: cloud`) the console's Neon row is the source of record: a spec
|
|
8
|
+
* filed there may not exist in this machine's clone at all, and a file edit is
|
|
9
|
+
* never read back. This tool is the write the brief now names there:
|
|
10
|
+
*
|
|
11
|
+
* - `cloud` write mode — `GET /api/specs/:id` for the row's current
|
|
12
|
+
* frontmatter + body, set `summary`, `PUT /api/specs/:id` through
|
|
13
|
+
* {@link updateSpecViaConsole}. Nothing touches this machine's disk.
|
|
14
|
+
* - `local` write mode — the vault file on disk is the source of record, so
|
|
15
|
+
* the `summary: |` block scalar is upserted into that file's frontmatter
|
|
16
|
+
* (the same shape a hand `Edit` produces); every other field and the body
|
|
17
|
+
* are byte-preserved.
|
|
18
|
+
*
|
|
19
|
+
* Both destinations run the SAME validator ({@link validateAuthoredSummary})
|
|
20
|
+
* and change ONLY `summary`. The write mode is resolved ONCE at handler entry
|
|
21
|
+
* from `resolveVaultWriteMode()` (the write twin of the read tier), exactly as
|
|
22
|
+
* `handleEditFile` does.
|
|
23
|
+
*
|
|
24
|
+
* Transport-free by design: `deps.consoleFetch` / `deps.resolveWriteMode` /
|
|
25
|
+
* `deps.consoleUrl` are injection seams so tests never open a socket.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
29
|
+
import { join } from 'node:path';
|
|
30
|
+
import { deferredPayloadFromError, updateSpecViaConsole } from './cloud-vault-writes.mjs';
|
|
31
|
+
import { consoleFetch, controlOk } from './console-transport.mjs';
|
|
32
|
+
import { consoleErrorCode, consoleErrorMessage } from './console-fetch.mjs';
|
|
33
|
+
import { getConsoleUrl } from './mcp-config.mjs';
|
|
34
|
+
import { resolveVaultWriteMode } from './read-path.mjs';
|
|
35
|
+
import { formatSummaryFrontmatterField, validateAuthoredSummary } from './summary-contract.mjs';
|
|
36
|
+
import { findSpecFile } from './vault-io.mjs';
|
|
37
|
+
|
|
38
|
+
export const AUTHOR_SUMMARY_TOOL_NAME = 'heyspex_author_summary';
|
|
39
|
+
|
|
40
|
+
/** The follow-up every success names, so the agent completes the phase without guessing. */
|
|
41
|
+
const NEXT_STEP = 'Re-run heyspex_next_on_deck { summary: true } — the brief now shows the summary and asks to claim.';
|
|
42
|
+
|
|
43
|
+
function errorResult(payload) {
|
|
44
|
+
return { isError: true, content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }] };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Upsert `summary` into a raw spec document's YAML frontmatter as a block
|
|
49
|
+
* scalar (`summary: |`), replacing an existing `summary:` field (single-line
|
|
50
|
+
* or block/folded scalar) in place, else appending it as the last frontmatter
|
|
51
|
+
* field. Every other byte — other fields, their order, the body, the file's
|
|
52
|
+
* line-ending style — is preserved. Pure.
|
|
53
|
+
*
|
|
54
|
+
* @param {string} raw
|
|
55
|
+
* @param {string} summary already validated, non-empty prose
|
|
56
|
+
* @returns {{ ok: true, raw: string, replaced: boolean } | { ok: false, error: string, message: string }}
|
|
57
|
+
*/
|
|
58
|
+
export function upsertSummaryInRawSpec(raw, summary) {
|
|
59
|
+
const text = String(raw ?? '');
|
|
60
|
+
const eol = text.includes('\r\n') ? '\r\n' : '\n';
|
|
61
|
+
const lines = text.split(/\r?\n/);
|
|
62
|
+
if (lines[0]?.trim() !== '---') {
|
|
63
|
+
return { ok: false, error: 'NO_FRONTMATTER', message: 'The spec file has no YAML frontmatter block to add `summary:` to.' };
|
|
64
|
+
}
|
|
65
|
+
let end = -1;
|
|
66
|
+
for (let i = 1; i < lines.length; i += 1) {
|
|
67
|
+
if (lines[i].trim() === '---') {
|
|
68
|
+
end = i;
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (end === -1) {
|
|
73
|
+
return { ok: false, error: 'NO_FRONTMATTER', message: 'The spec file\'s frontmatter block is not closed with `---`.' };
|
|
74
|
+
}
|
|
75
|
+
const field = formatSummaryFrontmatterField(summary).split('\n');
|
|
76
|
+
|
|
77
|
+
let start = -1;
|
|
78
|
+
for (let i = 1; i < end; i += 1) {
|
|
79
|
+
if (/^summary\s*:/.test(lines[i])) {
|
|
80
|
+
start = i;
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (start === -1) {
|
|
85
|
+
const next = [...lines.slice(0, end), ...field, ...lines.slice(end)];
|
|
86
|
+
return { ok: true, raw: next.join(eol), replaced: false };
|
|
87
|
+
}
|
|
88
|
+
// Extent of the existing field: continuation lines are indented (block /
|
|
89
|
+
// folded scalar bodies) or blank-but-followed-by-indented. Never swallow the
|
|
90
|
+
// frontmatter fence or the next top-level `key:`.
|
|
91
|
+
let stop = start + 1;
|
|
92
|
+
while (stop < end && /^(\s+\S.*|\s*)$/.test(lines[stop])) stop += 1;
|
|
93
|
+
while (stop > start + 1 && lines[stop - 1].trim() === '') stop -= 1;
|
|
94
|
+
const next = [...lines.slice(0, start), ...field, ...lines.slice(stop)];
|
|
95
|
+
return { ok: true, raw: next.join(eol), replaced: true };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Handler for `heyspex_author_summary`.
|
|
100
|
+
*
|
|
101
|
+
* @param {string} vaultRoot
|
|
102
|
+
* @param {{ spec_id?: string, summary?: string }} args
|
|
103
|
+
* @param {{ consoleFetch?: Function, consoleUrl?: string, resolveWriteMode?: () => Promise<string|null> }} [deps]
|
|
104
|
+
*/
|
|
105
|
+
export async function handleAuthorSummary(vaultRoot, args = {}, deps = {}) {
|
|
106
|
+
const specId = typeof args.spec_id === 'string' ? args.spec_id.trim() : '';
|
|
107
|
+
if (!specId) {
|
|
108
|
+
return errorResult({ error: 'INVALID_ARGS', message: 'spec_id is required (the spec whose `summary:` you authored).' });
|
|
109
|
+
}
|
|
110
|
+
const verdict = validateAuthoredSummary(args.summary);
|
|
111
|
+
if (!verdict.ok) {
|
|
112
|
+
return errorResult({ error: verdict.error, message: verdict.message, spec_id: specId, written: false });
|
|
113
|
+
}
|
|
114
|
+
const { summary, warnings } = verdict;
|
|
115
|
+
|
|
116
|
+
let writeMode = 'local';
|
|
117
|
+
try {
|
|
118
|
+
writeMode = (await (deps.resolveWriteMode ?? resolveVaultWriteMode)()) ?? 'local';
|
|
119
|
+
} catch {
|
|
120
|
+
writeMode = 'local';
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (writeMode === 'cloud') {
|
|
124
|
+
const fetchDep = deps.consoleFetch ?? consoleFetch;
|
|
125
|
+
const consoleUrl = deps.consoleUrl ?? getConsoleUrl();
|
|
126
|
+
const read = await fetchDep(`/api/specs/${encodeURIComponent(specId)}`);
|
|
127
|
+
if (!read?.ok) {
|
|
128
|
+
return errorResult({
|
|
129
|
+
error: read?.status === 404 ? 'SPEC_NOT_FOUND' : consoleErrorCode(read, 'CONSOLE_UNREACHABLE_CLOUD_MODE'),
|
|
130
|
+
message:
|
|
131
|
+
read?.status === 404
|
|
132
|
+
? `Spec ${specId} is not in this caller's vault on the console (${consoleUrl}). NOTHING WAS WRITTEN.`
|
|
133
|
+
: consoleErrorMessage(read, `GET /api/specs/${specId} returned ${read?.status ?? 'no response'}`),
|
|
134
|
+
spec_id: specId,
|
|
135
|
+
write_mode: 'cloud',
|
|
136
|
+
on_disk: false,
|
|
137
|
+
written: false,
|
|
138
|
+
console_url: consoleUrl,
|
|
139
|
+
...(read?.body ? { console_response: read.body } : {}),
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
const spec = read.body?.spec;
|
|
143
|
+
if (!spec || typeof spec !== 'object') {
|
|
144
|
+
return errorResult({
|
|
145
|
+
error: 'CONSOLE_UNREACHABLE_CLOUD_MODE',
|
|
146
|
+
message: `GET /api/specs/${specId} answered without a spec payload — cannot merge \`summary\` safely. NOTHING WAS WRITTEN.`,
|
|
147
|
+
spec_id: specId,
|
|
148
|
+
write_mode: 'cloud',
|
|
149
|
+
on_disk: false,
|
|
150
|
+
written: false,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
const frontmatter = { ...(spec.frontmatter && typeof spec.frontmatter === 'object' ? spec.frontmatter : {}), summary };
|
|
154
|
+
const body = typeof spec.body === 'string' ? spec.body : '';
|
|
155
|
+
const rowId = typeof spec.id === 'string' && spec.id.trim() ? spec.id.trim() : specId;
|
|
156
|
+
let written;
|
|
157
|
+
try {
|
|
158
|
+
written = await updateSpecViaConsole({ consoleFetch: fetchDep, consoleUrl, specId: rowId, frontmatter, body });
|
|
159
|
+
} catch (err) {
|
|
160
|
+
const deferred = deferredPayloadFromError(err);
|
|
161
|
+
if (deferred) {
|
|
162
|
+
return controlOk({ ...deferred, written: false, spec_id: rowId, summary, write_mode: 'cloud', on_disk: false });
|
|
163
|
+
}
|
|
164
|
+
return errorResult({
|
|
165
|
+
error: err?.notFound ? 'SPEC_NOT_FOUND' : 'CONSOLE_UNREACHABLE_CLOUD_MODE',
|
|
166
|
+
message: String(err?.message ?? err),
|
|
167
|
+
spec_id: rowId,
|
|
168
|
+
write_mode: 'cloud',
|
|
169
|
+
on_disk: false,
|
|
170
|
+
written: false,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
return controlOk({
|
|
174
|
+
written: true,
|
|
175
|
+
spec_id: written.specId,
|
|
176
|
+
summary,
|
|
177
|
+
warnings,
|
|
178
|
+
write_mode: 'cloud',
|
|
179
|
+
on_disk: false,
|
|
180
|
+
route: `PUT /api/specs/${written.specId}`,
|
|
181
|
+
rel_path: written.relPath,
|
|
182
|
+
console_url: written.consoleUrl,
|
|
183
|
+
next: NEXT_STEP,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Local write mode — the vault file is the source of record.
|
|
188
|
+
const relPath = findSpecFile(vaultRoot, specId);
|
|
189
|
+
const absPath = relPath ? join(vaultRoot, relPath) : null;
|
|
190
|
+
if (!absPath || !existsSync(absPath)) {
|
|
191
|
+
return errorResult({
|
|
192
|
+
error: 'SPEC_NOT_FOUND',
|
|
193
|
+
message: `Spec ${specId} was not found on disk under ${vaultRoot}. NOTHING WAS WRITTEN.`,
|
|
194
|
+
spec_id: specId,
|
|
195
|
+
write_mode: 'local',
|
|
196
|
+
written: false,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
const raw = readFileSync(absPath, 'utf8');
|
|
200
|
+
const patched = upsertSummaryInRawSpec(raw, summary);
|
|
201
|
+
if (!patched.ok) {
|
|
202
|
+
return errorResult({ error: patched.error, message: patched.message, spec_id: specId, file_path: absPath, write_mode: 'local', written: false });
|
|
203
|
+
}
|
|
204
|
+
try {
|
|
205
|
+
writeFileSync(absPath, patched.raw, 'utf8');
|
|
206
|
+
} catch (err) {
|
|
207
|
+
return errorResult({
|
|
208
|
+
error: 'WRITE_FAILED',
|
|
209
|
+
message: `Failed to write ${absPath}: ${err?.message ?? err}`,
|
|
210
|
+
spec_id: specId,
|
|
211
|
+
file_path: absPath,
|
|
212
|
+
write_mode: 'local',
|
|
213
|
+
written: false,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
return controlOk({
|
|
217
|
+
written: true,
|
|
218
|
+
spec_id: specId,
|
|
219
|
+
summary,
|
|
220
|
+
warnings,
|
|
221
|
+
write_mode: 'local',
|
|
222
|
+
on_disk: true,
|
|
223
|
+
file_path: absPath,
|
|
224
|
+
rel_path: String(relPath).replace(/\\/g, '/'),
|
|
225
|
+
replaced_existing: patched.replaced,
|
|
226
|
+
next: NEXT_STEP,
|
|
227
|
+
});
|
|
228
|
+
}
|