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
|
@@ -114,6 +114,26 @@ export function findReviewDocForSpec(vaultRoot, parentSpecId) {
|
|
|
114
114
|
*/
|
|
115
115
|
export const FILE_KEY_RE = /^(?=[0-9]*[A-Z])[A-Z0-9]{4,}$/;
|
|
116
116
|
|
|
117
|
+
/**
|
|
118
|
+
* `hs next CJV2` → `HS-CJV2` (owner ruling 2026-09-01): typing the bare file
|
|
119
|
+
* key is the natural gesture — the `HS-` prefix carries no information the
|
|
120
|
+
* CLI needs, so requiring it is friction. Accepts the long display form too
|
|
121
|
+
* (`CJV2-90JL`, `CJV2-90JL-EPIC`) by normalizing on the FIRST segment.
|
|
122
|
+
*
|
|
123
|
+
* Returns the `HS-`-prefixed uppercase id, or `null` when the token is not a
|
|
124
|
+
* spec-id shape at all (so the caller can keep its own "unexpected argument"
|
|
125
|
+
* error). A token that already starts with `HS-` comes back uppercased and
|
|
126
|
+
* otherwise untouched. Mirror: spex-console/src/lib/vault-id.ts
|
|
127
|
+
* `normalizeBareSpecIdToken`.
|
|
128
|
+
*/
|
|
129
|
+
export function normalizeBareSpecIdToken(input) {
|
|
130
|
+
const token = String(input ?? '').trim().toUpperCase();
|
|
131
|
+
if (!token) return null;
|
|
132
|
+
if (token.startsWith('HS-')) return token;
|
|
133
|
+
const head = token.split('-')[0];
|
|
134
|
+
return FILE_KEY_RE.test(head) ? `HS-${token}` : null;
|
|
135
|
+
}
|
|
136
|
+
|
|
117
137
|
/**
|
|
118
138
|
* File key carried by a spec id/basename in EITHER ordering, or null for
|
|
119
139
|
* legacy no-file-key names. The file key is the first title-tail token when it
|
|
@@ -202,14 +222,36 @@ export function allocateFileKey(occupied, options = {}) {
|
|
|
202
222
|
}
|
|
203
223
|
|
|
204
224
|
/**
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
*
|
|
225
|
+
* Mint a FILE KEY against an ALREADY-RESOLVED occupancy set. `randomUnit` is
|
|
226
|
+
* injectable for tests. `occupied` must be the UNION of file keys AND family
|
|
227
|
+
* keys: era-3 ids are bare file keys, so a file key that equals any family key
|
|
228
|
+
* makes a bare `HS-XXXX` citation ambiguous between one file and a whole
|
|
229
|
+
* family ([[HS-SJOT-AEP1-DIAG]]).
|
|
230
|
+
*
|
|
231
|
+
* TAKES THE OCCUPANCY, NOT A VAULT ROOT ([[HS-FJWL]]). This used to be
|
|
232
|
+
* `mintFileKey(vaultRoot)` and resolved occupancy itself, by walking
|
|
233
|
+
* `specs/**` — correct on the disk tier, blind on the cloud tier, where a
|
|
234
|
+
* filed spec has a Neon row and no local `.md` and the stale local clone still
|
|
235
|
+
* answers with a large, plausible-looking set. File keys are the unique
|
|
236
|
+
* per-file identity under Naming V3, so a blind scan there re-issues a live
|
|
237
|
+
* id. Callers now resolve occupancy for THEIR tier —
|
|
238
|
+
* `collectOccupiedKeysEitherKindViaTier` (occupied-keys.mjs) on a tier-aware
|
|
239
|
+
* path, `collectOccupiedKeysEitherKind` below on a path that is disk-only by
|
|
240
|
+
* construction — and hand the set in.
|
|
241
|
+
*
|
|
242
|
+
* The `Set` check is the migration's fail-loud: a call site left passing a
|
|
243
|
+
* vaultRoot string would otherwise mint against `collectOccupiedKeysEitherKind`
|
|
244
|
+
* of nothing, i.e. against an empty set, i.e. duplicates. Refuse instead.
|
|
210
245
|
*/
|
|
211
|
-
export function mintFileKey(
|
|
212
|
-
|
|
246
|
+
export function mintFileKey(occupied, options = {}) {
|
|
247
|
+
if (!(occupied instanceof Set)) {
|
|
248
|
+
throw new TypeError(
|
|
249
|
+
'mintFileKey(occupied, options) takes a resolved occupancy Set, not a vaultRoot ' +
|
|
250
|
+
'([[HS-FJWL]]). Use collectOccupiedKeysEitherKindViaTier(vaultRoot, { tier, consoleUrl }) ' +
|
|
251
|
+
'on a tier-aware path, or collectOccupiedKeysEitherKind(vaultRoot) on a disk-only one.',
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
return allocateFileKey(occupied, options);
|
|
213
255
|
}
|
|
214
256
|
|
|
215
257
|
/**
|
|
@@ -267,6 +309,20 @@ export function collectOccupiedKeysEitherKind(vaultRoot) {
|
|
|
267
309
|
* machinery as {@link allocateFileKey} (4 chars, at least one letter),
|
|
268
310
|
* checked against {@link collectOccupiedFamilyKeysFromVault} so a fresh mint
|
|
269
311
|
* doesn't accidentally collide with an existing unrelated family.
|
|
312
|
+
*
|
|
313
|
+
* DELIBERATELY STILL DISK-ONLY — do not "fix" this into a network call
|
|
314
|
+
* ([[HS-FJWL]]). {@link mintFileKey} was made tier-aware because a file key is
|
|
315
|
+
* the unique per-file identity: re-issuing one is a duplicate id. A FAMILY key
|
|
316
|
+
* is the opposite — any number of specs may legally share one (V3 shared-
|
|
317
|
+
* surname model, see {@link collectOccupiedFamilyKeysFromVault}), so this
|
|
318
|
+
* occupancy scan is a "pick something fresh" heuristic, NOT a uniqueness gate.
|
|
319
|
+
* Missing a cloud-only family key costs at most a family name reused by an
|
|
320
|
+
* unrelated spec, which the model already permits; paying a paged console walk
|
|
321
|
+
* per family mint would buy nothing. The cross-namespace half
|
|
322
|
+
* ([[HS-SJOT-AEP1-DIAG]] — a family key must not equal a FILE key) is carried
|
|
323
|
+
* by the union below on the disk tier and, in cloud reliance, by the console's
|
|
324
|
+
* own `createSpec` mint, which resolves occupancy through `loadAll()` and is
|
|
325
|
+
* tier-correct by construction.
|
|
270
326
|
*/
|
|
271
327
|
export function mintFamilyKey(vaultRoot, options = {}) {
|
|
272
328
|
return allocateFileKey(collectOccupiedKeysEitherKind(vaultRoot), options);
|
|
@@ -553,21 +609,41 @@ export function isBareFamilyToken(input) {
|
|
|
553
609
|
}
|
|
554
610
|
|
|
555
611
|
/**
|
|
556
|
-
* Guard for mutating MCP/CLI ops: reject a `spec_id` that is a bare
|
|
557
|
-
*
|
|
558
|
-
* `{ error, message }` to surface to the caller, or `null` when the id
|
|
559
|
-
* unambiguous enough to act on. Read-only resolvers do NOT call this — they
|
|
560
|
-
* a family token out to a set or just miss.
|
|
612
|
+
* Guard for mutating MCP/CLI ops: reject a `spec_id` that is a bare `HS-XXXX`
|
|
613
|
+
* token the caller has not proven names exactly one spec. Returns an error
|
|
614
|
+
* object `{ error, message }` to surface to the caller, or `null` when the id
|
|
615
|
+
* is unambiguous enough to act on. Read-only resolvers do NOT call this — they
|
|
616
|
+
* fan a family token out to a set or just miss.
|
|
617
|
+
*
|
|
618
|
+
* WORDING ([[HS-05ZW-XHR4-DIAG-the-capture-dropped-it-warning-blamed]] follow-on):
|
|
619
|
+
* {@link isBareFamilyToken} is SHAPE-ONLY — `HS-05ZW` is exactly as likely to be
|
|
620
|
+
* an era-3 FILE key (the stored `id:` of every newly minted spec) as a family
|
|
621
|
+
* key, and nothing here can tell them apart without a lookup. So the default
|
|
622
|
+
* message states only what was actually established: the token could not be
|
|
623
|
+
* resolved to one spec from this machine, and it may be either. A caller that
|
|
624
|
+
* HAS verified the family reading (e.g. `classifyBareSpecId` found the token
|
|
625
|
+
* among the vault's occupied family keys) passes `confirmedFamily: true` to get
|
|
626
|
+
* the stronger, now-earned wording. The actionable hint is identical either way.
|
|
627
|
+
*
|
|
628
|
+
* @param {string|null|undefined} input
|
|
629
|
+
* @param {{ confirmedFamily?: boolean, detail?: string|null }} [opts]
|
|
561
630
|
*/
|
|
562
|
-
export function requireResolvableSpecId(input) {
|
|
631
|
+
export function requireResolvableSpecId(input, opts = {}) {
|
|
563
632
|
if (isBareFamilyToken(input)) {
|
|
564
633
|
const base = normalizeIdInput(input);
|
|
634
|
+
const hint =
|
|
635
|
+
`Pass a full basename that includes the type label ` +
|
|
636
|
+
`(e.g. ${base}-TASK-<slug> or HS-TASK-${base.replace(/^HS-/, '')}-<slug>; ` +
|
|
637
|
+
`the era-3 form is ${base}-<FAMILY>-<TYPE>-<slug>).`;
|
|
638
|
+
const claim = opts.confirmedFamily
|
|
639
|
+
? `"${base}" is a bare family key, which names a whole shared-suffix family ` +
|
|
640
|
+
`rather than one spec.`
|
|
641
|
+
: `"${base}" is a bare token that could not be resolved to exactly one spec ` +
|
|
642
|
+
`from this machine — it may be the file key of a spec with no local copy, ` +
|
|
643
|
+
`or a family key naming a whole shared-suffix family.`;
|
|
565
644
|
return {
|
|
566
645
|
error: 'AMBIGUOUS_SPEC_ID',
|
|
567
|
-
message:
|
|
568
|
-
`"${base}" is a bare family key, which names a whole shared-suffix family ` +
|
|
569
|
-
`rather than one spec. Pass a full basename that includes the type label ` +
|
|
570
|
-
`(e.g. ${base}-TASK-<slug> or HS-TASK-${base.replace(/^HS-/, '')}-<slug>).`,
|
|
646
|
+
message: opts.detail ? `${claim} ${opts.detail} ${hint}` : `${claim} ${hint}`,
|
|
571
647
|
};
|
|
572
648
|
}
|
|
573
649
|
return null;
|
|
@@ -42,8 +42,27 @@ import {
|
|
|
42
42
|
mkdirSync,
|
|
43
43
|
unlinkSync,
|
|
44
44
|
} from 'node:fs';
|
|
45
|
-
import { join } from 'node:path';
|
|
46
|
-
import {
|
|
45
|
+
import { join, isAbsolute, resolve, sep } from 'node:path';
|
|
46
|
+
import {
|
|
47
|
+
deriveSlug,
|
|
48
|
+
fetchSpecRecordViaConsole,
|
|
49
|
+
resolveWriteModeFromDeps,
|
|
50
|
+
splitRawSpecForConsole,
|
|
51
|
+
cloudWriteFailureResult,
|
|
52
|
+
} from './promotion-helpers.mjs';
|
|
53
|
+
// Cloud-mode vault WRITES + the cloud-tier read selector
|
|
54
|
+
// ([[HS-BYU4-CWPX-TASK-promotion-writes-go-through-the-console]], HS-KR16).
|
|
55
|
+
// Split has the same create-first, link-second shape as the other promotion
|
|
56
|
+
// flows: each child's "Split-from" callout names its SIBLINGS, and the
|
|
57
|
+
// original's `split_into:` names every child — basenames the console only
|
|
58
|
+
// mints when the POST lands.
|
|
59
|
+
import {
|
|
60
|
+
createSpecViaConsole,
|
|
61
|
+
updateSpecViaConsole,
|
|
62
|
+
stripServerMintedFields,
|
|
63
|
+
} from './cloud-vault-writes.mjs';
|
|
64
|
+
import { readViaSelector } from './read-path.mjs';
|
|
65
|
+
|
|
47
66
|
import { yamlScalar } from './yaml-scalar.mjs';
|
|
48
67
|
import {
|
|
49
68
|
allocateFileKey,
|
|
@@ -60,6 +79,14 @@ import {
|
|
|
60
79
|
// caller would only learn that reactively when heyspex_promote_research
|
|
61
80
|
// later refuses a child with RESEARCH_STILL_STUB.
|
|
62
81
|
import { findUnresolvedResearchPlaceholders } from './promote-research.mjs';
|
|
82
|
+
import { assertPathUnderRoot } from '../../scripts/lib/shared-path-helpers.mjs';
|
|
83
|
+
|
|
84
|
+
// Placeholders the CLOUD path feeds `buildChildResearchContent` in place of a
|
|
85
|
+
// locally-minted id / file key / family key; `stripServerMintedFields` removes
|
|
86
|
+
// them from the POST body (plus an explicit `delete` of an un-inheritable
|
|
87
|
+
// `family:`), so the console never sees them.
|
|
88
|
+
const CLOUD_PENDING_ID = 'HS-PENDING';
|
|
89
|
+
const CLOUD_PENDING_KEY = 'PENDING';
|
|
63
90
|
|
|
64
91
|
// Id validation/classification is via parseSpecId (dual-read: accepts type-first
|
|
65
92
|
// HS-RES-<key> / HS-MIL-<key> / HS-CAP-<key> and the family-first orderings, plus
|
|
@@ -216,7 +243,7 @@ function appendChangeLogRow(body, dateStr, author, message) {
|
|
|
216
243
|
return body.replace(/\s*$/, '') + `\n${row}\n`;
|
|
217
244
|
}
|
|
218
245
|
|
|
219
|
-
function commitWrites(writes) {
|
|
246
|
+
function commitWrites(writes, vaultRoot) {
|
|
220
247
|
const snapshots = new Map();
|
|
221
248
|
const performed = [];
|
|
222
249
|
try {
|
|
@@ -230,7 +257,10 @@ function commitWrites(writes) {
|
|
|
230
257
|
0,
|
|
231
258
|
Math.max(w.absPath.lastIndexOf('/'), w.absPath.lastIndexOf('\\')),
|
|
232
259
|
);
|
|
233
|
-
if (dir)
|
|
260
|
+
if (dir) {
|
|
261
|
+
if (vaultRoot) assertPathUnderRoot(vaultRoot, dir, 'split-research write');
|
|
262
|
+
mkdirSync(dir, { recursive: true });
|
|
263
|
+
}
|
|
234
264
|
writeFileSync(w.absPath, w.content, 'utf8');
|
|
235
265
|
performed.push(w);
|
|
236
266
|
} else if (w.kind === 'update') {
|
|
@@ -396,6 +426,46 @@ function findCaptureByIdOrBasename(vaultRoot, ref) {
|
|
|
396
426
|
return null;
|
|
397
427
|
}
|
|
398
428
|
|
|
429
|
+
// ── Cloud-tier siblings of the three lookups above (HS-KR16 pattern) ────────
|
|
430
|
+
// `tier` is resolved ONCE per call by `executeSplitResearch` from `deps.tier`
|
|
431
|
+
// and threaded down; absent it is `disk`, so every pre-existing caller reads
|
|
432
|
+
// exactly as before. The neon branch reuses `fetchSpecRecordViaConsole` — the
|
|
433
|
+
// same injected-`consoleFetch` reader promote-capture/promote-research use —
|
|
434
|
+
// so a test drives the whole flow with a recorder and never opens a socket.
|
|
435
|
+
// These exist because a cloud WRITE is pointless if the INPUTS cannot be
|
|
436
|
+
// found: the writes below all derive from the original research doc and the
|
|
437
|
+
// captures it names.
|
|
438
|
+
|
|
439
|
+
async function findResearchByIdViaTier(vaultRoot, researchId, { tier, consoleUrl, consoleFetch }) {
|
|
440
|
+
return readViaSelector({
|
|
441
|
+
tier,
|
|
442
|
+
diskRead: () => findResearchByIdLocal(vaultRoot, researchId),
|
|
443
|
+
neonRead: () => fetchSpecRecordViaConsole(consoleFetch, researchId),
|
|
444
|
+
routeLabel: 'GET /api/specs/[id]',
|
|
445
|
+
consoleUrl,
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
async function findMilestoneViaTier(vaultRoot, ref, { tier, consoleUrl, consoleFetch }) {
|
|
450
|
+
return readViaSelector({
|
|
451
|
+
tier,
|
|
452
|
+
diskRead: () => findMilestoneByIdOrBasename(vaultRoot, ref),
|
|
453
|
+
neonRead: () => fetchSpecRecordViaConsole(consoleFetch, ref),
|
|
454
|
+
routeLabel: 'GET /api/specs/[id]',
|
|
455
|
+
consoleUrl,
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
async function findCaptureViaTier(vaultRoot, ref, { tier, consoleUrl, consoleFetch }) {
|
|
460
|
+
return readViaSelector({
|
|
461
|
+
tier,
|
|
462
|
+
diskRead: () => findCaptureByIdOrBasename(vaultRoot, ref),
|
|
463
|
+
neonRead: () => fetchSpecRecordViaConsole(consoleFetch, ref),
|
|
464
|
+
routeLabel: 'GET /api/specs/[id]',
|
|
465
|
+
consoleUrl,
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
|
|
399
469
|
function collectOccupiedResearchIds(vaultRoot) {
|
|
400
470
|
const occupied = new Set();
|
|
401
471
|
const dir = join(vaultRoot, 'specs', 'research');
|
|
@@ -601,7 +671,20 @@ export async function executeSplitResearch(vaultRoot, args = {}, deps = {}) {
|
|
|
601
671
|
const validationError = validateInputShape(args);
|
|
602
672
|
if (validationError) return validationError;
|
|
603
673
|
|
|
604
|
-
|
|
674
|
+
// Tier + write mode are resolved ONCE, here, by the caller and threaded down
|
|
675
|
+
// — never re-resolved inside a helper. Both default to the disk behaviour so
|
|
676
|
+
// every pre-existing caller/test is byte-identical
|
|
677
|
+
// ([[HS-BYU4-CWPX-TASK-promotion-writes-go-through-the-console]]).
|
|
678
|
+
const tierDeps = {
|
|
679
|
+
tier: deps?.tier ?? 'disk',
|
|
680
|
+
consoleUrl: deps?.consoleUrl,
|
|
681
|
+
consoleFetch: deps?.consoleFetch,
|
|
682
|
+
};
|
|
683
|
+
const writeMode = resolveWriteModeFromDeps(deps);
|
|
684
|
+
const cloudDeps = { consoleFetch: deps?.consoleFetch, consoleUrl: deps?.consoleUrl };
|
|
685
|
+
const cloudCompleted = [];
|
|
686
|
+
|
|
687
|
+
const research = await findResearchByIdViaTier(vaultRoot, args.research_id, tierDeps);
|
|
605
688
|
if (!research) {
|
|
606
689
|
return {
|
|
607
690
|
error: 'RESEARCH_NOT_FOUND',
|
|
@@ -650,9 +733,16 @@ export async function executeSplitResearch(vaultRoot, args = {}, deps = {}) {
|
|
|
650
733
|
// Resolve all splits up-front, before any writes.
|
|
651
734
|
// One occupied set across the whole loop (owner ruling 2026-07-10) — children
|
|
652
735
|
// are not on disk yet, so a fresh vault walk per split could repeat a key.
|
|
653
|
-
|
|
736
|
+
//
|
|
737
|
+
// In CLOUD mode nothing is minted here at all: `collectOccupiedKeysEitherKind`
|
|
738
|
+
// is a vault SCAN, and in a file-less vault it returns an EMPTY set — a key
|
|
739
|
+
// drawn against it can collide with one the cloud vault already holds. The
|
|
740
|
+
// console mints every child's file key, family and filename.
|
|
741
|
+
const occupiedFileKeys =
|
|
742
|
+
writeMode === 'cloud' ? new Set() : collectOccupiedKeysEitherKind(vaultRoot); // union: file+family keys (HS-SJOT)
|
|
654
743
|
const childRecords = [];
|
|
655
744
|
const captureChildMap = new Map();
|
|
745
|
+
const captureRecords = new Map(); // basename -> record (raw reused by the write phase)
|
|
656
746
|
|
|
657
747
|
for (let i = 0; i < args.splits.length; i++) {
|
|
658
748
|
const split = args.splits[i];
|
|
@@ -661,7 +751,7 @@ export async function executeSplitResearch(vaultRoot, args = {}, deps = {}) {
|
|
|
661
751
|
if (split.target_milestone === 'NEW') {
|
|
662
752
|
targetMilestone = { absent: true, newName: split.new_milestone_name.trim() };
|
|
663
753
|
} else {
|
|
664
|
-
const ms =
|
|
754
|
+
const ms = await findMilestoneViaTier(vaultRoot, split.target_milestone, tierDeps);
|
|
665
755
|
if (!ms) {
|
|
666
756
|
return {
|
|
667
757
|
error: 'MILESTONE_NOT_FOUND',
|
|
@@ -673,7 +763,8 @@ export async function executeSplitResearch(vaultRoot, args = {}, deps = {}) {
|
|
|
673
763
|
|
|
674
764
|
const childCaptureBasenames = [];
|
|
675
765
|
for (const captureRef of split.sources_captures) {
|
|
676
|
-
const cap =
|
|
766
|
+
const cap = await findCaptureViaTier(vaultRoot, captureRef, tierDeps);
|
|
767
|
+
if (cap) captureRecords.set(cap.basename, cap);
|
|
677
768
|
if (!cap) {
|
|
678
769
|
return {
|
|
679
770
|
error: 'CAPTURE_NOT_FOUND',
|
|
@@ -695,15 +786,18 @@ export async function executeSplitResearch(vaultRoot, args = {}, deps = {}) {
|
|
|
695
786
|
// original itself has no derivable family (should not occur for a real
|
|
696
787
|
// research doc).
|
|
697
788
|
const childFamilyKey =
|
|
698
|
-
|
|
789
|
+
writeMode === 'cloud'
|
|
790
|
+
? (originalFamilyKey ?? null)
|
|
791
|
+
: (originalFamilyKey ?? mintFamilyKey(vaultRoot, { randomUnit: deps?.randomUnit }));
|
|
699
792
|
// Per-file FILE KEY (owner ruling 2026-07-10) — filename + `file_id:` SSOT,
|
|
700
793
|
// and (V3) the anchor for `id:` itself
|
|
701
794
|
// ([[HS-ZU1Q-TASK-PNAS-flip-new-spec-minting-to-the-file-key]]).
|
|
702
|
-
const childFileKey =
|
|
703
|
-
|
|
704
|
-
|
|
795
|
+
const childFileKey =
|
|
796
|
+
writeMode === 'cloud' ? null : allocateFileKey(occupiedFileKeys, { randomUnit: deps?.randomUnit });
|
|
797
|
+
if (childFileKey) occupiedFileKeys.add(childFileKey);
|
|
798
|
+
const childId = childFileKey ? `HS-${childFileKey}` : null;
|
|
705
799
|
const slug = deriveSlug(split.name);
|
|
706
|
-
const basename = `${childId}-${childFamilyKey}-RES-${slug}
|
|
800
|
+
const basename = childFileKey ? `${childId}-${childFamilyKey}-RES-${slug}` : null;
|
|
707
801
|
|
|
708
802
|
childRecords.push({
|
|
709
803
|
id: childId,
|
|
@@ -716,9 +810,13 @@ export async function executeSplitResearch(vaultRoot, args = {}, deps = {}) {
|
|
|
716
810
|
bodySlice: split.body_slice,
|
|
717
811
|
});
|
|
718
812
|
|
|
813
|
+
// Hold the child RECORD, not its basename: in cloud mode the basename is
|
|
814
|
+
// still null here and only lands when that child's POST answers, so the
|
|
815
|
+
// capture→children map is read lazily at write time in both modes.
|
|
816
|
+
const childRecord = childRecords[childRecords.length - 1];
|
|
719
817
|
for (const capBasename of childCaptureBasenames) {
|
|
720
818
|
if (!captureChildMap.has(capBasename)) captureChildMap.set(capBasename, []);
|
|
721
|
-
captureChildMap.get(capBasename).push(
|
|
819
|
+
captureChildMap.get(capBasename).push(childRecord);
|
|
722
820
|
}
|
|
723
821
|
}
|
|
724
822
|
|
|
@@ -727,17 +825,16 @@ export async function executeSplitResearch(vaultRoot, args = {}, deps = {}) {
|
|
|
727
825
|
const today = now.slice(0, 10);
|
|
728
826
|
const author = 'split-research (agent)';
|
|
729
827
|
const writes = [];
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
basename: child.basename,
|
|
828
|
+
|
|
829
|
+
const buildChildContent = (child, siblingBasenames) =>
|
|
830
|
+
buildChildResearchContent({
|
|
831
|
+
id: child.id ?? CLOUD_PENDING_ID,
|
|
832
|
+
fileKey: child.fileKey ?? CLOUD_PENDING_KEY,
|
|
833
|
+
familyKey: child.familyKey ?? CLOUD_PENDING_KEY,
|
|
834
|
+
basename: child.basename ?? '',
|
|
738
835
|
name: child.name,
|
|
739
836
|
originalBasename: research.basename,
|
|
740
|
-
siblingBasenames
|
|
837
|
+
siblingBasenames,
|
|
741
838
|
targetMilestone: child.targetMilestone,
|
|
742
839
|
capturesBasenames: child.capturesBasenames,
|
|
743
840
|
bodySlice: child.bodySlice,
|
|
@@ -745,42 +842,186 @@ export async function executeSplitResearch(vaultRoot, args = {}, deps = {}) {
|
|
|
745
842
|
today,
|
|
746
843
|
author,
|
|
747
844
|
});
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
845
|
+
|
|
846
|
+
if (writeMode === 'cloud') {
|
|
847
|
+
// ── CREATE FIRST ────────────────────────────────────────────────────────
|
|
848
|
+
// Each child is POSTed with an EMPTY sibling list (a sibling's basename is
|
|
849
|
+
// minted by the console, so it cannot be known before that sibling's own
|
|
850
|
+
// POST answers), then re-PUT once every basename is in hand. The sibling
|
|
851
|
+
// cross-links live only in the "Split-from:" body callout, so the follow-up
|
|
852
|
+
// PUT replaces the BODY and keeps the frontmatter the console stored.
|
|
853
|
+
for (const child of childRecords) {
|
|
854
|
+
const content = buildChildContent(child, []);
|
|
855
|
+
child.unresolvedPlaceholderCount = findUnresolvedResearchPlaceholders(content);
|
|
856
|
+
const { frontmatter, body } = splitRawSpecForConsole(content);
|
|
857
|
+
const cleaned = stripServerMintedFields(frontmatter);
|
|
858
|
+
if (!child.familyKey) delete cleaned.family;
|
|
859
|
+
let created;
|
|
860
|
+
try {
|
|
861
|
+
created = await createSpecViaConsole({
|
|
862
|
+
...cloudDeps,
|
|
863
|
+
type: 'research',
|
|
864
|
+
name: child.name,
|
|
865
|
+
frontmatter: cleaned,
|
|
866
|
+
body,
|
|
867
|
+
actingAgent: deps?.actingAgent ?? null,
|
|
868
|
+
});
|
|
869
|
+
} catch (err) {
|
|
870
|
+
return cloudWriteFailureResult(err, {
|
|
871
|
+
step: { kind: 'create', type: 'research', name: child.name },
|
|
872
|
+
completed: cloudCompleted,
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
child.id = created.specId;
|
|
876
|
+
child.basename = created.basename;
|
|
877
|
+
child.relPath = created.relPath;
|
|
878
|
+
child.cloudFrontmatter = created.spec?.frontmatter ?? cleaned;
|
|
879
|
+
cloudCompleted.push({
|
|
880
|
+
kind: 'create',
|
|
881
|
+
type: 'research',
|
|
882
|
+
spec_id: created.specId,
|
|
883
|
+
basename: created.basename,
|
|
884
|
+
rel_path: created.relPath,
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
const cloudChildBasenames = childRecords.map((c) => c.basename);
|
|
889
|
+
|
|
890
|
+
// ── …LINK SECOND: sibling callouts, then the original, then the captures.
|
|
891
|
+
if (childRecords.length > 1) {
|
|
892
|
+
for (const child of childRecords) {
|
|
893
|
+
const rebuilt = buildChildContent(child, cloudChildBasenames);
|
|
894
|
+
const { body } = splitRawSpecForConsole(rebuilt);
|
|
895
|
+
try {
|
|
896
|
+
const res = await updateSpecViaConsole({
|
|
897
|
+
...cloudDeps,
|
|
898
|
+
specId: child.id,
|
|
899
|
+
frontmatter: child.cloudFrontmatter,
|
|
900
|
+
body,
|
|
901
|
+
});
|
|
902
|
+
cloudCompleted.push({
|
|
903
|
+
kind: 'update',
|
|
904
|
+
type: 'research.siblings',
|
|
905
|
+
spec_id: res.specId,
|
|
906
|
+
basename: res.basename ?? child.basename,
|
|
907
|
+
rel_path: res.relPath,
|
|
908
|
+
});
|
|
909
|
+
} catch (err) {
|
|
910
|
+
return cloudWriteFailureResult(err, {
|
|
911
|
+
step: { kind: 'update', type: 'research.siblings', basename: child.basename },
|
|
912
|
+
completed: cloudCompleted,
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
const originalUpdatedCloud = buildUpdatedOriginalContent(research.raw, {
|
|
919
|
+
childBasenames: cloudChildBasenames,
|
|
920
|
+
now,
|
|
921
|
+
today,
|
|
922
|
+
author,
|
|
753
923
|
});
|
|
754
|
-
|
|
924
|
+
{
|
|
925
|
+
const { frontmatter, body } = splitRawSpecForConsole(originalUpdatedCloud);
|
|
926
|
+
try {
|
|
927
|
+
const res = await updateSpecViaConsole({
|
|
928
|
+
...cloudDeps,
|
|
929
|
+
specId: originalId,
|
|
930
|
+
frontmatter,
|
|
931
|
+
body,
|
|
932
|
+
});
|
|
933
|
+
cloudCompleted.push({
|
|
934
|
+
kind: 'update',
|
|
935
|
+
type: 'research.original',
|
|
936
|
+
spec_id: res.specId,
|
|
937
|
+
basename: res.basename ?? research.basename,
|
|
938
|
+
rel_path: res.relPath,
|
|
939
|
+
});
|
|
940
|
+
} catch (err) {
|
|
941
|
+
return cloudWriteFailureResult(err, {
|
|
942
|
+
step: { kind: 'update', type: 'research.original', basename: research.basename },
|
|
943
|
+
completed: cloudCompleted,
|
|
944
|
+
});
|
|
945
|
+
}
|
|
946
|
+
}
|
|
755
947
|
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
948
|
+
for (const [capBasename, childList] of captureChildMap.entries()) {
|
|
949
|
+
const capRecord = captureRecords.get(capBasename);
|
|
950
|
+
if (!capRecord) continue;
|
|
951
|
+
const capUpdated = buildUpdatedCaptureContent(
|
|
952
|
+
capRecord.raw,
|
|
953
|
+
childList.map((c) => c.basename),
|
|
954
|
+
now,
|
|
955
|
+
);
|
|
956
|
+
if (capUpdated === capRecord.raw) continue;
|
|
957
|
+
const { frontmatter, body } = splitRawSpecForConsole(capUpdated);
|
|
958
|
+
try {
|
|
959
|
+
const res = await updateSpecViaConsole({
|
|
960
|
+
...cloudDeps,
|
|
961
|
+
specId: getScalarField(getFrontmatterBlock(capRecord.raw)?.block ?? '', 'id') ?? capBasename,
|
|
962
|
+
frontmatter,
|
|
963
|
+
body,
|
|
964
|
+
});
|
|
965
|
+
cloudCompleted.push({
|
|
966
|
+
kind: 'update',
|
|
967
|
+
type: 'capture',
|
|
968
|
+
spec_id: res.specId,
|
|
969
|
+
basename: res.basename ?? capBasename,
|
|
970
|
+
rel_path: res.relPath,
|
|
971
|
+
});
|
|
972
|
+
} catch (err) {
|
|
973
|
+
return cloudWriteFailureResult(err, {
|
|
974
|
+
step: { kind: 'update', type: 'capture', basename: capBasename },
|
|
975
|
+
completed: cloudCompleted,
|
|
976
|
+
});
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
} else {
|
|
980
|
+
const childBasenames = childRecords.map((c) => c.basename);
|
|
981
|
+
|
|
982
|
+
for (const child of childRecords) {
|
|
983
|
+
const content = buildChildContent(child, childBasenames);
|
|
984
|
+
child.unresolvedPlaceholderCount = findUnresolvedResearchPlaceholders(content);
|
|
985
|
+
writes.push({
|
|
986
|
+
kind: 'create',
|
|
987
|
+
absPath: join(vaultRoot, 'specs', 'research', `${child.basename}.md`),
|
|
988
|
+
content,
|
|
989
|
+
});
|
|
990
|
+
}
|
|
767
991
|
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
992
|
+
const originalUpdated = buildUpdatedOriginalContent(research.raw, {
|
|
993
|
+
childBasenames,
|
|
994
|
+
now,
|
|
995
|
+
today,
|
|
996
|
+
author,
|
|
997
|
+
});
|
|
998
|
+
writes.push({
|
|
999
|
+
kind: 'update',
|
|
1000
|
+
absPath: research.absPath,
|
|
1001
|
+
content: originalUpdated,
|
|
1002
|
+
});
|
|
1003
|
+
|
|
1004
|
+
for (const [capBasename, childList] of captureChildMap.entries()) {
|
|
1005
|
+
const capAbs = join(vaultRoot, 'specs', 'captures', `${capBasename}.md`);
|
|
1006
|
+
const capRaw = readFileSync(capAbs, 'utf8');
|
|
1007
|
+
const capUpdated = buildUpdatedCaptureContent(
|
|
1008
|
+
capRaw,
|
|
1009
|
+
childList.map((c) => c.basename),
|
|
1010
|
+
now,
|
|
1011
|
+
);
|
|
1012
|
+
if (capUpdated !== capRaw) {
|
|
1013
|
+
writes.push({ kind: 'update', absPath: capAbs, content: capUpdated });
|
|
1014
|
+
}
|
|
774
1015
|
}
|
|
775
|
-
}
|
|
776
1016
|
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
1017
|
+
const commitResult = commitWrites(writes, vaultRoot);
|
|
1018
|
+
if (!commitResult.ok) {
|
|
1019
|
+
return {
|
|
1020
|
+
error: 'WRITE_FAILED',
|
|
1021
|
+
message: commitResult.error?.message ?? 'unknown write failure',
|
|
1022
|
+
rollback_error: commitResult.error?.rollbackError,
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
784
1025
|
}
|
|
785
1026
|
|
|
786
1027
|
const anyChildStillStub = childRecords.some((c) => c.unresolvedPlaceholderCount > 0);
|
|
@@ -790,10 +1031,11 @@ export async function executeSplitResearch(vaultRoot, args = {}, deps = {}) {
|
|
|
790
1031
|
archive_basename: research.basename,
|
|
791
1032
|
archive_path: research.relPath,
|
|
792
1033
|
archive_status_after: getScalarField(fm.block, 'status') ?? 'open',
|
|
1034
|
+
...(writeMode === 'cloud' ? { write_mode: 'cloud' } : {}),
|
|
793
1035
|
children: childRecords.map((c) => ({
|
|
794
1036
|
id: c.id,
|
|
795
1037
|
basename: c.basename,
|
|
796
|
-
path: join('specs', 'research', `${c.basename}.md`),
|
|
1038
|
+
path: c.relPath ?? join('specs', 'research', `${c.basename}.md`),
|
|
797
1039
|
target_milestone: c.targetMilestone.absent ? 'NEW' : c.targetMilestone.basename,
|
|
798
1040
|
sources_captures: c.capturesBasenames,
|
|
799
1041
|
// Same placeholder count RESEARCH_STILL_STUB would later refuse
|
|
@@ -802,7 +1044,10 @@ export async function executeSplitResearch(vaultRoot, args = {}, deps = {}) {
|
|
|
802
1044
|
research_still_stub: c.unresolvedPlaceholderCount > 0,
|
|
803
1045
|
unresolved_placeholder_count: c.unresolvedPlaceholderCount,
|
|
804
1046
|
})),
|
|
805
|
-
writes:
|
|
1047
|
+
writes:
|
|
1048
|
+
writeMode === 'cloud'
|
|
1049
|
+
? cloudCompleted.map((w) => ({ kind: w.kind, path: w.rel_path ?? null, spec_id: w.spec_id }))
|
|
1050
|
+
: writes.map((w) => ({ kind: w.kind, path: w.absPath })),
|
|
806
1051
|
// No fallback warning: every child unconditionally inherits the
|
|
807
1052
|
// original's family key (V3 shared-surname model — see the block comment
|
|
808
1053
|
// above inheritedFamilyKey).
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// JSON-RPC writers for the stdio transport — the only place the MCP server
|
|
2
|
+
// writes to stdout. Extracted from cli.mjs under
|
|
3
|
+
// [[HS-BLNF-6W61-DIAG-cli-mjs-has-grown-into-a-13-665-line]] (no behavior change).
|
|
4
|
+
|
|
5
|
+
export function sendResponse(id, result) {
|
|
6
|
+
const response = { jsonrpc: '2.0', id, result };
|
|
7
|
+
process.stdout.write(JSON.stringify(response) + '\n');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function sendError(id, code, message, data) {
|
|
11
|
+
const response = {
|
|
12
|
+
jsonrpc: '2.0',
|
|
13
|
+
id,
|
|
14
|
+
error: { code, message, ...(data && { data }) },
|
|
15
|
+
};
|
|
16
|
+
process.stdout.write(JSON.stringify(response) + '\n');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function sendNotification(method, params) {
|
|
20
|
+
const notification = { jsonrpc: '2.0', method, params };
|
|
21
|
+
process.stdout.write(JSON.stringify(notification) + '\n');
|
|
22
|
+
}
|