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
|
@@ -46,16 +46,16 @@
|
|
|
46
46
|
* import) so tests can stub scripts/lib/dev-mode.mjs and prove the verdict
|
|
47
47
|
* comes from `resolveDevMode` itself, not from string-matching this file.
|
|
48
48
|
*/
|
|
49
|
-
import {
|
|
50
|
-
import {
|
|
51
|
-
import { getActiveConfig } from './vault-resolve.mjs';
|
|
52
|
-
|
|
53
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
49
|
+
import { join } from 'node:path';
|
|
50
|
+
import { importDevModeCore } from './dev-mode-core.mjs';
|
|
54
51
|
// THIS PACKAGE's own root (vendor/heyspex-mcp/read-path.mjs -> two levels
|
|
55
52
|
// up) — repo root in the monorepo, `node_modules/heyspex` in a packed
|
|
56
53
|
// install. Used to locate this package's OWN sibling code (dev-mode.mjs);
|
|
57
|
-
// see the module doc above for why this is never the vault root.
|
|
58
|
-
|
|
54
|
+
// see the module doc above for why this is never the vault root. Derived in
|
|
55
|
+
// ONE place (package-root.mjs) so a bundler bakes one build-machine path
|
|
56
|
+
// into the console's remote-MCP chunk instead of one per module.
|
|
57
|
+
import { PACKAGE_ROOT } from './package-root.mjs';
|
|
58
|
+
import { getActiveConfig } from './vault-resolve.mjs';
|
|
59
59
|
|
|
60
60
|
/**
|
|
61
61
|
* Resolve the VAULT root via the same shared 5-tier resolver every other MCP
|
|
@@ -78,8 +78,11 @@ export function resolveVaultRoot() {
|
|
|
78
78
|
|
|
79
79
|
/** Real loader: dynamic-imports the shared dev-mode core from THIS PACKAGE's own root. */
|
|
80
80
|
export async function loadRealDevMode() {
|
|
81
|
-
|
|
82
|
-
|
|
81
|
+
// Path candidate first (stdio / packed installs), then the static
|
|
82
|
+
// specifier a bundler can trace — see dev-mode-core.mjs. In a bundled
|
|
83
|
+
// deployment `import.meta.url` no longer points at a real checkout, so the
|
|
84
|
+
// path-built URL misses and the fallback is what actually loads.
|
|
85
|
+
return importDevModeCore([join(PACKAGE_ROOT, 'scripts', 'lib', 'dev-mode.mjs')]);
|
|
83
86
|
}
|
|
84
87
|
|
|
85
88
|
/**
|
|
@@ -111,6 +114,23 @@ export async function resolveReadTier({ env, homeDir, loadDevMode = loadRealDevM
|
|
|
111
114
|
return resolved.readTier;
|
|
112
115
|
}
|
|
113
116
|
|
|
117
|
+
/**
|
|
118
|
+
* resolveVaultWriteMode({ env, homeDir, loadDevMode? }) -> "local" | "cloud"
|
|
119
|
+
*
|
|
120
|
+
* The write-side twin of {@link resolveReadTier}: whatever
|
|
121
|
+
* `resolveDevMode(...).vaultWriteMode` says — `local` only for dev mode with
|
|
122
|
+
* local reliance, `cloud` for prod and for dev with cloud reliance. A
|
|
123
|
+
* create-flow that writes the vault directly (save_research) consults THIS
|
|
124
|
+
* before touching disk, so a cloud-reliant session never leaves a file the
|
|
125
|
+
* console's index has never heard of (the split-brain a disk write in cloud
|
|
126
|
+
* mode produces). Same injectable loader as the read tier.
|
|
127
|
+
*/
|
|
128
|
+
export async function resolveVaultWriteMode({ env, homeDir, loadDevMode = loadRealDevMode } = {}) {
|
|
129
|
+
const mod = await loadDevMode();
|
|
130
|
+
const resolved = mod.resolveDevMode({ env, homeDir });
|
|
131
|
+
return resolved.vaultWriteMode === 'local' ? 'local' : 'cloud';
|
|
132
|
+
}
|
|
133
|
+
|
|
114
134
|
/**
|
|
115
135
|
* readViaSelector({ tier, diskRead, neonRead, routeLabel, consoleUrl })
|
|
116
136
|
*
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// The read-back receipt envelope for `hs` verbs that live in plain .mjs under
|
|
2
|
+
// vendor/heyspex-mcp/ and so cannot import the TypeScript receipt
|
|
3
|
+
// (spex-console/src/lib/cli/receipt.ts, [[HS-STCY-WW9R-TASK-add-a-shared-receipt-that-reads-back]]).
|
|
4
|
+
// Same envelope, same words: `{ local, cloud, verified, agree, diffs, changed,
|
|
5
|
+
// failed?, retry? }`, a two-column "local (this machine) | cloud (console)"
|
|
6
|
+
// table and one "Result:" line with the same anchors (verified / unverified /
|
|
7
|
+
// disagree / failed). Only the side fields differ per verb, so a verb passes a
|
|
8
|
+
// row table naming which local field and which cloud field each row shows
|
|
9
|
+
// and whether the row must agree. `hs login` is the first user
|
|
10
|
+
// ([[HS-CRNE-WW9R-TASK-give-hs-install-and-hs-login-the]], epic
|
|
11
|
+
// [[HS-L6HW-WW9R-EPIC-one-http-transport-and-measured]] invariant 6).
|
|
12
|
+
//
|
|
13
|
+
// Pure: no IO, never throws.
|
|
14
|
+
|
|
15
|
+
/** `EXIT.ACCOUNT_MISMATCH` in spex-console/src/lib/cli/control-shared.ts — the console confirmed another account. */
|
|
16
|
+
export const EXIT_ACCOUNT_MISMATCH = 20;
|
|
17
|
+
/** `EXIT.RECEIPT_DISAGREE` in control-shared.ts — both sides were read back and disagree. */
|
|
18
|
+
export const EXIT_RECEIPT_DISAGREE = 21;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @typedef {{ read: boolean, reason?: string, [field: string]: unknown }} ReceiptSide
|
|
22
|
+
* @typedef {{ label: string, local?: string, cloud?: string, compare?: boolean }} ReceiptRow
|
|
23
|
+
* One table row: the local side's field, the cloud side's field, and
|
|
24
|
+
* whether the two must be equal for `agree` (a diff is named by `label`).
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/** A side that was not read back, with the reason. */
|
|
28
|
+
export function unreadSide(reason) {
|
|
29
|
+
return { read: false, reason };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const META = new Set(['read', 'reason']);
|
|
33
|
+
|
|
34
|
+
function valueOf(side, field) {
|
|
35
|
+
if (!side || !field) return null;
|
|
36
|
+
const v = side[field];
|
|
37
|
+
return v === undefined ? null : v;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Build the receipt from the two read-backs.
|
|
42
|
+
* - `verified`: both sides were read back;
|
|
43
|
+
* - `diffs`: every `compare` row whose two values differ (only when verified);
|
|
44
|
+
* - `agree`: verified with no diffs;
|
|
45
|
+
* - `changed`: per side, every field that differs from the `before` read of
|
|
46
|
+
* that side (`[]` on an unchanged re-run; a side with no before read, or
|
|
47
|
+
* not read after, contributes nothing).
|
|
48
|
+
* @param {{ local: ReceiptSide | null, cloud: ReceiptSide | null, rows: ReceiptRow[],
|
|
49
|
+
* before?: { local?: ReceiptSide | null, cloud?: ReceiptSide | null },
|
|
50
|
+
* failed?: { side: 'local' | 'cloud', reason?: string }, retry?: string }} input
|
|
51
|
+
*/
|
|
52
|
+
export function buildSidesReceipt({ local, cloud, rows, before = {}, failed, retry }) {
|
|
53
|
+
const l = local ?? unreadSide('not read back');
|
|
54
|
+
const c = cloud ?? unreadSide('not read back');
|
|
55
|
+
if (failed?.reason) (failed.side === 'cloud' ? c : l).reason = failed.reason;
|
|
56
|
+
const verified = l.read === true && c.read === true;
|
|
57
|
+
|
|
58
|
+
const diffs = [];
|
|
59
|
+
if (verified) {
|
|
60
|
+
for (const row of rows) {
|
|
61
|
+
if (!row.compare) continue;
|
|
62
|
+
const lv = valueOf(l, row.local);
|
|
63
|
+
const cv = valueOf(c, row.cloud);
|
|
64
|
+
if (lv !== cv) diffs.push({ field: row.label, local: lv, cloud: cv });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const changed = [];
|
|
69
|
+
for (const [name, after] of [['local', l], ['cloud', c]]) {
|
|
70
|
+
const prior = before[name];
|
|
71
|
+
if (!prior?.read || !after.read) continue;
|
|
72
|
+
const fields = new Set([...Object.keys(prior), ...Object.keys(after)].filter((f) => !META.has(f)));
|
|
73
|
+
for (const field of fields) {
|
|
74
|
+
const from = valueOf(prior, field);
|
|
75
|
+
const to = valueOf(after, field);
|
|
76
|
+
if (from !== to) changed.push({ side: name, field, from, to });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
local: l,
|
|
82
|
+
cloud: c,
|
|
83
|
+
verified,
|
|
84
|
+
agree: verified && diffs.length === 0,
|
|
85
|
+
diffs,
|
|
86
|
+
changed,
|
|
87
|
+
...(failed ? { failed: failed.side } : {}),
|
|
88
|
+
...(retry ? { retry } : {}),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** The one-word state a script or test can branch on (same as receipt.ts `receiptStatus`). */
|
|
93
|
+
export function sidesReceiptStatus(receipt) {
|
|
94
|
+
if (receipt.failed) return 'failed';
|
|
95
|
+
if (!receipt.verified) return 'unverified';
|
|
96
|
+
if (!receipt.agree) return 'disagree';
|
|
97
|
+
return 'verified';
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function cell(side, field) {
|
|
101
|
+
if (!field) return '';
|
|
102
|
+
if (!side.read) return '(not read)';
|
|
103
|
+
const v = valueOf(side, field);
|
|
104
|
+
if (v === null) return '(none)';
|
|
105
|
+
if (typeof v === 'boolean') return v ? 'yes' : 'no';
|
|
106
|
+
return String(v);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const show = (v) => (v === null || v === undefined ? '(none)' : String(v));
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The text sink: the same two-column table and "Result:" line as receipt.ts
|
|
113
|
+
* `formatReceipt`, rows taken from `rows`.
|
|
114
|
+
* @param {ReturnType<typeof buildSidesReceipt>} receipt
|
|
115
|
+
* @param {ReceiptRow[]} rows
|
|
116
|
+
*/
|
|
117
|
+
export function formatSidesReceipt(receipt, rows) {
|
|
118
|
+
const table = [
|
|
119
|
+
['', 'local (this machine)', 'cloud (console)'],
|
|
120
|
+
...rows.map((r) => [r.label, cell(receipt.local, r.local), cell(receipt.cloud, r.cloud)]),
|
|
121
|
+
];
|
|
122
|
+
const w0 = Math.max(...table.map((r) => r[0].length));
|
|
123
|
+
const w1 = Math.max(...table.map((r) => r[1].length));
|
|
124
|
+
const lines = [
|
|
125
|
+
'Read back after the write:',
|
|
126
|
+
...table.map(([a, b, c]) => ` ${a.padEnd(w0)} ${b.padEnd(w1)} ${c}`.trimEnd()),
|
|
127
|
+
];
|
|
128
|
+
const status = sidesReceiptStatus(receipt);
|
|
129
|
+
const retry = receipt.retry ? ` Retry: \`${receipt.retry}\`` : '';
|
|
130
|
+
if (status === 'failed') {
|
|
131
|
+
const reason = receipt[receipt.failed].reason;
|
|
132
|
+
lines.push(`Result: failed -- the ${receipt.failed} half did not land${reason ? ` (${reason})` : ''}.${retry}`);
|
|
133
|
+
} else if (status === 'unverified') {
|
|
134
|
+
const unread = ['local', 'cloud'].filter((s) => !receipt[s].read);
|
|
135
|
+
const why = unread.map((s) => `${s}${receipt[s].reason ? `: ${receipt[s].reason}` : ''}`).join('; ');
|
|
136
|
+
lines.push(`Result: unverified -- the ${unread.join(' and ')} side was not read back (${why}).`);
|
|
137
|
+
} else if (status === 'disagree') {
|
|
138
|
+
lines.push(
|
|
139
|
+
`Result: the two sides disagree on ${receipt.diffs.map((d) => d.field).join(', ')}.${retry}`,
|
|
140
|
+
...receipt.diffs.map((d) => ` ${d.field}: local ${show(d.local)} | cloud ${show(d.cloud)}`),
|
|
141
|
+
);
|
|
142
|
+
} else {
|
|
143
|
+
lines.push('Result: verified -- both sides were read back and agree.');
|
|
144
|
+
}
|
|
145
|
+
return lines.join('\n');
|
|
146
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// heyspex_start_recipe and heyspex_list_recipes handlers (recipe-tools.mjs holds the store). Extracted from cli.mjs under
|
|
2
|
+
// [[HS-BLNF-6W61-DIAG-cli-mjs-has-grown-into-a-13-665-line]] (no behavior change).
|
|
3
|
+
|
|
4
|
+
import { wrapDesignResult } from './tool-helpers.mjs';
|
|
5
|
+
import { executeListRecipes, executeStartRecipe } from './recipe-tools.mjs';
|
|
6
|
+
import { detectAgentIdentity } from './mcp-config.mjs';
|
|
7
|
+
import { consoleErrorMessage } from './console-fetch.mjs';
|
|
8
|
+
import { createCaptureSessionOnConsole, writeCaptureFocusVendor } from './capture-tools.mjs';
|
|
9
|
+
|
|
10
|
+
// heyspex_list_recipes — pure catalog scan wrapped for the tool surface.
|
|
11
|
+
export function handleListRecipes(vaultRoot, args) {
|
|
12
|
+
return wrapDesignResult(executeListRecipes(vaultRoot, args));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// heyspex_start_recipe — resolve the recipe (pure) then, when the console is
|
|
16
|
+
// reachable, create + pin a capture session tied to it (mirrors
|
|
17
|
+
// handlePinCapture). The recipe start context is returned in every case so the
|
|
18
|
+
// flow can be walked even when the capture could not be created (offline).
|
|
19
|
+
export async function handleStartRecipe(vaultRoot, args) {
|
|
20
|
+
const resolved = executeStartRecipe(vaultRoot, args);
|
|
21
|
+
if (resolved.error) {
|
|
22
|
+
return {
|
|
23
|
+
isError: true,
|
|
24
|
+
content: [{ type: 'text', text: JSON.stringify(resolved, null, 2) }],
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
const recipe = resolved.recipe;
|
|
28
|
+
const identity = detectAgentIdentity();
|
|
29
|
+
|
|
30
|
+
const createResult = await createCaptureSessionOnConsole({
|
|
31
|
+
opened_by: 'agent',
|
|
32
|
+
opened_by_session: identity.sessionId,
|
|
33
|
+
mode: 'attended',
|
|
34
|
+
status: 'pending',
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
if (createResult.ok && createResult.session) {
|
|
38
|
+
const session = createResult.session;
|
|
39
|
+
const pin = await writeCaptureFocusVendor(vaultRoot, {
|
|
40
|
+
specId: session.id,
|
|
41
|
+
specPath: session.specPath || '',
|
|
42
|
+
reason: `Started workflow-recipe ${recipe.recipe_id} (${recipe.name})`,
|
|
43
|
+
claim: { claimed_by: identity.agent, claim_session: identity.sessionId },
|
|
44
|
+
});
|
|
45
|
+
if (!pin.pinned) {
|
|
46
|
+
// The session row WAS created and the recipe context is intact — both are
|
|
47
|
+
// returned so nothing is lost and no second session gets minted. But the
|
|
48
|
+
// message may not say "pinned" over a pin that never persisted
|
|
49
|
+
// ([[HS-IOJW-5YDH-DIAG]] sweep: report the result, not the request).
|
|
50
|
+
return {
|
|
51
|
+
isError: true,
|
|
52
|
+
content: [{
|
|
53
|
+
type: 'text',
|
|
54
|
+
text: JSON.stringify({
|
|
55
|
+
started: true,
|
|
56
|
+
pinned: false,
|
|
57
|
+
error: 'CAPTURE_PIN_NOT_WRITTEN',
|
|
58
|
+
source: 'database',
|
|
59
|
+
capture: { spec_id: session.id, spec_path: session.specPath || null },
|
|
60
|
+
pin,
|
|
61
|
+
recipe,
|
|
62
|
+
message:
|
|
63
|
+
`Capture session ${session.id} was created for workflow-recipe ${recipe.recipe_id}, but the ` +
|
|
64
|
+
`capture-on-deck pin was NOT written (${pin.reason}). A later /hs-capture with no id will ` +
|
|
65
|
+
'resolve to a DIFFERENT capture. Do not re-run heyspex_start_recipe (it would mint a second ' +
|
|
66
|
+
`session) — re-pin with heyspex_pin_capture({ spec_id: "${session.id}" }) once the console is healthy.`,
|
|
67
|
+
}, null, 2),
|
|
68
|
+
}],
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
content: [{
|
|
73
|
+
type: 'text',
|
|
74
|
+
text: JSON.stringify({
|
|
75
|
+
started: true,
|
|
76
|
+
pinned: true,
|
|
77
|
+
pin,
|
|
78
|
+
source: 'database',
|
|
79
|
+
capture: { spec_id: session.id, spec_path: session.specPath || null },
|
|
80
|
+
recipe,
|
|
81
|
+
message: `Capture session ${session.id} created and pinned (${pin.tier}) for workflow-recipe ${recipe.recipe_id}. Walk the capture_sequence, then file the spec pre-filled from spec_template (recipe_id + tool_scope stamp provenance).`,
|
|
82
|
+
}, null, 2),
|
|
83
|
+
}],
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// The create failed → the recipe context is still fully returned so the flow
|
|
88
|
+
// can be walked / filed later. No divergent local capture file is minted
|
|
89
|
+
// (capture is online-only, [[heyspex-is-online-only-no-offline]]).
|
|
90
|
+
//
|
|
91
|
+
// HS-I6HT: this used to hardcode "HeySpex can't reach the server right now"
|
|
92
|
+
// for EVERY failure, so a sign-in failure printed an outage sentence next to
|
|
93
|
+
// a CONSOLE_NOT_SIGNED_IN code — two contradictory stories in one payload.
|
|
94
|
+
// `consoleErrorMessage` already writes the right sentence per class (sign-in,
|
|
95
|
+
// 401, wrong port, timeout, genuinely down); use it whenever it has one, and
|
|
96
|
+
// keep the old wording only as the last-resort fallback.
|
|
97
|
+
const answered =
|
|
98
|
+
typeof createResult.result?.body?.message === 'string'
|
|
99
|
+
? createResult.result.body.message
|
|
100
|
+
: null;
|
|
101
|
+
const whatHappened =
|
|
102
|
+
(createResult.result ? consoleErrorMessage(createResult.result, answered) : null)
|
|
103
|
+
|| "HeySpex can't reach the server right now.";
|
|
104
|
+
return {
|
|
105
|
+
content: [{
|
|
106
|
+
type: 'text',
|
|
107
|
+
text: JSON.stringify({
|
|
108
|
+
started: false,
|
|
109
|
+
status: 'deferred',
|
|
110
|
+
error: createResult.error || 'CONSOLE_UNREACHABLE',
|
|
111
|
+
recipe,
|
|
112
|
+
message:
|
|
113
|
+
`${whatHappened} The capture wasn't created, but the recipe's steps are returned below and nothing is lost. Fix the cause above, then call again to pin the capture.`,
|
|
114
|
+
}, null, 2),
|
|
115
|
+
}],
|
|
116
|
+
};
|
|
117
|
+
}
|
|
@@ -21,7 +21,24 @@
|
|
|
21
21
|
|
|
22
22
|
import { execFileSync } from 'node:child_process';
|
|
23
23
|
import { readFileSync, writeFileSync, mkdirSync, existsSync, renameSync } from 'node:fs';
|
|
24
|
-
import { dirname, join } from 'node:path';
|
|
24
|
+
import { dirname, join, isAbsolute, resolve, sep } from 'node:path';
|
|
25
|
+
import { assertPathUnderRoot } from '../../scripts/lib/shared-path-helpers.mjs';
|
|
26
|
+
// Cloud-mode vault WRITES ([[HS-BYU4-CWPX-TASK-promotion-writes-go-through-the-console]]).
|
|
27
|
+
// Regen has no create-first/link-second reorder — nothing it writes links to a
|
|
28
|
+
// basename another write in the same batch mints — but the same rule applies:
|
|
29
|
+
// a `writeFileSync` under `specs/` in cloud mode is a split brain.
|
|
30
|
+
import {
|
|
31
|
+
createSpecViaConsole,
|
|
32
|
+
updateSpecViaConsole,
|
|
33
|
+
stripServerMintedFields,
|
|
34
|
+
} from './cloud-vault-writes.mjs';
|
|
35
|
+
import {
|
|
36
|
+
resolveWriteModeFromDeps,
|
|
37
|
+
splitRawSpecForConsole,
|
|
38
|
+
cloudWriteFailureResult,
|
|
39
|
+
cloudModeUnsupportedWrite,
|
|
40
|
+
} from './promotion-helpers.mjs';
|
|
41
|
+
|
|
25
42
|
|
|
26
43
|
const TERMINAL_STATUSES = new Set([
|
|
27
44
|
'closed',
|
|
@@ -49,6 +66,11 @@ export function softDeleteToTrash(vaultRoot, relPath, bucket = 'misc') {
|
|
|
49
66
|
if (!existsSync(abs)) return { ok: false, error: 'not_found', source: relPath };
|
|
50
67
|
const basename = relPath.split(/[\\/]/).pop() || `${timestamp()}-spec.md`;
|
|
51
68
|
const trashDir = join(vaultRoot, 'specs', '.trash', bucket);
|
|
69
|
+
try {
|
|
70
|
+
assertPathUnderRoot(vaultRoot, trashDir, 'regen-tasks write');
|
|
71
|
+
} catch (err) {
|
|
72
|
+
return { ok: false, error: 'BAD_VAULT_ROOT', source: relPath, message: err.message };
|
|
73
|
+
}
|
|
52
74
|
mkdirSync(trashDir, { recursive: true });
|
|
53
75
|
const dest = join(trashDir, `${timestamp()}-${basename}`);
|
|
54
76
|
renameSync(abs, dest);
|
|
@@ -136,7 +158,77 @@ function epicSlugFromRel(relPath) {
|
|
|
136
158
|
*
|
|
137
159
|
* Both modes return `{ ok, mode, task, epic, brief?, commit_result? }`.
|
|
138
160
|
*/
|
|
139
|
-
|
|
161
|
+
/**
|
|
162
|
+
* The cloud twin of the `.trash/` soft-delete — EXPLICIT DEGRADE.
|
|
163
|
+
*
|
|
164
|
+
* `softDeleteToTrash` renames a file into `specs/.trash/<bucket>/`, which is
|
|
165
|
+
* not a spec of any type the console models (no TYPE_TO_FOLDER entry, no id,
|
|
166
|
+
* no frontmatter contract), so there is no POST/PUT route that could carry it.
|
|
167
|
+
* In cloud mode the replacement still happens — `PUT /api/specs/:id` overwrites
|
|
168
|
+
* the body and the console keeps its own version history — but the local
|
|
169
|
+
* `.trash/` copy the [[destructive-action-checklist]] recovery procedure names
|
|
170
|
+
* does not exist. That is REPORTED on every affected entry rather than
|
|
171
|
+
* silently skipped, so the caller knows which recovery path applies.
|
|
172
|
+
*/
|
|
173
|
+
function cloudTrashDegrade(relPath) {
|
|
174
|
+
return cloudModeUnsupportedWrite(
|
|
175
|
+
`specs/.trash/… (soft-delete of ${relPath})`,
|
|
176
|
+
'a trash copy is not a spec of any type the console models, so there is no route to write it; the replacement itself still landed through PUT /api/specs/:id and the console keeps its own version history',
|
|
177
|
+
{ recovery: 'Restore the previous body from the console\'s spec history rather than from a local .trash/ file.' },
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Replace one existing spec's body through the console.
|
|
183
|
+
* `new_body` is the agent-authored full markdown (frontmatter + body), the
|
|
184
|
+
* same string the disk path writes verbatim.
|
|
185
|
+
*/
|
|
186
|
+
async function putBodyViaConsole(specId, newBody, cloudDeps) {
|
|
187
|
+
const { frontmatter, body } = splitRawSpecForConsole(newBody);
|
|
188
|
+
return updateSpecViaConsole({ ...cloudDeps, specId, frontmatter, body });
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Cloud-mode `executeRegenTask` commit. Same result shape as the disk path
|
|
193
|
+
* plus `write_mode: 'cloud'` and the `.trash/` degrade note.
|
|
194
|
+
*/
|
|
195
|
+
async function regenTaskCommitViaConsole({ taskSummary, epicSummary, new_body, retire_reason, cloudDeps }) {
|
|
196
|
+
let res;
|
|
197
|
+
try {
|
|
198
|
+
res = await putBodyViaConsole(taskSummary.id, new_body, cloudDeps);
|
|
199
|
+
} catch (err) {
|
|
200
|
+
return {
|
|
201
|
+
ok: false,
|
|
202
|
+
...cloudWriteFailureResult(err, {
|
|
203
|
+
step: { kind: 'update', type: 'task', spec_id: taskSummary.id },
|
|
204
|
+
completed: [],
|
|
205
|
+
}),
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
return {
|
|
209
|
+
ok: true,
|
|
210
|
+
mode: 'commit',
|
|
211
|
+
write_mode: 'cloud',
|
|
212
|
+
task: { id: res.specId ?? taskSummary.id, rel_path: res.relPath ?? taskSummary.rel_path },
|
|
213
|
+
retired_to: null,
|
|
214
|
+
retire_reason: retire_reason || null,
|
|
215
|
+
epic: epicSummary ? { id: epicSummary.id } : null,
|
|
216
|
+
cloud_mode_unsupported_writes: [cloudTrashDegrade(taskSummary.rel_path ?? taskSummary.id)],
|
|
217
|
+
message:
|
|
218
|
+
`Replaced the body of ${taskSummary.id} through the console (cloud write mode — nothing was written to local disk). ` +
|
|
219
|
+
'No local .trash/ copy was made; recover the previous body from the console\'s spec history.',
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function executeRegenTask(vaultRoot, { task_id, commit = false, new_body, retire_reason, loadTask, writeMode, consoleFetch, consoleUrl }) {
|
|
224
|
+
// Vault WRITE mode ([[HS-BYU4-CWPX-TASK-promotion-writes-go-through-the-console]]),
|
|
225
|
+
// resolved ONCE by the caller and defaulting to `local`. This function stays
|
|
226
|
+
// SYNCHRONOUS on the local path on purpose — `spec-edit-tools.mjs` consumes
|
|
227
|
+
// its return value synchronously — and returns a Promise only in cloud mode,
|
|
228
|
+
// which a cloud-aware caller awaits (awaiting the plain object the local
|
|
229
|
+
// path returns is a no-op, so one call site serves both).
|
|
230
|
+
const cloudWriteMode = resolveWriteModeFromDeps({ writeMode });
|
|
231
|
+
const cloudDeps = { consoleFetch, consoleUrl };
|
|
140
232
|
if (!task_id) {
|
|
141
233
|
return { ok: false, error: 'MISSING_TASK_ID', message: 'task_id is required' };
|
|
142
234
|
}
|
|
@@ -159,6 +251,15 @@ export function executeRegenTask(vaultRoot, { task_id, commit = false, new_body,
|
|
|
159
251
|
message: 'commit: true requires non-empty new_body',
|
|
160
252
|
};
|
|
161
253
|
}
|
|
254
|
+
if (cloudWriteMode === 'cloud') {
|
|
255
|
+
return regenTaskCommitViaConsole({
|
|
256
|
+
taskSummary,
|
|
257
|
+
epicSummary,
|
|
258
|
+
new_body,
|
|
259
|
+
retire_reason,
|
|
260
|
+
cloudDeps,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
162
263
|
if (!taskSummary.rel_path) {
|
|
163
264
|
return { ok: false, error: 'NO_REL_PATH', task_id };
|
|
164
265
|
}
|
|
@@ -172,6 +273,11 @@ export function executeRegenTask(vaultRoot, { task_id, commit = false, new_body,
|
|
|
172
273
|
// it — that's an agent error and surfaces as a malformed spec at the
|
|
173
274
|
// next /hs-next.
|
|
174
275
|
const destAbs = join(vaultRoot, taskSummary.rel_path);
|
|
276
|
+
try {
|
|
277
|
+
assertPathUnderRoot(vaultRoot, dirname(destAbs), 'regen-tasks write');
|
|
278
|
+
} catch (err) {
|
|
279
|
+
return { ok: false, error: 'BAD_VAULT_ROOT', task_id, message: err.message };
|
|
280
|
+
}
|
|
175
281
|
mkdirSync(dirname(destAbs), { recursive: true });
|
|
176
282
|
writeFileSync(destAbs, new_body, 'utf8');
|
|
177
283
|
return {
|
|
@@ -222,7 +328,93 @@ export function executeRegenTask(vaultRoot, { task_id, commit = false, new_body,
|
|
|
222
328
|
* The tool is deliberately deterministic — the LLM thinking happens between
|
|
223
329
|
* brief mode and commit mode in the calling agent.
|
|
224
330
|
*/
|
|
225
|
-
|
|
331
|
+
/**
|
|
332
|
+
* Cloud-mode `executeRegenTasksForEpic` commit — the same two entry shapes the
|
|
333
|
+
* disk path takes, routed through the console:
|
|
334
|
+
*
|
|
335
|
+
* `{ id, new_body, retire_reason }` → `PUT /api/specs/:id` (replace in place;
|
|
336
|
+
* no local `.trash/` copy — reported, see {@link cloudTrashDegrade}).
|
|
337
|
+
* `{ new_id, body, rel_path }` → `POST /api/specs` (the caller-supplied
|
|
338
|
+
* `new_id`/`rel_path` are IGNORED: the console mints the id, the family
|
|
339
|
+
* and the filename, so nothing is hand-picked — HS-BYU4 AC3. The minted
|
|
340
|
+
* id is reported back in `wrote[]`.)
|
|
341
|
+
*/
|
|
342
|
+
async function regenTasksForEpicCommitViaConsole({ epicSummary, task_specs, loadTask, cloudDeps }) {
|
|
343
|
+
const retired = [];
|
|
344
|
+
const wrote = [];
|
|
345
|
+
const errors = [];
|
|
346
|
+
const unsupported = [];
|
|
347
|
+
for (const entry of task_specs) {
|
|
348
|
+
if (entry && typeof entry === 'object' && entry.id && typeof entry.new_body === 'string') {
|
|
349
|
+
const target = loadSpecRecord(loadTask, entry.id);
|
|
350
|
+
const specId = target?.id || target?.frontmatter?.id || entry.id;
|
|
351
|
+
try {
|
|
352
|
+
const res = await putBodyViaConsole(specId, entry.new_body, cloudDeps);
|
|
353
|
+
retired.push({ id: entry.id, trash_path: null, retire_reason: entry.retire_reason || null });
|
|
354
|
+
unsupported.push(cloudTrashDegrade(target?.relPath || target?.rel_path || entry.id));
|
|
355
|
+
wrote.push({ id: res.specId ?? specId, rel_path: res.relPath ?? null });
|
|
356
|
+
} catch (err) {
|
|
357
|
+
errors.push({
|
|
358
|
+
id: entry.id,
|
|
359
|
+
...cloudWriteFailureResult(err, {
|
|
360
|
+
step: { kind: 'update', type: 'task', spec_id: specId },
|
|
361
|
+
completed: wrote,
|
|
362
|
+
}),
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
} else if (entry && typeof entry === 'object' && entry.new_id && typeof entry.body === 'string') {
|
|
366
|
+
const { frontmatter, body } = splitRawSpecForConsole(entry.body);
|
|
367
|
+
const cleaned = stripServerMintedFields(frontmatter);
|
|
368
|
+
try {
|
|
369
|
+
const created = await createSpecViaConsole({
|
|
370
|
+
...cloudDeps,
|
|
371
|
+
type: cleaned.type ?? frontmatter.type ?? 'task',
|
|
372
|
+
name: cleaned.name ?? frontmatter.name ?? String(entry.new_id),
|
|
373
|
+
summary: cleaned.summary ?? null,
|
|
374
|
+
frontmatter: cleaned,
|
|
375
|
+
body,
|
|
376
|
+
});
|
|
377
|
+
wrote.push({
|
|
378
|
+
id: created.specId,
|
|
379
|
+
rel_path: created.relPath,
|
|
380
|
+
requested_id: entry.new_id,
|
|
381
|
+
note:
|
|
382
|
+
'The console minted this id/filename server-side; the caller-supplied `new_id`/`rel_path` were not used (file keys are never hand-picked).',
|
|
383
|
+
});
|
|
384
|
+
} catch (err) {
|
|
385
|
+
errors.push({
|
|
386
|
+
new_id: entry.new_id,
|
|
387
|
+
...cloudWriteFailureResult(err, {
|
|
388
|
+
step: { kind: 'create', type: 'task', name: entry.new_id },
|
|
389
|
+
completed: wrote,
|
|
390
|
+
}),
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
} else {
|
|
394
|
+
errors.push({ entry, error: 'INVALID_SHAPE' });
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
return {
|
|
398
|
+
ok: errors.length === 0,
|
|
399
|
+
mode: 'commit',
|
|
400
|
+
write_mode: 'cloud',
|
|
401
|
+
epic: { id: epicSummary.id },
|
|
402
|
+
retired,
|
|
403
|
+
wrote,
|
|
404
|
+
errors,
|
|
405
|
+
bucket: null,
|
|
406
|
+
...(unsupported.length > 0 ? { cloud_mode_unsupported_writes: unsupported } : {}),
|
|
407
|
+
message:
|
|
408
|
+
`Stage-1-2-3 commit for ${epicSummary.id} through the console: replaced ${retired.length}, wrote ${wrote.length}, errors ${errors.length}. ` +
|
|
409
|
+
'Cloud write mode — nothing was written to local disk and no .trash/ copies were made; recover previous bodies from the console\'s spec history.',
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
export function executeRegenTasksForEpic(vaultRoot, { epic_id, commit = false, task_specs, loadTask, writeMode, consoleFetch, consoleUrl }) {
|
|
414
|
+
// Same once-per-call resolution + sync-unless-cloud contract as
|
|
415
|
+
// {@link executeRegenTask} above.
|
|
416
|
+
const cloudWriteMode = resolveWriteModeFromDeps({ writeMode });
|
|
417
|
+
const cloudDeps = { consoleFetch, consoleUrl };
|
|
226
418
|
if (!epic_id) {
|
|
227
419
|
return { ok: false, error: 'MISSING_EPIC_ID', message: 'epic_id is required' };
|
|
228
420
|
}
|
|
@@ -255,6 +447,9 @@ export function executeRegenTasksForEpic(vaultRoot, { epic_id, commit = false, t
|
|
|
255
447
|
message: 'commit: true requires task_specs: [{ id, new_body, retire_reason } | { new_id, body, rel_path }]',
|
|
256
448
|
};
|
|
257
449
|
}
|
|
450
|
+
if (cloudWriteMode === 'cloud') {
|
|
451
|
+
return regenTasksForEpicCommitViaConsole({ epicSummary, task_specs, loadTask, cloudDeps });
|
|
452
|
+
}
|
|
258
453
|
const bucket = epicSlugFromRel(epicSummary.rel_path);
|
|
259
454
|
const retired = [];
|
|
260
455
|
const wrote = [];
|
|
@@ -273,6 +468,12 @@ export function executeRegenTasksForEpic(vaultRoot, { epic_id, commit = false, t
|
|
|
273
468
|
continue;
|
|
274
469
|
}
|
|
275
470
|
const abs = join(vaultRoot, rel);
|
|
471
|
+
try {
|
|
472
|
+
assertPathUnderRoot(vaultRoot, dirname(abs), 'regen-tasks write');
|
|
473
|
+
} catch (err) {
|
|
474
|
+
errors.push({ id: entry.id, error: 'BAD_VAULT_ROOT', message: err.message });
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
276
477
|
mkdirSync(dirname(abs), { recursive: true });
|
|
277
478
|
writeFileSync(abs, entry.new_body, 'utf8');
|
|
278
479
|
retired.push({ id: entry.id, trash_path: del.trash_path, retire_reason: entry.retire_reason || null });
|
|
@@ -283,6 +484,12 @@ export function executeRegenTasksForEpic(vaultRoot, { epic_id, commit = false, t
|
|
|
283
484
|
errors.push({ new_id: entry.new_id, error: 'CONFLICT', rel_path: entry.rel_path });
|
|
284
485
|
continue;
|
|
285
486
|
}
|
|
487
|
+
try {
|
|
488
|
+
assertPathUnderRoot(vaultRoot, dirname(abs), 'regen-tasks write');
|
|
489
|
+
} catch (err) {
|
|
490
|
+
errors.push({ new_id: entry.new_id, error: 'BAD_VAULT_ROOT', message: err.message });
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
286
493
|
mkdirSync(dirname(abs), { recursive: true });
|
|
287
494
|
writeFileSync(abs, entry.body, 'utf8');
|
|
288
495
|
wrote.push({ id: entry.new_id, rel_path: entry.rel_path });
|