peaks-loop 4.1.2 → 4.1.3
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/.claude-plugin/marketplace.json +136 -47
- package/CHANGELOG.md +56 -0
- package/README-en.md +1 -1
- package/README.md +1 -1
- package/contracts/readonly-argv-whitelist.json +211 -0
- package/dist/cli/commands/audit-artifact-runners.d.ts +16 -0
- package/dist/cli/commands/audit-artifact-runners.js +170 -0
- package/dist/cli/commands/audit-command-shared.d.ts +97 -0
- package/dist/cli/commands/audit-command-shared.js +60 -0
- package/dist/cli/commands/audit-commands.d.ts +7 -31
- package/dist/cli/commands/audit-commands.js +64 -473
- package/dist/cli/commands/audit-goal-runners.d.ts +17 -0
- package/dist/cli/commands/audit-goal-runners.js +114 -0
- package/dist/cli/commands/audit-red-lines-runners.d.ts +11 -0
- package/dist/cli/commands/audit-red-lines-runners.js +43 -0
- package/dist/cli/commands/audit-refusal-data.d.ts +23 -0
- package/dist/cli/commands/audit-refusal-data.js +69 -0
- package/dist/cli/commands/audit-static-runners.d.ts +13 -0
- package/dist/cli/commands/audit-static-runners.js +139 -0
- package/dist/cli/commands/await-runners.d.ts +11 -0
- package/dist/cli/commands/await-runners.js +148 -0
- package/dist/cli/commands/code-auto-compact-command.d.ts +3 -0
- package/dist/cli/commands/code-auto-compact-command.js +124 -0
- package/dist/cli/commands/code-auto-compact-envelope.d.ts +22 -0
- package/dist/cli/commands/code-auto-compact-envelope.js +39 -0
- package/dist/cli/commands/code-context-audit-command.d.ts +3 -0
- package/dist/cli/commands/code-context-audit-command.js +68 -0
- package/dist/cli/commands/code-context-now-command.d.ts +3 -0
- package/dist/cli/commands/code-context-now-command.js +124 -0
- package/dist/cli/commands/code-context-now-envelope.d.ts +66 -0
- package/dist/cli/commands/code-context-now-envelope.js +136 -0
- package/dist/cli/commands/code-context-now-mapping.d.ts +27 -0
- package/dist/cli/commands/code-context-now-mapping.js +43 -0
- package/dist/cli/commands/code-emit-handoff-command.d.ts +3 -0
- package/dist/cli/commands/code-emit-handoff-command.js +99 -0
- package/dist/cli/commands/code-gate-step-08-command.d.ts +3 -0
- package/dist/cli/commands/code-gate-step-08-command.js +139 -0
- package/dist/cli/commands/code-post-compact-detect-command.d.ts +3 -0
- package/dist/cli/commands/code-post-compact-detect-command.js +50 -0
- package/dist/cli/commands/code-runtime-commands.d.ts +9 -43
- package/dist/cli/commands/code-runtime-commands.js +20 -672
- package/dist/cli/commands/code-runtime-session.d.ts +1 -0
- package/dist/cli/commands/code-runtime-session.js +15 -0
- package/dist/cli/commands/compact-command.d.ts +6 -2
- package/dist/cli/commands/compact-command.js +12 -559
- package/dist/cli/commands/compact-dry-run-command.d.ts +3 -0
- package/dist/cli/commands/compact-dry-run-command.js +87 -0
- package/dist/cli/commands/compact-force-command.d.ts +3 -0
- package/dist/cli/commands/compact-force-command.js +98 -0
- package/dist/cli/commands/compact-harness-window-command.d.ts +3 -0
- package/dist/cli/commands/compact-harness-window-command.js +174 -0
- package/dist/cli/commands/compact-history-command.d.ts +3 -0
- package/dist/cli/commands/compact-history-command.js +89 -0
- package/dist/cli/commands/compact-read-commands.d.ts +3 -0
- package/dist/cli/commands/compact-read-commands.js +123 -0
- package/dist/cli/commands/compact-settle-command.d.ts +3 -0
- package/dist/cli/commands/compact-settle-command.js +130 -0
- package/dist/cli/commands/compact-shared.d.ts +9 -0
- package/dist/cli/commands/compact-shared.js +26 -0
- package/dist/cli/commands/core/skill-command.d.ts +1 -44
- package/dist/cli/commands/core/skill-command.js +13 -541
- package/dist/cli/commands/core/skill-context-verdict.d.ts +38 -0
- package/dist/cli/commands/core/skill-context-verdict.js +63 -0
- package/dist/cli/commands/core/skill-generated-config-notice.d.ts +41 -0
- package/dist/cli/commands/core/skill-generated-config-notice.js +75 -0
- package/dist/cli/commands/core/skill-heartbeat-commands.d.ts +3 -0
- package/dist/cli/commands/core/skill-heartbeat-commands.js +50 -0
- package/dist/cli/commands/core/skill-presence-commands.d.ts +3 -0
- package/dist/cli/commands/core/skill-presence-commands.js +123 -0
- package/dist/cli/commands/core/skill-presence-housekeeping-commands.d.ts +3 -0
- package/dist/cli/commands/core/skill-presence-housekeeping-commands.js +165 -0
- package/dist/cli/commands/core/skill-registry-commands.d.ts +3 -0
- package/dist/cli/commands/core/skill-registry-commands.js +117 -0
- package/dist/cli/commands/core/skill-runbook-command.d.ts +3 -0
- package/dist/cli/commands/core/skill-runbook-command.js +29 -0
- package/dist/cli/commands/dispatch-command-spec.d.ts +2 -0
- package/dist/cli/commands/dispatch-command-spec.js +62 -0
- package/dist/cli/commands/dispatch-commands.js +159 -775
- package/dist/cli/commands/dispatch-detached-branch.d.ts +3 -0
- package/dist/cli/commands/dispatch-detached-branch.js +58 -0
- package/dist/cli/commands/dispatch-envelope.d.ts +36 -0
- package/dist/cli/commands/dispatch-envelope.js +104 -0
- package/dist/cli/commands/dispatch-isolation-lease.d.ts +17 -0
- package/dist/cli/commands/dispatch-isolation-lease.js +149 -0
- package/dist/cli/commands/dispatch-preflight.d.ts +7 -0
- package/dist/cli/commands/dispatch-preflight.js +105 -0
- package/dist/cli/commands/dispatch-prompt-blocks.d.ts +10 -0
- package/dist/cli/commands/dispatch-prompt-blocks.js +56 -0
- package/dist/cli/commands/dispatch-prompt-composition.d.ts +30 -0
- package/dist/cli/commands/dispatch-prompt-composition.js +171 -0
- package/dist/cli/commands/dispatch-record-write.d.ts +24 -0
- package/dist/cli/commands/dispatch-record-write.js +96 -0
- package/dist/cli/commands/dispatch-tool-call.d.ts +15 -0
- package/dist/cli/commands/dispatch-tool-call.js +128 -0
- package/dist/cli/commands/evolution-command-shared.d.ts +78 -0
- package/dist/cli/commands/evolution-command-shared.js +107 -0
- package/dist/cli/commands/evolution-commands.d.ts +11 -1
- package/dist/cli/commands/evolution-commands.js +19 -350
- package/dist/cli/commands/evolution-evaluate-command.d.ts +24 -0
- package/dist/cli/commands/evolution-evaluate-command.js +138 -0
- package/dist/cli/commands/evolution-propose-command.d.ts +23 -0
- package/dist/cli/commands/evolution-propose-command.js +145 -0
- package/dist/cli/commands/evolution-status-command.d.ts +17 -0
- package/dist/cli/commands/evolution-status-command.js +41 -0
- package/dist/cli/commands/evolution-verdict-commands.d.ts +30 -0
- package/dist/cli/commands/evolution-verdict-commands.js +100 -0
- package/dist/cli/commands/finalize-accumulator.d.ts +44 -0
- package/dist/cli/commands/finalize-accumulator.js +55 -0
- package/dist/cli/commands/finalize-branches.d.ts +34 -0
- package/dist/cli/commands/finalize-branches.js +119 -0
- package/dist/cli/commands/finalize-runners.d.ts +3 -0
- package/dist/cli/commands/finalize-runners.js +92 -0
- package/dist/cli/commands/finalize-selection.d.ts +65 -0
- package/dist/cli/commands/finalize-selection.js +35 -0
- package/dist/cli/commands/gate-commands.js +26 -8
- package/dist/cli/commands/job-checkpoint-commands.d.ts +3 -0
- package/dist/cli/commands/job-checkpoint-commands.js +157 -0
- package/dist/cli/commands/job-command-shared.d.ts +109 -0
- package/dist/cli/commands/job-command-shared.js +72 -0
- package/dist/cli/commands/job-commands.d.ts +4 -64
- package/dist/cli/commands/job-commands.js +42 -552
- package/dist/cli/commands/job-cost-check-command.d.ts +3 -0
- package/dist/cli/commands/job-cost-check-command.js +41 -0
- package/dist/cli/commands/job-init-status-commands.d.ts +3 -0
- package/dist/cli/commands/job-init-status-commands.js +149 -0
- package/dist/cli/commands/job-run-commands.d.ts +3 -0
- package/dist/cli/commands/job-run-commands.js +97 -0
- package/dist/cli/commands/job-state-root.d.ts +35 -0
- package/dist/cli/commands/job-state-root.js +76 -0
- package/dist/cli/commands/loop-eval-capture-index.d.ts +4 -0
- package/dist/cli/commands/loop-eval-capture-index.js +44 -0
- package/dist/cli/commands/loop-eval-commands.d.ts +8 -5
- package/dist/cli/commands/loop-eval-commands.js +12 -626
- package/dist/cli/commands/loop-eval-eval-command.d.ts +3 -0
- package/dist/cli/commands/loop-eval-eval-command.js +168 -0
- package/dist/cli/commands/loop-eval-monotonic-command.d.ts +3 -0
- package/dist/cli/commands/loop-eval-monotonic-command.js +90 -0
- package/dist/cli/commands/loop-eval-parents.d.ts +7 -0
- package/dist/cli/commands/loop-eval-parents.js +20 -0
- package/dist/cli/commands/loop-eval-run-code.d.ts +7 -0
- package/dist/cli/commands/loop-eval-run-code.js +40 -0
- package/dist/cli/commands/loop-eval-run-command.d.ts +3 -0
- package/dist/cli/commands/loop-eval-run-command.js +113 -0
- package/dist/cli/commands/loop-eval-spec-commands.d.ts +3 -0
- package/dist/cli/commands/loop-eval-spec-commands.js +165 -0
- package/dist/cli/commands/loop-eval-spec-lint-command.d.ts +3 -0
- package/dist/cli/commands/loop-eval-spec-lint-command.js +42 -0
- package/dist/cli/commands/loop-eval-workflow-commands.d.ts +3 -0
- package/dist/cli/commands/loop-eval-workflow-commands.js +141 -0
- package/dist/cli/commands/mcp-commands.d.ts +10 -0
- package/dist/cli/commands/mcp-commands.js +157 -0
- package/dist/cli/commands/request-command-options.d.ts +49 -0
- package/dist/cli/commands/request-command-options.js +1 -0
- package/dist/cli/commands/request-commands.d.ts +3 -9
- package/dist/cli/commands/request-commands.js +17 -522
- package/dist/cli/commands/request-init-command.d.ts +3 -0
- package/dist/cli/commands/request-init-command.js +100 -0
- package/dist/cli/commands/request-lint-command.d.ts +3 -0
- package/dist/cli/commands/request-lint-command.js +50 -0
- package/dist/cli/commands/request-list-command.d.ts +10 -0
- package/dist/cli/commands/request-list-command.js +53 -0
- package/dist/cli/commands/request-repair-status-command.d.ts +3 -0
- package/dist/cli/commands/request-repair-status-command.js +61 -0
- package/dist/cli/commands/request-show-command.d.ts +3 -0
- package/dist/cli/commands/request-show-command.js +56 -0
- package/dist/cli/commands/request-transition-command.d.ts +3 -0
- package/dist/cli/commands/request-transition-command.js +122 -0
- package/dist/cli/commands/request-transition-failures.d.ts +8 -0
- package/dist/cli/commands/request-transition-failures.js +90 -0
- package/dist/cli/commands/request-transition-hooks.d.ts +50 -0
- package/dist/cli/commands/request-transition-hooks.js +115 -0
- package/dist/cli/commands/request-transition-preflight.d.ts +10 -0
- package/dist/cli/commands/request-transition-preflight.js +107 -0
- package/dist/cli/commands/scan-command-shared.d.ts +96 -0
- package/dist/cli/commands/scan-command-shared.js +83 -0
- package/dist/cli/commands/scan-commands.d.ts +3 -2
- package/dist/cli/commands/scan-commands.js +23 -463
- package/dist/cli/commands/scan-inventory-commands.d.ts +3 -0
- package/dist/cli/commands/scan-inventory-commands.js +96 -0
- package/dist/cli/commands/scan-project-commands.d.ts +3 -0
- package/dist/cli/commands/scan-project-commands.js +65 -0
- package/dist/cli/commands/scan-scope-commands.d.ts +3 -0
- package/dist/cli/commands/scan-scope-commands.js +149 -0
- package/dist/cli/commands/scan-scope-helpers.d.ts +8 -0
- package/dist/cli/commands/scan-scope-helpers.js +54 -0
- package/dist/cli/commands/scan-structural-commands.d.ts +3 -0
- package/dist/cli/commands/scan-structural-commands.js +113 -0
- package/dist/cli/commands/share-commands.d.ts +12 -68
- package/dist/cli/commands/share-commands.js +47 -556
- package/dist/cli/commands/share-error-hints.d.ts +14 -0
- package/dist/cli/commands/share-error-hints.js +38 -0
- package/dist/cli/commands/share-identity.d.ts +13 -0
- package/dist/cli/commands/share-identity.js +22 -0
- package/dist/cli/commands/share-record-paths.d.ts +37 -0
- package/dist/cli/commands/share-record-paths.js +68 -0
- package/dist/cli/commands/share-runners.d.ts +3 -0
- package/dist/cli/commands/share-runners.js +158 -0
- package/dist/cli/commands/shared-read-runners.d.ts +3 -0
- package/dist/cli/commands/shared-read-runners.js +51 -0
- package/dist/cli/commands/slice-check-command.d.ts +3 -0
- package/dist/cli/commands/slice-check-command.js +54 -0
- package/dist/cli/commands/slice-commands.d.ts +2 -22
- package/dist/cli/commands/slice-commands.js +18 -517
- package/dist/cli/commands/slice-decompose-artifacts.d.ts +18 -0
- package/dist/cli/commands/slice-decompose-artifacts.js +79 -0
- package/dist/cli/commands/slice-decompose-command.d.ts +3 -0
- package/dist/cli/commands/slice-decompose-command.js +123 -0
- package/dist/cli/commands/slice-list-commands.d.ts +3 -0
- package/dist/cli/commands/slice-list-commands.js +182 -0
- package/dist/cli/commands/slice-pick-command.d.ts +3 -0
- package/dist/cli/commands/slice-pick-command.js +60 -0
- package/dist/cli/commands/slice-plan-command.d.ts +24 -0
- package/dist/cli/commands/slice-plan-command.js +111 -0
- package/dist/cli/commands/workflow-commands.d.ts +10 -1
- package/dist/cli/commands/workflow-commands.js +6 -594
- package/dist/cli/commands/workflow-gate-commands.d.ts +3 -0
- package/dist/cli/commands/workflow-gate-commands.js +116 -0
- package/dist/cli/commands/workflow-plan-helpers.d.ts +48 -0
- package/dist/cli/commands/workflow-plan-helpers.js +103 -0
- package/dist/cli/commands/workflow-planning-commands.d.ts +3 -0
- package/dist/cli/commands/workflow-planning-commands.js +56 -0
- package/dist/cli/commands/workflow-swarm-actions.d.ts +7 -0
- package/dist/cli/commands/workflow-swarm-actions.js +109 -0
- package/dist/cli/commands/workflow-swarm-commands.d.ts +3 -0
- package/dist/cli/commands/workflow-swarm-commands.js +138 -0
- package/dist/cli/commands/workflow-tech-actions.d.ts +10 -0
- package/dist/cli/commands/workflow-tech-actions.js +150 -0
- package/dist/cli/commands/workspace/init-actions.d.ts +10 -0
- package/dist/cli/commands/workspace/init-actions.js +199 -0
- package/dist/cli/commands/workspace/init-codegraph-stake.d.ts +10 -0
- package/dist/cli/commands/workspace/init-codegraph-stake.js +54 -0
- package/dist/cli/commands/workspace/init-command.d.ts +8 -124
- package/dist/cli/commands/workspace/init-command.js +59 -623
- package/dist/cli/commands/workspace/init-context.d.ts +58 -0
- package/dist/cli/commands/workspace/init-context.js +121 -0
- package/dist/cli/commands/workspace/init-envelope.d.ts +3 -0
- package/dist/cli/commands/workspace/init-envelope.js +34 -0
- package/dist/cli/commands/workspace/init-failures.d.ts +3 -0
- package/dist/cli/commands/workspace/init-failures.js +84 -0
- package/dist/cli/commands/workspace/init-hooks-decision.d.ts +24 -0
- package/dist/cli/commands/workspace/init-hooks-decision.js +189 -0
- package/dist/cli/commands/workspace/init-options.d.ts +102 -0
- package/dist/cli/commands/workspace/init-options.js +1 -0
- package/dist/cli/commands/worktree-lease-commands.d.ts +10 -6
- package/dist/cli/commands/worktree-lease-commands.js +42 -512
- package/dist/cli/commands/worktree-lease-gc-command.d.ts +12 -0
- package/dist/cli/commands/worktree-lease-gc-command.js +117 -0
- package/dist/cli/commands/worktree-lease-git.d.ts +22 -0
- package/dist/cli/commands/worktree-lease-git.js +48 -0
- package/dist/cli/commands/worktree-lease-list-command.d.ts +12 -0
- package/dist/cli/commands/worktree-lease-list-command.js +100 -0
- package/dist/cli/commands/worktree-lease-load.d.ts +37 -0
- package/dist/cli/commands/worktree-lease-load.js +28 -0
- package/dist/cli/commands/worktree-lease-release-command.d.ts +13 -0
- package/dist/cli/commands/worktree-lease-release-command.js +105 -0
- package/dist/cli/commands/worktree-lease-renew-command.d.ts +11 -0
- package/dist/cli/commands/worktree-lease-renew-command.js +99 -0
- package/dist/cli/commands/worktree-lease-session.d.ts +22 -0
- package/dist/cli/commands/worktree-lease-session.js +29 -0
- package/dist/cli/commands/worktree-lease-spawn-command.d.ts +11 -0
- package/dist/cli/commands/worktree-lease-spawn-command.js +119 -0
- package/dist/cli/commands/worktree-lease-status-command.d.ts +11 -0
- package/dist/cli/commands/worktree-lease-status-command.js +86 -0
- package/dist/cli/commands/worktree-lease-store.d.ts +19 -0
- package/dist/cli/commands/worktree-lease-store.js +24 -0
- package/dist/cli/program.js +5 -0
- package/dist/services/distribution/mcp-install-runner.d.ts +21 -0
- package/dist/services/distribution/mcp-install-runner.js +59 -0
- package/dist/services/distribution/mcp-install.d.ts +95 -0
- package/dist/services/distribution/mcp-install.js +179 -0
- package/dist/services/distribution/plugin-manifest.d.ts +97 -0
- package/dist/services/distribution/plugin-manifest.js +126 -0
- package/dist/services/hooks/mcp-surface-gate.d.ts +19 -0
- package/dist/services/hooks/mcp-surface-gate.js +87 -0
- package/dist/services/ide/adapters/claude-code-adapter.d.ts +4 -35
- package/dist/services/ide/adapters/claude-code-adapter.js +48 -171
- package/dist/services/ide/adapters/claude-code-local-state.d.ts +28 -0
- package/dist/services/ide/adapters/claude-code-local-state.js +112 -0
- package/dist/services/ide/adapters/claude-code-model-window.d.ts +37 -0
- package/dist/services/ide/adapters/claude-code-model-window.js +113 -0
- package/dist/services/ide/ide-mcp-install-types.d.ts +78 -0
- package/dist/services/ide/ide-mcp-install-types.js +104 -0
- package/dist/services/ide/ide-types.d.ts +45 -1
- package/dist/services/ide/mcp-tool-matcher.d.ts +34 -0
- package/dist/services/ide/mcp-tool-matcher.js +84 -0
- package/dist/services/lint/npx-resolver.js +7 -0
- package/dist/services/mcp/cli-executor.d.ts +36 -0
- package/dist/services/mcp/cli-executor.js +97 -0
- package/dist/services/mcp/json-rpc.d.ts +81 -0
- package/dist/services/mcp/json-rpc.js +116 -0
- package/dist/services/mcp/memory-tool.d.ts +3 -0
- package/dist/services/mcp/memory-tool.js +62 -0
- package/dist/services/mcp/server-main.d.ts +1 -0
- package/dist/services/mcp/server-main.js +66 -0
- package/dist/services/mcp/server.d.ts +22 -0
- package/dist/services/mcp/server.js +152 -0
- package/dist/services/mcp/surface.d.ts +62 -0
- package/dist/services/mcp/surface.js +198 -0
- package/dist/services/mcp/tool-core.d.ts +42 -0
- package/dist/services/mcp/tool-core.js +137 -0
- package/dist/services/mcp/tools.d.ts +13 -0
- package/dist/services/mcp/tools.js +58 -0
- package/dist/services/readonly-surface/argv-guard.d.ts +32 -0
- package/dist/services/readonly-surface/argv-guard.js +164 -0
- package/dist/services/readonly-surface/readonly-whitelist.d.ts +147 -0
- package/dist/services/readonly-surface/readonly-whitelist.js +107 -0
- package/dist/services/readonly-surface/whitelist-generator.d.ts +64 -0
- package/dist/services/readonly-surface/whitelist-generator.js +212 -0
- package/dist/services/skills/hooks-codegate-superpowers.d.ts +1 -0
- package/dist/services/skills/hooks-codegate-superpowers.js +26 -0
- package/dist/services/skills/hooks-settings-service.js +10 -2
- package/package.json +17 -6
- package/scripts/install-skills-bundles.mjs +233 -0
- package/scripts/install-skills-fs.mjs +148 -0
- package/scripts/install-skills-ide.mjs +279 -0
- package/scripts/install-skills-output-style-default.mjs +196 -0
- package/scripts/install-skills-output-styles.mjs +121 -0
- package/scripts/install-skills-report.mjs +42 -0
- package/scripts/install-skills-user-config.mjs +198 -0
- package/scripts/install-skills.mjs +36 -1066
- package/scripts/watch-tree.mjs +140 -0
- package/scripts/watch.mjs +4 -126
- package/skills/peaks-status/SKILL.md +31 -13
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
+
* `peaks sub-agent dispatch` — the single-dispatch chokepoint.
|
|
2
3
|
*
|
|
3
4
|
* honor the 800-line file cap (Karpathy #2 Simplicity First). The single
|
|
4
5
|
* `dispatch` action lives here; the `--from-dag` sibling was further split
|
|
@@ -9,787 +10,170 @@
|
|
|
9
10
|
* a primitive that the peaks-code / peaks-rd / peaks-qa SKILL.md compose.
|
|
10
11
|
* Users do NOT invoke it directly; the --help text and dispatch
|
|
11
12
|
* envelope's `nextActions` reinforce the point.
|
|
13
|
+
*
|
|
14
|
+
* This module orchestrates: the option surface, the preflight refusals, the
|
|
15
|
+
* detached/DAG short-circuits and the record+envelope write are here; the lease,
|
|
16
|
+
* the prompt composition and the tool call each own a module.
|
|
12
17
|
*/
|
|
13
|
-
import { existsSync } from 'node:fs';
|
|
14
18
|
import { randomUUID } from 'node:crypto';
|
|
15
|
-
import { fail, getErrorMessage
|
|
16
|
-
import {
|
|
17
|
-
import { detectInstalledIde } from '../../services/ide/ide-detector.js';
|
|
18
|
-
import { getAdapter } from '../../services/ide/ide-registry.js';
|
|
19
|
-
import { SubAgentNotSupportedError } from '../../services/dispatch/sub-agent-dispatcher.js';
|
|
20
|
-
import { emitObservabilityEvent, OBSERVABILITY_SUBAGENT_ROLES } from '../../services/observability/observability-service.js';
|
|
21
|
-
import { noteDispatched, BATCH_LIMIT } from '../../services/dispatch/batch-counter.js';
|
|
22
|
-
import { writeInitialDispatchRecord } from '../../services/dispatch/dispatch-record-writer.js';
|
|
23
|
-
import { provisionDispatchNode } from '../../services/workflow/provision-dispatch-node.js';
|
|
24
|
-
import { evaluatePromptSize } from '../../services/context/context-guard.js';
|
|
19
|
+
import { fail, getErrorMessage } from 'peaks-loop-shared/result';
|
|
20
|
+
import { printResult } from '../cli-helpers.js';
|
|
25
21
|
import { getCurrentSessionId } from '../../services/skills/skill-presence-service.js';
|
|
26
|
-
import { resolveOuterSessionId } from '../../services/session/binding-status-service.js';
|
|
27
|
-
import { buildArtifactMeta, buildContextImpact } from '../../services/context/artifact-meta.js';
|
|
28
|
-
import { assertSafeArtifactPath } from 'peaks-loop-shared-channel';
|
|
29
|
-
import { playwrightProfilePaths } from '../../services/worktree/playwright-profile.js';
|
|
30
|
-
import { loadPreferences } from '../../services/preferences/preferences-service.js';
|
|
31
|
-
import { DEFAULT_PREFERENCES } from '../../services/preferences/preferences-types.js';
|
|
32
|
-
import { writeLogEntry } from '../../services/log/logger.js';
|
|
33
|
-
import { PROMPT_LIMIT_BYTES, RECOMMENDED_ROLES, deprecatedReviewerWarnings, validateRole } from './sub-agent-shared.js';
|
|
34
22
|
import { runDispatchFromDag } from './dispatch-from-dag.js';
|
|
35
|
-
import {
|
|
36
|
-
import {
|
|
37
|
-
import {
|
|
38
|
-
import {
|
|
39
|
-
import {
|
|
40
|
-
import {
|
|
41
|
-
import {
|
|
42
|
-
import {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
.
|
|
62
|
-
|
|
63
|
-
.
|
|
64
|
-
.
|
|
65
|
-
.
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
.
|
|
77
|
-
.
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
// circuit before the warm-path in-process pipeline runs. Branch
|
|
97
|
-
// lives in the existing action handler (NOT a sibling `peaks
|
|
98
|
-
// sub-agent-detached` command) per the slice decision memo:
|
|
99
|
-
// - 106+ existing dispatch tests reach this exact action path
|
|
100
|
-
// - Backward compat requires the default (no --mode) to keep
|
|
101
|
-
// the in-process envelope shape byte-identical
|
|
102
|
-
// - One validation entry-point reduces double-pipe maintenance
|
|
103
|
-
if (options.mode === 'detached') {
|
|
104
|
-
try {
|
|
105
|
-
const { dispatch: detachedDispatch } = await import('./sub-agent/detached.js');
|
|
106
|
-
const projectRoot = options.project ?? process.cwd();
|
|
107
|
-
const maxConcurrent = typeof options.maxConcurrent === 'string' && options.maxConcurrent.length > 0
|
|
108
|
-
? Number.parseInt(options.maxConcurrent, 10)
|
|
109
|
-
: undefined;
|
|
110
|
-
const result = await detachedDispatch({
|
|
111
|
-
role,
|
|
112
|
-
prompt: typeof options.prompt === 'string' ? options.prompt : '',
|
|
113
|
-
requestId: options.requestId ?? 'unknown-rid',
|
|
114
|
-
mode: 'detached',
|
|
115
|
-
...(typeof options.vendor === 'string' ? { vendor: options.vendor } : {}),
|
|
116
|
-
project: projectRoot,
|
|
117
|
-
json: asJson,
|
|
118
|
-
...(options.noThrottle === true ? { noThrottle: true } : {}),
|
|
119
|
-
...(typeof maxConcurrent === 'number' &&
|
|
120
|
-
Number.isInteger(maxConcurrent) &&
|
|
121
|
-
maxConcurrent > 0
|
|
122
|
-
? { maxConcurrent }
|
|
123
|
-
: {})
|
|
124
|
-
});
|
|
125
|
-
// The handler's `ok` is the launch outcome (a vendor CLI that is not
|
|
126
|
-
// installed is a failure, not a footnote) — the caller must not
|
|
127
|
-
// re-wrap it as `ok()` unconditionally, which is how `ok: true` with
|
|
128
|
-
// `pid: -1` reached the orchestrator.
|
|
129
|
-
if (result.ok) {
|
|
130
|
-
printResult(io, ok(result.command, result.data, result.warnings ?? [], result.nextActions ?? []), asJson);
|
|
131
|
-
}
|
|
132
|
-
else {
|
|
133
|
-
printResult(io, fail(result.command, 'DISPATCH_DETACHED_SPAWN_FAILED', `Could not launch the vendor CLI for a detached dispatch; nothing was started.`, result.data, result.nextActions ?? []), asJson);
|
|
134
|
-
process.exitCode = 1;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
catch (error) {
|
|
138
|
-
printResult(io, fail('sub-agent.dispatch', 'DISPATCH_DETACHED_ERROR', getErrorMessage(error), {
|
|
139
|
-
role,
|
|
140
|
-
toolCall: null,
|
|
141
|
-
dispatchRecordPath: null
|
|
142
|
-
}, [
|
|
143
|
-
'If --mode detached fails on import, the peaks-loop-internal-runtime package may be missing; reinstall and retry.',
|
|
144
|
-
'For environments without a vendor CLI on PATH, drop --mode to fall back to the default in-process dry-run.'
|
|
145
|
-
]), asJson);
|
|
146
|
-
process.exitCode = 1;
|
|
147
|
-
}
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
150
|
-
// 2.7.0 slice-dag-dispatcher MVP: --from-dag short-circuits the single
|
|
151
|
-
// sub-agent path and runs the full DAG plan via `dag-orchestrator`.
|
|
152
|
-
if (typeof options.fromDag === 'string' && options.fromDag.length > 0) {
|
|
153
|
-
await runDispatchFromDag(role, options, asJson, io);
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
|
-
const validation = validateRole(role);
|
|
157
|
-
if (validation !== null) {
|
|
158
|
-
printResult(io, fail('sub-agent.dispatch', 'INVALID_ROLE', validation, { role, toolCall: null, dispatchRecordPath: null }, [
|
|
159
|
-
'Use a non-empty role string with no control characters.',
|
|
160
|
-
`Recommended: ${RECOMMENDED_ROLES}.`
|
|
161
|
-
]), asJson);
|
|
162
|
-
process.exitCode = 1;
|
|
163
|
-
return;
|
|
164
|
-
}
|
|
165
|
-
// Slice 3 (on-demand-ecc) D-012: the `agent` role was removed in
|
|
166
|
-
// 4.0.0-beta.11 — there is no longer a subprocess path for it
|
|
167
|
-
// (the upstream ECC v2.0.0 ships no `ecc` binary). This guard
|
|
168
|
-
// sits AFTER role validation but BEFORE the missing-prompt
|
|
169
|
-
// check so an action-path dispatch with a valid prompt still
|
|
170
|
-
// returns a clear ROLE_REMOVED envelope + exit 1. Note that
|
|
171
|
-
// Commander short-circuits `--help` BEFORE `.action()` fires,
|
|
172
|
-
// so `peaks sub-agent dispatch agent --help` continues to
|
|
173
|
-
// exit 0 with the help text — that is intentional, not a bug.
|
|
174
|
-
if (role === 'agent') {
|
|
175
|
-
printResult(io, fail('sub-agent.dispatch', 'ROLE_REMOVED', 'The agent role was removed in Slice 3', {
|
|
176
|
-
role,
|
|
177
|
-
reason: 'role-removed-in-slice-3',
|
|
178
|
-
toolCall: null,
|
|
179
|
-
dispatchRecordPath: null
|
|
180
|
-
}, []), asJson);
|
|
181
|
-
process.exitCode = 1;
|
|
182
|
-
return;
|
|
183
|
-
}
|
|
184
|
-
if (!options.prompt || options.prompt.length === 0) {
|
|
185
|
-
printResult(io, fail('sub-agent.dispatch', 'MISSING_PROMPT', '--prompt is required when --from-dag is not provided', { role, toolCall: null, dispatchRecordPath: null }, [
|
|
186
|
-
'Re-run with either:',
|
|
187
|
-
' • `--prompt <text>` for single-role dispatch, OR',
|
|
188
|
-
' • `--from-dag <file>` for DAG-aware multi-slice dispatch (no --prompt needed; the per-slice prompt is generated from the DAG nodes).'
|
|
189
|
-
]), asJson);
|
|
190
|
-
process.exitCode = 1;
|
|
191
|
-
return;
|
|
192
|
-
}
|
|
193
|
-
// Slice 4.0.8 RD §4 D4c required `--graph-node` here, before the session
|
|
194
|
-
// id existed, so the only recovery it could offer was prose. The node is
|
|
195
|
-
// now provisioned inside the try block below, once `sid` is known.
|
|
196
|
-
// DOGFOOD ONLY: --prompt-length overrides the actual prompt content with
|
|
197
|
-
// a synthetic prompt of the given size in bytes. The original --prompt
|
|
198
|
-
// is still required (commander needs it). This avoids ARG_MAX limits
|
|
199
|
-
// on Windows when the dogfood prompt is > 200KB.
|
|
200
|
-
if (typeof options.promptLength === 'string' && options.promptLength.length > 0) {
|
|
201
|
-
const len = Number.parseInt(options.promptLength, 10);
|
|
202
|
-
if (Number.isInteger(len) && len > 0) {
|
|
203
|
-
options.prompt = 'x'.repeat(len);
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
if (options.prompt.length + TEST_TOOL_DETECTION_BLOCK.length > PROMPT_LIMIT_BYTES) {
|
|
207
|
-
printResult(io, fail('sub-agent.dispatch', 'PROMPT_TOO_LARGE', `prompt exceeds ${PROMPT_LIMIT_BYTES} bytes (got ${options.prompt.length})`, { role, toolCall: null, dispatchRecordPath: null }, [
|
|
208
|
-
'Truncate the prompt or split into multiple dispatches.',
|
|
209
|
-
'Pass --force to override the 80% threshold at CLI (NOT allowed at hook layer).'
|
|
210
|
-
]), asJson);
|
|
211
|
-
process.exitCode = 1;
|
|
212
|
-
return;
|
|
213
|
-
}
|
|
214
|
-
// G9 CLI 兜底 — evaluate prompt size against the threshold table.
|
|
215
|
-
const decision = evaluatePromptSize(options.prompt.length, { force: options.force === true });
|
|
216
|
-
if (!decision.allow) {
|
|
217
|
-
printResult(io, fail('sub-agent.dispatch', decision.code, `prompt size ${options.prompt.length} bytes exceeds threshold (tier=${decision.evaluation.tier}, ratio=${decision.evaluation.ratio.toFixed(3)})`, {
|
|
218
|
-
role,
|
|
219
|
-
toolCall: null,
|
|
220
|
-
dispatchRecordPath: null
|
|
221
|
-
}, [
|
|
222
|
-
decision.suggest ?? 'Trim prompt or pass --force to override at CLI.',
|
|
223
|
-
'PreToolUse hook layer will still reject regardless of --force (RL-30 strict).'
|
|
224
|
-
]), asJson);
|
|
225
|
-
process.exitCode = 1;
|
|
226
|
-
return;
|
|
227
|
-
}
|
|
228
|
-
try {
|
|
229
|
-
const projectRoot = options.project ?? process.cwd();
|
|
230
|
-
// passed, auto-resolve the active peaks session id from
|
|
231
|
-
// `.peaks/_runtime/session.json` (or PEAKS_SESSION_ID env var) so
|
|
232
|
-
// dispatch records land in `.peaks/_sub_agents/<real-sid>/` instead
|
|
233
|
-
// of the `unknown-sid` fallback. The unknown-sid branch is preserved
|
|
234
|
-
// as the last-resort so callers without a bound session (e.g. an
|
|
235
|
-
// ad-hoc dispatch in a fresh tree) still get a deterministic path.
|
|
236
|
-
const sid = options.sessionId ??
|
|
237
|
-
process.env.PEAKS_SESSION_ID ??
|
|
238
|
-
getCurrentSessionId(projectRoot) ??
|
|
239
|
-
'unknown-sid';
|
|
240
|
-
const rid = options.requestId ?? 'unknown-rid';
|
|
241
|
-
const batchId = options.batchId ?? randomUUID();
|
|
242
|
-
// auto-spawns a worktree lease and injects PEAKS_WORKTREE_LEASE_ID
|
|
243
|
-
// into the sub-agent dispatch envelope. This is the bridge that
|
|
244
|
-
// makes the lease-aware gate (Part 2.B) work for sub-agents:
|
|
245
|
-
// without this injection, the gate has no leaseId to consult and
|
|
246
|
-
// the sub-agent would need a separate `peaks worktree auth grant`.
|
|
247
|
-
let isolationMode = null;
|
|
248
|
-
let leaseId = null;
|
|
249
|
-
let worktreePath = null;
|
|
250
|
-
let worktreeBranch = null;
|
|
251
|
-
if (typeof options.isolation === 'string' && options.isolation.length > 0) {
|
|
252
|
-
if (options.isolation !== 'worktree' &&
|
|
253
|
-
options.isolation !== 'container' &&
|
|
254
|
-
options.isolation !== 'vm') {
|
|
255
|
-
printResult(io, fail('sub-agent.dispatch', 'INVALID_ISOLATION', `--isolation only accepts "worktree" | "container" | "vm" (got "${options.isolation}")`, {
|
|
256
|
-
role,
|
|
257
|
-
toolCall: null,
|
|
258
|
-
dispatchRecordPath: null
|
|
259
|
-
}, [
|
|
260
|
-
'Drop --isolation or pass --isolation worktree / --isolation container / --isolation vm.'
|
|
261
|
-
]), asJson);
|
|
262
|
-
process.exitCode = 1;
|
|
263
|
-
return;
|
|
264
|
-
}
|
|
265
|
-
if (options.isolation === 'container') {
|
|
266
|
-
// isolation is now live (Part 8 contract was the
|
|
267
|
-
// bridge; Part 12 is the runtime). Shell out to
|
|
268
|
-
// `peaks container spawn` to run `docker run` and
|
|
269
|
-
// write the container lease.
|
|
270
|
-
isolationMode = 'container';
|
|
271
|
-
try {
|
|
272
|
-
const spawnResult = await spawnContainerLease({
|
|
273
|
-
projectRoot,
|
|
274
|
-
sessionId: sid,
|
|
275
|
-
rid,
|
|
276
|
-
role,
|
|
277
|
-
purpose: `auto-spawned by dispatch --isolation container (batch=${batchId})`
|
|
278
|
-
});
|
|
279
|
-
// Reuse the leaseId variable — same field semantically
|
|
280
|
-
// (id of the isolation surface the dispatch owns).
|
|
281
|
-
leaseId = spawnResult.leaseId;
|
|
282
|
-
}
|
|
283
|
-
catch (error) {
|
|
284
|
-
printResult(io, fail('sub-agent.dispatch', 'ISOLATION_CONTAINER_SPAWN_FAILED', getErrorMessage(error), {
|
|
285
|
-
role,
|
|
286
|
-
toolCall: null,
|
|
287
|
-
dispatchRecordPath: null
|
|
288
|
-
}, [
|
|
289
|
-
'The dispatch aborts when --isolation container lease spawn fails; retry without --isolation or fix the underlying docker error.',
|
|
290
|
-
'For environments without a docker daemon, use --isolation worktree (the L2 production path).'
|
|
291
|
-
]), asJson);
|
|
292
|
-
process.exitCode = 1;
|
|
293
|
-
return;
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
if (options.isolation === 'worktree') {
|
|
297
|
-
isolationMode = 'worktree';
|
|
298
|
-
try {
|
|
299
|
-
const spawnResult = await spawnWorktreeLease({
|
|
300
|
-
projectRoot,
|
|
301
|
-
sessionId: sid,
|
|
302
|
-
rid,
|
|
303
|
-
role,
|
|
304
|
-
purpose: `auto-spawned by dispatch --isolation worktree (batch=${batchId})`
|
|
305
|
-
});
|
|
306
|
-
leaseId = spawnResult.leaseId;
|
|
307
|
-
worktreePath = spawnResult.path;
|
|
308
|
-
worktreeBranch = spawnResult.branch;
|
|
309
|
-
}
|
|
310
|
-
catch (error) {
|
|
311
|
-
printResult(io, fail('sub-agent.dispatch', 'ISOLATION_SPAWN_FAILED', getErrorMessage(error), {
|
|
312
|
-
role,
|
|
313
|
-
toolCall: null,
|
|
314
|
-
dispatchRecordPath: null
|
|
315
|
-
}, [
|
|
316
|
-
'The dispatch aborts when --isolation worktree lease spawn fails; retry without --isolation or fix the underlying git error.'
|
|
317
|
-
]), asJson);
|
|
318
|
-
process.exitCode = 1;
|
|
319
|
-
return;
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
else if (options.isolation === 'vm') {
|
|
323
|
-
// VM isolation mode is the L4 follow-up to L4 container.
|
|
324
|
-
// The CLI contract is shipped (--isolation vm is accepted
|
|
325
|
-
// by the dispatch parser and reflected in the envelope's
|
|
326
|
-
// isolationMode type). The VM runtime (Linux KVM / macOS
|
|
327
|
-
// HyperKit / Windows Hyper-V) is a much larger follow-up
|
|
328
|
-
// and is intentionally not implemented yet — we
|
|
329
|
-
// fail-fast with ISOLATION_VM_NOT_YET_IMPLEMENTED so
|
|
330
|
-
// operators see a clear "this is a placeholder" signal
|
|
331
|
-
// rather than a silent fallback to worktree.
|
|
332
|
-
//
|
|
333
|
-
// The full implementation lives in a future rid; the
|
|
334
|
-
// design is:
|
|
335
|
-
// 1. New service: src/services/vm/vm-lease.ts (parallels
|
|
336
|
-
// worktree-lease.ts / container-lease.ts) — pure lease
|
|
337
|
-
// store with vmId + hypervisor + status.
|
|
338
|
-
// 2. CLI: 'peaks vm spawn --hypervisor kvm|hyperkit|hyperv
|
|
339
|
-
// --image <name> --rid <rid> --role <role>' — runs
|
|
340
|
-
// virsh create / hvftool / hvcreate, captures the
|
|
341
|
-
// vm id, writes the lease.
|
|
342
|
-
// 3. CLI: 'peaks vm release --lease-id <id>' — virsh
|
|
343
|
-
// destroy + cleanup.
|
|
344
|
-
// 4. dispatch --isolation vm: shells out to peaks vm
|
|
345
|
-
// spawn, injects PEAKS_VM_LEASE_ID env (parallel
|
|
346
|
-
// to PEAKS_CONTAINER_LEASE_ID).
|
|
347
|
-
// 5. PreToolUse gate: when the env var is set AND the
|
|
348
|
-
// tool call is bash-with-vm-bearing-command,
|
|
349
|
-
// allow via the vm lease.
|
|
350
|
-
//
|
|
351
|
-
// Until then: fail-fast.
|
|
352
|
-
printResult(io, fail('sub-agent.dispatch', 'ISOLATION_VM_NOT_YET_IMPLEMENTED', '--isolation vm is the L4 follow-up to --isolation container (Part 25 contract); the VM runtime (KVM / HyperKit / Hyper-V) is a much larger follow-up rid and is intentionally not implemented yet. Drop --isolation or pass --isolation worktree / --isolation container for now.', {
|
|
353
|
-
role,
|
|
354
|
-
toolCall: null,
|
|
355
|
-
dispatchRecordPath: null
|
|
356
|
-
}, [
|
|
357
|
-
'The VM contract is shipped (--isolation vm is accepted by the dispatch parser); the runtime is the next rid.',
|
|
358
|
-
'Use --isolation worktree (L2 production) or --isolation container (L4 docker, Part 12) for now.'
|
|
359
|
-
]), asJson);
|
|
360
|
-
process.exitCode = 1;
|
|
361
|
-
return;
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
// loadPreferences can throw on schema mismatch; we fall back to defaults
|
|
365
|
-
// to avoid breaking the dispatch on a stale preferences.json file.
|
|
366
|
-
let projectPrefs = DEFAULT_PREFERENCES;
|
|
367
|
-
try {
|
|
368
|
-
projectPrefs = loadPreferences(projectRoot);
|
|
369
|
-
}
|
|
370
|
-
catch {
|
|
371
|
-
// TODO(g2): legacy silent catch — grace: 1 minor release (v2.14.0)
|
|
372
|
-
// Keep default preferences.
|
|
373
|
-
}
|
|
374
|
-
const ide = detectInstalledIde(projectRoot) ?? 'claude-code';
|
|
375
|
-
const adapter = getAdapter(ide);
|
|
376
|
-
if (!adapter.subAgentDispatcher.supportsRole(role)) {
|
|
377
|
-
printResult(io, fail('sub-agent.dispatch', 'IDE_NOT_SUPPORTED', `IDE ${ide} does not support role "${role}"`, { role, toolCall: null, dispatchRecordPath: null }, [
|
|
378
|
-
'Switch to a registered IDE (e.g. claude-code) or pick a role the current IDE supports.'
|
|
379
|
-
]), asJson);
|
|
380
|
-
process.exitCode = 1;
|
|
381
|
-
return;
|
|
382
|
-
}
|
|
383
|
-
// memory preflight block (or silently skip when unavailable) via the
|
|
384
|
-
// pure-function builder.
|
|
385
|
-
const preflightService = new MemoryPreflightService(projectRoot, projectPrefs);
|
|
386
|
-
// task at hand (role + first line of the brief), not the bare role.
|
|
387
|
-
const memoryBlock = await preflightService.fetchBlock(deriveMemoryQuery(role, options.prompt));
|
|
388
|
-
// codegraph preflight for RD planning. BEFORE the RD sub-agent's
|
|
389
|
-
// prompt is composed, ensure the index exists (init + index best-effort
|
|
390
|
-
// when `.codegraph/` is absent) and read a BOUNDED structure summary.
|
|
391
|
-
// `rd` only — other roles keep the legacy prompt byte-identical.
|
|
392
|
-
// Fail-soft: any failure degrades and dispatch proceeds. On failure the
|
|
393
|
-
// preflight's NOTE — the upstream cause — travels as the same value as
|
|
394
|
-
// the block, so the reason cannot be dropped while the block survives.
|
|
395
|
-
// The old `block : null` dropped it, so every degraded dispatch read as success.
|
|
396
|
-
let codegraphBlock;
|
|
397
|
-
if (role === 'rd') {
|
|
398
|
-
try {
|
|
399
|
-
const { buildCodegraphPreflightBlock } = await import('../../services/codegraph/codegraph-preflight-service.js');
|
|
400
|
-
const preflight = await buildCodegraphPreflightBlock(projectRoot);
|
|
401
|
-
codegraphBlock = preflight.available ? preflight.block : preflight.note;
|
|
402
|
-
}
|
|
403
|
-
catch (error) {
|
|
404
|
-
codegraphBlock = `codegraph preflight threw: ${getErrorMessage(error)}`;
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
// component library (+ CSS framework + build tool) in the RD/UI
|
|
408
|
-
// dispatch system prompt so the sub-agent prefers the library over
|
|
409
|
-
// hand-rolled native DOM. Only injected for frontend-generating roles
|
|
410
|
-
// (rd + ui) — qa/sc/txt/prd keep the legacy prompt byte-identical.
|
|
411
|
-
// Fail-soft inside the wrapper: a detection error degrades to null.
|
|
412
|
-
let projectStackBlock;
|
|
413
|
-
if (role === 'rd' || role === 'ui') {
|
|
414
|
-
projectStackBlock = computeUiLibraryDispatchBlock(projectRoot);
|
|
415
|
-
}
|
|
416
|
-
// Slice 2026-09-07-search-first-preflight: read the orchestrator-
|
|
417
|
-
// synthesized fresh-context block for rd + prd dispatches. The block
|
|
418
|
-
// was written to .peaks/_runtime/<sid>/fresh-context.md when the
|
|
419
|
-
// preflight triggered; it carries ≤5 binding directives that hedge
|
|
420
|
-
// model training-data lag. Fail-soft: a missing / unreadable file, or
|
|
421
|
-
// a file without the `## Fresh context` heading, degrades to null so
|
|
422
|
-
// the prompt stays byte-identical to the legacy shape. Other roles
|
|
423
|
-
// (qa/sc/txt/ui) keep the legacy prompt unchanged.
|
|
424
|
-
let freshContextBlock;
|
|
425
|
-
if (role === 'rd' || role === 'prd') {
|
|
426
|
-
freshContextBlock = readFreshContextBlock(projectRoot, sid);
|
|
427
|
-
}
|
|
428
|
-
// authoritative context-fill probe before composing the
|
|
429
|
-
// dispatch prompt. The probe is token-counted (IDE adapter's
|
|
430
|
-
// `compact` env-var), not a byte estimate. The composer
|
|
431
|
-
// prepends a `## Context window` block so the dispatched
|
|
432
|
-
// sub-agent reads the actual ratio instead of guessing
|
|
433
|
-
// from message length (char/4 estimates diverge by 2-4x
|
|
434
|
-
// and have caused false "context too low" reports at 60%+
|
|
435
|
-
// free).
|
|
436
|
-
let contextProbe = null;
|
|
437
|
-
try {
|
|
438
|
-
const { readContextPercent } = await import('../../services/context/auto-compact-reader.js');
|
|
439
|
-
const outerSessionId = resolveOuterSessionId(projectRoot, sid);
|
|
440
|
-
contextProbe = readContextPercent({
|
|
441
|
-
projectRoot,
|
|
442
|
-
sessionId: sid,
|
|
443
|
-
outerSessionId,
|
|
444
|
-
env: process.env
|
|
445
|
-
});
|
|
446
|
-
}
|
|
447
|
-
catch {
|
|
448
|
-
// Probe is best-effort; the composer renders a
|
|
449
|
-
// "no probe available" hint instead of failing the
|
|
450
|
-
// dispatch.
|
|
451
|
-
}
|
|
452
|
-
// capsule published by the orchestrator via `peaks sub-agent share`.
|
|
453
|
-
// Absent capsule → no pointer and no precedence line (byte-identical
|
|
454
|
-
// legacy prompt).
|
|
455
|
-
const capsuleRef = readSessionCapsule({ projectRoot, sid, rid });
|
|
456
|
-
const memoryAugmentedBody = buildDispatchSystemPrompt({
|
|
457
|
-
taskTitle: role,
|
|
458
|
-
taskBody: options.prompt,
|
|
459
|
-
memoryBlock,
|
|
460
|
-
// unified Test Tool Detection injection for every role.
|
|
461
|
-
contextProbe,
|
|
462
|
-
// exactOptionalPropertyTypes: only set codegraphBlock when the rd
|
|
463
|
-
// preflight actually produced a value (null = attempted-unavailable,
|
|
464
|
-
// undefined = not requested → legacy prompt unchanged).
|
|
465
|
-
...(codegraphBlock !== undefined ? { codegraphBlock } : {}),
|
|
466
|
-
// Same optionality contract for the project-stack block: set only
|
|
467
|
-
// when the rd/ui detection ran (null = no library → no block,
|
|
468
|
-
// undefined = non-frontend role → legacy prompt unchanged).
|
|
469
|
-
...(projectStackBlock !== undefined ? { projectStackBlock } : {}),
|
|
470
|
-
// Same optionality contract for the fresh-context block: set only
|
|
471
|
-
// when the rd/prd read ran (null = no block on disk → no block,
|
|
472
|
-
// undefined = non-rd/prd role → legacy prompt unchanged).
|
|
473
|
-
...(freshContextBlock !== undefined ? { freshContextBlock } : {}),
|
|
474
|
-
// §4: advisory session capsule pointer + precedence line.
|
|
475
|
-
...(capsuleRef !== null
|
|
476
|
-
? {
|
|
477
|
-
capsule: { batchId: capsuleRef.batchId, key: capsuleRef.key, bytes: capsuleRef.bytes }
|
|
478
|
-
}
|
|
479
|
-
: {})
|
|
480
|
-
});
|
|
481
|
-
// Part 2.C: when --isolation worktree, prepend an isolation envelope
|
|
482
|
-
// block so the sub-agent sees the lease id + worktree path. The block
|
|
483
|
-
// is short (a few lines). We deliberately do NOT set
|
|
484
|
-
// process.env.PEAKS_WORKTREE_LEASE_ID here — sub-agents are spawned
|
|
485
|
-
// by the LLM in its own environment, not as children of this CLI;
|
|
486
|
-
// the lease id travels through the dispatch record + prompt body.
|
|
487
|
-
const isolationBlock = isolationMode !== null && leaseId !== null
|
|
488
|
-
? `\n## Worktree isolation (Part 2.C)\n` +
|
|
489
|
-
`leaseId: ${leaseId}\n` +
|
|
490
|
-
`worktreePath: ${worktreePath}\n` +
|
|
491
|
-
`branch: ${worktreeBranch}\n` +
|
|
492
|
-
`You MAY ` +
|
|
493
|
-
'`git worktree add` ' +
|
|
494
|
-
`and ` +
|
|
495
|
-
'`git worktree remove` ' +
|
|
496
|
-
`against this lease without a separate ` +
|
|
497
|
-
'`peaks worktree auth grant` ' +
|
|
498
|
-
`— the PreToolUse gate reads the lease file. Run ` +
|
|
499
|
-
'`peaks worktree release --lease-id ${leaseId}` ' +
|
|
500
|
-
`when done.\n`
|
|
501
|
-
: '';
|
|
502
|
-
// F5 follow-up: anti-fake-green gate. When `--must-ls-files <glob>`
|
|
503
|
-
// is supplied, run `git ls-files <glob>` upfront, surface the
|
|
504
|
-
// result in the envelope as `mustLsFilesVerification: { path,
|
|
505
|
-
// exists, files }`, and prepend a `## must_ls_files enforcement`
|
|
506
|
-
// frontmatter block to the sub-agent prompt that mandates the
|
|
507
|
-
// file-existence verification as the LLM's FIRST action (before
|
|
508
|
-
// any "completed"/"PASS" claim). When the flag is absent the
|
|
509
|
-
// field is `null` and no block is injected — old call sites
|
|
510
|
-
let mustLsFilesVerification = null;
|
|
511
|
-
let mustLsFilesBlock = '';
|
|
512
|
-
if (typeof options.mustLsFiles === 'string' && options.mustLsFiles.length > 0) {
|
|
513
|
-
const glob = options.mustLsFiles;
|
|
514
|
-
const files = runGitLsFiles(projectRoot, glob);
|
|
515
|
-
const exists = files.length > 0;
|
|
516
|
-
mustLsFilesVerification = { path: glob, exists, files };
|
|
517
|
-
mustLsFilesBlock =
|
|
518
|
-
`\n## must_ls_files enforcement (F5 anti-fake-green)\n` +
|
|
519
|
-
`glob: ${glob}\n` +
|
|
520
|
-
`verification: ${exists ? `EXISTS (${files.length} file${files.length === 1 ? '' : 's'} found)` : 'MISSING (no files matched the glob)'}\n` +
|
|
521
|
-
(exists ? `first match: ${files[0] ?? ''}\n` : '') +
|
|
522
|
-
`BEFORE any claim that work is "completed" or "PASS", you MUST run \`git ls-files ${glob}\` from the project root and ` +
|
|
523
|
-
`confirm the file exists. Anti-fake-green rule (sediment 2026-08-11-rid-001-redo-fake-green-recovery-closure §Lesson 1): ` +
|
|
524
|
-
`if the file does not exist, your verdict MUST be \`status: "blocked"\` with reason "must_ls_files_failed". Do NOT silently skip this step.\n`;
|
|
525
|
-
}
|
|
526
|
-
const effectivePrompt = `${memoryAugmentedBody}${isolationBlock}${mustLsFilesBlock}`;
|
|
527
|
-
// + rerouted here, never refused — rationale on the helper itself.
|
|
528
|
-
const warnings = [...decision.warnings, ...deprecatedReviewerWarnings(role)];
|
|
529
|
-
let toolCall;
|
|
530
|
-
try {
|
|
531
|
-
toolCall = adapter.subAgentDispatcher.buildToolCall({
|
|
532
|
-
role,
|
|
533
|
-
prompt: effectivePrompt,
|
|
534
|
-
requestId: rid,
|
|
535
|
-
sessionId: sid
|
|
536
|
-
});
|
|
537
|
-
// Part 2.C: stamp the toolCall with `isolation` + a sub-agent
|
|
538
|
-
// env block so adapters that surface it (Claude Code's Task
|
|
539
|
-
// tool) propagate the lease id to the spawned process. The
|
|
540
|
-
// env block is the canonical hook the PreToolUse gate reads
|
|
541
|
-
// (gate-commands.ts: process.env.PEAKS_WORKTREE_LEASE_ID).
|
|
542
|
-
if (isolationMode !== null && leaseId !== null) {
|
|
543
|
-
const existingEnv = toolCall.args['env'] ?? {};
|
|
544
|
-
const provenanceToken = createDispatchProvenanceToken({
|
|
545
|
-
sessionId: sid,
|
|
546
|
-
requestId: rid,
|
|
547
|
-
leaseId
|
|
548
|
-
});
|
|
549
|
-
if (isolationMode === 'worktree') {
|
|
550
|
-
writeDispatchProvenance({
|
|
551
|
-
projectRoot,
|
|
552
|
-
record: {
|
|
553
|
-
schemaVersion: 1,
|
|
554
|
-
token: provenanceToken,
|
|
555
|
-
sessionId: sid,
|
|
556
|
-
requestId: rid,
|
|
557
|
-
leaseId,
|
|
558
|
-
isolation: 'worktree',
|
|
559
|
-
issuedAt: new Date().toISOString()
|
|
560
|
-
}
|
|
561
|
-
});
|
|
562
|
-
}
|
|
563
|
-
// two Playwright profile-isolation env vars so the sub-agent's
|
|
564
|
-
// browser MCP session lands in a deterministic
|
|
565
|
-
// `.peaks/_runtime/<sid>/pw-profiles/<dispatchId>/` directory
|
|
566
|
-
// (see src/services/worktree/playwright-profile.ts). Without
|
|
567
|
-
// these, concurrent dispatches share the user's default
|
|
568
|
-
// Chromium profile and corrupt cookies / localStorage.
|
|
569
|
-
const profile = playwrightProfilePaths({
|
|
570
|
-
projectRoot,
|
|
571
|
-
sessionId: sid,
|
|
572
|
-
dispatchId: rid
|
|
573
|
-
});
|
|
574
|
-
toolCall = {
|
|
575
|
-
...toolCall,
|
|
576
|
-
args: {
|
|
577
|
-
...toolCall.args,
|
|
578
|
-
isolation: isolationMode,
|
|
579
|
-
env: {
|
|
580
|
-
...existingEnv,
|
|
581
|
-
PEAKS_WORKTREE_LEASE_ID: leaseId,
|
|
582
|
-
PEAKS_PLAYWRIGHT_USER_DATA_DIR: profile.userDataDir,
|
|
583
|
-
PEAKS_PLAYWRIGHT_PROFILE_NAME: profile.profileName,
|
|
584
|
-
...(isolationMode === 'worktree'
|
|
585
|
-
? { [DISPATCH_PROVENANCE_ENV]: provenanceToken }
|
|
586
|
-
: {})
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
};
|
|
590
|
-
}
|
|
591
|
-
// Slice C of v2.11.1 — observability hook #2/7. Fire-and-forget
|
|
592
|
-
// per PRD Q4 (full-auto must never fail-loud). The
|
|
593
|
-
// synchronous emit returns {written:false} on disk-full; we
|
|
594
|
-
// deliberately swallow the result so dispatch contract is
|
|
595
|
-
// unchanged. role is only included when it matches the schema's
|
|
596
|
-
// known sub-agent role set; otherwise it's omitted (non-standard
|
|
597
|
-
// roles like 'qa-business' would otherwise drop the event
|
|
598
|
-
// through schema rejection).
|
|
599
|
-
const KNOWN_ROLES = new Set(OBSERVABILITY_SUBAGENT_ROLES);
|
|
600
|
-
const knownRole = KNOWN_ROLES.has(role)
|
|
601
|
-
? role
|
|
602
|
-
: null;
|
|
603
|
-
emitObservabilityEvent({
|
|
604
|
-
schemaVersion: 1,
|
|
605
|
-
ts: new Date().toISOString(),
|
|
606
|
-
sessionId: sid,
|
|
607
|
-
category: 'dispatch',
|
|
608
|
-
...(knownRole !== null ? { role: knownRole } : {}),
|
|
609
|
-
detail: {
|
|
610
|
-
requestId: rid,
|
|
611
|
-
ide: adapter.subAgentDispatcher.label,
|
|
612
|
-
promptBytes: effectivePrompt.length
|
|
613
|
-
}
|
|
614
|
-
}, { projectRoot });
|
|
615
|
-
}
|
|
616
|
-
catch (error) {
|
|
617
|
-
if (error instanceof SubAgentNotSupportedError) {
|
|
618
|
-
printResult(io, fail('sub-agent.dispatch', 'IDE_NOT_SUPPORTED', error.message, { role, toolCall: null, dispatchRecordPath: null }, ['Switch IDE or pick a role the current IDE supports.']), asJson);
|
|
619
|
-
process.exitCode = 1;
|
|
620
|
-
return;
|
|
621
|
-
}
|
|
622
|
-
throw error;
|
|
623
|
-
}
|
|
624
|
-
// G7 — optional --write-artifact: build ArtifactMeta, attach to record.
|
|
625
|
-
let artifactMeta = null;
|
|
626
|
-
if (typeof options.writeArtifact === 'string' && options.writeArtifact.length > 0) {
|
|
627
|
-
try {
|
|
628
|
-
assertSafeArtifactPath(options.writeArtifact, projectRoot);
|
|
629
|
-
if (!existsSync(options.writeArtifact)) {
|
|
630
|
-
warnings.push('ARTIFACT_NOT_FOUND');
|
|
631
|
-
}
|
|
632
|
-
else {
|
|
633
|
-
artifactMeta = buildArtifactMeta({
|
|
634
|
-
path: options.writeArtifact,
|
|
635
|
-
rid,
|
|
636
|
-
role,
|
|
637
|
-
idx: 1, // single dispatch, idx=1
|
|
638
|
-
summary: null
|
|
639
|
-
});
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
catch (err) {
|
|
643
|
-
warnings.push(`ARTIFACT_PATH_INVALID: ${getErrorMessage(err)}`);
|
|
644
|
-
}
|
|
645
|
-
}
|
|
646
|
-
// Slice 4.0.8 RD §4 D4c: bind this dispatch to a graph node. When the
|
|
647
|
-
// caller names none, provision one — that is what collapses the
|
|
648
|
-
// documented three-step ritual (create graph -> `peaks workflow node
|
|
649
|
-
// prepare` -> dispatch) into a single call. Those three fields were
|
|
650
|
-
// also never passed to the writer, so the record's graph binding was
|
|
651
|
-
// always null and the writer's transition never fired: the feature was
|
|
652
|
-
// inert, and only its precondition (the required flag) was real.
|
|
653
|
-
const graphBinding = (() => {
|
|
654
|
-
if (typeof options.graphNode === 'string' && options.graphNode.length > 0) {
|
|
655
|
-
return {
|
|
656
|
-
nodeId: options.graphNode,
|
|
657
|
-
workflowId: options.workflowId ?? null,
|
|
658
|
-
graphRef: options.graphRef ?? null
|
|
659
|
-
};
|
|
660
|
-
}
|
|
661
|
-
const provisioned = provisionDispatchNode({
|
|
662
|
-
projectRoot,
|
|
663
|
-
sessionId: sid,
|
|
664
|
-
role,
|
|
665
|
-
workflowId: options.workflowId,
|
|
666
|
-
graphRef: options.graphRef
|
|
667
|
-
});
|
|
668
|
-
return {
|
|
669
|
-
nodeId: provisioned.nodeId,
|
|
670
|
-
workflowId: provisioned.workflowId,
|
|
671
|
-
graphRef: provisioned.graphRef
|
|
672
|
-
};
|
|
673
|
-
})();
|
|
674
|
-
const { path: dispatchRecordPath } = writeInitialDispatchRecord({
|
|
675
|
-
projectRoot,
|
|
676
|
-
sessionId: sid,
|
|
677
|
-
requestId: rid,
|
|
678
|
-
role,
|
|
679
|
-
prompt: effectivePrompt,
|
|
680
|
-
workflowId: graphBinding.workflowId,
|
|
681
|
-
graphNodeId: graphBinding.nodeId,
|
|
682
|
-
graphRef: graphBinding.graphRef,
|
|
683
|
-
toolCall,
|
|
684
|
-
batchId,
|
|
685
|
-
// lease id on the dispatch record so the finalize-time
|
|
686
|
-
// release hook (markCompleted / heartbeat --status done) can
|
|
687
|
-
// auto-fire `peaks worktree release` when the sub-agent
|
|
688
|
-
// completes. Null when --isolation was not requested.
|
|
689
|
-
leaseId,
|
|
690
|
-
// ISO timestamp when the isolation mode was set up. Null
|
|
691
|
-
// when --isolation was not requested. The dashboard reads
|
|
692
|
-
// this directly off the dispatch record to compute
|
|
693
|
-
// isolation duration without cross-referencing the
|
|
694
|
-
// metrics stream.
|
|
695
|
-
isolationStartedAt: isolationMode !== null ? new Date().toISOString() : null
|
|
696
|
-
});
|
|
697
|
-
const counter = noteDispatched(projectRoot, sid, batchId);
|
|
698
|
-
if (counter.warning) {
|
|
699
|
-
warnings.push(counter.warning.message);
|
|
700
|
-
}
|
|
701
|
-
const contextImpact = buildContextImpact({
|
|
702
|
-
promptSize: effectivePrompt.length,
|
|
703
|
-
artifactSizes: artifactMeta ? [artifactMeta.size] : []
|
|
704
|
-
});
|
|
705
|
-
const nextActions = [
|
|
706
|
-
'Tool call is dry-run; LLM must execute the tool to actually dispatch the sub-agent.',
|
|
707
|
-
'After dispatching, the sub-agent should call `peaks sub-agent heartbeat --record ' +
|
|
708
|
-
dispatchRecordPath +
|
|
709
|
-
'` periodically.'
|
|
710
|
-
];
|
|
711
|
-
if (counter.warning) {
|
|
712
|
-
nextActions.push(`Batch is over the RL-1 limit (${BATCH_LIMIT}); consider splitting into multiple batches.`);
|
|
713
|
-
}
|
|
714
|
-
const expectedCompletionSeconds = 45;
|
|
715
|
-
const artifactsPublicPaths = typeof options.writeArtifact === 'string' && options.writeArtifact.length > 0
|
|
716
|
-
? [options.writeArtifact]
|
|
717
|
-
: [];
|
|
718
|
-
const orchestratorVisibleHint = `⏳ Spawning sub-agent via Task tool: ${role} for rid=${rid}, batch-id=${batchId} (ETA ~${expectedCompletionSeconds}s)`;
|
|
719
|
-
printResult(io, ok('sub-agent.dispatch', {
|
|
720
|
-
// an envelopeVersion marker so consumers can detect contract
|
|
721
|
-
// changes (the previous #4 dropped `data.prompt` silently).
|
|
722
|
-
envelopeVersion: '2.3.0',
|
|
723
|
-
role,
|
|
724
|
-
ide: adapter.subAgentDispatcher.label,
|
|
725
|
-
// Prompts can carry user content (sometimes test credentials /
|
|
726
|
-
// internal URLs) that has no business landing in shell history,
|
|
727
|
-
// log aggregators, or tmux scrollback. The dispatch record on
|
|
728
|
-
// disk (gitignored under .peaks/_sub_agents/) keeps the prompt
|
|
729
|
-
// for the sub-agent to read; CLI stdout stays metadata-only.
|
|
730
|
-
// Surface promptSize + originalPromptSize so the LLM-side
|
|
731
|
-
// runner can reason about the size delta without seeing the
|
|
732
|
-
// content.
|
|
733
|
-
originalPromptSize: options.prompt.length,
|
|
734
|
-
promptSize: effectivePrompt.length,
|
|
735
|
-
toolCall,
|
|
736
|
-
dispatchRecordPath,
|
|
737
|
-
batchId,
|
|
738
|
-
dispatchedInBatch: counter.count,
|
|
739
|
-
forcedAt: decision.forcedAt,
|
|
740
|
-
contextImpact,
|
|
741
|
-
artifactMetas: artifactMeta ? [artifactMeta] : [],
|
|
742
|
-
orchestratorVisibleHint,
|
|
743
|
-
artifactsPublicPaths,
|
|
744
|
-
expectedCompletionSeconds,
|
|
745
|
-
// Part 2.C: when --isolation worktree, surface the lease
|
|
746
|
-
// handle to the LLM-side runner so it can call
|
|
747
|
-
// `peaks worktree release --lease-id <id>` after the sub-agent
|
|
748
|
-
// finishes (or rely on the next gc pass to clean up). When
|
|
749
|
-
// isolation is not requested, isolation === null.
|
|
750
|
-
isolation: isolationMode,
|
|
751
|
-
leaseId,
|
|
752
|
-
worktreePath,
|
|
753
|
-
worktreeBranch,
|
|
754
|
-
// F5: anti-fake-green gate envelope surface. When
|
|
755
|
-
// `--must-ls-files <glob>` is supplied this carries the
|
|
756
|
-
// pre-dispatch verification result so the orchestrator can
|
|
757
|
-
// surface "the file exists" (or "missing — block") before
|
|
758
|
-
// spawning the sub-agent. Null when the flag is absent.
|
|
759
|
-
mustLsFilesVerification
|
|
760
|
-
}, warnings, nextActions), asJson);
|
|
761
|
-
// Best-effort: writeLogEntry swallows its own errors (logger.ts:155-159),
|
|
762
|
-
// so a full disk or missing ~/.peaks/logs/ dir never blocks the dispatch.
|
|
763
|
-
try {
|
|
764
|
-
writeLogEntry({
|
|
765
|
-
ts: new Date().toISOString(),
|
|
766
|
-
level: 'info',
|
|
767
|
-
command: 'sub-agent.dispatch',
|
|
768
|
-
msg: 'dispatched',
|
|
769
|
-
sessionId: sid,
|
|
770
|
-
batchId,
|
|
771
|
-
data: {
|
|
772
|
-
rid,
|
|
773
|
-
role,
|
|
774
|
-
batchId,
|
|
775
|
-
dispatchedInBatch: counter.count,
|
|
776
|
-
forcedAt: decision.forcedAt
|
|
777
|
-
}
|
|
778
|
-
});
|
|
779
|
-
}
|
|
780
|
-
catch {
|
|
781
|
-
// TODO(g2): legacy silent catch — grace: 1 minor release (v2.14.0)
|
|
782
|
-
/* best-effort */
|
|
783
|
-
}
|
|
784
|
-
}
|
|
785
|
-
catch (error) {
|
|
786
|
-
printResult(io, fail('sub-agent.dispatch', 'DISPATCH_ERROR', getErrorMessage(error), { role, toolCall: null, dispatchRecordPath: null }, [
|
|
787
|
-
'See error message; if you are dispatching from a SKILL.md, the LLM should retry with a smaller prompt or pick a different role.'
|
|
788
|
-
]), asJson);
|
|
789
|
-
process.exitCode = 1;
|
|
790
|
-
}
|
|
23
|
+
import { buildDispatchCommandSpec } from './dispatch-command-spec.js';
|
|
24
|
+
import { runDetachedDispatch } from './dispatch-detached-branch.js';
|
|
25
|
+
import { preflightDispatch } from './dispatch-preflight.js';
|
|
26
|
+
import { resolveIsolationLease } from './dispatch-isolation-lease.js';
|
|
27
|
+
import { composeDispatchPrompt } from './dispatch-prompt-composition.js';
|
|
28
|
+
import { buildDispatchToolCall } from './dispatch-tool-call.js';
|
|
29
|
+
import { writeDispatchRecord } from './dispatch-record-write.js';
|
|
30
|
+
import { emitDispatchEnvelope, writeDispatchLogEntry } from './dispatch-envelope.js';
|
|
31
|
+
function buildDispatchContext(options, role, asJson) {
|
|
32
|
+
const projectRoot = options.project ?? process.cwd();
|
|
33
|
+
// passed, auto-resolve the active peaks session id from
|
|
34
|
+
// `.peaks/_runtime/session.json` (or PEAKS_SESSION_ID env var) so
|
|
35
|
+
// dispatch records land in `.peaks/_sub_agents/<real-sid>/` instead
|
|
36
|
+
// of the `unknown-sid` fallback. The unknown-sid branch is preserved
|
|
37
|
+
// as the last-resort so callers without a bound session (e.g. an
|
|
38
|
+
// ad-hoc dispatch in a fresh tree) still get a deterministic path.
|
|
39
|
+
const sid = options.sessionId ??
|
|
40
|
+
process.env.PEAKS_SESSION_ID ??
|
|
41
|
+
getCurrentSessionId(projectRoot) ??
|
|
42
|
+
'unknown-sid';
|
|
43
|
+
const rid = options.requestId ?? 'unknown-rid';
|
|
44
|
+
const batchId = options.batchId ?? randomUUID();
|
|
45
|
+
return { projectRoot, sid, rid, role, batchId, asJson };
|
|
46
|
+
}
|
|
47
|
+
async function acquireIsolationLease(io, options, ctx) {
|
|
48
|
+
return resolveIsolationLease(io, options, {
|
|
49
|
+
projectRoot: ctx.projectRoot,
|
|
50
|
+
sid: ctx.sid,
|
|
51
|
+
rid: ctx.rid,
|
|
52
|
+
role: ctx.role,
|
|
53
|
+
batchId: ctx.batchId,
|
|
54
|
+
asJson: ctx.asJson
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
async function composePromptPhase(io, options, ctx, input) {
|
|
58
|
+
return composeDispatchPrompt(io, {
|
|
59
|
+
projectRoot: ctx.projectRoot,
|
|
60
|
+
sid: ctx.sid,
|
|
61
|
+
rid: ctx.rid,
|
|
62
|
+
role: ctx.role,
|
|
63
|
+
options,
|
|
64
|
+
prompt: input.prompt,
|
|
65
|
+
isolationMode: input.lease.isolationMode,
|
|
66
|
+
leaseId: input.lease.leaseId,
|
|
67
|
+
worktreePath: input.lease.worktreePath,
|
|
68
|
+
worktreeBranch: input.lease.worktreeBranch,
|
|
69
|
+
decision: input.decision,
|
|
70
|
+
asJson: ctx.asJson
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
function buildToolCallPhase(io, ctx, input) {
|
|
74
|
+
return buildDispatchToolCall(io, {
|
|
75
|
+
adapter: input.adapter,
|
|
76
|
+
role: ctx.role,
|
|
77
|
+
effectivePrompt: input.effectivePrompt,
|
|
78
|
+
rid: ctx.rid,
|
|
79
|
+
sid: ctx.sid,
|
|
80
|
+
projectRoot: ctx.projectRoot,
|
|
81
|
+
isolationMode: input.lease.isolationMode,
|
|
82
|
+
leaseId: input.lease.leaseId,
|
|
83
|
+
asJson: ctx.asJson
|
|
791
84
|
});
|
|
792
85
|
}
|
|
86
|
+
function writeRecordPhase(ctx, options, input) {
|
|
87
|
+
return writeDispatchRecord({
|
|
88
|
+
options,
|
|
89
|
+
projectRoot: ctx.projectRoot,
|
|
90
|
+
sid: ctx.sid,
|
|
91
|
+
rid: ctx.rid,
|
|
92
|
+
role: ctx.role,
|
|
93
|
+
effectivePrompt: input.effectivePrompt,
|
|
94
|
+
toolCall: input.toolCall,
|
|
95
|
+
batchId: ctx.batchId,
|
|
96
|
+
isolationMode: input.lease.isolationMode,
|
|
97
|
+
leaseId: input.lease.leaseId,
|
|
98
|
+
warnings: input.warnings
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
async function runDispatchPipeline(io, options, role, input) {
|
|
102
|
+
const ctx = buildDispatchContext(options, role, input.asJson);
|
|
103
|
+
const lease = await acquireIsolationLease(io, options, ctx);
|
|
104
|
+
if (lease === null)
|
|
105
|
+
return;
|
|
106
|
+
const composed = await composePromptPhase(io, options, ctx, {
|
|
107
|
+
prompt: input.prompt,
|
|
108
|
+
decision: input.decision,
|
|
109
|
+
lease
|
|
110
|
+
});
|
|
111
|
+
if (composed === null)
|
|
112
|
+
return;
|
|
113
|
+
const { adapter, effectivePrompt, warnings, mustLsFilesVerification } = composed;
|
|
114
|
+
const toolCall = buildToolCallPhase(io, ctx, { adapter, effectivePrompt, lease });
|
|
115
|
+
if (toolCall === null)
|
|
116
|
+
return;
|
|
117
|
+
const { artifactMeta, dispatchRecordPath, counter } = writeRecordPhase(ctx, options, {
|
|
118
|
+
effectivePrompt,
|
|
119
|
+
toolCall,
|
|
120
|
+
lease,
|
|
121
|
+
warnings
|
|
122
|
+
});
|
|
123
|
+
const envelopeInput = {
|
|
124
|
+
ctx,
|
|
125
|
+
options,
|
|
126
|
+
prompt: input.prompt,
|
|
127
|
+
adapter,
|
|
128
|
+
effectivePrompt,
|
|
129
|
+
toolCall,
|
|
130
|
+
dispatchRecordPath,
|
|
131
|
+
counter,
|
|
132
|
+
decision: input.decision,
|
|
133
|
+
artifactMeta,
|
|
134
|
+
mustLsFilesVerification,
|
|
135
|
+
lease
|
|
136
|
+
};
|
|
137
|
+
emitDispatchEnvelope(io, envelopeInput, warnings, input.asJson);
|
|
138
|
+
writeDispatchLogEntry(ctx, counter.count, input.decision.forcedAt);
|
|
139
|
+
}
|
|
140
|
+
async function runDispatchAction(role, options, io) {
|
|
141
|
+
const asJson = options.json === true;
|
|
142
|
+
// explicitly requested, lazy-import the detached handler and short-
|
|
143
|
+
// circuit before the warm-path in-process pipeline runs. Branch
|
|
144
|
+
// lives in the existing action handler (NOT a sibling `peaks
|
|
145
|
+
// sub-agent-detached` command) per the slice decision memo:
|
|
146
|
+
// - 106+ existing dispatch tests reach this exact action path
|
|
147
|
+
// - Backward compat requires the default (no --mode) to keep
|
|
148
|
+
// the in-process envelope shape byte-identical
|
|
149
|
+
// - One validation entry-point reduces double-pipe maintenance
|
|
150
|
+
if (options.mode === 'detached') {
|
|
151
|
+
await runDetachedDispatch(io, role, options, asJson);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
// 2.7.0 slice-dag-dispatcher MVP: --from-dag short-circuits the single
|
|
155
|
+
// sub-agent path and runs the full DAG plan via `dag-orchestrator`.
|
|
156
|
+
if (typeof options.fromDag === 'string' && options.fromDag.length > 0) {
|
|
157
|
+
await runDispatchFromDag(role, options, asJson, io);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const preflight = preflightDispatch(io, role, options, asJson);
|
|
161
|
+
if (preflight === null)
|
|
162
|
+
return;
|
|
163
|
+
const { decision, prompt } = preflight;
|
|
164
|
+
try {
|
|
165
|
+
await runDispatchPipeline(io, options, role, { decision, prompt, asJson });
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
printResult(io, fail('sub-agent.dispatch', 'DISPATCH_ERROR', getErrorMessage(error), { role, toolCall: null, dispatchRecordPath: null }, [
|
|
169
|
+
'See error message; if you are dispatching from a SKILL.md, the LLM should retry with a smaller prompt or pick a different role.'
|
|
170
|
+
]), asJson);
|
|
171
|
+
process.exitCode = 1;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
export function registerDispatchCommand(parent, io) {
|
|
175
|
+
buildDispatchCommandSpec(parent).action((role, options) => runDispatchAction(role, options, io));
|
|
176
|
+
}
|
|
793
177
|
/**
|
|
794
178
|
* 2.7.0 slice-dag-dispatcher MVP — see `dispatch-from-dag.ts`.
|
|
795
179
|
* #7 to honor the 800-line file cap and isolate the three heavy
|