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
|
@@ -29,10 +29,72 @@
|
|
|
29
29
|
import fs from 'node:fs';
|
|
30
30
|
import path from 'node:path';
|
|
31
31
|
import crypto from 'node:crypto';
|
|
32
|
+
import { fileURLToPath } from 'node:url';
|
|
32
33
|
|
|
33
34
|
import { yamlScalar } from './yaml-scalar.mjs';
|
|
35
|
+
import { getSettingEntry, SETTINGS_REGISTRY } from './settings-registry.mjs';
|
|
36
|
+
import { listTrustedFolders, readTrustStore, trustStorePath } from './folder-trust-store.mjs';
|
|
34
37
|
|
|
35
38
|
export const CONFIG_REL = path.join('.heyspex', 'config.yaml');
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The seed template a vault's `.heyspex/config.yaml` is created from when it is
|
|
42
|
+
* missing ([[HS-4C76-9W86-DIAG-heyspex-config-yaml-has-no-single-home]] fix 3).
|
|
43
|
+
* Ships in the npm package next to this module (`package.json` `files`), so a
|
|
44
|
+
* published install seeds the same defaults a source checkout does. `hs init`
|
|
45
|
+
* seeds from the same file; the settings core seeds on the FIRST WRITE only —
|
|
46
|
+
* reads (`listConfigLeaves` / `getConfigLeaf`) never create files.
|
|
47
|
+
*/
|
|
48
|
+
export const CONFIG_SEED_PATH = path.resolve(
|
|
49
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
50
|
+
'config-seed.yaml',
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
/** The seed template's text. Throws only if the package itself is broken. */
|
|
54
|
+
export function readConfigSeed() {
|
|
55
|
+
return fs.readFileSync(CONFIG_SEED_PATH, 'utf8');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The one non-scalar, non-`.heyspex/config.yaml` COMPUTED key the settings capability exposes
|
|
60
|
+
* ([[HS-25AJ-8CMW-TASK-folder-trust-revoke-through-the]], agent-settings-protocol.md): the list
|
|
61
|
+
* of `hs chat`/`spexcode` folder-trust decisions, sourced live from the machine-local
|
|
62
|
+
* `~/.heyspex/trusted-folders.json` store (`folder-trust-store.mjs` — the SAME read primitive
|
|
63
|
+
* `spex-console/src/lib/cli/folder-trust.ts` re-exports for `hs trust list`). READ-ONLY: writes
|
|
64
|
+
* are refused in both `requestSettingsWrite` (MCP two-step) and `setConfigLeaf` (the CLI's
|
|
65
|
+
* direct-write path) — see each function below. Never reads or writes config.yaml for this key.
|
|
66
|
+
*
|
|
67
|
+
* Why the core hosts a computed key rather than distorting itself around one: this module's
|
|
68
|
+
* entire model — `listConfigLeaves`/`getConfigLeaf`/`setConfigLeaf` — is a scalar-leaf scan and
|
|
69
|
+
* surgical single-line rewrite of ONE git-tracked YAML file. `trusted_folders` is a LIST of
|
|
70
|
+
* records (path/trusted/decidedAt) sourced from a DIFFERENT, machine-local, never-git-tracked
|
|
71
|
+
* JSON file — hosting a `…trusted_folders.revoke = <path>` WRITE through the two-step confirm
|
|
72
|
+
* store would mean that store's pending-write record and audit row both lie about writing to
|
|
73
|
+
* config.yaml (they don't — a revoke deletes an entry from trusted-folders.json). So the read
|
|
74
|
+
* is computed here (small, honest addition), but the write stays CLI-only (`hs trust revoke
|
|
75
|
+
* <path>`), documented as the task's AC2 explicitly allows.
|
|
76
|
+
*/
|
|
77
|
+
export const TRUSTED_FOLDERS_KEY = 'agent_behaviors.spexcode.trusted_folders';
|
|
78
|
+
|
|
79
|
+
function isTrustedFoldersKey(key) {
|
|
80
|
+
return key === TRUSTED_FOLDERS_KEY || key.startsWith(TRUSTED_FOLDERS_KEY + '.');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Shared refusal for both write entry points (`setConfigLeaf`'s direct/CLI path and
|
|
84
|
+
* `requestSettingsWrite`'s MCP two-step path) — `null` when `key` isn't governed by this
|
|
85
|
+
* computed key, so callers can `return refusal ?? ...normal flow`. */
|
|
86
|
+
function refuseTrustedFoldersWrite(key) {
|
|
87
|
+
if (!isTrustedFoldersKey(key)) return null;
|
|
88
|
+
return {
|
|
89
|
+
ok: false,
|
|
90
|
+
error: 'read_only_computed_key',
|
|
91
|
+
message:
|
|
92
|
+
`"${key}" is a read-only computed key — it is sourced live from the machine-local hs ` +
|
|
93
|
+
`trust store (${trustStorePath()}), never from .heyspex/config.yaml, and settings writes ` +
|
|
94
|
+
'are not supported here. Run "hs trust revoke <path>" instead — it clears one folder; ' +
|
|
95
|
+
'omit <path> to clear the current folder.',
|
|
96
|
+
};
|
|
97
|
+
}
|
|
36
98
|
export const SETTINGS_AUDIT_REL = path.join('.heyspex', 'settings-history.jsonl');
|
|
37
99
|
export const PENDING_WRITE_REL = path.join('.heyspex', 'settings-write-pending.json');
|
|
38
100
|
|
|
@@ -60,6 +122,82 @@ export function validateSettingKey(key) {
|
|
|
60
122
|
return { ok: true };
|
|
61
123
|
}
|
|
62
124
|
|
|
125
|
+
// ── registry-backed write validation (HS-WH06, HS-JXT9) ─────────────────────
|
|
126
|
+
//
|
|
127
|
+
// The registry (`vendor/heyspex-mcp/settings-registry.mjs`) currently declares
|
|
128
|
+
// only the `close_out.*` namespace. Validation below applies ONLY to a key whose
|
|
129
|
+
// top-level segment the registry governs — a key in an ungoverned namespace
|
|
130
|
+
// (`mvp_mode.*`, `agent_behaviors.*`, …) keeps its pre-registry freeform write
|
|
131
|
+
// behavior unchanged. This keeps the epic's scope to "close-out settings" without
|
|
132
|
+
// silently locking down every other setting the registry has not been taught yet
|
|
133
|
+
// ([[HS-KW38-15XI-EPIC-settings-registry-core]] scope).
|
|
134
|
+
|
|
135
|
+
/** Top-level key segments the registry declares at least one entry for. */
|
|
136
|
+
const REGISTRY_GOVERNED_PREFIXES = new Set(
|
|
137
|
+
SETTINGS_REGISTRY.map((entry) => entry.key.split('.')[0]),
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
function isRegistryGoverned(key) {
|
|
141
|
+
return REGISTRY_GOVERNED_PREFIXES.has(key.split('.')[0]);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Human-readable description of what `entry` accepts, for a refusal message. */
|
|
145
|
+
function describeExpectedSettingValue(entry) {
|
|
146
|
+
if (entry.type === 'enum') return `one of: ${entry.values.join(', ')}`;
|
|
147
|
+
if (entry.type === 'boolean') return 'a boolean (true or false)';
|
|
148
|
+
return 'a string';
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** True when `value` matches the type/allowed-values `entry` declares. */
|
|
152
|
+
function settingValueMatchesEntry(entry, value) {
|
|
153
|
+
if (entry.type === 'boolean') return typeof value === 'boolean';
|
|
154
|
+
if (entry.type === 'enum') return typeof value === 'string' && entry.values.includes(value);
|
|
155
|
+
if (entry.type === 'string') return typeof value === 'string';
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Validate a proposed write against the settings registry, ahead of any file or
|
|
161
|
+
* audit-log write. Only applies within a registry-governed namespace (see above);
|
|
162
|
+
* a key outside it always passes through with `{ ok: true }`.
|
|
163
|
+
*
|
|
164
|
+
* Refuses, in order:
|
|
165
|
+
* 1. a key the registry does not declare (`unknown_setting`);
|
|
166
|
+
* 2. any change to an entry marked `enforced`, regardless of who the caller is —
|
|
167
|
+
* there is no admin bypass (`enforced_setting`);
|
|
168
|
+
* 3. a value whose type doesn't match the entry, or that falls outside the
|
|
169
|
+
* entry's declared `values` list (`invalid_setting_value`) — names the
|
|
170
|
+
* setting, the value given, and what was expected.
|
|
171
|
+
*
|
|
172
|
+
* Returns `{ ok: true }` or `{ ok: false, error, message }`. Never touches disk.
|
|
173
|
+
*/
|
|
174
|
+
export function validateSettingWrite(key, value) {
|
|
175
|
+
if (typeof key !== 'string' || !isRegistryGoverned(key)) return { ok: true };
|
|
176
|
+
const entry = getSettingEntry(key);
|
|
177
|
+
if (!entry) {
|
|
178
|
+
return {
|
|
179
|
+
ok: false,
|
|
180
|
+
error: 'unknown_setting',
|
|
181
|
+
message: `"${key}" is not a setting declared in the settings registry — refusing to write an unknown key.`,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
if (entry.enforced) {
|
|
185
|
+
return {
|
|
186
|
+
ok: false,
|
|
187
|
+
error: 'enforced_setting',
|
|
188
|
+
message: `"${key}" is a hard rule and cannot be changed by anyone, including an administrator (${entry.description})`,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
if (!settingValueMatchesEntry(entry, value)) {
|
|
192
|
+
return {
|
|
193
|
+
ok: false,
|
|
194
|
+
error: 'invalid_setting_value',
|
|
195
|
+
message: `"${key}" was given ${JSON.stringify(value)}, but expected ${describeExpectedSettingValue(entry)}.`,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
return { ok: true };
|
|
199
|
+
}
|
|
200
|
+
|
|
63
201
|
/**
|
|
64
202
|
* Parse a raw YAML scalar string into a JS value (boolean/number/null/string).
|
|
65
203
|
* Quoted strings are unquoted; anything else stays a string.
|
|
@@ -92,6 +230,72 @@ function readRaw(repoRoot, options = {}) {
|
|
|
92
230
|
}
|
|
93
231
|
}
|
|
94
232
|
|
|
233
|
+
function isExistingDirectory(dir) {
|
|
234
|
+
try {
|
|
235
|
+
return typeof dir === 'string' && dir.length > 0 && fs.statSync(dir).isDirectory();
|
|
236
|
+
} catch {
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Make sure `<repoRoot>/.heyspex/` exists ahead of a write into it. Only ever
|
|
243
|
+
* creates the `.heyspex/` directory INSIDE an existing vault root — never the
|
|
244
|
+
* vault root itself (a typo'd `HS_VAULT_ROOT` must not grow a `.heyspex/` tree
|
|
245
|
+
* in a path that does not exist). Returns false when `repoRoot` is not an
|
|
246
|
+
* existing directory or the mkdir fails.
|
|
247
|
+
*/
|
|
248
|
+
function ensureHeyspexDir(repoRoot) {
|
|
249
|
+
if (!isExistingDirectory(repoRoot)) return false;
|
|
250
|
+
try {
|
|
251
|
+
fs.mkdirSync(path.join(repoRoot, '.heyspex'), { recursive: true });
|
|
252
|
+
return true;
|
|
253
|
+
} catch {
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Seed a MISSING config file from the template ([[HS-4C76-9W86-DIAG]] fix 3).
|
|
260
|
+
* Returns `{ ok: true, raw }` with the seeded text, or `{ ok: false, error,
|
|
261
|
+
* message }` when seeding is not allowed / fails. Refuses (keeps the historic
|
|
262
|
+
* `config_unreadable` contract) when:
|
|
263
|
+
* - `repoRoot` is not an existing directory — never mkdir a vault into a
|
|
264
|
+
* path that does not exist;
|
|
265
|
+
* - the file EXISTS but could not be read (permissions, a directory at that
|
|
266
|
+
* path…) — seeding over it would silently destroy the user's settings.
|
|
267
|
+
*/
|
|
268
|
+
function seedMissingConfig(repoRoot, file) {
|
|
269
|
+
if (fs.existsSync(file)) {
|
|
270
|
+
return { ok: false, error: 'config_unreadable', message: `${file} exists but is unreadable` };
|
|
271
|
+
}
|
|
272
|
+
if (!isExistingDirectory(repoRoot)) {
|
|
273
|
+
return {
|
|
274
|
+
ok: false,
|
|
275
|
+
error: 'config_unreadable',
|
|
276
|
+
message: `${file} not found and its vault root ${repoRoot} does not exist — refusing to create a vault there. Run \`hs init\` in the directory that should become the vault.`,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
let seed;
|
|
280
|
+
try {
|
|
281
|
+
seed = readConfigSeed();
|
|
282
|
+
} catch (err) {
|
|
283
|
+
return { ok: false, error: 'seed_unreadable', message: `config seed template ${CONFIG_SEED_PATH} could not be read: ${String(err?.message ?? err)}` };
|
|
284
|
+
}
|
|
285
|
+
try {
|
|
286
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
287
|
+
// 'wx' — create only; if a concurrent writer beat us to it, read theirs.
|
|
288
|
+
fs.writeFileSync(file, seed, { encoding: 'utf8', flag: 'wx' });
|
|
289
|
+
} catch (err) {
|
|
290
|
+
if (err?.code === 'EEXIST') {
|
|
291
|
+
const raced = readRaw(repoRoot, { configPath: file });
|
|
292
|
+
if (raced !== null) return { ok: true, raw: raced, seeded: false };
|
|
293
|
+
}
|
|
294
|
+
return { ok: false, error: 'write_failed', message: `could not seed ${file}: ${String(err?.message ?? err)}` };
|
|
295
|
+
}
|
|
296
|
+
return { ok: true, raw: seed, seeded: true };
|
|
297
|
+
}
|
|
298
|
+
|
|
95
299
|
/**
|
|
96
300
|
* Walk config.yaml lines maintaining an indent-scoped key path (the same scan the
|
|
97
301
|
* hook loader uses). Calls `visit({ pathHere, key, indent, rawValue, lineIdx })` for
|
|
@@ -177,6 +381,50 @@ export function getConfigLeaf(repoRoot, key, options = {}) {
|
|
|
177
381
|
return { key, found: true, raw: hit, value: parseScalar(hit) };
|
|
178
382
|
}
|
|
179
383
|
|
|
384
|
+
/**
|
|
385
|
+
* Resolve the effective value of one setting through the registry-aware read
|
|
386
|
+
* chain ([[HS-FCF5-15XI-RES-user-editable-close-out-settings-with]] §"One-time
|
|
387
|
+
* bypass resolve order"):
|
|
388
|
+
* 1. (future) a one-time command-line flag override — NOT implemented here; a
|
|
389
|
+
* later epic (the `hs close` one-time flag layer) adds it above the file
|
|
390
|
+
* value. This function's `source` field leaves room for a `'flag'` value
|
|
391
|
+
* without changing its shape.
|
|
392
|
+
* 2. the file's value in `.heyspex/config.yaml`, when the key is present.
|
|
393
|
+
* 3. the registry's shipped default (`vendor/heyspex-mcp/settings-registry.mjs`),
|
|
394
|
+
* when the key is a declared registry entry and the file has no value.
|
|
395
|
+
* A key that is neither present in the file nor declared in the registry resolves
|
|
396
|
+
* to `source: 'absent'` with a null value — unchanged from the pre-registry
|
|
397
|
+
* behavior of `getConfigLeaf`.
|
|
398
|
+
*
|
|
399
|
+
* `found` keeps `getConfigLeaf`'s existing meaning (present in the file) so
|
|
400
|
+
* existing callers of that lower-level function are unaffected; this function is
|
|
401
|
+
* the one the CLI/MCP/console faces are meant to resolve reads through instead of
|
|
402
|
+
* each rolling their own fallback (a later task in
|
|
403
|
+
* [[HS-KW38-15XI-EPIC-settings-registry-core]] wires them up).
|
|
404
|
+
*
|
|
405
|
+
* @returns {{ key: string, value: *, found: boolean, source: 'file'|'registry_default'|'absent' }}
|
|
406
|
+
*/
|
|
407
|
+
export function getEffectiveSetting(repoRoot, key, options = {}) {
|
|
408
|
+
// Computed key — answered from the machine-local trust store, NEVER from config.yaml (see
|
|
409
|
+
// TRUSTED_FOLDERS_KEY's doc comment above). Only the exact key resolves; a deeper child (e.g.
|
|
410
|
+
// "…trusted_folders.revoke") falls through to the normal absent handling below.
|
|
411
|
+
// `options.trustStorePath` is a test-only override (mirrors `options.configPath` elsewhere in
|
|
412
|
+
// this module) — every real caller omits it and reads the real `~/.heyspex/trusted-folders.json`.
|
|
413
|
+
if (key === TRUSTED_FOLDERS_KEY) {
|
|
414
|
+
const value = listTrustedFolders(readTrustStore(options.trustStorePath));
|
|
415
|
+
return { key, value, found: true, source: 'computed' };
|
|
416
|
+
}
|
|
417
|
+
const fileLeaf = getConfigLeaf(repoRoot, key, options);
|
|
418
|
+
if (fileLeaf.found) {
|
|
419
|
+
return { key, value: fileLeaf.value, found: true, source: 'file' };
|
|
420
|
+
}
|
|
421
|
+
const entry = getSettingEntry(key);
|
|
422
|
+
if (entry) {
|
|
423
|
+
return { key, value: entry.default, found: false, source: 'registry_default' };
|
|
424
|
+
}
|
|
425
|
+
return { key, value: null, found: false, source: 'absent' };
|
|
426
|
+
}
|
|
427
|
+
|
|
180
428
|
/** Append one row to the settings audit log. Fail-safe: never throws. */
|
|
181
429
|
export function appendSettingsAudit(repoRoot, row) {
|
|
182
430
|
try {
|
|
@@ -200,21 +448,41 @@ export function appendSettingsAudit(repoRoot, row) {
|
|
|
200
448
|
* indent, after the last line of the deepest existing ancestor's block. Preserves
|
|
201
449
|
* every other line (comments included) byte-for-byte.
|
|
202
450
|
*
|
|
203
|
-
* Returns { ok, key, previous, value, created } or { ok:false, error }.
|
|
451
|
+
* Returns { ok, key, previous, value, created, audited } or { ok:false, error }.
|
|
204
452
|
* Appends a `settings.write` audit row on success (meta: { actor, face }).
|
|
453
|
+
* `audited` is the MEASURED result of that append — false means the config write
|
|
454
|
+
* landed but the audit trail did not, which callers must report as a partial.
|
|
455
|
+
*
|
|
456
|
+
* SELF-HEALS a missing file ([[HS-4C76-9W86-DIAG-heyspex-config-yaml-has-no-single-home]]
|
|
457
|
+
* fix 3): when `.heyspex/config.yaml` does not exist under an EXISTING vault root,
|
|
458
|
+
* it is created from `config-seed.yaml` first and the write lands on that seeded
|
|
459
|
+
* text; the result and the audit row then carry `seeded: true`. A vault root that
|
|
460
|
+
* does not exist, or a file that exists but is unreadable, still refuses with
|
|
461
|
+
* `config_unreadable` — seeding never creates a vault and never overwrites.
|
|
205
462
|
*/
|
|
206
463
|
export function setConfigLeaf(repoRoot, key, value, meta = {}, options = {}) {
|
|
207
464
|
const check = validateSettingKey(key);
|
|
208
465
|
if (!check.ok) return { ok: false, error: 'invalid_key', message: check.reason };
|
|
466
|
+
const trustedFoldersRefusal = refuseTrustedFoldersWrite(key);
|
|
467
|
+
if (trustedFoldersRefusal) return trustedFoldersRefusal;
|
|
468
|
+
const registryCheck = validateSettingWrite(key, value);
|
|
469
|
+
if (!registryCheck.ok) return registryCheck;
|
|
209
470
|
if (value !== null && !['string', 'number', 'boolean'].includes(typeof value)) {
|
|
210
471
|
return { ok: false, error: 'invalid_value', message: 'value must be a scalar (string, number, boolean, or null)' };
|
|
211
472
|
}
|
|
212
473
|
|
|
213
474
|
const file = configPath(repoRoot, options);
|
|
214
|
-
|
|
475
|
+
let raw = readRaw(repoRoot, options);
|
|
476
|
+
let seeded = false;
|
|
215
477
|
if (raw === null) {
|
|
216
|
-
|
|
478
|
+
const seedResult = seedMissingConfig(repoRoot, file);
|
|
479
|
+
if (!seedResult.ok) return seedResult;
|
|
480
|
+
raw = seedResult.raw;
|
|
481
|
+
seeded = seedResult.seeded === true;
|
|
217
482
|
}
|
|
483
|
+
// Only present on the row/result when a seed actually happened — an existing
|
|
484
|
+
// file's write result and audit row keep their exact pre-existing shape.
|
|
485
|
+
const seededFields = seeded ? { seeded: true } : {};
|
|
218
486
|
|
|
219
487
|
const eol = raw.includes('\r\n') ? '\r\n' : '\n';
|
|
220
488
|
const lines = raw.split(/\r?\n/);
|
|
@@ -277,14 +545,19 @@ export function setConfigLeaf(repoRoot, key, value, meta = {}, options = {}) {
|
|
|
277
545
|
} catch (err) {
|
|
278
546
|
return { ok: false, error: 'write_failed', message: String(err?.message ?? err) };
|
|
279
547
|
}
|
|
280
|
-
appendSettingsAudit
|
|
548
|
+
// `appendSettingsAudit` is fail-safe and returns FALSE on an append that did
|
|
549
|
+
// not land; that boolean used to be discarded, so every face reported the
|
|
550
|
+
// write as "applied and audited" over a missing audit row
|
|
551
|
+
// ([[HS-IOJW-5YDH-DIAG]] sweep: report the result, not the request).
|
|
552
|
+
const audited = appendSettingsAudit(repoRoot, {
|
|
281
553
|
key,
|
|
282
554
|
previous,
|
|
283
555
|
value,
|
|
284
556
|
actor: meta.actor ?? 'unknown',
|
|
285
557
|
face: meta.face ?? 'unknown',
|
|
558
|
+
...seededFields,
|
|
286
559
|
});
|
|
287
|
-
return { ok: true, key, previous, value, created: false };
|
|
560
|
+
return { ok: true, key, previous, value, created: false, audited, ...seededFields };
|
|
288
561
|
}
|
|
289
562
|
|
|
290
563
|
// Insert. Build the missing chain below the deepest existing ancestor.
|
|
@@ -318,14 +591,15 @@ export function setConfigLeaf(repoRoot, key, value, meta = {}, options = {}) {
|
|
|
318
591
|
} catch (err) {
|
|
319
592
|
return { ok: false, error: 'write_failed', message: String(err?.message ?? err) };
|
|
320
593
|
}
|
|
321
|
-
appendSettingsAudit(repoRoot, {
|
|
594
|
+
const audited = appendSettingsAudit(repoRoot, {
|
|
322
595
|
key,
|
|
323
596
|
previous: null,
|
|
324
597
|
value,
|
|
325
598
|
actor: meta.actor ?? 'unknown',
|
|
326
599
|
face: meta.face ?? 'unknown',
|
|
600
|
+
...seededFields,
|
|
327
601
|
});
|
|
328
|
-
return { ok: true, key, previous: null, value, created: true };
|
|
602
|
+
return { ok: true, key, previous: null, value, created: true, audited, ...seededFields };
|
|
329
603
|
}
|
|
330
604
|
|
|
331
605
|
// ── two-step confirm store (the MCP write-permission handshake) ─────────────
|
|
@@ -357,10 +631,19 @@ export function peekPendingWrite(repoRoot) {
|
|
|
357
631
|
export function requestSettingsWrite(repoRoot, { key, value, actor, face } = {}, options = {}) {
|
|
358
632
|
const check = validateSettingKey(key);
|
|
359
633
|
if (!check.ok) return { ok: false, error: 'invalid_key', message: check.reason };
|
|
634
|
+
const trustedFoldersRefusal = refuseTrustedFoldersWrite(key);
|
|
635
|
+
if (trustedFoldersRefusal) return trustedFoldersRefusal;
|
|
636
|
+
const registryCheck = validateSettingWrite(key, value);
|
|
637
|
+
if (!registryCheck.ok) return registryCheck;
|
|
360
638
|
if (value !== null && !['string', 'number', 'boolean'].includes(typeof value)) {
|
|
361
639
|
return { ok: false, error: 'invalid_value', message: 'value must be a scalar (string, number, boolean, or null)' };
|
|
362
640
|
}
|
|
363
641
|
const current = getConfigLeaf(repoRoot, key, options);
|
|
642
|
+
// A fresh vault may have no `.heyspex/` directory yet (HS-4C76): the pending
|
|
643
|
+
// record and the request audit row both live there, so create the directory
|
|
644
|
+
// now — inside an EXISTING vault root only (see ensureHeyspexDir). The config
|
|
645
|
+
// file itself is NOT created here; that happens on confirm, via setConfigLeaf.
|
|
646
|
+
ensureHeyspexDir(repoRoot);
|
|
364
647
|
const token = crypto.randomBytes(6).toString('hex');
|
|
365
648
|
const record = {
|
|
366
649
|
token,
|