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,113 @@
|
|
|
1
|
+
// vendor/heyspex-mcp/seat-block-ledger.mjs
|
|
2
|
+
//
|
|
3
|
+
// Per-session "has the DEV-FACTORY SEAT banner already gone out this session"
|
|
4
|
+
// decision for the two vendor surfaces that used to each keep their OWN flag:
|
|
5
|
+
// - tool-dispatch.mjs handleInitialize() → surface 'mcp_instructions'
|
|
6
|
+
// - cli.mjs maybePrependSeatBanner() → surface 'preflight' (when the
|
|
7
|
+
// first tool called is heyspex_preflight) or 'tool_result' otherwise
|
|
8
|
+
//
|
|
9
|
+
// Before this module, `initialize` always appended the banner to `instructions`
|
|
10
|
+
// AND, independently, the first tools/call response ALSO got the banner
|
|
11
|
+
// prepended (a separate `seatBannerSeenSessions` Set tracked only THAT surface) —
|
|
12
|
+
// so a caller could see "DEV-FACTORY SEAT: ..." twice in one session. That is
|
|
13
|
+
// exactly the bug spexharness/src/bootstrap-injection.mjs's engine contract
|
|
14
|
+
// exists to close for every surface (its own header calls out wiring
|
|
15
|
+
// vendor/heyspex-mcp as the deferred follow-up — HS-8U8X).
|
|
16
|
+
//
|
|
17
|
+
// spexharness/ is dev-only tooling: it is deliberately NOT part of the published
|
|
18
|
+
// `heyspex` npm package (see dev-factory-bridge.mjs for the identical open-core
|
|
19
|
+
// concern with dev-factory/spexcrawl/). scripts/verify-mcp-package.mjs's
|
|
20
|
+
// `closure` check fails the build if the server's STATIC import graph reaches a
|
|
21
|
+
// file the package doesn't ship, so the engine contract is reached the same way
|
|
22
|
+
// spexcrawl is: a memoized `await import()`, never a static import. A
|
|
23
|
+
// published install (no spexharness/ tree on disk) gets the fallback ledger
|
|
24
|
+
// below instead — same "at most one surface, across the whole session" contract,
|
|
25
|
+
// just without a shared spexharness bootstrap ledger to defer to.
|
|
26
|
+
|
|
27
|
+
/** @type {Promise<typeof import('../../spexharness/src/bootstrap-injection.mjs')>|undefined} */
|
|
28
|
+
let enginePromise;
|
|
29
|
+
|
|
30
|
+
function loadEngine() {
|
|
31
|
+
if (!enginePromise) {
|
|
32
|
+
enginePromise = import('../../spexharness/src/bootstrap-injection.mjs').catch(() => null);
|
|
33
|
+
}
|
|
34
|
+
return enginePromise;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Local, dependency-free ledger with the exact `shouldIncludeSeatBlock` /
|
|
39
|
+
* `markSeatBlockSent` shape bootstrap-injection.mjs's `withSeatBlockLedger`
|
|
40
|
+
* exposes — used when the engine module is not reachable (a published install,
|
|
41
|
+
* or any import failure). "No bootstrap yet" degrades to "emit once, guarded",
|
|
42
|
+
* never to "emit unguarded".
|
|
43
|
+
*/
|
|
44
|
+
function createFallbackLedger() {
|
|
45
|
+
const sent = new Set();
|
|
46
|
+
return {
|
|
47
|
+
shouldIncludeSeatBlock(surface) {
|
|
48
|
+
if (sent.has(surface)) return false;
|
|
49
|
+
return sent.size === 0;
|
|
50
|
+
},
|
|
51
|
+
markSeatBlockSent(surface) {
|
|
52
|
+
sent.add(surface);
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function fallbackSeatBlockForSurface(ledger, surface, seatLine) {
|
|
58
|
+
if (!ledger.shouldIncludeSeatBlock(surface)) return null;
|
|
59
|
+
ledger.markSeatBlockSent(surface);
|
|
60
|
+
return seatLine ?? null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** One ledger controller per session id — two sessions sharing this process (e.g. two
|
|
64
|
+
* fleet workers spawned back-to-back) never see each other's sends. */
|
|
65
|
+
const controllersBySession = new Map();
|
|
66
|
+
|
|
67
|
+
async function getSeatBlockController(sessionId) {
|
|
68
|
+
const key = sessionId || 'local';
|
|
69
|
+
const existing = controllersBySession.get(key);
|
|
70
|
+
if (existing) return existing;
|
|
71
|
+
|
|
72
|
+
const engine = await loadEngine();
|
|
73
|
+
const ledger = engine?.withSeatBlockLedger ? engine.withSeatBlockLedger({}) : createFallbackLedger();
|
|
74
|
+
const forSurface = engine?.seatBlockForSurface
|
|
75
|
+
? (surface, seatLine) => engine.seatBlockForSurface(ledger, surface, seatLine)
|
|
76
|
+
: (surface, seatLine) => fallbackSeatBlockForSurface(ledger, surface, seatLine);
|
|
77
|
+
|
|
78
|
+
const controller = { forSurface };
|
|
79
|
+
controllersBySession.set(key, controller);
|
|
80
|
+
return controller;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Decide whether `surface` should emit the seat banner for `sessionId` this
|
|
85
|
+
* session — sourced from the engine contract's per-session ledger when
|
|
86
|
+
* spexharness/ is reachable (memoized per session id), else a local fallback
|
|
87
|
+
* with identical "exactly once, across every surface" semantics. Returns the
|
|
88
|
+
* banner text when `surface` is cleared to send it, else `null`. Never throws;
|
|
89
|
+
* a falsy `seatLine` (not enabled / unknown caller) short-circuits to `null`
|
|
90
|
+
* WITHOUT touching the ledger, so an ineligible caller never "uses up" the
|
|
91
|
+
* session's one send.
|
|
92
|
+
*
|
|
93
|
+
* @param {string|null|undefined} sessionId
|
|
94
|
+
* @param {string} surface one of bootstrap-injection.mjs's SEAT_BLOCK_SURFACES
|
|
95
|
+
* ('mcp_instructions', 'preflight', 'tool_result', 'bootstrap').
|
|
96
|
+
* @param {string|null|undefined} seatLine
|
|
97
|
+
* @returns {Promise<string|null>}
|
|
98
|
+
*/
|
|
99
|
+
export async function seatBlockForSessionSurface(sessionId, surface, seatLine) {
|
|
100
|
+
if (!seatLine) return null;
|
|
101
|
+
try {
|
|
102
|
+
const controller = await getSeatBlockController(sessionId);
|
|
103
|
+
return controller.forSurface(surface, seatLine);
|
|
104
|
+
} catch {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Test-only: forget every per-session ledger and the memoized engine probe. */
|
|
110
|
+
export function resetSeatBlockLedgerForTests() {
|
|
111
|
+
controllersBySession.clear();
|
|
112
|
+
enginePromise = undefined;
|
|
113
|
+
}
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
|
|
20
20
|
import { readFileSync } from "node:fs";
|
|
21
21
|
import path from "node:path";
|
|
22
|
-
import {
|
|
22
|
+
import { PACKAGE_ROOT } from "./package-root.mjs";
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
25
|
* Read and parse <vaultRoot>/dev-factory/seats.json.
|
|
@@ -169,11 +169,7 @@ function selfTest() {
|
|
|
169
169
|
};
|
|
170
170
|
|
|
171
171
|
// Repo root is two dirs up from this file (vendor/heyspex-mcp/ → repo root).
|
|
172
|
-
const repoRoot =
|
|
173
|
-
path.dirname(fileURLToPath(import.meta.url)),
|
|
174
|
-
"..",
|
|
175
|
-
"..",
|
|
176
|
-
);
|
|
172
|
+
const repoRoot = PACKAGE_ROOT;
|
|
177
173
|
|
|
178
174
|
// 1. Real registry smoke-read.
|
|
179
175
|
const real = readSeatsRegistry(repoRoot);
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// session-owner.mjs — the ONE place the MCP server learns which account owns
|
|
2
|
+
// the documents it files ([[HS-WE87-WW9R-TASK-thread-the-gated-caller-through-every]],
|
|
3
|
+
// epic [[HS-P1AW-WW9R-EPIC-identity-threading-and-the-end-of-the]] — invariant 1).
|
|
4
|
+
//
|
|
5
|
+
// Until this module the three frontmatter writers (`capture-focus.mjs`,
|
|
6
|
+
// `research-focus.mjs`, `defer-spec.mjs`) each stamped the HeySpex owner's
|
|
7
|
+
// handle as a literal — on a customer machine that put the platform owner's
|
|
8
|
+
// name on the customer's own documents. They now ask here, and when nothing
|
|
9
|
+
// resolves the `owner:` key is OMITTED rather than guessed.
|
|
10
|
+
//
|
|
11
|
+
// Resolution order (no network — this runs inside synchronous writers and
|
|
12
|
+
// under `node --test`):
|
|
13
|
+
// 1. `HS_ACCOUNT_ID` env — the same override the CLI's account reader honours.
|
|
14
|
+
// 2. the credentials store's current account (`credentials.mjs`
|
|
15
|
+
// `readCurrentAccountId`, [[HS-UA9S-WW9R-TASK-store-credentials-per-account-login]]):
|
|
16
|
+
// `current_account_id` of `~/.heyspex/credentials.yaml`, set by `hs login` /
|
|
17
|
+
// `hs mcp pair` from the account GET /api/mcp/session confirmed for the
|
|
18
|
+
// stored bearer and moved by `hs login --switch-account` / `hs logout`.
|
|
19
|
+
// An existing single-account (v1) file has no such key: its account is
|
|
20
|
+
// the `~/.heyspex/config.yaml` `account_id:` the earlier `hs login` wrote
|
|
21
|
+
// ([[HS-630L-WW9R-TASK-one-strict-cli-account-reader-hs-login]]) — read
|
|
22
|
+
// for that case only, never written any more (one account-id store).
|
|
23
|
+
// 3. null — the writer omits `owner:`.
|
|
24
|
+
//
|
|
25
|
+
// This is ALSO the CLI's account reader: `resolveConfiguredAccountId` in
|
|
26
|
+
// spex-console/src/lib/cli/vault-workspace-shared.ts and `readConfiguredAccountId`
|
|
27
|
+
// in spex-console/src/lib/cli/install-identity.ts both delegate to
|
|
28
|
+
// `resolveSessionOwnerUncached`, so one precedence serves `hs vault`, `hs connect`,
|
|
29
|
+
// `hs install` and every MCP frontmatter writer. Its bearer twin is
|
|
30
|
+
// `./credentials.mjs` (HS-HL0D).
|
|
31
|
+
//
|
|
32
|
+
// Cached per process once resolved from the real environment (the MCP server
|
|
33
|
+
// is spawned once per session; pairing changes restart it). Callers inject
|
|
34
|
+
// `env` / `homeDir` / `readFile` for tests, which bypasses the cache.
|
|
35
|
+
|
|
36
|
+
import { readFileSync } from 'node:fs';
|
|
37
|
+
import { homedir } from 'node:os';
|
|
38
|
+
import { join } from 'node:path';
|
|
39
|
+
|
|
40
|
+
import { readCurrentAccountId } from './credentials.mjs';
|
|
41
|
+
|
|
42
|
+
const ACCOUNT_ID_LINE_RE = /^account_id:\s*["']?([^"'\r\n#]+)/m;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Read the paired account id from `~/.heyspex/config.yaml`, or null.
|
|
46
|
+
* @param {{ homeDir?: string, readFile?: (p: string) => string }} [opts]
|
|
47
|
+
*/
|
|
48
|
+
export function readConfigAccountId({ homeDir = homedir(), readFile } = {}) {
|
|
49
|
+
const read = readFile ?? ((p) => readFileSync(p, 'utf8'));
|
|
50
|
+
try {
|
|
51
|
+
const raw = read(join(homeDir, '.heyspex', 'config.yaml'));
|
|
52
|
+
const match = String(raw ?? '').match(ACCOUNT_ID_LINE_RE);
|
|
53
|
+
const value = match?.[1]?.trim() ?? '';
|
|
54
|
+
return value || null;
|
|
55
|
+
} catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Resolve the session owner without caching — the pure core.
|
|
62
|
+
* @param {{ env?: NodeJS.ProcessEnv | Record<string, string | undefined>, homeDir?: string, readFile?: (p: string) => string }} [opts]
|
|
63
|
+
* @returns {string | null}
|
|
64
|
+
*/
|
|
65
|
+
export function resolveSessionOwnerUncached({ env = process.env, homeDir = homedir(), readFile } = {}) {
|
|
66
|
+
const fromEnv = typeof env?.HS_ACCOUNT_ID === 'string' ? env.HS_ACCOUNT_ID.trim() : '';
|
|
67
|
+
if (fromEnv) return fromEnv;
|
|
68
|
+
return readCurrentAccountId({ homeDir, readFile });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let cached = null;
|
|
72
|
+
let cacheFilled = false;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The account every MCP-authored document is stamped with, or null when the
|
|
76
|
+
* machine has no resolvable account (the writers then OMIT `owner:`).
|
|
77
|
+
*
|
|
78
|
+
* The real-environment answer is memoised per process; any injected option
|
|
79
|
+
* bypasses the cache so tests stay hermetic.
|
|
80
|
+
* @param {{ env?: NodeJS.ProcessEnv | Record<string, string | undefined>, homeDir?: string, readFile?: (p: string) => string }} [opts]
|
|
81
|
+
* @returns {string | null}
|
|
82
|
+
*/
|
|
83
|
+
export function resolveSessionOwner(opts) {
|
|
84
|
+
if (opts && Object.keys(opts).length > 0) return resolveSessionOwnerUncached(opts);
|
|
85
|
+
if (!cacheFilled) {
|
|
86
|
+
cached = resolveSessionOwnerUncached();
|
|
87
|
+
cacheFilled = true;
|
|
88
|
+
}
|
|
89
|
+
return cached;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Test seam: forget the per-process answer. */
|
|
93
|
+
export function __resetSessionOwnerCacheForTests() {
|
|
94
|
+
cached = null;
|
|
95
|
+
cacheFilled = false;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The `owner:` frontmatter entry for an object-shaped frontmatter — `{ owner }`
|
|
100
|
+
* when an account resolved, `{}` when none did (the key is omitted, never
|
|
101
|
+
* guessed). `explicit` (an `args.owner` the caller supplied) wins.
|
|
102
|
+
* @param {{ explicit?: unknown, resolve?: () => string | null }} [opts]
|
|
103
|
+
*/
|
|
104
|
+
export function ownerFrontmatterEntry({ explicit, resolve = resolveSessionOwner } = {}) {
|
|
105
|
+
const chosen =
|
|
106
|
+
typeof explicit === 'string' && explicit.trim() ? explicit.trim() : (resolve() ?? null);
|
|
107
|
+
return chosen ? { owner: chosen } : {};
|
|
108
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// Session hand-off and agent identity tools: heyspex_handoff_record, handoff_load,
|
|
2
|
+
// import_agent. 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 { consoleFetch } from './console-transport.mjs';
|
|
6
|
+
import { consoleErrorCode, consoleErrorMessage } from './console-fetch.mjs';
|
|
7
|
+
import { detectAgentIdentity } from './mcp-config.mjs';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Import a community agent role via console `POST /api/agents/import` — the MCP
|
|
11
|
+
* face of agent import ([[HS-TASK-Z666]]). Same core as `hs agent import`; no
|
|
12
|
+
* field-mapping logic duplicated in JS.
|
|
13
|
+
*/
|
|
14
|
+
export async function handleImportAgent(args = {}) {
|
|
15
|
+
if (!args.collection || typeof args.collection !== 'string') {
|
|
16
|
+
return {
|
|
17
|
+
isError: true,
|
|
18
|
+
content: [{ type: 'text', text: JSON.stringify({ error: 'INVALID_ARGS', message: 'collection is required' }) }],
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
if (!args.role || typeof args.role !== 'string') {
|
|
22
|
+
return {
|
|
23
|
+
isError: true,
|
|
24
|
+
content: [{ type: 'text', text: JSON.stringify({ error: 'INVALID_ARGS', message: 'role is required' }) }],
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
const body = {
|
|
28
|
+
collection: args.collection,
|
|
29
|
+
role: args.role,
|
|
30
|
+
...(args.dry_run === true || args.dry_run === 'true' ? { dry_run: true } : {}),
|
|
31
|
+
};
|
|
32
|
+
const result = await consoleFetch('/api/agents/import', { method: 'POST', body });
|
|
33
|
+
if (!result.ok) {
|
|
34
|
+
const payload = {
|
|
35
|
+
error: consoleErrorCode(result, 'IMPORT_AGENT_FAILED'),
|
|
36
|
+
message: consoleErrorMessage(result, `POST /api/agents/import returned ${result.status}`),
|
|
37
|
+
status: result.status,
|
|
38
|
+
url: result.url,
|
|
39
|
+
console_response: result.body,
|
|
40
|
+
attempted: body,
|
|
41
|
+
};
|
|
42
|
+
return {
|
|
43
|
+
isError: true,
|
|
44
|
+
content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
const responseBody = result.body ?? {};
|
|
48
|
+
const payload = {
|
|
49
|
+
spec_id: responseBody.spec_id ?? null,
|
|
50
|
+
path: responseBody.path ?? null,
|
|
51
|
+
warnings: Array.isArray(responseBody.warnings) ? responseBody.warnings : [],
|
|
52
|
+
...(responseBody.dry_run ? { dry_run: true } : {}),
|
|
53
|
+
console_response: responseBody,
|
|
54
|
+
};
|
|
55
|
+
return {
|
|
56
|
+
content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Upsert the current resume context for this agent/lane into the Neon control
|
|
62
|
+
* plane — [[HS-EPIC-NCPS]] handoff_context / [[HS-EPIC-CLMW]] `hs handoff record`.
|
|
63
|
+
*
|
|
64
|
+
* Self-purging overwrite-in-place (PUT /api/control/handoff-context). The lane
|
|
65
|
+
* defaults to this agent's session id so each session is its own lane. Coordi-
|
|
66
|
+
* nation only — the endpoint rejects durable-knowledge fields.
|
|
67
|
+
*/
|
|
68
|
+
export async function handleHandoffRecord(args = {}) {
|
|
69
|
+
const identity = detectAgentIdentity(args.agent, args.session_id);
|
|
70
|
+
const laneKey =
|
|
71
|
+
typeof args.lane === 'string' && args.lane.trim()
|
|
72
|
+
? args.lane.trim()
|
|
73
|
+
: identity.sessionId;
|
|
74
|
+
const body = {
|
|
75
|
+
lane_key: laneKey,
|
|
76
|
+
agent: identity.agent,
|
|
77
|
+
...(typeof args.spec_id === 'string' ? { spec_id: args.spec_id } : {}),
|
|
78
|
+
...(typeof args.phase === 'string' ? { phase: args.phase } : {}),
|
|
79
|
+
...(typeof args.estimated_time === 'string' ? { estimated_time: args.estimated_time } : {}),
|
|
80
|
+
...(Array.isArray(args.blockers) ? { blockers: args.blockers.map(String) } : {}),
|
|
81
|
+
...(Array.isArray(args.links) ? { links: args.links.map(String) } : {}),
|
|
82
|
+
};
|
|
83
|
+
const result = await consoleFetch('/api/control/handoff-context', {
|
|
84
|
+
method: 'PUT',
|
|
85
|
+
body,
|
|
86
|
+
});
|
|
87
|
+
if (!result.ok) {
|
|
88
|
+
const payload = {
|
|
89
|
+
error: consoleErrorCode(result, 'HANDOFF_RECORD_FAILED'),
|
|
90
|
+
message: consoleErrorMessage(result, `Handoff-context endpoint returned ${result.status}`),
|
|
91
|
+
status: result.status,
|
|
92
|
+
url: result.url,
|
|
93
|
+
console_response: result.body,
|
|
94
|
+
attempted: { lane: laneKey },
|
|
95
|
+
};
|
|
96
|
+
return {
|
|
97
|
+
isError: true,
|
|
98
|
+
content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
content: [{
|
|
103
|
+
type: 'text',
|
|
104
|
+
text: JSON.stringify({ lane: laneKey, console_response: result.body }, null, 2),
|
|
105
|
+
}],
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Read the current resume context for a lane from the Neon control plane —
|
|
111
|
+
* [[HS-EPIC-NCPS]] handoff_context / [[HS-EPIC-CLMW]] `hs handoff load`.
|
|
112
|
+
*
|
|
113
|
+
* Defaults to this agent's own lane (its session); `all: true` lists every
|
|
114
|
+
* active lane (parallel-agent off-on-deck). Self-purging — no per-session
|
|
115
|
+
* history; expired rows read as absent.
|
|
116
|
+
*/
|
|
117
|
+
export async function handleHandoffLoad(args = {}) {
|
|
118
|
+
const identity = detectAgentIdentity(args.agent, args.session_id);
|
|
119
|
+
const wantAll = args.all === true;
|
|
120
|
+
const laneKey =
|
|
121
|
+
typeof args.lane === 'string' && args.lane.trim()
|
|
122
|
+
? args.lane.trim()
|
|
123
|
+
: identity.sessionId;
|
|
124
|
+
const path = wantAll
|
|
125
|
+
? '/api/control/handoff-context'
|
|
126
|
+
: `/api/control/handoff-context?lane=${encodeURIComponent(laneKey)}`;
|
|
127
|
+
const result = await consoleFetch(path);
|
|
128
|
+
if (!result.ok) {
|
|
129
|
+
const payload = {
|
|
130
|
+
error: consoleErrorCode(result, 'HANDOFF_LOAD_FAILED'),
|
|
131
|
+
message: consoleErrorMessage(result, `Handoff-context endpoint returned ${result.status}`),
|
|
132
|
+
status: result.status,
|
|
133
|
+
url: result.url,
|
|
134
|
+
console_response: result.body,
|
|
135
|
+
attempted: wantAll ? { all: true } : { lane: laneKey },
|
|
136
|
+
};
|
|
137
|
+
return {
|
|
138
|
+
isError: true,
|
|
139
|
+
content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
content: [{
|
|
144
|
+
type: 'text',
|
|
145
|
+
text: JSON.stringify(
|
|
146
|
+
wantAll ? result.body : { lane: laneKey, ...result.body },
|
|
147
|
+
null,
|
|
148
|
+
2,
|
|
149
|
+
),
|
|
150
|
+
}],
|
|
151
|
+
};
|
|
152
|
+
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HeySpex close-out settings registry — the single declaration of every close-out
|
|
3
|
+
* setting: its dotted key, type, shipped default, description, CLI flag, admin-only
|
|
4
|
+
* marker, enforced marker, and which vault templates ship it.
|
|
5
|
+
*
|
|
6
|
+
* WHY: nothing today says which close-out settings exist, so a shipped default has
|
|
7
|
+
* nowhere to live and a mistyped key fails silently ([[HS-FCF5-15XI-RES-user-editable-close-out-settings-with]]).
|
|
8
|
+
* This registry is the one source every face is meant to read — `hs settings`, the
|
|
9
|
+
* MCP settings tools, and (later) the console — instead of each guessing its own
|
|
10
|
+
* fallback.
|
|
11
|
+
*
|
|
12
|
+
* DECLARATION ONLY (HS-6TDH): this module does not read or write
|
|
13
|
+
* `.heyspex/config.yaml`, does not import `config-settings.mjs`, and does not change
|
|
14
|
+
* any existing settings behavior. A later task in the same epic
|
|
15
|
+
* ([[HS-KW38-15XI-EPIC-settings-registry-core]]) teaches `config-settings.mjs` to
|
|
16
|
+
* resolve reads/writes through this registry (defaults, unknown-key refusal,
|
|
17
|
+
* enforced-entry refusal, type/enum validation).
|
|
18
|
+
*
|
|
19
|
+
* DEPENDENCY-FREE: Node stdlib only, no filesystem access at all — same rationale as
|
|
20
|
+
* the sibling module's "DEPENDENCY-FREE" note.
|
|
21
|
+
*
|
|
22
|
+
* Traceability: [[HS-KW38-15XI-EPIC-settings-registry-core]] ·
|
|
23
|
+
* [[HS-FCF5-15XI-RES-user-editable-close-out-settings-with]] ·
|
|
24
|
+
* [[HS-6TDH-15XI-TASK-declare-every-close-out-setting-in-one]]
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @typedef {'boolean' | 'string' | 'enum'} SettingType
|
|
29
|
+
*
|
|
30
|
+
* @typedef {Object} SettingEntry
|
|
31
|
+
* @property {string} key - Dotted key, e.g. "close_out.update_docs".
|
|
32
|
+
* @property {SettingType} type - The value's declared type.
|
|
33
|
+
* @property {*} default - The shipped default value (absent-key resolution target).
|
|
34
|
+
* @property {string} description - One or two sentences a person can read as help text.
|
|
35
|
+
* @property {string|null} cliFlag - The `hs close` / `hs settings` flag name (e.g.
|
|
36
|
+
* "--update-docs"), or `null` for an enforced entry with no user-facing flag.
|
|
37
|
+
* @property {boolean} adminOnly - True for a HeySpex-admin-only entry; a non-admin
|
|
38
|
+
* vault must treat the key as unknown.
|
|
39
|
+
* @property {boolean} enforced - True for a hard rule that cannot be changed by
|
|
40
|
+
* anyone through any face, including a HeySpex admin.
|
|
41
|
+
* @property {string[]} templates - Which vault templates ship this entry:
|
|
42
|
+
* "heyspex" (the HeySpex platform vault) and/or "customer" (the shipped customer
|
|
43
|
+
* template).
|
|
44
|
+
* @property {string[]} [values] - Allowed values; present only when `type` is "enum".
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The ordered list of every declared close-out setting. Order is the order the
|
|
49
|
+
* close-out runner is meant to evaluate them in (a later task); it is also the
|
|
50
|
+
* order `hs settings list` should render them in.
|
|
51
|
+
*
|
|
52
|
+
* @type {ReadonlyArray<Readonly<SettingEntry>>}
|
|
53
|
+
*/
|
|
54
|
+
export const SETTINGS_REGISTRY = Object.freeze(
|
|
55
|
+
[
|
|
56
|
+
{
|
|
57
|
+
key: 'close_out.update_docs',
|
|
58
|
+
type: 'boolean',
|
|
59
|
+
default: true,
|
|
60
|
+
description:
|
|
61
|
+
'Updates docs/user and docs/admin per the docs-action rule (create/edit/append) at every close-out.',
|
|
62
|
+
cliFlag: '--update-docs',
|
|
63
|
+
adminOnly: false,
|
|
64
|
+
enforced: false,
|
|
65
|
+
templates: ['heyspex', 'customer'],
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
key: 'close_out.review_grain',
|
|
69
|
+
type: 'enum',
|
|
70
|
+
values: ['milestone', 'task'],
|
|
71
|
+
default: 'milestone',
|
|
72
|
+
description:
|
|
73
|
+
'Chooses the close-out review unit: "milestone" flips a completed task straight to closed with no per-task review companion, "task" is the canonical per-task close-out protocol with one HS-REV per task.',
|
|
74
|
+
cliFlag: '--review-grain',
|
|
75
|
+
adminOnly: false,
|
|
76
|
+
enforced: false,
|
|
77
|
+
templates: ['heyspex', 'customer'],
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
key: 'close_out.review_backlog_never_blocks',
|
|
81
|
+
type: 'boolean',
|
|
82
|
+
default: true,
|
|
83
|
+
description:
|
|
84
|
+
'A review backlog sitting at in_review/review_pending never blocks a new claim and never needs owner permission.',
|
|
85
|
+
cliFlag: '--review-backlog-never-blocks',
|
|
86
|
+
adminOnly: false,
|
|
87
|
+
enforced: false,
|
|
88
|
+
templates: ['heyspex', 'customer'],
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
key: 'close_out.flip_completed_to_closed',
|
|
92
|
+
type: 'boolean',
|
|
93
|
+
default: true,
|
|
94
|
+
description: 'Flips a completed task straight to closed at close-out.',
|
|
95
|
+
cliFlag: '--flip-completed-to-closed',
|
|
96
|
+
adminOnly: false,
|
|
97
|
+
enforced: false,
|
|
98
|
+
templates: ['heyspex', 'customer'],
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
key: 'close_out.append_review_seed',
|
|
102
|
+
type: 'boolean',
|
|
103
|
+
default: true,
|
|
104
|
+
description: 'Appends a one-line review seed to the task change-log at close-out.',
|
|
105
|
+
cliFlag: '--append-review-seed',
|
|
106
|
+
adminOnly: false,
|
|
107
|
+
enforced: false,
|
|
108
|
+
templates: ['heyspex', 'customer'],
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
key: 'close_out.rolling_review_doc',
|
|
112
|
+
type: 'boolean',
|
|
113
|
+
default: true,
|
|
114
|
+
description: 'Accumulates one rolling review doc per milestone at close-out.',
|
|
115
|
+
cliFlag: '--rolling-review-doc',
|
|
116
|
+
adminOnly: false,
|
|
117
|
+
enforced: false,
|
|
118
|
+
templates: ['heyspex', 'customer'],
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
key: 'close_out.run_command_drift_doctor',
|
|
122
|
+
type: 'boolean',
|
|
123
|
+
default: true,
|
|
124
|
+
description:
|
|
125
|
+
'Runs the HeySpex command-drift doctor at close-out. HeySpex-admin-only; a customer vault never sees this key.',
|
|
126
|
+
cliFlag: '--run-command-drift-doctor',
|
|
127
|
+
adminOnly: true,
|
|
128
|
+
enforced: false,
|
|
129
|
+
templates: ['heyspex'],
|
|
130
|
+
},
|
|
131
|
+
// Hard rules: enforced entries cannot be changed by anyone through any face,
|
|
132
|
+
// including a HeySpex admin, so they carry no user-facing CLI flag.
|
|
133
|
+
{
|
|
134
|
+
key: 'close_out.alignment',
|
|
135
|
+
type: 'boolean',
|
|
136
|
+
default: true,
|
|
137
|
+
description: 'Hard rule: alignment stays on. No mode may relax it.',
|
|
138
|
+
cliFlag: null,
|
|
139
|
+
adminOnly: false,
|
|
140
|
+
enforced: true,
|
|
141
|
+
templates: ['heyspex', 'customer'],
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
key: 'close_out.capture',
|
|
145
|
+
type: 'boolean',
|
|
146
|
+
default: true,
|
|
147
|
+
description: 'Hard rule: capture stays on. No mode may relax it.',
|
|
148
|
+
cliFlag: null,
|
|
149
|
+
adminOnly: false,
|
|
150
|
+
enforced: true,
|
|
151
|
+
templates: ['heyspex', 'customer'],
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
key: 'close_out.research',
|
|
155
|
+
type: 'boolean',
|
|
156
|
+
default: true,
|
|
157
|
+
description: 'Hard rule: research stays on. No mode may relax it.',
|
|
158
|
+
cliFlag: null,
|
|
159
|
+
adminOnly: false,
|
|
160
|
+
enforced: true,
|
|
161
|
+
templates: ['heyspex', 'customer'],
|
|
162
|
+
},
|
|
163
|
+
// `close_out.git_commit` moved OUT of the hard-rule set above (owner ruling
|
|
164
|
+
// 2026-09-11, [[HS-PROTO-spexsource-agent-protocol]]): whether an agent
|
|
165
|
+
// ever commits at close-out is now a real, permission-gated toggle instead
|
|
166
|
+
// of an unconditional refusal. Default stays `false` for every vault —
|
|
167
|
+
// heyspex and customer alike — so nobody's out-of-box behavior changes; an
|
|
168
|
+
// owner opts in per vault via the normal settings-write-permission flow
|
|
169
|
+
// (`heyspex_settings_set` / `hs settings set --confirmed-by-user`). When
|
|
170
|
+
// `true`, the agent does not commit unconditionally — it still obeys the
|
|
171
|
+
// existing mode logic: the close-out commit lands at the boundary of the
|
|
172
|
+
// *active review grain* (`close_out.review_grain` — task / epic /
|
|
173
|
+
// milestone), routes only through the sanctioned `hs source preflight` →
|
|
174
|
+
// `hs source commit` path, and stays bound by every entry in
|
|
175
|
+
// spexsource-agent-protocol's always-forbidden list (no `--no-verify`, no
|
|
176
|
+
// force, no push — pushing is the separate `close_out.git_push` key,
|
|
177
|
+
// still enforced below). An explicit owner instruction for the session
|
|
178
|
+
// (e.g. "don't commit this one") overrides the setting for that session;
|
|
179
|
+
// the setting only changes the *default* when the owner said nothing.
|
|
180
|
+
{
|
|
181
|
+
key: 'close_out.git_commit',
|
|
182
|
+
type: 'boolean',
|
|
183
|
+
default: false,
|
|
184
|
+
description:
|
|
185
|
+
'Whether the agent commits automatically at close-out. Default false (a dirty tree is the expected hand-off; the human reviewer commits on Pass). When true, the agent still obeys the active review grain (close_out.review_grain) for WHEN to commit and the spexsource-agent-protocol always-forbidden list for HOW — never a bypass of those rules, just an opt-in default.',
|
|
186
|
+
cliFlag: '--git-commit',
|
|
187
|
+
adminOnly: false,
|
|
188
|
+
enforced: false,
|
|
189
|
+
templates: ['heyspex', 'customer'],
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
key: 'close_out.git_push',
|
|
193
|
+
type: 'boolean',
|
|
194
|
+
default: false,
|
|
195
|
+
description: 'Hard rule: an agent never pushes at close-out.',
|
|
196
|
+
cliFlag: null,
|
|
197
|
+
adminOnly: false,
|
|
198
|
+
enforced: true,
|
|
199
|
+
templates: ['heyspex', 'customer'],
|
|
200
|
+
},
|
|
201
|
+
].map((entry) => Object.freeze(entry)),
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
/** Fast key → entry lookup, built once from `SETTINGS_REGISTRY`. */
|
|
205
|
+
const BY_KEY = new Map(SETTINGS_REGISTRY.map((entry) => [entry.key, entry]));
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Look up one registry entry by its dotted key.
|
|
209
|
+
* @param {string} key
|
|
210
|
+
* @returns {Readonly<SettingEntry>|null}
|
|
211
|
+
*/
|
|
212
|
+
export function getSettingEntry(key) {
|
|
213
|
+
if (typeof key !== 'string') return null;
|
|
214
|
+
return BY_KEY.get(key) ?? null;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** True when `key` names a declared registry entry. */
|
|
218
|
+
export function isKnownSetting(key) {
|
|
219
|
+
return getSettingEntry(key) !== null;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Every declared key, in registry order. */
|
|
223
|
+
export function listRegistryKeys() {
|
|
224
|
+
return SETTINGS_REGISTRY.map((entry) => entry.key);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Every declared key the given vault template ships ("heyspex" or "customer"). */
|
|
228
|
+
export function listKeysForTemplate(template) {
|
|
229
|
+
return SETTINGS_REGISTRY.filter((entry) => entry.templates.includes(template)).map(
|
|
230
|
+
(entry) => entry.key,
|
|
231
|
+
);
|
|
232
|
+
}
|