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
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* HeySpex settings MCP tools — the agent face over the settings core
|
|
3
|
-
* (config-settings.mjs)
|
|
3
|
+
* (config-settings.mjs) and the settings registry (settings-registry.mjs),
|
|
4
|
+
* mounted in cli.mjs as:
|
|
4
5
|
* - heyspex_settings_list (read — open)
|
|
5
6
|
* - heyspex_settings_get (read — open)
|
|
6
7
|
* - heyspex_settings_set (WRITE — permission-gated two-step confirm)
|
|
7
8
|
*
|
|
9
|
+
* THIRD FACE (HS-OTEQ): these tools resolve every read through
|
|
10
|
+
* `getEffectiveSetting` (file value → registry default → absent) and the
|
|
11
|
+
* registry's key list, and every write through `validateSettingWrite` — the
|
|
12
|
+
* same functions the CLI face (`hs settings`) and, later, the console face
|
|
13
|
+
* resolve through. No fallback of its own: a registry-governed key answers
|
|
14
|
+
* identically no matter which face asks. A key the registry does not declare
|
|
15
|
+
* (e.g. `agent_behaviors.*`, `mvp_mode.*`) keeps working exactly as before —
|
|
16
|
+
* registry entries are additive, never a replacement for the freeform
|
|
17
|
+
* `.heyspex/config.yaml` surface.
|
|
18
|
+
*
|
|
8
19
|
* WRITE-PERMISSION GUARDRAIL (owner directive 2026-07-11, documented in
|
|
9
20
|
* specs/protocols/agent-settings-protocol.md): a settings write is a protected
|
|
10
21
|
* class. `heyspex_settings_set` without `confirm_token` never writes — it records
|
|
@@ -16,31 +27,62 @@
|
|
|
16
27
|
* (scripts/claude-hooks/settings-write-gate.mjs) additionally routes the confirm
|
|
17
28
|
* call through the harness permission prompt in Claude Code.
|
|
18
29
|
*
|
|
30
|
+
* `requestSettingsWrite` (step 1) and `confirmSettingsWrite`/`setConfigLeaf`
|
|
31
|
+
* (step 2) already run every write through `validateSettingWrite` inside the
|
|
32
|
+
* core, so this file does not re-validate — it only shapes the core's
|
|
33
|
+
* `{ ok:false, error, message }` refusal into the tool's response unchanged
|
|
34
|
+
* (never re-worded), which keeps the reason identical to what the CLI face
|
|
35
|
+
* gives for the same refused write.
|
|
36
|
+
*
|
|
19
37
|
* Pattern source: procedure-tools.mjs (pure-logic execute* functions; cli.mjs wraps).
|
|
20
38
|
*/
|
|
21
39
|
|
|
22
40
|
import {
|
|
23
41
|
listConfigLeaves,
|
|
24
|
-
|
|
42
|
+
getEffectiveSetting,
|
|
25
43
|
requestSettingsWrite,
|
|
26
44
|
confirmSettingsWrite,
|
|
27
45
|
validateSettingKey,
|
|
28
46
|
CONFIG_REL,
|
|
29
47
|
} from './config-settings.mjs';
|
|
48
|
+
import { getSettingEntry, listRegistryKeys } from './settings-registry.mjs';
|
|
30
49
|
|
|
31
50
|
const MCP_FACE = 'mcp';
|
|
32
51
|
|
|
52
|
+
/** True when `key` equals `prefix` or is nested under it (dotted-segment match). */
|
|
53
|
+
function matchesPrefix(key, prefix) {
|
|
54
|
+
return !prefix || key === prefix || key.startsWith(prefix + '.');
|
|
55
|
+
}
|
|
56
|
+
|
|
33
57
|
export function executeSettingsList(vaultRoot, args = {}) {
|
|
34
58
|
const prefix = typeof args.prefix === 'string' ? args.prefix.trim() : '';
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
59
|
+
|
|
60
|
+
const fileLeaves = listConfigLeaves(vaultRoot);
|
|
61
|
+
const seen = new Set();
|
|
62
|
+
const settings = [];
|
|
63
|
+
|
|
64
|
+
// Registry-declared keys first, in registry order, resolved through the same
|
|
65
|
+
// file → registry-default chain every face reads through — so a key never
|
|
66
|
+
// written to disk still appears, carrying its shipped default.
|
|
67
|
+
for (const key of listRegistryKeys()) {
|
|
68
|
+
if (!matchesPrefix(key, prefix)) continue;
|
|
69
|
+
const effective = getEffectiveSetting(vaultRoot, key);
|
|
70
|
+
settings.push({ key, value: effective.value, source: effective.source, found: effective.found });
|
|
71
|
+
seen.add(key);
|
|
38
72
|
}
|
|
73
|
+
|
|
74
|
+
// Then any file leaf outside the registry's namespaces (agent_behaviors.*,
|
|
75
|
+
// mvp_mode.*, …) — additive, unchanged from the pre-registry behavior.
|
|
76
|
+
for (const leaf of fileLeaves) {
|
|
77
|
+
if (seen.has(leaf.key) || !matchesPrefix(leaf.key, prefix)) continue;
|
|
78
|
+
settings.push({ key: leaf.key, value: leaf.value, source: 'file', found: true });
|
|
79
|
+
}
|
|
80
|
+
|
|
39
81
|
return {
|
|
40
82
|
ok: true,
|
|
41
83
|
config_file: CONFIG_REL.replace(/\\/g, '/'),
|
|
42
84
|
count: settings.length,
|
|
43
|
-
settings
|
|
85
|
+
settings,
|
|
44
86
|
};
|
|
45
87
|
}
|
|
46
88
|
|
|
@@ -48,13 +90,26 @@ export function executeSettingsGet(vaultRoot, args = {}) {
|
|
|
48
90
|
const key = typeof args.key === 'string' ? args.key.trim() : '';
|
|
49
91
|
const check = validateSettingKey(key);
|
|
50
92
|
if (!check.ok) return { ok: false, error: 'invalid_key', message: check.reason };
|
|
51
|
-
|
|
93
|
+
|
|
94
|
+
const effective = getEffectiveSetting(vaultRoot, key);
|
|
95
|
+
let note;
|
|
96
|
+
if (effective.source === 'registry_default') {
|
|
97
|
+
const entry = getSettingEntry(key);
|
|
98
|
+
note = `Key absent from .heyspex/config.yaml — resolved to the registry default (${entry?.description ?? 'see settings-registry.mjs'}).`;
|
|
99
|
+
} else if (effective.source === 'absent') {
|
|
100
|
+
note = 'Key absent from .heyspex/config.yaml and not declared in the settings registry — consumers fall back to their documented default.';
|
|
101
|
+
} else if (effective.source === 'computed') {
|
|
102
|
+
note =
|
|
103
|
+
'Computed live from the machine-local hs trust store (~/.heyspex/trusted-folders.json) — ' +
|
|
104
|
+
'never read from .heyspex/config.yaml. Read-only: revoke a folder with "hs trust revoke <path>".';
|
|
105
|
+
}
|
|
52
106
|
return {
|
|
53
107
|
ok: true,
|
|
54
108
|
key,
|
|
55
|
-
found:
|
|
56
|
-
value:
|
|
57
|
-
|
|
109
|
+
found: effective.found,
|
|
110
|
+
value: effective.value,
|
|
111
|
+
source: effective.source,
|
|
112
|
+
...(note ? { note } : {}),
|
|
58
113
|
};
|
|
59
114
|
}
|
|
60
115
|
|
|
@@ -81,14 +136,32 @@ export function executeSettingsSet(vaultRoot, args = {}) {
|
|
|
81
136
|
face: MCP_FACE,
|
|
82
137
|
});
|
|
83
138
|
if (!result.ok) return result;
|
|
139
|
+
// MEASURED, NOT ASSUMED ([[HS-IOJW-5YDH-DIAG]] sweep). "applied and audited"
|
|
140
|
+
// used to be printed unconditionally while `appendSettingsAudit`'s boolean
|
|
141
|
+
// was thrown away inside `setConfigLeaf`, so a settings write with no audit
|
|
142
|
+
// row — the one artifact that makes a protected-class write reviewable after
|
|
143
|
+
// the fact — read exactly like a fully audited one. The config write itself
|
|
144
|
+
// DID land, so `ok` stays true and nothing is retried; `audited` + `status`
|
|
145
|
+
// are what let the caller tell a full success from a partial one.
|
|
146
|
+
const audited = result.audited === true;
|
|
84
147
|
return {
|
|
85
148
|
ok: true,
|
|
86
|
-
status: 'applied',
|
|
149
|
+
status: audited ? 'applied' : 'applied_not_audited',
|
|
150
|
+
partial: !audited,
|
|
151
|
+
audited,
|
|
87
152
|
key: result.key,
|
|
88
153
|
previous: result.previous,
|
|
89
154
|
value: result.value,
|
|
90
155
|
created: result.created,
|
|
91
|
-
|
|
156
|
+
// HS-4C76 fix 3: the core seeds a missing config.yaml from the template on
|
|
157
|
+
// the first write instead of refusing; surface that so the caller can say so.
|
|
158
|
+
...(result.seeded === true ? { seeded: true } : {}),
|
|
159
|
+
note: audited
|
|
160
|
+
? `Setting applied and audited to .heyspex/settings-history.jsonl.${result.seeded === true ? ' (.heyspex/config.yaml was missing and was seeded from the template first.)' : ''}`
|
|
161
|
+
: 'Setting APPLIED to .heyspex/config.yaml, but the audit append to ' +
|
|
162
|
+
'.heyspex/settings-history.jsonl FAILED — this write has NO audit row. The value is live; do not ' +
|
|
163
|
+
're-apply it. Tell the user the change is in effect but unaudited, and check that ' +
|
|
164
|
+
'.heyspex/settings-history.jsonl is writable.',
|
|
92
165
|
};
|
|
93
166
|
}
|
|
94
167
|
|
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
// Spec editing tools: heyspex_regen_task, regen_tasks_for_epic, backfill_epic,
|
|
2
|
+
// edit_file, propose_metadata, list_known_tags, canvas_inbox. 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 { executeCanvasInbox } from './canvas-inbox.mjs';
|
|
6
|
+
import { consoleFetch, controlError, controlOk } from './console-transport.mjs';
|
|
7
|
+
import { detectTagDrift } from './plan-ground.mjs';
|
|
8
|
+
import { consoleErrorMessage } from './console-fetch.mjs';
|
|
9
|
+
import { detectAgentIdentity, getConsoleUrl } from './mcp-config.mjs';
|
|
10
|
+
import { resolveVaultWriteMode } from './read-path.mjs';
|
|
11
|
+
import { splitRawSpec, updateSpecViaConsole } from './cloud-vault-writes.mjs';
|
|
12
|
+
import { writeFileSync } from 'node:fs';
|
|
13
|
+
import { executeShipEpicCloudAware } from './plan-push.mjs';
|
|
14
|
+
import { executeRegenTask, executeRegenTasksForEpic } from './regen-tasks.mjs';
|
|
15
|
+
import { loadTask } from './vault-io.mjs';
|
|
16
|
+
import { InvalidListCursorError, fitPageToByteBudget, resolvePageLimit } from './list-tools.mjs';
|
|
17
|
+
import { DEFAULT_TOOL_BUDGET_BYTES } from './response-budget.mjs';
|
|
18
|
+
|
|
19
|
+
export function handleRegenTask(vaultRoot, args = {}) {
|
|
20
|
+
const result = executeRegenTask(vaultRoot, {
|
|
21
|
+
task_id: args.task_id,
|
|
22
|
+
commit: args.commit === true || args.commit === 'true',
|
|
23
|
+
new_body: args.new_body,
|
|
24
|
+
retire_reason: args.retire_reason,
|
|
25
|
+
loadTask: (ref) => loadTask(vaultRoot, ref),
|
|
26
|
+
});
|
|
27
|
+
return {
|
|
28
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
29
|
+
isError: result.ok === false,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function handleRegenTasksForEpic(vaultRoot, args = {}) {
|
|
34
|
+
const result = executeRegenTasksForEpic(vaultRoot, {
|
|
35
|
+
epic_id: args.epic_id,
|
|
36
|
+
commit: args.commit === true || args.commit === 'true',
|
|
37
|
+
task_specs: args.task_specs,
|
|
38
|
+
loadTask: (ref) => loadTask(vaultRoot, ref),
|
|
39
|
+
});
|
|
40
|
+
return {
|
|
41
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
42
|
+
isError: result.ok === false,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* [[HS-Y00G-EPIC]] — Safe file edit: claim → write → release.
|
|
48
|
+
* Acquires the file claim, writes content to disk, and releases the claim.
|
|
49
|
+
* If claim fails (already claimed by another agent), returns the conflict info.
|
|
50
|
+
*
|
|
51
|
+
* WHERE THE WRITE GOES ([[HS-BYU4-CWPX-TASK-promotion-writes-go-through-the-console]]).
|
|
52
|
+
* The write mode is resolved ONCE here, at the handler entry, and threaded to
|
|
53
|
+
* step 2 below:
|
|
54
|
+
*
|
|
55
|
+
* - `local` — byte-identical to the original behavior: `writeFileSync` to
|
|
56
|
+
* `args.file_path`, and the success payload carries exactly the fields it
|
|
57
|
+
* always did.
|
|
58
|
+
* - `cloud` — the mutated markdown is split back into the
|
|
59
|
+
* `{ frontmatter, body }` pair `PUT /api/specs/:id` takes and sent through
|
|
60
|
+
* {@link updateSpecViaConsole}. Nothing touches this machine's disk: a
|
|
61
|
+
* markdown file written in cloud reliance is a split brain, a document on a
|
|
62
|
+
* machine the console's index has never heard of (and that may hold no
|
|
63
|
+
* vault clone at all).
|
|
64
|
+
*
|
|
65
|
+
* The spec id comes from the POST-EDIT content's own `id:` frontmatter — the
|
|
66
|
+
* caller has already produced the finished document, so its frontmatter is the
|
|
67
|
+
* authority on which spec this edit replaces. Content with no resolvable `id:`
|
|
68
|
+
* (a non-spec file, or a spec fragment) has NO cloud route: there is no console
|
|
69
|
+
* verb for "write these bytes to this arbitrary path". That degrades explicitly
|
|
70
|
+
* as `CLOUD_MODE_UNSUPPORTED_WRITE` naming the file — never a silent fallback
|
|
71
|
+
* to the disk write the mode just forbade.
|
|
72
|
+
*
|
|
73
|
+
* @param {object} args
|
|
74
|
+
* @param {object} [deps] Injection seam so tests drive every branch without a
|
|
75
|
+
* socket: `consoleFetch` (defaults to the real transport), `consoleUrl`
|
|
76
|
+
* (defaults to `getConsoleUrl()`), `resolveWriteMode` (defaults to
|
|
77
|
+
* `resolveVaultWriteMode`).
|
|
78
|
+
*/
|
|
79
|
+
export async function handleEditFile(args = {}, deps = {}) {
|
|
80
|
+
if (!args.file_path || typeof args.file_path !== 'string') {
|
|
81
|
+
return {
|
|
82
|
+
isError: true,
|
|
83
|
+
content: [{ type: 'text', text: JSON.stringify({ error: 'INVALID_ARGS', message: 'file_path is required' }) }],
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
if (typeof args.content !== 'string') {
|
|
87
|
+
return {
|
|
88
|
+
isError: true,
|
|
89
|
+
content: [{ type: 'text', text: JSON.stringify({ error: 'INVALID_ARGS', message: 'content is required and must be a string' }) }],
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const identity = detectAgentIdentity(args.agent, args.session_id);
|
|
94
|
+
const fetchDep = deps.consoleFetch ?? consoleFetch;
|
|
95
|
+
// Resolved ONCE per handler entry, then threaded to step 2. `?? 'local'`
|
|
96
|
+
// keeps every pre-existing caller on exactly the disk write it had.
|
|
97
|
+
const writeMode = (await (deps.resolveWriteMode ?? resolveVaultWriteMode)()) ?? 'local';
|
|
98
|
+
|
|
99
|
+
// Step 1: Acquire claim
|
|
100
|
+
const claimBody = {
|
|
101
|
+
file_path: args.file_path,
|
|
102
|
+
agent: identity.agent,
|
|
103
|
+
session_id: identity.sessionId,
|
|
104
|
+
ttl_ms: 300000, // 5-minute default for file claims
|
|
105
|
+
};
|
|
106
|
+
if (args.working_spec_id && typeof args.working_spec_id === 'string') {
|
|
107
|
+
claimBody.working_spec_id = args.working_spec_id;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const claimResult = await fetchDep('/api/file-claims/acquire', { method: 'POST', body: claimBody });
|
|
111
|
+
|
|
112
|
+
if (claimResult.status === 409) {
|
|
113
|
+
const conflict = claimResult.body && typeof claimResult.body === 'object' ? claimResult.body : {};
|
|
114
|
+
return {
|
|
115
|
+
isError: true,
|
|
116
|
+
content: [{ type: 'text', text: JSON.stringify({
|
|
117
|
+
error: 'FILE_CLAIMED',
|
|
118
|
+
message: `CONFLICT: file "${args.file_path}" is claimed by another agent.`,
|
|
119
|
+
claimed_by: conflict.existing?.agent ?? 'unknown',
|
|
120
|
+
claimed_session: conflict.existing?.sessionId ?? null,
|
|
121
|
+
expires_at: conflict.existing?.expiresAt ?? null,
|
|
122
|
+
conflict: conflict,
|
|
123
|
+
}, null, 2) }],
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (!claimResult.ok) {
|
|
128
|
+
return controlError(claimResult, 'EDIT_FILE_CLAIM_FAILED', { file_path: args.file_path });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Always release before returning a failure, so a rejected write never leaves
|
|
132
|
+
// the file locked for the claim's full TTL.
|
|
133
|
+
const releaseClaim = () =>
|
|
134
|
+
fetchDep('/api/file-claims/release', { method: 'DELETE', body: { file_path: args.file_path } });
|
|
135
|
+
|
|
136
|
+
// Step 2: Write — to disk in local mode, to `PUT /api/specs/:id` in cloud mode.
|
|
137
|
+
let cloudWrite = null;
|
|
138
|
+
if (writeMode === 'cloud') {
|
|
139
|
+
const consoleUrl = deps.consoleUrl ?? getConsoleUrl();
|
|
140
|
+
const { frontmatter, body } = splitRawSpec(args.content);
|
|
141
|
+
const specId = typeof frontmatter?.id === 'string' ? frontmatter.id.trim() : '';
|
|
142
|
+
if (!specId) {
|
|
143
|
+
await releaseClaim();
|
|
144
|
+
return {
|
|
145
|
+
isError: true,
|
|
146
|
+
content: [{ type: 'text', text: JSON.stringify({
|
|
147
|
+
error: 'CLOUD_MODE_UNSUPPORTED_WRITE',
|
|
148
|
+
message:
|
|
149
|
+
`heyspex_edit_file cannot write "${args.file_path}" in cloud vault-write mode: the content carries no `
|
|
150
|
+
+ '`id:` frontmatter, and the console has no route that writes arbitrary bytes to an arbitrary path '
|
|
151
|
+
+ '(POST /api/specs creates a typed spec, PUT /api/specs/:id replaces one by id). NOTHING WAS WRITTEN — '
|
|
152
|
+
+ 'the local disk write is deliberately NOT used as a fallback, because a file written here would be '
|
|
153
|
+
+ 'invisible to the console index this session reads from.',
|
|
154
|
+
file_path: args.file_path,
|
|
155
|
+
write_mode: 'cloud',
|
|
156
|
+
console_url: consoleUrl,
|
|
157
|
+
on_disk: false,
|
|
158
|
+
}, null, 2) }],
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
try {
|
|
162
|
+
cloudWrite = await updateSpecViaConsole({
|
|
163
|
+
consoleFetch: fetchDep,
|
|
164
|
+
consoleUrl,
|
|
165
|
+
specId,
|
|
166
|
+
frontmatter,
|
|
167
|
+
body,
|
|
168
|
+
});
|
|
169
|
+
} catch (err) {
|
|
170
|
+
await releaseClaim();
|
|
171
|
+
return {
|
|
172
|
+
isError: true,
|
|
173
|
+
content: [{ type: 'text', text: JSON.stringify({
|
|
174
|
+
error: 'CONSOLE_UNREACHABLE_CLOUD_MODE',
|
|
175
|
+
message: String(err?.message ?? err),
|
|
176
|
+
file_path: args.file_path,
|
|
177
|
+
spec_id: specId,
|
|
178
|
+
write_mode: 'cloud',
|
|
179
|
+
on_disk: false,
|
|
180
|
+
...(err?.notFound ? { not_found: true } : {}),
|
|
181
|
+
}, null, 2) }],
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
} else {
|
|
185
|
+
try {
|
|
186
|
+
writeFileSync(args.file_path, args.content, 'utf8');
|
|
187
|
+
} catch (err) {
|
|
188
|
+
// Release the claim on write failure so the file isn't locked forever
|
|
189
|
+
await releaseClaim();
|
|
190
|
+
return {
|
|
191
|
+
isError: true,
|
|
192
|
+
content: [{ type: 'text', text: JSON.stringify({
|
|
193
|
+
error: 'WRITE_FAILED',
|
|
194
|
+
message: `Failed to write file: ${err?.message ?? err}`,
|
|
195
|
+
file_path: args.file_path,
|
|
196
|
+
}) }],
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Step 3: Release claim
|
|
202
|
+
const releaseResult = await releaseClaim();
|
|
203
|
+
|
|
204
|
+
return controlOk({
|
|
205
|
+
written: true,
|
|
206
|
+
file_path: args.file_path,
|
|
207
|
+
released: releaseResult.ok ? releaseResult.body?.released ?? true : false,
|
|
208
|
+
agent: identity.agent,
|
|
209
|
+
session_id: identity.sessionId,
|
|
210
|
+
// Cloud-only additions — the local payload stays byte-identical to the
|
|
211
|
+
// shape every existing caller already parses.
|
|
212
|
+
...(cloudWrite
|
|
213
|
+
? {
|
|
214
|
+
write_mode: 'cloud',
|
|
215
|
+
on_disk: false,
|
|
216
|
+
spec_id: cloudWrite.specId,
|
|
217
|
+
rel_path: cloudWrite.relPath,
|
|
218
|
+
console_url: cloudWrite.consoleUrl,
|
|
219
|
+
}
|
|
220
|
+
: {}),
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Known-tags paging contract, mirrored from the console route
|
|
226
|
+
* (`spex-console/src/lib/scheduler/known-tags-query.ts`): the cursor is
|
|
227
|
+
* base64url JSON `{ c: count, t: tag }` over the `(count DESC, tag ASC)` sort
|
|
228
|
+
* key of the aggregate query. Encoded here only to RE-POINT the cursor when the
|
|
229
|
+
* byte budget trims a page — the console mints every cursor it hands out.
|
|
230
|
+
*/
|
|
231
|
+
export const KNOWN_TAGS_TOOL = Object.freeze({
|
|
232
|
+
default_limit: 100,
|
|
233
|
+
max_limit: 500,
|
|
234
|
+
// Measured on `content[0].text`; the dispatcher re-measures the whole envelope (escaped), so leave headroom.
|
|
235
|
+
byte_budget: Math.floor(DEFAULT_TOOL_BUDGET_BYTES * 0.8),
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
export function encodeKnownTagsCursor({ count, tag }) {
|
|
239
|
+
return Buffer.from(JSON.stringify({ c: Number(count) || 0, t: String(tag ?? '') }), 'utf8').toString('base64url');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* `heyspex_list_known_tags` — the closed tag vocabulary for the focus-areas NLU
|
|
244
|
+
* path ([[HS-TASK-I0EK]] / [[HS-EPIC-7WJN]]). GET /api/scheduler/known-tags →
|
|
245
|
+
* ONE page `{ tags: [{ name, count }], next_cursor, page }` from an aggregate
|
|
246
|
+
* query paged with a keyset cursor over (count, tag)
|
|
247
|
+
* ([[HS-4H8T-8KK5-TASK-every-mcp-read-tool-declares-a-byte]], resolving
|
|
248
|
+
* [[HS-7QM6-1Q1C-DIAG-heyspex-list-known-tags-response-busts-t]] — the reply
|
|
249
|
+
* that once hit 138 KB). `limit` and `after` pass straight through to the
|
|
250
|
+
* console; the page is then fitted to the tool's byte budget, re-pointing
|
|
251
|
+
* `next_cursor` at the last tag kept so nothing is lost. Agents call this
|
|
252
|
+
* before proposing a focus pivot so they only suggest tags that exist.
|
|
253
|
+
* Requires a reachable console.
|
|
254
|
+
*
|
|
255
|
+
* Augments the console response with an additive `drift` array ([[HS-TASK-H0SI]]
|
|
256
|
+
* / [[HS-EPIC-OOPW]]): near-duplicate tag pairs within the page flagged by
|
|
257
|
+
* lexical string similarity (NOT embeddings) so the agent can propose a merge to
|
|
258
|
+
* keep the closed vocabulary clean. No tag is mutated here — `drift` is a
|
|
259
|
+
* suggestion signal only.
|
|
260
|
+
*/
|
|
261
|
+
export async function handleListKnownTags(args = {}, { consoleFetch: fetchDep = consoleFetch, byteBudget = KNOWN_TAGS_TOOL.byte_budget } = {}) {
|
|
262
|
+
const limit = resolvePageLimit(args.limit, KNOWN_TAGS_TOOL);
|
|
263
|
+
const params = new URLSearchParams({ limit: String(limit) });
|
|
264
|
+
if (typeof args.after === 'string' && args.after) params.set('after', args.after);
|
|
265
|
+
const result = await fetchDep(`/api/scheduler/known-tags?${params.toString()}`);
|
|
266
|
+
if (!result.ok) {
|
|
267
|
+
if (result.status === 400 && result.body?.error === 'invalid_cursor') {
|
|
268
|
+
return {
|
|
269
|
+
isError: true,
|
|
270
|
+
content: [{ type: 'text', text: JSON.stringify({ error: 'INVALID_CURSOR', message: new InvalidListCursorError().message, tool: 'heyspex_list_known_tags' }, null, 2) }],
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
return controlError(result, 'KNOWN_TAGS_FAILED');
|
|
274
|
+
}
|
|
275
|
+
const body = result.body && typeof result.body === 'object' ? result.body : {};
|
|
276
|
+
const tags = Array.isArray(body.tags) ? body.tags : [];
|
|
277
|
+
const base = {
|
|
278
|
+
...body,
|
|
279
|
+
tags,
|
|
280
|
+
next_cursor: typeof body.next_cursor === 'string' && body.next_cursor ? body.next_cursor : null,
|
|
281
|
+
page: { limit, returned: tags.length, byte_budget: byteBudget, budget_trimmed: false, ...(body.page ?? {}) },
|
|
282
|
+
};
|
|
283
|
+
const fitOpts = {
|
|
284
|
+
rows_key: 'tags',
|
|
285
|
+
// A page of tags that would fill the whole budget leaves no room for the
|
|
286
|
+
// drift signal the tool exists to surface: reserve a tenth for the
|
|
287
|
+
// strongest pairs. A page that needs no trim gets the whole budget.
|
|
288
|
+
byte_budget: byteBudget - Math.floor(byteBudget * 0.1),
|
|
289
|
+
cursorFor: (row) => encodeKnownTagsCursor({ count: row.count, tag: row.name }),
|
|
290
|
+
};
|
|
291
|
+
// 1. Fit the TAGS to the budget first (with an empty drift envelope in
|
|
292
|
+
// place so its bytes are accounted for) — the cursor must land on a tag
|
|
293
|
+
// the caller received, and drift over an un-returned tag is noise.
|
|
294
|
+
let current = fitPageToByteBudget({ ...base, drift: [] }, fitOpts).result;
|
|
295
|
+
// 2. Drift over the returned page only. It is pairwise (quadratic in page
|
|
296
|
+
// size) and sorted by similarity desc, so when it would bust the budget
|
|
297
|
+
// keep the strongest pairs that fit. Only when not even zero pairs fit
|
|
298
|
+
// (the truncation markers cost a few bytes) does one more tag go, and
|
|
299
|
+
// drift is recomputed over what remains.
|
|
300
|
+
for (;;) {
|
|
301
|
+
const fitted = fitKnownTagsDrift(current, detectTagDrift(current.tags), byteBudget);
|
|
302
|
+
if (fitted) return { content: [{ type: 'text', text: fitted.text }] };
|
|
303
|
+
if (current.tags.length <= 1) {
|
|
304
|
+
// A lone tag that cannot fit even with no drift: return it rather than
|
|
305
|
+
// an empty page with no way forward (the list-tools rule).
|
|
306
|
+
return { content: [{ type: 'text', text: JSON.stringify({ ...current, drift: [] }, null, 2) }] };
|
|
307
|
+
}
|
|
308
|
+
const kept = current.tags.slice(0, -1);
|
|
309
|
+
current = {
|
|
310
|
+
...current,
|
|
311
|
+
tags: kept,
|
|
312
|
+
next_cursor: fitOpts.cursorFor(kept[kept.length - 1]),
|
|
313
|
+
page: { ...current.page, returned: kept.length, budget_trimmed: true },
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Largest similarity-desc prefix of `drift` whose page fits `byteBudget`:
|
|
320
|
+
* the whole list untouched when it fits, else `page.drift_truncated: true` +
|
|
321
|
+
* `drift_dropped`. `null` when not even an empty drift list fits.
|
|
322
|
+
*/
|
|
323
|
+
function fitKnownTagsDrift(page, drift, byteBudget) {
|
|
324
|
+
const serialize = (payload) => JSON.stringify(payload, null, 2);
|
|
325
|
+
const fits = (text) => Buffer.byteLength(text, 'utf8') <= byteBudget;
|
|
326
|
+
const full = { ...page, drift };
|
|
327
|
+
const fullText = serialize(full);
|
|
328
|
+
if (fits(fullText)) return { payload: full, text: fullText };
|
|
329
|
+
let lo = 0;
|
|
330
|
+
let hi = drift.length - 1;
|
|
331
|
+
let best = null;
|
|
332
|
+
while (lo <= hi) {
|
|
333
|
+
const mid = (lo + hi) >>> 1;
|
|
334
|
+
const candidate = { ...page, drift: drift.slice(0, mid), page: { ...page.page, drift_truncated: true, drift_dropped: drift.length - mid } };
|
|
335
|
+
const text = serialize(candidate);
|
|
336
|
+
if (fits(text)) {
|
|
337
|
+
best = { payload: candidate, text };
|
|
338
|
+
lo = mid + 1;
|
|
339
|
+
} else {
|
|
340
|
+
hi = mid - 1;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return best;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* AI metadata proposal — [[HS-TASK-TAOD]] Phase 4 of [[HS-EPIC-Y9OJ]].
|
|
348
|
+
*
|
|
349
|
+
* Thin MCP wrapper around console `POST /api/ai/propose-metadata` (Haiku 4.5).
|
|
350
|
+
* One-shot at create time — agents must NOT call this on edit, status change,
|
|
351
|
+
* or update flows; that's the closed-loop bias the epic explicitly rejects.
|
|
352
|
+
*/
|
|
353
|
+
export async function handleProposeMetadata(args = {}) {
|
|
354
|
+
const name = typeof args?.name === 'string' ? args.name : '';
|
|
355
|
+
const body = typeof args?.body === 'string' ? args.body : '';
|
|
356
|
+
if (!name.trim() && !body.trim()) {
|
|
357
|
+
return {
|
|
358
|
+
isError: true,
|
|
359
|
+
content: [
|
|
360
|
+
{
|
|
361
|
+
type: 'text',
|
|
362
|
+
text: JSON.stringify(
|
|
363
|
+
{ ok: false, reason: 'invalid_request', message: 'name or body is required' },
|
|
364
|
+
null,
|
|
365
|
+
2,
|
|
366
|
+
),
|
|
367
|
+
},
|
|
368
|
+
],
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const payload = { action: 'propose', name, body };
|
|
373
|
+
if (typeof args?.specType === 'string') payload.specType = args.specType;
|
|
374
|
+
if (typeof args?.parentExcerpt === 'string') payload.parentExcerpt = args.parentExcerpt;
|
|
375
|
+
|
|
376
|
+
const result = await consoleFetch('/api/ai/propose-metadata', {
|
|
377
|
+
method: 'POST',
|
|
378
|
+
body: payload,
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
if (!result.ok) {
|
|
382
|
+
const reason =
|
|
383
|
+
result.errorKind === 'timeout'
|
|
384
|
+
? 'console_timeout'
|
|
385
|
+
: result.status === 0
|
|
386
|
+
? 'console_unreachable'
|
|
387
|
+
: 'console_error';
|
|
388
|
+
const errorPayload = {
|
|
389
|
+
ok: false,
|
|
390
|
+
reason,
|
|
391
|
+
message: consoleErrorMessage(
|
|
392
|
+
result,
|
|
393
|
+
`Console /api/ai/propose-metadata returned ${result.status}`,
|
|
394
|
+
),
|
|
395
|
+
console_response: result.body,
|
|
396
|
+
};
|
|
397
|
+
return {
|
|
398
|
+
isError: true,
|
|
399
|
+
content: [{ type: 'text', text: JSON.stringify(errorPayload, null, 2) }],
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return {
|
|
404
|
+
content: [{ type: 'text', text: JSON.stringify(result.body, null, 2) }],
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Ground-truth dev-server status. The heavy logic lives in the repo's
|
|
409
|
+
// scripts/dev-server-status.mjs (the shared core for the hs CLI verb and the
|
|
410
|
+
// PreToolUse reminder hook too); we dynamic-import it from the vault root so
|
|
411
|
+
// the three faces stay in lockstep. Must never throw out of the tool call.
|
|
412
|
+
export async function handleCanvasInbox(vaultRoot, args = {}) {
|
|
413
|
+
// Session id: explicit arg wins; otherwise the ambient Claude Code session.
|
|
414
|
+
const explicit = typeof args.session_id === 'string' ? args.session_id.trim() : '';
|
|
415
|
+
const sessionId =
|
|
416
|
+
explicit ||
|
|
417
|
+
(process.env.CLAUDE_CODE_SESSION_ID || process.env.CLAUDE_SESSION_ID || '').trim();
|
|
418
|
+
|
|
419
|
+
const result = await executeCanvasInbox({ vaultRoot, sessionId });
|
|
420
|
+
return {
|
|
421
|
+
...(result.ok ? {} : { isError: true }),
|
|
422
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* `heyspex_ship_epic` / `backfill_epic`. Cloud-aware entry
|
|
428
|
+
* ([[HS-BYU4-CWPX-TASK-promotion-writes-go-through-the-console]] sweep):
|
|
429
|
+
* `executeShipEpicCloudAware` resolves `resolveVaultWriteMode()` ONCE and either
|
|
430
|
+
* runs the untouched synchronous disk path (`local`) or creates the epic, its
|
|
431
|
+
* tasks and the review doc through `POST /api/specs` in create-first/link-second
|
|
432
|
+
* order (`cloud`). The handler became `async` for that — awaiting is safe in
|
|
433
|
+
* both modes, since the local branch still returns the same plain object.
|
|
434
|
+
*/
|
|
435
|
+
export async function handleShipEpic(vaultRoot, args) {
|
|
436
|
+
const result = await executeShipEpicCloudAware(vaultRoot, args);
|
|
437
|
+
if (result.error) {
|
|
438
|
+
return {
|
|
439
|
+
isError: true,
|
|
440
|
+
content: [{ type: 'text', text: JSON.stringify(result) }],
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
return {
|
|
444
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
445
|
+
};
|
|
446
|
+
}
|