heyspex 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +14 -0
- package/README.md +21 -0
- package/bin/hs.mjs +1089 -104
- package/bin/spexcode.mjs +262 -0
- package/dev-factory/engine/worktree.mjs +740 -0
- package/dev-factory/lib/code-repo-root.mjs +67 -0
- package/dev-factory/lib/context-budget.mjs +84 -0
- package/dev-factory/lib/epic-detect.mjs +190 -0
- package/dev-factory/lib/fleet-queue-file.mjs +846 -0
- package/dev-factory/lib/fleet-queue.mjs +426 -0
- package/dev-factory/lib/fleet-state.mjs +14 -18
- package/dev-factory/lib/model-catalog.mjs +338 -0
- package/dev-factory/lib/seat-context.mjs +199 -0
- package/dev-factory/lib/spec-io.mjs +93 -0
- package/dev-factory/lib/sufficiency-score.mjs +8 -2
- package/dev-factory/lib/worktree-allocator.mjs +744 -0
- package/dev-factory/model-catalog.json +222 -0
- package/dist/cli/hs-check.mjs +1127 -103
- package/dist/cli/hs-claim.mjs +981 -63
- package/dist/cli/hs-connect.mjs +10059 -1729
- package/dist/cli/hs-defer.mjs +5507 -3072
- package/dist/cli/hs-dev.mjs +576 -0
- package/dist/cli/hs-env.mjs +416 -302
- package/dist/cli/hs-index-rebuild.mjs +7334 -0
- package/dist/cli/hs-index-reconcile.mjs +8138 -0
- package/dist/cli/hs-init.mjs +2840 -149
- package/dist/cli/hs-install.mjs +7214 -594
- package/dist/cli/hs-join.mjs +10640 -2421
- package/dist/cli/hs-vault-pull.mjs +8157 -6370
- package/dist/cli/hs-vault-push.mjs +6433 -0
- package/dist/cli/hs-vault.mjs +4029 -1555
- package/dist/cli/hs-workspace.mjs +2702 -538
- package/dist/cli-templates/customer-vault/specs/entity-model.md +594 -601
- package/dist/cli-templates/customer-vault/specs/protocols/canonical-frontmatter-templates.md +110 -177
- package/dist/cli-templates/customer-vault/specs/protocols/destructive-action-checklist.md +122 -0
- package/dist/cli-templates/customer-vault/specs/protocols/memory-protocol.md +13 -15
- package/dist/cli-templates/customer-vault/specs/protocols/session-close-protocol.md +91 -0
- package/dist/cli-templates/customer-vault/specs/protocols/spec-hierarchy.md +150 -168
- package/dist/cli-templates/customer-vault/specs/protocols/spec-summary-authoring.md +83 -0
- package/dist/cli-templates/harness-commands/claude/hs-auto.md +36 -0
- package/dist/cli-templates/harness-commands/claude/hs-backfill.md +28 -0
- package/dist/cli-templates/harness-commands/claude/hs-capture.md +62 -0
- package/dist/cli-templates/harness-commands/claude/hs-defer.md +29 -0
- package/dist/cli-templates/harness-commands/claude/hs-dev.md +20 -0
- package/dist/cli-templates/harness-commands/claude/hs-doctor.md +32 -0
- package/dist/cli-templates/harness-commands/claude/hs-ground.md +30 -0
- package/dist/cli-templates/harness-commands/claude/hs-handoff.md +33 -0
- package/dist/cli-templates/harness-commands/claude/hs-log.md +27 -0
- package/dist/cli-templates/harness-commands/claude/hs-next.md +29 -0
- package/dist/cli-templates/harness-commands/claude/hs-promote.md +70 -0
- package/dist/cli-templates/harness-commands/claude/hs-push.md +35 -0
- package/dist/cli-templates/harness-commands/claude/hs-qc.md +27 -0
- package/dist/cli-templates/harness-commands/claude/hs-res.md +63 -0
- package/dist/cli-templates/harness-commands/claude/hs-resolve.md +23 -0
- package/dist/cli-templates/harness-commands/claude/hs-review.md +23 -0
- package/dist/cli-templates/harness-commands/claude/hs-view.md +28 -0
- package/dist/cli-templates/harness-commands/claude/kill-ports.md +32 -0
- package/dist/cli-templates/harness-commands/claude/prototype.md +91 -0
- package/dist/cli-templates/harness-commands/cursor/hs-auto.md +40 -0
- package/dist/cli-templates/harness-commands/cursor/hs-backfill.md +31 -0
- package/dist/cli-templates/harness-commands/cursor/hs-capture.md +65 -0
- package/dist/cli-templates/harness-commands/cursor/hs-defer.md +32 -0
- package/dist/cli-templates/harness-commands/cursor/hs-dev.md +24 -0
- package/dist/cli-templates/harness-commands/cursor/hs-doctor.md +36 -0
- package/dist/cli-templates/harness-commands/cursor/hs-ground.md +33 -0
- package/dist/cli-templates/harness-commands/cursor/hs-handoff.md +37 -0
- package/dist/cli-templates/harness-commands/cursor/hs-log.md +31 -0
- package/dist/cli-templates/harness-commands/cursor/hs-next.md +32 -0
- package/dist/cli-templates/harness-commands/cursor/hs-promote.md +73 -0
- package/dist/cli-templates/harness-commands/cursor/hs-push.md +38 -0
- package/dist/cli-templates/harness-commands/cursor/hs-qc.md +30 -0
- package/dist/cli-templates/harness-commands/cursor/hs-res.md +67 -0
- package/dist/cli-templates/harness-commands/cursor/hs-resolve.md +26 -0
- package/dist/cli-templates/harness-commands/cursor/hs-review.md +26 -0
- package/dist/cli-templates/harness-commands/cursor/hs-view.md +32 -0
- package/dist/cli-templates/harness-commands/cursor/kill-ports.md +35 -0
- package/dist/cli-templates/harness-commands/cursor/prototype.md +95 -0
- package/docs/copilot-plugin-install.md +162 -0
- package/docs/driving-heyspex-from-copilot.md +165 -0
- package/docs/mcp-client-config.md +259 -6
- package/package.json +249 -124
- package/plugins/heyspex/.claude-plugin/plugin.json +12 -0
- package/plugins/heyspex/.cursor-plugin/plugin.json +15 -0
- package/plugins/heyspex/.mcp.json +12 -0
- package/plugins/heyspex/README.md +309 -0
- package/plugins/heyspex/bin/launch-mcp.mjs +95 -0
- package/plugins/heyspex/bin/run-hook.mjs +80 -0
- package/plugins/heyspex/hooks/cursor-hooks.json +308 -0
- package/plugins/heyspex/hooks/hooks.json +418 -0
- package/plugins/heyspex/mcp.json +13 -0
- package/scripts/check-mcp-freshness.mjs +17 -9
- package/scripts/claude-hooks/_env-vault-events.mjs +68 -0
- package/scripts/claude-hooks/_guardrail-events.mjs +96 -0
- package/scripts/claude-hooks/_guardrail-loader.mjs +494 -0
- package/scripts/claude-hooks/_intent-classifier-router.mjs +456 -0
- package/scripts/claude-hooks/_intent-classifier-semantic.mjs +356 -0
- package/scripts/claude-hooks/_run.mjs +413 -0
- package/scripts/claude-hooks/_transcript-text.mjs +60 -0
- package/scripts/claude-hooks/_vault-retrieval.mjs +322 -0
- package/scripts/claude-hooks/agent-spawn-delegation-gate.mjs +133 -0
- package/scripts/claude-hooks/capture-execution-gate.mjs +51 -0
- package/scripts/claude-hooks/capture-workflow-gate.mjs +11 -0
- package/scripts/claude-hooks/check-auto-memory-budget.mjs +396 -0
- package/scripts/claude-hooks/check-claude-md-budget.mjs +327 -0
- package/scripts/claude-hooks/check-mcp-freshness-hook.mjs +14 -0
- package/scripts/claude-hooks/claim-before-code-gate.mjs +11 -0
- package/scripts/claude-hooks/claim-liveness-shared.mjs +577 -0
- package/scripts/claude-hooks/close-terminal-on-ship.mjs +64 -0
- package/scripts/claude-hooks/coo-denylist-gate.mjs +11 -0
- package/scripts/claude-hooks/coo-guard-check.mjs +11 -0
- package/scripts/claude-hooks/deep-research-cost-gate.mjs +11 -0
- package/scripts/claude-hooks/dev-server-task-reminder.mjs +15 -0
- package/scripts/claude-hooks/diagnose-capture-reminder.mjs +11 -0
- package/scripts/claude-hooks/directive-obligation-gate.mjs +11 -0
- package/scripts/claude-hooks/emit-claim-status.mjs +41 -0
- package/scripts/claude-hooks/enforce-session-sequence.mjs +11 -0
- package/scripts/claude-hooks/enforce-vault-naming.mjs +11 -0
- package/scripts/claude-hooks/env-vault-guard.mjs +11 -0
- package/scripts/claude-hooks/epic-milestone-phrasing-gate.mjs +11 -0
- package/scripts/claude-hooks/git-identity-guard.mjs +11 -0
- package/scripts/claude-hooks/git-reflog-audit.mjs +15 -0
- package/scripts/claude-hooks/git-stash-gate.mjs +11 -0
- package/scripts/claude-hooks/guard-auto-memory-writes.mjs +137 -0
- package/scripts/claude-hooks/id-display-format-gate.mjs +11 -0
- package/scripts/claude-hooks/inject-communication-style.mjs +86 -0
- package/scripts/claude-hooks/inject-coo-seat.mjs +78 -0
- package/scripts/claude-hooks/inject-dev-server-awareness.mjs +11 -0
- package/scripts/claude-hooks/inject-doctrine.mjs +13 -0
- package/scripts/claude-hooks/inject-vault-context.mjs +14 -0
- package/scripts/claude-hooks/intent-classifier-trigger.mjs +279 -0
- package/scripts/claude-hooks/log-session-activity.mjs +341 -0
- package/scripts/claude-hooks/naughty-list-gate.mjs +11 -0
- package/scripts/claude-hooks/notify-status-flip.mjs +72 -0
- package/scripts/claude-hooks/plan-exit-ingest-gate.mjs +170 -0
- package/scripts/claude-hooks/plan-ingest-close-reminder.mjs +66 -0
- package/scripts/claude-hooks/plan-review-gate.mjs +11 -0
- package/scripts/claude-hooks/preflight-reminder.mjs +262 -0
- package/scripts/claude-hooks/protected-action-intent.mjs +221 -0
- package/scripts/claude-hooks/research-method-guard.mjs +398 -0
- package/scripts/claude-hooks/research-protocol-gate.mjs +11 -0
- package/scripts/claude-hooks/session-end.mjs +451 -0
- package/scripts/claude-hooks/settings-write-gate.mjs +11 -0
- package/scripts/claude-hooks/settlement-check-gate.mjs +39 -0
- package/scripts/claude-hooks/status-flip-detect.mjs +17 -0
- package/scripts/claude-hooks/terminal-close-shared.mjs +426 -0
- package/scripts/claude-hooks/terminal-close-watcher.mjs +122 -0
- package/scripts/claude-hooks/vault-mirror-catchup.mjs +11 -0
- package/scripts/claude-hooks/vault-write-tier-guard.mjs +11 -0
- package/scripts/claude-hooks/wip-checkpoint.mjs +302 -0
- package/scripts/cursor-hooks/manifest.mjs +281 -0
- package/scripts/dev-server-status.mjs +34 -13
- package/scripts/dev-server-stop.mjs +313 -0
- package/scripts/heyspex-mode.mjs +288 -0
- package/scripts/hs-auto-resolve.mjs +240 -0
- package/scripts/hs-auto-source.mjs +398 -0
- package/scripts/hs-auto.mjs +134 -0
- package/scripts/hs-capture-cli.mjs +326 -0
- package/scripts/hs-classifier.mjs +136 -0
- package/scripts/hs-companion.mjs +88 -0
- package/scripts/hs-gateway.mjs +115 -0
- package/scripts/hs-mode.mjs +125 -0
- package/scripts/hs-next.mjs +441 -0
- package/scripts/hs-settings.mjs +338 -0
- package/scripts/hs-view.mjs +496 -0
- package/scripts/hs-worktree.mjs +201 -0
- package/scripts/install-claude-code-hooks.mjs +272 -0
- package/scripts/install-cursor-hooks.mjs +471 -0
- package/scripts/lib/agent-env-markers.mjs +38 -0
- package/scripts/lib/console-url-resolver.mjs +199 -61
- package/scripts/lib/customer-kit.mjs +1203 -0
- package/scripts/lib/dev-mode.mjs +249 -32
- package/scripts/lib/directive-obligations.mjs +349 -0
- package/scripts/lib/heyspex-home.mjs +72 -0
- package/scripts/lib/hook-dispositions.mjs +210 -0
- package/scripts/lib/hook-vault-header.mjs +30 -0
- package/scripts/lib/hs-command.mjs +277 -0
- package/scripts/lib/hs-companion-settings.mjs +472 -0
- package/scripts/lib/hs-source-branch.mjs +196 -0
- package/scripts/lib/hs-source-commit.mjs +59 -5
- package/scripts/lib/hs-source-identity.mjs +526 -0
- package/scripts/lib/hs-source-log.mjs +13 -22
- package/scripts/lib/hs-source-preflight.mjs +18 -4
- package/scripts/lib/hs-source-push.mjs +386 -307
- package/scripts/lib/hs-worktree-args.mjs +115 -0
- package/scripts/lib/shared-path-helpers.mjs +134 -0
- package/scripts/lib/standalone-doctor.mjs +540 -3
- package/scripts/lib/subagent-git-lock.mjs +109 -0
- package/scripts/lib/template-pin.mjs +34 -0
- package/scripts/lib/window-placement.mjs +109 -0
- package/scripts/provenance-lib.mjs +91 -0
- package/scripts/stamp-provenance-on-edit.mjs +222 -0
- package/scripts/wip-ledger-health.mjs +653 -0
- package/spexharness/doctrine/baseline/SPEXCODE-DOCTRINE.baseline.json +29 -0
- package/spexharness/src/adapters/audit.mjs +197 -0
- package/spexharness/src/adapters/claude-code.mjs +449 -0
- package/spexharness/src/adapters/copilot.mjs +236 -0
- package/spexharness/src/adapters/detect-harness.mjs +34 -0
- package/spexharness/src/chat/tui/ansi.mjs +448 -0
- package/spexharness/src/chat/tui/palette.mjs +22 -0
- package/spexharness/src/chat/tui/select.mjs +208 -0
- package/spexharness/src/config-root.mjs +54 -0
- package/spexharness/src/context-budget.mjs +141 -0
- package/spexharness/src/dev-server-control.mjs +389 -0
- package/spexharness/src/doctrine/behaviour.mjs +371 -0
- package/spexharness/src/doctrine/corpus.mjs +733 -0
- package/spexharness/src/doctrine/inject.mjs +338 -0
- package/spexharness/src/doctrine/merge.mjs +184 -0
- package/spexharness/src/doctrine/sources.mjs +252 -0
- package/spexharness/src/doctrine/synthesize.mjs +401 -0
- package/spexharness/src/doctrine/vault-source.mjs +574 -0
- package/spexharness/src/doctrine/writer.mjs +176 -0
- package/spexharness/src/events.mjs +84 -0
- package/spexharness/src/gates/_shell-write-targets.mjs +77 -0
- package/spexharness/src/gates/capture-workflow-gate.mjs +202 -0
- package/spexharness/src/gates/claim-before-code-gate.decide.mjs +33 -0
- package/spexharness/src/gates/claim-before-code-gate.mjs +129 -0
- package/spexharness/src/gates/deep-research-cost-gate.mjs +275 -0
- package/spexharness/src/gates/diagnose-capture-reminder.mjs +112 -0
- package/spexharness/src/gates/directive-obligation-gate.decide.mjs +132 -0
- package/spexharness/src/gates/directive-obligation-gate.mjs +183 -0
- package/spexharness/src/gates/enforce-session-sequence.mjs +895 -0
- package/spexharness/src/gates/enforce-vault-naming.mjs +215 -0
- package/spexharness/src/gates/epic-milestone-phrasing-gate.mjs +233 -0
- package/spexharness/src/gates/git-identity-guard.decide.mjs +436 -0
- package/spexharness/src/gates/git-identity-guard.mjs +61 -0
- package/spexharness/src/gates/git-reflog-audit.mjs +151 -0
- package/spexharness/src/gates/git-stash-gate.decide.mjs +361 -0
- package/spexharness/src/gates/git-stash-gate.mjs +159 -0
- package/spexharness/src/gates/id-display-format-gate.mjs +190 -0
- package/spexharness/src/gates/inject-doctrine-gate.mjs +89 -0
- package/spexharness/src/gates/inject-vault-context.mjs +239 -0
- package/spexharness/src/gates/naughty-list-gate.mjs +213 -0
- package/spexharness/src/gates/plan-review-gate.mjs +233 -0
- package/spexharness/src/gates/preflight-reminder-gate.mjs +265 -0
- package/spexharness/src/gates/research-protocol-gate.mjs +175 -0
- package/spexharness/src/gates/settings-write-gate.decide.mjs +96 -0
- package/spexharness/src/gates/settings-write-gate.mjs +104 -0
- package/spexharness/src/gates/vault-mirror-catchup.mjs +124 -0
- package/spexharness/src/gates/vault-write-tier-guard.decide.mjs +125 -0
- package/spexharness/src/gates/vault-write-tier-guard.mjs +42 -0
- package/spexharness/src/git-reflog-audit.mjs +294 -0
- package/spexharness/src/intent-classifier.mjs +196 -0
- package/spexharness/src/intent-directive.mjs +626 -0
- package/spexharness/src/profile/learn.mjs +551 -0
- package/spexharness/src/profile/proposals.mjs +357 -0
- package/spexharness/src/profile/vault-inclusion.mjs +386 -0
- package/spexharness/src/redact.mjs +151 -0
- package/spexharness/src/rule-table.mjs +849 -0
- package/spexharness/src/seat-context.mjs +122 -0
- package/spexharness/src/session-bootstrap.mjs +512 -0
- package/spexharness/src/session-record.mjs +815 -0
- package/spexharness/src/token-inventory.mjs +207 -0
- package/vendor/heyspex-mcp/account-switch.mjs +96 -0
- package/vendor/heyspex-mcp/alignment-gate.mjs +315 -1
- package/vendor/heyspex-mcp/blocker-resolver.mjs +37 -2
- package/vendor/heyspex-mcp/breakdown-ladder.mjs +119 -4
- package/vendor/heyspex-mcp/breakdown-source.mjs +271 -0
- package/vendor/heyspex-mcp/breakdown-writer.mjs +23 -3
- package/vendor/heyspex-mcp/capture-buffer-patches.mjs +77 -8
- package/vendor/heyspex-mcp/capture-channel.mjs +55 -5
- package/vendor/heyspex-mcp/capture-console-client.mjs +117 -3
- package/vendor/heyspex-mcp/capture-focus.mjs +1061 -31
- package/vendor/heyspex-mcp/capture-outbox.mjs +15 -5
- package/vendor/heyspex-mcp/capture-sources.mjs +195 -0
- package/vendor/heyspex-mcp/capture-tools.mjs +1264 -0
- package/vendor/heyspex-mcp/claim-tools.mjs +1147 -0
- package/vendor/heyspex-mcp/cli-verbs.mjs +2074 -0
- package/vendor/heyspex-mcp/cli.mjs +207 -15524
- package/vendor/heyspex-mcp/close-branch-report.mjs +102 -0
- package/vendor/heyspex-mcp/cloud-tool-surface.mjs +128 -0
- package/vendor/heyspex-mcp/cloud-vault-writes.mjs +602 -0
- package/vendor/heyspex-mcp/config-seed.yaml +15 -0
- package/vendor/heyspex-mcp/config-settings.mjs +290 -7
- package/vendor/heyspex-mcp/console-fetch.mjs +822 -17
- package/vendor/heyspex-mcp/console-transport.mjs +172 -0
- package/vendor/heyspex-mcp/credentials.mjs +585 -0
- package/vendor/heyspex-mcp/defer-spec.mjs +44 -4
- package/vendor/heyspex-mcp/deferred-reclaim.mjs +29 -3
- package/vendor/heyspex-mcp/design-handlers.mjs +31 -0
- package/vendor/heyspex-mcp/design-tools.mjs +4 -1
- package/vendor/heyspex-mcp/dev-factory-bridge.mjs +4 -4
- package/vendor/heyspex-mcp/dev-mode-core.mjs +45 -0
- package/vendor/heyspex-mcp/dev-tools.mjs +131 -0
- package/vendor/heyspex-mcp/diagnose-handlers.mjs +54 -0
- package/vendor/heyspex-mcp/events-tools.mjs +207 -0
- package/vendor/heyspex-mcp/filing-tools.mjs +723 -0
- package/vendor/heyspex-mcp/fleet-handlers.mjs +37 -0
- package/vendor/heyspex-mcp/fleet-queue-tools.mjs +71 -15
- package/vendor/heyspex-mcp/focus-tools.mjs +190 -0
- package/vendor/heyspex-mcp/folder-trust-store.mjs +60 -0
- package/vendor/heyspex-mcp/http-transport.mjs +302 -0
- package/vendor/heyspex-mcp/list-tools.mjs +257 -0
- package/vendor/heyspex-mcp/login-handoff.mjs +15 -1
- package/vendor/heyspex-mcp/mcp-config.mjs +107 -0
- package/vendor/heyspex-mcp/memory-tools.mjs +523 -0
- package/vendor/heyspex-mcp/mvp-mode.mjs +53 -6
- package/vendor/heyspex-mcp/obligation-backstop.mjs +103 -0
- package/vendor/heyspex-mcp/occupied-keys.mjs +142 -0
- package/vendor/heyspex-mcp/on-deck-brief.mjs +3377 -0
- package/vendor/heyspex-mcp/on-deck-scheduler.mjs +1708 -0
- package/vendor/heyspex-mcp/package-root.mjs +32 -0
- package/vendor/heyspex-mcp/peer-presence.mjs +8 -1
- package/vendor/heyspex-mcp/plan-ground.mjs +621 -652
- package/vendor/heyspex-mcp/plan-push.mjs +908 -22
- package/vendor/heyspex-mcp/plan-review.mjs +124 -30
- package/vendor/heyspex-mcp/preflight-gate.mjs +0 -0
- package/vendor/heyspex-mcp/procedure-handlers.mjs +327 -0
- package/vendor/heyspex-mcp/procedure-tools.mjs +1181 -970
- package/vendor/heyspex-mcp/promote-capture.mjs +388 -69
- package/vendor/heyspex-mcp/promote-diagnose.mjs +74 -0
- package/vendor/heyspex-mcp/promote-research-to-diagnose.mjs +9 -2
- package/vendor/heyspex-mcp/promote-research.mjs +666 -53
- package/vendor/heyspex-mcp/promotion-helpers.mjs +251 -0
- package/vendor/heyspex-mcp/prompts.mjs +1460 -0
- package/vendor/heyspex-mcp/read-path.mjs +191 -15
- package/vendor/heyspex-mcp/receipt-shape.mjs +146 -0
- package/vendor/heyspex-mcp/recipe-handlers.mjs +117 -0
- package/vendor/heyspex-mcp/regen-tasks.mjs +210 -3
- package/vendor/heyspex-mcp/register-spec.mjs +266 -54
- package/vendor/heyspex-mcp/request-context.mjs +83 -0
- package/vendor/heyspex-mcp/research-focus.mjs +207 -4
- package/vendor/heyspex-mcp/research-handlers.mjs +625 -0
- package/vendor/heyspex-mcp/research-tools.mjs +4 -4
- package/vendor/heyspex-mcp/response-budget.mjs +348 -0
- package/vendor/heyspex-mcp/review-focus.mjs +25 -1
- package/vendor/heyspex-mcp/roadmap-default.mjs +70 -0
- package/vendor/heyspex-mcp/search-specs.mjs +280 -0
- package/vendor/heyspex-mcp/seat-block-ledger.mjs +113 -0
- package/vendor/heyspex-mcp/seat-identity.mjs +2 -6
- package/vendor/heyspex-mcp/session-owner.mjs +109 -0
- package/vendor/heyspex-mcp/session-tools.mjs +162 -0
- package/vendor/heyspex-mcp/settings-registry.mjs +232 -0
- package/vendor/heyspex-mcp/settings-tools.mjs +85 -12
- package/vendor/heyspex-mcp/spec-edit-tools.mjs +453 -0
- package/vendor/heyspex-mcp/spec-id-lib.mjs +93 -17
- package/vendor/heyspex-mcp/split-research.mjs +304 -56
- package/vendor/heyspex-mcp/stdio-rpc.mjs +22 -0
- package/vendor/heyspex-mcp/submit-for-review.mjs +150 -7
- package/vendor/heyspex-mcp/summary-contract.mjs +105 -0
- package/vendor/heyspex-mcp/summary-write-tools.mjs +228 -0
- package/vendor/heyspex-mcp/symptom-query.mjs +146 -0
- package/vendor/heyspex-mcp/test-helpers.mjs +123 -1
- package/vendor/heyspex-mcp/tool-catalog.mjs +2819 -0
- package/vendor/heyspex-mcp/tool-dispatch.mjs +657 -0
- package/vendor/heyspex-mcp/tool-helpers.mjs +325 -0
- package/vendor/heyspex-mcp/validate-linkage.mjs +414 -27
- package/vendor/heyspex-mcp/vault-io.mjs +720 -0
- package/vendor/heyspex-mcp/vault-less-endpoints.mjs +52 -0
- package/vendor/heyspex-mcp/vault-resolve.mjs +554 -68
- package/vendor/heyspex-mcp/verify-reproduction.mjs +270 -30
- package/vendor/heyspex-mcp/whoami.mjs +92 -0
- package/vendor/heyspex-mcp/write-outbox.mjs +512 -0
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
// spexharness/src/profile/vault-inclusion.mjs
|
|
2
|
+
//
|
|
3
|
+
// P7 (epic [[HS-HEBV-8CMW-EPIC-spexcode-profile-v0-learn-the-developer]], task
|
|
4
|
+
// [[HS-TYX1-8CMW-TASK-p7-vault-inclusion-by-reference-and]]) — two things bolted onto
|
|
5
|
+
// P1's profile file without touching learn.mjs itself:
|
|
6
|
+
//
|
|
7
|
+
// 1. Vault inclusion (scope item 1): every launch, every vault `memory-decision` and
|
|
8
|
+
// `memory-lesson` (plus `memory-ui-pattern` when the caller says this is console UI
|
|
9
|
+
// work) is merged into the profile BY REFERENCE — id plus one line, never the
|
|
10
|
+
// memory's body. Same recall path the harness already uses for prompt-submit vault
|
|
11
|
+
// grounding: `scripts/claude-hooks/_vault-retrieval.mjs`'s `loadVaultIndex(repoRoot)`,
|
|
12
|
+
// which reads the machine-local `.heyspex/vault-index.json` — this module never
|
|
13
|
+
// re-implements that read or re-parses a vault markdown file itself. Because the
|
|
14
|
+
// references are recomputed live from the index on every call, a memory removed from
|
|
15
|
+
// the vault (and so absent from a rebuilt index) simply stops appearing — no explicit
|
|
16
|
+
// "delete" path is needed.
|
|
17
|
+
//
|
|
18
|
+
// 2. Supersession (scope items 2-3): a new candidate observation that CONTRADICTS an
|
|
19
|
+
// already-accepted profile entry (same subject, opposite stance — including an
|
|
20
|
+
// explicit "I changed my mind about X") never retires anything silently. It is routed
|
|
21
|
+
// through P6's own proposal queue (`./proposals.mjs`'s `queueProposal`/`accept`) as an
|
|
22
|
+
// accept/approve ask, carrying a `supersedes` pointer at the matched entry. Only once
|
|
23
|
+
// that proposal is APPROVED does the old entry move under a `## retired` heading (with
|
|
24
|
+
// the date) and the new entry's line gain a `supersedes: <retired id>` reference. The
|
|
25
|
+
// contradiction alone changes nothing.
|
|
26
|
+
//
|
|
27
|
+
// Reuse, never duplicate: section state and rendering are P1's (`learn.mjs`'s
|
|
28
|
+
// `loadProfileState`/`saveProfileState`/`classifySection`/`renderProfileMarkdown`); the
|
|
29
|
+
// accept/decline ledger is P6's (`./proposals.mjs`). This module owns only: reading the
|
|
30
|
+
// vault index for memory references, detecting a same-subject/opposite-stance contradiction,
|
|
31
|
+
// the retired-entries store, and composing the final profile markdown (learned sections +
|
|
32
|
+
// vault references + retired) from all three.
|
|
33
|
+
|
|
34
|
+
import { createHash } from 'node:crypto';
|
|
35
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
36
|
+
import { dirname, join } from 'node:path';
|
|
37
|
+
|
|
38
|
+
import { loadProfileState, renderProfileMarkdown, saveProfileState } from './learn.mjs';
|
|
39
|
+
import { accept, listPending, normalizeFingerprint, queueProposal } from './proposals.mjs';
|
|
40
|
+
import { loadVaultIndex } from '../../../scripts/claude-hooks/_vault-retrieval.mjs';
|
|
41
|
+
|
|
42
|
+
export { loadVaultIndex };
|
|
43
|
+
|
|
44
|
+
// ── paths ─────────────────────────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
/** The retired-entries store's canonical, machine-local location. */
|
|
47
|
+
export function defaultRetiredStorePath(repoRoot) {
|
|
48
|
+
return join(repoRoot, '.heyspex', 'profile-retired.json');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ── vault inclusion (scope item 1) ──────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
/** The vault memory `type`s this module ever includes by reference — `memory-ui-pattern`
|
|
54
|
+
* only when the caller opts in (`includeUiPatterns`), since it is only relevant to
|
|
55
|
+
* console UI work, not every session. */
|
|
56
|
+
export const VAULT_MEMORY_TYPES = Object.freeze(['memory-decision', 'memory-lesson']);
|
|
57
|
+
export const VAULT_UI_PATTERN_TYPE = 'memory-ui-pattern';
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Every vault memory (decision/lesson, plus ui-pattern when opted in) currently in the
|
|
61
|
+
* index — id, type, title, summary only, sorted by id for a deterministic render. Reads
|
|
62
|
+
* the index fresh every call: a memory removed from the vault since the index was last
|
|
63
|
+
* built is simply absent on the next call, no stale reference carried forward.
|
|
64
|
+
*
|
|
65
|
+
* @param {{ repoRoot: string, includeUiPatterns?: boolean, loadIndex?: typeof loadVaultIndex }} opts
|
|
66
|
+
* @returns {Array<{ id: string, type: string, title: string, summary: string|null }>}
|
|
67
|
+
*/
|
|
68
|
+
export function collectVaultReferences({ repoRoot, includeUiPatterns = false, loadIndex = loadVaultIndex } = {}) {
|
|
69
|
+
if (!repoRoot) throw new Error('collectVaultReferences: repoRoot is required');
|
|
70
|
+
const wantedTypes = new Set(VAULT_MEMORY_TYPES);
|
|
71
|
+
if (includeUiPatterns) wantedTypes.add(VAULT_UI_PATTERN_TYPE);
|
|
72
|
+
|
|
73
|
+
const specs = loadIndex(repoRoot) ?? [];
|
|
74
|
+
const refs = [];
|
|
75
|
+
for (const spec of specs) {
|
|
76
|
+
if (!spec || !wantedTypes.has(spec.type)) continue;
|
|
77
|
+
refs.push({
|
|
78
|
+
id: spec.id,
|
|
79
|
+
type: spec.type,
|
|
80
|
+
title: spec.title ?? spec.id,
|
|
81
|
+
summary: spec.summary ?? spec.meta_description ?? null,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
refs.sort((a, b) => String(a.id).localeCompare(String(b.id)));
|
|
85
|
+
return refs;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** One reference line — id plus one line, never the memory's body. */
|
|
89
|
+
export function vaultReferenceLine(ref) {
|
|
90
|
+
const type = ref.type ? String(ref.type).toUpperCase() : 'MEMORY';
|
|
91
|
+
const title = String(ref.title ?? ref.id);
|
|
92
|
+
const line = `- ${ref.id} (${type}) ${title}`;
|
|
93
|
+
if (!ref.summary) return line;
|
|
94
|
+
return `${line} — ${String(ref.summary).replace(/\s+/g, ' ').trim()}`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** The "## Vault references" block, or `''` when there is nothing to include (the whole
|
|
98
|
+
* heading is omitted, same "empty section omitted" rule `renderProfileMarkdown` follows). */
|
|
99
|
+
export function renderVaultReferencesSection(refs) {
|
|
100
|
+
if (!refs || refs.length === 0) return '';
|
|
101
|
+
const lines = ['## Vault references', ''];
|
|
102
|
+
for (const ref of refs) lines.push(vaultReferenceLine(ref));
|
|
103
|
+
return lines.join('\n');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ── retired store + rendering (scope item 3) ────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
/** Read the retired-entries store; missing/corrupt -> []. */
|
|
109
|
+
export function loadRetiredStore(path) {
|
|
110
|
+
if (!existsSync(path)) return [];
|
|
111
|
+
try {
|
|
112
|
+
const parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
113
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
114
|
+
} catch {
|
|
115
|
+
return [];
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Persist the retired-entries store. */
|
|
120
|
+
export function saveRetiredStore(path, entries) {
|
|
121
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
122
|
+
writeFileSync(path, `${JSON.stringify(entries, null, 2)}\n`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** One retired line — the old entry's text, its retirement date, and what superseded it. */
|
|
126
|
+
export function retiredLine(entry) {
|
|
127
|
+
return `- ${entry.text} _(retired: ${entry.retiredAt}, superseded by: ${entry.supersededBy})_`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** The "## retired" block, or `''` when nothing has ever been retired. */
|
|
131
|
+
export function renderRetiredSection(entries) {
|
|
132
|
+
if (!entries || entries.length === 0) return '';
|
|
133
|
+
const lines = ['## retired', ''];
|
|
134
|
+
for (const entry of entries) lines.push(retiredLine(entry));
|
|
135
|
+
return lines.join('\n');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** A short, stable id for a retired entry, derived from its own fingerprint — used as the
|
|
139
|
+
* `supersedes` pointer a newly accepted entry's line cites. */
|
|
140
|
+
export function retiredIdForFingerprint(fingerprint) {
|
|
141
|
+
return `RETIRED-${createHash('sha1').update(String(fingerprint ?? '')).digest('hex').slice(0, 10)}`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ── compose (pure — learned sections + vault references + retired) ─────────────────
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Assemble the full profile markdown from all three stores. Pure, no I/O — the same
|
|
148
|
+
* "always rebuild the whole document fresh from persisted state" discipline
|
|
149
|
+
* `renderProfileMarkdown` itself follows, just composed with two more sections.
|
|
150
|
+
*
|
|
151
|
+
* @param {{ sections: object, vaultReferences?: Array, retiredEntries?: Array }} opts
|
|
152
|
+
* @returns {string}
|
|
153
|
+
*/
|
|
154
|
+
export function composeProfileMarkdown({ sections, vaultReferences = [], retiredEntries = [] }) {
|
|
155
|
+
const base = renderProfileMarkdown(sections).replace(/\n+$/, '');
|
|
156
|
+
const blocks = [base, renderVaultReferencesSection(vaultReferences), renderRetiredSection(retiredEntries)].filter(
|
|
157
|
+
(b) => b.length > 0,
|
|
158
|
+
);
|
|
159
|
+
return `${blocks.join('\n\n')}\n`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* The "at every launch" entrypoint for scope item 1: recompute vault references live,
|
|
164
|
+
* recompose the full profile (learned sections + vault refs + whatever is already
|
|
165
|
+
* retired), and write it. Never touches the proposal ledger — vault inclusion is
|
|
166
|
+
* automatic, with no proposal step.
|
|
167
|
+
*
|
|
168
|
+
* @param {{ repoRoot: string, profilePath: string, profileStatePath: string,
|
|
169
|
+
* retiredStorePath?: string|null, includeUiPatterns?: boolean,
|
|
170
|
+
* loadIndex?: typeof loadVaultIndex }} opts
|
|
171
|
+
* @returns {{ profilePath: string, markdown: string, vaultReferences: Array }}
|
|
172
|
+
*/
|
|
173
|
+
export function writeProfile({
|
|
174
|
+
repoRoot,
|
|
175
|
+
profilePath,
|
|
176
|
+
profileStatePath,
|
|
177
|
+
retiredStorePath = null,
|
|
178
|
+
includeUiPatterns = false,
|
|
179
|
+
loadIndex = loadVaultIndex,
|
|
180
|
+
} = {}) {
|
|
181
|
+
if (!repoRoot) throw new Error('writeProfile: repoRoot is required');
|
|
182
|
+
if (!profilePath) throw new Error('writeProfile: profilePath is required');
|
|
183
|
+
if (!profileStatePath) throw new Error('writeProfile: profileStatePath is required');
|
|
184
|
+
|
|
185
|
+
const state = loadProfileState(profileStatePath);
|
|
186
|
+
const vaultReferences = collectVaultReferences({ repoRoot, includeUiPatterns, loadIndex });
|
|
187
|
+
const retiredEntries = retiredStorePath ? loadRetiredStore(retiredStorePath) : [];
|
|
188
|
+
const markdown = composeProfileMarkdown({ sections: state.sections, vaultReferences, retiredEntries });
|
|
189
|
+
|
|
190
|
+
mkdirSync(dirname(profilePath), { recursive: true });
|
|
191
|
+
writeFileSync(profilePath, markdown);
|
|
192
|
+
return { profilePath, markdown, vaultReferences };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ── contradiction detection (scope item 2) ──────────────────────────────────────────
|
|
196
|
+
|
|
197
|
+
const NEGATIVE_STANCE_MARKERS = [
|
|
198
|
+
/\bnever\b/i,
|
|
199
|
+
/\bdon'?t\b/i,
|
|
200
|
+
/\bdo not\b/i,
|
|
201
|
+
/\bstop\b/i,
|
|
202
|
+
/\bavoid\b/i,
|
|
203
|
+
/\bno longer\b/i,
|
|
204
|
+
/\bnot anymore\b/i,
|
|
205
|
+
/\bchanged (my|your|their) mind\b/i,
|
|
206
|
+
];
|
|
207
|
+
|
|
208
|
+
const POSITIVE_STANCE_MARKERS = [/\balways\b/i, /\bprefer(s|red)?\b/i, /\bfrom now on\b/i];
|
|
209
|
+
|
|
210
|
+
/** Words a stance marker itself introduces — dropped from the "subject" comparison so
|
|
211
|
+
* "always use tabs" and "never use tabs" compare on {use, tabs}, not on the polarity word. */
|
|
212
|
+
const POLARITY_WORDS = new Set([
|
|
213
|
+
'never', 'always', 'dont', 'do', 'not', 'stop', 'avoid', 'longer', 'anymore', 'changed', 'mind', 'now',
|
|
214
|
+
]);
|
|
215
|
+
|
|
216
|
+
const MIN_SUBJECT_WORD_LENGTH = 4;
|
|
217
|
+
|
|
218
|
+
function stanceOf(text) {
|
|
219
|
+
const t = String(text ?? '');
|
|
220
|
+
if (NEGATIVE_STANCE_MARKERS.some((re) => re.test(t))) return 'negative';
|
|
221
|
+
if (POSITIVE_STANCE_MARKERS.some((re) => re.test(t))) return 'positive';
|
|
222
|
+
return 'neutral';
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function subjectWords(text) {
|
|
226
|
+
const words = normalizeFingerprint(text)
|
|
227
|
+
.split(' ')
|
|
228
|
+
.filter((w) => w.length >= MIN_SUBJECT_WORD_LENGTH && !POLARITY_WORDS.has(w));
|
|
229
|
+
return new Set(words);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function jaccard(a, b) {
|
|
233
|
+
if (a.size === 0 || b.size === 0) return 0;
|
|
234
|
+
let intersection = 0;
|
|
235
|
+
for (const w of a) if (b.has(w)) intersection += 1;
|
|
236
|
+
return intersection / (a.size + b.size - intersection);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Does `observation` contradict an already-accepted profile entry — the same subject,
|
|
241
|
+
* opposite stance (an explicit "I changed my mind about X" counts as a negative-stance
|
|
242
|
+
* marker too)? Returns the first match found (accepted entries are checked in section
|
|
243
|
+
* order, then array order), or `null`.
|
|
244
|
+
*
|
|
245
|
+
* @param {{ text: string }} observation
|
|
246
|
+
* @param {object} sections - the same shape `learn.mjs`'s profile-state `sections` carries.
|
|
247
|
+
* @param {{ minOverlap?: number }} [opts]
|
|
248
|
+
* @returns {{ sectionKey: string, entry: object, overlap: number }|null}
|
|
249
|
+
*/
|
|
250
|
+
export function detectContradiction(observation, sections, { minOverlap = 0.34 } = {}) {
|
|
251
|
+
const text = observation?.text;
|
|
252
|
+
const newStance = stanceOf(text);
|
|
253
|
+
if (newStance === 'neutral') return null;
|
|
254
|
+
const newSubject = subjectWords(text);
|
|
255
|
+
if (newSubject.size === 0) return null;
|
|
256
|
+
|
|
257
|
+
for (const sectionKey of Object.keys(sections ?? {})) {
|
|
258
|
+
for (const entry of sections[sectionKey] ?? []) {
|
|
259
|
+
const entryStance = stanceOf(entry.text);
|
|
260
|
+
if (entryStance === 'neutral' || entryStance === newStance) continue;
|
|
261
|
+
const overlap = jaccard(newSubject, subjectWords(entry.text));
|
|
262
|
+
if (overlap >= minOverlap) return { sectionKey, entry, overlap };
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Run `detectContradiction` against the CURRENT accepted profile state and, on a match,
|
|
270
|
+
* queue an accept/approve proposal through P6's own queue (never retires anything itself —
|
|
271
|
+
* that only happens on `approveSupersession`). No match -> `{ contradiction: false }`, and
|
|
272
|
+
* the observation is left for the caller's normal (non-supersession) proposal path.
|
|
273
|
+
*
|
|
274
|
+
* @param {object} observation
|
|
275
|
+
* @param {{ profileStatePath: string, ledgerPath: string, now?: () => string }} opts
|
|
276
|
+
* @returns {{ contradiction: boolean, queued?: boolean, id?: string, matched?: object }}
|
|
277
|
+
*/
|
|
278
|
+
export function checkForContradictionAndPropose(observation, { profileStatePath, ledgerPath, now } = {}) {
|
|
279
|
+
if (!profileStatePath) throw new Error('checkForContradictionAndPropose: profileStatePath is required');
|
|
280
|
+
if (!ledgerPath) throw new Error('checkForContradictionAndPropose: ledgerPath is required');
|
|
281
|
+
|
|
282
|
+
const state = loadProfileState(profileStatePath);
|
|
283
|
+
const match = detectContradiction(observation, state.sections);
|
|
284
|
+
if (!match) return { contradiction: false };
|
|
285
|
+
|
|
286
|
+
const supersedes = {
|
|
287
|
+
sectionKey: match.sectionKey,
|
|
288
|
+
fingerprint: normalizeFingerprint(match.entry.text),
|
|
289
|
+
};
|
|
290
|
+
const result = queueProposal({ ...observation, supersedes }, { ledgerPath, now });
|
|
291
|
+
return { contradiction: true, queued: result.queued, id: result.id, matched: match.entry };
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// ── approval: the ONLY path that ever retires an entry ──────────────────────────────
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Approve a pending supersession proposal (queued by `checkForContradictionAndPropose`).
|
|
298
|
+
* Accepts it through P6's own `accept()` (writing the new entry with its usual
|
|
299
|
+
* provenance), then — and ONLY on this explicit approval — moves the matched old entry
|
|
300
|
+
* under `## retired` with the date, and appends a `supersedes: <retired id>` reference to
|
|
301
|
+
* the new entry's line. A plain accept (no `supersedes` on the queued observation) behaves
|
|
302
|
+
* exactly like P6's own `accept()` — no retirement, nothing extra.
|
|
303
|
+
*
|
|
304
|
+
* @param {string} id
|
|
305
|
+
* @param {{ ledgerPath: string, profilePath: string, profileStatePath: string,
|
|
306
|
+
* retiredStorePath: string, repoRoot: string, includeUiPatterns?: boolean,
|
|
307
|
+
* loadIndex?: typeof loadVaultIndex, now?: () => string }} opts
|
|
308
|
+
* @returns {{ ok: boolean, id: string, reason?: string, retired?: object|null, retiredAt?: string|null }}
|
|
309
|
+
*/
|
|
310
|
+
export function approveSupersession(id, {
|
|
311
|
+
ledgerPath,
|
|
312
|
+
profilePath,
|
|
313
|
+
profileStatePath,
|
|
314
|
+
retiredStorePath,
|
|
315
|
+
repoRoot,
|
|
316
|
+
includeUiPatterns = false,
|
|
317
|
+
loadIndex = loadVaultIndex,
|
|
318
|
+
now = () => new Date().toISOString(),
|
|
319
|
+
} = {}) {
|
|
320
|
+
if (!retiredStorePath) throw new Error('approveSupersession: retiredStorePath is required');
|
|
321
|
+
if (!repoRoot) throw new Error('approveSupersession: repoRoot is required');
|
|
322
|
+
|
|
323
|
+
const pending = listPending({ ledgerPath }).find((p) => p.id === id);
|
|
324
|
+
const supersedes = pending?.observation?.supersedes ?? null;
|
|
325
|
+
|
|
326
|
+
const acceptResult = accept(id, { ledgerPath, profilePath, profileStatePath, now });
|
|
327
|
+
if (!acceptResult.ok) return acceptResult;
|
|
328
|
+
|
|
329
|
+
if (!supersedes) {
|
|
330
|
+
return { ok: true, id, retired: null, retiredAt: null };
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const state = loadProfileState(profileStatePath);
|
|
334
|
+
const entries = state.sections[supersedes.sectionKey] ?? [];
|
|
335
|
+
const idx = entries.findIndex((e) => normalizeFingerprint(e.text) === supersedes.fingerprint);
|
|
336
|
+
let retiredEntry = null;
|
|
337
|
+
if (idx !== -1) {
|
|
338
|
+
[retiredEntry] = entries.splice(idx, 1);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const retiredAt = now();
|
|
342
|
+
const retiredStore = loadRetiredStore(retiredStorePath);
|
|
343
|
+
let retiredRecord = null;
|
|
344
|
+
if (retiredEntry) {
|
|
345
|
+
const retiredId = retiredIdForFingerprint(supersedes.fingerprint);
|
|
346
|
+
retiredRecord = { id: retiredId, ...retiredEntry, retiredAt, supersededBy: id };
|
|
347
|
+
retiredStore.push(retiredRecord);
|
|
348
|
+
|
|
349
|
+
// Tag the newly accepted entry's own line with the supersedes pointer — it is always
|
|
350
|
+
// the LAST entry in its own section (accept() only ever pushes), unaffected by the
|
|
351
|
+
// splice above even when both entries happen to share a section.
|
|
352
|
+
const newSectionEntries = state.sections[acceptResult.section] ?? [];
|
|
353
|
+
const newest = newSectionEntries[newSectionEntries.length - 1];
|
|
354
|
+
if (newest) newest.text = `${newest.text} _(supersedes: ${retiredId})_`;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
saveProfileState(profileStatePath, state);
|
|
358
|
+
saveRetiredStore(retiredStorePath, retiredStore);
|
|
359
|
+
|
|
360
|
+
const vaultReferences = collectVaultReferences({ repoRoot, includeUiPatterns, loadIndex });
|
|
361
|
+
const markdown = composeProfileMarkdown({ sections: state.sections, vaultReferences, retiredEntries: retiredStore });
|
|
362
|
+
mkdirSync(dirname(profilePath), { recursive: true });
|
|
363
|
+
writeFileSync(profilePath, markdown);
|
|
364
|
+
|
|
365
|
+
return { ok: true, id, retired: retiredRecord, retiredAt, markdown };
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export default {
|
|
369
|
+
defaultRetiredStorePath,
|
|
370
|
+
VAULT_MEMORY_TYPES,
|
|
371
|
+
VAULT_UI_PATTERN_TYPE,
|
|
372
|
+
collectVaultReferences,
|
|
373
|
+
vaultReferenceLine,
|
|
374
|
+
renderVaultReferencesSection,
|
|
375
|
+
loadRetiredStore,
|
|
376
|
+
saveRetiredStore,
|
|
377
|
+
retiredLine,
|
|
378
|
+
renderRetiredSection,
|
|
379
|
+
retiredIdForFingerprint,
|
|
380
|
+
composeProfileMarkdown,
|
|
381
|
+
writeProfile,
|
|
382
|
+
detectContradiction,
|
|
383
|
+
checkForContradictionAndPropose,
|
|
384
|
+
approveSupersession,
|
|
385
|
+
loadVaultIndex,
|
|
386
|
+
};
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// spexharness/src/redact.mjs
|
|
2
|
+
//
|
|
3
|
+
// The ONE redaction module every log sink calls before a byte reaches disk
|
|
4
|
+
// ([[HS-Y424-NYPR-TASK-add-one-redaction-module-that-strips]] of
|
|
5
|
+
// [[HS-KRK5-NYPR-EPIC-harness-security-customer-hooks]]).
|
|
6
|
+
//
|
|
7
|
+
// Two call sites today (the epic's own listed sinks — "session record, run
|
|
8
|
+
// records" — both live in spexharness):
|
|
9
|
+
// - ../session-record.mjs's `appendSessionRecord` — the session record
|
|
10
|
+
// (ask, tool-call inputs/outputs, event decisions).
|
|
11
|
+
// - ./adapters/audit.mjs's `recordGuardrailAudit` — the guardrail-history
|
|
12
|
+
// row, whose `matchContext` field is literally a snippet of whatever text
|
|
13
|
+
// tripped a rule (naughty-list-gate) — the single field most likely to
|
|
14
|
+
// carry ingested prose verbatim.
|
|
15
|
+
//
|
|
16
|
+
// WHAT GETS STRIPPED (a closed, named set — never a generic high-entropy
|
|
17
|
+
// heuristic, which would false-positive on git SHAs and other ordinary hex
|
|
18
|
+
// content a record legitimately carries):
|
|
19
|
+
// - fixture/provider KEY SHAPES — the same closed vocabulary
|
|
20
|
+
// scripts/scan-key-custody.mjs scans FOR (OpenAI/DeepSeek `sk-`,
|
|
21
|
+
// Anthropic `sk-ant-`, AWS `AKIA…`, GitHub `ghp_…`/`github_pat_…`,
|
|
22
|
+
// Google `AIza…`, Slack `xox…`, a JWT). That scan proves a key never
|
|
23
|
+
// LANDS in a committed/generated file; THIS module is the reason it
|
|
24
|
+
// doesn't even reach the byte stream those files are made of.
|
|
25
|
+
// - BEARER TOKENS — `Authorization: Bearer <token>` (and a bare
|
|
26
|
+
// `Bearer <token>` fragment) wherever it appears in a string field.
|
|
27
|
+
// - INGESTED PROSE — a caller marks a block of untrusted/ingested content
|
|
28
|
+
// (a fetched document, a scraped page — [[uc-ingest-anywhere]]'s
|
|
29
|
+
// preview/confirm boundary) by wrapping it with
|
|
30
|
+
// {@link wrapIngestedProse} before it ever reaches a record; this module
|
|
31
|
+
// recognizes that marker pair and strips the ENTIRE marked block,
|
|
32
|
+
// regardless of what it contains.
|
|
33
|
+
//
|
|
34
|
+
// Pure: no I/O. `redactRecord`/`redactDeep` never mutate their input — every
|
|
35
|
+
// sink gets a redacted COPY to serialize, so the in-memory record a caller
|
|
36
|
+
// keeps using (or hands to another consumer) is never silently rewritten out
|
|
37
|
+
// from under it.
|
|
38
|
+
|
|
39
|
+
/** Sentinel pair a caller wraps a block of untrusted/ingested prose in, so this module can find and strip the WHOLE block regardless of content. */
|
|
40
|
+
export const INGESTED_PROSE_MARKERS = Object.freeze({
|
|
41
|
+
start: '☃HEYSPEX-INGESTED-PROSE-START☃',
|
|
42
|
+
end: '☃HEYSPEX-INGESTED-PROSE-END☃',
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
/** Wrap `text` as an ingested-prose block — the marker pair {@link redactText} strips wholesale. */
|
|
46
|
+
export function wrapIngestedProse(text) {
|
|
47
|
+
return `${INGESTED_PROSE_MARKERS.start}${text}${INGESTED_PROSE_MARKERS.end}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function escapeRegExp(literal) {
|
|
51
|
+
return literal.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const INGESTED_PROSE_RE = new RegExp(
|
|
55
|
+
`${escapeRegExp(INGESTED_PROSE_MARKERS.start)}[\\s\\S]*?${escapeRegExp(INGESTED_PROSE_MARKERS.end)}`,
|
|
56
|
+
'g',
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Closed vocabulary of key-shaped patterns — kept in sync by hand with
|
|
61
|
+
* scripts/scan-key-custody.mjs's own table (same shapes, different job: that
|
|
62
|
+
* script SCANS for a leak that already happened; this module PREVENTS one
|
|
63
|
+
* from reaching disk in the first place).
|
|
64
|
+
* @type {ReadonlyArray<{ id: string, re: RegExp }>}
|
|
65
|
+
*/
|
|
66
|
+
export const KEY_SHAPE_PATTERNS = Object.freeze([
|
|
67
|
+
{ id: 'openai-or-deepseek-secret-key', re: /\bsk-[A-Za-z0-9]{20,}\b/g },
|
|
68
|
+
{ id: 'anthropic-secret-key', re: /\bsk-ant-[A-Za-z0-9_-]{20,}\b/g },
|
|
69
|
+
{ id: 'aws-access-key-id', re: /\bAKIA[0-9A-Z]{16}\b/g },
|
|
70
|
+
{ id: 'github-token', re: /\b(ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{36,}\b/g },
|
|
71
|
+
{ id: 'github-fine-grained-pat', re: /\bgithub_pat_[A-Za-z0-9_]{20,}\b/g },
|
|
72
|
+
{ id: 'google-api-key', re: /\bAIza[0-9A-Za-z\-_]{35,}\b/g },
|
|
73
|
+
{ id: 'slack-token', re: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g },
|
|
74
|
+
{ id: 'jwt', re: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g },
|
|
75
|
+
]);
|
|
76
|
+
|
|
77
|
+
/** `Authorization: Bearer <token>` or a bare `Bearer <token>` fragment. */
|
|
78
|
+
const BEARER_TOKEN_RE = /\bBearer\s+[A-Za-z0-9\-._~+/]{8,}=*/g;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Redact one string: ingested-prose blocks first (a whole-block replace, so
|
|
82
|
+
* a key shape ALSO inside a marked block is subsumed by the block redaction
|
|
83
|
+
* rather than double-processed), then every named key shape, then bearer
|
|
84
|
+
* tokens. Non-string input is returned unchanged (callers use
|
|
85
|
+
* {@link redactDeep} for a whole object/array).
|
|
86
|
+
*
|
|
87
|
+
* @param {unknown} input
|
|
88
|
+
* @returns {unknown}
|
|
89
|
+
*/
|
|
90
|
+
export function redactText(input) {
|
|
91
|
+
if (typeof input !== 'string' || input.length === 0) return input;
|
|
92
|
+
let out = input.replace(INGESTED_PROSE_RE, '[REDACTED:ingested-prose]');
|
|
93
|
+
for (const { id, re } of KEY_SHAPE_PATTERNS) {
|
|
94
|
+
out = out.replace(re, `[REDACTED:${id}]`);
|
|
95
|
+
}
|
|
96
|
+
out = out.replace(BEARER_TOKEN_RE, '[REDACTED:bearer-token]');
|
|
97
|
+
return out;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Recursively redact every string leaf of `value` — plain objects, arrays,
|
|
102
|
+
* Maps, and Sets included (a session record's `toolCalls[].output` or a
|
|
103
|
+
* gate's `metadata` can nest arbitrarily). Never mutates `value`; returns a
|
|
104
|
+
* new structure. Numbers/booleans/null/undefined pass through unchanged.
|
|
105
|
+
* Circular references are left as-is on a second visit rather than
|
|
106
|
+
* infinite-looping (defensive — a real circular record would already fail
|
|
107
|
+
* `JSON.stringify` before reaching this module).
|
|
108
|
+
*
|
|
109
|
+
* @param {unknown} value
|
|
110
|
+
* @param {WeakSet<object>} [seen]
|
|
111
|
+
* @returns {unknown}
|
|
112
|
+
*/
|
|
113
|
+
export function redactDeep(value, seen = new WeakSet()) {
|
|
114
|
+
if (typeof value === 'string') return redactText(value);
|
|
115
|
+
if (value === null || typeof value !== 'object') return value;
|
|
116
|
+
if (seen.has(value)) return value;
|
|
117
|
+
seen.add(value);
|
|
118
|
+
|
|
119
|
+
if (Array.isArray(value)) {
|
|
120
|
+
return value.map((item) => redactDeep(item, seen));
|
|
121
|
+
}
|
|
122
|
+
if (value instanceof Map) {
|
|
123
|
+
const out = new Map();
|
|
124
|
+
for (const [k, v] of value.entries()) out.set(k, redactDeep(v, seen));
|
|
125
|
+
return out;
|
|
126
|
+
}
|
|
127
|
+
if (value instanceof Set) {
|
|
128
|
+
const out = new Set();
|
|
129
|
+
for (const item of value.values()) out.add(redactDeep(item, seen));
|
|
130
|
+
return out;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const out = {};
|
|
134
|
+
for (const [key, v] of Object.entries(value)) {
|
|
135
|
+
out[key] = redactDeep(v, seen);
|
|
136
|
+
}
|
|
137
|
+
return out;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Redact a whole record (a session record, a guardrail-audit row, a WIP
|
|
142
|
+
* hand-off block, a cost-ledger row) before it is serialized to any sink.
|
|
143
|
+
* Alias of {@link redactDeep} at the top level — named separately so a call
|
|
144
|
+
* site reads as "redact this whole record" rather than "redact this value".
|
|
145
|
+
*
|
|
146
|
+
* @param {object} record
|
|
147
|
+
* @returns {object}
|
|
148
|
+
*/
|
|
149
|
+
export function redactRecord(record) {
|
|
150
|
+
return redactDeep(record);
|
|
151
|
+
}
|