heyspex 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -0
- package/bin/hs.mjs +931 -98
- package/bin/spexcode.mjs +262 -0
- package/dev-factory/engine/worktree.mjs +740 -0
- package/dev-factory/lib/code-repo-root.mjs +67 -0
- package/dev-factory/lib/context-budget.mjs +84 -0
- package/dev-factory/lib/epic-detect.mjs +190 -0
- package/dev-factory/lib/fleet-queue-file.mjs +846 -0
- package/dev-factory/lib/fleet-queue.mjs +426 -0
- package/dev-factory/lib/fleet-state.mjs +14 -18
- package/dev-factory/lib/model-catalog.mjs +338 -0
- package/dev-factory/lib/seat-context.mjs +199 -0
- package/dev-factory/lib/spec-io.mjs +93 -0
- package/dev-factory/lib/sufficiency-score.mjs +8 -2
- package/dev-factory/lib/worktree-allocator.mjs +744 -0
- package/dev-factory/model-catalog.json +222 -0
- package/dist/cli/hs-check.mjs +1085 -103
- package/dist/cli/hs-claim.mjs +936 -63
- package/dist/cli/hs-connect.mjs +9786 -1910
- package/dist/cli/hs-defer.mjs +5267 -3053
- package/dist/cli/hs-dev.mjs +564 -0
- package/dist/cli/hs-env.mjs +385 -299
- package/dist/cli/hs-index-rebuild.mjs +7129 -0
- package/dist/cli/hs-index-reconcile.mjs +7852 -0
- package/dist/cli/hs-init.mjs +777 -138
- package/dist/cli/hs-install.mjs +6583 -485
- package/dist/cli/hs-join.mjs +10137 -2368
- package/dist/cli/hs-vault-pull.mjs +4659 -5882
- package/dist/cli/hs-vault.mjs +3793 -1537
- package/dist/cli/hs-workspace.mjs +2550 -524
- package/dist/cli-templates/customer-vault/specs/entity-model.md +594 -601
- package/dist/cli-templates/customer-vault/specs/protocols/canonical-frontmatter-templates.md +110 -177
- package/dist/cli-templates/customer-vault/specs/protocols/destructive-action-checklist.md +122 -0
- package/dist/cli-templates/customer-vault/specs/protocols/memory-protocol.md +13 -15
- package/dist/cli-templates/customer-vault/specs/protocols/session-close-protocol.md +91 -0
- package/dist/cli-templates/customer-vault/specs/protocols/spec-hierarchy.md +150 -168
- package/dist/cli-templates/customer-vault/specs/protocols/spec-summary-authoring.md +83 -0
- package/dist/cli-templates/harness-commands/claude/hs-auto.md +36 -0
- package/dist/cli-templates/harness-commands/claude/hs-backfill.md +28 -0
- package/dist/cli-templates/harness-commands/claude/hs-capture.md +62 -0
- package/dist/cli-templates/harness-commands/claude/hs-defer.md +29 -0
- package/dist/cli-templates/harness-commands/claude/hs-dev.md +20 -0
- package/dist/cli-templates/harness-commands/claude/hs-doctor.md +32 -0
- package/dist/cli-templates/harness-commands/claude/hs-ground.md +28 -0
- package/dist/cli-templates/harness-commands/claude/hs-handoff.md +33 -0
- package/dist/cli-templates/harness-commands/claude/hs-log.md +27 -0
- package/dist/cli-templates/harness-commands/claude/hs-next.md +29 -0
- package/dist/cli-templates/harness-commands/claude/hs-promote.md +70 -0
- package/dist/cli-templates/harness-commands/claude/hs-push.md +35 -0
- package/dist/cli-templates/harness-commands/claude/hs-qc.md +27 -0
- package/dist/cli-templates/harness-commands/claude/hs-res.md +63 -0
- package/dist/cli-templates/harness-commands/claude/hs-resolve.md +23 -0
- package/dist/cli-templates/harness-commands/claude/hs-review.md +23 -0
- package/dist/cli-templates/harness-commands/claude/hs-view.md +26 -0
- package/dist/cli-templates/harness-commands/claude/kill-ports.md +32 -0
- package/dist/cli-templates/harness-commands/claude/prototype.md +91 -0
- package/dist/cli-templates/harness-commands/cursor/hs-auto.md +40 -0
- package/dist/cli-templates/harness-commands/cursor/hs-backfill.md +31 -0
- package/dist/cli-templates/harness-commands/cursor/hs-capture.md +65 -0
- package/dist/cli-templates/harness-commands/cursor/hs-defer.md +32 -0
- package/dist/cli-templates/harness-commands/cursor/hs-dev.md +24 -0
- package/dist/cli-templates/harness-commands/cursor/hs-doctor.md +36 -0
- package/dist/cli-templates/harness-commands/cursor/hs-ground.md +31 -0
- package/dist/cli-templates/harness-commands/cursor/hs-handoff.md +37 -0
- package/dist/cli-templates/harness-commands/cursor/hs-log.md +31 -0
- package/dist/cli-templates/harness-commands/cursor/hs-next.md +32 -0
- package/dist/cli-templates/harness-commands/cursor/hs-promote.md +73 -0
- package/dist/cli-templates/harness-commands/cursor/hs-push.md +38 -0
- package/dist/cli-templates/harness-commands/cursor/hs-qc.md +30 -0
- package/dist/cli-templates/harness-commands/cursor/hs-res.md +67 -0
- package/dist/cli-templates/harness-commands/cursor/hs-resolve.md +26 -0
- package/dist/cli-templates/harness-commands/cursor/hs-review.md +26 -0
- package/dist/cli-templates/harness-commands/cursor/hs-view.md +30 -0
- package/dist/cli-templates/harness-commands/cursor/kill-ports.md +35 -0
- package/dist/cli-templates/harness-commands/cursor/prototype.md +95 -0
- package/docs/mcp-client-config.md +259 -6
- package/package.json +231 -124
- package/plugins/heyspex/.claude-plugin/plugin.json +12 -0
- package/plugins/heyspex/.cursor-plugin/plugin.json +15 -0
- package/plugins/heyspex/.mcp.json +12 -0
- package/plugins/heyspex/bin/launch-mcp.mjs +95 -0
- package/plugins/heyspex/bin/run-hook.mjs +62 -0
- package/plugins/heyspex/hooks/cursor-hooks.json +227 -0
- package/plugins/heyspex/hooks/hooks.json +361 -0
- package/plugins/heyspex/mcp.json +13 -0
- package/scripts/check-mcp-freshness.mjs +17 -9
- package/scripts/claude-hooks/_env-vault-events.mjs +68 -0
- package/scripts/claude-hooks/_guardrail-events.mjs +96 -0
- package/scripts/claude-hooks/_guardrail-loader.mjs +494 -0
- package/scripts/claude-hooks/_intent-classifier-router.mjs +410 -0
- package/scripts/claude-hooks/_intent-classifier-semantic.mjs +351 -0
- package/scripts/claude-hooks/_run.mjs +120 -0
- package/scripts/claude-hooks/_transcript-text.mjs +60 -0
- package/scripts/claude-hooks/_vault-retrieval.mjs +322 -0
- package/scripts/claude-hooks/agent-spawn-delegation-gate.mjs +133 -0
- package/scripts/claude-hooks/capture-workflow-gate.mjs +11 -0
- package/scripts/claude-hooks/check-auto-memory-budget.mjs +396 -0
- package/scripts/claude-hooks/check-claude-md-budget.mjs +327 -0
- package/scripts/claude-hooks/check-mcp-freshness-hook.mjs +14 -0
- package/scripts/claude-hooks/claim-before-code-gate.mjs +11 -0
- package/scripts/claude-hooks/claim-liveness-shared.mjs +575 -0
- package/scripts/claude-hooks/close-terminal-on-ship.mjs +64 -0
- package/scripts/claude-hooks/coo-denylist-gate.mjs +11 -0
- package/scripts/claude-hooks/coo-guard-check.mjs +11 -0
- package/scripts/claude-hooks/deep-research-cost-gate.mjs +11 -0
- package/scripts/claude-hooks/dev-server-task-reminder.mjs +15 -0
- package/scripts/claude-hooks/emit-claim-status.mjs +41 -0
- package/scripts/claude-hooks/enforce-session-sequence.mjs +11 -0
- package/scripts/claude-hooks/enforce-vault-naming.mjs +11 -0
- package/scripts/claude-hooks/env-vault-guard.mjs +11 -0
- package/scripts/claude-hooks/epic-milestone-phrasing-gate.mjs +11 -0
- package/scripts/claude-hooks/git-identity-guard.mjs +11 -0
- package/scripts/claude-hooks/git-reflog-audit.mjs +15 -0
- package/scripts/claude-hooks/git-stash-gate.mjs +11 -0
- package/scripts/claude-hooks/guard-auto-memory-writes.mjs +137 -0
- package/scripts/claude-hooks/id-display-format-gate.mjs +11 -0
- package/scripts/claude-hooks/inject-communication-style.mjs +66 -0
- package/scripts/claude-hooks/inject-coo-seat.mjs +78 -0
- package/scripts/claude-hooks/inject-dev-server-awareness.mjs +11 -0
- package/scripts/claude-hooks/inject-doctrine.mjs +13 -0
- package/scripts/claude-hooks/inject-vault-context.mjs +14 -0
- package/scripts/claude-hooks/intent-classifier-trigger.mjs +247 -0
- package/scripts/claude-hooks/log-session-activity.mjs +341 -0
- package/scripts/claude-hooks/naughty-list-gate.mjs +11 -0
- package/scripts/claude-hooks/notify-status-flip.mjs +72 -0
- package/scripts/claude-hooks/plan-exit-ingest-gate.mjs +170 -0
- package/scripts/claude-hooks/plan-ingest-close-reminder.mjs +66 -0
- package/scripts/claude-hooks/plan-review-gate.mjs +11 -0
- package/scripts/claude-hooks/preflight-reminder.mjs +262 -0
- package/scripts/claude-hooks/protected-action-intent.mjs +221 -0
- package/scripts/claude-hooks/research-method-guard.mjs +398 -0
- package/scripts/claude-hooks/research-protocol-gate.mjs +11 -0
- package/scripts/claude-hooks/session-end.mjs +449 -0
- package/scripts/claude-hooks/settings-write-gate.mjs +11 -0
- package/scripts/claude-hooks/settlement-check-gate.mjs +39 -0
- package/scripts/claude-hooks/status-flip-detect.mjs +17 -0
- package/scripts/claude-hooks/terminal-close-shared.mjs +426 -0
- package/scripts/claude-hooks/terminal-close-watcher.mjs +122 -0
- package/scripts/claude-hooks/wip-checkpoint.mjs +298 -0
- package/scripts/cursor-hooks/manifest.mjs +235 -0
- package/scripts/dev-server-status.mjs +34 -13
- package/scripts/dev-server-stop.mjs +313 -0
- package/scripts/heyspex-mode.mjs +268 -0
- package/scripts/hs-auto-resolve.mjs +240 -0
- package/scripts/hs-auto-source.mjs +398 -0
- package/scripts/hs-auto.mjs +134 -0
- package/scripts/hs-capture-cli.mjs +326 -0
- package/scripts/hs-classifier.mjs +136 -0
- package/scripts/hs-companion.mjs +88 -0
- package/scripts/hs-gateway.mjs +115 -0
- package/scripts/hs-mode.mjs +125 -0
- package/scripts/hs-next.mjs +441 -0
- package/scripts/hs-settings.mjs +338 -0
- package/scripts/hs-view.mjs +333 -0
- package/scripts/hs-worktree.mjs +201 -0
- package/scripts/install-claude-code-hooks.mjs +270 -0
- package/scripts/install-cursor-hooks.mjs +471 -0
- package/scripts/lib/agent-env-markers.mjs +38 -0
- package/scripts/lib/console-url-resolver.mjs +197 -60
- package/scripts/lib/customer-kit.mjs +886 -0
- package/scripts/lib/dev-mode.mjs +244 -28
- package/scripts/lib/heyspex-home.mjs +72 -0
- package/scripts/lib/hs-command.mjs +277 -0
- package/scripts/lib/hs-companion-settings.mjs +472 -0
- package/scripts/lib/hs-source-commit.mjs +59 -5
- package/scripts/lib/hs-source-identity.mjs +526 -0
- package/scripts/lib/hs-source-log.mjs +11 -21
- package/scripts/lib/hs-source-push.mjs +107 -58
- package/scripts/lib/hs-worktree-args.mjs +115 -0
- package/scripts/lib/shared-path-helpers.mjs +134 -0
- package/scripts/lib/standalone-doctor.mjs +540 -3
- package/scripts/lib/subagent-git-lock.mjs +109 -0
- package/scripts/lib/template-pin.mjs +34 -0
- package/scripts/provenance-lib.mjs +91 -0
- package/scripts/stamp-provenance-on-edit.mjs +222 -0
- package/scripts/wip-ledger-health.mjs +653 -0
- package/spexharness/doctrine/baseline/SPEXCODE-DOCTRINE.baseline.json +29 -0
- package/spexharness/src/adapters/audit.mjs +197 -0
- package/spexharness/src/adapters/claude-code.mjs +441 -0
- package/spexharness/src/chat/tui/ansi.mjs +448 -0
- package/spexharness/src/chat/tui/palette.mjs +22 -0
- package/spexharness/src/chat/tui/select.mjs +208 -0
- package/spexharness/src/config-root.mjs +54 -0
- package/spexharness/src/context-budget.mjs +141 -0
- package/spexharness/src/dev-server-control.mjs +389 -0
- package/spexharness/src/doctrine/behaviour.mjs +371 -0
- package/spexharness/src/doctrine/corpus.mjs +733 -0
- package/spexharness/src/doctrine/inject.mjs +338 -0
- package/spexharness/src/doctrine/merge.mjs +184 -0
- package/spexharness/src/doctrine/sources.mjs +252 -0
- package/spexharness/src/doctrine/synthesize.mjs +401 -0
- package/spexharness/src/doctrine/vault-source.mjs +574 -0
- package/spexharness/src/doctrine/writer.mjs +176 -0
- package/spexharness/src/events.mjs +84 -0
- package/spexharness/src/gates/capture-workflow-gate.mjs +202 -0
- package/spexharness/src/gates/claim-before-code-gate.decide.mjs +33 -0
- package/spexharness/src/gates/claim-before-code-gate.mjs +129 -0
- package/spexharness/src/gates/deep-research-cost-gate.mjs +274 -0
- package/spexharness/src/gates/enforce-session-sequence.mjs +895 -0
- package/spexharness/src/gates/enforce-vault-naming.mjs +215 -0
- package/spexharness/src/gates/epic-milestone-phrasing-gate.mjs +233 -0
- package/spexharness/src/gates/git-identity-guard.decide.mjs +436 -0
- package/spexharness/src/gates/git-identity-guard.mjs +61 -0
- package/spexharness/src/gates/git-reflog-audit.mjs +151 -0
- package/spexharness/src/gates/git-stash-gate.decide.mjs +361 -0
- package/spexharness/src/gates/git-stash-gate.mjs +159 -0
- package/spexharness/src/gates/id-display-format-gate.mjs +190 -0
- package/spexharness/src/gates/inject-doctrine-gate.mjs +89 -0
- package/spexharness/src/gates/inject-vault-context.mjs +239 -0
- package/spexharness/src/gates/naughty-list-gate.mjs +213 -0
- package/spexharness/src/gates/plan-review-gate.mjs +233 -0
- package/spexharness/src/gates/preflight-reminder-gate.mjs +265 -0
- package/spexharness/src/gates/research-protocol-gate.mjs +175 -0
- package/spexharness/src/gates/settings-write-gate.decide.mjs +96 -0
- package/spexharness/src/gates/settings-write-gate.mjs +104 -0
- package/spexharness/src/git-reflog-audit.mjs +294 -0
- package/spexharness/src/intent-classifier.mjs +194 -0
- package/spexharness/src/intent-directive.mjs +541 -0
- package/spexharness/src/profile/learn.mjs +551 -0
- package/spexharness/src/profile/proposals.mjs +357 -0
- package/spexharness/src/profile/vault-inclusion.mjs +386 -0
- package/spexharness/src/redact.mjs +151 -0
- package/spexharness/src/rule-table.mjs +819 -0
- package/spexharness/src/seat-context.mjs +122 -0
- package/spexharness/src/session-bootstrap.mjs +512 -0
- package/spexharness/src/session-record.mjs +815 -0
- package/spexharness/src/token-inventory.mjs +207 -0
- package/vendor/heyspex-mcp/account-switch.mjs +95 -0
- package/vendor/heyspex-mcp/alignment-gate.mjs +315 -1
- package/vendor/heyspex-mcp/blocker-resolver.mjs +37 -2
- package/vendor/heyspex-mcp/breakdown-ladder.mjs +119 -4
- package/vendor/heyspex-mcp/breakdown-source.mjs +271 -0
- package/vendor/heyspex-mcp/breakdown-writer.mjs +23 -3
- package/vendor/heyspex-mcp/capture-buffer-patches.mjs +46 -5
- package/vendor/heyspex-mcp/capture-channel.mjs +55 -5
- package/vendor/heyspex-mcp/capture-console-client.mjs +80 -1
- package/vendor/heyspex-mcp/capture-focus.mjs +1035 -29
- package/vendor/heyspex-mcp/capture-outbox.mjs +5 -1
- package/vendor/heyspex-mcp/capture-tools.mjs +1219 -0
- package/vendor/heyspex-mcp/claim-tools.mjs +1124 -0
- package/vendor/heyspex-mcp/cli-verbs.mjs +2076 -0
- package/vendor/heyspex-mcp/cli.mjs +201 -15524
- package/vendor/heyspex-mcp/cloud-tool-surface.mjs +128 -0
- package/vendor/heyspex-mcp/cloud-vault-writes.mjs +412 -0
- package/vendor/heyspex-mcp/config-seed.yaml +15 -0
- package/vendor/heyspex-mcp/config-settings.mjs +290 -7
- package/vendor/heyspex-mcp/console-fetch.mjs +657 -16
- package/vendor/heyspex-mcp/console-transport.mjs +166 -0
- package/vendor/heyspex-mcp/credentials.mjs +584 -0
- package/vendor/heyspex-mcp/defer-spec.mjs +44 -4
- package/vendor/heyspex-mcp/deferred-reclaim.mjs +29 -3
- package/vendor/heyspex-mcp/design-handlers.mjs +31 -0
- package/vendor/heyspex-mcp/design-tools.mjs +4 -1
- package/vendor/heyspex-mcp/dev-factory-bridge.mjs +4 -4
- package/vendor/heyspex-mcp/dev-mode-core.mjs +45 -0
- package/vendor/heyspex-mcp/dev-tools.mjs +124 -0
- package/vendor/heyspex-mcp/diagnose-handlers.mjs +54 -0
- package/vendor/heyspex-mcp/events-tools.mjs +207 -0
- package/vendor/heyspex-mcp/filing-tools.mjs +688 -0
- package/vendor/heyspex-mcp/fleet-handlers.mjs +37 -0
- package/vendor/heyspex-mcp/fleet-queue-tools.mjs +69 -14
- package/vendor/heyspex-mcp/focus-tools.mjs +190 -0
- package/vendor/heyspex-mcp/folder-trust-store.mjs +59 -0
- package/vendor/heyspex-mcp/http-transport.mjs +300 -0
- package/vendor/heyspex-mcp/list-tools.mjs +257 -0
- package/vendor/heyspex-mcp/login-handoff.mjs +15 -1
- package/vendor/heyspex-mcp/mcp-config.mjs +94 -0
- package/vendor/heyspex-mcp/memory-tools.mjs +523 -0
- package/vendor/heyspex-mcp/mvp-mode.mjs +53 -6
- package/vendor/heyspex-mcp/occupied-keys.mjs +141 -0
- package/vendor/heyspex-mcp/on-deck-brief.mjs +3360 -0
- package/vendor/heyspex-mcp/on-deck-scheduler.mjs +1708 -0
- package/vendor/heyspex-mcp/package-root.mjs +32 -0
- package/vendor/heyspex-mcp/peer-presence.mjs +8 -1
- package/vendor/heyspex-mcp/plan-ground.mjs +621 -652
- package/vendor/heyspex-mcp/plan-push.mjs +906 -21
- package/vendor/heyspex-mcp/plan-review.mjs +124 -30
- package/vendor/heyspex-mcp/preflight-gate.mjs +0 -0
- package/vendor/heyspex-mcp/procedure-handlers.mjs +325 -0
- package/vendor/heyspex-mcp/procedure-tools.mjs +540 -351
- package/vendor/heyspex-mcp/promote-capture.mjs +386 -69
- package/vendor/heyspex-mcp/promote-diagnose.mjs +74 -0
- package/vendor/heyspex-mcp/promote-research-to-diagnose.mjs +9 -2
- package/vendor/heyspex-mcp/promote-research.mjs +660 -53
- package/vendor/heyspex-mcp/promotion-helpers.mjs +251 -0
- package/vendor/heyspex-mcp/prompts.mjs +1460 -0
- package/vendor/heyspex-mcp/read-path.mjs +29 -9
- package/vendor/heyspex-mcp/receipt-shape.mjs +146 -0
- package/vendor/heyspex-mcp/recipe-handlers.mjs +117 -0
- package/vendor/heyspex-mcp/regen-tasks.mjs +210 -3
- package/vendor/heyspex-mcp/register-spec.mjs +266 -54
- package/vendor/heyspex-mcp/request-context.mjs +59 -0
- package/vendor/heyspex-mcp/research-focus.mjs +207 -4
- package/vendor/heyspex-mcp/research-handlers.mjs +623 -0
- package/vendor/heyspex-mcp/research-tools.mjs +4 -4
- package/vendor/heyspex-mcp/response-budget.mjs +348 -0
- package/vendor/heyspex-mcp/review-focus.mjs +25 -1
- package/vendor/heyspex-mcp/search-specs.mjs +280 -0
- package/vendor/heyspex-mcp/seat-block-ledger.mjs +113 -0
- package/vendor/heyspex-mcp/seat-identity.mjs +2 -6
- package/vendor/heyspex-mcp/session-owner.mjs +108 -0
- package/vendor/heyspex-mcp/session-tools.mjs +152 -0
- package/vendor/heyspex-mcp/settings-registry.mjs +232 -0
- package/vendor/heyspex-mcp/settings-tools.mjs +85 -12
- package/vendor/heyspex-mcp/spec-edit-tools.mjs +446 -0
- package/vendor/heyspex-mcp/spec-id-lib.mjs +93 -17
- package/vendor/heyspex-mcp/split-research.mjs +301 -56
- package/vendor/heyspex-mcp/stdio-rpc.mjs +22 -0
- package/vendor/heyspex-mcp/submit-for-review.mjs +132 -6
- package/vendor/heyspex-mcp/summary-contract.mjs +105 -0
- package/vendor/heyspex-mcp/summary-write-tools.mjs +224 -0
- package/vendor/heyspex-mcp/symptom-query.mjs +146 -0
- package/vendor/heyspex-mcp/test-helpers.mjs +108 -1
- package/vendor/heyspex-mcp/tool-catalog.mjs +2813 -0
- package/vendor/heyspex-mcp/tool-dispatch.mjs +639 -0
- package/vendor/heyspex-mcp/tool-helpers.mjs +325 -0
- package/vendor/heyspex-mcp/validate-linkage.mjs +414 -27
- package/vendor/heyspex-mcp/vault-io.mjs +724 -0
- package/vendor/heyspex-mcp/vault-less-endpoints.mjs +52 -0
- package/vendor/heyspex-mcp/vault-resolve.mjs +366 -68
- package/vendor/heyspex-mcp/verify-reproduction.mjs +270 -30
- package/vendor/heyspex-mcp/whoami.mjs +91 -0
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* hs-source-identity.mjs — `hs source identity [show|set|clear]`: the ONE
|
|
4
|
+
* sanctioned way a git author identity is written on this machine.
|
|
5
|
+
*
|
|
6
|
+
* THE INCIDENT (2026-09-19)
|
|
7
|
+
* ------------------------
|
|
8
|
+
* Agents that hit git's "Author identity unknown — Please tell me who you
|
|
9
|
+
* are" worked around it by running `git config user.email <the email on the
|
|
10
|
+
* owner's Claude account>` in the repo-local `.git/config` of four repos. A
|
|
11
|
+
* repo-local value beats the global identity the owner set later, so every
|
|
12
|
+
* commit from this machine was authored as a non-team address for ten days
|
|
13
|
+
* and Vercel silently refused two production deploys. Vault lesson:
|
|
14
|
+
* `when-git-says-author-identity-unknown`.
|
|
15
|
+
*
|
|
16
|
+
* THE RULING
|
|
17
|
+
* ----------
|
|
18
|
+
* A repo-local (or global) git identity override is a HUMAN-AUTHORIZED
|
|
19
|
+
* action only, run through this command by the human themselves — in their
|
|
20
|
+
* own terminal, or in Claude Code with the `!` prefix (which runs outside the
|
|
21
|
+
* agent's tool calls). Agents may READ identity (`show`), never write it.
|
|
22
|
+
*
|
|
23
|
+
* THREE LAYERS enforce it (docs/admin/workflow/git-identity.md):
|
|
24
|
+
* 1. this command — `set`/`clear` refuse when an agent env marker is
|
|
25
|
+
* present, unless `--human` / HS_HUMAN_IDENTITY=1 says a human typed it
|
|
26
|
+
* through Claude Code's `!` prefix (which inherits the agent's env);
|
|
27
|
+
* 2. the PreToolUse gate spexharness/src/gates/git-identity-guard.mjs —
|
|
28
|
+
* an agent cannot even ask a tool to run `git config user.email …`;
|
|
29
|
+
* 3. the pre-commit guard scripts/guard-git-identity.mjs — a repo-local
|
|
30
|
+
* override that this command did not record cannot commit.
|
|
31
|
+
*
|
|
32
|
+
* WHAT `set` RECORDS: `.heyspex/git-identity.json` at the repo root —
|
|
33
|
+
* `{ scope, email, name, authorizedAt, authorizedBy: "human", via }`. Layer 3
|
|
34
|
+
* and `show` compare the live `.git/config` against it; a mismatch is an
|
|
35
|
+
* override written behind the command's back. The email is ALWAYS the
|
|
36
|
+
* human's explicit `--email` argument — never read from any HeySpex, Clerk
|
|
37
|
+
* or Claude identity source (that is precisely the mistake the incident made).
|
|
38
|
+
*
|
|
39
|
+
* Exit codes: 0 ok · 2 usage · 3 refused (agent shell without --human).
|
|
40
|
+
*
|
|
41
|
+
* Every git call goes through an injectable runner (`run`) so the test
|
|
42
|
+
* (hs-source-identity.test.mjs) drives temp repos only and never touches this
|
|
43
|
+
* machine's real identity.
|
|
44
|
+
*/
|
|
45
|
+
import { execFileSync } from 'node:child_process';
|
|
46
|
+
import fs from 'node:fs';
|
|
47
|
+
import path from 'node:path';
|
|
48
|
+
import { fileURLToPath } from 'node:url';
|
|
49
|
+
|
|
50
|
+
import { detectAgentMarkers } from './agent-env-markers.mjs';
|
|
51
|
+
|
|
52
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
53
|
+
export const REPO_ROOT = path.resolve(__dirname, '..', '..');
|
|
54
|
+
|
|
55
|
+
/** Where `set` records the human authorization, relative to the repo root. Git-ignored. */
|
|
56
|
+
export const AUTH_FILE_REL = path.join('.heyspex', 'git-identity.json');
|
|
57
|
+
|
|
58
|
+
/** The flag / env var that says "a human typed this through Claude Code's `!` prefix". */
|
|
59
|
+
export const HUMAN_FLAG = '--human';
|
|
60
|
+
export const HUMAN_ENV = 'HS_HUMAN_IDENTITY';
|
|
61
|
+
|
|
62
|
+
/** The env vars git consults before any config file. Any of these set = an override. */
|
|
63
|
+
export const GIT_IDENTITY_ENV_VARS = [
|
|
64
|
+
'GIT_AUTHOR_EMAIL',
|
|
65
|
+
'GIT_AUTHOR_NAME',
|
|
66
|
+
'GIT_COMMITTER_EMAIL',
|
|
67
|
+
'GIT_COMMITTER_NAME',
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
export const EXIT = Object.freeze({ OK: 0, USAGE: 2, REFUSED: 3 });
|
|
71
|
+
|
|
72
|
+
/** The two sanctioned human forms — quoted verbatim in every refusal, doc and gate reason. */
|
|
73
|
+
export const HUMAN_FORMS = Object.freeze({
|
|
74
|
+
terminal: 'hs source identity set --email you@example.com',
|
|
75
|
+
claudeCode: '!hs source identity set --email you@example.com --human',
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
export const VAULT_LESSON = 'when-git-says-author-identity-unknown';
|
|
79
|
+
|
|
80
|
+
export const USAGE =
|
|
81
|
+
'hs source identity — read or (human-only) write the git author identity\n\n' +
|
|
82
|
+
' hs source identity [show] [--json] Effective user.name / user.email, where each\n' +
|
|
83
|
+
' comes from, and whether a repo-local override is\n' +
|
|
84
|
+
' authorized (read-only; agents may run this)\n' +
|
|
85
|
+
' hs source identity set --email <email> [--name <name>] Write the identity (repo-local by default,\n' +
|
|
86
|
+
' [--global] [--human] --global for the global one) and record the\n' +
|
|
87
|
+
' authorization in .heyspex/git-identity.json\n' +
|
|
88
|
+
' hs source identity clear [--human] Remove the repo-local [user] override and the\n' +
|
|
89
|
+
' authorization; the global identity applies again\n\n' +
|
|
90
|
+
' set / clear are HUMAN-ONLY. Type them yourself:\n' +
|
|
91
|
+
` in your own terminal: ${HUMAN_FORMS.terminal}\n` +
|
|
92
|
+
` inside Claude Code: ${HUMAN_FORMS.claudeCode}\n` +
|
|
93
|
+
' (the `!` prefix runs outside the agent\'s tool calls but inherits its env — that is the only\n' +
|
|
94
|
+
' reason --human exists). Exit codes: 0 ok · 2 usage · 3 refused.\n';
|
|
95
|
+
|
|
96
|
+
// ---- git runner ------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Default subprocess runner — injectable so tests can spy / redirect to a
|
|
100
|
+
* temp repo. Never throws on a non-zero exit; callers check `.status`.
|
|
101
|
+
* @param {string} cmd
|
|
102
|
+
* @param {string[]} args
|
|
103
|
+
* @param {{ cwd?: string, env?: Record<string, string|undefined> }} [opts]
|
|
104
|
+
* @returns {{ status: number, stdout: string, stderr: string }}
|
|
105
|
+
*/
|
|
106
|
+
export function defaultRun(cmd, args, opts = {}) {
|
|
107
|
+
try {
|
|
108
|
+
const stdout = execFileSync(cmd, args, {
|
|
109
|
+
encoding: 'utf8',
|
|
110
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
111
|
+
...opts,
|
|
112
|
+
});
|
|
113
|
+
return { status: 0, stdout, stderr: '' };
|
|
114
|
+
} catch (err) {
|
|
115
|
+
return {
|
|
116
|
+
status: typeof err.status === 'number' ? err.status : 1,
|
|
117
|
+
stdout: err.stdout ? String(err.stdout) : '',
|
|
118
|
+
stderr: err.stderr ? String(err.stderr) : String(err.message ?? ''),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function git(run, args, { cwd, env }) {
|
|
124
|
+
return run('git', args, { cwd, env });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ---- reading ---------------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
/** Infer git's config scope from a `--show-origin` origin string when `--show-scope` is unavailable. */
|
|
130
|
+
function scopeFromOrigin(origin) {
|
|
131
|
+
const o = String(origin ?? '').replace(/\\/g, '/');
|
|
132
|
+
if (/\/\.git\/config$/.test(o) || /\/\.git\/worktrees\/[^/]+\/config\.worktree$/.test(o)) return 'local';
|
|
133
|
+
if (/\/config\.worktree$/.test(o)) return 'worktree';
|
|
134
|
+
if (/\/etc\/gitconfig$/.test(o)) return 'system';
|
|
135
|
+
if (/\.gitconfig$/.test(o) || /\/\.config\/git\/config$/.test(o)) return 'global';
|
|
136
|
+
if (/^command line/.test(o) || /^blob:/.test(o)) return 'command';
|
|
137
|
+
return 'file';
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Every value git holds for `key`, most-specific LAST (git's own precedence:
|
|
142
|
+
* the last line wins), with the scope and file each comes from.
|
|
143
|
+
*
|
|
144
|
+
* @param {string} key
|
|
145
|
+
* @param {{ run?: Function, cwd: string, env?: Record<string, string|undefined> }} deps
|
|
146
|
+
* @returns {{ scope: string, origin: string, value: string }[]}
|
|
147
|
+
*/
|
|
148
|
+
export function readConfigOrigins(key, { run = defaultRun, cwd, env = process.env }) {
|
|
149
|
+
const r = git(run, ['config', '--show-scope', '--show-origin', '--get-all', key], { cwd, env });
|
|
150
|
+
if (r.status !== 0 || !r.stdout.trim()) return [];
|
|
151
|
+
return r.stdout
|
|
152
|
+
.split(/\r?\n/)
|
|
153
|
+
.filter((l) => l.trim())
|
|
154
|
+
.map((line) => {
|
|
155
|
+
const parts = line.split('\t');
|
|
156
|
+
if (parts.length >= 3) {
|
|
157
|
+
return { scope: parts[0], origin: parts[1], value: parts.slice(2).join('\t') };
|
|
158
|
+
}
|
|
159
|
+
// Older git without --show-scope: `<origin>\t<value>`.
|
|
160
|
+
return { scope: scopeFromOrigin(parts[0]), origin: parts[0], value: parts.slice(1).join('\t') };
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Repo-local (`.git/config`) values only — what an unauthorized override looks like. */
|
|
165
|
+
export function readLocalIdentity({ run = defaultRun, cwd, env = process.env }) {
|
|
166
|
+
const get = (key) => {
|
|
167
|
+
const r = git(run, ['config', '--local', '--get-all', key], { cwd, env });
|
|
168
|
+
if (r.status !== 0) return null;
|
|
169
|
+
const lines = r.stdout.split(/\r?\n/).filter((l) => l.trim());
|
|
170
|
+
return lines.length ? lines[lines.length - 1] : null;
|
|
171
|
+
};
|
|
172
|
+
return { email: get('user.email'), name: get('user.name') };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Repo top-level for `cwd`, or null when not inside a git work tree. */
|
|
176
|
+
export function resolveRepoRoot({ run = defaultRun, cwd, env = process.env }) {
|
|
177
|
+
const r = git(run, ['rev-parse', '--show-toplevel'], { cwd, env });
|
|
178
|
+
if (r.status !== 0) return null;
|
|
179
|
+
const top = r.stdout.trim();
|
|
180
|
+
return top ? path.resolve(top) : null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** The recorded human authorization, or null when absent / unreadable. */
|
|
184
|
+
export function readAuthorization(repoRoot) {
|
|
185
|
+
if (!repoRoot) return null;
|
|
186
|
+
try {
|
|
187
|
+
const parsed = JSON.parse(fs.readFileSync(path.join(repoRoot, AUTH_FILE_REL), 'utf8'));
|
|
188
|
+
return parsed && typeof parsed === 'object' ? parsed : null;
|
|
189
|
+
} catch {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** The GIT_AUTHOR_* / GIT_COMMITTER_* values present in `env` (set to anything non-blank). */
|
|
195
|
+
export function envOverrides(env = process.env) {
|
|
196
|
+
const out = {};
|
|
197
|
+
for (const k of GIT_IDENTITY_ENV_VARS) {
|
|
198
|
+
if (env[k] != null && String(env[k]).trim() !== '') out[k] = String(env[k]);
|
|
199
|
+
}
|
|
200
|
+
return out;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* The env values that actually OVERRIDE the config-resolved identity.
|
|
205
|
+
*
|
|
206
|
+
* git itself exports GIT_AUTHOR_NAME / GIT_AUTHOR_EMAIL into a hook's
|
|
207
|
+
* environment (pre-commit included), mirroring the identity it resolved from
|
|
208
|
+
* config — so "the var is set" is not evidence of an override inside a hook.
|
|
209
|
+
* An env value is an override only when it DIFFERS from config: a forged
|
|
210
|
+
* author for one commit, or `git commit --author=…` (git exports that too,
|
|
211
|
+
* which is how the pre-commit guard catches --author as a side effect).
|
|
212
|
+
*
|
|
213
|
+
* @param {Record<string,string|undefined>} env
|
|
214
|
+
* @param {{ email: string|null, name: string|null }} configIdentity - config-only values (no env).
|
|
215
|
+
* @returns {Record<string, string>} var → value, for the vars that differ.
|
|
216
|
+
*/
|
|
217
|
+
export function effectiveEnvOverrides(env, configIdentity) {
|
|
218
|
+
const present = envOverrides(env);
|
|
219
|
+
const out = {};
|
|
220
|
+
for (const [k, v] of Object.entries(present)) {
|
|
221
|
+
const expected = /EMAIL$/.test(k) ? configIdentity.email : configIdentity.name;
|
|
222
|
+
if ((expected ?? null) === null || v.trim() !== String(expected).trim()) out[k] = v;
|
|
223
|
+
}
|
|
224
|
+
return out;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Does the live repo-local override match the recorded authorization?
|
|
229
|
+
* Both email AND name must match (the authorization records the post-`set`
|
|
230
|
+
* local values, so a name someone added afterwards is a mismatch too).
|
|
231
|
+
*/
|
|
232
|
+
export function authorizationMatches(local, auth) {
|
|
233
|
+
if (!auth || auth.scope !== 'local' || auth.authorizedBy !== 'human') return false;
|
|
234
|
+
return (local.email ?? null) === (auth.email ?? null) && (local.name ?? null) === (auth.name ?? null);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* The full read-only picture: effective identity + provenance, the local
|
|
239
|
+
* override, the authorization record, env overrides, and a one-line verdict.
|
|
240
|
+
*
|
|
241
|
+
* @param {{ run?: Function, cwd?: string, env?: Record<string, string|undefined> }} [deps]
|
|
242
|
+
*/
|
|
243
|
+
export function inspectIdentity({ run = defaultRun, cwd = process.cwd(), env = process.env } = {}) {
|
|
244
|
+
const repoRoot = resolveRepoRoot({ run, cwd, env });
|
|
245
|
+
const emailOrigins = readConfigOrigins('user.email', { run, cwd, env });
|
|
246
|
+
const nameOrigins = readConfigOrigins('user.name', { run, cwd, env });
|
|
247
|
+
const configIdentity = {
|
|
248
|
+
email: emailOrigins[emailOrigins.length - 1]?.value ?? null,
|
|
249
|
+
name: nameOrigins[nameOrigins.length - 1]?.value ?? null,
|
|
250
|
+
};
|
|
251
|
+
const overrides = effectiveEnvOverrides(env, configIdentity);
|
|
252
|
+
const local = repoRoot ? readLocalIdentity({ run, cwd, env }) : { email: null, name: null };
|
|
253
|
+
const localOverride = local.email != null || local.name != null;
|
|
254
|
+
const auth = readAuthorization(repoRoot);
|
|
255
|
+
const authorized = localOverride ? authorizationMatches(local, auth) : null;
|
|
256
|
+
|
|
257
|
+
const effective = (origins, envKeys) => {
|
|
258
|
+
for (const k of envKeys) {
|
|
259
|
+
if (overrides[k]) return { value: overrides[k], source: `env ${k}`, scope: 'env' };
|
|
260
|
+
}
|
|
261
|
+
const last = origins[origins.length - 1];
|
|
262
|
+
if (!last) return { value: null, source: 'not set', scope: null };
|
|
263
|
+
return { value: last.value, source: `${last.scope}: ${last.origin}`, scope: last.scope };
|
|
264
|
+
};
|
|
265
|
+
const email = effective(emailOrigins, ['GIT_AUTHOR_EMAIL', 'GIT_COMMITTER_EMAIL']);
|
|
266
|
+
const name = effective(nameOrigins, ['GIT_AUTHOR_NAME', 'GIT_COMMITTER_NAME']);
|
|
267
|
+
|
|
268
|
+
let verdict;
|
|
269
|
+
let ok;
|
|
270
|
+
if (Object.keys(overrides).length) {
|
|
271
|
+
ok = false;
|
|
272
|
+
verdict =
|
|
273
|
+
`unauthorized environment override (${Object.keys(overrides).join(', ')}) — unset it; ` +
|
|
274
|
+
`identity is written only by hs source identity set (human)`;
|
|
275
|
+
} else if (localOverride && !authorized) {
|
|
276
|
+
ok = false;
|
|
277
|
+
verdict =
|
|
278
|
+
'unauthorized repo-local override — run hs source identity clear (human) or ' +
|
|
279
|
+
`hs source identity set --email <email> (human) to record it`;
|
|
280
|
+
} else if (!email.value || !name.value) {
|
|
281
|
+
ok = false;
|
|
282
|
+
verdict = `no ${!email.value ? 'user.email' : 'user.name'} configured — a human runs: ${HUMAN_FORMS.terminal}`;
|
|
283
|
+
} else if (localOverride) {
|
|
284
|
+
ok = true;
|
|
285
|
+
verdict = `ok (repo-local override authorized by human ${auth.authorizedAt ?? ''})`.replace(/\s+\)/, ')');
|
|
286
|
+
} else {
|
|
287
|
+
ok = true;
|
|
288
|
+
verdict = 'ok';
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return {
|
|
292
|
+
repoRoot,
|
|
293
|
+
email,
|
|
294
|
+
name,
|
|
295
|
+
origins: { email: emailOrigins, name: nameOrigins },
|
|
296
|
+
localOverride: localOverride ? local : null,
|
|
297
|
+
authorization: auth,
|
|
298
|
+
authorized,
|
|
299
|
+
envOverrides: overrides,
|
|
300
|
+
ok,
|
|
301
|
+
verdict,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** Human-readable `show` output. */
|
|
306
|
+
export function formatIdentityReport(info) {
|
|
307
|
+
const lines = [];
|
|
308
|
+
lines.push(`git identity${info.repoRoot ? ` — ${info.repoRoot}` : ' — (not inside a git work tree)'}`);
|
|
309
|
+
lines.push(` user.email ${info.email.value ?? '(not set)'} [${info.email.source}]`);
|
|
310
|
+
lines.push(` user.name ${info.name.value ?? '(not set)'} [${info.name.source}]`);
|
|
311
|
+
if (info.localOverride) {
|
|
312
|
+
const l = info.localOverride;
|
|
313
|
+
lines.push(` repo-local [user] override: present (email=${l.email ?? '-'}, name=${l.name ?? '-'})`);
|
|
314
|
+
if (!info.authorization) lines.push(` authorization (${AUTH_FILE_REL}): none recorded`);
|
|
315
|
+
else if (info.authorized) {
|
|
316
|
+
lines.push(
|
|
317
|
+
` authorization (${AUTH_FILE_REL}): matches — ${info.authorization.authorizedBy} via "${info.authorization.via}" at ${info.authorization.authorizedAt}`,
|
|
318
|
+
);
|
|
319
|
+
} else {
|
|
320
|
+
lines.push(
|
|
321
|
+
` authorization (${AUTH_FILE_REL}): MISMATCH — recorded scope=${info.authorization.scope ?? '-'} email=${info.authorization.email ?? '-'} name=${info.authorization.name ?? '-'}`,
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
} else {
|
|
325
|
+
lines.push(' repo-local [user] override: none');
|
|
326
|
+
}
|
|
327
|
+
const envKeys = Object.keys(info.envOverrides);
|
|
328
|
+
lines.push(` env overrides: ${envKeys.length ? envKeys.map((k) => `${k}=${info.envOverrides[k]}`).join(', ') : 'none'}`);
|
|
329
|
+
lines.push(` verdict: ${info.verdict}`);
|
|
330
|
+
return lines.join('\n') + '\n';
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// ---- writing (human-only) --------------------------------------------------
|
|
334
|
+
|
|
335
|
+
export const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
336
|
+
|
|
337
|
+
/** Parse `hs source identity …` argv (after the `identity` token). */
|
|
338
|
+
export function parseIdentityArgs(argv) {
|
|
339
|
+
const out = { verb: 'show', json: false, human: false, global: false, email: null, name: null, errors: [] };
|
|
340
|
+
const args = [...argv];
|
|
341
|
+
if (args[0] && !args[0].startsWith('-')) out.verb = args.shift();
|
|
342
|
+
while (args.length) {
|
|
343
|
+
const a = args.shift();
|
|
344
|
+
if (a === '--json') out.json = true;
|
|
345
|
+
else if (a === HUMAN_FLAG) out.human = true;
|
|
346
|
+
else if (a === '--global') out.global = true;
|
|
347
|
+
else if (a === '--email' || a.startsWith('--email=')) {
|
|
348
|
+
out.email = a.includes('=') ? a.slice('--email='.length) : args.shift() ?? null;
|
|
349
|
+
} else if (a === '--name' || a.startsWith('--name=')) {
|
|
350
|
+
out.name = a.includes('=') ? a.slice('--name='.length) : args.shift() ?? null;
|
|
351
|
+
} else if (a === '--help' || a === '-h') out.verb = 'help';
|
|
352
|
+
else out.errors.push(`unknown argument: ${a}`);
|
|
353
|
+
}
|
|
354
|
+
if (!['show', 'set', 'clear', 'help'].includes(out.verb)) {
|
|
355
|
+
out.errors.push(`unknown verb: ${out.verb} (expected show | set | clear)`);
|
|
356
|
+
}
|
|
357
|
+
if (out.verb === 'set') {
|
|
358
|
+
if (!out.email) out.errors.push('set requires --email <email>');
|
|
359
|
+
else if (!EMAIL_RE.test(out.email)) out.errors.push(`--email does not look like an email address: ${out.email}`);
|
|
360
|
+
if (out.name != null && !out.name.trim()) out.errors.push('--name must not be empty');
|
|
361
|
+
}
|
|
362
|
+
return out;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/** True when a human vouched for this invocation (`--human` or HS_HUMAN_IDENTITY). */
|
|
366
|
+
export function humanVouched({ human, env = process.env }) {
|
|
367
|
+
if (human) return true;
|
|
368
|
+
const v = String(env[HUMAN_ENV] ?? '').trim().toLowerCase();
|
|
369
|
+
return v !== '' && v !== '0' && v !== 'false' && v !== 'no' && v !== 'off';
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/** The refusal text printed when an agent shell runs `set`/`clear` without `--human`. */
|
|
373
|
+
export function refusalMessage(verb, markers) {
|
|
374
|
+
return (
|
|
375
|
+
`hs source identity ${verb}: REFUSED — an AI agent shell is running this (${markers.join(', ')}).\n` +
|
|
376
|
+
'Git identity is a human-authorized action only (owner ruling 2026-09-19, vault lesson\n' +
|
|
377
|
+
`${VAULT_LESSON}). Agents read identity (hs source identity show); they never write it.\n` +
|
|
378
|
+
'\n' +
|
|
379
|
+
'If you are the human, type the command yourself:\n' +
|
|
380
|
+
` in your own terminal: ${HUMAN_FORMS.terminal}\n` +
|
|
381
|
+
` inside Claude Code: ${HUMAN_FORMS.claudeCode}\n` +
|
|
382
|
+
'\n' +
|
|
383
|
+
"(The `!` prefix runs outside the agent's tool calls but inherits its environment, which is\n" +
|
|
384
|
+
` the only reason ${HUMAN_FLAG} / ${HUMAN_ENV}=1 exist. An agent must never pass them.)\n`
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function writeAuthorization(repoRoot, record) {
|
|
389
|
+
const file = path.join(repoRoot, AUTH_FILE_REL);
|
|
390
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
391
|
+
fs.writeFileSync(file, JSON.stringify(record, null, 2) + '\n', 'utf8');
|
|
392
|
+
return file;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function removeAuthorization(repoRoot) {
|
|
396
|
+
const file = path.join(repoRoot, AUTH_FILE_REL);
|
|
397
|
+
try {
|
|
398
|
+
fs.rmSync(file, { force: true });
|
|
399
|
+
} catch {
|
|
400
|
+
/* absent */
|
|
401
|
+
}
|
|
402
|
+
return file;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* The command. Returns the exit code; prints through `stdout`/`stderr`.
|
|
407
|
+
*
|
|
408
|
+
* @param {string[]} argv - tokens AFTER `identity` (e.g. `['set','--email','a@b.c']`).
|
|
409
|
+
* @param {{ cwd?: string, env?: Record<string,string|undefined>, run?: Function,
|
|
410
|
+
* stdout?: (s: string) => void, stderr?: (s: string) => void, now?: () => Date }} [deps]
|
|
411
|
+
* @returns {number}
|
|
412
|
+
*/
|
|
413
|
+
export function runIdentityCommand(argv, deps = {}) {
|
|
414
|
+
const {
|
|
415
|
+
cwd = process.env.HS_INVOKED_CWD || process.cwd(),
|
|
416
|
+
env = process.env,
|
|
417
|
+
run = defaultRun,
|
|
418
|
+
stdout = (s) => process.stdout.write(s),
|
|
419
|
+
stderr = (s) => process.stderr.write(s),
|
|
420
|
+
now = () => new Date(),
|
|
421
|
+
} = deps;
|
|
422
|
+
|
|
423
|
+
const args = parseIdentityArgs(argv);
|
|
424
|
+
if (args.verb === 'help') {
|
|
425
|
+
stdout(USAGE);
|
|
426
|
+
return EXIT.OK;
|
|
427
|
+
}
|
|
428
|
+
if (args.errors.length) {
|
|
429
|
+
stderr(`hs source identity: ${args.errors.join('; ')}\n\n${USAGE}`);
|
|
430
|
+
return EXIT.USAGE;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (args.verb === 'show') {
|
|
434
|
+
const info = inspectIdentity({ run, cwd, env });
|
|
435
|
+
stdout(args.json ? JSON.stringify(info, null, 2) + '\n' : formatIdentityReport(info));
|
|
436
|
+
return EXIT.OK;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// ---- set / clear: human-only from here -----------------------------------
|
|
440
|
+
const markers = detectAgentMarkers(env);
|
|
441
|
+
if (markers.length && !humanVouched({ human: args.human, env })) {
|
|
442
|
+
stderr(refusalMessage(args.verb, markers));
|
|
443
|
+
return EXIT.REFUSED;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const repoRoot = resolveRepoRoot({ run, cwd, env });
|
|
447
|
+
if (!repoRoot && !(args.verb === 'set' && args.global)) {
|
|
448
|
+
stderr(`hs source identity ${args.verb}: not inside a git work tree (${cwd})\n`);
|
|
449
|
+
return EXIT.USAGE;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if (args.verb === 'set') {
|
|
453
|
+
const scope = args.global ? 'global' : 'local';
|
|
454
|
+
const scopeFlag = `--${scope}`;
|
|
455
|
+
const w = git(run, ['config', scopeFlag, 'user.email', args.email], { cwd, env });
|
|
456
|
+
if (w.status !== 0) {
|
|
457
|
+
stderr(`hs source identity set: git config ${scopeFlag} user.email failed: ${w.stderr.trim()}\n`);
|
|
458
|
+
return EXIT.USAGE;
|
|
459
|
+
}
|
|
460
|
+
if (args.name != null) {
|
|
461
|
+
const n = git(run, ['config', scopeFlag, 'user.name', args.name], { cwd, env });
|
|
462
|
+
if (n.status !== 0) {
|
|
463
|
+
stderr(`hs source identity set: git config ${scopeFlag} user.name failed: ${n.stderr.trim()}\n`);
|
|
464
|
+
return EXIT.USAGE;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
// Record what is actually there after the write (the name may pre-exist).
|
|
468
|
+
let recordedName = args.name;
|
|
469
|
+
if (scope === 'local') recordedName = readLocalIdentity({ run, cwd, env }).name;
|
|
470
|
+
const record = {
|
|
471
|
+
scope,
|
|
472
|
+
email: args.email,
|
|
473
|
+
name: recordedName ?? null,
|
|
474
|
+
authorizedAt: now().toISOString(),
|
|
475
|
+
authorizedBy: 'human',
|
|
476
|
+
via: 'hs source identity set',
|
|
477
|
+
};
|
|
478
|
+
let file = null;
|
|
479
|
+
if (repoRoot) file = writeAuthorization(repoRoot, record);
|
|
480
|
+
stdout(
|
|
481
|
+
`hs source identity set: wrote ${scope} user.email=${args.email}` +
|
|
482
|
+
(args.name != null ? ` user.name=${args.name}` : '') +
|
|
483
|
+
(file ? `\n recorded authorization → ${file}` : '') +
|
|
484
|
+
'\n\n',
|
|
485
|
+
);
|
|
486
|
+
stdout(formatIdentityReport(inspectIdentity({ run, cwd, env })));
|
|
487
|
+
return EXIT.OK;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// clear
|
|
491
|
+
const before = readLocalIdentity({ run, cwd, env });
|
|
492
|
+
const had = before.email != null || before.name != null;
|
|
493
|
+
if (had) {
|
|
494
|
+
const r = git(run, ['config', '--local', '--remove-section', 'user'], { cwd, env });
|
|
495
|
+
if (r.status !== 0) {
|
|
496
|
+
stderr(`hs source identity clear: git config --local --remove-section user failed: ${r.stderr.trim()}\n`);
|
|
497
|
+
return EXIT.USAGE;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
const file = removeAuthorization(repoRoot);
|
|
501
|
+
stdout(
|
|
502
|
+
(had
|
|
503
|
+
? `hs source identity clear: removed repo-local [user] override (email=${before.email ?? '-'}, name=${before.name ?? '-'})`
|
|
504
|
+
: 'hs source identity clear: no repo-local [user] override was present') +
|
|
505
|
+
`\n removed authorization file (if any) → ${file}\n\n`,
|
|
506
|
+
);
|
|
507
|
+
stdout(formatIdentityReport(inspectIdentity({ run, cwd, env })));
|
|
508
|
+
return EXIT.OK;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// ---- direct-invocation entry -----------------------------------------------
|
|
512
|
+
|
|
513
|
+
function invokedDirectly() {
|
|
514
|
+
try {
|
|
515
|
+
return path.resolve(fileURLToPath(import.meta.url)) === path.resolve(process.argv[1] ?? '');
|
|
516
|
+
} catch {
|
|
517
|
+
return false;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
if (invokedDirectly()) {
|
|
522
|
+
// bin/hs.mjs spawns `[script, 'identity', ...rest]`; a direct call may omit the verb token.
|
|
523
|
+
const tokens = process.argv.slice(2);
|
|
524
|
+
if (tokens[0] === 'identity') tokens.shift();
|
|
525
|
+
process.exitCode = runIdentityCommand(tokens);
|
|
526
|
+
}
|
|
@@ -26,41 +26,31 @@
|
|
|
26
26
|
* {@link formatSettlementResult}.
|
|
27
27
|
*/
|
|
28
28
|
import { spawnSync } from 'node:child_process';
|
|
29
|
-
import { readFileSync } from 'node:fs';
|
|
30
|
-
import { homedir } from 'node:os';
|
|
31
|
-
import { join } from 'node:path';
|
|
32
29
|
import path from 'node:path';
|
|
33
30
|
import { fileURLToPath } from 'node:url';
|
|
34
31
|
|
|
32
|
+
import { readBearerToken } from '../../vendor/heyspex-mcp/credentials.mjs';
|
|
35
33
|
import { resolveConsoleUrl } from './console-url-resolver.mjs';
|
|
36
34
|
|
|
37
35
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
38
36
|
export const REPO_ROOT = path.resolve(__dirname, '..', '..');
|
|
39
37
|
|
|
40
38
|
// ---- Bearer-header idiom -------------------------------------------------
|
|
41
|
-
//
|
|
42
|
-
// `spex-console/src/lib/cli/control-shared.ts
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
// gives for staying dependency-free.
|
|
39
|
+
// Same contract as `readPairedToken`/`bearerHeaderFor` in
|
|
40
|
+
// `spex-console/src/lib/cli/control-shared.ts`: the bearer comes from the ONE
|
|
41
|
+
// shared reader (`vendor/heyspex-mcp/credentials.mjs`,
|
|
42
|
+
// [[HS-HL0D-WW9R-TASK-share-one-bearer-reader-and-one-console]] — env
|
|
43
|
+
// `HEYSPEX_MCP_TOKEN`, then `mcp_token:` in `~/.heyspex/credentials.yaml`),
|
|
44
|
+
// and the loopback-skip rule stays (a local dev console never needed auth; a
|
|
45
|
+
// genuinely remote/paired console does —
|
|
46
|
+
// [[HS-PH7O-ILNC-DIAG-hs-check-report-cannot-authenticate]]).
|
|
50
47
|
|
|
51
48
|
/**
|
|
52
|
-
*
|
|
53
|
-
* if absent/unreadable.
|
|
49
|
+
* Paired Bearer token for this machine, or `undefined` if none resolves.
|
|
54
50
|
* @returns {string | undefined}
|
|
55
51
|
*/
|
|
56
52
|
export function readPairedToken() {
|
|
57
|
-
|
|
58
|
-
const raw = readFileSync(join(homedir(), '.heyspex', 'credentials.yaml'), 'utf-8');
|
|
59
|
-
const match = raw.match(/\b(hsmcp_[A-Za-z0-9_-]+|hscmp_[A-Za-z0-9_-]+)\b/);
|
|
60
|
-
return match?.[1];
|
|
61
|
-
} catch {
|
|
62
|
-
return undefined;
|
|
63
|
-
}
|
|
53
|
+
return readBearerToken() ?? undefined;
|
|
64
54
|
}
|
|
65
55
|
|
|
66
56
|
/**
|