@deksden-com/dd-flow-cli 0.9.0-beta.137 → 0.9.0-beta.139
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/CHANGELOG.md +19 -0
- package/README.md +9 -0
- package/dist/build-info.json +6 -6
- package/dist/cli/argument-values.js +5 -5
- package/dist/cli/command-inputs.js +89 -20
- package/dist/cli/command-metadata.js +197 -0
- package/dist/cli/command-option-descriptions.js +191 -0
- package/dist/cli/completion-cli.js +70 -0
- package/dist/cli/completion.js +57 -0
- package/dist/cli/error-output.js +52 -0
- package/dist/cli/executor-onboarding.js +76 -0
- package/dist/cli/flows.js +29 -0
- package/dist/cli/harnesses.js +84 -0
- package/dist/cli/help.js +240 -6
- package/dist/cli/input-preparation.js +54 -5
- package/dist/cli/memory-flow-admission.js +44 -0
- package/dist/cli/memory-init-invocation.js +78 -0
- package/dist/cli/memory-init.js +240 -0
- package/dist/cli/memory-storage-admission.js +53 -0
- package/dist/cli/models.js +39 -0
- package/dist/cli/native-hook-ingress.js +1 -1
- package/dist/cli/operator-profiles.js +61 -0
- package/dist/cli/output-options.js +35 -0
- package/dist/cli/response-file.js +71 -0
- package/dist/cli/run-cli.js +283 -160
- package/dist/cli/runtime-process.js +1 -1
- package/dist/cli/setup.js +149 -0
- package/dist/cli.js +56 -2
- package/dist/domain/stage-handler-identities.js +20 -0
- package/dist/harness-runtime/lib/daemon-operations.d.mts +1 -0
- package/dist/harness-runtime/lib/daemon-operations.mjs +4 -3
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +53 -22
- package/dist/harness-runtime/lib/dd-agy.mjs +16 -5
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +12 -6
- package/dist/harness-runtime/lib/dd-codex.mjs +24 -12
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +2 -1
- package/dist/harness-runtime/lib/dd-droid.mjs +32 -9
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +11 -3
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +100 -15
- package/dist/harness-runtime/lib/dd-opencode.mjs +51 -20
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +22 -10
- package/dist/harness-runtime/lib/dd-zcode.mjs +17 -4
- package/dist/harness-runtime/lib/delegation-instructions.d.mts +1 -0
- package/dist/harness-runtime/lib/delegation-instructions.mjs +16 -3
- package/dist/harness-runtime/lib/lease-renewal.mjs +2 -1
- package/dist/harness-runtime/lib/lifecycle-command.mjs +1 -0
- package/dist/harness-runtime/lib/managed-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/managed-daemon.mjs +50 -15
- package/dist/harness-runtime/lib/managed-flow-env.mjs +15 -0
- package/dist/harness-runtime/lib/native-children.d.mts +2 -0
- package/dist/harness-runtime/lib/native-children.mjs +20 -1
- package/dist/harness-runtime/lib/native-hook-command.mjs +1 -1
- package/dist/harness-runtime/lib/native-request-proof.d.mts +2 -0
- package/dist/harness-runtime/lib/native-request-proof.mjs +65 -0
- package/dist/harness-runtime/lib/opencode-plugin.mjs +32 -9
- package/dist/harness-runtime/lib/operation-progress.mjs +3 -1
- package/dist/harness-runtime/lib/provider-overload-policy.d.mts +13 -0
- package/dist/harness-runtime/lib/provider-overload-policy.mjs +138 -0
- package/dist/runtime/context.js +1 -1
- package/dist/schemas/discovery-result.schema.json +184 -0
- package/dist/schemas/execution-bundle.schema.json +205 -0
- package/dist/schemas/execution-settings.schema.json +162 -0
- package/dist/schemas/flow-descriptor.schema.json +54 -0
- package/dist/schemas/flow-run.schema.json +26 -0
- package/dist/schemas/harness-catalog.schema.json +80 -0
- package/dist/schemas/mb-init-compose-result.schema.json +457 -0
- package/dist/schemas/mb-init-discover-result.schema.json +500 -0
- package/dist/schemas/mb-init-finish-result.schema.json +422 -0
- package/dist/schemas/mb-init-preflight-result.schema.json +367 -0
- package/dist/schemas/mb-init-review-result.schema.json +396 -0
- package/dist/schemas/mb-init-verify-result.schema.json +391 -0
- package/dist/schemas/memory-bootstrap-receipt-v2.schema.json +67 -0
- package/dist/schemas/memory-bootstrap-receipt.schema.json +354 -0
- package/dist/schemas/memory-composition-work-result.schema.json +38 -0
- package/dist/schemas/memory-repair-work-result.schema.json +49 -0
- package/dist/schemas/memory-review-correction-plan.schema.json +30 -0
- package/dist/schemas/memory-review-work-result.schema.json +43 -0
- package/dist/schemas/model-catalog.schema.json +153 -0
- package/dist/schemas/profile-settings.schema.json +122 -0
- package/dist/schemas/release-manifest.schema.json +291 -0
- package/dist/schemas/run-engine-binding.schema.json +1 -1
- package/dist/services/check-runner-evidence.js +51 -4
- package/dist/services/cli-operation-classifier.js +8 -47
- package/dist/services/cli-upgrade-allowlist.js +27 -0
- package/dist/services/code-checks.js +387 -34
- package/dist/services/controller-child-capacity.js +13 -0
- package/dist/services/controller-child-observer.js +18 -2
- package/dist/services/controller-execution.js +88 -0
- package/dist/services/controller-fanout.js +71 -12
- package/dist/services/dashboard.js +8 -2
- package/dist/services/descriptor-coordinator-session.js +35 -0
- package/dist/services/descriptor-root-work.js +53 -0
- package/dist/services/descriptor-run-settlement.js +105 -0
- package/dist/services/descriptor-run-start.js +449 -0
- package/dist/services/descriptor-stage-coordinator.js +197 -0
- package/dist/services/descriptor-stage-settlement.js +222 -0
- package/dist/services/descriptor-stage-start.js +139 -0
- package/dist/services/discovery/agy-discovery.js +2 -0
- package/dist/services/discovery/codex-discovery.js +2 -0
- package/dist/services/discovery/droid-discovery.js +2 -0
- package/dist/services/discovery/grok-discovery.js +2 -0
- package/dist/services/discovery/opencode-discovery.js +2 -0
- package/dist/services/discovery/static.js +31 -0
- package/dist/services/discovery/zcode-discovery.js +2 -0
- package/dist/services/engines.js +39 -22
- package/dist/services/eval-snapshots.js +83 -25
- package/dist/services/execution-bundles.js +263 -0
- package/dist/services/execution-policy.js +20 -16
- package/dist/services/executor-onboarding.js +56 -0
- package/dist/services/external-work-launch.js +93 -15
- package/dist/services/flow-catalog.js +21 -0
- package/dist/services/flow-descriptors.js +82 -0
- package/dist/services/frozen-engine.js +250 -0
- package/dist/services/generic-execution.js +164 -0
- package/dist/services/harness-adapter.js +28 -2
- package/dist/services/harness-discovery.js +29 -0
- package/dist/services/harness-doctor.js +75 -0
- package/dist/services/harness-probe-effects.js +51 -0
- package/dist/services/harness-version-probe.js +100 -0
- package/dist/services/hooks.js +24 -6
- package/dist/services/hybrid-contracts.js +483 -0
- package/dist/services/ids.js +29 -1
- package/dist/services/installation-lock.js +95 -0
- package/dist/services/installed-flow-source.js +183 -0
- package/dist/services/lifecycle-invocations.js +257 -78
- package/dist/services/managed-processes.js +31 -10
- package/dist/services/memory-accepted-composition.js +77 -0
- package/dist/services/memory-baseline-preservation.js +65 -0
- package/dist/services/memory-completed-correction-wave.js +320 -0
- package/dist/services/memory-compose-handler.js +258 -0
- package/dist/services/memory-composition-acceptance.js +199 -0
- package/dist/services/memory-composition-graph.js +134 -0
- package/dist/services/memory-composition-registration.js +146 -0
- package/dist/services/memory-composition-work-custody.js +151 -0
- package/dist/services/memory-composition-work-results.js +96 -0
- package/dist/services/memory-corrected-review-acceptance.js +129 -0
- package/dist/services/memory-corrected-review-batch.js +30 -0
- package/dist/services/memory-corrected-review-findings.js +106 -0
- package/dist/services/memory-corrected-review-inputs.js +210 -0
- package/dist/services/memory-corrected-review-projection.js +78 -0
- package/dist/services/memory-corrected-review-receipt.js +170 -0
- package/dist/services/memory-corrected-review-registration.js +191 -0
- package/dist/services/memory-correction-check-receipt.js +219 -0
- package/dist/services/memory-correction-input-owner.js +6 -0
- package/dist/services/memory-discover-handler.js +104 -0
- package/dist/services/memory-finish-evidence.js +435 -0
- package/dist/services/memory-finish-handler.js +215 -0
- package/dist/services/memory-finish-integration.js +280 -0
- package/dist/services/memory-finish-readiness.js +122 -0
- package/dist/services/memory-git-integration.js +151 -0
- package/dist/services/memory-historical-corrected-review.js +206 -0
- package/dist/services/memory-historical-original-review.js +130 -0
- package/dist/services/memory-historical-repair-registration.js +99 -0
- package/dist/services/memory-historical-second-review.js +128 -0
- package/dist/services/memory-init-bootstrap-readiness.js +210 -0
- package/dist/services/memory-init-contexts.js +89 -0
- package/dist/services/memory-init-controller-start.js +84 -0
- package/dist/services/memory-init-engine-release.js +223 -0
- package/dist/services/memory-init-git-policy.js +136 -0
- package/dist/services/memory-init-lint-capability.js +185 -0
- package/dist/services/memory-init-native-capacity.js +296 -0
- package/dist/services/memory-init-preflight.js +330 -0
- package/dist/services/memory-init-project-bootstrap.js +599 -0
- package/dist/services/memory-init-request-candidate.js +157 -0
- package/dist/services/memory-init-request-input.js +66 -0
- package/dist/services/memory-init-resource-storage.js +125 -0
- package/dist/services/memory-init-run-allocation.js +81 -0
- package/dist/services/memory-init-run-bundle.js +98 -0
- package/dist/services/memory-init-start.js +181 -0
- package/dist/services/memory-init-startup-custody.js +80 -0
- package/dist/services/memory-init-startup-input.js +240 -0
- package/dist/services/memory-init-storage.js +81 -0
- package/dist/services/memory-init-stores.js +359 -0
- package/dist/services/memory-init-worktrunk-capability.js +178 -0
- package/dist/services/memory-layer-publication.js +611 -0
- package/dist/services/memory-layer.js +156 -0
- package/dist/services/memory-lint-config.js +85 -0
- package/dist/services/memory-native-review-copy.js +165 -0
- package/dist/services/memory-normal-flow-entry.js +75 -0
- package/dist/services/memory-ordinary-defaults.js +112 -0
- package/dist/services/memory-preflight-bootstrap-custody.js +115 -0
- package/dist/services/memory-preflight-handler.js +259 -0
- package/dist/services/memory-repair-current-view.js +299 -0
- package/dist/services/memory-repair-document-delta.js +451 -0
- package/dist/services/memory-repair-history.js +491 -0
- package/dist/services/memory-repair-physical-binding.js +312 -0
- package/dist/services/memory-repair-publication-files.js +152 -0
- package/dist/services/memory-repair-publication-intent.js +27 -0
- package/dist/services/memory-repair-publication-plan.js +160 -0
- package/dist/services/memory-repair-publication-source.js +225 -0
- package/dist/services/memory-repair-publication.js +794 -0
- package/dist/services/memory-repair-registration.js +239 -0
- package/dist/services/memory-repair-work-baseline.js +413 -0
- package/dist/services/memory-repair-work-completion.js +97 -0
- package/dist/services/memory-repair-work-custody.js +73 -0
- package/dist/services/memory-repair-work-results.js +89 -0
- package/dist/services/memory-repair-workspace.js +252 -0
- package/dist/services/memory-repair-writer-copy.js +735 -0
- package/dist/services/memory-result-binding.js +173 -0
- package/dist/services/memory-retained-review-authority.js +129 -0
- package/dist/services/memory-retained-review-results.js +70 -0
- package/dist/services/memory-retained-second-baseline.js +102 -0
- package/dist/services/memory-retained-second-repairs.js +156 -0
- package/dist/services/memory-review-acceptance.js +214 -0
- package/dist/services/memory-review-correction-plan.js +171 -0
- package/dist/services/memory-review-handler.js +585 -0
- package/dist/services/memory-review-inputs.js +200 -0
- package/dist/services/memory-review-receipt.js +180 -0
- package/dist/services/memory-review-registration.js +108 -0
- package/dist/services/memory-review-work-custody.js +124 -0
- package/dist/services/memory-review-work-input.js +72 -0
- package/dist/services/memory-review-work-results.js +73 -0
- package/dist/services/memory-second-corrected-review-inputs.js +165 -0
- package/dist/services/memory-second-correction-plan.js +76 -0
- package/dist/services/memory-second-repair-baseline.js +91 -0
- package/dist/services/memory-second-repair-launch.js +614 -0
- package/dist/services/memory-second-repair-registration.js +88 -0
- package/dist/services/memory-second-repair-source.js +187 -0
- package/dist/services/memory-second-review-acceptance.js +115 -0
- package/dist/services/memory-source-observations.js +117 -0
- package/dist/services/memory-stage-dispatch.js +28 -0
- package/dist/services/memory-stage-results.js +199 -0
- package/dist/services/memory-verification-acceptance.js +135 -0
- package/dist/services/memory-verification-checks.js +252 -0
- package/dist/services/memory-verification-receipt.js +159 -0
- package/dist/services/memory-verify-handler.js +135 -0
- package/dist/services/memory-work-result-preparation.js +63 -0
- package/dist/services/merge-server.js +2 -4
- package/dist/services/model-catalog.js +109 -0
- package/dist/services/model-discovery.js +46 -0
- package/dist/services/operator-profiles.js +236 -0
- package/dist/services/projects.js +17 -2
- package/dist/services/{codex-capacity.js → provider-capacity.js} +34 -16
- package/dist/services/recovery-snapshot-database.js +417 -2
- package/dist/services/release-archive-producer.js +52 -0
- package/dist/services/release-archive.js +143 -0
- package/dist/services/release-cache.js +142 -0
- package/dist/services/release-compatibility.js +18 -0
- package/dist/services/release-download.js +67 -0
- package/dist/services/release-envelope.js +104 -0
- package/dist/services/release-manifest.js +45 -0
- package/dist/services/release-payload.js +114 -0
- package/dist/services/release-producer.js +63 -0
- package/dist/services/release-provenance.js +160 -0
- package/dist/services/release-registry.js +66 -0
- package/dist/services/release-source.js +194 -0
- package/dist/services/repair-intents.js +1 -1
- package/dist/services/review-copy.js +10 -2
- package/dist/services/run-control.js +80 -9
- package/dist/services/run-controller-adapter.js +1 -1
- package/dist/services/run-controller-capture.js +10 -3
- package/dist/services/run-controller-process.js +21 -0
- package/dist/services/run-controller-recovery.js +5 -5
- package/dist/services/run-controller-state.js +1 -1
- package/dist/services/run-controller.js +158 -64
- package/dist/services/run-recovery-guard.js +5 -3
- package/dist/services/run-recovery-runtime.js +3 -3
- package/dist/services/run-recovery.js +145 -5
- package/dist/services/run-start-custody.js +177 -0
- package/dist/services/runs.js +413 -30
- package/dist/services/setup.js +377 -0
- package/dist/services/stage-context.js +9 -1
- package/dist/services/stage-handlers.js +74 -0
- package/dist/services/stage-report-renderer.js +63 -0
- package/dist/services/stage-work-graph.js +17 -0
- package/dist/services/vnext-code-review.js +1 -1
- package/dist/services/vnext-code.js +6 -6
- package/dist/services/vnext-execution-profile.js +2 -2
- package/dist/services/vnext-fanout.js +73 -11
- package/dist/services/vnext-merge.js +8 -3
- package/dist/services/vnext-plan-review.js +7 -5
- package/dist/services/vnext-plan.js +3 -3
- package/dist/services/vnext-protocolize.js +20 -6
- package/dist/services/vnext-workspace-policy.js +7 -2
- package/dist/services/work-registry.js +616 -71
- package/dist/services/workspace-files.js +1 -1
- package/dist/storage/database.js +84 -6
- package/dist/storage/writer-contract.js +11 -7
- package/package.json +13 -3
- package/tools/create-canon-release.mjs +33 -0
- package/tools/produce-hybrid-release.mjs +41 -0
- package/dist/harness-runtime/lib/codex-capacity-policy.d.mts +0 -10
- package/dist/harness-runtime/lib/codex-capacity-policy.mjs +0 -102
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
import { bindLifecyclePacketAssignments, managedLifecycleCommand, prepareWorkStartAuthority, issueLifecycleInvocation, retireWorkPacketAuthorities } from "./lifecycle-invocations.js";
|
|
2
|
-
import { bindStageWork, currentStageWorkIds, stageWorkIds } from "./stage-work-graph.js";
|
|
2
|
+
import { bindStageWork, currentStageWorkIds, stageWorkIds, stageGraphScope } from "./stage-work-graph.js";
|
|
3
|
+
import { PreparedRegisteredMemoryCompositionWorkResult } from "./memory-composition-work-custody.js";
|
|
4
|
+
import { prepareMemoryReviewWorkInput } from "./memory-review-work-input.js";
|
|
5
|
+
import { prepareExternalMemoryRepairCopyFromStage, restoreStartedMemoryRepairCopyFromStage, prepareMemoryRepairDispatchFromStage, prepareMemorySecondRepairDispatchFromStage, prepareMemorySecondRepairLaunchFromStage, restoreStartedMemorySecondRepairLaunchFromStage } from "./memory-review-handler.js";
|
|
6
|
+
import { PreparedSecondMemoryRepairLaunch } from "./memory-second-repair-launch.js";
|
|
7
|
+
import { PreparedRegisteredMemoryReviewWorkResult } from "./memory-review-work-custody.js";
|
|
8
|
+
import { PreparedPublishedMemoryRepairWorkResult } from "./memory-repair-work-custody.js";
|
|
9
|
+
import { prepareMemoryRepairWorkCompletion, publishMemoryRepairWorkCompletion } from "./memory-repair-work-completion.js";
|
|
10
|
+
import { prepareRegisteredMemoryWorkResult } from "./memory-work-result-preparation.js";
|
|
11
|
+
import { hybridDigest } from "./hybrid-contracts.js";
|
|
12
|
+
import { memoryRepairExecutionIdentity } from "./memory-repair-physical-binding.js";
|
|
13
|
+
import { prepareMemoryNativeReviewCopy } from "./memory-native-review-copy.js";
|
|
14
|
+
import { readDescriptorStageInput } from "./descriptor-stage-coordinator.js";
|
|
3
15
|
import { runtimeCommand } from "./runtime-command.js";
|
|
4
16
|
import crypto from "node:crypto";
|
|
5
17
|
import fs from "node:fs";
|
|
@@ -14,8 +26,8 @@ import { prepareAgentOutputFile } from "../storage/agent-output.js";
|
|
|
14
26
|
import { refreshRunSessionProjection } from "./run-projection.js";
|
|
15
27
|
import { assertSourceRepairEligible, ignoredWorkspacePaths, normalizeWorkspacePath, readCodeCheckProfile, runCodeChecks, validateCheckDeclaration, validateFailedCheckRetry, workspaceFingerprint } from "./code-checks.js";
|
|
16
28
|
import { nextWorkId, nextWorkIds } from "./ids.js";
|
|
17
|
-
import { appendFlowRunTimelineEvent } from "./runs.js";
|
|
18
|
-
import { resolveStageTransition } from "./execution-policy.js";
|
|
29
|
+
import { appendFlowRunTimelineEvent, publishRunProjections, assertRunBundleSqlAuthority, assertPreparedRunBundleSqlAuthority, readFlowRunExecutionBundle } from "./runs.js";
|
|
30
|
+
import { resolveStageTransition, runtimeHarness } from "./execution-policy.js";
|
|
19
31
|
import { appendAudit } from "./audit.js";
|
|
20
32
|
import { flowCommand } from "./stage-pause.js";
|
|
21
33
|
import { assertPortableArtifactRef } from "./portable-refs.js";
|
|
@@ -28,7 +40,7 @@ import { acceptedDecisionPrecedence, loadExternalStageContext, renderRetainedSta
|
|
|
28
40
|
import { reviewEnvironmentAllowed, reviewExecutionCopyHash, reviewSourceHash, reviewSourcePaths } from "./review-copy.js";
|
|
29
41
|
import { latestRunVerificationMatrix, verificationMatrixInstruction, verificationMatrixContract } from "./verification-matrix.js";
|
|
30
42
|
const workColumns = "work_id, project_id, run_id, parent_work_id, task, launch_policy, result_schema, payload_json, depends_on_json, status, result, created_at, started_at, updated_at, completed_at";
|
|
31
|
-
export function ensureWorkRegistry(context) { context.db.
|
|
43
|
+
export function ensureWorkRegistry(context) { context.db.get("SELECT 1 FROM works LIMIT 1"); context.db.get("SELECT 1 FROM work_sessions LIMIT 1"); }
|
|
32
44
|
function ownsPhysicalWorkSession(context, projectId, owner, identity) {
|
|
33
45
|
if (identity.sessionId !== owner.session_id)
|
|
34
46
|
return false;
|
|
@@ -50,6 +62,15 @@ export function assertStageLifecycleOwner(context, input) {
|
|
|
50
62
|
if (!project)
|
|
51
63
|
throw new AppError("not_found", "Project is not registered", 1, { project_root: projectRoot });
|
|
52
64
|
requireRun(context, project.id, input.runId);
|
|
65
|
+
const runIndex = JSON.parse(context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [project.id, input.runId]).index_json);
|
|
66
|
+
const boundScope = assertRunBundleSqlAuthority(context, { projectId: project.id, runId: input.runId }, runIndex.execution_bundle)
|
|
67
|
+
? stageGraphScope(context, project.id, input.runId, input.stage) : null;
|
|
68
|
+
if (boundScope && (runIndex.current_stage !== input.stage || !runIndex.stage_runs.some(stage => stage.stage === input.stage))) {
|
|
69
|
+
throw new AppError("trusted_session_binding_required", "Stage lifecycle command does not belong to the current frozen stage", 1, { run_id: input.runId, stage: input.stage, reason: "inactive_stage" });
|
|
70
|
+
}
|
|
71
|
+
const boundOwnerClause = boundScope ? `AND s.run_id = ? AND s.current_stage = ?
|
|
72
|
+
AND EXISTS (SELECT 1 FROM stage_work_memberships m WHERE m.project_id = w.project_id AND m.run_id = w.run_id
|
|
73
|
+
AND m.work_id = w.work_id AND m.stage = ? AND m.attempt = ? AND m.cycle = ?)` : "";
|
|
53
74
|
const scopedWork = input.workId ? requireWork(context, input.workId, { projectId: project.id, runId: input.runId }) : undefined;
|
|
54
75
|
let owner = context.db.get(`SELECT ws.id, ws.work_id, ws.session_id, ws.hook_event_id
|
|
55
76
|
FROM work_sessions ws
|
|
@@ -57,8 +78,9 @@ export function assertStageLifecycleOwner(context, input) {
|
|
|
57
78
|
JOIN sessions s ON s.project_id = w.project_id AND s.session_id = ws.session_id
|
|
58
79
|
WHERE w.project_id = ? AND w.run_id = ? AND ws.status = 'running'
|
|
59
80
|
AND w.status IN ('running', 'paused') AND (w.parent_work_id IS NULL OR s.current_stage = ?)
|
|
81
|
+
${boundOwnerClause}
|
|
60
82
|
${scopedWork ? "AND w.work_id = ?" : ""}
|
|
61
|
-
ORDER BY ws.created_at DESC LIMIT 1`,
|
|
83
|
+
ORDER BY ws.created_at DESC LIMIT 1`, [project.id, input.runId, input.stage, ...(boundScope ? [input.runId, input.stage, input.stage, boundScope.attempt, boundScope.cycle] : []), ...(scopedWork ? [scopedWork.work_id] : [])]);
|
|
62
84
|
if (!owner && input.operation === "stage_finish" && ["code", "code-review"].includes(input.stage)) {
|
|
63
85
|
const row = context.db.get("SELECT run_root, index_json FROM runs WHERE project_id = ? AND id = ?", [project.id, input.runId]);
|
|
64
86
|
const index = row ? JSON.parse(row.index_json) : null;
|
|
@@ -83,6 +105,11 @@ export function assertStageLifecycleOwner(context, input) {
|
|
|
83
105
|
: [project.id, input.runId, path.join(root, completed.data)]);
|
|
84
106
|
}
|
|
85
107
|
}
|
|
108
|
+
// Completed-stage replay branches above retain their historical receipts, but
|
|
109
|
+
// cannot borrow a Session or coordinator that has moved to another bound scope.
|
|
110
|
+
if (owner && boundScope && (!context.db.get("SELECT 1 FROM sessions WHERE project_id = ? AND session_id = ? AND run_id = ? AND current_stage = ?", [project.id, owner.session_id, input.runId, input.stage])
|
|
111
|
+
|| !context.db.get("SELECT 1 FROM stage_work_memberships WHERE project_id = ? AND run_id = ? AND work_id = ? AND stage = ? AND attempt = ? AND cycle = ?", [project.id, input.runId, owner.work_id, input.stage, boundScope.attempt, boundScope.cycle])))
|
|
112
|
+
owner = undefined;
|
|
86
113
|
if (!owner)
|
|
87
114
|
throw new AppError("trusted_session_binding_required", "Stage lifecycle command has no running coordinator Work/Session binding", 1, { run_id: input.runId, stage: input.stage, ...(input.workId ? { work_id: input.workId } : {}) });
|
|
88
115
|
const identity = claimStageLifecycleHookEvent(context, {
|
|
@@ -157,6 +184,7 @@ export function validateWorkBatch(value) {
|
|
|
157
184
|
assertNoParentCycles(items.filter((item) => Boolean(item.parent)).map((item) => ({ id: item.key, parent: item.parent })));
|
|
158
185
|
}
|
|
159
186
|
export function prepareWorkBatch(context, input) {
|
|
187
|
+
validateBatchRequest(input);
|
|
160
188
|
if (input.file !== undefined && input.value !== undefined)
|
|
161
189
|
throw new AppError("usage", "Work batch accepts either file or value, not both", 2);
|
|
162
190
|
if (input.file === undefined && input.value === undefined)
|
|
@@ -191,11 +219,20 @@ function prepareBatchParent(context, parentWorkId, items) {
|
|
|
191
219
|
return parent;
|
|
192
220
|
}
|
|
193
221
|
export function addWorkBatch(context, input) {
|
|
222
|
+
validateBatchRequest(input);
|
|
194
223
|
const items = input.prepared ?? prepareWorkBatch(context, input);
|
|
195
224
|
return context.db.writeTransaction(() => {
|
|
196
225
|
// Only mutable authority/references are rechecked under the write lock.
|
|
197
226
|
// Caller bytes were read once, before admission, and are never reopened.
|
|
198
227
|
const parent = prepareBatchParent(context, input.parentWorkId, items);
|
|
228
|
+
const registration = input.requestKey ? batchRegistration(context, input, parent, items) : null;
|
|
229
|
+
if (registration?.prior) {
|
|
230
|
+
// The SQL commit may have succeeded while its prior projection failed.
|
|
231
|
+
// An unchanged index must still republish derived files on owned replay.
|
|
232
|
+
context.db.afterCommit(() => publishRunProjections(context, parent.project_id, parent.run_id));
|
|
233
|
+
scheduleObsoleteWorkProjectionCleanup(context, parent.project_id, parent.run_id);
|
|
234
|
+
return { ok: true, work_ids: registration.prior, replayed: true };
|
|
235
|
+
}
|
|
199
236
|
const now = context.now();
|
|
200
237
|
const allocated = nextWorkIds(context, parent.project_id, items.map((item) => item.key));
|
|
201
238
|
const ids = new Map(items.map((item, index) => [item.key, allocated[index]]));
|
|
@@ -205,7 +242,9 @@ export function addWorkBatch(context, input) {
|
|
|
205
242
|
assertNoParentCycles(proposed.map((item) => ({ id: item.id, parent: item.parentId })));
|
|
206
243
|
for (const item of proposed)
|
|
207
244
|
context.db.run(`INSERT INTO works (work_id, project_id, run_id, parent_work_id, task, launch_policy, result_schema, payload_json, depends_on_json, status, result, started_at, created_at, updated_at, completed_at)
|
|
208
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'created', NULL, NULL, ?, ?, NULL)`, [item.id, parent.project_id, parent.run_id, item.parentId, item.task, item.launch_policy ?? "reuse_allowed", item.result_schema ?? null,
|
|
245
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'created', NULL, NULL, ?, ?, NULL)`, [item.id, parent.project_id, parent.run_id, item.parentId, item.task, item.launch_policy ?? "reuse_allowed", item.result_schema ?? null,
|
|
246
|
+
registration ? JSON.stringify({ ...item.payload, _dd_flow_batch_request: { request_sha256: registration.requestSha256, batch_sha256: registration.batchSha256, item_key: item.key } })
|
|
247
|
+
: item.payload ? JSON.stringify(item.payload) : null, JSON.stringify(item.dependencies), now, now]);
|
|
209
248
|
const run = context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [parent.project_id, parent.run_id]);
|
|
210
249
|
const stages = run ? (JSON.parse(run.index_json).stage_runs ?? []) : [];
|
|
211
250
|
const ownerStage = input.stage ?? [...stages].reverse().find(stage => stage.status === "running")?.stage;
|
|
@@ -215,9 +254,115 @@ export function addWorkBatch(context, input) {
|
|
|
215
254
|
appendFlowRunTimelineEvent(context, parent.project_id, parent.run_id, { type: "work_materialized", work_id: item.id, parent_work_id: item.parentId, depends_on: item.dependencies, launch_policy: item.launch_policy ?? "reuse_allowed" });
|
|
216
255
|
}
|
|
217
256
|
refreshRunWorkProjection(context, parent.project_id, parent.run_id);
|
|
218
|
-
|
|
257
|
+
const workIds = Object.fromEntries(ids);
|
|
258
|
+
if (registration)
|
|
259
|
+
appendAudit(context, { projectId: parent.project_id, eventType: "work.batch_registration", payload: {
|
|
260
|
+
run_id: parent.run_id, scope: registration.scope, request_sha256: registration.requestSha256, batch_sha256: registration.batchSha256, work_ids: workIds,
|
|
261
|
+
work_sha256: Object.fromEntries(proposed.map(item => [item.id, immutableBatchWorkDigest(requireWork(context, item.id))]))
|
|
262
|
+
} });
|
|
263
|
+
return { ok: true, work_ids: workIds, ...(registration ? { replayed: false } : {}) };
|
|
219
264
|
});
|
|
220
265
|
}
|
|
266
|
+
function immutableBatchWorkDigest(work) {
|
|
267
|
+
return hybridDigest({ work_id: work.work_id, project_id: work.project_id, run_id: work.run_id,
|
|
268
|
+
parent_work_id: work.parent_work_id, task: work.task, launch_policy: work.launch_policy,
|
|
269
|
+
result_schema: work.result_schema, payload: work.payload_json === null ? null : JSON.parse(work.payload_json),
|
|
270
|
+
depends_on: JSON.parse(work.depends_on_json) });
|
|
271
|
+
}
|
|
272
|
+
/** Independent allocation custody only: this does not attest Session settlement,
|
|
273
|
+
* semantic acceptance, or that the retained stage is still active. Historical
|
|
274
|
+
* receipts without immutable hashes remain replayable but cannot grant custody.
|
|
275
|
+
*/
|
|
276
|
+
export function readWorkBatchCustody(context, id) {
|
|
277
|
+
const work = requireWork(context, id);
|
|
278
|
+
try {
|
|
279
|
+
const marker = JSON.parse(work.payload_json ?? "null")?._dd_flow_batch_request;
|
|
280
|
+
if (!marker || typeof marker.request_sha256 !== "string" || typeof marker.batch_sha256 !== "string" || typeof marker.item_key !== "string")
|
|
281
|
+
throw new Error("Missing ownership marker");
|
|
282
|
+
const rows = context.db.all("SELECT payload_json FROM audit_events WHERE project_id = ? AND event_type = 'work.batch_registration' AND json_extract(payload_json, '$.request_sha256') = ?", [work.project_id, marker.request_sha256]);
|
|
283
|
+
if (rows.length !== 1 || Buffer.byteLength(rows[0].payload_json) > 1024 * 1024)
|
|
284
|
+
throw new Error("Missing or ambiguous receipt");
|
|
285
|
+
const receipt = JSON.parse(rows[0].payload_json);
|
|
286
|
+
const scope = receipt.scope;
|
|
287
|
+
if (!scope || scope.project_id !== work.project_id || scope.run_id !== work.run_id || receipt.run_id !== work.run_id
|
|
288
|
+
|| typeof scope.stage !== "string" || typeof scope.attempt !== "string" || typeof scope.cycle !== "string"
|
|
289
|
+
|| typeof scope.parent_work_id !== "string" || typeof scope.request_key !== "string"
|
|
290
|
+
|| receipt.request_sha256 !== marker.request_sha256 || hybridDigest(scope) !== marker.request_sha256
|
|
291
|
+
|| receipt.batch_sha256 !== marker.batch_sha256
|
|
292
|
+
|| !Object.hasOwn(receipt.work_ids ?? {}, marker.item_key) || receipt.work_ids[marker.item_key] !== work.work_id
|
|
293
|
+
|| !Object.hasOwn(receipt.work_sha256 ?? {}, work.work_id) || receipt.work_sha256[work.work_id] !== immutableBatchWorkDigest(work)
|
|
294
|
+
|| !context.db.get("SELECT 1 FROM works WHERE project_id = ? AND run_id = ? AND work_id = ?", [work.project_id, work.run_id, scope.parent_work_id])
|
|
295
|
+
|| !context.db.get("SELECT 1 FROM works WHERE project_id = ? AND run_id = ? AND work_id = ?", [work.project_id, work.run_id, work.parent_work_id])
|
|
296
|
+
|| !context.db.get("SELECT 1 FROM stage_work_memberships WHERE project_id = ? AND run_id = ? AND work_id = ? AND stage = ? AND attempt = ? AND cycle = ?", [work.project_id, work.run_id, work.work_id, scope.stage, scope.attempt, scope.cycle]))
|
|
297
|
+
throw new Error("Changed allocation binding");
|
|
298
|
+
return { work_id: work.work_id, item_key: marker.item_key, scope: { ...scope },
|
|
299
|
+
work_ids: { ...receipt.work_ids },
|
|
300
|
+
request_sha256: marker.request_sha256, batch_sha256: marker.batch_sha256,
|
|
301
|
+
work_sha256: receipt.work_sha256[work.work_id] };
|
|
302
|
+
}
|
|
303
|
+
catch {
|
|
304
|
+
throw new AppError("work_batch_custody_invalid", "Independent batch receipt cannot attest this Work allocation", 1, { work_id: work.work_id });
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
function validateBatchRequest(input) {
|
|
308
|
+
if (input.requestKey === undefined)
|
|
309
|
+
return;
|
|
310
|
+
if (typeof input.requestKey !== "string" || !/^[A-Za-z0-9][A-Za-z0-9_.-]{0,95}$/.test(input.requestKey)
|
|
311
|
+
|| typeof input.stage !== "string" || !/^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$/.test(input.stage))
|
|
312
|
+
throw new AppError("work_batch_request_invalid", "Replayable batch requires a bounded request key and explicit stage", 2);
|
|
313
|
+
}
|
|
314
|
+
/** Optional request-owned registration for generic flows. Receipt and Work allocation
|
|
315
|
+
* commit in one writer transaction; mutable Work state may advance on replay, but
|
|
316
|
+
* immutable task/dependency/scope bytes may not be replaced or rediscovered.
|
|
317
|
+
*/
|
|
318
|
+
function batchRegistration(context, input, parent, items) {
|
|
319
|
+
const scope = stageGraphScope(context, parent.project_id, parent.run_id, input.stage);
|
|
320
|
+
const run = context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [parent.project_id, parent.run_id]);
|
|
321
|
+
const index = run ? JSON.parse(run.index_json) : null;
|
|
322
|
+
if (!index || index.current_stage !== input.stage || !index.stage_runs.some(stage => stage.stage === input.stage && stage.status === "running")
|
|
323
|
+
|| input.cycle !== undefined && input.cycle !== scope.cycle
|
|
324
|
+
|| index.execution_bundle && !stageWorkIds(context, parent.project_id, parent.run_id, input.stage).has(parent.work_id)) {
|
|
325
|
+
throw new AppError("work_batch_scope_invalid", "Batch registration does not belong to the active coordinator stage", 1);
|
|
326
|
+
}
|
|
327
|
+
if (items.some(item => item.payload && Object.hasOwn(item.payload, "_dd_flow_batch_request")))
|
|
328
|
+
throw new AppError("work_batch_request_invalid", "Batch ownership marker is CLI-owned", 2);
|
|
329
|
+
const requestScope = { ...scope, stage: input.stage, parent_work_id: parent.work_id, request_key: input.requestKey };
|
|
330
|
+
const requestSha256 = hybridDigest(requestScope);
|
|
331
|
+
const batchSha256 = hybridDigest(items);
|
|
332
|
+
const rows = context.db.all("SELECT payload_json FROM audit_events WHERE project_id = ? AND event_type = 'work.batch_registration' AND json_extract(payload_json, '$.request_sha256') = ?", [parent.project_id, requestSha256]);
|
|
333
|
+
const owned = context.db.all(`SELECT ${workColumns} FROM works WHERE project_id = ? AND run_id = ? AND json_extract(payload_json, '$._dd_flow_batch_request.request_sha256') = ?`, [parent.project_id, parent.run_id, requestSha256]);
|
|
334
|
+
const invalid = (code) => { throw new AppError(code, "Retained batch registration cannot authorize allocation or replay", 1); };
|
|
335
|
+
if (!rows.length) {
|
|
336
|
+
if (owned.length)
|
|
337
|
+
invalid("work_batch_receipt_missing");
|
|
338
|
+
return { requestSha256, batchSha256, scope: requestScope, prior: null };
|
|
339
|
+
}
|
|
340
|
+
if (rows.length !== 1 || Buffer.byteLength(rows[0].payload_json, "utf8") > 1024 * 1024)
|
|
341
|
+
invalid("work_batch_receipt_invalid");
|
|
342
|
+
const receipt = JSON.parse(rows[0].payload_json);
|
|
343
|
+
if (receipt.run_id !== parent.run_id || hybridDigest(receipt.scope) !== requestSha256)
|
|
344
|
+
invalid("work_batch_receipt_invalid");
|
|
345
|
+
if (receipt.batch_sha256 !== batchSha256)
|
|
346
|
+
invalid("work_batch_request_conflict");
|
|
347
|
+
const ids = receipt.work_ids;
|
|
348
|
+
if (!ids || typeof ids !== "object" || Array.isArray(ids) || Object.keys(ids).length !== items.length
|
|
349
|
+
|| new Set(Object.values(ids)).size !== items.length || owned.length !== items.length)
|
|
350
|
+
invalid("work_batch_receipt_invalid");
|
|
351
|
+
const resolve = (key) => Object.hasOwn(ids, key) ? ids[key] : key;
|
|
352
|
+
for (const item of items) {
|
|
353
|
+
if (!Object.hasOwn(ids, item.key))
|
|
354
|
+
invalid("work_batch_receipt_invalid");
|
|
355
|
+
const work = owned.find(row => row.work_id === ids[item.key]);
|
|
356
|
+
if (!work || work.parent_work_id !== (item.parent ? resolve(item.parent) : parent.work_id) || work.task !== item.task
|
|
357
|
+
|| work.launch_policy !== (item.launch_policy ?? "reuse_allowed") || work.result_schema !== (item.result_schema ?? null)
|
|
358
|
+
|| hybridDigest(JSON.parse(work.depends_on_json)) !== hybridDigest((item.depends_on ?? []).map(resolve))
|
|
359
|
+
|| hybridDigest(JSON.parse(work.payload_json)) !== hybridDigest({ ...item.payload, _dd_flow_batch_request: { request_sha256: requestSha256, batch_sha256: batchSha256, item_key: item.key } })
|
|
360
|
+
|| receipt.work_sha256 !== undefined && (!Object.hasOwn(receipt.work_sha256, work.work_id) || receipt.work_sha256[work.work_id] !== immutableBatchWorkDigest(work))
|
|
361
|
+
|| !context.db.get("SELECT 1 FROM stage_work_memberships WHERE project_id = ? AND run_id = ? AND work_id = ? AND stage = ? AND attempt = ? AND cycle = ?", [parent.project_id, parent.run_id, work.work_id, input.stage, scope.attempt, scope.cycle]))
|
|
362
|
+
invalid("work_batch_receipt_invalid");
|
|
363
|
+
}
|
|
364
|
+
return { requestSha256, batchSha256, scope: requestScope, prior: ids };
|
|
365
|
+
}
|
|
221
366
|
export function listWorks(context, input) {
|
|
222
367
|
ensureWorkRegistry(context);
|
|
223
368
|
if (!input.runId && !input.parentWorkId)
|
|
@@ -241,7 +386,7 @@ export function listWorks(context, input) {
|
|
|
241
386
|
const filtered = input.ready ? rows.filter(ready) : rows;
|
|
242
387
|
return { works: filtered.slice(0, input.limit ?? filtered.length).map((work) => {
|
|
243
388
|
const isReadyNow = ready(work);
|
|
244
|
-
return { ...work, payload: parsePayload(work), payload_json: undefined, depends_on: parseDependencies(work), ready: isReadyNow, ...(isReadyNow ? { start_command: workStartCommand(context, work) } : {}), ...(input.includeResults ? {} : { result: undefined }) };
|
|
389
|
+
return { ...work, payload: parsePayload(work), payload_json: undefined, depends_on: parseDependencies(work), ready: isReadyNow, ...(isReadyNow ? { start_command: workStartCommand(context, work, { preview: true }) } : {}), ...(input.includeResults ? {} : { result: undefined }) };
|
|
245
390
|
}) };
|
|
246
391
|
}
|
|
247
392
|
export function showWork(context, id) { const work = requireWork(context, id); const sessions = context.db.all("SELECT ws.id, ws.work_id, ws.session_id, ws.hook_event_id, ws.status, ws.prompt_path, ws.result_path, ws.created_at, ws.completed_at, s.harness AS harness_id, COALESCE(s.provider_session_id, s.session_id) AS native_session_id FROM work_sessions ws LEFT JOIN sessions s ON s.project_id = ? AND s.session_id = ws.session_id WHERE ws.work_id = ? ORDER BY ws.created_at", [work.project_id, work.work_id]); return { work: { ...work, short_id: shortWorkId(work.work_id), payload: parsePayload(work), payload_json: undefined, depends_on: parseDependencies(work), sessions: sessions.map(({ session_id, harness_id, native_session_id, ...session }) => ({ ...session, ...(harness_id && native_session_id ? { session: publicSessionIdentity({ harness: harness_id, provider_session_id: native_session_id, session_id }) } : {}) })) } }; }
|
|
@@ -429,6 +574,8 @@ export function prepareWorkDeps(context, input) {
|
|
|
429
574
|
if (dependencies.has(work.work_id))
|
|
430
575
|
throw new AppError("validation", "Work cannot depend on itself", 2);
|
|
431
576
|
assertNoCycles(context.db.all(`SELECT ${workColumns} FROM works WHERE project_id = ? AND run_id = ?`, [work.project_id, work.run_id]).map((row) => ({ id: row.work_id, dependencies: row.work_id === work.work_id ? [...dependencies] : parseDependencies(row) })));
|
|
577
|
+
if (parsePayload(work)?._dd_flow_batch_request && hybridDigest([...dependencies]) !== hybridDigest(parseDependencies(work)))
|
|
578
|
+
throw new AppError("validation", "This Work belongs to a registered batch: use its registered graph; dependencies are declared during allocation", 2, { phase: "prepare", effect: "no_effect", recoverable: true, work_id: work.work_id, depends_on: parseDependencies(work) });
|
|
432
579
|
return { work, dependencies: [...dependencies] };
|
|
433
580
|
}
|
|
434
581
|
export function prepareWorkDelete(context, id) {
|
|
@@ -453,8 +600,11 @@ function rawWorkStartCommand(context, work) {
|
|
|
453
600
|
const run = requireRun(context, work.project_id, work.run_id), recovery = recoveryGuard(context, work.project_id, work.run_id);
|
|
454
601
|
return runtimeCommand(context, ["work", "start", shortWorkId(work.work_id), "--project-root", run.project_root, ...(recovery ? ["--recovery-id", recovery.recovery_id] : []), "--json"]);
|
|
455
602
|
}
|
|
456
|
-
export function workStartCommand(context, work) {
|
|
603
|
+
export function workStartCommand(context, work, options = {}) {
|
|
457
604
|
const command = rawWorkStartCommand(context, work);
|
|
605
|
+
// Graph/readiness projections do not choose the future executor's scope.
|
|
606
|
+
if (options.preview)
|
|
607
|
+
return command;
|
|
458
608
|
const retained = context.db.get("SELECT status,current_start_invocation_id FROM works WHERE work_id = ?", [work.work_id]);
|
|
459
609
|
if (retained?.current_start_invocation_id) {
|
|
460
610
|
const prepared = prepareWorkStartAuthority(context, command, retained.current_start_invocation_id);
|
|
@@ -489,9 +639,19 @@ export function assertWorkLaunchReady(context, id) {
|
|
|
489
639
|
throw new AppError("invalid_work_state", "Work is not created", 2, { work_id: id, status: work.status });
|
|
490
640
|
if (!isReady(context, work)) {
|
|
491
641
|
const blockers = readinessBlockers(context, work);
|
|
492
|
-
const ready = context.db.all(`SELECT ${workColumns} FROM works WHERE project_id = ? AND run_id = ? AND status = 'created' ORDER BY created_at, work_id`, [work.project_id, work.run_id]).filter((candidate) => currentStageWorkIds(context, candidate.project_id, candidate.run_id)?.has(candidate.work_id) !== false && isReady(context, candidate)).map((candidate) => ({ work_id: candidate.work_id, start_command: workStartCommand(context, candidate) }));
|
|
642
|
+
const ready = context.db.all(`SELECT ${workColumns} FROM works WHERE project_id = ? AND run_id = ? AND status = 'created' ORDER BY created_at, work_id`, [work.project_id, work.run_id]).filter((candidate) => currentStageWorkIds(context, candidate.project_id, candidate.run_id)?.has(candidate.work_id) !== false && isReady(context, candidate)).map((candidate) => ({ work_id: candidate.work_id, start_command: workStartCommand(context, candidate, { preview: true }) }));
|
|
493
643
|
throw new AppError("work_not_ready", "Work cannot start until dependencies complete and overlapping planned coordination areas are free", 2, { work_id: id, blockers, ready });
|
|
494
644
|
}
|
|
645
|
+
const run = requireRun(context, work.project_id, work.run_id);
|
|
646
|
+
const wave = registeredMemoryRepairWave(context, work, run);
|
|
647
|
+
if (wave) {
|
|
648
|
+
const launched = context.db.get("SELECT 1 FROM sqlite_master WHERE type='table' AND name='work_launches'")
|
|
649
|
+
&& context.db.get("SELECT 1 FROM work_launches WHERE work_id=?", [work.work_id]);
|
|
650
|
+
if (launched)
|
|
651
|
+
prepareMemoryRepairStartOwner(context, work, run, wave);
|
|
652
|
+
else
|
|
653
|
+
(wave === 1 ? prepareMemoryRepairDispatchFromStage : prepareMemorySecondRepairDispatchFromStage)(context, { projectRoot: run.project_root, runId: work.run_id, workId: work.work_id });
|
|
654
|
+
}
|
|
495
655
|
if (parsePayload(work)?.read_only === true)
|
|
496
656
|
assertSharedReviewInput(context, work, requireRun(context, work.project_id, work.run_id));
|
|
497
657
|
return work;
|
|
@@ -563,39 +723,105 @@ export function bindRunningWorkSession(context, input) {
|
|
|
563
723
|
refreshRunWorkProjection(context, work.project_id, work.run_id);
|
|
564
724
|
return { work_session_id: id, session_id: identity.sessionId };
|
|
565
725
|
}
|
|
726
|
+
function preparedCoordinatorRoute(context, work, input, snapshot) {
|
|
727
|
+
const bound = assertRunBundleSqlAuthority(context, { projectId: work.project_id, runId: work.run_id }, snapshot.execution_bundle);
|
|
728
|
+
const admitted = context.stageBindingBundle;
|
|
729
|
+
if (admitted && !bound)
|
|
730
|
+
throw new AppError("run_bundle_anchor_invalid", "Prepared coordinator bundle lost its RUN anchor", 1);
|
|
731
|
+
if (bound && (!admitted || admitted.stage !== input.stage || admitted.contextSha256 !== input.contextSha256))
|
|
732
|
+
throw new AppError("frozen_stage_binding_required", "Coordinator binding requires its prepared frozen stage bundle", 1);
|
|
733
|
+
if (bound) {
|
|
734
|
+
// The request-local bundle was admitted before coordinator dispatch. The
|
|
735
|
+
// independently retained RUN authority fences it again without compiling
|
|
736
|
+
// descriptor schemas under this (possibly caller-owned) SQL writer.
|
|
737
|
+
assertPreparedRunBundleSqlAuthority(context, { projectId: work.project_id, runId: work.run_id }, admitted.bundle);
|
|
738
|
+
if (admitted.bundle.descriptor.descriptor.id === "mb-init") {
|
|
739
|
+
const retained = readDescriptorStageInput(context, { projectId: work.project_id, runId: work.run_id, stage: input.stage }, admitted.bundle);
|
|
740
|
+
const payload = retained.input.payload_base64;
|
|
741
|
+
if (typeof payload !== "string")
|
|
742
|
+
throw new AppError("stage_context_binding_invalid", "Memory Stage context has no retained payload", 1);
|
|
743
|
+
const bytes = Buffer.from(payload, "base64");
|
|
744
|
+
if (bytes.toString("base64") !== payload || (bytes.length ? sha256(bytes) !== input.contextSha256 : input.contextSha256 !== undefined))
|
|
745
|
+
throw new AppError("stage_context_binding_invalid", "Coordinator context checksum differs from retained Memory Stage bytes", 1);
|
|
746
|
+
}
|
|
747
|
+
const currentStage = snapshot.current_stage ?? (admitted.bundle.descriptor.descriptor.id === "sdlc" ? snapshot.stage_runs.at(-1)?.stage : undefined);
|
|
748
|
+
if (currentStage !== input.stage || !snapshot.stage_runs.some(stage => stage.stage === input.stage && stage.status === "running")
|
|
749
|
+
|| !context.db.get("SELECT 1 FROM works WHERE project_id = ? AND run_id = ? AND work_id = ?", [work.project_id, work.run_id, work.work_id])
|
|
750
|
+
|| !stageWorkIds(context, work.project_id, work.run_id, input.stage).has(work.work_id))
|
|
751
|
+
throw new AppError("stage_work_scope_invalid", "Coordinator does not belong to the active frozen stage", 1);
|
|
752
|
+
}
|
|
753
|
+
return bound ? admitted.bundle.execution.stage(input.stage) : null;
|
|
754
|
+
}
|
|
566
755
|
/** A stage coordinator is launched by `stage start`, not by an invented nested `work start`. */
|
|
567
756
|
export function bindStageCoordinatorWork(context, input) {
|
|
568
757
|
const work = requireWork(context, input.workId);
|
|
569
758
|
assertRunMutationAllowed(context, work.project_id, work.run_id);
|
|
570
759
|
const run = requireRun(context, work.project_id, work.run_id);
|
|
571
760
|
const snapshot = JSON.parse((context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [work.project_id, work.run_id])?.index_json) ?? "{}");
|
|
572
|
-
const
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
if (
|
|
576
|
-
|
|
577
|
-
const
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
761
|
+
const frozenRoute = preparedCoordinatorRoute(context, work, input, snapshot), bound = frozenRoute !== null;
|
|
762
|
+
const bind = () => {
|
|
763
|
+
assertRunMutationAllowed(context, work.project_id, work.run_id);
|
|
764
|
+
if (bound)
|
|
765
|
+
preparedCoordinatorRoute(context, work, input, JSON.parse(context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [work.project_id, work.run_id]).index_json));
|
|
766
|
+
const profile = snapshot.execution_profile;
|
|
767
|
+
if (!bound && profile && input.contextSha256) {
|
|
768
|
+
const file = path.join(path.dirname(input.promptPath), "stage-context.json");
|
|
769
|
+
if (!fs.existsSync(file) || sha256(fs.readFileSync(file, "utf8")) !== input.contextSha256)
|
|
770
|
+
throw new AppError("context_checksum_mismatch", "Retained Stage context changed before coordinator binding", 2);
|
|
771
|
+
const item = { path: path.relative(requireRunHome(run), file), sha256: input.contextSha256 };
|
|
772
|
+
const prior = profile.stage_context_inputs?.[input.stage];
|
|
773
|
+
if (prior && JSON.stringify(prior) !== JSON.stringify(item))
|
|
774
|
+
throw new AppError("stage_context_replacement", "Stage already has a different accepted context", 2);
|
|
775
|
+
profile.stage_context_inputs = { ...profile.stage_context_inputs, [input.stage]: item };
|
|
776
|
+
context.db.run("UPDATE runs SET index_json = ? WHERE id = ? AND project_id = ?", [JSON.stringify(snapshot), run.id, work.project_id]);
|
|
777
|
+
}
|
|
778
|
+
// A stage may settle its Session while the RUN's root Work remains running.
|
|
779
|
+
// Completion must not erase the frozen handoff policy for the next stage.
|
|
780
|
+
const previous = context.db.get(`SELECT ws.id, ws.status, ws.session_id, ws.hook_event_id, s.current_stage, ws.result_path FROM work_sessions ws JOIN sessions s ON s.project_id = ? AND s.session_id = ws.session_id WHERE ws.work_id = ? AND ${bound ? "ws.status IN ('running', 'completed')" : "ws.status = 'running'"} ORDER BY ws.created_at DESC, ws.rowid DESC LIMIT 1`, [work.project_id, work.work_id]);
|
|
781
|
+
let sessionMode = frozenRoute?.session_mode ?? profile?.settings.stage_session_mode ?? "same_session";
|
|
782
|
+
if (!bound && profile?.settings.execution) {
|
|
783
|
+
if (!profile.agent_profiles)
|
|
784
|
+
throw new AppError("execution_profile_not_frozen", "Stage handoff requires frozen RUN profiles", 1);
|
|
785
|
+
sessionMode = resolveStageTransition({ ddFlowHome: context.ddFlowHome, policy: profile.settings.execution, profiles: profile.agent_profiles, projectRoot: run.project_root, workspaceRoot: run.workspace_root, ...(previous?.current_stage ? { fromStage: previous.current_stage } : {}), stage: input.stage, stageSessionMode: sessionMode, mergeMode: profile.settings.merge_mode }).session_mode;
|
|
786
|
+
}
|
|
787
|
+
const identity = hookSessionIdentity(context, work.project_id, input.hookEventId);
|
|
788
|
+
if (frozenRoute && runtimeHarness(frozenRoute.profile) !== identity.harness)
|
|
789
|
+
throw new AppError("stage_profile_mismatch", "Coordinator hook belongs to another frozen harness route", 1);
|
|
790
|
+
if (bound && previous && sessionMode === "same_session" && !ownsPhysicalWorkSession(context, work.project_id, previous, identity)) {
|
|
791
|
+
// Boundary recovery intentionally retains the terminal WorkSession. Only
|
|
792
|
+
// its explicitly accepted current-generation replacement may change daemon.
|
|
793
|
+
const guard = recoveryGuard(context, work.project_id, work.run_id);
|
|
794
|
+
const recoveredOwner = previous.status === "completed" && previous.session_id === identity.sessionId && guard?.status === "recovered" && context.db.get(`SELECT b.recovery_id FROM run_recovery_bindings b JOIN hook_events h ON h.project_id = ? AND h.event_key = b.hook_event_key AND h.status = 'claimed' AND h.daemon_id = b.daemon_id AND h.session_id = b.session_id WHERE b.recovery_id = ? AND b.status = 'accepted' AND b.work_id = ? AND b.prior_work_session_id = ? AND b.session_id = ? AND b.harness = ? AND b.provider_session_id = ? AND b.daemon_id = ?`, [work.project_id, guard.recovery_id, work.work_id, previous.id, identity.sessionId, identity.harness, identity.providerSessionId, identity.daemonId ?? null]);
|
|
795
|
+
if (!recoveredOwner)
|
|
796
|
+
throw new AppError("handoff_session_mismatch", "Frozen same-session handoff must retain the preceding coordinator's physical Session", 1, { stage: input.stage });
|
|
797
|
+
}
|
|
798
|
+
if (frozenRoute?.session_mode === "new_session")
|
|
799
|
+
assertFreshCoordinatorSession(context, work, input.stage, identity);
|
|
800
|
+
if (previous && sessionMode === "new_session" && previous.session_id === identity.sessionId
|
|
801
|
+
&& (previous.current_stage && previous.current_stage !== input.stage || bound && path.resolve(previous.result_path ?? "") !== path.resolve(input.resultPath ?? "")))
|
|
802
|
+
throw new AppError("stage_handoff_required", "This stage requires a fresh coordinator Session under its frozen execution policy", 1, { from_stage: previous.current_stage, stage: input.stage });
|
|
803
|
+
claimStageStartHookEvent(context, { projectId: work.project_id, eventKey: input.hookEventId, runId: work.run_id, stage: input.stage, projectRoot: run.project_root, ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
|
|
804
|
+
const binding = bindRunningWorkSession(context, { ...input, allowStageHandoff: sessionMode === "new_session" });
|
|
805
|
+
context.db.run("UPDATE sessions SET current_stage = ?, updated_at = ? WHERE project_id = ? AND session_id = ?", [input.stage, context.now(), work.project_id, binding.session_id]);
|
|
806
|
+
if (frozenRoute)
|
|
807
|
+
appendAudit(context, { projectId: work.project_id, eventType: "stage.coordinator_binding", payload: {
|
|
808
|
+
run_id: work.run_id, scope: { ...stageGraphScope(context, work.project_id, work.run_id, input.stage), stage: input.stage },
|
|
809
|
+
work_id: work.work_id, work_session_id: binding.work_session_id, session_id: identity.sessionId,
|
|
810
|
+
harness: identity.harness, provider_session_id: identity.providerSessionId,
|
|
811
|
+
execution_sha256: context.stageBindingBundle.bundle.execution.sha256,
|
|
812
|
+
profile_id: frozenRoute.profile.id, profile_sha256: hybridDigest(frozenRoute.profile), session_mode: frozenRoute.session_mode,
|
|
813
|
+
...(input.contextSha256 ? { context_sha256: input.contextSha256 } : {})
|
|
814
|
+
} });
|
|
815
|
+
refreshRunSessionProjection(context, work.project_id, work.run_id);
|
|
816
|
+
return binding;
|
|
817
|
+
};
|
|
818
|
+
return bound ? context.db.writeTransaction(bind, "stage.bindFrozenCoordinator") : bind();
|
|
819
|
+
}
|
|
820
|
+
function assertFreshCoordinatorSession(context, work, stage, identity) {
|
|
821
|
+
const prior = context.db.get("SELECT 1 FROM audit_events WHERE project_id = ? AND event_type = 'stage.coordinator_binding' AND json_extract(payload_json, '$.run_id') = ? AND json_extract(payload_json, '$.harness') = ? AND json_extract(payload_json, '$.provider_session_id') = ? AND json_extract(payload_json, '$.scope.stage') <> ? LIMIT 1", [work.project_id, work.run_id, identity.harness, identity.providerSessionId, stage]);
|
|
822
|
+
const current = context.db.get("SELECT current_stage FROM sessions WHERE project_id = ? AND session_id = ? AND run_id = ?", [work.project_id, identity.sessionId, work.run_id]);
|
|
823
|
+
if (prior || current?.current_stage && current.current_stage !== stage)
|
|
824
|
+
throw new AppError("stage_handoff_required", "Frozen stage requires a coordinator Session not used by another stage", 1, { stage });
|
|
599
825
|
}
|
|
600
826
|
/** Starts a created coordinator Work from its trusted `stage start` event. */
|
|
601
827
|
export function startStageCoordinatorWork(context, input) {
|
|
@@ -606,12 +832,26 @@ export function startStageCoordinatorWork(context, input) {
|
|
|
606
832
|
const run = requireRun(context, work.project_id, work.run_id);
|
|
607
833
|
if (input.projectRoot && resolveProjectRoot(input.projectRoot) !== resolveProjectRoot(run.project_root))
|
|
608
834
|
throw new AppError("project_mismatch", "stage coordinator project root does not match its RUN", 1, { work_id: work.work_id });
|
|
609
|
-
const
|
|
835
|
+
const snapshot = JSON.parse(context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [work.project_id, work.run_id]).index_json);
|
|
836
|
+
const frozenRoute = preparedCoordinatorRoute(context, work, input, snapshot);
|
|
837
|
+
const binding = startBoundWork(context, work, run, () => {
|
|
838
|
+
if (frozenRoute) {
|
|
839
|
+
const current = JSON.parse(context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [work.project_id, work.run_id]).index_json);
|
|
840
|
+
preparedCoordinatorRoute(context, work, input, current);
|
|
841
|
+
const identity = hookSessionIdentity(context, work.project_id, input.hookEventId);
|
|
842
|
+
if (identity.harness !== runtimeHarness(frozenRoute.profile))
|
|
843
|
+
throw new AppError("stage_profile_mismatch", "Coordinator hook belongs to another frozen harness route", 1);
|
|
844
|
+
if (frozenRoute.session_mode === "new_session")
|
|
845
|
+
assertFreshCoordinatorSession(context, work, input.stage, identity);
|
|
846
|
+
}
|
|
847
|
+
return claimStageStartHookEvent(context, { projectId: work.project_id, eventKey: input.hookEventId, runId: work.run_id, stage: input.stage, projectRoot: run.project_root, ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
|
|
848
|
+
}, input.hookEventId, input.stage);
|
|
610
849
|
refreshRunSessionProjection(context, work.project_id, work.run_id);
|
|
611
850
|
return binding;
|
|
612
851
|
}
|
|
613
852
|
/** Claim, command authority and binding commit together; only packet projection follows. */
|
|
614
853
|
export function startBoundWork(context, work, run, claimIdentity, hookEventId, stage) {
|
|
854
|
+
const repairWave = registeredMemoryRepairWave(context, work, run);
|
|
615
855
|
const generation = assertRunMutationAllowed(context, work.project_id, work.run_id);
|
|
616
856
|
const directory = path.join(requireRunHome(run), "works", work.work_id);
|
|
617
857
|
const linkId = `WS-${crypto.randomUUID()}`;
|
|
@@ -621,12 +861,18 @@ export function startBoundWork(context, work, run, claimIdentity, hookEventId, s
|
|
|
621
861
|
const promptPath = path.join(packetDirectory, "prompt.md"), resultPath = path.join(directory, "result.json");
|
|
622
862
|
// The first reviewer freezes shared inputs in the same transaction as its claim.
|
|
623
863
|
const readOnly = parsePayload(work)?.read_only === true;
|
|
624
|
-
const
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
864
|
+
const repairPreparationContext = { ...context, env: { ...context.env, DD_FLOW_MEMORY_REPAIR_HOOK_EVENT: hookEventId } };
|
|
865
|
+
const preparedRepairCopy = repairWave ? prepareMemoryRepairStartOwner(repairPreparationContext, work, run, repairWave) : null;
|
|
866
|
+
const preparedMemoryReview = readOnly && parsePayload(work)?.kind === "memory-review"
|
|
867
|
+
? prepareMemoryReviewWorkInput(context, work, run, generation) : null;
|
|
868
|
+
const nativeReviewCopy = preparedMemoryReview?.bundle.execution.work("review").delegation.mode === "native"
|
|
869
|
+
? prepareMemoryNativeReviewCopy(context, work.work_id, false, preparedMemoryReview) : null;
|
|
870
|
+
const workerRun = preparedRepairCopy ? { ...run, workspace_root: preparedRepairCopy.root() }
|
|
871
|
+
: nativeReviewCopy ? { ...run, workspace_root: nativeReviewCopy.root } : readOnly && context.env.DD_FLOW_READ_ONLY_WORKSPACE
|
|
872
|
+
? { ...run, workspace_root: path.resolve(context.env.DD_FLOW_READ_ONLY_WORKSPACE) } : run;
|
|
873
|
+
const preparedReviewInput = preparedMemoryReview?.input ?? (readOnly ? assertSharedReviewInput(context, work, run, generation) : null);
|
|
628
874
|
const reviewPaths = preparedReviewInput?.run_inputs.filter(item => item.root === "workspace").map(item => item.path) ?? [];
|
|
629
|
-
const preparedCopy = readOnly && context.env.DD_FLOW_READ_ONLY_WORKSPACE ? prepareBoundReviewCopy(context, work, run, context.env.DD_FLOW_READ_ONLY_WORKSPACE, reviewPaths) : null;
|
|
875
|
+
const preparedCopy = nativeReviewCopy ?? (readOnly && context.env.DD_FLOW_READ_ONLY_WORKSPACE ? prepareBoundReviewCopy(context, work, run, context.env.DD_FLOW_READ_ONLY_WORKSPACE, reviewPaths) : null);
|
|
630
876
|
const fingerprint = readOnly ? preparedReviewInput?.source_sha256 ?? preparedReviewInput?.workspace_fingerprint ?? legacyReviewFingerprint(work, run) ?? workspaceFingerprint(run.workspace_root) : undefined;
|
|
631
877
|
const preparedDependencies = parseDependencies(work).map(dependency => context.db.get("SELECT work_id, result FROM works WHERE work_id = ?", [dependency])).filter(Boolean);
|
|
632
878
|
const preparedRecovery = recoveryPromptForWork(context, work.work_id);
|
|
@@ -634,7 +880,7 @@ export function startBoundWork(context, work, run, claimIdentity, hookEventId, s
|
|
|
634
880
|
const finishMarker = `DD_FLOW_FINISH_${crypto.randomUUID()}`;
|
|
635
881
|
const failMarker = `DD_FLOW_FAIL_${crypto.randomUUID()}`;
|
|
636
882
|
const workspaceView = preparedCopy ? { work_id: work.work_id, from: run.workspace_root, to: reviewInputPath(requireRunHome(run), preparedCopy.copy.root), source_sha256: preparedCopy.copy.source_sha256, copy_sha256: preparedCopy.copy.sha256 } : undefined;
|
|
637
|
-
let promptTemplate = renderWorkerPrompt(context, work, workerRun, preparedDependencies, { finish: finishMarker, fail: failMarker }, workspaceView);
|
|
883
|
+
let promptTemplate = renderWorkerPrompt(context, work, workerRun, preparedDependencies, { finish: finishMarker, fail: failMarker }, workspaceView, preparedReviewInput ?? undefined);
|
|
638
884
|
if (preparedRecovery)
|
|
639
885
|
promptTemplate = `${recoveryPrompt(preparedRecovery)}\n\n${promptTemplate}`;
|
|
640
886
|
let binding;
|
|
@@ -646,17 +892,36 @@ export function startBoundWork(context, work, run, claimIdentity, hookEventId, s
|
|
|
646
892
|
throw new AppError("conflict", "Work was claimed concurrently", 1, { work_id: current.work_id, status: current.status });
|
|
647
893
|
if (Object.keys(current).some(key => current[key] !== work[key]))
|
|
648
894
|
throw new AppError("work_not_ready", "Work inputs changed while preparing its start packet", 2, { work_id: current.work_id });
|
|
895
|
+
preparedMemoryReview?.assertCustody(context);
|
|
649
896
|
if (!isReady(context, current))
|
|
650
897
|
throw new AppError("work_not_ready", "Work dependencies are not ready", 2, { work_id: current.work_id, blockers: readinessBlockers(context, current) });
|
|
651
898
|
if (retryPending)
|
|
652
899
|
throw new AppError("work_retry_incomplete", "Complete the interrupted Work retry before starting its next attempt", 1, { work_id: work.work_id });
|
|
653
|
-
const
|
|
900
|
+
const claimedIdentity = claimIdentity();
|
|
901
|
+
const nativeReviewExecution = nativeReviewCopy ? memoryRepairExecutionIdentity(context, { projectId: current.project_id, runId: current.run_id,
|
|
902
|
+
workId: current.work_id, eventKey: hookEventId, root: nativeReviewCopy.root }) : null;
|
|
903
|
+
const identity = preparedRepairCopy ? memoryRepairExecutionIdentity(context, { projectId: current.project_id,
|
|
904
|
+
runId: current.run_id, workId: current.work_id, eventKey: hookEventId, root: preparedRepairCopy.root() }).identity
|
|
905
|
+
: nativeReviewExecution?.identity ?? claimedIdentity;
|
|
906
|
+
if (identity.hookEventId !== claimedIdentity.hookEventId || identity.sessionId !== claimedIdentity.sessionId)
|
|
907
|
+
throw new AppError("memory_repair_native_binding_invalid", "Repair execution proof differs from the claimed Hook", 1, { effect: "no_effect" });
|
|
654
908
|
assertRecoveryHookCurrent(context, current.project_id, current.run_id, identity);
|
|
909
|
+
if (PreparedSecondMemoryRepairLaunch.isPrepared(preparedRepairCopy))
|
|
910
|
+
preparedRepairCopy.assertClaimIdentity(context, hookEventId, identity);
|
|
911
|
+
else
|
|
912
|
+
preparedRepairCopy?.assertClaimIdentity(context, identity);
|
|
655
913
|
if (readOnly && !context.env.DD_FLOW_READ_ONLY_WORKSPACE
|
|
656
914
|
&& context.db.get("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'work_launches'")
|
|
657
915
|
&& context.db.get("SELECT 1 FROM work_launches WHERE work_id = ? AND status IN ('launching','start_requested')", [current.work_id]))
|
|
658
916
|
throw new AppError("read_only_workspace_unbound", "External reviewer did not inherit its qualified execution workspace", 1, { work_id: current.work_id });
|
|
659
|
-
if (
|
|
917
|
+
if (nativeReviewCopy) {
|
|
918
|
+
nativeReviewCopy.assertCustody(context);
|
|
919
|
+
const worker = nativeReviewCopy.route.worker;
|
|
920
|
+
if (identity.harness !== runtimeHarness(worker) || identity.model !== worker.model
|
|
921
|
+
|| identity.provider && identity.provider !== worker.provider)
|
|
922
|
+
throw new AppError("read_only_workspace_unbound", "Native review Hook differs from the frozen worker profile", 1, { effect: "no_effect" });
|
|
923
|
+
}
|
|
924
|
+
else if (preparedCopy && 'providerSessionId' in preparedCopy) {
|
|
660
925
|
const launch = context.db.get("SELECT provider_session_id, profile_json FROM work_launches WHERE work_id = ? AND status IN ('launching','start_requested')", [current.work_id]);
|
|
661
926
|
if (!launch || launch.provider_session_id !== identity.providerSessionId || launch.provider_session_id !== preparedCopy.providerSessionId || launch.profile_json !== preparedCopy.profileJson)
|
|
662
927
|
throw new AppError("read_only_workspace_unbound", "Read-only execution copy belongs to another physical Session or launch revision", 1, { work_id: current.work_id });
|
|
@@ -676,7 +941,7 @@ export function startBoundWork(context, work, run, claimIdentity, hookEventId, s
|
|
|
676
941
|
const commands = workerLifecycleCommands({ ...context, captureLifecycleAssignment: (id) => assignments.add(id) }, current, workerRun, true);
|
|
677
942
|
const prompt = promptTemplate.replace(finishMarker, commands.finish).replace(failMarker, commands.fail);
|
|
678
943
|
bindLifecyclePacketAssignments(context, prompt, assignments);
|
|
679
|
-
const workContext = { schema_id: "dd-flow/work-context@1", run_id: current.run_id, work_id: current.work_id, parent_work_id: current.parent_work_id, project_root: run.project_root, workspace_root: workerRun.workspace_root, run_root: requireRunHome(run), depends_on: parseDependencies(current), launch_policy: current.launch_policy, result_schema: current.result_schema, read_only: readOnly, ...(recovery ? { recovery } : {}), ...(readOnly ? { workspace_fingerprint: fingerprint, ...(reviewInput ? { review_input: reviewInput } : {}), ...(reviewCopy ? { review_copy: reviewCopy } : {}) } : {}) };
|
|
944
|
+
const workContext = { schema_id: "dd-flow/work-context@1", run_id: current.run_id, work_id: current.work_id, parent_work_id: current.parent_work_id, project_root: run.project_root, workspace_root: workerRun.workspace_root, run_root: requireRunHome(run), depends_on: parseDependencies(current), launch_policy: current.launch_policy, result_schema: current.result_schema, read_only: readOnly, ...(preparedRepairCopy ? { memory_repair_input: preparedRepairCopy.input() } : {}), ...(recovery ? { recovery } : {}), ...(readOnly ? { workspace_fingerprint: fingerprint, ...(reviewInput ? { review_input: reviewInput } : {}), ...(reviewCopy ? { review_copy: reviewCopy } : {}) } : {}) };
|
|
680
945
|
const contextBytes = `${JSON.stringify(workContext, null, 2)}\n`;
|
|
681
946
|
const response = { ok: true, work_id: current.work_id, work_session_id: linkId, worker_prompt_markdown: prompt, prompt_path: promptPath, ...(recovery ? { recovery } : {}), session_binding: { source: "PreToolUse", session: { harness_id: identity.harness, session_id: identity.nativeSessionId } } };
|
|
682
947
|
const receipt = JSON.stringify({ version: 2, prompt, context: contextBytes, prompt_sha256: sha256(prompt), context_sha256: sha256(contextBytes), response, response_sha256: sha256(JSON.stringify(response)), generation, invocation_id: context.env.DD_FLOW_CURRENT_INVOCATION ?? null });
|
|
@@ -687,8 +952,26 @@ export function startBoundWork(context, work, run, claimIdentity, hookEventId, s
|
|
|
687
952
|
if (context.db.run("UPDATE works SET status = 'running', started_at = COALESCE(started_at, ?), updated_at = ? WHERE work_id = ? AND status = 'created'", [now, now, current.work_id]).changes !== 1)
|
|
688
953
|
throw new AppError("conflict", "Work was claimed concurrently", 1, { work_id: current.work_id });
|
|
689
954
|
context.db.run("INSERT INTO work_sessions (id, work_id, session_id, hook_event_id, status, prompt_path, result_path, start_receipt_json, start_publication_status, created_at, updated_at, completed_at) VALUES (?, ?, ?, ?, 'running', ?, ?, ?, 'pending', ?, ?, NULL)", [linkId, current.work_id, identity.sessionId, hookEventId, promptPath, resultPath, receipt, now, now]);
|
|
955
|
+
preparedRepairCopy?.recordStart(context, linkId);
|
|
956
|
+
if (nativeReviewCopy)
|
|
957
|
+
appendAudit(context, { projectId: current.project_id, eventType: "memory.native_review_binding", payload: {
|
|
958
|
+
run_id: current.run_id, work_id: current.work_id, work_session_id: linkId, hook_event_key: hookEventId,
|
|
959
|
+
session_id: identity.sessionId, provider_session_id: identity.providerSessionId, invocation_id: nativeReviewExecution?.invocationId ?? null,
|
|
960
|
+
copy: nativeReviewCopy.copy
|
|
961
|
+
} });
|
|
690
962
|
if (stage)
|
|
691
963
|
context.db.run("UPDATE sessions SET current_stage = ?, updated_at = ? WHERE project_id = ? AND session_id = ?", [stage, now, current.project_id, identity.sessionId]);
|
|
964
|
+
if (stage && context.stageBindingBundle) {
|
|
965
|
+
const route = context.stageBindingBundle.bundle.execution.stage(stage);
|
|
966
|
+
appendAudit(context, { projectId: current.project_id, eventType: "stage.coordinator_binding", payload: {
|
|
967
|
+
run_id: current.run_id, scope: { ...stageGraphScope(context, current.project_id, current.run_id, stage), stage },
|
|
968
|
+
work_id: current.work_id, work_session_id: linkId, session_id: identity.sessionId,
|
|
969
|
+
harness: identity.harness, provider_session_id: identity.providerSessionId,
|
|
970
|
+
execution_sha256: context.stageBindingBundle.bundle.execution.sha256,
|
|
971
|
+
profile_id: route.profile.id, profile_sha256: hybridDigest(route.profile), session_mode: route.session_mode,
|
|
972
|
+
...(context.stageBindingBundle.contextSha256 ? { context_sha256: context.stageBindingBundle.contextSha256 } : {})
|
|
973
|
+
} });
|
|
974
|
+
}
|
|
692
975
|
if (recovery)
|
|
693
976
|
activateRecoveryWork(context, current.work_id);
|
|
694
977
|
return response;
|
|
@@ -716,6 +999,7 @@ export function startBoundWork(context, work, run, claimIdentity, hookEventId, s
|
|
|
716
999
|
throw error;
|
|
717
1000
|
}
|
|
718
1001
|
try {
|
|
1002
|
+
preparedRepairCopy?.revalidateCommittedStart(context, linkId);
|
|
719
1003
|
if (readOnly) {
|
|
720
1004
|
const committedWork = requireWork(context, work.work_id);
|
|
721
1005
|
assertSharedReviewInput(context, committedWork, run, generation);
|
|
@@ -736,13 +1020,37 @@ export function startBoundWork(context, work, run, claimIdentity, hookEventId, s
|
|
|
736
1020
|
}
|
|
737
1021
|
/** Read the exact committed reply, never bind another Session or regenerate a packet. */
|
|
738
1022
|
export function recoverCommittedWorkStart(context, invocationId, eventKey, generation, workId) {
|
|
739
|
-
const row = context.db.get("SELECT ws.start_receipt_json, w.project_id, w.run_id FROM work_sessions ws JOIN works w ON w.work_id = ws.work_id WHERE ws.start_receipt_json IS NOT NULL AND ws.hook_event_id = ? AND json_extract(ws.start_receipt_json, '$.invocation_id') IS ? AND (? IS NULL OR w.work_id = ?)", [eventKey, invocationId, workId ?? null, workId ?? null]);
|
|
1023
|
+
const row = context.db.get("SELECT ws.start_receipt_json, w.project_id, w.run_id, w.work_id, ws.id AS work_session_id, w.result_schema, w.payload_json FROM work_sessions ws JOIN works w ON w.work_id = ws.work_id WHERE ws.start_receipt_json IS NOT NULL AND ws.hook_event_id = ? AND json_extract(ws.start_receipt_json, '$.invocation_id') IS ? AND (? IS NULL OR w.work_id = ?)", [eventKey, invocationId, workId ?? null, workId ?? null]);
|
|
740
1024
|
if (!row)
|
|
741
1025
|
return undefined;
|
|
1026
|
+
// Replay restores registered physical start custody, never prelaunch admission.
|
|
1027
|
+
memoryRepairWave(row);
|
|
1028
|
+
const retainedRun = requireRun(context, row.project_id, row.run_id);
|
|
1029
|
+
const repairWave = registeredMemoryRepairWave(context, row, retainedRun);
|
|
742
1030
|
const receipt = JSON.parse(row.start_receipt_json);
|
|
743
1031
|
if (receipt.generation !== generation || !receipt.response || receipt.response_sha256 !== sha256(JSON.stringify(receipt.response)))
|
|
744
1032
|
throw new AppError("work_start_receipt_invalid", "Committed reply identity or checksum differs", 1, { invocation_id: invocationId });
|
|
745
1033
|
assertRunMutationAllowed(context, row.project_id, row.run_id, generation);
|
|
1034
|
+
if (repairWave) {
|
|
1035
|
+
let workspaceRoot;
|
|
1036
|
+
try {
|
|
1037
|
+
if (typeof receipt.context !== "string" || Buffer.byteLength(receipt.context) > 16 * 1024 * 1024)
|
|
1038
|
+
throw new Error("Invalid retained context");
|
|
1039
|
+
const input = JSON.parse(receipt.context);
|
|
1040
|
+
if (typeof input.workspace_root !== "string")
|
|
1041
|
+
throw new Error("Missing retained workspace");
|
|
1042
|
+
workspaceRoot = input.workspace_root;
|
|
1043
|
+
}
|
|
1044
|
+
catch {
|
|
1045
|
+
throw new AppError("work_start_receipt_invalid", "Committed repair context is unavailable", 1, { effect: "no_effect" });
|
|
1046
|
+
}
|
|
1047
|
+
const scope = { projectRoot: retainedRun.project_root, runId: row.run_id, workId: row.work_id, workspaceRoot, workSessionId: row.work_session_id };
|
|
1048
|
+
const owner = repairWave === 1 ? restoreStartedMemoryRepairCopyFromStage(context, scope)
|
|
1049
|
+
: restoreStartedMemorySecondRepairLaunchFromStage(context, scope);
|
|
1050
|
+
if (owner.root() !== workspaceRoot)
|
|
1051
|
+
throw new AppError("work_start_receipt_invalid", "Retained repair workspace differs from its physical start", 1, { effect: "no_effect" });
|
|
1052
|
+
owner.revalidateCommittedStart(context, row.work_session_id);
|
|
1053
|
+
}
|
|
746
1054
|
materializePendingWorkStarts(context, row.project_id, row.run_id);
|
|
747
1055
|
return receipt.response;
|
|
748
1056
|
}
|
|
@@ -760,17 +1068,66 @@ export function finishStageCoordinatorWork(context, id, result) {
|
|
|
760
1068
|
context.db.run("UPDATE sessions SET status = 'idle', updated_at = ? WHERE project_id = ? AND session_id = ? AND NOT EXISTS (SELECT 1 FROM work_sessions WHERE session_id = ? AND status = 'running')", [context.now(), work.project_id, link.session_id, link.session_id]);
|
|
761
1069
|
settleRecoveryWork(context, id, "completed");
|
|
762
1070
|
}
|
|
763
|
-
export function finishWork(context, id, result, progress, retry) {
|
|
1071
|
+
export async function finishWork(context, id, result, progress, retry) {
|
|
1072
|
+
const work = requireWork(context, id);
|
|
1073
|
+
if (work.result_schema === "dd-flow/memory-repair-work-result@1") {
|
|
1074
|
+
if (retry)
|
|
1075
|
+
throw new AppError("usage", "Memory repair completion uses retained publication recovery, not code-check retry", 2, { effect: "no_effect" });
|
|
1076
|
+
prepareWorkSettlement(context, id, "completed", result);
|
|
1077
|
+
const published = publishMemoryRepairWorkCompletion(context, work, result);
|
|
1078
|
+
try {
|
|
1079
|
+
return await finishPublishedMemoryRepairWork(context, published);
|
|
1080
|
+
}
|
|
1081
|
+
catch (error) {
|
|
1082
|
+
throw new AppError("memory_repair_completion_pending", "Repair publication completed but Work settlement requires reconciliation", 1, { effect: error instanceof AppError && error.details.effect === "unknown" ? "unknown" : "partial",
|
|
1083
|
+
run_id: work.run_id, work_id: work.work_id, result_sha256: published.sha256,
|
|
1084
|
+
cause: error instanceof Error ? error.message : String(error) });
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
const prepared = prepareRegisteredMemoryWorkResult(context, work, result);
|
|
1088
|
+
return settle(context, id, "completed", result, progress, retry, prepared ?? undefined);
|
|
1089
|
+
}
|
|
1090
|
+
/** Controller-selected registration/result preparation, never an agent-provided binding. */
|
|
1091
|
+
export function finishRegisteredMemoryCompositionWork(context, prepared) {
|
|
1092
|
+
if (!(prepared instanceof PreparedRegisteredMemoryCompositionWorkResult))
|
|
1093
|
+
throw new AppError("memory_work_result_preparation_required", "Memory Work completion requires admitted registered output", 1);
|
|
1094
|
+
return settle(context, prepared.workId, "completed", prepared.bytes().toString("utf8"), undefined, undefined, prepared);
|
|
1095
|
+
}
|
|
1096
|
+
export function finishRegisteredMemoryReviewWork(context, prepared) {
|
|
1097
|
+
if (!(prepared instanceof PreparedRegisteredMemoryReviewWorkResult))
|
|
1098
|
+
throw new AppError("memory_work_result_preparation_required", "Memory reviewer completion requires registered input and independent Session proof", 1);
|
|
1099
|
+
return settle(context, prepared.workId, "completed", prepared.bytes().toString("utf8"), undefined, undefined, prepared);
|
|
1100
|
+
}
|
|
1101
|
+
export function finishPublishedMemoryRepairWork(context, prepared) {
|
|
1102
|
+
if (!PreparedPublishedMemoryRepairWorkResult.isPrepared(prepared))
|
|
1103
|
+
throw new AppError("memory_work_result_preparation_required", "Repair completion requires independently published registered output", 1, { effect: "no_effect" });
|
|
1104
|
+
return settle(context, prepared.workId, "completed", prepared.bytes().toString("utf8"), undefined, undefined, prepared);
|
|
1105
|
+
}
|
|
764
1106
|
/** Structured fan-in closes a parent after its Session was handed to a child. */
|
|
765
|
-
export function finishFanInWork(context, id, result) {
|
|
766
|
-
|
|
767
|
-
|
|
1107
|
+
export function finishFanInWork(context, id, result) {
|
|
1108
|
+
const work = requireWork(context, id);
|
|
1109
|
+
assertMemoryRepairWritePrepared(work);
|
|
1110
|
+
if (["dd-flow/memory-composition-work-result@1", "dd-flow/memory-review-work-result@1"].includes(work.result_schema ?? ""))
|
|
1111
|
+
throw new AppError("memory_work_result_preparation_required", "Memory Work cannot bypass registered result settlement through fan-in", 1);
|
|
1112
|
+
assertRunMutationAllowed(context, work.project_id, work.run_id);
|
|
1113
|
+
if (work.status !== "running")
|
|
1114
|
+
throw new AppError("invalid_work_state", "Fan-in Work is not running", 2, { work_id: work.work_id, status: work.status });
|
|
1115
|
+
if (context.db.get("SELECT 1 FROM works WHERE parent_work_id = ? AND status IN ('created','running','paused') LIMIT 1", [work.work_id]))
|
|
1116
|
+
throw new AppError("active_child_work", "Fan-in Work still has active children", 2, { work_id: work.work_id });
|
|
1117
|
+
const now = context.now();
|
|
1118
|
+
context.db.run("UPDATE works SET status = 'completed', result = ?, completed_at = ?, updated_at = ? WHERE work_id = ?", [result, now, now, work.work_id]);
|
|
1119
|
+
context.db.run("UPDATE work_sessions SET status = 'completed', completed_at = COALESCE(completed_at, ?), updated_at = ? WHERE work_id = ? AND status = 'running'", [now, now, work.work_id]);
|
|
1120
|
+
refreshRunWorkProjection(context, work.project_id, work.run_id);
|
|
1121
|
+
appendFlowRunTimelineEvent(context, work.project_id, work.run_id, { type: "work_completed", work_id: work.work_id, fan_in: true });
|
|
1122
|
+
return { ok: true, work_id: work.work_id, status: "completed", fan_in: true };
|
|
1123
|
+
}
|
|
768
1124
|
export function failWork(context, id, reason) { return settle(context, id, "failed", reason); }
|
|
769
1125
|
export function cancelWork(context, id, reason) { return settle(context, id, "cancelled", reason); }
|
|
770
1126
|
export function prepareWorkSettlement(context, id, status, result) {
|
|
771
1127
|
if (status !== "completed" && (typeof result !== "string" || !result.trim()))
|
|
772
1128
|
throw new AppError("usage", "Work settlement requires a nonempty --reason", 2);
|
|
773
1129
|
const work = requireWork(context, id);
|
|
1130
|
+
assertMemoryRepairTerminalFailureAllowed(context, work, status);
|
|
774
1131
|
assertWorkSettlementAllowed(context, work.project_id, work.run_id, work.work_id);
|
|
775
1132
|
if (work.status !== "running" && !(status === "cancelled" && work.status === "created"))
|
|
776
1133
|
throw new AppError("invalid_work_state", "Work is not running", 2, { status: work.status });
|
|
@@ -978,6 +1335,11 @@ function legacyReviewFingerprint(work, run) {
|
|
|
978
1335
|
}
|
|
979
1336
|
function assertSharedReviewInput(context, work, run, generation = recoveryGuard(context, work.project_id, work.run_id)?.generation ?? 0) {
|
|
980
1337
|
const payload = parsePayload(work), kind = payload?.kind;
|
|
1338
|
+
if (kind === "memory-review") {
|
|
1339
|
+
const input = prepareMemoryReviewWorkInput(context, work, run, generation).input;
|
|
1340
|
+
assertReviewSiblingBaseline(context, work, input, generation);
|
|
1341
|
+
return input;
|
|
1342
|
+
}
|
|
981
1343
|
const relative = kind === "code-review" ? "06-code-review/review-context.json" : kind === "plan-review" ? "04-plan-review/work-context.json" : null;
|
|
982
1344
|
if (!relative || !work.parent_work_id)
|
|
983
1345
|
return null;
|
|
@@ -1099,7 +1461,7 @@ function assertReviewSiblingBaseline(context, work, current, generation) {
|
|
|
1099
1461
|
break;
|
|
1100
1462
|
}
|
|
1101
1463
|
}
|
|
1102
|
-
async function settle(context, id, status, result, progress, retry) {
|
|
1464
|
+
async function settle(context, id, status, result, progress, retry, memoryResult) {
|
|
1103
1465
|
const { work, run, link } = prepareWorkSettlement(context, id, status, result);
|
|
1104
1466
|
id = work.work_id;
|
|
1105
1467
|
let receipts = [];
|
|
@@ -1123,7 +1485,23 @@ async function settle(context, id, status, result, progress, retry) {
|
|
|
1123
1485
|
}
|
|
1124
1486
|
};
|
|
1125
1487
|
if (status === "completed") {
|
|
1126
|
-
|
|
1488
|
+
if (!PreparedPublishedMemoryRepairWorkResult.isPrepared(memoryResult))
|
|
1489
|
+
assertMemoryRepairWritePrepared(work);
|
|
1490
|
+
if (["dd-flow/memory-composition-work-result@1", "dd-flow/memory-review-work-result@1", "dd-flow/memory-repair-work-result@1"].includes(work.result_schema ?? "")) {
|
|
1491
|
+
const correctOwner = work.result_schema === "dd-flow/memory-composition-work-result@1"
|
|
1492
|
+
? memoryResult instanceof PreparedRegisteredMemoryCompositionWorkResult
|
|
1493
|
+
: work.result_schema === "dd-flow/memory-review-work-result@1" ? memoryResult instanceof PreparedRegisteredMemoryReviewWorkResult
|
|
1494
|
+
: PreparedPublishedMemoryRepairWorkResult.isPrepared(memoryResult);
|
|
1495
|
+
if (!correctOwner || !memoryResult || memoryResult.workId !== work.work_id || !link)
|
|
1496
|
+
throw new AppError("memory_work_result_preparation_required", "Memory Work completion requires admitted registered output", 1);
|
|
1497
|
+
memoryResult.revalidate(context);
|
|
1498
|
+
memoryResult.assertSettlementScope(context, link.session_id, link.id);
|
|
1499
|
+
}
|
|
1500
|
+
else {
|
|
1501
|
+
if (memoryResult)
|
|
1502
|
+
throw new AppError("memory_work_result_preparation_required", "Prepared memory output cannot settle another Work schema", 1);
|
|
1503
|
+
validateWorkResult(work, result, run.project_root, run.workspace_root, requireRunHome(run), run.id, link?.result_path ?? null);
|
|
1504
|
+
}
|
|
1127
1505
|
const packet = codePacket(work);
|
|
1128
1506
|
if (packet) {
|
|
1129
1507
|
// CODE packets are projected from a PLAN that already validated this
|
|
@@ -1158,6 +1536,9 @@ async function settle(context, id, status, result, progress, retry) {
|
|
|
1158
1536
|
const now = context.now();
|
|
1159
1537
|
context.db.writeTransaction(() => {
|
|
1160
1538
|
assertWorkSettlementAllowed(context, work.project_id, work.run_id, work.work_id);
|
|
1539
|
+
assertMemoryRepairTerminalFailureAllowed(context, requireWork(context, work.work_id), status);
|
|
1540
|
+
if (memoryResult)
|
|
1541
|
+
memoryResult.recordSettlement(context, link.session_id, link.id);
|
|
1161
1542
|
if (link && !context.db.get("SELECT 1 FROM work_sessions WHERE id = ? AND status = 'running'", [link.id]))
|
|
1162
1543
|
throw new AppError("work_generation_stale", "Work Session was interrupted or superseded while checks ran", 1, { work_id: id });
|
|
1163
1544
|
if (link && context.db.get("SELECT start_publication_status FROM work_sessions WHERE id = ?", [link.id])?.start_publication_status && !context.db.get("SELECT 1 FROM work_sessions WHERE id = ? AND start_publication_status = 'published'", [link.id]))
|
|
@@ -1165,7 +1546,8 @@ async function settle(context, id, status, result, progress, retry) {
|
|
|
1165
1546
|
if (context.db.run("UPDATE works SET status = ?, result = ?, completed_at = ?, updated_at = ? WHERE work_id = ? AND status = ? AND updated_at = ?", [status, result, now, now, id, work.status, work.updated_at]).changes !== 1)
|
|
1166
1547
|
throw new AppError("work_generation_stale", "Work changed while its result and review inputs were prepared", 1, { work_id: id });
|
|
1167
1548
|
if (link) {
|
|
1168
|
-
context.db.run("UPDATE work_sessions SET status = ?, result_receipt_json = ?, completed_at = ?, updated_at = ? WHERE id = ?", [status, resultReceipt, now, now, link.id])
|
|
1549
|
+
if (context.db.run("UPDATE work_sessions SET status = ?, result_receipt_json = ?, completed_at = ?, updated_at = ? WHERE id = ? AND work_id = ? AND session_id = ? AND status = 'running'", [status, resultReceipt, now, now, link.id, work.work_id, link.session_id]).changes !== 1)
|
|
1550
|
+
throw new AppError("work_generation_stale", "Work Session changed or refused settlement while its result was committed", 1, { work_id: id });
|
|
1169
1551
|
const stillRunning = context.db.get("SELECT 1 FROM work_sessions WHERE session_id = ? AND status = 'running' LIMIT 1", [link.session_id]);
|
|
1170
1552
|
if (!stillRunning)
|
|
1171
1553
|
context.db.run("UPDATE sessions SET status = 'idle', updated_at = ? WHERE project_id = ? AND session_id = ?", [now, work.project_id, link.session_id]);
|
|
@@ -1175,7 +1557,7 @@ async function settle(context, id, status, result, progress, retry) {
|
|
|
1175
1557
|
settleRecoveryWork(context, work.work_id, status);
|
|
1176
1558
|
refreshRunWorkProjection(context, work.project_id, work.run_id);
|
|
1177
1559
|
const control = runControlDirective(context, work.project_id, work.run_id);
|
|
1178
|
-
const newlyReady = control ? [] : context.db.all(`SELECT ${workColumns} FROM works WHERE project_id = ? AND run_id = ? AND status = 'created' ORDER BY created_at, work_id`, [work.project_id, work.run_id]).filter((candidate) => isReady(context, candidate)).map((candidate) => ({ work_id: candidate.work_id, launch_policy: candidate.launch_policy, start_command: workStartCommand(context, candidate) }));
|
|
1560
|
+
const newlyReady = control ? [] : context.db.all(`SELECT ${workColumns} FROM works WHERE project_id = ? AND run_id = ? AND status = 'created' ORDER BY created_at, work_id`, [work.project_id, work.run_id]).filter((candidate) => isReady(context, candidate)).map((candidate) => ({ work_id: candidate.work_id, launch_policy: candidate.launch_policy, start_command: workStartCommand(context, candidate, { preview: true }) }));
|
|
1179
1561
|
appendFlowRunTimelineEvent(context, work.project_id, work.run_id, { type: `work_${status}`, work_id: work.work_id, session_id: link?.session_id ?? null, ...(coordinationDrift.length ? { coordination_drift: coordinationDrift } : {}) });
|
|
1180
1562
|
if (status === "completed")
|
|
1181
1563
|
for (const ready of newlyReady) {
|
|
@@ -1240,9 +1622,25 @@ export function bindSessionForResume(context, workId, identity, now) {
|
|
|
1240
1622
|
function reactivateBoundSession(context, projectId, sessionId, now) {
|
|
1241
1623
|
context.db.run("UPDATE sessions SET status = 'active', stop_reason = NULL, stopped_at = NULL, updated_at = ? WHERE project_id = ? AND session_id = ?", [now, projectId, sessionId]);
|
|
1242
1624
|
}
|
|
1625
|
+
export function workResultByteLimit(context, id) {
|
|
1626
|
+
const schema = requireWork(context, id).result_schema;
|
|
1627
|
+
return ["dd-flow/memory-composition-work-result@1", "dd-flow/memory-review-work-result@1", "dd-flow/memory-repair-work-result@1"].includes(schema ?? "") ? 1024 * 1024 : undefined;
|
|
1628
|
+
}
|
|
1243
1629
|
export function prepareWorkResult(context, id, result) {
|
|
1244
1630
|
try {
|
|
1245
1631
|
const { work, run, link } = prepareWorkSettlement(context, id, "completed", result);
|
|
1632
|
+
if (work.result_schema === "dd-flow/memory-repair-work-result@1") {
|
|
1633
|
+
prepareMemoryRepairWorkCompletion(context, work, result);
|
|
1634
|
+
return;
|
|
1635
|
+
}
|
|
1636
|
+
const memoryResult = prepareRegisteredMemoryWorkResult(context, work, result);
|
|
1637
|
+
if (memoryResult) {
|
|
1638
|
+
if (!link)
|
|
1639
|
+
throw new AppError("memory_work_result_preparation_required", "Memory Work completion requires an active Work Session", 1);
|
|
1640
|
+
memoryResult.revalidate(context);
|
|
1641
|
+
memoryResult.assertSettlementScope(context, link.session_id, link.id);
|
|
1642
|
+
return;
|
|
1643
|
+
}
|
|
1246
1644
|
validateWorkResult(work, result, run.project_root, run.workspace_root, requireRunHome(run), run.id, link?.result_path ?? null);
|
|
1247
1645
|
}
|
|
1248
1646
|
catch (error) {
|
|
@@ -1250,8 +1648,11 @@ export function prepareWorkResult(context, id, result) {
|
|
|
1250
1648
|
}
|
|
1251
1649
|
}
|
|
1252
1650
|
function validateWorkResult(work, result, projectRoot, workspaceRoot, runHome, runId, resultPath) {
|
|
1651
|
+
assertMemoryRepairWritePrepared(work);
|
|
1253
1652
|
if (!work.result_schema)
|
|
1254
1653
|
return;
|
|
1654
|
+
if (["dd-flow/memory-composition-work-result@1", "dd-flow/memory-review-work-result@1"].includes(work.result_schema ?? ""))
|
|
1655
|
+
throw new AppError("memory_work_result_preparation_required", "Memory Work completion requires admitted registered output, not a mutable schema lookup", 1);
|
|
1255
1656
|
if (!resultPath)
|
|
1256
1657
|
throw new AppError("runtime_missing", "Work result path is unavailable", 1, { work_id: work.work_id });
|
|
1257
1658
|
let parsed;
|
|
@@ -1269,6 +1670,73 @@ function validateWorkResult(work, result, projectRoot, workspaceRoot, runHome, r
|
|
|
1269
1670
|
if (work.result_schema === "dd-flow/plan-review-result@1")
|
|
1270
1671
|
validatePlanReviewResult(work, parsed, { workspaceRoot, runHome, runId });
|
|
1271
1672
|
}
|
|
1673
|
+
/** Allocation is not write admission. Registered repairs must not reach a
|
|
1674
|
+
* provider, claim a Session or use generic mutable-schema settlement before
|
|
1675
|
+
* their private writer baseline/delta owner is integrated. The memory handler
|
|
1676
|
+
* group stays unregistered during that implementation; no permissive fallback.
|
|
1677
|
+
*/
|
|
1678
|
+
function assertMemoryRepairWritePrepared(work) {
|
|
1679
|
+
const payload = parsePayload(work);
|
|
1680
|
+
if (work.result_schema === "dd-flow/memory-repair-work-result@1" || payload?.kind === "memory-repair" || payload?.memory_repair) {
|
|
1681
|
+
throw new AppError("memory_repair_write_preparation_required", "Repair execution requires registered writer baseline and bounded delta custody", 1, { effect: "no_effect" });
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
/** Recognizing packet metadata never grants launch or claim authority. Every
|
|
1685
|
+
* supported wave must subsequently acquire its actual private owner. */
|
|
1686
|
+
/** Intent presence is a conservative denial, never publication authority.
|
|
1687
|
+
* Fail/cancel must not discard the physical WorkSession required to reconcile
|
|
1688
|
+
* an independently reserved repair. Recheck inside the settlement writer. */
|
|
1689
|
+
function assertMemoryRepairTerminalFailureAllowed(context, work, status) {
|
|
1690
|
+
if (status === "completed")
|
|
1691
|
+
return;
|
|
1692
|
+
const payload = parsePayload(work);
|
|
1693
|
+
if (work.result_schema !== "dd-flow/memory-repair-work-result@1" && payload?.kind !== "memory-repair" && !payload?.memory_repair)
|
|
1694
|
+
return;
|
|
1695
|
+
if (context.db.get("SELECT 1 FROM audit_events WHERE project_id=? AND event_type='memory.repair_publication_intent' AND json_extract(payload_json,'$.run_id')=? AND json_extract(payload_json,'$.work_id')=? LIMIT 1", [work.project_id, work.run_id, work.work_id])) {
|
|
1696
|
+
throw new AppError("memory_repair_completion_pending", "Reconcile the retained repair publication before terminal failure or cancellation", 1, { effect: "no_effect", run_id: work.run_id, work_id: work.work_id,
|
|
1697
|
+
next_action: "Preserve the running physical WorkSession and exact publication bytes; reconcile through the controller." });
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
function memoryRepairWave(work) {
|
|
1701
|
+
const payload = parsePayload(work);
|
|
1702
|
+
if (work.result_schema !== "dd-flow/memory-repair-work-result@1" && payload?.kind !== "memory-repair" && !payload?.memory_repair)
|
|
1703
|
+
return null;
|
|
1704
|
+
const wave = payload?.memory_repair?.wave;
|
|
1705
|
+
if (work.result_schema !== "dd-flow/memory-repair-work-result@1" || payload?.kind !== "memory-repair" || payload.read_only !== false || wave !== 1 && wave !== 2)
|
|
1706
|
+
assertMemoryRepairWritePrepared(work);
|
|
1707
|
+
return wave;
|
|
1708
|
+
}
|
|
1709
|
+
function registeredMemoryRepairWave(context, work, run) {
|
|
1710
|
+
const wave = memoryRepairWave(work);
|
|
1711
|
+
if (!wave)
|
|
1712
|
+
return null;
|
|
1713
|
+
const bundle = readFlowRunExecutionBundle(context, { projectRoot: run.project_root, runId: work.run_id });
|
|
1714
|
+
if (bundle?.descriptor.descriptor.id !== "mb-init")
|
|
1715
|
+
assertMemoryRepairWritePrepared(work);
|
|
1716
|
+
return wave;
|
|
1717
|
+
}
|
|
1718
|
+
function prepareMemoryRepairStartOwner(context, work, run, wave) {
|
|
1719
|
+
// A native child owns no external adapter environment. Its independently
|
|
1720
|
+
// observed Hook CWD is still only a candidate: the copy/route owner below
|
|
1721
|
+
// must admit it before any claim or filesystem projection.
|
|
1722
|
+
const hookKey = context.env.DD_FLOW_MEMORY_REPAIR_HOOK_EVENT;
|
|
1723
|
+
const retained = readFlowRunExecutionBundle(context, { projectRoot: run.project_root, runId: work.run_id });
|
|
1724
|
+
const selectedWork = requireWork(context, work.work_id);
|
|
1725
|
+
const override = parsePayload(selectedWork)?.execution_override;
|
|
1726
|
+
const native = retained?.execution.work("review", override?.agent_profile_id).delegation.mode === "native";
|
|
1727
|
+
const scope = { projectRoot: run.project_root, runId: work.run_id, workId: work.work_id };
|
|
1728
|
+
const workspaceRoot = context.env.DD_FLOW_MEMORY_REPAIR_WORKSPACE ?? (native && hookKey
|
|
1729
|
+
? (wave === 1 ? prepareMemoryRepairDispatchFromStage : prepareMemorySecondRepairDispatchFromStage)(context, scope).copy_root : null) ?? "";
|
|
1730
|
+
const input = { projectRoot: run.project_root, runId: work.run_id, workId: work.work_id, workspaceRoot };
|
|
1731
|
+
const owner = wave === 1 ? prepareExternalMemoryRepairCopyFromStage(context, input) : prepareMemorySecondRepairLaunchFromStage(context, input);
|
|
1732
|
+
if (owner.root() !== workspaceRoot)
|
|
1733
|
+
throw new AppError("memory_repair_workspace_unbound", "Repair start must inherit its exact independently prepared private copy", 1, { effect: "no_effect" });
|
|
1734
|
+
// The second-wave factory already verifies the private copy and shared
|
|
1735
|
+
// sources before returning; no mutation occurs between that and this return.
|
|
1736
|
+
if (wave === 1)
|
|
1737
|
+
owner.revalidate(context);
|
|
1738
|
+
return owner;
|
|
1739
|
+
}
|
|
1272
1740
|
function validateCodeWorkResult(work, value, projectRoot, runHome, runId) {
|
|
1273
1741
|
const result = value;
|
|
1274
1742
|
const changedPaths = (result.changed_paths ?? []).map((item) => normalizeWorkspacePath(projectRoot, item));
|
|
@@ -1319,7 +1787,32 @@ export function validateReviewResolutions(workId, assigned, resolutions) {
|
|
|
1319
1787
|
if (missing.length || unexpected.length || duplicate)
|
|
1320
1788
|
throw new AppError("review_repair_incomplete", "Review repair must include exactly one resolution for each assigned finding reference", 2, { work_id: workId, missing, unexpected, duplicate });
|
|
1321
1789
|
}
|
|
1322
|
-
function renderWorkerPrompt(context, work, run, dependencies, commands, workspaceView) {
|
|
1790
|
+
function renderWorkerPrompt(context, work, run, dependencies, commands, workspaceView, reviewInput) {
|
|
1791
|
+
const payload = parsePayload(work);
|
|
1792
|
+
const runIndex = JSON.parse(context.db.get("SELECT index_json FROM runs WHERE project_id=? AND id=?", [work.project_id, work.run_id]).index_json);
|
|
1793
|
+
if (runIndex.execution_bundle?.flow_id === "mb-init" && (payload?.memory_composition || payload?.kind === "memory-review" || payload?.kind === "memory-repair")) {
|
|
1794
|
+
assertRunBundleSqlAuthority(context, { projectId: work.project_id, runId: work.run_id }, runIndex.execution_bundle);
|
|
1795
|
+
// Memory Work owns a descriptor packet, not a missing SDLC SPECIFY or PLAN.
|
|
1796
|
+
// Reviewer assignment custody is admitted before rendering and rechecked
|
|
1797
|
+
// by the Work claim writer; composition settlement owns its output proof.
|
|
1798
|
+
const packet = payload.memory_review ?? payload.memory_repair ?? payload.memory_composition;
|
|
1799
|
+
const reviewPacket = payload.kind === "memory-review" ? packet : null;
|
|
1800
|
+
if (reviewPacket && !reviewInput)
|
|
1801
|
+
throw new AppError("memory_review_input_invalid", "Reviewer prompt requires independently prepared wave inputs", 1);
|
|
1802
|
+
return ["<memory_work_packet>", JSON.stringify(packet, null, 2), "</memory_work_packet>", "",
|
|
1803
|
+
...(payload.memory_composition ? ["<code_check_profile_contract>",
|
|
1804
|
+
"The existing CLI contract for .memory-bank/spec/engineering/code-check-profile.json is dd-flow/code-check-profile@6 (or an existing @7 runner-evidence profile). The installed project-check-profiles@1 schema is a different contract, not a prerequisite for authoring this file.",
|
|
1805
|
+
"This is a shape template, not an assignment or permission to execute checks. Author this file only when your registered task requires it and its path is inside your exact write_scopes. Preserve valid existing @6/@7 bindings; do not downgrade an existing profile or invent runner-evidence contracts.",
|
|
1806
|
+
"```json", JSON.stringify({ schema_id: "dd-flow/code-check-profile@6",
|
|
1807
|
+
aliases: { "@check/project-tests": "<observed explicitly requested command>" }, require_alias_for: {},
|
|
1808
|
+
mandatory_by_gate: { readiness: ["@check/project-tests"] } }, null, 2), "```",
|
|
1809
|
+
"aliases maps @check/name to an actual observed, explicitly requested command; replace the placeholder before submission. require_alias_for maps guarded raw command prefixes to an existing alias and may be empty. mandatory_by_gate maps work/code/readiness/merge/release to arrays of defined aliases; explicitly requested init checks belong at readiness. Optional inputs_by_alias maps defined aliases to existing workspace-relative files/directories used by that command. Do not copy example paths, invent commands, edit package scripts, install dependencies, grant network access or select deployment checks. Profile authoring does not run checks or prove a passed receipt; VERIFY executes the selected readiness checks.",
|
|
1810
|
+
"</code_check_profile_contract>", ""] : []),
|
|
1811
|
+
...(reviewPacket ? ["<frozen_review_criteria>", Buffer.from(reviewPacket.instruction.base64, "base64").toString("utf8"), "</frozen_review_criteria>", "",
|
|
1812
|
+
"<registered_review_inputs>", JSON.stringify(reviewInput?.run_inputs, null, 2),
|
|
1813
|
+
"Workspace references resolve inside workspace_root, including the isolated copy; RUN references resolve inside run_home. These are accepted input bytes, not permission to mutate them.", "</registered_review_inputs>", ""] : []),
|
|
1814
|
+
renderWorkerPromptBody(context, work, run, dependencies, commands)].join("\n");
|
|
1815
|
+
}
|
|
1323
1816
|
const profile = JSON.parse(context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [work.project_id, work.run_id]).index_json).execution_profile;
|
|
1324
1817
|
const operational = profile?.operational_contract ? renderRunOperationalContext(requireRunHome(run), profile.operational_contract.sha256, profile.operational_contract.path) : "";
|
|
1325
1818
|
const kind = parsePayload(work)?.kind;
|
|
@@ -1350,25 +1843,69 @@ function renderWorkerPromptBody(context, work, run, dependencies, commands) {
|
|
|
1350
1843
|
const finishWorkCommand = lifecycleCommands.finish;
|
|
1351
1844
|
const failWorkCommand = lifecycleCommands.fail;
|
|
1352
1845
|
const mergeWork = parsePayload(work)?.kind === "merge";
|
|
1846
|
+
const repairPacket = parsePayload(work)?.kind === "memory-repair" ? parsePayload(work)?.memory_repair : null;
|
|
1353
1847
|
const writeBoundary = readOnly
|
|
1354
|
-
? [`Review inputs under workspace_root (${run.workspace_root}) and run_home are read-only.
|
|
1355
|
-
:
|
|
1356
|
-
? [
|
|
1357
|
-
|
|
1848
|
+
? [`Review inputs under workspace_root (${run.workspace_root}) and run_home are read-only. Use workspace_root for Work start and project evidence reads. Finish/Fail use explicit owned paths and may run from RUN home. Do not edit project source, PLAN, aspect maps, CODE evidence, or another Work's files.`, "Do not write inside workspace_root, including an isolated review copy. Write only this Work's declared result-input.json and explicitly assigned Work-owned evidence paths under run_home. Report changed or inaccessible review inputs as a blocker; do not repair them."]
|
|
1849
|
+
: repairPacket
|
|
1850
|
+
? [`Work only inside the registered private workspace_root (${run.workspace_root}). Edit only these exact MB paths: ${JSON.stringify(repairPacket.write_paths)}. This is a hard allowlist, not a coordination hint.`,
|
|
1851
|
+
"Do not write through project_root or shared checkout paths. Do not edit canonical dd-flow/mbb, environment files, Git/worktree data, non-MB source, another Work or any other RUN. Do not install tools, commit, merge, or publish your changes; the CLI owns publication and integration.",
|
|
1852
|
+
"Workspace evidence resolves inside this private copy. RUN evidence is read-only. Write only this Work's result-input.json and assigned Work-owned evidence under run_home; neither a result JSON nor a private edit proves accepted publication or finding closure."]
|
|
1853
|
+
: mergeWork
|
|
1854
|
+
? ["MERGE does not own product-code repair. Use only the stage packet's merge apply command; resolve only paths that Git reports as unmerged. Do not edit integration code, tests, documentation or configuration to make a gate pass.", "RUN artifacts are read-only evidence: refer to them with run:// URIs and let dd-flow persist your submitted result."]
|
|
1855
|
+
: [`HARD RULE: project source reads and writes must remain under ${run.workspace_root}.`, "Do not write through project_root, outside workspace_root, into another RUN, or into Git/worktree control data. Do not create, switch, merge or delete branches/worktrees.", "RUN artifacts are read-only evidence: refer to them with run:// URIs and let dd-flow persist your submitted result. Accepted requirements, non-goals and stop_conditions are semantic hard boundaries. planned_write_areas is not."];
|
|
1358
1856
|
const completionRepair = readOnly
|
|
1359
1857
|
? ["If Finish reports input drift or an infrastructure failure, report the exact receipt and stop. Do not change the reviewed files to make the check pass."]
|
|
1360
|
-
:
|
|
1361
|
-
? ["
|
|
1362
|
-
:
|
|
1858
|
+
: repairPacket
|
|
1859
|
+
? ["On input drift, custody loss or publication/infrastructure failure, retain the exact receipt and stop. Do not edit shared files, reconstruct authority, change unassigned paths, or retry by starting another Session. A needed path outside write_paths is an assignment conflict, not permission to widen the repair."]
|
|
1860
|
+
: mergeWork
|
|
1861
|
+
? ["A failed integration check is evidence for a source-repair cycle, not permission to repair the integration workspace. Follow the exact merge repair command returned by dd-flow."]
|
|
1862
|
+
: ["Read the failed receipt and its stdout/stderr. Fix project-owned source, migration, test, formatting, or configuration errors in this same Work, then use the returned retry_command with your result. Internal invocation IDs are runtime-owned; do not copy or invent them. Do not invent a cause that does not appear in the retained output."];
|
|
1363
1863
|
const codeContext = packet ? ["<semantic_spine>", JSON.stringify(packet.semantic_spine, null, 2), "</semantic_spine>", "", ...(packet.repair ? ["<repair_context>", JSON.stringify(packet.repair, null, 2), "Read the failed receipt and its linked stdout/stderr before editing. Preserve the accepted origin context and fix only the evidenced failure.", "</repair_context>", ""] : []), "<accepted_requirements>", JSON.stringify(packet.requirements, null, 2), "</accepted_requirements>", "", "<write_boundary_invariant>", "For a mutation guarded by membership, ownership, authorization or parent lifecycle state, preserve that predicate in the write statement or make guard and write one explicit transaction with the needed lock. A prior read may diagnose an error but never proves a later write remains allowed. Apply this to create, update, delete and parent-state mutations.", "</write_boundary_invariant>", "", "<acceptance_context>", "The criteria below are end-to-end context. Complete this Work's semantic contribution and declared checks; another ordered Work may own a different acceptance surface.", JSON.stringify(packet.acceptance, null, 2), "</acceptance_context>", "", "<required_read>", "These are mandatory starting sources, not a read allowlist. Read any additional project files needed to implement the Work correctly.", ...packet.required_read.map((item) => `- ${resolveRunReferences(item, work.run_id, requireRunHome(run))}`), "</required_read>", "", "<discovery_boundary>", "These are likely discovery areas, not a hard boundary. Expand project-local investigation when required and report material additions.", ...packet.discovery_boundary.map((item) => `- ${item}`), "</discovery_boundary>", "", "<planned_write_areas>", "SOFT COORDINATION HINT ONLY. These paths help the coordinator avoid concurrent collisions. They do not grant or deny write permission and do not limit the files needed for this Work. You may create or change any project file under workspace_root that is necessary and in semantic scope; report every actual changed path.", ...(packet.planned_write_areas.length ? packet.planned_write_areas.map((item) => `- ${item}`) : ["- none predicted; derive the necessary files from the task"]), "</planned_write_areas>", "", ...(packet.provides_checks.length ? ["<provided_checks>", ...packet.provides_checks.map((item) => `- ${item.id}: materialize ${item.command}${item.definition ? ` as ${item.definition}` : ""}; it is not usable until this Work finishes.`), "Update the declared project command or alias before Work finish. The CLI verifies materialization at Work finish. Execution occurs at its declared run_at gate, not necessarily in this Work.", "</provided_checks>", ""] : []), "<verification>", ...packet.checks.map((item) => `- ${item.id} at ${item.run_at}: ${item.command} — ${item.purpose}`), "The CLI executes work-scoped checks and retains their receipts. Report semantic evidence only; do not rerun declared checks manually.", "</verification>", "", "<stop_conditions>", ...packet.stop_conditions.map((item) => `- ${item}`), "</stop_conditions>", ""] : [];
|
|
1364
1864
|
if (packet)
|
|
1365
1865
|
codeContext.push("<document_updates>", JSON.stringify(packet.document_updates, null, 2), "Materialize every listed update. dd-flow verifies the resulting file against its PLAN-time baseline.", "</document_updates>", "", "<completion_contract>", "Successful completion requires empty deviations and blockers and every assigned document update in changed_paths. A necessary path outside planned_write_areas is normal coordination drift, not a blocker; include it in changed_paths and continue.", "</completion_contract>", "");
|
|
1366
1866
|
if (packet)
|
|
1367
1867
|
codeContext.push("<temporary_services>", "Prefer the declared check launcher: it already owns check resources. If the planned scenario genuinely requires an interactive HTTP service, use the managed supervisor below. This is a template: replace the project service command, port names and readiness path from the plan/project instructions; do not invent a fixed port.", `${command} runtime process start --run ${run.id} --project-root ${JSON.stringify(run.project_root)} --command '<project-service-command>' --ports api --ready-port api --ready-path /health --json --progress-jsonl`, "The service receives DD_FLOW_PORT_API (and equivalent variables for all declared names). The command stays running as its supervisor. Retain its tool handle; wait for the service ready event and read its service.json receipt. Pass those exact ports and the same project environment to reset/seed, API and browser operations.", "A ready receipt proves service readiness only. Record the scenario outcome and real evidence separately. After the scenario, execute the exact stop_command from that receipt, then wait for the supervisor to exit. Never use pkill/killall or stop a sibling's process. If cleanup fails, retain the process id and report the failure; do not claim the resource is free.", "</temporary_services>", "");
|
|
1368
|
-
return ["<work>", `- work_id: ${work.work_id}`, `- run_id: ${work.run_id}`, `- project_root: ${run.project_root}`, `- workspace_root: ${run.workspace_root}`, `- run_home: ${requireRunHome(run)}`, "</work>", "", "<cli_context>", `- Work commands use the short ID ${shortWorkId(work.work_id)}; dd-flow binds it to this RUN.`, `- @project = ${run.project_root}`, `- @workspace = ${run.workspace_root}`, `- @run = ${requireRunHome(run)}`, "- @ aliases are accepted only by declared dd-flow path parameters. Copy the quoted alias literally; do not add backslashes. Shell tools such as cat and rg require ordinary paths relative to cwd or absolute paths.", "- Lifecycle invocation IDs are internal runtime authority and are intentionally omitted from commands.", "</cli_context>", "", "<hard_write_boundary>", ...writeBoundary, "</hard_write_boundary>", "", ...codeContext, "<dependency_results>", JSON.stringify(dependencies.filter(Boolean), null, 2), "</dependency_results>", "", "<task>", resolveRunReferences(work.task, work.run_id, requireRunHome(run)), "</task>", "", ...(work.result_schema ? ["<result_contract>", `Return JSON matching \`${work.result_schema}\`.`, ...resultSchemaGuidance(work, run.id), `Write the completed JSON to ${resultInputPath}. This is an attempt input, not the canonical result.json receipt. dd-flow validates the file and atomically stores the canonical receipt.`, "</result_contract>", ""] : []), "<completion>", packet?.repair?.verification_check_refs?.length ? "Work finish runs its normal work-scoped checks plus the listed causal repair checks. Their original run_at remains an aggregate obligation; this is the additional proof required before accepting this repair." : "Work finish runs only declared run_at=work checks. Stage finish owns readiness/code/merge gates; successful Work completion does not mean those gates have passed.", "A failed receipt means only that the check failed; it is not proof of an engine, harness, dependency, or environment blocker.", ...completionRepair, lifecycleRetryInstruction, "Use Fail only for a concrete external blocker after deterministic bootstrap or a contradiction with an accepted requirement/non-goal. Never fail merely because a necessary project path was absent from planned_write_areas.", "Finish may run for several minutes. Preserve the shell tool's process/session handle and poll that same invocation until it exits; progress arrives as JSONL on stderr. Never reissue Finish merely because final stdout has not arrived.", `First write only the result JSON to ${resultInputPath}. Then run this standalone command:\n${finishWorkCommand}`, `Fail only for an evidenced external or semantic-contract blocker: ${failWorkCommand}`, "</completion>", ""].join("\n");
|
|
1868
|
+
return ["<work>", `- work_id: ${work.work_id}`, `- run_id: ${work.run_id}`, `- project_root: ${run.project_root}`, `- workspace_root: ${run.workspace_root}`, `- run_home: ${requireRunHome(run)}`, "</work>", "", "<cli_context>", `- Work commands use the short ID ${shortWorkId(work.work_id)}; dd-flow binds it to this RUN.`, `- @project = ${run.project_root}`, `- @workspace = ${run.workspace_root}`, `- @run = ${requireRunHome(run)}`, "- @ aliases are accepted only by declared dd-flow path parameters. Copy the quoted alias literally; do not add backslashes. Shell tools such as cat and rg require ordinary paths relative to cwd or absolute paths.", "- Lifecycle invocation IDs are internal runtime authority and are intentionally omitted from commands.", "</cli_context>", "", "<hard_write_boundary>", ...writeBoundary, "</hard_write_boundary>", "", ...codeContext, "<dependency_results>", JSON.stringify(dependencies.filter(Boolean), null, 2), "</dependency_results>", "", "<task>", resolveRunReferences(work.task, work.run_id, requireRunHome(run)), "</task>", "", ...(work.result_schema ? ["<result_contract>", `Return JSON matching \`${work.result_schema}\`.`, ...resultSchemaGuidance(work, run.id), `Write the completed JSON to ${resultInputPath}. This is an attempt input, not the canonical result.json receipt. dd-flow validates the file and atomically stores the canonical receipt.`, "</result_contract>", ""] : []), "<completion>", repairPacket ? "Repair Finish validates and publishes the exact private delta before Work settlement. Independent re-review, correction checks and Stage/RUN acceptance remain separate obligations." : packet?.repair?.verification_check_refs?.length ? "Work finish runs its normal work-scoped checks plus the listed causal repair checks. Their original run_at remains an aggregate obligation; this is the additional proof required before accepting this repair." : "Work finish runs only declared run_at=work checks. Stage finish owns readiness/code/merge gates; successful Work completion does not mean those gates have passed.", "A failed receipt means only that the check failed; it is not proof of an engine, harness, dependency, or environment blocker.", ...completionRepair, ...(repairPacket ? [] : [lifecycleRetryInstruction]), repairPacket ? "Use Fail for an evidenced external blocker or assignment conflict, including any required edit outside the exact write_paths allowlist. Never expand that allowlist or edit the shared checkout." : "Use Fail only for a concrete external blocker after deterministic bootstrap or a contradiction with an accepted requirement/non-goal. Never fail merely because a necessary project path was absent from planned_write_areas.", "Finish may run for several minutes. Preserve the shell tool's process/session handle and poll that same invocation until it exits; progress arrives as JSONL on stderr. Never reissue Finish merely because final stdout has not arrived.", `First write only the result JSON to ${resultInputPath}. Then run this standalone command:\n${finishWorkCommand}`, `Fail only for an evidenced external or semantic-contract blocker: ${failWorkCommand}`, "</completion>", ""].join("\n");
|
|
1369
1869
|
}
|
|
1370
1870
|
export function resultSchemaGuidance(work, runId) {
|
|
1371
1871
|
const schema = work.result_schema;
|
|
1872
|
+
if (schema === "dd-flow/memory-composition-work-result@1") {
|
|
1873
|
+
const packet = parsePayload(work)?.memory_composition;
|
|
1874
|
+
if (!packet || typeof packet.packet_sha256 !== "string" || !Array.isArray(packet.unit_ids) || !packet.unit_ids.length
|
|
1875
|
+
|| packet.unit_ids.some(id => typeof id !== "string" || !id) || !Array.isArray(packet.write_scopes) || !packet.write_scopes.length
|
|
1876
|
+
|| packet.write_scopes.some(scope => !scope || !["workspace", "run"].includes(scope.root) || typeof scope.path !== "string" || !scope.path))
|
|
1877
|
+
throw new AppError("memory_composition_input_invalid", "Composition result guidance requires its registered assignment", 1);
|
|
1878
|
+
const { packet_sha256 } = packet;
|
|
1879
|
+
const scope = packet.write_scopes[0];
|
|
1880
|
+
return ["This is a shape template, not a completed result. Return only JSON with the fields shown; do not add agent metadata, evidence, findings or changed_paths. Keep the assigned run_id, work_id and packet_sha256 unchanged and include every assigned unit exactly once.",
|
|
1881
|
+
`Exact write_scopes: ${JSON.stringify(packet.write_scopes)}. Each unit needs a summary and nonempty artifacts with exactly root, path and sha256. Artifact root is workspace or run; path is relative to that root and must name an actual regular output file within an assigned write_scope. Compute sha256 from its current bytes. Replace the example path when it names a directory, and replace the zero hash; never invent output hashes or cite the old bootstrap template as current output.`,
|
|
1882
|
+
"Use accepted only after completing the assigned documents. For an evidenced blocker, retain the evidence and use the assigned Fail command, not a blocked result with Finish. Work settlement does not accept Stage/RUN or independent review.",
|
|
1883
|
+
"```json", JSON.stringify({ schema_id: schema, run_id: runId, work_id: work.work_id, packet_sha256, outcome: "accepted", summary: "Describe the actual completed documents.",
|
|
1884
|
+
units: packet.unit_ids.map(id => ({ id, summary: "Describe this unit's actual output.", artifacts: [{ root: scope.root, path: scope.path === "." ? "replace-with-assigned-output.md" : scope.path, sha256: "0".repeat(64) }] })) }, null, 2), "```"];
|
|
1885
|
+
}
|
|
1886
|
+
if (schema === "dd-flow/memory-repair-work-result@1") {
|
|
1887
|
+
const packet = parsePayload(work)?.memory_repair;
|
|
1888
|
+
if (!packet || typeof packet.packet_sha256 !== "string" || !Array.isArray(packet.write_paths) || !Array.isArray(packet.findings))
|
|
1889
|
+
throw new AppError("memory_repair_input_invalid", "Repair guidance requires its registered packet", 1);
|
|
1890
|
+
return ["This is a shape template, not a completed repair. Return only JSON matching the frozen schema. Use accepted only with a nonempty exact physical create/update/delete delta and one causal resolution for every assigned finding. Only registered write_paths may change. before_sha256 comes from the original registered start view; after_sha256 comes from the actual private output bytes; null means absent or deleted. Never invent hashes, paths, findings, source refs or completion.",
|
|
1891
|
+
`Assigned finding ids: ${JSON.stringify(packet.findings.map(finding => finding.id))}. Exact write_paths: ${JSON.stringify(packet.write_paths)}.`,
|
|
1892
|
+
"Each resolution needs finding_id, summary, nonempty delta_paths and nonempty evidence with exact root/path/sha256. A surviving changed output must be cited with its after hash. A deletion-only resolution needs registered retained RUN evidence. Unchanged evidence must match registered input hashes. Accepted does not claim independent re-review or closure.",
|
|
1893
|
+
"Use blocked only when the MB copy is unchanged and changes/resolutions are empty. If a blocker occurs after edits, preserve the private partial outcome and use Fail; do not conceal those edits as a clean blocked result or mutate shared files.",
|
|
1894
|
+
"Repair completion is publication, not the generic CODE check/retry path. Submit only an accepted private result to the assigned managed work finish command. The CLI independently verifies your bounded UTF-8 result (maximum 1 MiB), exact original start/delta and current publication custody, reserves or restores publication, applies its finite retained plan, then settles Work. Do not run memory repair publication-* commands manually or use --retry-check. Successful Work settlement does not close findings, accept independent re-review, or finish Stage/RUN.",
|
|
1895
|
+
"For a clean blocked result, retain its evidence and use the assigned managed Fail command with the concrete reason; do not submit blocked to Finish. Before any publication attempt, an evidenced blocker after private edits also uses Fail while preserving those private bytes. Once Finish reports partial, unknown, memory_repair_publication_pending or memory_repair_completion_pending, do not use Fail, replace result bytes, reissue Finish, or start another Session: retain the exact operation/WorkSession/receipt and await controller reconciliation. A pending publication is not a new semantic blocker.",
|
|
1896
|
+
"Poll the original Finish tool handle until terminal. A missing reply, timeout or observation failure is not permission to repeat the command. Resume only through the controller's exact retained-invocation recovery assignment; never invent invocation IDs, adopt today's shared bytes as a new baseline, or treat generic check-retry guidance as repair-publication recovery.",
|
|
1897
|
+
"```json", JSON.stringify({ schema_id: schema, run_id: runId, work_id: work.work_id, packet_sha256: packet.packet_sha256,
|
|
1898
|
+
wave: packet.wave, outcome: "blocked", summary: "Replace with the concrete evidenced outcome.", changes: [], resolutions: [] }, null, 2), "```"];
|
|
1899
|
+
}
|
|
1900
|
+
if (schema === "dd-flow/memory-review-work-result@1") {
|
|
1901
|
+
const packet = parsePayload(work)?.memory_review;
|
|
1902
|
+
if (!packet?.unit || typeof packet.unit.id !== "string" || typeof packet.packet_sha256 !== "string")
|
|
1903
|
+
throw new AppError("memory_review_input_invalid", "Reviewer result guidance requires its registered assignment", 1);
|
|
1904
|
+
return ["The template below is not a completed review. Replace the summary with an evidence-backed assessment and choose accepted, needs_repair or blocked. Populate assessment evidence with nonempty exact root/path/sha256 references from the registered wave inputs, including an assigned unit output and, when present, an assigned baseline source; project sources use the registered workspace alias. Findings require the assigned unit_id, a concrete problem, minor/major/critical severity and nonempty exact registered references. A material finding cannot accompany accepted. Choose needs_repair only when reporting a major or critical finding. Do not claim another unit or invent references.",
|
|
1905
|
+
"```json", JSON.stringify({ schema_id: schema, run_id: runId, work_id: work.work_id,
|
|
1906
|
+
packet_sha256: packet.packet_sha256, unit_id: packet.unit.id, wave: packet.wave,
|
|
1907
|
+
outcome: "accepted", summary: "Replace with the actual independent assessment.", evidence: [], findings: [] }, null, 2), "```"];
|
|
1908
|
+
}
|
|
1372
1909
|
const refs = `Evidence refs for project source are relative to workspace_root; RUN evidence uses run://${runId}/path/to/artifact.`;
|
|
1373
1910
|
if (schema === "dd-flow/code-work-result@3") {
|
|
1374
1911
|
const packet = codePacket(work);
|
|
@@ -1533,12 +2070,20 @@ function validateItem(value, requireExecutionContext = false) { if (!value || ty
|
|
|
1533
2070
|
throw new AppError("validation", "result_schema must be a non-empty schema id", 2); const payload = code ? item : (item.payload && typeof item.payload === "object" && !Array.isArray(item.payload) ? item.payload : undefined); return { key: item.key, task: item.task, ...(Array.isArray(item.depends_on) ? { depends_on: item.depends_on } : {}), ...(typeof item.parent === "string" ? { parent: item.parent } : {}), ...(typeof item.launch_policy === "string" ? { launch_policy: item.launch_policy } : {}), ...(typeof item.result_schema === "string" ? { result_schema: item.result_schema } : {}), ...(payload ? { payload } : {}) }; }
|
|
1534
2071
|
function allowsEmptyRepairChecks(item) { const repair = item.repair; if (!repair || typeof repair !== "object" || Array.isArray(repair))
|
|
1535
2072
|
return false; const value = repair; return typeof value.check_receipt_id === "string" || (Array.isArray(value.review_findings) && value.review_findings.some((finding) => typeof finding === "object" && finding !== null && Array.isArray(finding.check_refs) && finding.check_refs.some((ref) => typeof ref === "string"))) || (Array.isArray(value.semantic_unresolved) && value.semantic_unresolved.some((item) => typeof item === "string")); }
|
|
1536
|
-
export function refreshRunWorkProjection(context, projectId, runId) {
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
2073
|
+
export function refreshRunWorkProjection(context, projectId, runId) {
|
|
2074
|
+
refreshRunSessionProjection(context, projectId, runId);
|
|
2075
|
+
scheduleObsoleteWorkProjectionCleanup(context, projectId, runId);
|
|
2076
|
+
}
|
|
2077
|
+
function scheduleObsoleteWorkProjectionCleanup(context, projectId, runId) {
|
|
2078
|
+
const run = context.db.get("SELECT run_root FROM runs WHERE project_id = ? AND id = ?", [projectId, runId]);
|
|
2079
|
+
if (run?.run_root) {
|
|
2080
|
+
const obsolete = path.join(run.run_root, "work.json");
|
|
2081
|
+
// A failed allocation/settlement must not remove a pre-existing projection.
|
|
2082
|
+
// Like publication, filesystem cleanup belongs after the outer SQL commit.
|
|
2083
|
+
context.db.afterCommit(() => fs.rmSync(obsolete, { force: true }));
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
export function codeWorkGraph(context, projectId, runId) { const index = JSON.parse(context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [projectId, runId])?.index_json ?? "{}"); const active = index.stage_runs?.some(item => item.stage === "code-review" && item.status === "running") ? "code-review" : "code"; const members = index.stage_runs?.length ? stageWorkIds(context, projectId, runId, active) : null; const works = context.db.all(`SELECT ${workColumns} FROM works WHERE project_id = ? AND run_id = ? ORDER BY created_at, work_id`, [projectId, runId]).filter((work) => Boolean(codePacket(work)) && (!members || members.has(work.work_id))); const ready = works.filter((work) => isReady(context, work)); return { total: works.length, created: works.filter((work) => work.status === "created").length, running: works.filter((work) => work.status === "running").length, completed: works.filter((work) => work.status === "completed").length, failed: works.filter((work) => work.status === "failed").length, ready: ready.map((work) => ({ work_id: work.work_id, task: work.task, launch_policy: work.launch_policy, start_command: workStartCommand(context, work, { preview: true }) })), blocked: works.filter((work) => work.status === "created" && !ready.includes(work)).map((work) => ({ work_id: work.work_id, depends_on: parseDependencies(work) })) }; }
|
|
1542
2087
|
function parsePayload(work) { if (!work.payload_json)
|
|
1543
2088
|
return null; try {
|
|
1544
2089
|
const value = JSON.parse(work.payload_json);
|