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
|
@@ -52,6 +52,7 @@ import { scoreSufficiency } from '../../dev-factory/lib/sufficiency-score.mjs';
|
|
|
52
52
|
// capture-focus.mjs deep-dive reader rather than a third regex copy.
|
|
53
53
|
import { extractCaptureExpandedConceptFromBody } from './capture-focus.mjs';
|
|
54
54
|
import {
|
|
55
|
+
collectOccupiedKeysEitherKind,
|
|
55
56
|
mintFileKey,
|
|
56
57
|
mintFamilyKey,
|
|
57
58
|
parseSpecId,
|
|
@@ -82,7 +83,27 @@ import {
|
|
|
82
83
|
bumpUpdated,
|
|
83
84
|
appendChangeLogRow,
|
|
84
85
|
commitWrites,
|
|
86
|
+
fetchSpecRecordViaConsole,
|
|
87
|
+
resolveWriteModeFromDeps,
|
|
88
|
+
splitRawSpecForConsole,
|
|
89
|
+
cloudWriteFailureResult,
|
|
85
90
|
} from './promotion-helpers.mjs';
|
|
91
|
+
// Cloud-mode vault WRITES ([[HS-BYU4-CWPX-TASK-promotion-writes-go-through-the-console]]).
|
|
92
|
+
// The write-side twin of read-path.mjs: in `cloud` write mode a create becomes
|
|
93
|
+
// POST /api/specs (server-minted id + family + filename) and an edit becomes
|
|
94
|
+
// PUT /api/specs/:id. Nothing here opens a socket itself — `consoleFetch` is
|
|
95
|
+
// the same injected dep the cloud-tier READS already use.
|
|
96
|
+
import {
|
|
97
|
+
createSpecViaConsole,
|
|
98
|
+
updateSpecViaConsole,
|
|
99
|
+
stripServerMintedFields,
|
|
100
|
+
} from './cloud-vault-writes.mjs';
|
|
101
|
+
// Cloud-tier read selector ([[HS-KR16-CWPX-TASK-promotion-reads-through-the-cloud-tier]]) —
|
|
102
|
+
// reused verbatim, never copied. `readViaSelector` picks disk vs neon; the
|
|
103
|
+
// neon branch below fetches through `fetchSpecRecordViaConsole` (an injected
|
|
104
|
+
// consoleFetch, not read-path.mjs's own transport) so tests never open a
|
|
105
|
+
// socket, matching this file's existing deps-injection convention.
|
|
106
|
+
import { readViaSelector } from './read-path.mjs';
|
|
86
107
|
// Reuse the exact same placeholder-count logic RESEARCH_STILL_STUB uses to
|
|
87
108
|
// refuse `heyspex_promote_research` on a stub research doc — proactively
|
|
88
109
|
// surfaced here at creation/extension time instead of only reactively at
|
|
@@ -99,6 +120,15 @@ import { findUnresolvedResearchPlaceholders } from './promote-research.mjs';
|
|
|
99
120
|
// vendor/heyspex-mcp/spec-id-lib.mjs (`parseSpecId`).
|
|
100
121
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
101
122
|
|
|
123
|
+
// Placeholders the CLOUD path feeds the shared content builders in place of a
|
|
124
|
+
// locally-minted id / file key / family key. They never reach the console:
|
|
125
|
+
// `stripServerMintedFields` removes `id:` and `file_id:` from the request, and
|
|
126
|
+
// an un-inheritable `family:` is deleted alongside them. Their only job is to
|
|
127
|
+
// keep the builder's template well-formed so its BODY is byte-for-byte the
|
|
128
|
+
// markdown the disk path would have written.
|
|
129
|
+
const CLOUD_PENDING_ID = 'HS-PENDING';
|
|
130
|
+
const CLOUD_PENDING_KEY = 'PENDING';
|
|
131
|
+
|
|
102
132
|
/** True when `id` (either ordering) parses to a spec with the given type label. */
|
|
103
133
|
function hasTypeLabel(id, label) {
|
|
104
134
|
const p = parseSpecId(id);
|
|
@@ -295,6 +325,36 @@ function findResearchByIdLocal(vaultRoot, researchId) {
|
|
|
295
325
|
return null;
|
|
296
326
|
}
|
|
297
327
|
|
|
328
|
+
/**
|
|
329
|
+
* Cloud-tier sibling of {@link findCaptureByIdLocal} (HS-KR16). `tier`,
|
|
330
|
+
* `consoleUrl` and `consoleFetch` are resolved ONCE per handler call
|
|
331
|
+
* (`handlePromoteCapture` in capture-tools.mjs) and threaded down here —
|
|
332
|
+
* never re-resolved inside this helper.
|
|
333
|
+
*/
|
|
334
|
+
async function findCaptureByIdViaTier(vaultRoot, captureId, { tier, consoleUrl, consoleFetch }) {
|
|
335
|
+
return readViaSelector({
|
|
336
|
+
tier,
|
|
337
|
+
diskRead: () => findCaptureByIdLocal(vaultRoot, captureId),
|
|
338
|
+
neonRead: () => fetchSpecRecordViaConsole(consoleFetch, captureId),
|
|
339
|
+
routeLabel: 'GET /api/specs/[id]',
|
|
340
|
+
consoleUrl,
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Cloud-tier sibling of {@link findResearchByIdLocal} (HS-KR16). Same
|
|
346
|
+
* contract: a bare record, or `null` on a genuine not-found.
|
|
347
|
+
*/
|
|
348
|
+
async function findResearchByIdViaTier(vaultRoot, researchId, { tier, consoleUrl, consoleFetch }) {
|
|
349
|
+
return readViaSelector({
|
|
350
|
+
tier,
|
|
351
|
+
diskRead: () => findResearchByIdLocal(vaultRoot, researchId),
|
|
352
|
+
neonRead: () => fetchSpecRecordViaConsole(consoleFetch, researchId),
|
|
353
|
+
routeLabel: 'GET /api/specs/[id]',
|
|
354
|
+
consoleUrl,
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
|
|
298
358
|
function findResearchByBasename(vaultRoot, basename) {
|
|
299
359
|
const absPath = join(vaultRoot, 'specs', 'research', `${basename}.md`);
|
|
300
360
|
if (!existsSync(absPath)) return null;
|
|
@@ -652,6 +712,26 @@ async function runExecutePhase(args, ctx, deps) {
|
|
|
652
712
|
const now = (deps?.clock ?? isoNow)();
|
|
653
713
|
const today = now.slice(0, 10);
|
|
654
714
|
const author = 'promote-capture (agent)';
|
|
715
|
+
// Vault WRITE mode ([[HS-BYU4-CWPX-TASK-promotion-writes-go-through-the-console]]).
|
|
716
|
+
// Resolved ONCE by the caller (`resolveVaultWriteMode()`) and threaded in on
|
|
717
|
+
// `deps`; absent (every pre-existing caller and test) it is `local`, so the
|
|
718
|
+
// disk path below is byte-identical to before this task.
|
|
719
|
+
//
|
|
720
|
+
// CREATE FIRST, LINK SECOND: on disk the flow stages every write and commits
|
|
721
|
+
// them as one transaction, because it mints the research filename itself. In
|
|
722
|
+
// cloud mode the console mints the id, the family and the filename, so the
|
|
723
|
+
// basename each capture's `promoted_to:` / `informs_research:` links to is
|
|
724
|
+
// only knowable AFTER `POST /api/specs` returns. The order therefore
|
|
725
|
+
// inverts: POST the research doc, take its basename, then PUT each capture.
|
|
726
|
+
const writeMode = resolveWriteModeFromDeps(deps);
|
|
727
|
+
const cloudDeps = { consoleFetch: deps?.consoleFetch, consoleUrl: deps?.consoleUrl };
|
|
728
|
+
// Steps that have ALREADY landed in the cloud vault, in order. There is no
|
|
729
|
+
// rollback (the console has no un-create) — on a mid-sequence failure this
|
|
730
|
+
// list is what the caller is told actually happened.
|
|
731
|
+
const cloudCompleted = [];
|
|
732
|
+
// PUTs queued while the primary create is being resolved; flushed at the
|
|
733
|
+
// commit point below, in order.
|
|
734
|
+
const cloudUpdates = [];
|
|
655
735
|
|
|
656
736
|
// ── Resolve / allocate target research basename + path ────────────────────
|
|
657
737
|
let researchBasename;
|
|
@@ -672,28 +752,49 @@ async function runExecutePhase(args, ctx, deps) {
|
|
|
672
752
|
// (ctx.captures[0]) just below. No fallback: era-3 `id:` keys onto the
|
|
673
753
|
// file key alone, so a shared family can never collide.
|
|
674
754
|
const primaryCapture = ctx.captures[0] ?? null;
|
|
675
|
-
|
|
676
|
-
(
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
755
|
+
const inherited = primaryCapture
|
|
756
|
+
? inheritedFamilyKey(
|
|
757
|
+
primaryCapture.frontmatter?.id ?? null,
|
|
758
|
+
primaryCapture.frontmatter,
|
|
759
|
+
primaryCapture.basename,
|
|
760
|
+
)
|
|
761
|
+
: null;
|
|
762
|
+
if (writeMode === 'cloud') {
|
|
763
|
+
// NOTHING is minted locally in cloud mode (AC3): `mintFileKey` /
|
|
764
|
+
// `mintFamilyKey` both derive their answer by scanning the vault for
|
|
765
|
+
// occupied keys, and in a file-less vault that scan sees an EMPTY set —
|
|
766
|
+
// so a key minted here can collide with one the cloud vault already
|
|
767
|
+
// holds. The console mints the file key, the family and the filename
|
|
768
|
+
// inside `createSpec`. An INHERITED family is still passed through (it
|
|
769
|
+
// is read off the source capture, not minted, and `createSpec` honours
|
|
770
|
+
// an explicit `family:` over parent inheritance); with no inheritable
|
|
771
|
+
// family the field is simply omitted and the server picks one.
|
|
772
|
+
researchFamilyKey = inherited ?? null;
|
|
773
|
+
researchFileKey = null;
|
|
774
|
+
researchId = null;
|
|
775
|
+
researchBasename = null;
|
|
776
|
+
researchAbsPath = null;
|
|
777
|
+
researchRelPath = null;
|
|
778
|
+
} else {
|
|
779
|
+
researchFamilyKey =
|
|
780
|
+
inherited ?? mintFamilyKey(ctx.vaultRoot, { randomUnit: deps?.randomUnit });
|
|
781
|
+
// Per-file FILE KEY (owner ruling 2026-07-10) — filename + `file_id:` SSOT,
|
|
782
|
+
// and (V3) the anchor for `id:` itself ([[HS-ZU1Q-TASK-PNAS-flip-new-spec-minting-to-the-file-key]]).
|
|
783
|
+
// Disk occupancy ([[HS-FJWL]]): the `writeMode === 'cloud'` branch above
|
|
784
|
+
// mints NOTHING locally, so this is the local-vault branch only.
|
|
785
|
+
researchFileKey = mintFileKey(collectOccupiedKeysEitherKind(ctx.vaultRoot), { randomUnit: deps?.randomUnit });
|
|
786
|
+
researchId = `HS-${researchFileKey}`;
|
|
787
|
+
const outlineName = args.accepted_outline?.name ?? args.new_research_name;
|
|
788
|
+
const slug = deriveSlug(outlineName);
|
|
789
|
+
researchBasename = `${researchId}-${researchFamilyKey}-RES-${slug}`;
|
|
790
|
+
researchAbsPath = join(
|
|
791
|
+
ctx.vaultRoot,
|
|
792
|
+
'specs',
|
|
793
|
+
'research',
|
|
794
|
+
`${researchBasename}.md`,
|
|
795
|
+
);
|
|
796
|
+
researchRelPath = join('specs', 'research', `${researchBasename}.md`);
|
|
797
|
+
}
|
|
697
798
|
researchCreated = true;
|
|
698
799
|
|
|
699
800
|
// Sanity: accepted_outline is required to actually create the file.
|
|
@@ -775,10 +876,14 @@ async function runExecutePhase(args, ctx, deps) {
|
|
|
775
876
|
// interim `JTBD (job-to-be-done):` leading block, when present.
|
|
776
877
|
const jtbdSeed =
|
|
777
878
|
ctx.captures.length > 0 ? extractCaptureJtbd(ctx.captures[0].raw) : '';
|
|
879
|
+
// In cloud mode the id/file key/family placeholders below are stripped
|
|
880
|
+
// before the request goes out (`stripServerMintedFields`) — the builder is
|
|
881
|
+
// reused verbatim rather than forked so the BODY the console stores is the
|
|
882
|
+
// same markdown the disk path writes.
|
|
778
883
|
const newContent = buildResearchStubContent(
|
|
779
|
-
researchId,
|
|
780
|
-
researchFileKey,
|
|
781
|
-
researchFamilyKey,
|
|
884
|
+
researchId ?? CLOUD_PENDING_ID,
|
|
885
|
+
researchFileKey ?? CLOUD_PENDING_KEY,
|
|
886
|
+
researchFamilyKey ?? CLOUD_PENDING_KEY,
|
|
782
887
|
outlineForCreate,
|
|
783
888
|
captureBasenames,
|
|
784
889
|
enhancesBasename,
|
|
@@ -786,7 +891,42 @@ async function runExecutePhase(args, ctx, deps) {
|
|
|
786
891
|
suggestedType,
|
|
787
892
|
jtbdSeed,
|
|
788
893
|
);
|
|
789
|
-
|
|
894
|
+
if (writeMode === 'cloud') {
|
|
895
|
+
// ── CREATE FIRST ────────────────────────────────────────────────────
|
|
896
|
+
const { frontmatter, body } = splitRawSpecForConsole(newContent);
|
|
897
|
+
const cleaned = stripServerMintedFields(frontmatter);
|
|
898
|
+
if (!researchFamilyKey) delete cleaned.family;
|
|
899
|
+
let created;
|
|
900
|
+
try {
|
|
901
|
+
created = await createSpecViaConsole({
|
|
902
|
+
...cloudDeps,
|
|
903
|
+
type: 'research',
|
|
904
|
+
name: outlineForCreate.name,
|
|
905
|
+
frontmatter: cleaned,
|
|
906
|
+
body,
|
|
907
|
+
actingAgent: deps?.actingAgent ?? null,
|
|
908
|
+
});
|
|
909
|
+
} catch (err) {
|
|
910
|
+
return cloudWriteFailureResult(err, {
|
|
911
|
+
step: { kind: 'create', type: 'research', name: outlineForCreate.name },
|
|
912
|
+
completed: cloudCompleted,
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
// ── …LINK SECOND: every sibling write below now has a real basename.
|
|
916
|
+
researchId = created.specId;
|
|
917
|
+
researchBasename = created.basename;
|
|
918
|
+
researchRelPath = created.relPath;
|
|
919
|
+
researchAbsPath = null;
|
|
920
|
+
cloudCompleted.push({
|
|
921
|
+
kind: 'create',
|
|
922
|
+
type: 'research',
|
|
923
|
+
spec_id: created.specId,
|
|
924
|
+
basename: created.basename,
|
|
925
|
+
rel_path: created.relPath,
|
|
926
|
+
});
|
|
927
|
+
} else {
|
|
928
|
+
writes.push({ kind: 'create', absPath: researchAbsPath, content: newContent });
|
|
929
|
+
}
|
|
790
930
|
finalResearchContent = newContent;
|
|
791
931
|
} else {
|
|
792
932
|
const updated = buildUpdatedResearchContent(
|
|
@@ -800,11 +940,23 @@ async function runExecutePhase(args, ctx, deps) {
|
|
|
800
940
|
},
|
|
801
941
|
);
|
|
802
942
|
if (updated.changed) {
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
943
|
+
if (writeMode === 'cloud') {
|
|
944
|
+
// The target research already exists, so its basename is already
|
|
945
|
+
// known — this PUT carries no create-first dependency and is simply
|
|
946
|
+
// queued with the capture PUTs below.
|
|
947
|
+
cloudUpdates.push({
|
|
948
|
+
label: 'research',
|
|
949
|
+
specId: researchId,
|
|
950
|
+
basename: researchBasename,
|
|
951
|
+
content: updated.content,
|
|
952
|
+
});
|
|
953
|
+
} else {
|
|
954
|
+
writes.push({
|
|
955
|
+
kind: 'update',
|
|
956
|
+
absPath: researchAbsPath,
|
|
957
|
+
content: updated.content,
|
|
958
|
+
});
|
|
959
|
+
}
|
|
808
960
|
}
|
|
809
961
|
finalResearchContent = updated.changed ? updated.content : ctx.targetResolution.research.raw;
|
|
810
962
|
}
|
|
@@ -842,15 +994,28 @@ async function runExecutePhase(args, ctx, deps) {
|
|
|
842
994
|
promoted_to_set: updated.setPromotedTo,
|
|
843
995
|
});
|
|
844
996
|
if (updated.changed) {
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
997
|
+
if (writeMode === 'cloud') {
|
|
998
|
+
cloudUpdates.push({
|
|
999
|
+
label: 'capture',
|
|
1000
|
+
specId: c.frontmatter?.id ?? c.basename,
|
|
1001
|
+
basename: c.basename,
|
|
1002
|
+
content: updated.content,
|
|
1003
|
+
});
|
|
1004
|
+
} else {
|
|
1005
|
+
writes.push({
|
|
1006
|
+
kind: 'update',
|
|
1007
|
+
absPath: c.absPath,
|
|
1008
|
+
content: updated.content,
|
|
1009
|
+
});
|
|
1010
|
+
}
|
|
850
1011
|
}
|
|
851
1012
|
}
|
|
852
1013
|
|
|
853
|
-
|
|
1014
|
+
const nothingToWrite =
|
|
1015
|
+
writeMode === 'cloud'
|
|
1016
|
+
? cloudCompleted.length === 0 && cloudUpdates.length === 0
|
|
1017
|
+
: writes.length === 0;
|
|
1018
|
+
if (nothingToWrite) {
|
|
854
1019
|
// Idempotent no-op: research already references every input capture and
|
|
855
1020
|
// each capture already references the research. Surface this clearly.
|
|
856
1021
|
return {
|
|
@@ -871,13 +1036,41 @@ async function runExecutePhase(args, ctx, deps) {
|
|
|
871
1036
|
}
|
|
872
1037
|
|
|
873
1038
|
// ── Commit ────────────────────────────────────────────────────────────────
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
1039
|
+
if (writeMode === 'cloud') {
|
|
1040
|
+
// Flush the queued PUTs IN ORDER, stopping at the first failure. No
|
|
1041
|
+
// rollback: the error result names the failing step and lists what landed.
|
|
1042
|
+
for (const u of cloudUpdates) {
|
|
1043
|
+
const { frontmatter, body } = splitRawSpecForConsole(u.content);
|
|
1044
|
+
try {
|
|
1045
|
+
const res = await updateSpecViaConsole({
|
|
1046
|
+
...cloudDeps,
|
|
1047
|
+
specId: u.specId,
|
|
1048
|
+
frontmatter,
|
|
1049
|
+
body,
|
|
1050
|
+
});
|
|
1051
|
+
cloudCompleted.push({
|
|
1052
|
+
kind: 'update',
|
|
1053
|
+
type: u.label,
|
|
1054
|
+
spec_id: res.specId,
|
|
1055
|
+
basename: res.basename ?? u.basename,
|
|
1056
|
+
rel_path: res.relPath,
|
|
1057
|
+
});
|
|
1058
|
+
} catch (err) {
|
|
1059
|
+
return cloudWriteFailureResult(err, {
|
|
1060
|
+
step: { kind: 'update', type: u.label, spec_id: u.specId, basename: u.basename },
|
|
1061
|
+
completed: cloudCompleted,
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
} else {
|
|
1066
|
+
const commitResult = commitWrites(writes);
|
|
1067
|
+
if (!commitResult.ok) {
|
|
1068
|
+
return {
|
|
1069
|
+
error: 'WRITE_FAILED',
|
|
1070
|
+
message: commitResult.error?.message ?? 'unknown write failure',
|
|
1071
|
+
rollback_error: commitResult.error?.rollbackError,
|
|
1072
|
+
};
|
|
1073
|
+
}
|
|
881
1074
|
}
|
|
882
1075
|
|
|
883
1076
|
return {
|
|
@@ -887,7 +1080,11 @@ async function runExecutePhase(args, ctx, deps) {
|
|
|
887
1080
|
research_path: researchRelPath,
|
|
888
1081
|
research_created: researchCreated,
|
|
889
1082
|
captures: capturePromotionResults,
|
|
890
|
-
|
|
1083
|
+
...(writeMode === 'cloud' ? { write_mode: 'cloud' } : {}),
|
|
1084
|
+
writes:
|
|
1085
|
+
writeMode === 'cloud'
|
|
1086
|
+
? cloudCompleted.map((w) => ({ kind: w.kind, path: w.rel_path ?? null, spec_id: w.spec_id }))
|
|
1087
|
+
: writes.map((w) => ({ kind: w.kind, path: w.absPath })),
|
|
891
1088
|
// TARGET_RESEARCH_CLOSED only occurs on the existing-research branch
|
|
892
1089
|
// (researchCreated === false). Family inheritance on the NEW-research
|
|
893
1090
|
// branch no longer has a fallback case to warn about (era-3 minting —
|
|
@@ -919,6 +1116,19 @@ export async function executePromoteCapture(vaultRoot, args = {}, deps = {}) {
|
|
|
919
1116
|
const validationError = validateInputShape(args);
|
|
920
1117
|
if (validationError) return validationError;
|
|
921
1118
|
|
|
1119
|
+
// Cloud-tier reads ([[HS-KR16-CWPX-TASK-promotion-reads-through-the-cloud-tier]]):
|
|
1120
|
+
// `deps.tier` is resolved ONCE by the caller (`handlePromoteCapture` in
|
|
1121
|
+
// capture-tools.mjs, via `resolveReadTier()`) and threaded down — never
|
|
1122
|
+
// re-resolved here. Absent (every pre-existing caller/test) defaults to
|
|
1123
|
+
// `disk`, so disk-tier behavior is byte-identical to before this task.
|
|
1124
|
+
// Cloud-mode vault WRITES ([[HS-BYU4-CWPX-TASK-promotion-writes-go-through-the-console]]):
|
|
1125
|
+
// `deps.writeMode` is resolved ONCE by the caller (`resolveVaultWriteMode()`)
|
|
1126
|
+
// exactly like `deps.tier`, and defaults to `local` so every pre-existing
|
|
1127
|
+
// caller/test writes to disk unchanged. It is consumed in `runExecutePhase`.
|
|
1128
|
+
const tier = deps.tier ?? 'disk';
|
|
1129
|
+
const consoleUrl = deps.consoleUrl;
|
|
1130
|
+
const consoleFetchDep = deps.consoleFetch;
|
|
1131
|
+
|
|
922
1132
|
// 1. Load every input capture; fail on first miss. Ids collapse to the
|
|
923
1133
|
// short stored form first (display long `HS-XV4W-HV6H` → `HS-XV4W`) so the
|
|
924
1134
|
// basename-prefix and frontmatter-id matches inside the loader hit for any
|
|
@@ -926,7 +1136,11 @@ export async function executePromoteCapture(vaultRoot, args = {}, deps = {}) {
|
|
|
926
1136
|
const captures = [];
|
|
927
1137
|
for (const rawCaptureId of args.capture_ids) {
|
|
928
1138
|
const captureId = shortLookupIdOf(rawCaptureId) ?? rawCaptureId;
|
|
929
|
-
const capture =
|
|
1139
|
+
const capture = await findCaptureByIdViaTier(vaultRoot, captureId, {
|
|
1140
|
+
tier,
|
|
1141
|
+
consoleUrl,
|
|
1142
|
+
consoleFetch: consoleFetchDep,
|
|
1143
|
+
});
|
|
930
1144
|
if (!capture) {
|
|
931
1145
|
return {
|
|
932
1146
|
error: 'CAPTURE_NOT_FOUND',
|
|
@@ -1008,9 +1222,10 @@ export async function executePromoteCapture(vaultRoot, args = {}, deps = {}) {
|
|
|
1008
1222
|
if (args.target_research === 'NEW') {
|
|
1009
1223
|
targetResolution = { mode: 'new' };
|
|
1010
1224
|
} else {
|
|
1011
|
-
const research =
|
|
1225
|
+
const research = await findResearchByIdViaTier(
|
|
1012
1226
|
vaultRoot,
|
|
1013
1227
|
shortLookupIdOf(args.target_research) ?? args.target_research,
|
|
1228
|
+
{ tier, consoleUrl, consoleFetch: consoleFetchDep },
|
|
1014
1229
|
);
|
|
1015
1230
|
if (!research) {
|
|
1016
1231
|
return {
|
|
@@ -1190,7 +1405,19 @@ export async function executePromoteCaptureToDiagnose(vaultRoot, args = {}, deps
|
|
|
1190
1405
|
const severityRaw = typeof args.severity === 'string' ? args.severity.trim().toLowerCase() : 'warning';
|
|
1191
1406
|
const severity = VALID_DIAGNOSE_SEVERITIES.has(severityRaw) ? severityRaw : 'warning';
|
|
1192
1407
|
|
|
1193
|
-
|
|
1408
|
+
// Same once-per-call resolution rule as executePromoteCapture: `tier` and
|
|
1409
|
+
// `writeMode` come in on `deps`, already resolved, and default to
|
|
1410
|
+
// `disk`/`local` so every pre-existing caller is byte-identical.
|
|
1411
|
+
const tier = deps?.tier ?? 'disk';
|
|
1412
|
+
const writeMode = resolveWriteModeFromDeps(deps);
|
|
1413
|
+
const cloudDeps = { consoleFetch: deps?.consoleFetch, consoleUrl: deps?.consoleUrl };
|
|
1414
|
+
const cloudCompleted = [];
|
|
1415
|
+
|
|
1416
|
+
const capture = await findCaptureByIdViaTier(vaultRoot, captureId, {
|
|
1417
|
+
tier,
|
|
1418
|
+
consoleUrl: deps?.consoleUrl,
|
|
1419
|
+
consoleFetch: deps?.consoleFetch,
|
|
1420
|
+
});
|
|
1194
1421
|
if (!capture) {
|
|
1195
1422
|
return { error: 'CAPTURE_NOT_FOUND', message: `capture ${captureId} not found under specs/captures/` };
|
|
1196
1423
|
}
|
|
@@ -1207,23 +1434,73 @@ export async function executePromoteCaptureToDiagnose(vaultRoot, args = {}, deps
|
|
|
1207
1434
|
|
|
1208
1435
|
// A capture→diagnose promotion has no documented family-inheritance hop
|
|
1209
1436
|
// (unlike capture→research) — mints a fresh family, same as any other
|
|
1210
|
-
// parentless spec.
|
|
1211
|
-
|
|
1437
|
+
// parentless spec. In CLOUD mode nothing is minted here at all: the console
|
|
1438
|
+
// mints the file key, the family and the filename (AC3).
|
|
1439
|
+
const familyKey =
|
|
1440
|
+
writeMode === 'cloud' ? null : mintFamilyKey(vaultRoot, { randomUnit: deps?.randomUnit });
|
|
1212
1441
|
// Per-file FILE KEY (owner ruling 2026-07-10) — filename + `file_id:` SSOT,
|
|
1213
1442
|
// and (V3) the anchor for `id:` itself.
|
|
1214
|
-
|
|
1215
|
-
|
|
1443
|
+
// Disk occupancy ([[HS-FJWL]]): guarded by the same `writeMode === 'cloud'`
|
|
1444
|
+
// ternary as the family above — in cloud mode nothing is minted here at all.
|
|
1445
|
+
const fileKey =
|
|
1446
|
+
writeMode === 'cloud'
|
|
1447
|
+
? null
|
|
1448
|
+
: mintFileKey(collectOccupiedKeysEitherKind(vaultRoot), { randomUnit: deps?.randomUnit });
|
|
1216
1449
|
const slug = deriveSlug(name);
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1450
|
+
let diagnoseId = fileKey ? `HS-${fileKey}` : null;
|
|
1451
|
+
let diagnoseBasename = fileKey ? `${diagnoseId}-${familyKey}-DIAG-${slug}` : null;
|
|
1452
|
+
let diagnoseRelPath = fileKey ? `specs/diagnoses/${diagnoseBasename}.md` : null;
|
|
1453
|
+
const diagnoseAbsPath = fileKey
|
|
1454
|
+
? join(vaultRoot, 'specs', 'diagnoses', `${diagnoseBasename}.md`)
|
|
1455
|
+
: null;
|
|
1456
|
+
|
|
1457
|
+
const diagnoseContent = buildDiagnoseContent(
|
|
1458
|
+
diagnoseId ?? CLOUD_PENDING_ID,
|
|
1459
|
+
fileKey ?? CLOUD_PENDING_KEY,
|
|
1460
|
+
familyKey ?? CLOUD_PENDING_KEY,
|
|
1461
|
+
name,
|
|
1462
|
+
severity,
|
|
1463
|
+
capture.basename,
|
|
1464
|
+
coreIdea,
|
|
1465
|
+
now,
|
|
1466
|
+
);
|
|
1220
1467
|
|
|
1221
1468
|
const writes = [];
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1469
|
+
if (writeMode === 'cloud') {
|
|
1470
|
+
// CREATE FIRST — the capture's `promoted_to:` links to the basename this
|
|
1471
|
+
// POST returns, so it cannot be built until the POST has answered.
|
|
1472
|
+
const { frontmatter, body } = splitRawSpecForConsole(diagnoseContent);
|
|
1473
|
+
const cleaned = stripServerMintedFields(frontmatter);
|
|
1474
|
+
delete cleaned.family; // never inherited on this hop; let the server mint it
|
|
1475
|
+
let created;
|
|
1476
|
+
try {
|
|
1477
|
+
created = await createSpecViaConsole({
|
|
1478
|
+
...cloudDeps,
|
|
1479
|
+
type: 'diagnose',
|
|
1480
|
+
name,
|
|
1481
|
+
frontmatter: cleaned,
|
|
1482
|
+
body,
|
|
1483
|
+
actingAgent: deps?.actingAgent ?? null,
|
|
1484
|
+
});
|
|
1485
|
+
} catch (err) {
|
|
1486
|
+
return cloudWriteFailureResult(err, {
|
|
1487
|
+
step: { kind: 'create', type: 'diagnose', name },
|
|
1488
|
+
completed: cloudCompleted,
|
|
1489
|
+
});
|
|
1490
|
+
}
|
|
1491
|
+
diagnoseId = created.specId;
|
|
1492
|
+
diagnoseBasename = created.basename;
|
|
1493
|
+
diagnoseRelPath = created.relPath;
|
|
1494
|
+
cloudCompleted.push({
|
|
1495
|
+
kind: 'create',
|
|
1496
|
+
type: 'diagnose',
|
|
1497
|
+
spec_id: created.specId,
|
|
1498
|
+
basename: created.basename,
|
|
1499
|
+
rel_path: created.relPath,
|
|
1500
|
+
});
|
|
1501
|
+
} else {
|
|
1502
|
+
writes.push({ kind: 'create', absPath: diagnoseAbsPath, content: diagnoseContent });
|
|
1503
|
+
}
|
|
1227
1504
|
|
|
1228
1505
|
const captureUpdate = buildUpdatedCaptureContentForDiagnose(capture.raw, {
|
|
1229
1506
|
diagnoseBasename,
|
|
@@ -1232,16 +1509,50 @@ export async function executePromoteCaptureToDiagnose(vaultRoot, args = {}, deps
|
|
|
1232
1509
|
author: 'promote-capture (agent)',
|
|
1233
1510
|
});
|
|
1234
1511
|
if (captureUpdate.changed) {
|
|
1235
|
-
|
|
1512
|
+
if (writeMode === 'cloud') {
|
|
1513
|
+
// …LINK SECOND.
|
|
1514
|
+
const { frontmatter, body } = splitRawSpecForConsole(captureUpdate.content);
|
|
1515
|
+
try {
|
|
1516
|
+
const res = await updateSpecViaConsole({
|
|
1517
|
+
...cloudDeps,
|
|
1518
|
+
specId: getScalarField(getFrontmatterBlock(capture.raw)?.block ?? '', 'id') ?? capture.basename,
|
|
1519
|
+
frontmatter,
|
|
1520
|
+
body,
|
|
1521
|
+
});
|
|
1522
|
+
cloudCompleted.push({
|
|
1523
|
+
kind: 'update',
|
|
1524
|
+
type: 'capture',
|
|
1525
|
+
spec_id: res.specId,
|
|
1526
|
+
basename: res.basename ?? capture.basename,
|
|
1527
|
+
rel_path: res.relPath,
|
|
1528
|
+
});
|
|
1529
|
+
} catch (err) {
|
|
1530
|
+
return cloudWriteFailureResult(err, {
|
|
1531
|
+
step: { kind: 'update', type: 'capture', basename: capture.basename },
|
|
1532
|
+
completed: cloudCompleted,
|
|
1533
|
+
});
|
|
1534
|
+
}
|
|
1535
|
+
} else {
|
|
1536
|
+
writes.push({ kind: 'update', absPath: capture.absPath, content: captureUpdate.content });
|
|
1537
|
+
}
|
|
1236
1538
|
}
|
|
1237
1539
|
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1540
|
+
if (writeMode !== 'cloud') {
|
|
1541
|
+
const commitResult = commitWrites(writes);
|
|
1542
|
+
if (!commitResult.ok) {
|
|
1543
|
+
return {
|
|
1544
|
+
error: 'WRITE_FAILED',
|
|
1545
|
+
message: commitResult.error?.message ?? 'unknown write failure',
|
|
1546
|
+
rollback_error: commitResult.error?.rollbackError,
|
|
1547
|
+
};
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
// By this point every path (local mint or cloud create-first) has replaced
|
|
1552
|
+
// the transitional fileKey-pending null with a real id — narrow it here so
|
|
1553
|
+
// callers (e.g. `promote-diagnose.ts`) see `string`, not `string | null`.
|
|
1554
|
+
if (!diagnoseId || !diagnoseBasename || !diagnoseRelPath) {
|
|
1555
|
+
throw new Error('promoteCaptureToDiagnose: missing diagnose id after promotion (unexpected)');
|
|
1245
1556
|
}
|
|
1246
1557
|
|
|
1247
1558
|
return {
|
|
@@ -1257,7 +1568,11 @@ export async function executePromoteCaptureToDiagnose(vaultRoot, args = {}, deps
|
|
|
1257
1568
|
informs_diagnose_added: captureUpdate.changed,
|
|
1258
1569
|
promoted_to_set: captureUpdate.setPromotedTo,
|
|
1259
1570
|
},
|
|
1260
|
-
|
|
1571
|
+
...(writeMode === 'cloud' ? { write_mode: 'cloud' } : {}),
|
|
1572
|
+
writes:
|
|
1573
|
+
writeMode === 'cloud'
|
|
1574
|
+
? cloudCompleted.map((w) => ({ kind: w.kind, path: w.rel_path ?? null, spec_id: w.spec_id }))
|
|
1575
|
+
: writes.map((w) => ({ kind: w.kind, path: w.absPath })),
|
|
1261
1576
|
human_review_required: false,
|
|
1262
1577
|
};
|
|
1263
1578
|
}
|
|
@@ -1266,6 +1581,8 @@ export const __testables = {
|
|
|
1266
1581
|
validateInputShape,
|
|
1267
1582
|
findCaptureByIdLocal,
|
|
1268
1583
|
findResearchByIdLocal,
|
|
1584
|
+
findCaptureByIdViaTier,
|
|
1585
|
+
findResearchByIdViaTier,
|
|
1269
1586
|
findResearchByBasename,
|
|
1270
1587
|
collectOccupiedResearchIds,
|
|
1271
1588
|
collectOccupiedDiagnoseIds,
|