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
|
@@ -17,14 +17,17 @@
|
|
|
17
17
|
*
|
|
18
18
|
* `RUNTIME_FILES` is the same set `scripts/lib/packed-runtime-imports.test.mjs`
|
|
19
19
|
* proves ships in the published tarball (its two explicitly-diagnosed
|
|
20
|
-
* dynamic-import targets, the `hs source` entrypoints it spawns, and the
|
|
21
|
-
* `dist/cli/*.mjs` bundle entries `hs init`/`hs install` dispatch to) — this
|
|
20
|
+
* dynamic-import targets, the `hs source` entrypoints it spawns, and the
|
|
21
|
+
* `dist/cli/*.mjs` bundle entries `hs init`/`hs install`/`hs dev` dispatch to) — this
|
|
22
22
|
* is deliberately the SAME list, not a re-derivation, so the two checks can
|
|
23
23
|
* never silently drift apart (one says "ships", the other says "present").
|
|
24
24
|
*/
|
|
25
|
-
import {
|
|
25
|
+
import { spawnSync } from "node:child_process";
|
|
26
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
26
27
|
import { join } from "node:path";
|
|
27
28
|
|
|
29
|
+
import { TEMPLATE_CORE_DESTS, TEMPLATE_VERSION } from "./template-pin.mjs";
|
|
30
|
+
|
|
28
31
|
/**
|
|
29
32
|
* Runtime files a published install must have next to `bin/hs.mjs` — each
|
|
30
33
|
* entry is a path-segment array (portable across `path.join`/posix).
|
|
@@ -39,6 +42,9 @@ export const RUNTIME_FILES = Object.freeze([
|
|
|
39
42
|
["scripts", "lib", "hs-source-secret-scan.mjs"],
|
|
40
43
|
["dist", "cli", "hs-init.mjs"],
|
|
41
44
|
["dist", "cli", "hs-install.mjs"],
|
|
45
|
+
// HS-2HII: the `hs dev` bundle — invariant 10's visibility verb for a mode
|
|
46
|
+
// override, so a package missing it is named on the first command.
|
|
47
|
+
["dist", "cli", "hs-dev.mjs"],
|
|
42
48
|
]);
|
|
43
49
|
|
|
44
50
|
/**
|
|
@@ -87,3 +93,534 @@ export function runtimeFilesCheckRow(pkgRoot, opts = {}) {
|
|
|
87
93
|
"if it's still missing after a clean reinstall, the published package itself is broken.",
|
|
88
94
|
};
|
|
89
95
|
}
|
|
96
|
+
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
// `vault-config` — the vault's `.heyspex/config.yaml` exists
|
|
99
|
+
// ([[HS-4C76-9W86-DIAG-heyspex-config-yaml-has-no-single-home]] fix 5)
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Path segments of the HeySpex workflow-settings file, relative to the VAULT
|
|
104
|
+
* root (ADR-0009 §1: the vault root is its one and only home; it is
|
|
105
|
+
* git-tracked there). Kept as a segment array like {@link RUNTIME_FILES}.
|
|
106
|
+
*/
|
|
107
|
+
export const VAULT_CONFIG_REL = Object.freeze([".heyspex", "config.yaml"]);
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Does `<vaultRoot>/.heyspex/config.yaml` exist? Returns `{ ok, path }` where
|
|
111
|
+
* `path` is the POSIX-joined RELATIVE path (never absolute — same
|
|
112
|
+
* no-machine-path-leak rule as {@link checkRuntimeFiles}). `exists` is the
|
|
113
|
+
* same injectable seam. A null/empty `vaultRoot` (no vault resolved at all)
|
|
114
|
+
* reports `ok: false` with `resolved: false` so the row can say WHY.
|
|
115
|
+
*/
|
|
116
|
+
export function checkVaultConfig(vaultRoot, { exists = existsSync } = {}) {
|
|
117
|
+
const relPosix = VAULT_CONFIG_REL.join("/");
|
|
118
|
+
if (typeof vaultRoot !== "string" || vaultRoot.length === 0) {
|
|
119
|
+
return { ok: false, resolved: false, path: relPosix };
|
|
120
|
+
}
|
|
121
|
+
const abs = join(vaultRoot, ...VAULT_CONFIG_REL);
|
|
122
|
+
return { ok: exists(abs), resolved: true, path: relPosix };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Render {@link checkVaultConfig}'s result as one `hs doctor` check row —
|
|
127
|
+
* the same `id`/`status`/`detail`/optional `human` shape as
|
|
128
|
+
* {@link runtimeFilesCheckRow}. FAILS when the file is missing: with it
|
|
129
|
+
* absent every settings reader falls to its registry/documented default
|
|
130
|
+
* (MVP mode off, strict task grain — the HS-4C76 symptom), and until the
|
|
131
|
+
* fix the settings faces refused to write at all. Both remedies now seed
|
|
132
|
+
* the file from the shipped template: `hs init` (in the vault directory),
|
|
133
|
+
* or any `hs settings set` / `heyspex_settings_set` write.
|
|
134
|
+
*/
|
|
135
|
+
export function vaultConfigCheckRow(vaultRoot, opts = {}) {
|
|
136
|
+
const { ok, resolved, path: relPosix } = checkVaultConfig(vaultRoot, opts);
|
|
137
|
+
if (ok) {
|
|
138
|
+
return {
|
|
139
|
+
id: "vault-config",
|
|
140
|
+
status: "ok",
|
|
141
|
+
detail: `${relPosix} present at the vault root`,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
if (!resolved) {
|
|
145
|
+
return {
|
|
146
|
+
id: "vault-config",
|
|
147
|
+
status: "fail",
|
|
148
|
+
detail: `${relPosix} not checked — no vault root resolved`,
|
|
149
|
+
human:
|
|
150
|
+
"No vault was found (no heyspex.yaml / specs/ up from the invoked directory, no registry " +
|
|
151
|
+
"pointer). Run `hs init` in the directory that should become the vault — it seeds " +
|
|
152
|
+
`${relPosix} from the shipped template too.`,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
id: "vault-config",
|
|
157
|
+
status: "fail",
|
|
158
|
+
detail: `${relPosix} missing at the vault root`,
|
|
159
|
+
human:
|
|
160
|
+
`The vault has no ${relPosix}, so every HeySpex settings reader (console, MCP, CLI, hooks) ` +
|
|
161
|
+
"falls back to defaults — MVP mode reads as OFF and close-out gates go strict. Run `hs init` " +
|
|
162
|
+
"in the vault directory to seed it from the shipped template (an existing file is never " +
|
|
163
|
+
"touched), or any `hs settings set <key> <value>` — the settings core now seeds the file on " +
|
|
164
|
+
"its first write instead of refusing. Then commit it with the vault (ADR-0009).",
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Marker file that only the HeySpex platform vault has: it IS the template source. */
|
|
169
|
+
const PLATFORM_VAULT_MARKER =
|
|
170
|
+
"specs/protocols/dev-factory-delegation-routing.md";
|
|
171
|
+
|
|
172
|
+
/** `template_version:` from raw `heyspex.yaml` text, or null (mirrors `readTemplateVersion` in init-git-finalize.ts). */
|
|
173
|
+
function readTemplatePin(yamlText) {
|
|
174
|
+
const m = /^template_version:\s*(.+?)\s*$/m.exec(yamlText ?? "");
|
|
175
|
+
if (!m) return null;
|
|
176
|
+
const v = m[1]
|
|
177
|
+
.replace(/^"(.*)"$/, "$1")
|
|
178
|
+
.replace(/^'(.*)'$/, "$1")
|
|
179
|
+
.trim();
|
|
180
|
+
return v.length > 0 ? v : null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* The `vault-template` doctor row: does a customer vault carry the current
|
|
185
|
+
* HeySpex template (HS-KH0Q)? Pure over injectable `exists` / `readFile`
|
|
186
|
+
* (default node:fs; `readFile` returns the text or null), same row shape as
|
|
187
|
+
* {@link vaultConfigCheckRow}.
|
|
188
|
+
* - platform vault (marker protocol present) -> ok: it is the template source;
|
|
189
|
+
* - any core dest missing -> fail, naming the files, repair `hs init --customer-vault .`;
|
|
190
|
+
* - `heyspex.yaml` pins {@link TEMPLATE_VERSION} -> ok;
|
|
191
|
+
* - pin older/newer/absent while the files exist -> warn.
|
|
192
|
+
*/
|
|
193
|
+
export function templateCheckRow(vaultRoot, opts = {}) {
|
|
194
|
+
const exists = opts.exists ?? existsSync;
|
|
195
|
+
const readFile =
|
|
196
|
+
opts.readFile ??
|
|
197
|
+
((p) => {
|
|
198
|
+
try {
|
|
199
|
+
return readFileSync(p, "utf8");
|
|
200
|
+
} catch {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
const id = "vault-template";
|
|
205
|
+
const repair = "hs init --customer-vault .";
|
|
206
|
+
if (typeof vaultRoot !== "string" || vaultRoot.length === 0) {
|
|
207
|
+
return {
|
|
208
|
+
id,
|
|
209
|
+
status: "fail",
|
|
210
|
+
detail: "template not checked — no vault root resolved",
|
|
211
|
+
human: `No vault was found. Run \`${repair}\` in the directory that should become the vault.`,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
const at = (rel) => join(vaultRoot, ...rel.split("/"));
|
|
215
|
+
if (exists(at(PLATFORM_VAULT_MARKER))) {
|
|
216
|
+
return {
|
|
217
|
+
id,
|
|
218
|
+
status: "ok",
|
|
219
|
+
detail: "platform vault (the template source, not a customer vault)",
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
const missing = TEMPLATE_CORE_DESTS.filter((rel) => !exists(at(rel)));
|
|
223
|
+
if (missing.length > 0) {
|
|
224
|
+
return {
|
|
225
|
+
id,
|
|
226
|
+
status: "fail",
|
|
227
|
+
detail: `missing template file(s): ${missing.join(", ")}`,
|
|
228
|
+
human:
|
|
229
|
+
`The vault is missing ${missing.length} shipped template file(s): ${missing.join(", ")}. ` +
|
|
230
|
+
`Run \`${repair}\` in the vault directory — it adds only what is missing and never overwrites an existing file.`,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
const pin = readTemplatePin(readFile(at("heyspex.yaml")));
|
|
234
|
+
if (pin === TEMPLATE_VERSION) {
|
|
235
|
+
return {
|
|
236
|
+
id,
|
|
237
|
+
status: "ok",
|
|
238
|
+
detail: `template v${TEMPLATE_VERSION} pinned; all ${TEMPLATE_CORE_DESTS.length} core files present`,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
return {
|
|
242
|
+
id,
|
|
243
|
+
status: "warn",
|
|
244
|
+
detail: pin
|
|
245
|
+
? `heyspex.yaml pins template v${pin}, current is v${TEMPLATE_VERSION}`
|
|
246
|
+
: `heyspex.yaml has no template_version pin (current is v${TEMPLATE_VERSION})`,
|
|
247
|
+
human: `Core template files are present but the pin is not current. Run \`${repair}\` to refresh the pin and add any newer shipped files.`,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* The `hs doctor` repair for a pre-v2 `~/.heyspex/registry.json`: the argv
|
|
253
|
+
* `--fix` hands to Node from the repo root (`bin/hs.mjs vault migrate-registry`,
|
|
254
|
+
* [[HS-RXLG-WW9R-TASK-key-registry-json-per-account-schema-v2]] AC5).
|
|
255
|
+
*/
|
|
256
|
+
export const REGISTRY_SCHEMA_FIX = Object.freeze([
|
|
257
|
+
"bin/hs.mjs",
|
|
258
|
+
"vault",
|
|
259
|
+
"migrate-registry",
|
|
260
|
+
]);
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Classify the raw text of `~/.heyspex/registry.json` (`null` = no file). Pure.
|
|
264
|
+
* Mirrors `registry-file.ts`'s `inspectVaultRegistry` + `hasLegacyAccountState`
|
|
265
|
+
* on the fields a doctor needs:
|
|
266
|
+
* - `missing` — nothing registered on this machine yet;
|
|
267
|
+
* - `unreadable` — the file is not a JSON object (the loader reads it as
|
|
268
|
+
* empty, and the next write would start over);
|
|
269
|
+
* - `v1` — machine-global pointers, rows with no account;
|
|
270
|
+
* - `legacy` — v2, but some state is still keyed to `legacy` (migrated
|
|
271
|
+
* while signed out, or written before migration);
|
|
272
|
+
* - `v2` — per-account pointers, every row keyed.
|
|
273
|
+
*/
|
|
274
|
+
export function classifyRegistryFile(rawText) {
|
|
275
|
+
if (rawText === null || rawText === undefined) return { kind: "missing" };
|
|
276
|
+
let json;
|
|
277
|
+
try {
|
|
278
|
+
json = JSON.parse(rawText);
|
|
279
|
+
} catch {
|
|
280
|
+
return { kind: "unreadable" };
|
|
281
|
+
}
|
|
282
|
+
if (!json || typeof json !== "object" || Array.isArray(json))
|
|
283
|
+
return { kind: "unreadable" };
|
|
284
|
+
if (json.schema_version !== 2) return { kind: "v1" };
|
|
285
|
+
const LEGACY = "legacy";
|
|
286
|
+
const rows = [
|
|
287
|
+
...(Array.isArray(json.workspaces) ? json.workspaces : []),
|
|
288
|
+
...(Array.isArray(json.vaults) ? json.vaults : []),
|
|
289
|
+
];
|
|
290
|
+
const accounts =
|
|
291
|
+
json.accounts && typeof json.accounts === "object" ? json.accounts : {};
|
|
292
|
+
const legacy =
|
|
293
|
+
Object.prototype.hasOwnProperty.call(accounts, LEGACY) ||
|
|
294
|
+
json.current_account_id === LEGACY ||
|
|
295
|
+
rows.some(
|
|
296
|
+
(r) => r && typeof r === "object" && (r.account_id ?? LEGACY) === LEGACY,
|
|
297
|
+
);
|
|
298
|
+
return { kind: legacy ? "legacy" : "v2" };
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Render {@link classifyRegistryFile} as one `hs doctor` row (`registry-schema`).
|
|
303
|
+
* A v1 file (or v2 with `legacy` state) is a `warn` that carries
|
|
304
|
+
* {@link REGISTRY_SCHEMA_FIX}, so `hs doctor --fix` runs `hs vault
|
|
305
|
+
* migrate-registry`. It is a warn, not a fail: the loader still reads a v1
|
|
306
|
+
* file. An unreadable file is a warn with no fix — the migration refuses to
|
|
307
|
+
* overwrite it, so a human looks first.
|
|
308
|
+
*/
|
|
309
|
+
export function registrySchemaCheckRow(rawText) {
|
|
310
|
+
const { kind } = classifyRegistryFile(rawText);
|
|
311
|
+
const id = "registry-schema";
|
|
312
|
+
if (kind === "missing") {
|
|
313
|
+
return {
|
|
314
|
+
id,
|
|
315
|
+
status: "ok",
|
|
316
|
+
detail: "no ~/.heyspex/registry.json yet (nothing registered)",
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
if (kind === "v2") {
|
|
320
|
+
return {
|
|
321
|
+
id,
|
|
322
|
+
status: "ok",
|
|
323
|
+
detail: "schema v2 (active vault/workspace kept per account)",
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
if (kind === "unreadable") {
|
|
327
|
+
return {
|
|
328
|
+
id,
|
|
329
|
+
status: "warn",
|
|
330
|
+
detail: "~/.heyspex/registry.json is unreadable — it loads as empty",
|
|
331
|
+
human:
|
|
332
|
+
"The registry file is not valid JSON, so every reader treats it as empty and the next " +
|
|
333
|
+
"`hs connect` / `hs vault register` would start it over. Fix or move it aside by hand " +
|
|
334
|
+
"(`hs vault migrate-registry` will not overwrite it).",
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
const detail =
|
|
338
|
+
kind === "v1"
|
|
339
|
+
? "schema v1 — one machine-global active vault for every account"
|
|
340
|
+
: "schema v2, but some rows are still keyed to `legacy` (no account)";
|
|
341
|
+
return {
|
|
342
|
+
id,
|
|
343
|
+
status: "warn",
|
|
344
|
+
detail: `${detail}; fix: \`hs vault migrate-registry\``,
|
|
345
|
+
fix: [...REGISTRY_SCHEMA_FIX],
|
|
346
|
+
human:
|
|
347
|
+
"Run `hs vault migrate-registry --dry-run` to see the plan, then `hs vault migrate-registry` " +
|
|
348
|
+
"(or `hs doctor --fix`) while signed in: it rewrites ~/.heyspex/registry.json as schema v2 " +
|
|
349
|
+
"and keys each row to the account PROVEN for it (its heyspex.yaml `account:`, or your cloud " +
|
|
350
|
+
"vault roster). Unproven rows stay `legacy` and keep resolving. A second run changes nothing.",
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// ---------------------------------------------------------------------------
|
|
355
|
+
// `connection` — the local-vs-cloud view, folded from `hs connect --check
|
|
356
|
+
// --json` ([[HS-W73V-WW9R-TASK-make-both-doctor-shapes-fail-on-cloud]],
|
|
357
|
+
// invariant 8: proof from both sides)
|
|
358
|
+
// ---------------------------------------------------------------------------
|
|
359
|
+
//
|
|
360
|
+
// ONE fold for both doctor shapes: the full doctor (`scripts/heyspex-doctor.mjs`,
|
|
361
|
+
// source checkout) and the published-install doctor (`bin/hs.mjs`
|
|
362
|
+
// `runStandaloneDoctor`) both feed the SAME report through
|
|
363
|
+
// `foldConnectionReport`, so a broken setup reads the same on any machine.
|
|
364
|
+
// Neither doctor keeps its own list of comparison rows: the report is computed
|
|
365
|
+
// by `spex-console/src/lib/cli/connect-checks.ts` from the routing fact table
|
|
366
|
+
// (`routing-facts.ts`), and a cloud-view row is recognised by the field it
|
|
367
|
+
// carries (`agree`: only the seven local-vs-cloud rows have it), never by a
|
|
368
|
+
// second copy of its id list.
|
|
369
|
+
|
|
370
|
+
/** The connection section's summary row id; per-artifact rows are `connect-<id>`. */
|
|
371
|
+
export const CONNECTION_ROW_ID = "connect";
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* The connect-check report on a stream. `hs connect --check --json` prints its
|
|
375
|
+
* payload as ONE JSON line, but other output may precede or follow it (the
|
|
376
|
+
* identity note, a customer-kit line from the bundle), so every `{`-led line
|
|
377
|
+
* is tried and the LAST one with the report's shape wins; a pretty-printed
|
|
378
|
+
* whole-stream document is the fallback. Pure.
|
|
379
|
+
* @returns {{ ok: true, report: object } | { ok: false, error: string }}
|
|
380
|
+
*/
|
|
381
|
+
export function parseConnectCheckOutput(text) {
|
|
382
|
+
const isReport = (v) =>
|
|
383
|
+
v &&
|
|
384
|
+
typeof v === "object" &&
|
|
385
|
+
typeof v.connected === "boolean" &&
|
|
386
|
+
Array.isArray(v.checks);
|
|
387
|
+
const candidates = [];
|
|
388
|
+
for (const line of String(text ?? "").split(/\r?\n/)) {
|
|
389
|
+
if (!/^\s*\{/.test(line)) continue;
|
|
390
|
+
try {
|
|
391
|
+
candidates.push(JSON.parse(line));
|
|
392
|
+
} catch {
|
|
393
|
+
// not a one-line document
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
try {
|
|
397
|
+
candidates.push(JSON.parse(String(text ?? "")));
|
|
398
|
+
} catch {
|
|
399
|
+
// not one whole document either
|
|
400
|
+
}
|
|
401
|
+
const report = candidates.filter(isReport).pop();
|
|
402
|
+
if (report) return { ok: true, report };
|
|
403
|
+
if (candidates.length === 0) {
|
|
404
|
+
return { ok: false, error: "`hs connect --check --json` printed no JSON" };
|
|
405
|
+
}
|
|
406
|
+
return {
|
|
407
|
+
ok: false,
|
|
408
|
+
error: "`hs connect --check --json` returned an unexpected shape",
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/** A local-vs-cloud row: the only rows of the report that carry `agree`. Pure. */
|
|
413
|
+
export function isCloudViewRow(check) {
|
|
414
|
+
return (
|
|
415
|
+
Boolean(check) && typeof check === "object" && Object.hasOwn(check, "agree")
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const sideText = (v) =>
|
|
420
|
+
v === null || v === undefined || v === "" ? "(unknown)" : String(v);
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* One report row to one doctor row. The severity rule (HS-W73V AC1/AC2):
|
|
424
|
+
* - local artifact rows keep the pre-W73V behaviour: `present` (and
|
|
425
|
+
* `not_applicable`, which the report's own `connected` already accepts) is
|
|
426
|
+
* ok, anything else a warn with no fix ("not connected" is a
|
|
427
|
+
* recommendation, never a failure);
|
|
428
|
+
* - a cloud-view row whose two sides DISAGREE (`agree: false`) is a `fail`
|
|
429
|
+
* whose `human` is the row's own `repair` command, verbatim;
|
|
430
|
+
* - a cloud-view row whose other side could not be read (`agree: null`,
|
|
431
|
+
* not `not_applicable`) is a `warn` naming the cause: unknown is never
|
|
432
|
+
* success, and never a failure on its own;
|
|
433
|
+
* - agreeing and not-applicable cloud rows are ok.
|
|
434
|
+
* Cloud rows keep `local` / `cloud` / `agree` / `repair` / `fact` so the JSON
|
|
435
|
+
* shows both sides next to each other.
|
|
436
|
+
*/
|
|
437
|
+
function foldCheckRow(check, idPrefix) {
|
|
438
|
+
const base = {
|
|
439
|
+
id: `${idPrefix}-${check.id}`,
|
|
440
|
+
label: `Connect: ${check.label ?? check.id}`,
|
|
441
|
+
fix: null,
|
|
442
|
+
};
|
|
443
|
+
if (!isCloudViewRow(check)) {
|
|
444
|
+
return {
|
|
445
|
+
...base,
|
|
446
|
+
// `not_applicable` counts as connected in the report itself (`connected`).
|
|
447
|
+
status:
|
|
448
|
+
check.status === "present" || check.status === "not_applicable"
|
|
449
|
+
? "ok"
|
|
450
|
+
: "warn",
|
|
451
|
+
detail: check.detail
|
|
452
|
+
? `${check.status} (${check.detail})`
|
|
453
|
+
: String(check.status),
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
const view = {
|
|
457
|
+
local: check.local ?? null,
|
|
458
|
+
cloud: check.cloud ?? null,
|
|
459
|
+
agree: check.agree ?? null,
|
|
460
|
+
repair: check.repair ?? null,
|
|
461
|
+
...(check.fact ? { fact: check.fact } : {}),
|
|
462
|
+
};
|
|
463
|
+
const sides = `local: ${sideText(view.local)} | cloud: ${sideText(view.cloud)}`;
|
|
464
|
+
if (check.status === "not_applicable") {
|
|
465
|
+
return {
|
|
466
|
+
...base,
|
|
467
|
+
...view,
|
|
468
|
+
status: "ok",
|
|
469
|
+
detail: `not applicable — ${check.detail ?? "nothing to compare"}`,
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
if (view.agree === true) {
|
|
473
|
+
return { ...base, ...view, status: "ok", detail: `${sides} — agree` };
|
|
474
|
+
}
|
|
475
|
+
if (view.agree === false) {
|
|
476
|
+
return {
|
|
477
|
+
...base,
|
|
478
|
+
...view,
|
|
479
|
+
status: "fail",
|
|
480
|
+
detail: `${sides} — DISAGREE: ${check.detail ?? "the two sides differ"}`,
|
|
481
|
+
human:
|
|
482
|
+
view.repair ??
|
|
483
|
+
"Run `hs connect --check` to see the disagreement and its repair.",
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
return {
|
|
487
|
+
...base,
|
|
488
|
+
...view,
|
|
489
|
+
status: "warn",
|
|
490
|
+
detail: `local: ${sideText(view.local)} | cloud: unknown — ${check.detail ?? "the cloud side could not be read"}`,
|
|
491
|
+
...(view.repair ? { human: view.repair } : {}),
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Fold a `hs connect --check --json` result (`parseConnectCheckOutput`'s
|
|
497
|
+
* `{ ok, report }`, or `{ ok: false, error }` when it could not be run) into
|
|
498
|
+
* doctor rows: one summary row (`connect`) plus one row per report row
|
|
499
|
+
* (`connect-<id>`). The summary FAILS when any cloud row fails; otherwise it
|
|
500
|
+
* is a warn for an unconnected repo or an unknown cloud side, else ok. Rows
|
|
501
|
+
* never carry a `fix`: doctor never runs `hs connect` for you, `--fix`
|
|
502
|
+
* included. Pure.
|
|
503
|
+
*
|
|
504
|
+
* Returns `{ rows, json, ok }`: `json` is the raw report (doctor's
|
|
505
|
+
* `connection` key) plus `cloud_view` counts; `ok` is false iff a row failed.
|
|
506
|
+
*/
|
|
507
|
+
export function foldConnectionReport(
|
|
508
|
+
surface,
|
|
509
|
+
{
|
|
510
|
+
idPrefix = CONNECTION_ROW_ID,
|
|
511
|
+
label = "HeySpex connection (hs connect --check)",
|
|
512
|
+
connectCommand = "hs connect",
|
|
513
|
+
checkCommand = "hs connect --check",
|
|
514
|
+
} = {},
|
|
515
|
+
) {
|
|
516
|
+
if (!surface?.ok) {
|
|
517
|
+
const error = surface?.error ?? "no report";
|
|
518
|
+
return {
|
|
519
|
+
rows: [
|
|
520
|
+
{
|
|
521
|
+
id: idPrefix,
|
|
522
|
+
label,
|
|
523
|
+
status: "warn",
|
|
524
|
+
detail: `connection state unknown — ${error}`,
|
|
525
|
+
human: `Run \`${checkCommand}\` directly to see the full report — this check never runs it for you.`,
|
|
526
|
+
fix: null,
|
|
527
|
+
},
|
|
528
|
+
],
|
|
529
|
+
json: { checked: false, connected: null, error },
|
|
530
|
+
ok: true,
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
const report = surface.report;
|
|
534
|
+
const checks = Array.isArray(report.checks) ? report.checks : [];
|
|
535
|
+
const rows = checks.map((c) => foldCheckRow(c, idPrefix));
|
|
536
|
+
const cloudRows = rows.filter((_r, i) => isCloudViewRow(checks[i]));
|
|
537
|
+
const failed = cloudRows.filter((r) => r.status === "fail");
|
|
538
|
+
const unknown = cloudRows.filter((r) => r.status === "warn");
|
|
539
|
+
const agreed = cloudRows.filter((r) => r.status === "ok" && r.agree === true);
|
|
540
|
+
const localConnected = checks
|
|
541
|
+
.filter((c) => !isCloudViewRow(c))
|
|
542
|
+
.every((c) => c.status === "present" || c.status === "not_applicable");
|
|
543
|
+
|
|
544
|
+
let summary;
|
|
545
|
+
if (failed.length > 0) {
|
|
546
|
+
const repairs = [...new Set(failed.map((r) => r.repair).filter(Boolean))];
|
|
547
|
+
summary = {
|
|
548
|
+
status: "fail",
|
|
549
|
+
detail: `${failed.length} local-vs-cloud row(s) disagree: ${failed.map((r) => r.id.slice(idPrefix.length + 1)).join(", ")}`,
|
|
550
|
+
human: `Run the repair named on each failing row: ${repairs.join("; ")}`,
|
|
551
|
+
};
|
|
552
|
+
} else if (!localConnected) {
|
|
553
|
+
summary = {
|
|
554
|
+
status: "warn",
|
|
555
|
+
detail: `not connected to HeySpex — run \`${connectCommand}\``,
|
|
556
|
+
human: `Run \`${connectCommand}\` to wire this repo up to HeySpex — this check recommends it but never runs it for you.`,
|
|
557
|
+
};
|
|
558
|
+
} else if (unknown.length > 0) {
|
|
559
|
+
summary = {
|
|
560
|
+
status: "warn",
|
|
561
|
+
detail: `connected locally; the cloud side is unknown on ${unknown.length} row(s) — ${unknown[0].detail.replace(/^.*?cloud: unknown — /, "")}`,
|
|
562
|
+
};
|
|
563
|
+
} else {
|
|
564
|
+
summary = {
|
|
565
|
+
status: "ok",
|
|
566
|
+
detail: `connected — ${checks.length} row(s); local and cloud agree on ${agreed.length} row(s)`,
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
return {
|
|
570
|
+
rows: [{ id: idPrefix, label, fix: null, ...summary }, ...rows],
|
|
571
|
+
json: {
|
|
572
|
+
checked: true,
|
|
573
|
+
connected: report.connected,
|
|
574
|
+
repo_root: report.repo_root,
|
|
575
|
+
checks,
|
|
576
|
+
...(report.satellite ? { satellite: report.satellite } : {}),
|
|
577
|
+
cloud_view: {
|
|
578
|
+
agree: agreed.length,
|
|
579
|
+
disagree: failed.length,
|
|
580
|
+
unknown: unknown.length,
|
|
581
|
+
},
|
|
582
|
+
},
|
|
583
|
+
ok: failed.length === 0,
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Run the BUNDLED `dist/cli/hs-connect.mjs connect --check --json` of a
|
|
589
|
+
* published install (the package ships it; `scripts/heyspex-doctor.mjs` does
|
|
590
|
+
* not) and parse its report. The bundle's cloud view is GET-only by
|
|
591
|
+
* construction (`connect-checks.ts` `cloudGet`). `spawn` / `exists` are
|
|
592
|
+
* injectable for tests.
|
|
593
|
+
* @returns {{ ok: true, report: object } | { ok: false, error: string }}
|
|
594
|
+
*/
|
|
595
|
+
export function runBundledConnectCheck(
|
|
596
|
+
pkgRoot,
|
|
597
|
+
{
|
|
598
|
+
cwd,
|
|
599
|
+
env = process.env,
|
|
600
|
+
spawn = spawnSync,
|
|
601
|
+
timeoutMs = 60_000,
|
|
602
|
+
exists = existsSync,
|
|
603
|
+
} = {},
|
|
604
|
+
) {
|
|
605
|
+
const script = join(pkgRoot, "dist", "cli", "hs-connect.mjs");
|
|
606
|
+
if (!exists(script)) {
|
|
607
|
+
return {
|
|
608
|
+
ok: false,
|
|
609
|
+
error: "dist/cli/hs-connect.mjs is missing from this install",
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
const r = spawn(process.execPath, [script, "connect", "--check", "--json"], {
|
|
613
|
+
cwd,
|
|
614
|
+
env,
|
|
615
|
+
encoding: "utf8",
|
|
616
|
+
timeout: timeoutMs,
|
|
617
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
618
|
+
});
|
|
619
|
+
if (r.error) {
|
|
620
|
+
return {
|
|
621
|
+
ok: false,
|
|
622
|
+
error: `could not run \`hs connect --check --json\`: ${r.error.message}`,
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
return parseConnectCheckOutput(r.stdout);
|
|
626
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// scripts/lib/subagent-git-lock.mjs
|
|
2
|
+
//
|
|
3
|
+
// The orchestrator (COO)'s hard lock around a delegated sub-agent's work
|
|
4
|
+
// window (HS-H8KI-P3TJ, epic HS-GG01-P3TJ). While set, EVERY destructive git
|
|
5
|
+
// command in the covered session is denied outright by
|
|
6
|
+
// scripts/claude-hooks/git-stash-gate.mjs's runGitDestructiveGate — no intent
|
|
7
|
+
// file can override it (deny beats ask beats allow; this task may only
|
|
8
|
+
// ESCALATE toward deny, never soften toward allow — see
|
|
9
|
+
// specs/memory/decisions/protected-shared-tree-actions-assisted.md).
|
|
10
|
+
//
|
|
11
|
+
// SPIKE VERDICT (HS-H8KI, static analysis, no harness spawn): Claude Code's
|
|
12
|
+
// PreToolUse hook payload carries NO sub-agent identity distinguishing an
|
|
13
|
+
// orchestrator's own tool calls from a spawned sub-agent's. The only identity
|
|
14
|
+
// field present anywhere in this repo's hook payloads is `session_id`
|
|
15
|
+
// (resolved by scripts/claude-hooks/claim-liveness-shared.mjs's
|
|
16
|
+
// resolveSessionId) — and it is the SAME value for every tool call inside one
|
|
17
|
+
// CLI session, orchestrator and any Task-tool-spawned sub-agent alike (they
|
|
18
|
+
// share one process/session). Every hook in scripts/claude-hooks/ that reads
|
|
19
|
+
// `transcript_path` (enforce-session-sequence.mjs, naughty-list-gate.mjs,
|
|
20
|
+
// id-display-format-gate.mjs) uses it for OTHER purposes; none of them, and no
|
|
21
|
+
// other file in this repo, reads an `agentId`/`isSidechain`/`sidechain` field —
|
|
22
|
+
// because the standard PreToolUse schema does not carry one
|
|
23
|
+
// (hook_event_name, session_id, transcript_path, cwd, tool_name, tool_input,
|
|
24
|
+
// tool_response are the complete field set observed across every hook here).
|
|
25
|
+
// Conclusion: per-agent locking is NOT possible from the hook payload alone.
|
|
26
|
+
// This module therefore implements the SESSION-WIDE fallback the task
|
|
27
|
+
// prescribes for that case — keyed by session id (so concurrent sessions on
|
|
28
|
+
// different checkouts/agents never share lock state), but NOT distinguishing
|
|
29
|
+
// the orchestrator's own tool calls from its delegated sub-agent's within that
|
|
30
|
+
// one session. That is the correct scope for the orchestrator's own use: it
|
|
31
|
+
// sets the lock immediately before spawning a sub-agent for destructive-git-
|
|
32
|
+
// adjacent delegated work and clears it right after the sub-agent returns, so
|
|
33
|
+
// the lock's live window IS the delegation window regardless of which of the
|
|
34
|
+
// two (orchestrator or sub-agent) happens to issue the git command.
|
|
35
|
+
//
|
|
36
|
+
// A session id may be unresolvable (e.g. a bare script run outside any hook
|
|
37
|
+
// context) — that case files under a fixed NO_SESSION_KEY so it still works
|
|
38
|
+
// but is trivially inspectable in .heyspex/subagent-locks/ instead of being
|
|
39
|
+
// silently lost.
|
|
40
|
+
|
|
41
|
+
import fs from 'node:fs';
|
|
42
|
+
import path from 'node:path';
|
|
43
|
+
|
|
44
|
+
const LOCK_DIR_NAME = 'subagent-locks';
|
|
45
|
+
const NO_SESSION_KEY = '_no-session-id';
|
|
46
|
+
|
|
47
|
+
/** Filesystem-safe key for a session id (or the fixed fallback key). Exported for tests. */
|
|
48
|
+
export function lockKey(sessionId) {
|
|
49
|
+
const trimmed = typeof sessionId === 'string' ? sessionId.trim() : '';
|
|
50
|
+
if (!trimmed) return NO_SESSION_KEY;
|
|
51
|
+
return trimmed.replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 128);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function lockDir(root) {
|
|
55
|
+
return path.join(root, '.heyspex', LOCK_DIR_NAME);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function lockPath(root, sessionId) {
|
|
59
|
+
return path.join(lockDir(root), `${lockKey(sessionId)}.json`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Set the session-wide hard lock. Idempotent — calling it again while already
|
|
64
|
+
* set just overwrites the record (last setter wins; there is deliberately no
|
|
65
|
+
* "already locked" error since the orchestrator is the only writer of its own
|
|
66
|
+
* lock file).
|
|
67
|
+
*
|
|
68
|
+
* @returns {object} the record written
|
|
69
|
+
*/
|
|
70
|
+
export function setSubagentLock({ root, sessionId = null, reason = null, setBy = null, now = Date.now() }) {
|
|
71
|
+
const dir = lockDir(root);
|
|
72
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
73
|
+
const record = {
|
|
74
|
+
sessionId: sessionId ?? null,
|
|
75
|
+
reason: reason ?? null,
|
|
76
|
+
setBy: setBy ?? null,
|
|
77
|
+
setAt: new Date(now).toISOString(),
|
|
78
|
+
};
|
|
79
|
+
const target = lockPath(root, sessionId);
|
|
80
|
+
const tmp = `${target}.${process.pid}.tmp`;
|
|
81
|
+
fs.writeFileSync(tmp, JSON.stringify(record, null, 2));
|
|
82
|
+
fs.renameSync(tmp, target);
|
|
83
|
+
return record;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Clear the session-wide hard lock. Returns true if a lock file was actually removed. */
|
|
87
|
+
export function clearSubagentLock({ root, sessionId = null }) {
|
|
88
|
+
try {
|
|
89
|
+
fs.rmSync(lockPath(root, sessionId), { force: true });
|
|
90
|
+
return true;
|
|
91
|
+
} catch {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Read the current lock record for `sessionId`, or null when unset/unreadable.
|
|
98
|
+
* Never throws — a corrupt lock file reads as "no lock" (fail toward allowing
|
|
99
|
+
* the gate to proceed to its next check, never toward a crash).
|
|
100
|
+
*/
|
|
101
|
+
export function checkSubagentLock({ root, sessionId = null }) {
|
|
102
|
+
try {
|
|
103
|
+
const raw = fs.readFileSync(lockPath(root, sessionId), 'utf8');
|
|
104
|
+
const parsed = JSON.parse(raw);
|
|
105
|
+
return parsed && typeof parsed === 'object' ? parsed : null;
|
|
106
|
+
} catch {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
}
|