@duckmind/dm-windows-x64 0.61.4 → 0.61.9
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/dm.exe +0 -0
- package/extensions/.dm-extensions.json +211 -67
- package/extensions/dm-subagents/agents/claude-code-writer.md +15 -0
- package/extensions/dm-subagents/agents/claude-code.md +15 -0
- package/extensions/dm-subagents/agents/codex-exec-writer.md +15 -0
- package/extensions/dm-subagents/agents/codex-exec.md +15 -0
- package/extensions/dm-subagents/agents/cursor-agent-writer.md +14 -0
- package/extensions/dm-subagents/agents/cursor-agent.md +14 -0
- package/extensions/dm-subagents/agents/delegate.md +3 -2
- package/extensions/dm-subagents/agents/oracle.md +10 -5
- package/extensions/dm-subagents/agents/researcher.md +2 -2
- package/extensions/dm-subagents/agents/reviewer.md +17 -7
- package/extensions/dm-subagents/agents/scout.md +5 -5
- package/extensions/dm-subagents/agents/worker.md +6 -2
- package/extensions/dm-subagents/async-retention-discovery-worker.mjs +167 -0
- package/extensions/dm-subagents/index.js +4 -0
- package/extensions/dm-subagents/inspector-runner.mjs +10 -0
- package/extensions/dm-subagents/install.mjs +3 -2
- package/extensions/dm-subagents/package.json +2 -2
- package/extensions/dm-subagents/prompts/council.md +60 -0
- package/extensions/dm-subagents/prompts/parallel-review.md +5 -1
- package/extensions/dm-subagents/prompts/review-loop.md +13 -7
- package/extensions/dm-subagents/skills/council-mode/SKILL.md +59 -0
- package/extensions/dm-subagents/skills/council-mode/references/pass-contracts.md +150 -0
- package/extensions/dm-subagents/skills/dm-subagents/SKILL.md +96 -911
- package/extensions/dm-subagents/skills/dm-subagents/references/constraints-and-recipes.md +70 -0
- package/extensions/dm-subagents/skills/dm-subagents/references/execution-controls.md +539 -0
- package/extensions/dm-subagents/skills/dm-subagents/references/management-authoring-rpc.md +161 -0
- package/extensions/dm-subagents/skills/dm-subagents/references/multi-lane-orchestration.md +51 -0
- package/extensions/dm-subagents/skills/dm-subagents/references/prompting-and-roles.md +295 -0
- package/extensions/dm-subagents/skills/dm-subagents/references/review-and-validation.md +73 -0
- package/extensions/dm-subagents/src/agents/agent-management.js +716 -484
- package/extensions/dm-subagents/src/agents/agent-refinements.js +563 -0
- package/extensions/dm-subagents/src/agents/agent-serializer.js +70 -5
- package/extensions/dm-subagents/src/agents/agents.js +1447 -299
- package/extensions/dm-subagents/src/agents/builtin-names.js +15 -0
- package/extensions/dm-subagents/src/agents/chain-serializer.js +12 -7
- package/extensions/dm-subagents/src/agents/frontmatter.js +64 -14
- package/extensions/dm-subagents/src/agents/identity.js +1 -1
- package/extensions/dm-subagents/src/agents/proactive-skills.js +14 -11
- package/extensions/dm-subagents/src/agents/runtime-agent-events.js +49 -0
- package/extensions/dm-subagents/src/agents/runtime-agent-registry.js +412 -0
- package/extensions/dm-subagents/src/agents/skills.js +52 -35
- package/extensions/dm-subagents/src/api/agents.js +6 -0
- package/extensions/dm-subagents/src/api/background-work.js +151 -0
- package/extensions/dm-subagents/src/api/capability-ceiling.js +12 -0
- package/extensions/dm-subagents/src/api/control-channel.js +3 -0
- package/extensions/dm-subagents/src/api/delegation.js +5 -0
- package/extensions/dm-subagents/src/api/dm-args.js +3 -0
- package/extensions/dm-subagents/src/api/external-job-provider.js +137 -0
- package/extensions/dm-subagents/src/api/external-runs.js +233 -0
- package/extensions/dm-subagents/src/api/intercom-bridge.js +3 -0
- package/extensions/dm-subagents/src/api/preflight.js +322 -0
- package/extensions/dm-subagents/src/api/project-panes.js +11 -0
- package/extensions/dm-subagents/src/api/shared-types.js +4 -0
- package/extensions/dm-subagents/src/extension/config.js +175 -0
- package/extensions/dm-subagents/src/extension/control-notices.js +4 -43
- package/extensions/dm-subagents/src/extension/doctor.js +71 -17
- package/extensions/dm-subagents/src/extension/fanout-child.js +49 -32
- package/extensions/dm-subagents/src/extension/index.js +711 -265
- package/extensions/dm-subagents/src/extension/public-execution.js +114 -0
- package/extensions/dm-subagents/src/extension/rpc.js +427 -22
- package/extensions/dm-subagents/src/extension/schemas.js +158 -65
- package/extensions/dm-subagents/src/extension/steering-notices.js +23 -0
- package/extensions/dm-subagents/src/extension/subagent-guide.js +31 -0
- package/extensions/dm-subagents/src/extension/tool-description.js +92 -74
- package/extensions/dm-subagents/src/extension/tool-result.js +7 -0
- package/extensions/dm-subagents/src/inspectors/herdr/actions.js +218 -0
- package/extensions/dm-subagents/src/inspectors/herdr/client.js +123 -0
- package/extensions/dm-subagents/src/inspectors/herdr/focus.js +47 -0
- package/extensions/dm-subagents/src/inspectors/herdr/inspector-runner.js +160 -0
- package/extensions/dm-subagents/src/inspectors/herdr/project-panes.js +618 -0
- package/extensions/dm-subagents/src/inspectors/herdr/session-roots-codec.js +21 -0
- package/extensions/dm-subagents/src/inspectors/herdr/shell-command.js +15 -0
- package/extensions/dm-subagents/src/integrations/herdr-status.js +377 -0
- package/extensions/dm-subagents/src/intercom/intercom-bridge.js +17 -13
- package/extensions/dm-subagents/src/intercom/native-supervisor-channel.js +371 -79
- package/extensions/dm-subagents/src/intercom/result-intercom.js +47 -7
- package/extensions/dm-subagents/src/missions/actions.js +394 -0
- package/extensions/dm-subagents/src/missions/goal-driver.js +149 -0
- package/extensions/dm-subagents/src/missions/lifecycle.js +331 -0
- package/extensions/dm-subagents/src/missions/store.js +548 -0
- package/extensions/dm-subagents/src/missions/types.js +9 -0
- package/extensions/dm-subagents/src/missions/workflow-state.js +245 -0
- package/extensions/dm-subagents/src/policy/authority.js +37 -0
- package/extensions/dm-subagents/src/profiles/profiles.js +36 -18
- package/extensions/dm-subagents/src/runs/background/active-async-capacity.js +427 -0
- package/extensions/dm-subagents/src/runs/background/active-run-index.js +122 -0
- package/extensions/dm-subagents/src/runs/background/async-execution.js +925 -137
- package/extensions/dm-subagents/src/runs/background/async-job-tracker.js +515 -148
- package/extensions/dm-subagents/src/runs/background/async-resume.js +378 -51
- package/extensions/dm-subagents/src/runs/background/async-retention.js +828 -0
- package/extensions/dm-subagents/src/runs/background/async-status-snapshot.js +31 -0
- package/extensions/dm-subagents/src/runs/background/async-status.js +259 -22
- package/extensions/dm-subagents/src/runs/background/auto-drain.js +46 -0
- package/extensions/dm-subagents/src/runs/background/chain-append.js +50 -15
- package/extensions/dm-subagents/src/runs/background/chain-root-attachment.js +67 -12
- package/extensions/dm-subagents/src/runs/background/completion-batcher.js +5 -1
- package/extensions/dm-subagents/src/runs/background/completion-dedupe.js +3 -11
- package/extensions/dm-subagents/src/runs/background/completion-replay.js +245 -0
- package/extensions/dm-subagents/src/runs/background/control-channel.js +423 -36
- package/extensions/dm-subagents/src/runs/background/fleet-view.js +132 -59
- package/extensions/dm-subagents/src/runs/background/index-segment.js +38 -0
- package/extensions/dm-subagents/src/runs/background/inspect-rpc.js +373 -0
- package/extensions/dm-subagents/src/runs/background/notify.js +357 -57
- package/extensions/dm-subagents/src/runs/background/owned-process-tree.js +86 -0
- package/extensions/dm-subagents/src/runs/background/process-terminal.js +269 -0
- package/extensions/dm-subagents/src/runs/background/result-delivery-ownership.js +34 -0
- package/extensions/dm-subagents/src/runs/background/result-files.js +469 -0
- package/extensions/dm-subagents/src/runs/background/result-watcher.js +540 -75
- package/extensions/dm-subagents/src/runs/background/resume-guidance.js +44 -0
- package/extensions/dm-subagents/src/runs/background/retained-children.js +119 -0
- package/extensions/dm-subagents/src/runs/background/run-id-query.js +5 -0
- package/extensions/dm-subagents/src/runs/background/run-id-resolver.js +93 -9
- package/extensions/dm-subagents/src/runs/background/run-status.js +303 -32
- package/extensions/dm-subagents/src/runs/background/scheduled-runs.js +784 -376
- package/extensions/dm-subagents/src/runs/background/stale-run-reconciler.js +75 -34
- package/extensions/dm-subagents/src/runs/background/steering.js +221 -0
- package/extensions/dm-subagents/src/runs/background/subagent-runner.js +3106 -844
- package/extensions/dm-subagents/src/runs/background/subagent-wait.js +529 -0
- package/extensions/dm-subagents/src/runs/background/terminal-run-index.js +106 -0
- package/extensions/dm-subagents/src/runs/background/top-level-async.js +1 -1
- package/extensions/dm-subagents/src/runs/background/wait-completions.js +155 -0
- package/extensions/dm-subagents/src/runs/background/wait-config.js +46 -0
- package/extensions/dm-subagents/src/runs/background/wait-subscriptions.js +278 -0
- package/extensions/dm-subagents/src/runs/background/wait-tool.js +47 -0
- package/extensions/dm-subagents/src/runs/foreground/async-dismiss-action.js +81 -0
- package/extensions/dm-subagents/src/runs/foreground/async-steering-action.js +245 -0
- package/extensions/dm-subagents/src/runs/foreground/async-stop-action.js +74 -0
- package/extensions/dm-subagents/src/runs/foreground/execution.js +1401 -342
- package/extensions/dm-subagents/src/runs/foreground/foreground-control.js +133 -0
- package/extensions/dm-subagents/src/runs/foreground/foreground-history.js +148 -0
- package/extensions/dm-subagents/src/runs/foreground/prompt-audit.js +139 -0
- package/extensions/dm-subagents/src/runs/foreground/subagent-executor.js +4278 -1441
- package/extensions/dm-subagents/src/runs/foreground/workflow-detach-reconcile.js +278 -0
- package/extensions/dm-subagents/src/runs/foreground/workflow-foreground-steering.js +155 -0
- package/extensions/dm-subagents/src/runs/shared/abort-recovery.js +97 -0
- package/extensions/dm-subagents/src/runs/shared/acceptance.js +597 -148
- package/extensions/dm-subagents/src/runs/shared/agent-contract.js +35 -0
- package/extensions/dm-subagents/src/runs/shared/async-status-projection.js +472 -0
- package/extensions/dm-subagents/src/runs/shared/background-process-options.js +6 -0
- package/extensions/dm-subagents/src/runs/shared/capability-ceiling.js +175 -0
- package/extensions/dm-subagents/src/runs/shared/child-identity.js +32 -0
- package/extensions/dm-subagents/src/runs/shared/child-launch-plan.js +65 -0
- package/extensions/dm-subagents/src/runs/shared/child-protocol.js +447 -0
- package/extensions/dm-subagents/src/runs/shared/claude-code-adapter.js +120 -0
- package/extensions/dm-subagents/src/runs/shared/codex-exec-adapter.js +129 -0
- package/extensions/dm-subagents/src/runs/shared/completion-evidence.js +40 -0
- package/extensions/dm-subagents/src/runs/shared/completion-guard.js +140 -83
- package/extensions/dm-subagents/src/runs/shared/context-mode.js +38 -0
- package/extensions/dm-subagents/src/runs/shared/cursor-agent-adapter.js +101 -0
- package/extensions/dm-subagents/src/runs/shared/dm-args.js +445 -72
- package/extensions/dm-subagents/src/runs/shared/dm-spawn.js +27 -16
- package/extensions/dm-subagents/src/runs/shared/dynamic-fanout.js +19 -6
- package/extensions/dm-subagents/src/runs/shared/extension-bindings.js +81 -0
- package/extensions/dm-subagents/src/runs/shared/external-cli-contract.js +134 -0
- package/extensions/dm-subagents/src/runs/shared/external-cli-preflight.js +98 -0
- package/extensions/dm-subagents/src/runs/shared/external-cli-runner.js +419 -0
- package/extensions/dm-subagents/src/runs/shared/external-job-bridge.js +404 -0
- package/extensions/dm-subagents/src/runs/shared/external-job-runner.js +334 -0
- package/extensions/dm-subagents/src/runs/shared/fast-mode-extension.js +8 -0
- package/extensions/dm-subagents/src/runs/shared/host-step-status.js +228 -0
- package/extensions/dm-subagents/src/runs/shared/lane-metadata.js +104 -0
- package/extensions/dm-subagents/src/runs/shared/launch-cwd.js +17 -0
- package/extensions/dm-subagents/src/runs/shared/llm-intent-arbiter.js +190 -0
- package/extensions/dm-subagents/src/runs/shared/long-running-guard.js +48 -3
- package/extensions/dm-subagents/src/runs/shared/mcp-config-sources.js +387 -0
- package/extensions/dm-subagents/src/runs/shared/mcp-direct-tool-allowlist.js +212 -137
- package/extensions/dm-subagents/src/runs/shared/mcp-direct-tool-grant.js +131 -0
- package/extensions/dm-subagents/src/runs/shared/model-exclusions.js +207 -0
- package/extensions/dm-subagents/src/runs/shared/model-fallback.js +225 -55
- package/extensions/dm-subagents/src/runs/shared/model-scope.js +85 -28
- package/extensions/dm-subagents/src/runs/shared/mutation-evidence.js +182 -0
- package/extensions/dm-subagents/src/runs/shared/nested-events.js +264 -102
- package/extensions/dm-subagents/src/runs/shared/nested-render.js +15 -5
- package/extensions/dm-subagents/src/runs/shared/orca-progress-tabs.js +505 -0
- package/extensions/dm-subagents/src/runs/shared/parallel-handoff.js +653 -0
- package/extensions/dm-subagents/src/runs/shared/parallel-utils.js +29 -12
- package/extensions/dm-subagents/src/runs/shared/permissions.js +108 -0
- package/extensions/dm-subagents/src/runs/shared/process-signal.js +13 -0
- package/extensions/dm-subagents/src/runs/shared/run-fanout-budget.js +257 -0
- package/extensions/dm-subagents/src/runs/shared/run-history.js +133 -13
- package/extensions/dm-subagents/src/runs/shared/runtime-acknowledged-extensions.js +62 -0
- package/extensions/dm-subagents/src/runs/shared/session-lease.js +225 -0
- package/extensions/dm-subagents/src/runs/shared/single-output.js +129 -27
- package/extensions/dm-subagents/src/runs/shared/spawn-budget.js +95 -0
- package/extensions/dm-subagents/src/runs/shared/structured-output.js +129 -10
- package/extensions/dm-subagents/src/runs/shared/subagent-control.js +66 -11
- package/extensions/dm-subagents/src/runs/shared/subagent-prompt-runtime.js +548 -70
- package/extensions/dm-subagents/src/runs/shared/subagent-startup-retry.js +50 -0
- package/extensions/dm-subagents/src/runs/shared/task-intent.js +130 -0
- package/extensions/dm-subagents/src/runs/shared/tool-availability.js +59 -0
- package/extensions/dm-subagents/src/runs/shared/tool-budget.js +7 -5
- package/extensions/dm-subagents/src/runs/shared/tool-timeout.js +64 -0
- package/extensions/dm-subagents/src/runs/shared/usage-budget.js +74 -0
- package/extensions/dm-subagents/src/runs/shared/workflow-graph.js +22 -0
- package/extensions/dm-subagents/src/runs/shared/worktree-cleanup-plan.js +721 -0
- package/extensions/dm-subagents/src/runs/shared/worktree.js +168 -19
- package/extensions/dm-subagents/src/shared/accessible-dir.js +35 -0
- package/extensions/dm-subagents/src/shared/agent-stream-options.js +3 -0
- package/extensions/dm-subagents/src/shared/artifacts.js +170 -11
- package/extensions/dm-subagents/src/shared/atomic-json.js +36 -38
- package/extensions/dm-subagents/src/shared/capacity-resilient-json.js +77 -0
- package/extensions/dm-subagents/src/shared/child-session-name.js +15 -0
- package/extensions/dm-subagents/src/shared/child-transcript.js +57 -2
- package/extensions/dm-subagents/src/shared/completion-owner.js +7 -0
- package/extensions/dm-subagents/src/shared/display-text.js +142 -0
- package/extensions/dm-subagents/src/shared/extension-context.js +17 -0
- package/extensions/dm-subagents/src/shared/file-coalescer.js +9 -0
- package/extensions/dm-subagents/src/shared/file-system-retry.js +56 -0
- package/extensions/dm-subagents/src/shared/fork-context.js +96 -33
- package/extensions/dm-subagents/src/shared/formatters.js +21 -7
- package/extensions/dm-subagents/src/shared/launch-contract.js +94 -0
- package/extensions/dm-subagents/src/shared/model-info.js +10 -5
- package/extensions/dm-subagents/src/shared/node-executable.js +19 -0
- package/extensions/dm-subagents/src/shared/prompt-resources.js +10 -0
- package/extensions/dm-subagents/src/shared/pruned-fork.js +427 -0
- package/extensions/dm-subagents/src/shared/session-file-trust.js +19 -0
- package/extensions/dm-subagents/src/shared/session-tokens.js +14 -3
- package/extensions/dm-subagents/src/shared/settings.js +39 -47
- package/extensions/dm-subagents/src/shared/shortcuts.js +16 -0
- package/extensions/dm-subagents/src/shared/status-format.js +9 -2
- package/extensions/dm-subagents/src/shared/thinking-ceiling.js +41 -0
- package/extensions/dm-subagents/src/shared/types.js +47 -7
- package/extensions/dm-subagents/src/shared/utf8.js +12 -0
- package/extensions/dm-subagents/src/shared/utils.js +151 -131
- package/extensions/dm-subagents/src/shared/watch-strategy.js +3 -0
- package/extensions/dm-subagents/src/shared/workflow-child-permit.js +84 -0
- package/extensions/dm-subagents/src/slash/delegation-adapters.js +274 -0
- package/extensions/dm-subagents/src/slash/delegation-json.js +113 -0
- package/extensions/dm-subagents/src/slash/delegation-request.js +152 -0
- package/extensions/dm-subagents/src/slash/prompt-template-bridge.js +294 -243
- package/extensions/dm-subagents/src/slash/prompt-workflows.js +35 -73
- package/extensions/dm-subagents/src/slash/selector.js +101 -0
- package/extensions/dm-subagents/src/slash/slash-bridge.js +17 -1
- package/extensions/dm-subagents/src/slash/slash-commands.js +722 -732
- package/extensions/dm-subagents/src/slash/slash-live-state.js +37 -19
- package/extensions/dm-subagents/src/slash/subagents-admin.js +410 -0
- package/extensions/dm-subagents/src/tui/fleet-status.js +824 -0
- package/extensions/dm-subagents/src/tui/fleet-transcript.js +479 -0
- package/extensions/dm-subagents/src/tui/fleet.js +1326 -0
- package/extensions/dm-subagents/src/tui/render-helpers.js +22 -0
- package/extensions/dm-subagents/src/tui/render.js +1511 -261
- package/extensions/dm-subagents/src/watchdog/change-signature.js +220 -0
- package/extensions/dm-subagents/src/watchdog/child-status.js +151 -0
- package/extensions/dm-subagents/src/watchdog/emission-guard.js +90 -0
- package/extensions/dm-subagents/src/watchdog/lsp-diagnostics.js +484 -0
- package/extensions/dm-subagents/src/watchdog/model-selection.js +154 -0
- package/extensions/dm-subagents/src/watchdog/permission-arbiter.js +138 -0
- package/extensions/dm-subagents/src/watchdog/register-child.js +112 -0
- package/extensions/dm-subagents/src/watchdog/register-main.js +419 -0
- package/extensions/dm-subagents/src/watchdog/render.js +54 -0
- package/extensions/dm-subagents/src/watchdog/review.js +251 -0
- package/extensions/dm-subagents/src/watchdog/runtime.js +803 -0
- package/extensions/dm-subagents/src/watchdog/scope.js +56 -0
- package/extensions/dm-subagents/src/watchdog/settings.js +515 -0
- package/extensions/dm-subagents/src/watchdog/tool-actions.js +151 -0
- package/extensions/dm-subagents/src/watchdog/turn-delta.js +169 -0
- package/extensions/dm-subagents/src/watchdog/types.js +29 -0
- package/extensions/dm-subagents/src/watchdog/warning-format.js +58 -0
- package/extensions/dm-subagents/src/workflows/chat-progress.js +116 -0
- package/extensions/dm-subagents/src/workflows/host-command.js +227 -0
- package/extensions/dm-subagents/src/workflows/scripted-workflow.js +2011 -0
- package/extensions/dm-subagents/src/workflows/workflow-child-summary.js +116 -0
- package/extensions/dm-subagents/src/workflows/workflow-preflight.js +243 -0
- package/extensions/dm-subagents/src/workflows/workflow-receipt.js +387 -0
- package/extensions/dm-subagents/src/workflows/workflow-settlement.js +189 -0
- package/package.json +4 -3
- package/extensions/dm-fff/package.json +0 -21
- package/extensions/dm-fff/src/index.js +0 -691
- package/extensions/dm-fff/src/query.js +0 -60
- package/extensions/dm-subagents/agents/context-builder.md +0 -46
- package/extensions/dm-subagents/agents/planner.md +0 -55
- package/extensions/dm-subagents/prompts/parallel-context-build.md +0 -55
- package/extensions/dm-subagents/prompts/parallel-handoff-plan.md +0 -61
- package/extensions/dm-subagents/src/runs/background/wait.js +0 -206
- package/extensions/dm-subagents/src/runs/foreground/chain-clarify.js +0 -1013
- package/extensions/dm-subagents/src/runs/foreground/chain-execution.js +0 -981
- package/extensions/dm-subagents/src/runs/shared/turn-budget.js +0 -50
|
@@ -0,0 +1,653 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { writeAtomicJson } from "../../shared/atomic-json.js";
|
|
4
|
+
import { stableJsonDigest } from "../../shared/launch-contract.js";
|
|
5
|
+
import { cleanupWorktrees } from "./worktree.js";
|
|
6
|
+
import { assertWorkflowLaneKey, normalizeWorkflowLaneMetadata } from "./lane-metadata.js";
|
|
7
|
+
function optionalIdentity(value, label) {
|
|
8
|
+
if (value === undefined)
|
|
9
|
+
return;
|
|
10
|
+
if (typeof value !== "string" || !value.trim())
|
|
11
|
+
throw new Error(`${label} must be a non-empty string.`);
|
|
12
|
+
return value.trim();
|
|
13
|
+
}
|
|
14
|
+
function nonNegativeIndex(value, label) {
|
|
15
|
+
if (!Number.isSafeInteger(value) || value < 0)
|
|
16
|
+
throw new Error(`${label} must be a non-negative integer.`);
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
function normalizeLaneBinding(value, label) {
|
|
20
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
21
|
+
throw new Error(`${label} must be an object.`);
|
|
22
|
+
const binding = value;
|
|
23
|
+
const unknown = Object.keys(binding).filter((field) => !["index", "taskIndex", "workflowKey", "runId", "lane"].includes(field));
|
|
24
|
+
if (unknown.length > 0)
|
|
25
|
+
throw new Error(`${label} has unsupported fields: ${unknown.join(", ")}.`);
|
|
26
|
+
const index = nonNegativeIndex(binding.index, `${label}.index`);
|
|
27
|
+
const taskIndex = nonNegativeIndex(binding.taskIndex, `${label}.taskIndex`);
|
|
28
|
+
const workflowKey = optionalIdentity(binding.workflowKey, `${label}.workflowKey`);
|
|
29
|
+
const runId = optionalIdentity(binding.runId, `${label}.runId`);
|
|
30
|
+
const lane = normalizeWorkflowLaneMetadata(binding.lane, `${label}.lane`);
|
|
31
|
+
assertWorkflowLaneKey(lane, workflowKey, `${label}.lane`);
|
|
32
|
+
return {
|
|
33
|
+
index,
|
|
34
|
+
taskIndex,
|
|
35
|
+
...workflowKey ? { workflowKey } : {},
|
|
36
|
+
...runId ? { runId } : {},
|
|
37
|
+
...lane ? { lane } : {}
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function validateManifestIdentity(parsed, manifestPath) {
|
|
41
|
+
if (parsed.version !== 1 || !Array.isArray(parsed.groups)) {
|
|
42
|
+
throw new Error(`Invalid parallel handoff manifest: ${manifestPath}`);
|
|
43
|
+
}
|
|
44
|
+
if (typeof parsed.runId !== "string" || !parsed.runId.trim())
|
|
45
|
+
throw new Error(`Invalid parallel handoff manifest '${manifestPath}': runId must be a non-empty string.`);
|
|
46
|
+
parsed.runId = parsed.runId.trim();
|
|
47
|
+
const workflowKeys = new Set;
|
|
48
|
+
const childRunIds = new Set;
|
|
49
|
+
const childIndexes = new Set;
|
|
50
|
+
for (const [groupIndex, group] of parsed.groups.entries()) {
|
|
51
|
+
if (!group || typeof group !== "object" || !Array.isArray(group.children) || !group.cleanup || !Array.isArray(group.cleanup.tasks))
|
|
52
|
+
throw new Error(`Invalid parallel handoff manifest '${manifestPath}': groups[${groupIndex}] is malformed.`);
|
|
53
|
+
if (group.laneBindings !== undefined) {
|
|
54
|
+
if (!Array.isArray(group.laneBindings))
|
|
55
|
+
throw new Error(`Invalid parallel handoff manifest '${manifestPath}': groups[${groupIndex}].laneBindings must be an array.`);
|
|
56
|
+
const taskIndexes = new Set;
|
|
57
|
+
const cleanupIndexes = new Set(group.cleanup.tasks.map((task) => task.index));
|
|
58
|
+
group.laneBindings = group.laneBindings.map((binding, bindingIndex) => {
|
|
59
|
+
const normalized = normalizeLaneBinding(binding, `Invalid parallel handoff manifest '${manifestPath}': groups[${groupIndex}].laneBindings[${bindingIndex}]`);
|
|
60
|
+
if (taskIndexes.has(normalized.taskIndex))
|
|
61
|
+
throw new Error(`Invalid parallel handoff manifest '${manifestPath}': groups[${groupIndex}] has duplicate lane binding taskIndex ${normalized.taskIndex}.`);
|
|
62
|
+
if (!cleanupIndexes.has(normalized.taskIndex))
|
|
63
|
+
throw new Error(`Invalid parallel handoff manifest '${manifestPath}': groups[${groupIndex}].laneBindings[${bindingIndex}] has no cleanup task for taskIndex ${normalized.taskIndex}.`);
|
|
64
|
+
taskIndexes.add(normalized.taskIndex);
|
|
65
|
+
return normalized;
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
const taskIndexes = new Set;
|
|
69
|
+
for (const [childIndex, child] of group.children.entries()) {
|
|
70
|
+
if (!child || typeof child !== "object" || !child.patch || typeof child.patch !== "object")
|
|
71
|
+
throw new Error(`Invalid parallel handoff manifest '${manifestPath}': groups[${groupIndex}].children[${childIndex}] is malformed.`);
|
|
72
|
+
const childRecord = child;
|
|
73
|
+
const index = nonNegativeIndex(childRecord.index, `Invalid parallel handoff manifest '${manifestPath}': groups[${groupIndex}].children[${childIndex}].index`);
|
|
74
|
+
const taskIndex = nonNegativeIndex(childRecord.taskIndex, `Invalid parallel handoff manifest '${manifestPath}': groups[${groupIndex}].children[${childIndex}].taskIndex`);
|
|
75
|
+
if (taskIndexes.has(taskIndex))
|
|
76
|
+
throw new Error(`Invalid parallel handoff manifest '${manifestPath}': groups[${groupIndex}] has duplicate child taskIndex ${taskIndex}.`);
|
|
77
|
+
taskIndexes.add(taskIndex);
|
|
78
|
+
if (childIndexes.has(index))
|
|
79
|
+
throw new Error(`Invalid parallel handoff manifest '${manifestPath}': duplicate child index ${index}.`);
|
|
80
|
+
childIndexes.add(index);
|
|
81
|
+
if (!group.cleanup.tasks.some((task) => task.index === taskIndex))
|
|
82
|
+
throw new Error(`Invalid parallel handoff manifest '${manifestPath}': groups[${groupIndex}].children[${childIndex}] has no cleanup task for taskIndex ${taskIndex}.`);
|
|
83
|
+
const workflowKey = optionalIdentity(childRecord.workflowKey, `Invalid parallel handoff manifest '${manifestPath}': groups[${groupIndex}].children[${childIndex}].workflowKey`);
|
|
84
|
+
const runId = optionalIdentity(childRecord.runId, `Invalid parallel handoff manifest '${manifestPath}': groups[${groupIndex}].children[${childIndex}].runId`);
|
|
85
|
+
const lane = normalizeWorkflowLaneMetadata(childRecord.lane, `Invalid parallel handoff manifest '${manifestPath}': groups[${groupIndex}].children[${childIndex}].lane`);
|
|
86
|
+
assertWorkflowLaneKey(lane, workflowKey, `Invalid parallel handoff manifest '${manifestPath}': groups[${groupIndex}].children[${childIndex}].lane`);
|
|
87
|
+
if (workflowKey && workflowKeys.has(workflowKey))
|
|
88
|
+
throw new Error(`Invalid parallel handoff manifest '${manifestPath}': duplicate workflow key '${workflowKey}'.`);
|
|
89
|
+
if (workflowKey)
|
|
90
|
+
workflowKeys.add(workflowKey);
|
|
91
|
+
if (runId && childRunIds.has(runId))
|
|
92
|
+
throw new Error(`Invalid parallel handoff manifest '${manifestPath}': duplicate child run id '${runId}'.`);
|
|
93
|
+
if (runId)
|
|
94
|
+
childRunIds.add(runId);
|
|
95
|
+
if (lane)
|
|
96
|
+
childRecord.lane = lane;
|
|
97
|
+
if (workflowKey)
|
|
98
|
+
childRecord.workflowKey = workflowKey;
|
|
99
|
+
if (runId)
|
|
100
|
+
childRecord.runId = runId;
|
|
101
|
+
childRecord.index = index;
|
|
102
|
+
childRecord.taskIndex = taskIndex;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return parsed;
|
|
106
|
+
}
|
|
107
|
+
export function readParallelHandoffManifest(manifestPath) {
|
|
108
|
+
if (!fs.existsSync(manifestPath))
|
|
109
|
+
return;
|
|
110
|
+
const parsed = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
|
|
111
|
+
return validateManifestIdentity(parsed, manifestPath);
|
|
112
|
+
}
|
|
113
|
+
export function resolveParallelHandoffChild(input) {
|
|
114
|
+
const manifest = readParallelHandoffManifest(input.manifestPath);
|
|
115
|
+
if (!manifest)
|
|
116
|
+
return;
|
|
117
|
+
if (manifest.runId !== input.runId)
|
|
118
|
+
throw new Error(`Managed worktree handoff belongs to run '${manifest.runId}', not '${input.runId}'.`);
|
|
119
|
+
const workflowKey = input.workflowKey?.trim() || undefined;
|
|
120
|
+
const childRunId = input.childRunId?.trim() || undefined;
|
|
121
|
+
if (!workflowKey && !childRunId)
|
|
122
|
+
throw new Error("Parallel handoff child resolution requires workflowKey or childRunId.");
|
|
123
|
+
const matches = manifest.groups.flatMap((group) => group.children.map((child) => ({ group, child }))).filter(({ child }) => {
|
|
124
|
+
if (workflowKey && child.workflowKey !== workflowKey)
|
|
125
|
+
return false;
|
|
126
|
+
if (childRunId && child.runId !== childRunId)
|
|
127
|
+
return false;
|
|
128
|
+
return true;
|
|
129
|
+
});
|
|
130
|
+
if (matches.length > 1)
|
|
131
|
+
throw new Error(`Parallel handoff has multiple children matching workflow identity${workflowKey ? ` '${workflowKey}'` : ` '${childRunId}'`}.`);
|
|
132
|
+
return matches[0];
|
|
133
|
+
}
|
|
134
|
+
function resolveExistingPath(candidate) {
|
|
135
|
+
try {
|
|
136
|
+
return fs.realpathSync(candidate);
|
|
137
|
+
} catch {
|
|
138
|
+
return path.resolve(candidate);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
function pathInside(root, candidate) {
|
|
142
|
+
const relative = path.relative(root, candidate);
|
|
143
|
+
return relative === "" || relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
|
|
144
|
+
}
|
|
145
|
+
export function resolveRetainedWorktreeCwd(manifestPath, runId, childIndex) {
|
|
146
|
+
const manifest = readParallelHandoffManifest(manifestPath);
|
|
147
|
+
if (!manifest)
|
|
148
|
+
return;
|
|
149
|
+
if (manifest.runId !== runId)
|
|
150
|
+
throw new Error(`Managed worktree handoff belongs to run '${manifest.runId}', not '${runId}'.`);
|
|
151
|
+
const match = manifest.groups.flatMap((group) => group.children.map((child) => ({ group, child }))).find(({ child }) => child.index === childIndex);
|
|
152
|
+
if (!match)
|
|
153
|
+
return;
|
|
154
|
+
const cleanup = match.group.cleanup.tasks.find((task) => task.index === match.child.taskIndex);
|
|
155
|
+
if (!cleanup)
|
|
156
|
+
throw new Error(`Async run '${runId}' child ${childIndex} has no managed worktree cleanup record.`);
|
|
157
|
+
const relativeCwd = path.relative(resolveExistingPath(match.group.repoRoot), resolveExistingPath(manifest.cwd));
|
|
158
|
+
if (path.isAbsolute(relativeCwd) || relativeCwd === ".." || relativeCwd.startsWith(`..${path.sep}`))
|
|
159
|
+
throw new Error(`Async run '${runId}' has an invalid managed worktree cwd.`);
|
|
160
|
+
const requiredCwd = path.join(cleanup.path, relativeCwd);
|
|
161
|
+
if (cleanup.worktreeRemoved || cleanup.branchRemoved)
|
|
162
|
+
throw new Error(`Async run '${runId}' required managed worktree was removed: ${requiredCwd}`);
|
|
163
|
+
let worktreeRoot = "";
|
|
164
|
+
let resolvedRequiredCwd = "";
|
|
165
|
+
try {
|
|
166
|
+
const rootStat = fs.lstatSync(cleanup.path);
|
|
167
|
+
if (!rootStat.isDirectory() || rootStat.isSymbolicLink())
|
|
168
|
+
throw new Error("worktree root is not a directory");
|
|
169
|
+
worktreeRoot = fs.realpathSync(cleanup.path);
|
|
170
|
+
if (!fs.statSync(requiredCwd).isDirectory())
|
|
171
|
+
throw new Error("path is not a directory");
|
|
172
|
+
resolvedRequiredCwd = fs.realpathSync(requiredCwd);
|
|
173
|
+
} catch (error) {
|
|
174
|
+
throw new Error(`Async run '${runId}' required managed worktree cwd is missing: ${requiredCwd}`, { cause: error instanceof Error ? error : undefined });
|
|
175
|
+
}
|
|
176
|
+
if (!pathInside(worktreeRoot, resolvedRequiredCwd))
|
|
177
|
+
throw new Error(`Async run '${runId}' has an invalid managed worktree cwd.`);
|
|
178
|
+
return requiredCwd;
|
|
179
|
+
}
|
|
180
|
+
function referenceFor(manifestPath, manifest) {
|
|
181
|
+
const children = manifest.groups.flatMap((group) => group.children);
|
|
182
|
+
return {
|
|
183
|
+
version: 1,
|
|
184
|
+
path: manifestPath,
|
|
185
|
+
groupCount: manifest.groups.length,
|
|
186
|
+
childCount: children.length,
|
|
187
|
+
changedPatches: children.filter((child) => child.patch.changed).length,
|
|
188
|
+
cleanupState: manifest.groups.every((group) => group.cleanup.state === "complete") ? "complete" : "partial",
|
|
189
|
+
...Object.prototype.hasOwnProperty.call(manifest, "cleanupEligibility") ? { cleanupEligibility: trustedStoredCleanupEligibility(manifest) } : {}
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
const COMMIT_PATTERN = /^[0-9a-f]{40}$/i;
|
|
193
|
+
const MANIFEST_DIGEST_PATTERN = /^[0-9a-f]{64}$/i;
|
|
194
|
+
const MAX_ATTESTATION_ACTOR_LENGTH = 128;
|
|
195
|
+
const MAX_ATTESTATION_TIMESTAMP_LENGTH = 64;
|
|
196
|
+
const MAX_SUPERSESSION_ID_LENGTH = 128;
|
|
197
|
+
function boundedString(value, label, maxBytes) {
|
|
198
|
+
if (typeof value !== "string" || !value.trim())
|
|
199
|
+
throw new Error(`${label} must be a non-empty string.`);
|
|
200
|
+
const normalized = value.trim();
|
|
201
|
+
if (normalized.includes(`
|
|
202
|
+
`) || normalized.includes("\r"))
|
|
203
|
+
throw new Error(`${label} must not contain newlines.`);
|
|
204
|
+
if (Buffer.byteLength(normalized, "utf-8") > maxBytes)
|
|
205
|
+
throw new Error(`${label} must be at most ${maxBytes} bytes.`);
|
|
206
|
+
return normalized;
|
|
207
|
+
}
|
|
208
|
+
function commitString(value, label) {
|
|
209
|
+
const normalized = boundedString(value, label, 64).toLowerCase();
|
|
210
|
+
if (!COMMIT_PATTERN.test(normalized))
|
|
211
|
+
throw new Error(`${label} must be a full 40-character commit SHA.`);
|
|
212
|
+
return normalized;
|
|
213
|
+
}
|
|
214
|
+
function manifestDigestString(value, label) {
|
|
215
|
+
const normalized = boundedString(value, label, 64).toLowerCase();
|
|
216
|
+
if (!MANIFEST_DIGEST_PATTERN.test(normalized))
|
|
217
|
+
throw new Error(`${label} must be a full 64-character SHA-256 digest.`);
|
|
218
|
+
return normalized;
|
|
219
|
+
}
|
|
220
|
+
function manifestFactsDigest(manifest) {
|
|
221
|
+
return stableJsonDigest({
|
|
222
|
+
version: manifest.version,
|
|
223
|
+
runId: manifest.runId,
|
|
224
|
+
mode: manifest.mode,
|
|
225
|
+
source: manifest.source,
|
|
226
|
+
cwd: manifest.cwd,
|
|
227
|
+
groups: manifest.groups
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
function attestationTimestamp(value) {
|
|
231
|
+
const normalized = boundedString(value, "attestedAt", MAX_ATTESTATION_TIMESTAMP_LENGTH);
|
|
232
|
+
if (!Number.isFinite(Date.parse(normalized)))
|
|
233
|
+
throw new Error("attestedAt must be a valid timestamp.");
|
|
234
|
+
return normalized;
|
|
235
|
+
}
|
|
236
|
+
function normalizeMergeEvidence(value) {
|
|
237
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
238
|
+
throw new Error("merge must be an object.");
|
|
239
|
+
const merge = value;
|
|
240
|
+
const prNumber = merge.prNumber;
|
|
241
|
+
if (typeof prNumber !== "number" || !Number.isInteger(prNumber) || prNumber < 1)
|
|
242
|
+
throw new Error("merge.prNumber must be a positive integer.");
|
|
243
|
+
const treeEquivalent = merge.treeEquivalent;
|
|
244
|
+
if (treeEquivalent !== true && treeEquivalent !== false && treeEquivalent !== "unknown")
|
|
245
|
+
throw new Error("merge.treeEquivalent must be true, false, or 'unknown'.");
|
|
246
|
+
const postMergeChecks = merge.postMergeChecks;
|
|
247
|
+
if (postMergeChecks !== "recorded" && postMergeChecks !== "unknown")
|
|
248
|
+
throw new Error("merge.postMergeChecks must be 'recorded' or 'unknown'.");
|
|
249
|
+
return {
|
|
250
|
+
prNumber,
|
|
251
|
+
reviewedHead: commitString(merge.reviewedHead, "merge.reviewedHead"),
|
|
252
|
+
mergeCommit: commitString(merge.mergeCommit, "merge.mergeCommit"),
|
|
253
|
+
treeEquivalent,
|
|
254
|
+
postMergeChecks,
|
|
255
|
+
attestedBy: boundedString(merge.attestedBy, "merge.attestedBy", MAX_ATTESTATION_ACTOR_LENGTH),
|
|
256
|
+
attestedAt: attestationTimestamp(merge.attestedAt),
|
|
257
|
+
...merge.manifestDigest === undefined ? {} : { manifestDigest: manifestDigestString(merge.manifestDigest, "merge.manifestDigest") }
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
function normalizeSupersessionEvidence(value) {
|
|
261
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
262
|
+
throw new Error("supersession must be an object.");
|
|
263
|
+
const supersession = value;
|
|
264
|
+
return {
|
|
265
|
+
supersededBy: boundedString(supersession.supersededBy, "supersession.supersededBy", MAX_SUPERSESSION_ID_LENGTH),
|
|
266
|
+
attestedBy: boundedString(supersession.attestedBy, "supersession.attestedBy", MAX_ATTESTATION_ACTOR_LENGTH),
|
|
267
|
+
attestedAt: attestationTimestamp(supersession.attestedAt),
|
|
268
|
+
...supersession.manifestDigest === undefined ? {} : { manifestDigest: manifestDigestString(supersession.manifestDigest, "supersession.manifestDigest") }
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
function normalizeStoredCleanupEligibility(value) {
|
|
272
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
273
|
+
return { state: "unknown" };
|
|
274
|
+
const eligibility = value;
|
|
275
|
+
if (eligibility.state === "active" || eligibility.state === "terminal-eligible" || eligibility.state === "superseded-eligible" || eligibility.state === "unknown") {
|
|
276
|
+
return { state: eligibility.state };
|
|
277
|
+
}
|
|
278
|
+
if (eligibility.state === "terminal-blocked" && typeof eligibility.reason === "string" && eligibility.reason.trim() && !eligibility.reason.includes(`
|
|
279
|
+
`) && !eligibility.reason.includes("\r")) {
|
|
280
|
+
return { state: "terminal-blocked", reason: eligibility.reason.trim().slice(0, 256) };
|
|
281
|
+
}
|
|
282
|
+
return { state: "unknown" };
|
|
283
|
+
}
|
|
284
|
+
const STORED_CHILD_STATUSES = new Set(["pending", "running", "completed", "complete", "failed", "paused", "stopped", "detached", "rejected"]);
|
|
285
|
+
export function isTerminalParallelHandoffChildStatus(status) {
|
|
286
|
+
return status === "completed" || status === "complete" || status === "failed" || status === "paused" || status === "stopped" || status === "rejected";
|
|
287
|
+
}
|
|
288
|
+
function hasValidStoredLaneShape(manifest) {
|
|
289
|
+
if (!manifest || typeof manifest !== "object" || manifest.version !== 1 || typeof manifest.runId !== "string" || !manifest.runId.trim() || !Array.isArray(manifest.groups) || manifest.groups.length === 0)
|
|
290
|
+
return false;
|
|
291
|
+
let hasManagedTask = false;
|
|
292
|
+
const valid = manifest.groups.every((group) => {
|
|
293
|
+
if (!group || typeof group !== "object" || typeof group.baseCommit !== "string" || !group.baseCommit.trim() || typeof group.repoRoot !== "string" || !group.repoRoot.trim())
|
|
294
|
+
return false;
|
|
295
|
+
const cleanup = group.cleanup;
|
|
296
|
+
if (!cleanup || typeof cleanup !== "object" || cleanup.state !== "complete" && cleanup.state !== "partial" || !Array.isArray(cleanup.tasks) || !Array.isArray(group.children))
|
|
297
|
+
return false;
|
|
298
|
+
if (cleanup.tasks.some((task) => !task || typeof task !== "object" || !Number.isSafeInteger(task.index) || task.index < 0 || typeof task.path !== "string" || !task.path.trim() || typeof task.branch !== "string" || !task.branch.trim()))
|
|
299
|
+
return false;
|
|
300
|
+
if (cleanup.tasks.length > 0)
|
|
301
|
+
hasManagedTask = true;
|
|
302
|
+
return group.children.every((child) => Boolean(child) && typeof child === "object" && typeof child.status === "string" && STORED_CHILD_STATUSES.has(child.status));
|
|
303
|
+
});
|
|
304
|
+
return valid && hasManagedTask;
|
|
305
|
+
}
|
|
306
|
+
function trustedStoredCleanupEligibility(manifest) {
|
|
307
|
+
const eligibility = normalizeStoredCleanupEligibility(manifest.cleanupEligibility);
|
|
308
|
+
if (!hasValidStoredLaneShape(manifest))
|
|
309
|
+
return { state: "unknown" };
|
|
310
|
+
if (hasActiveChildren(manifest))
|
|
311
|
+
return { state: "active" };
|
|
312
|
+
if (eligibility.state === "active")
|
|
313
|
+
return cleanupEligibilityForEvidence(manifest);
|
|
314
|
+
if (eligibility.state === "terminal-eligible" || eligibility.state === "superseded-eligible") {
|
|
315
|
+
const current = cleanupEligibilityForEvidence(manifest);
|
|
316
|
+
if (current.state === eligibility.state || current.state === "terminal-blocked" && / evidence is stale$/.test(current.reason))
|
|
317
|
+
return current;
|
|
318
|
+
return { state: "unknown" };
|
|
319
|
+
}
|
|
320
|
+
return eligibility;
|
|
321
|
+
}
|
|
322
|
+
function hasActiveChildren(manifest) {
|
|
323
|
+
return Array.isArray(manifest.groups) && manifest.groups.some((group) => {
|
|
324
|
+
if (!group || typeof group !== "object")
|
|
325
|
+
return false;
|
|
326
|
+
const children = Array.isArray(group.children) ? group.children : [];
|
|
327
|
+
if (children.length === 0 && Array.isArray(group.cleanup?.tasks) && group.cleanup.tasks.length > 0)
|
|
328
|
+
return true;
|
|
329
|
+
return children.some((child) => !child || typeof child !== "object" || !isTerminalParallelHandoffChildStatus(child.status));
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
function hasManagedWorktreeTasks(manifest) {
|
|
333
|
+
return manifest.groups.length > 0 && manifest.groups.some((group) => Array.isArray(group.cleanup?.tasks) && group.cleanup.tasks.length > 0);
|
|
334
|
+
}
|
|
335
|
+
function validateManifestForLaneEvidence(manifest, laneId) {
|
|
336
|
+
if (manifest.runId !== laneId)
|
|
337
|
+
throw new Error(`Lane '${laneId}' does not match manifest run '${manifest.runId}'.`);
|
|
338
|
+
if (!hasValidStoredLaneShape(manifest))
|
|
339
|
+
throw new Error("Lane manifest is malformed.");
|
|
340
|
+
if (!manifest.groups.length || !hasManagedWorktreeTasks(manifest))
|
|
341
|
+
throw new Error("Lane manifest has no managed worktree tasks.");
|
|
342
|
+
if (hasActiveChildren(manifest))
|
|
343
|
+
throw new Error("Lane has an active child owner; merge evidence must be recorded after local reconciliation.");
|
|
344
|
+
for (const group of manifest.groups) {
|
|
345
|
+
if (typeof group.repoRoot !== "string" || !group.repoRoot.trim())
|
|
346
|
+
throw new Error("Lane manifest has no valid repository root.");
|
|
347
|
+
if (group.cleanup.state !== "complete" && group.cleanup.state !== "partial")
|
|
348
|
+
throw new Error("Lane manifest has an invalid cleanup state.");
|
|
349
|
+
if (!Array.isArray(group.cleanup.tasks) || group.cleanup.tasks.some((task) => typeof task.path !== "string" || !task.path.trim() || typeof task.branch !== "string" || !task.branch.trim())) {
|
|
350
|
+
throw new Error("Lane manifest has an invalid cleanup task.");
|
|
351
|
+
}
|
|
352
|
+
if (!Array.isArray(group.children))
|
|
353
|
+
throw new Error("Lane manifest has invalid child records.");
|
|
354
|
+
for (const child of group.children) {
|
|
355
|
+
if (!STORED_CHILD_STATUSES.has(child.status)) {
|
|
356
|
+
throw new Error("Lane manifest has an invalid child status.");
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
function cleanupEligibilityForEvidence(manifest) {
|
|
362
|
+
if (hasActiveChildren(manifest))
|
|
363
|
+
return { state: "active" };
|
|
364
|
+
let merge;
|
|
365
|
+
if (manifest.merge) {
|
|
366
|
+
try {
|
|
367
|
+
merge = normalizeMergeEvidence(manifest.merge);
|
|
368
|
+
} catch {
|
|
369
|
+
return { state: "terminal-blocked", reason: "stored merge evidence is invalid" };
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
if (manifest.supersession) {
|
|
373
|
+
try {
|
|
374
|
+
const supersession = normalizeSupersessionEvidence(manifest.supersession);
|
|
375
|
+
if (supersession.manifestDigest !== manifestFactsDigest(manifest))
|
|
376
|
+
return { state: "terminal-blocked", reason: "stored supersession evidence is stale" };
|
|
377
|
+
} catch {
|
|
378
|
+
return { state: "terminal-blocked", reason: "stored supersession evidence is invalid" };
|
|
379
|
+
}
|
|
380
|
+
return { state: "superseded-eligible" };
|
|
381
|
+
}
|
|
382
|
+
if (!merge)
|
|
383
|
+
return { state: "terminal-blocked", reason: "no merge or supersession evidence recorded" };
|
|
384
|
+
if (merge.manifestDigest !== manifestFactsDigest(manifest))
|
|
385
|
+
return { state: "terminal-blocked", reason: "stored merge evidence is stale" };
|
|
386
|
+
if (merge.treeEquivalent !== true) {
|
|
387
|
+
return { state: "terminal-blocked", reason: merge.treeEquivalent === "unknown" ? "merged tree equivalence was not attested" : "merged tree is not equivalent to the reviewed head" };
|
|
388
|
+
}
|
|
389
|
+
if (merge.postMergeChecks !== "recorded")
|
|
390
|
+
return { state: "terminal-blocked", reason: "required post-merge checks were not recorded" };
|
|
391
|
+
return { state: "terminal-eligible" };
|
|
392
|
+
}
|
|
393
|
+
function sameMergeEvidence(left, right) {
|
|
394
|
+
return left.prNumber === right.prNumber && left.reviewedHead === right.reviewedHead && left.mergeCommit === right.mergeCommit && left.treeEquivalent === right.treeEquivalent && left.postMergeChecks === right.postMergeChecks && left.attestedBy === right.attestedBy && left.attestedAt === right.attestedAt;
|
|
395
|
+
}
|
|
396
|
+
function sameSupersessionEvidence(left, right) {
|
|
397
|
+
return left.supersededBy === right.supersededBy && left.attestedBy === right.attestedBy && left.attestedAt === right.attestedAt;
|
|
398
|
+
}
|
|
399
|
+
function firstRepositoryRoot(manifest) {
|
|
400
|
+
const repoRoot = Array.isArray(manifest.groups) ? manifest.groups[0]?.repoRoot : undefined;
|
|
401
|
+
return typeof repoRoot === "string" && repoRoot.trim() ? repoRoot.trim() : undefined;
|
|
402
|
+
}
|
|
403
|
+
function formatCleanupCommand(manifestPath, manifest) {
|
|
404
|
+
const repoRoot = firstRepositoryRoot(manifest);
|
|
405
|
+
return repoRoot ? `subagent({ action: "worktree.cleanup", repo: ${JSON.stringify(repoRoot)}, handoffPath: ${JSON.stringify(manifestPath)}, mode: "plan" })` : undefined;
|
|
406
|
+
}
|
|
407
|
+
export function formatStoredParallelHandoffCleanup(manifestPath, manifest) {
|
|
408
|
+
let stored = manifest;
|
|
409
|
+
if (!stored) {
|
|
410
|
+
try {
|
|
411
|
+
stored = readParallelHandoffManifest(manifestPath);
|
|
412
|
+
} catch {
|
|
413
|
+
stored = undefined;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
if (!stored)
|
|
417
|
+
return ["Cleanup eligibility: unknown", "Reason: lane manifest is missing or invalid; removal is not safe.", `Plan command: subagent({ action: "worktree.cleanup", handoffPath: ${JSON.stringify(manifestPath)}, mode: "plan" })`].join(`
|
|
418
|
+
`);
|
|
419
|
+
const eligibility = trustedStoredCleanupEligibility(stored);
|
|
420
|
+
const lines = [`Cleanup eligibility: ${eligibility.state}`];
|
|
421
|
+
if (eligibility.state === "terminal-blocked")
|
|
422
|
+
lines.push(`Reason: ${eligibility.reason}`);
|
|
423
|
+
if (eligibility.state === "unknown")
|
|
424
|
+
lines.push("Reason: stored eligibility is missing or invalid; removal is not safe.");
|
|
425
|
+
if (eligibility.state === "active")
|
|
426
|
+
lines.push("Reason: a child owner is still active; removal is not safe.");
|
|
427
|
+
const command = formatCleanupCommand(manifestPath, stored);
|
|
428
|
+
if (command)
|
|
429
|
+
lines.push(`Plan command: ${command}`);
|
|
430
|
+
return lines.join(`
|
|
431
|
+
`);
|
|
432
|
+
}
|
|
433
|
+
export function recordParallelHandoffMerge(input) {
|
|
434
|
+
const manifest = readParallelHandoffManifest(input.manifestPath);
|
|
435
|
+
if (!manifest)
|
|
436
|
+
throw new Error(`Parallel handoff manifest not found: ${input.manifestPath}`);
|
|
437
|
+
validateManifestForLaneEvidence(manifest, boundedString(input.laneId, "laneId", MAX_SUPERSESSION_ID_LENGTH));
|
|
438
|
+
if (hasActiveChildren(manifest))
|
|
439
|
+
throw new Error("Lane has an active child owner; merge evidence must be recorded after local reconciliation.");
|
|
440
|
+
const merge = { ...normalizeMergeEvidence(input.merge), manifestDigest: manifestFactsDigest(manifest) };
|
|
441
|
+
if (manifest.merge) {
|
|
442
|
+
const existing = normalizeMergeEvidence(manifest.merge);
|
|
443
|
+
if (existing.reviewedHead !== merge.reviewedHead)
|
|
444
|
+
throw new Error(`Lane manifest is stale: reviewed head is already recorded as ${existing.reviewedHead}.`);
|
|
445
|
+
if (!sameMergeEvidence(existing, merge))
|
|
446
|
+
throw new Error("Lane manifest already contains different merge evidence for this reviewed head.");
|
|
447
|
+
}
|
|
448
|
+
const reconciled = { ...manifest, merge };
|
|
449
|
+
delete reconciled.supersession;
|
|
450
|
+
const updated = { ...reconciled, cleanupEligibility: cleanupEligibilityForEvidence(reconciled), updatedAt: input.now ?? Date.now() };
|
|
451
|
+
writeAtomicJson(input.manifestPath, updated);
|
|
452
|
+
return { manifest: updated, reference: referenceFor(input.manifestPath, updated), text: formatStoredParallelHandoffCleanup(input.manifestPath, updated) };
|
|
453
|
+
}
|
|
454
|
+
export function recordParallelHandoffSupersession(input) {
|
|
455
|
+
const manifest = readParallelHandoffManifest(input.manifestPath);
|
|
456
|
+
if (!manifest)
|
|
457
|
+
throw new Error(`Parallel handoff manifest not found: ${input.manifestPath}`);
|
|
458
|
+
const laneId = boundedString(input.laneId, "laneId", MAX_SUPERSESSION_ID_LENGTH);
|
|
459
|
+
validateManifestForLaneEvidence(manifest, laneId);
|
|
460
|
+
if (hasActiveChildren(manifest))
|
|
461
|
+
throw new Error("Lane has an active child owner; supersession must be recorded after local reconciliation.");
|
|
462
|
+
const supersession = { ...normalizeSupersessionEvidence(input.supersession), manifestDigest: manifestFactsDigest(manifest) };
|
|
463
|
+
if (supersession.supersededBy === laneId)
|
|
464
|
+
throw new Error("supersession.supersededBy must identify a different replacement lane.");
|
|
465
|
+
if (manifest.supersession) {
|
|
466
|
+
const existing = normalizeSupersessionEvidence(manifest.supersession);
|
|
467
|
+
if (!sameSupersessionEvidence(existing, supersession))
|
|
468
|
+
throw new Error("Lane manifest already contains different supersession evidence.");
|
|
469
|
+
}
|
|
470
|
+
const updated = { ...manifest, supersession, cleanupEligibility: cleanupEligibilityForEvidence({ ...manifest, supersession }), updatedAt: input.now ?? Date.now() };
|
|
471
|
+
writeAtomicJson(input.manifestPath, updated);
|
|
472
|
+
return { manifest: updated, reference: referenceFor(input.manifestPath, updated), text: formatStoredParallelHandoffCleanup(input.manifestPath, updated) };
|
|
473
|
+
}
|
|
474
|
+
function safeHandoffAgentName(agent) {
|
|
475
|
+
return agent.replace(/[^\w.-]/g, "_");
|
|
476
|
+
}
|
|
477
|
+
function missingDiff(input) {
|
|
478
|
+
const patchPath = path.join(path.dirname(input.manifestPath), `missing-diff-step-${input.stepIndex}-task-${input.taskIndex}-${safeHandoffAgentName(input.agent)}.patch`);
|
|
479
|
+
try {
|
|
480
|
+
fs.mkdirSync(path.dirname(patchPath), { recursive: true });
|
|
481
|
+
fs.writeFileSync(patchPath, "", "utf-8");
|
|
482
|
+
} catch {}
|
|
483
|
+
return {
|
|
484
|
+
index: input.taskIndex,
|
|
485
|
+
agent: input.agent,
|
|
486
|
+
branch: input.branch ?? "",
|
|
487
|
+
diffStat: "",
|
|
488
|
+
filesChanged: 0,
|
|
489
|
+
insertions: 0,
|
|
490
|
+
deletions: 0,
|
|
491
|
+
patchPath,
|
|
492
|
+
error: "diff artifact unavailable; no patch was captured"
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
export function writeParallelHandoffGroup(input) {
|
|
496
|
+
const now = input.now ?? Date.now();
|
|
497
|
+
const existing = readParallelHandoffManifest(input.manifestPath);
|
|
498
|
+
if (existing && (existing.runId !== input.runId || existing.mode !== input.mode || existing.source !== input.source)) {
|
|
499
|
+
throw new Error(`Parallel handoff manifest belongs to a different run: ${input.manifestPath}`);
|
|
500
|
+
}
|
|
501
|
+
const laneBindings = input.laneBindings?.map((binding, index) => normalizeLaneBinding(binding, `parallel handoff lane binding ${index}`));
|
|
502
|
+
if (laneBindings) {
|
|
503
|
+
const taskIndexes = new Set;
|
|
504
|
+
for (const binding of laneBindings) {
|
|
505
|
+
if (taskIndexes.has(binding.taskIndex))
|
|
506
|
+
throw new Error(`Parallel handoff has duplicate lane binding taskIndex ${binding.taskIndex}.`);
|
|
507
|
+
if (!input.setup.worktrees.some((worktree) => worktree.index === binding.taskIndex))
|
|
508
|
+
throw new Error(`Parallel handoff lane binding taskIndex ${binding.taskIndex} has no managed worktree.`);
|
|
509
|
+
if (binding.index !== input.flatStartIndex + binding.taskIndex)
|
|
510
|
+
throw new Error(`Parallel handoff lane binding index ${binding.index} does not match taskIndex ${binding.taskIndex}.`);
|
|
511
|
+
taskIndexes.add(binding.taskIndex);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
const bindingForTask = (taskIndex) => laneBindings?.find((binding) => binding.taskIndex === taskIndex);
|
|
515
|
+
const group = {
|
|
516
|
+
stepIndex: input.stepIndex,
|
|
517
|
+
baseCommit: input.setup.baseCommit,
|
|
518
|
+
repoRoot: input.setup.cwd,
|
|
519
|
+
children: input.results.map((result, taskIndex) => {
|
|
520
|
+
const binding = bindingForTask(taskIndex);
|
|
521
|
+
const runId = optionalIdentity(result.runId ?? binding?.runId, `parallel handoff child ${taskIndex}.runId`);
|
|
522
|
+
const lane = normalizeWorkflowLaneMetadata(result.lane ?? binding?.lane, `parallel handoff child ${taskIndex}.lane`);
|
|
523
|
+
const workflowKey = optionalIdentity(result.workflowKey ?? binding?.workflowKey, `parallel handoff child ${taskIndex}.workflowKey`);
|
|
524
|
+
assertWorkflowLaneKey(lane, workflowKey, `parallel handoff child ${taskIndex}.lane`);
|
|
525
|
+
const diff = input.diffs[taskIndex] ?? missingDiff({
|
|
526
|
+
manifestPath: input.manifestPath,
|
|
527
|
+
stepIndex: input.stepIndex,
|
|
528
|
+
taskIndex,
|
|
529
|
+
agent: result.agent,
|
|
530
|
+
branch: input.setup.worktrees[taskIndex]?.branch
|
|
531
|
+
});
|
|
532
|
+
return {
|
|
533
|
+
index: input.flatStartIndex + taskIndex,
|
|
534
|
+
taskIndex,
|
|
535
|
+
agent: result.agent,
|
|
536
|
+
...workflowKey ? { workflowKey } : {},
|
|
537
|
+
...runId ? { runId } : {},
|
|
538
|
+
...lane ? { lane } : {},
|
|
539
|
+
status: result.status,
|
|
540
|
+
summary: result.summary,
|
|
541
|
+
...result.outputPath ? { outputPath: result.outputPath } : {},
|
|
542
|
+
...result.structuredOutput !== undefined ? { structuredOutput: result.structuredOutput } : {},
|
|
543
|
+
...result.structuredOutputPath ? { structuredOutputPath: result.structuredOutputPath } : {},
|
|
544
|
+
...result.sessionPath ? { sessionPath: result.sessionPath } : {},
|
|
545
|
+
patch: {
|
|
546
|
+
path: diff.patchPath,
|
|
547
|
+
branch: diff.branch,
|
|
548
|
+
changed: diff.filesChanged > 0 || diff.insertions > 0 || diff.deletions > 0 || diff.diffStat.trim().length > 0,
|
|
549
|
+
diffStat: diff.diffStat,
|
|
550
|
+
filesChanged: diff.filesChanged,
|
|
551
|
+
insertions: diff.insertions,
|
|
552
|
+
deletions: diff.deletions,
|
|
553
|
+
...diff.error ? { error: diff.error } : {}
|
|
554
|
+
}
|
|
555
|
+
};
|
|
556
|
+
}),
|
|
557
|
+
...laneBindings && laneBindings.length > 0 && input.results.length === 0 ? { laneBindings } : {},
|
|
558
|
+
cleanup: input.cleanup ?? {
|
|
559
|
+
state: "partial",
|
|
560
|
+
pruned: false,
|
|
561
|
+
tasks: input.setup.worktrees.map((worktree) => ({
|
|
562
|
+
index: worktree.index,
|
|
563
|
+
path: worktree.path,
|
|
564
|
+
branch: worktree.branch,
|
|
565
|
+
worktreeRemoved: false,
|
|
566
|
+
branchRemoved: false,
|
|
567
|
+
preserved: true,
|
|
568
|
+
reason: "cleanup pending durable handoff capture"
|
|
569
|
+
}))
|
|
570
|
+
}
|
|
571
|
+
};
|
|
572
|
+
const groups = existing?.groups.filter((candidate) => candidate.stepIndex !== input.stepIndex) ?? [];
|
|
573
|
+
groups.push(group);
|
|
574
|
+
groups.sort((left, right) => left.stepIndex - right.stepIndex);
|
|
575
|
+
const manifestWithoutEligibility = {
|
|
576
|
+
version: 1,
|
|
577
|
+
runId: input.runId,
|
|
578
|
+
mode: input.mode,
|
|
579
|
+
source: input.source,
|
|
580
|
+
cwd: input.cwd,
|
|
581
|
+
createdAt: existing?.createdAt ?? now,
|
|
582
|
+
updatedAt: now,
|
|
583
|
+
groups,
|
|
584
|
+
...existing && Object.hasOwn(existing, "merge") ? { merge: existing.merge } : {},
|
|
585
|
+
...existing && Object.hasOwn(existing, "supersession") ? { supersession: existing.supersession } : {}
|
|
586
|
+
};
|
|
587
|
+
const carriesCleanupMetadata = existing !== undefined && (Object.hasOwn(existing, "merge") || Object.hasOwn(existing, "supersession") || Object.hasOwn(existing, "cleanupEligibility"));
|
|
588
|
+
const manifest = {
|
|
589
|
+
...manifestWithoutEligibility,
|
|
590
|
+
...carriesCleanupMetadata ? { cleanupEligibility: cleanupEligibilityForEvidence(manifestWithoutEligibility) } : {}
|
|
591
|
+
};
|
|
592
|
+
writeAtomicJson(input.manifestPath, manifest);
|
|
593
|
+
return referenceFor(input.manifestPath, manifest);
|
|
594
|
+
}
|
|
595
|
+
export function parallelHandoffPath(baseDir, runId) {
|
|
596
|
+
return runId ? path.join(baseDir, "handoffs", `${runId}.json`) : path.join(baseDir, "handoff.json");
|
|
597
|
+
}
|
|
598
|
+
export function writePendingParallelHandoff(input) {
|
|
599
|
+
return writeParallelHandoffGroup({ ...input, diffs: [], results: [] });
|
|
600
|
+
}
|
|
601
|
+
export function formatParallelHandoffReference(reference) {
|
|
602
|
+
return `Worktree handoff: ${reference.path} (${reference.childCount} children, ${reference.changedPatches} changed patches, cleanup ${reference.cleanupState})`;
|
|
603
|
+
}
|
|
604
|
+
export function formatParallelHandoffError(error) {
|
|
605
|
+
return `Worktree handoff unavailable: ${error instanceof Error ? error.message : String(error)}`;
|
|
606
|
+
}
|
|
607
|
+
export function discardPreservedWorktrees(manifestPath, authorization) {
|
|
608
|
+
const resolvedPath = path.resolve(manifestPath);
|
|
609
|
+
const manifest = readParallelHandoffManifest(resolvedPath);
|
|
610
|
+
if (!manifest)
|
|
611
|
+
throw new Error(`Parallel handoff manifest not found: ${resolvedPath}`);
|
|
612
|
+
let attempted = 0;
|
|
613
|
+
for (const group of manifest.groups) {
|
|
614
|
+
const pending = group.cleanup.tasks.filter((task) => task.preserved && (!task.worktreeRemoved || !task.branchRemoved));
|
|
615
|
+
if (pending.length === 0)
|
|
616
|
+
continue;
|
|
617
|
+
attempted += pending.length;
|
|
618
|
+
const report = cleanupWorktrees({
|
|
619
|
+
cwd: group.repoRoot,
|
|
620
|
+
baseCommit: group.baseCommit,
|
|
621
|
+
worktrees: pending.map((task) => ({
|
|
622
|
+
path: task.path,
|
|
623
|
+
agentCwd: task.path,
|
|
624
|
+
branch: task.branch,
|
|
625
|
+
index: task.index,
|
|
626
|
+
nodeModulesLinked: false,
|
|
627
|
+
syntheticPaths: []
|
|
628
|
+
}))
|
|
629
|
+
}, { kind: "discard", authorization });
|
|
630
|
+
const updates = new Map(report.tasks.map((task) => [task.index, task.worktreeRemoved && task.branchRemoved ? task : { ...task, preserved: true, reason: task.reason ?? "discard cleanup remains incomplete" }]));
|
|
631
|
+
group.cleanup = {
|
|
632
|
+
state: group.cleanup.tasks.every((task) => {
|
|
633
|
+
const next = updates.get(task.index) ?? task;
|
|
634
|
+
return next.worktreeRemoved && next.branchRemoved;
|
|
635
|
+
}) && report.pruned ? "complete" : "partial",
|
|
636
|
+
tasks: group.cleanup.tasks.map((task) => updates.get(task.index) ?? task),
|
|
637
|
+
pruned: report.pruned,
|
|
638
|
+
...report.errors ? { errors: report.errors } : {}
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
manifest.updatedAt = Date.now();
|
|
642
|
+
writeAtomicJson(resolvedPath, manifest);
|
|
643
|
+
const remaining = manifest.groups.flatMap((group) => group.cleanup.tasks.map((task) => ({ group, task }))).filter(({ task }) => task.preserved && (!task.worktreeRemoved || !task.branchRemoved));
|
|
644
|
+
const lines = attempted === 0 ? [`No preserved worktrees remain in ${resolvedPath}.`] : [`Discard processed ${attempted} preserved worktree${attempted === 1 ? "" : "s"}.`, `Manifest: ${resolvedPath}`];
|
|
645
|
+
if (remaining.length > 0) {
|
|
646
|
+
lines.push("", "Some worktrees remain. Inspect and remove them manually if appropriate:");
|
|
647
|
+
for (const { group, task } of remaining) {
|
|
648
|
+
lines.push(` git -C ${JSON.stringify(group.repoRoot)} status --short`, ` git -C ${JSON.stringify(group.repoRoot)} worktree remove --force ${JSON.stringify(task.path)}`, ` git -C ${JSON.stringify(group.repoRoot)} branch -D ${JSON.stringify(task.branch)}`);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
return { manifest, text: lines.join(`
|
|
652
|
+
`) };
|
|
653
|
+
}
|