@deksden-com/dd-flow-cli 0.9.0-beta.136 → 0.9.0-beta.138
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 +12 -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 +56 -3
- package/dist/cli/command-option-descriptions.js +5 -0
- package/dist/cli/executor-onboarding.js +8 -3
- package/dist/cli/flows.js +29 -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/native-hook-ingress.js +1 -1
- package/dist/cli/response-file.js +2 -0
- package/dist/cli/run-cli.js +283 -160
- package/dist/cli/setup.js +9 -7
- package/dist/cli.js +54 -2
- package/dist/domain/stage-handler-identities.js +20 -0
- package/dist/harness-runtime/DD-ZCODE.md +13 -0
- package/dist/harness-runtime/bin/dd-opencode.mjs +3 -3
- 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 +4 -2
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +11 -5
- package/dist/harness-runtime/lib/dd-codex.mjs +22 -10
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +2 -1
- package/dist/harness-runtime/lib/dd-droid.mjs +2 -1
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +4 -2
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +20 -5
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +45 -10
- package/dist/harness-runtime/lib/dd-zcode.mjs +7 -3
- package/dist/harness-runtime/lib/delegation-instructions.mjs +5 -3
- 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-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 +5 -1
- package/dist/harness-runtime/lib/operation-progress.mjs +3 -1
- 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 +22 -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/code-checks.js +387 -34
- package/dist/services/controller-child-observer.js +99 -0
- package/dist/services/controller-execution.js +88 -0
- package/dist/services/controller-fanout.js +79 -13
- 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/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/external-work-launch.js +91 -11
- package/dist/services/flow-catalog.js +21 -0
- package/dist/services/flow-descriptors.js +82 -0
- package/dist/services/frozen-engine.js +242 -0
- package/dist/services/generic-execution.js +164 -0
- package/dist/services/harness-adapter.js +28 -2
- package/dist/services/hooks.js +24 -6
- package/dist/services/hybrid-contracts.js +31 -3
- package/dist/services/ids.js +29 -1
- package/dist/services/installation-lock.js +7 -4
- package/dist/services/installed-flow-source.js +183 -0
- package/dist/services/lifecycle-invocations.js +257 -78
- 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 +241 -0
- package/dist/services/memory-init-preflight.js +312 -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 +175 -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 +172 -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/projects.js +17 -2
- package/dist/services/recovery-snapshot-database.js +416 -1
- 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-capture.js +10 -3
- package/dist/services/run-controller-process.js +21 -0
- package/dist/services/run-controller-recovery.js +3 -1
- package/dist/services/run-controller-state.js +1 -1
- package/dist/services/run-controller.js +184 -48
- package/dist/services/run-recovery-guard.js +2 -0
- package/dist/services/run-recovery.js +145 -5
- package/dist/services/run-start-custody.js +177 -0
- package/dist/services/runs.js +319 -25
- package/dist/services/stage-context.js +9 -1
- package/dist/services/stage-handlers.js +74 -0
- package/dist/services/stage-lifecycle.js +2 -1
- package/dist/services/stage-report-renderer.js +76 -0
- package/dist/services/stage-work-graph.js +17 -0
- package/dist/services/vnext-code.js +3 -3
- package/dist/services/vnext-execution-profile.js +2 -2
- package/dist/services/vnext-fanout.js +136 -10
- package/dist/services/vnext-merge.js +8 -3
- package/dist/services/vnext-plan-review.js +6 -4
- package/dist/services/vnext-protocolize.js +60 -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
|
@@ -4,6 +4,69 @@ import crypto from "node:crypto";
|
|
|
4
4
|
import { AppError } from "../shared/errors.js";
|
|
5
5
|
import { withProjectionLock } from "../storage/projection-lock.js";
|
|
6
6
|
import { writeJsonAtomic } from "../storage/paths.js";
|
|
7
|
+
import { assertRunBundleSqlAuthority } from "./runs.js";
|
|
8
|
+
import { hybridDigest } from "./hybrid-contracts.js";
|
|
9
|
+
/** Accepted bytes come from SQL receipts, never from the mutable publication.
|
|
10
|
+
* Stage completion and its independent committed checksum must agree. This is
|
|
11
|
+
* byte custody, not a substitute for the registered handler's semantic gates.
|
|
12
|
+
*/
|
|
13
|
+
export function readAcceptedStageData(context, scope) {
|
|
14
|
+
function refuse() { throw new AppError("stage_data_custody_invalid", "Accepted stage data lacks matching committed SQL byte custody", 1, { run_id: scope.runId, stage: scope.stage }); }
|
|
15
|
+
const run = context.db.get("SELECT index_json, run_root, run_dir FROM runs WHERE project_id = ? AND id = ?", [scope.projectId, scope.runId]);
|
|
16
|
+
if (!run)
|
|
17
|
+
refuse();
|
|
18
|
+
const index = JSON.parse(run.index_json);
|
|
19
|
+
assertRunBundleSqlAuthority(context, scope, index.execution_bundle);
|
|
20
|
+
const stage = index.stage_runs?.find((item) => item.stage === scope.stage);
|
|
21
|
+
const portable = (value) => typeof value === "string" && Boolean(value) && !path.posix.isAbsolute(value) && !path.win32.isAbsolute(value) && !value.includes("\\") && !value.includes("\0") && value.split("/").every(part => Boolean(part) && part !== "." && part !== "..");
|
|
22
|
+
if (!stage || stage.status !== "done" || !portable(stage.dir) || !portable(stage.data)
|
|
23
|
+
|| typeof stage.attempt !== "string" || !/^try-[0-9]{3,}$/.test(stage.attempt)
|
|
24
|
+
|| typeof stage.data_sha256 !== "string" || !/^[a-f0-9]{64}$/.test(stage.data_sha256))
|
|
25
|
+
refuse();
|
|
26
|
+
const root = run.run_root ?? run.run_dir;
|
|
27
|
+
if (!path.isAbsolute(root))
|
|
28
|
+
refuse();
|
|
29
|
+
const relative = `${stage.dir}/${stage.data}`, destination = path.resolve(root, relative);
|
|
30
|
+
const rows = context.db.all("SELECT event_type, payload_json FROM audit_events WHERE project_id = ? AND event_type IN ('flow_run.stage_packet', 'flow_run.stage_report') AND json_extract(payload_json, '$.run_id') = ? AND json_extract(payload_json, '$.stage') = ? AND json_extract(payload_json, '$.attempt') = ? LIMIT 33", [scope.projectId, scope.runId, scope.stage, stage.attempt]);
|
|
31
|
+
if (rows.length > 32)
|
|
32
|
+
refuse();
|
|
33
|
+
let accepted;
|
|
34
|
+
for (const row of rows) {
|
|
35
|
+
if (Buffer.byteLength(row.payload_json) > 8 * 1024 * 1024)
|
|
36
|
+
refuse();
|
|
37
|
+
const receipt = JSON.parse(row.payload_json);
|
|
38
|
+
if (receipt.sha256 !== stage.data_sha256)
|
|
39
|
+
continue;
|
|
40
|
+
const locator = row.event_type === "flow_run.stage_packet" ? receipt.file : typeof receipt.root === "string" ? path.join(receipt.root, "stage-report.json") : null;
|
|
41
|
+
if (typeof locator !== "string" || resolveStagePublicationPath(context, scope.projectId, scope.runId, locator) !== destination)
|
|
42
|
+
continue;
|
|
43
|
+
let bytes;
|
|
44
|
+
if (row.event_type === "flow_run.stage_packet") {
|
|
45
|
+
if (typeof receipt.base64 !== "string")
|
|
46
|
+
refuse();
|
|
47
|
+
bytes = Buffer.from(receipt.base64, "base64");
|
|
48
|
+
if (bytes.toString("base64") !== receipt.base64)
|
|
49
|
+
refuse();
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
if (typeof receipt.bytes?.json !== "string")
|
|
53
|
+
refuse();
|
|
54
|
+
bytes = Buffer.from(receipt.bytes.json);
|
|
55
|
+
}
|
|
56
|
+
if (!bytes.length || bytes.length > 1024 * 1024 || crypto.createHash("sha256").update(bytes).digest("hex") !== stage.data_sha256)
|
|
57
|
+
refuse();
|
|
58
|
+
if (accepted && !accepted.equals(bytes))
|
|
59
|
+
refuse();
|
|
60
|
+
accepted = bytes;
|
|
61
|
+
}
|
|
62
|
+
if (!accepted)
|
|
63
|
+
refuse();
|
|
64
|
+
return { bytes: Buffer.from(accepted), result: { path: relative, sha256: stage.data_sha256 },
|
|
65
|
+
input_sha256: stage.input_sha256, data_schema_id: stage.data_schema_id,
|
|
66
|
+
authority_sha256: hybridDigest({ scope, attempt: stage.attempt, directory: stage.dir, data: stage.data,
|
|
67
|
+
input_sha256: stage.input_sha256 ?? null, data_schema_id: stage.data_schema_id ?? null,
|
|
68
|
+
anchor: index.execution_bundle ?? null, data_sha256: stage.data_sha256 }) };
|
|
69
|
+
}
|
|
7
70
|
/** Deterministic JSON -> Markdown/HTML projection shared by every stage. */
|
|
8
71
|
export function writeStageReport(root, report) {
|
|
9
72
|
const json = path.join(root, "stage-report.json");
|
|
@@ -164,6 +227,19 @@ export function reconcileRunStageReports(context, projectId, runId, exceptStage)
|
|
|
164
227
|
}
|
|
165
228
|
function normalizeStageReport(report) {
|
|
166
229
|
const semantic = object(report.semantic);
|
|
230
|
+
if (semantic.materialization !== undefined) {
|
|
231
|
+
const value = object(semantic.materialization);
|
|
232
|
+
const authored = object(value.authored_result);
|
|
233
|
+
const relative = (item) => typeof item === "string" && item.length > 0 && !path.isAbsolute(item) && !item.split(/[\\/]/).includes("..");
|
|
234
|
+
if (value.schema_id !== "dd-flow/protocolize-materialization@1" || value.run_id !== report.run_id || report.stage !== "protocolize" || value.stage !== "protocolize"
|
|
235
|
+
|| !["create", "link", "update", "not_applicable"].includes(String(value.action)) || value.status !== "passed"
|
|
236
|
+
|| authored.role !== "authored_input" || !relative(authored.run_relative_path) || !/^[a-f0-9]{64}$/.test(String(authored.sha256))
|
|
237
|
+
|| !(value.feature_path === null || relative(value.feature_path)) || !Array.isArray(value.protocol_ids) || value.protocol_ids.some(id => typeof id !== "string")
|
|
238
|
+
|| !Array.isArray(value.documents) || value.documents.some(item => { const ref = object(item); return !relative(ref.path) || !/^[a-f0-9]{64}$/.test(String(ref.sha256)); })
|
|
239
|
+
|| !Array.isArray(value.owned_links) || value.owned_links.some(item => { const edge = object(item); return !relative(edge.feature_path) || !relative(edge.protocol_path) || !value.protocol_ids.includes(edge.protocol_id); })) {
|
|
240
|
+
throw new AppError("stage_report_materialization_invalid", "PROTOCOLIZE materialization receipt is malformed or out of scope", 1);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
167
243
|
const sourceArtifacts = object(report.artifacts);
|
|
168
244
|
return {
|
|
169
245
|
...report,
|
|
@@ -7,6 +7,23 @@ export function stageAttempt(context, projectId, runId, stage) {
|
|
|
7
7
|
return [...stages].reverse().find(item => item.stage === stage)?.attempt ?? "try-001";
|
|
8
8
|
}
|
|
9
9
|
export function bindStageWork(context, input) {
|
|
10
|
+
context.db.writeTransaction(() => bindStageWorkOwned(context, input), "stage-work.bind");
|
|
11
|
+
}
|
|
12
|
+
function bindStageWorkOwned(context, input) {
|
|
13
|
+
const run = context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [input.projectId, input.runId]);
|
|
14
|
+
const index = run ? JSON.parse(run.index_json) : null;
|
|
15
|
+
if (index?.execution_bundle) {
|
|
16
|
+
// Stage attachment already admitted the frozen descriptor outside the writer.
|
|
17
|
+
// Membership checks here consume only authoritative SQLite facts.
|
|
18
|
+
const stage = index.stage_runs.find(item => item.stage === input.stage && item.status === "running");
|
|
19
|
+
const work = context.db.get("SELECT parent_work_id FROM works WHERE project_id = ? AND run_id = ? AND work_id = ?", [input.projectId, input.runId, input.workId]);
|
|
20
|
+
const parentOwned = !work?.parent_work_id || context.db.get("SELECT 1 FROM works WHERE project_id = ? AND run_id = ? AND work_id = ?", [input.projectId, input.runId, work.parent_work_id]);
|
|
21
|
+
const currentStage = index.current_stage ?? (index.execution_bundle.flow_id === "sdlc" ? index.stage_runs.at(-1)?.stage : undefined);
|
|
22
|
+
if (!stage || currentStage !== input.stage || !work || !parentOwned || input.attempt !== undefined && input.attempt !== (stage.attempt ?? "try-001")
|
|
23
|
+
|| input.cycle !== undefined && input.cycle !== stageCycle(context, input.projectId, input.runId, input.stage)) {
|
|
24
|
+
throw new AppError("stage_work_scope_invalid", "Work membership does not belong to the active frozen stage scope", 1, { run_id: input.runId, stage: input.stage, work_id: input.workId });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
10
27
|
context.db.run("INSERT OR IGNORE INTO stage_work_memberships (project_id, run_id, work_id, stage, attempt, cycle) VALUES (?, ?, ?, ?, ?, ?)", [input.projectId, input.runId, input.workId, input.stage, input.attempt ?? stageAttempt(context, input.projectId, input.runId, input.stage), input.cycle ?? stageCycle(context, input.projectId, input.runId, input.stage)]);
|
|
11
28
|
}
|
|
12
29
|
export function stageCycle(context, projectId, runId, stage) {
|
|
@@ -498,7 +498,7 @@ export function prepareVnextCodeRepair(context, input) {
|
|
|
498
498
|
export function addVnextCodeRepair(context, input, prepared = prepareVnextCodeRepair(context, input)) {
|
|
499
499
|
const { project, run } = prepared;
|
|
500
500
|
if (prepared.kind === "replay")
|
|
501
|
-
return { ok: true, run_id: run.id, repair_work_id: prepared.previous.work_id, start_command: workStartCommand(context, prepared.previous), reused: true };
|
|
501
|
+
return { ok: true, run_id: run.id, repair_work_id: prepared.previous.work_id, start_command: workStartCommand(context, prepared.previous, { preview: true }), reused: true };
|
|
502
502
|
const { parentWorkId, items, key, receipt } = prepared;
|
|
503
503
|
const create = () => String(addWorkBatch(context, { parentWorkId, prepared: items, stage: input.stage ?? (input.reviewCycle ? "code-review" : "code"), ...(input.reviewCycle ? { cycle: input.reviewCycle } : {}) }).work_ids[key]);
|
|
504
504
|
const id = input.registrationOwned ? create() : ensureRepairIntent(context, {
|
|
@@ -513,7 +513,7 @@ export function addVnextCodeRepair(context, input, prepared = prepareVnextCodeRe
|
|
|
513
513
|
...(receipt ? { check_receipt_id: receipt.id } : prepared.input.reviewFindings?.length ? { review_finding_ids: prepared.input.reviewFindings.map((finding) => finding.finding_ref) } : { semantic_unresolved: prepared.input.semanticUnresolved })
|
|
514
514
|
});
|
|
515
515
|
refreshRunWorkProjection(context, project.id, run.id);
|
|
516
|
-
return { ok: true, run_id: run.id, repair_work_id: id, start_command: workStartCommand(context, work) };
|
|
516
|
+
return { ok: true, run_id: run.id, repair_work_id: id, start_command: workStartCommand(context, work, { preview: true }) };
|
|
517
517
|
}
|
|
518
518
|
/** Retain the causal declaration for worker context; run_at still records its original gate. */
|
|
519
519
|
export function selectRepairChecks(input) {
|
|
@@ -730,7 +730,7 @@ async function ensureCodeWorkspaceReady(context, projectId, run, root, readiness
|
|
|
730
730
|
const profile = JSON.parse(run.index_json);
|
|
731
731
|
const bootstrap = profile.execution_profile?.settings?.code_bootstrap;
|
|
732
732
|
if (!bootstrap || typeof bootstrap.command !== "string" || typeof bootstrap.policy_ref !== "string") {
|
|
733
|
-
throw new AppError("execution_profile_invalid", "CODE requires
|
|
733
|
+
throw new AppError("execution_profile_invalid", "CODE requires an evidenced bootstrap command and policy in the frozen RUN profile; configure code_bootstrap before starting a code-ready RUN", 1, { run_id: run.id });
|
|
734
734
|
}
|
|
735
735
|
const receiptPath = path.join(root, "workspace-readiness.json");
|
|
736
736
|
const ownerId = context.env.DD_FLOW_CODE_READINESS_OWNER;
|
|
@@ -93,8 +93,8 @@ export function validateVnextExecutionProfile(value) {
|
|
|
93
93
|
|| profile.merge_delivery?.strategy !== "local"
|
|
94
94
|
|| !["retain", "delete_after_success"].includes(String(profile.merge_cleanup?.source))
|
|
95
95
|
|| !["code_completed", "code_review_completed", "merge_completed"].includes(String(profile.stop_target))
|
|
96
|
-
|| !profile.code_bootstrap || typeof profile.code_bootstrap.command !== "string" || !profile.code_bootstrap.command.trim()
|
|
97
|
-
|
|
96
|
+
|| (profile.code_bootstrap !== undefined && (!profile.code_bootstrap || typeof profile.code_bootstrap.command !== "string" || !profile.code_bootstrap.command.trim()
|
|
97
|
+
|| typeof profile.code_bootstrap.policy_ref !== "string" || !profile.code_bootstrap.policy_ref.trim()))
|
|
98
98
|
|| (profile.execution === undefined ? profile.schema_id === "dd-flow/project-execution@3" : !isExecutionRouting(profile.execution))) {
|
|
99
99
|
throw new AppError("execution_profile_invalid", "Project execution profile does not match its supported dd-flow schema", 1);
|
|
100
100
|
}
|
|
@@ -3,14 +3,16 @@ import path from "node:path";
|
|
|
3
3
|
import { AppError } from "../shared/errors.js";
|
|
4
4
|
import { resolveProjectRoot } from "../storage/paths.js";
|
|
5
5
|
import { requireProjectByRoot } from "./projects.js";
|
|
6
|
-
import { getFlowRunVariables, appendFlowRunTimelineEvent } from "./runs.js";
|
|
6
|
+
import { getFlowRunVariables, appendFlowRunTimelineEvent, readFlowRunExecutionBundle } from "./runs.js";
|
|
7
7
|
import { resolveExecutionPolicy } from "./execution-policy.js";
|
|
8
|
-
import { listWorks, failWork, cancelWork } from "./work-registry.js";
|
|
8
|
+
import { listWorks, failWork, cancelWork, readWorkBatchCustody } from "./work-registry.js";
|
|
9
9
|
import { storageSessionId } from "./session-identity.js";
|
|
10
10
|
import { reconcileUnfinishedChecks } from "./code-checks.js";
|
|
11
11
|
import { vnextStageDirectory } from "../domain/stage-catalog.js";
|
|
12
12
|
import { stageWorkIds, stageGraphScope } from "./stage-work-graph.js";
|
|
13
13
|
import { eligibleCodexChildOverload } from "./codex-capacity.js";
|
|
14
|
+
import { readDescriptorRootWork } from "./descriptor-root-work.js";
|
|
15
|
+
import { selectMemoryCompositionRegistration } from "./memory-composition-registration.js";
|
|
14
16
|
import { safeNativeFailure } from "../harness-runtime/lib/native-children.mjs";
|
|
15
17
|
export const subagentCapacityKey = "runtime.subagent.available_slots";
|
|
16
18
|
/** External concurrency is policy, never evidence of native child qualification. */
|
|
@@ -20,17 +22,85 @@ export function getStageFanoutCapacity(context, input) {
|
|
|
20
22
|
const run = context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [project.id, input.runId]);
|
|
21
23
|
if (!run)
|
|
22
24
|
throw new AppError("not_found", "RUN is unavailable", 1);
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
+
const index = JSON.parse(run.index_json);
|
|
26
|
+
const bundle = index.execution_bundle ? readFlowRunExecutionBundle(context, { projectRoot, runId: input.runId }) : null;
|
|
27
|
+
const generic = bundle?.execution.work(input.stage).delegation;
|
|
28
|
+
if (generic?.mode === "external")
|
|
29
|
+
return input.nativeOnly
|
|
30
|
+
? { run_key: null, available_slots: null, source: "harness_qualification" }
|
|
31
|
+
: { run_key: null, available_slots: generic.max_parallel, source: "external_policy" };
|
|
32
|
+
// Generic native admission already requires a scoped capacity receipt whose
|
|
33
|
+
// observed count covers this frozen limit. Mutable legacy variables cannot
|
|
34
|
+
// replace that receipt or silently prevent its productive dispatch.
|
|
35
|
+
if (generic?.mode === "native")
|
|
36
|
+
return { run_key: null, available_slots: generic.max_parallel, source: "harness_qualification" };
|
|
37
|
+
const frozen = index.execution_profile;
|
|
38
|
+
if (!bundle && frozen?.settings.execution) {
|
|
25
39
|
if (!frozen.agent_profiles)
|
|
26
40
|
throw new AppError("execution_profile_not_frozen", "Fan-out needs the RUN's frozen profiles", 1);
|
|
27
41
|
const resolved = resolveExecutionPolicy({ ddFlowHome: context.ddFlowHome, policy: frozen.settings.execution, profiles: frozen.agent_profiles, stage: input.stage });
|
|
28
|
-
if (resolved.delegation.mode === "external")
|
|
42
|
+
if (resolved.delegation.mode === "external" && !input.nativeOnly)
|
|
29
43
|
return { run_key: null, available_slots: resolved.delegation.max_parallel, source: "external_policy" };
|
|
30
44
|
}
|
|
31
45
|
const variables = getFlowRunVariables(context, { projectRoot, runId: input.runId });
|
|
32
46
|
const available = variables.variables[subagentCapacityKey];
|
|
33
|
-
return { run_key: subagentCapacityKey, available_slots: typeof available === "number" && Number.isInteger(available) && available >= 0 ? available : null, source: "harness_qualification" };
|
|
47
|
+
return { run_key: subagentCapacityKey, available_slots: typeof available === "number" && Number.isInteger(available) && available >= 0 ? Math.min(available, generic?.max_parallel ?? available) : null, source: "harness_qualification" };
|
|
48
|
+
}
|
|
49
|
+
function nativeFailureController(context, projectId, runId, stage, parent) {
|
|
50
|
+
if (!context.db.get("SELECT 1 FROM sqlite_master WHERE type='table' AND name='run_controllers'"))
|
|
51
|
+
return null;
|
|
52
|
+
const rows = context.db.all("SELECT controller_id,generation,state_json FROM run_controllers WHERE project_id=? AND run_id=? AND status NOT IN ('completed','cancelled','superseded')", [projectId, runId]);
|
|
53
|
+
const matching = rows.filter(row => {
|
|
54
|
+
try {
|
|
55
|
+
const state = JSON.parse(row.state_json);
|
|
56
|
+
return state.stage === stage && state.sessions?.[state.current_session]?.id === parent;
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
return matching.length === 1 ? matching[0] : null;
|
|
63
|
+
}
|
|
64
|
+
/** Failure metadata authored by a Work is not native evidence. Require the
|
|
65
|
+
* independently recorded reconciliation and its exact current registry links. */
|
|
66
|
+
export function verifiedNativeWorkFailure(context, input) {
|
|
67
|
+
const b = input.receipt.native_binding;
|
|
68
|
+
if (!b || b.schema_id !== "dd-flow/native-work-failure@1")
|
|
69
|
+
return;
|
|
70
|
+
const project = requireProjectByRoot(context, input.projectRoot);
|
|
71
|
+
const scope = stageGraphScope(context, project.id, input.runId, input.stage);
|
|
72
|
+
const controller = nativeFailureController(context, project.id, input.runId, input.stage, input.parentSessionId);
|
|
73
|
+
if (!controller || b.controller_id !== controller.controller_id || b.generation !== controller.generation
|
|
74
|
+
|| b.project_id !== project.id || b.run_id !== input.runId || b.stage !== input.stage
|
|
75
|
+
|| b.stage_attempt_id !== scope.attempt || b.stage_cycle !== scope.cycle
|
|
76
|
+
|| b.work_id !== input.workId || b.parent_work_id !== input.parentWorkId
|
|
77
|
+
|| b.parent_provider_session_id !== input.parentSessionId || b.terminal_turn_id !== input.receipt.terminal_turn_id
|
|
78
|
+
|| input.receipt.source !== "native_child_observation" || input.receipt.outcome !== "failed")
|
|
79
|
+
return;
|
|
80
|
+
const link = context.db.get(`SELECT ws.id FROM work_sessions ws JOIN works w ON w.work_id=ws.work_id JOIN sessions s ON s.project_id=w.project_id AND s.session_id=ws.session_id
|
|
81
|
+
WHERE ws.id=? AND w.project_id=? AND w.run_id=? AND w.work_id=? AND w.parent_work_id=? AND ws.status='failed' AND w.status='failed' AND s.parent_session_id=? AND s.provider_session_id=? AND s.harness=?`, [b.work_session_id, project.id, input.runId, input.workId, input.parentWorkId, storageSessionId({ harness_id: b.harness_id, session_id: input.parentSessionId }), b.provider_session_id, b.harness_id]);
|
|
82
|
+
if (!link)
|
|
83
|
+
return;
|
|
84
|
+
const run = context.db.get("SELECT run_root FROM runs WHERE project_id=? AND id=?", [project.id, input.runId]);
|
|
85
|
+
try {
|
|
86
|
+
const events = fs.readFileSync(path.join(run.run_root, "timeline.jsonl"), "utf8").trim().split("\n");
|
|
87
|
+
const proof = events.some(line => {
|
|
88
|
+
const event = JSON.parse(line);
|
|
89
|
+
if (event.type !== "native_child_reconciliation" || event.stage !== input.stage)
|
|
90
|
+
return false;
|
|
91
|
+
const terminal = event.observations?.children?.find((child) => child.session_id === b.provider_session_id && child.terminal_turn_id === b.terminal_turn_id);
|
|
92
|
+
if (event.observations?.parent_session_id !== b.parent_provider_session_id || event.observations?.harness_id !== b.harness_id
|
|
93
|
+
|| terminal?.status !== "failed" || terminal.terminal_observed_at !== input.receipt.terminal_observed_at
|
|
94
|
+
|| JSON.stringify(safeNativeFailure(terminal.provider_error)) !== JSON.stringify(safeNativeFailure(input.receipt.provider_error)))
|
|
95
|
+
return false;
|
|
96
|
+
return event.reconciled?.some((entry) => entry.outcome === "failed"
|
|
97
|
+
&& entry.native_binding && Object.keys(b).every(key => b[key] === entry.native_binding[key]));
|
|
98
|
+
});
|
|
99
|
+
return proof ? b : undefined;
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
34
104
|
}
|
|
35
105
|
export function fanoutState(input) {
|
|
36
106
|
if (!input.hasWork)
|
|
@@ -57,17 +127,64 @@ export function readFanoutDescriptor(stageRoot) {
|
|
|
57
127
|
}
|
|
58
128
|
return value;
|
|
59
129
|
}
|
|
130
|
+
/** Memory graphs are SQL-owned; a mutable/missing fanout.json is not their
|
|
131
|
+
* dispatch authority. Domain registrations and Work launch owners still fence
|
|
132
|
+
* productive assignments, including reviewer/correction waves.
|
|
133
|
+
*/
|
|
134
|
+
export function memoryStageFanoutDescriptor(context, input) {
|
|
135
|
+
if (!["compose", "review"].includes(input.stage))
|
|
136
|
+
return null;
|
|
137
|
+
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
138
|
+
const run = context.db.get("SELECT flow_kind,index_json FROM runs WHERE project_id=? AND id=?", [project.id, input.runId]);
|
|
139
|
+
if (run?.flow_kind !== "mb-init")
|
|
140
|
+
return null;
|
|
141
|
+
const bundle = readFlowRunExecutionBundle(context, input);
|
|
142
|
+
if (!bundle || bundle.descriptor.descriptor.id !== "mb-init")
|
|
143
|
+
throw new AppError("stage_graph_compatibility_required", "Memory graph requires its admitted RUN bundle", 1);
|
|
144
|
+
const index = JSON.parse(run.index_json);
|
|
145
|
+
if (index.current_stage !== input.stage || !index.stage_runs.some(stage => stage.stage === input.stage && stage.status === "running"))
|
|
146
|
+
return null;
|
|
147
|
+
const root = readDescriptorRootWork(context, { projectId: project.id, runId: input.runId }, bundle);
|
|
148
|
+
const scope = stageGraphScope(context, project.id, input.runId, input.stage), members = stageWorkIds(context, project.id, input.runId, input.stage);
|
|
149
|
+
if (root.status !== "running" || !members.has(root.workId))
|
|
150
|
+
throw new AppError("stage_graph_conflict", "Memory graph lost its active coordinator", 1);
|
|
151
|
+
for (const id of members) {
|
|
152
|
+
if (id === root.workId)
|
|
153
|
+
continue;
|
|
154
|
+
const work = context.db.get("SELECT parent_work_id,result_schema FROM works WHERE project_id=? AND run_id=? AND work_id=?", [project.id, input.runId, id]);
|
|
155
|
+
const schemas = input.stage === "compose" ? ["dd-flow/memory-composition-work-result@1"] : ["dd-flow/memory-review-work-result@1", "dd-flow/memory-repair-work-result@1"];
|
|
156
|
+
if (!work || work.parent_work_id !== root.workId || !schemas.includes(work.result_schema ?? ""))
|
|
157
|
+
throw new AppError("stage_graph_conflict", "Memory graph contains an unassigned Work", 1);
|
|
158
|
+
const custody = readWorkBatchCustody(context, id);
|
|
159
|
+
if (custody.scope.project_id !== scope.project_id || custody.scope.run_id !== scope.run_id || custody.scope.attempt !== scope.attempt
|
|
160
|
+
|| custody.scope.cycle !== scope.cycle || custody.scope.stage !== input.stage || custody.scope.parent_work_id !== root.workId)
|
|
161
|
+
throw new AppError("stage_graph_conflict", "Memory graph contains foreign allocation custody", 1);
|
|
162
|
+
}
|
|
163
|
+
if (input.stage === "compose") {
|
|
164
|
+
const children = context.db.all("SELECT work_id FROM works WHERE project_id=? AND run_id=? AND parent_work_id=?", [project.id, input.runId, root.workId]).filter(work => members.has(work.work_id));
|
|
165
|
+
const registered = context.db.get("SELECT 1 FROM audit_events WHERE project_id=? AND event_type='memory.composition_registration' AND json_extract(payload_json,'$.run_id')=? AND json_extract(payload_json,'$.scope.attempt')=? AND json_extract(payload_json,'$.scope.cycle')=? LIMIT 1", [project.id, input.runId, scope.attempt, scope.cycle]);
|
|
166
|
+
if (children.length || registered) {
|
|
167
|
+
const registration = selectMemoryCompositionRegistration(context, { ...scope, stage: "compose", parent_work_id: root.workId });
|
|
168
|
+
const ids = new Set(Object.values(registration.work_ids));
|
|
169
|
+
if (ids.size !== children.length || children.some(work => !ids.has(work.work_id)))
|
|
170
|
+
throw new AppError("stage_graph_conflict", "Memory composition differs from its registered graph", 1);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return { schema_id: "dd-flow/work-fanout@2", kind: "work_fanout", scope, stage: input.stage, parent_work_id: root.workId, dispatch: "none", capacity_required: true };
|
|
174
|
+
}
|
|
60
175
|
/** Read-only projection for an external orchestrator. dd-flow keeps the graph authority. */
|
|
61
176
|
export function getVnextFanoutStatus(context, input) {
|
|
62
177
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
63
178
|
const project = requireProjectByRoot(context, projectRoot);
|
|
64
|
-
const run = context.db.get("SELECT run_root FROM runs WHERE project_id = ? AND id = ?", [project.id, input.runId]);
|
|
179
|
+
const run = context.db.get("SELECT run_root, index_json FROM runs WHERE project_id = ? AND id = ?", [project.id, input.runId]);
|
|
65
180
|
if (!run)
|
|
66
181
|
throw new AppError("not_found", "RUN is not registered", 1, { run_id: input.runId });
|
|
67
182
|
if (!run.run_root)
|
|
68
183
|
throw new AppError("runtime_artifact_missing", "RUN artifact root is unavailable", 1, { run_id: input.runId });
|
|
69
|
-
const
|
|
70
|
-
|
|
184
|
+
const index = JSON.parse(run.index_json);
|
|
185
|
+
const bundle = index.execution_bundle ? readFlowRunExecutionBundle(context, { projectRoot, runId: input.runId }) : null;
|
|
186
|
+
const stageRoot = path.join(run.run_root, bundle ? bundle.descriptor.stage(input.stage).directory : vnextStageDirectory(input.stage));
|
|
187
|
+
let descriptor = memoryStageFanoutDescriptor(context, input) ?? readFanoutDescriptor(stageRoot);
|
|
71
188
|
// Registration is authoritative even if projection publication was interrupted,
|
|
72
189
|
// or an otherwise review-off stage acquired an aggregate repair graph.
|
|
73
190
|
if (!descriptor && context.db.get("SELECT 1 FROM sqlite_master WHERE name = 'repair_intents'")) {
|
|
@@ -177,12 +294,21 @@ export async function reconcileVnextFanout(context, input) {
|
|
|
177
294
|
continue;
|
|
178
295
|
}
|
|
179
296
|
const providerError = safeNativeFailure(child.provider_error);
|
|
297
|
+
const controller = nativeFailureController(context, project.id, input.runId, input.stage, value.parent_session_id);
|
|
298
|
+
const scope = stageGraphScope(context, project.id, input.runId, input.stage);
|
|
299
|
+
const terminalTime = typeof child.terminal_observed_at === "string" ? Date.parse(child.terminal_observed_at) : NaN;
|
|
300
|
+
const nativeBinding = child.status === "failed" && controller && typeof child.terminal_turn_id === "string" && child.terminal_turn_id.length > 0 && child.terminal_turn_id.length <= 256 && Number.isFinite(terminalTime)
|
|
301
|
+
? { schema_id: "dd-flow/native-work-failure@1", project_id: project.id, run_id: input.runId, stage: input.stage, stage_attempt_id: scope.attempt, stage_cycle: scope.cycle,
|
|
302
|
+
parent_work_id: status.orchestration.parent_work_id, work_id: link.work_id, work_session_id: link.id, harness_id: value.harness_id,
|
|
303
|
+
parent_provider_session_id: value.parent_session_id, provider_session_id: String(child.session_id), terminal_turn_id: child.terminal_turn_id,
|
|
304
|
+
controller_id: controller.controller_id, generation: controller.generation } : undefined;
|
|
180
305
|
const reason = JSON.stringify({ source: "native_child_observation", harness_id: value.harness_id, session_id: child.session_id, work_session_id: link.id, outcome: child.status,
|
|
306
|
+
...(nativeBinding ? { native_binding: nativeBinding } : {}),
|
|
181
307
|
...(typeof child.terminal_turn_id === "string" ? { terminal_turn_id: child.terminal_turn_id } : {}),
|
|
182
308
|
...(typeof child.terminal_observed_at === "string" ? { terminal_observed_at: child.terminal_observed_at } : {}),
|
|
183
309
|
...(providerError ? { provider_error: providerError } : {}) });
|
|
184
310
|
const result = await (child.status === "failed" ? failWork : cancelWork)(context, link.work_id, reason);
|
|
185
|
-
reconciled.push({ work_id: link.work_id, work_session_id: link.id, outcome: child.status, result });
|
|
311
|
+
reconciled.push({ work_id: link.work_id, work_session_id: link.id, outcome: child.status, ...(nativeBinding ? { native_binding: nativeBinding } : {}), result });
|
|
186
312
|
}
|
|
187
313
|
appendFlowRunTimelineEvent(context, project.id, input.runId, { type: "native_child_reconciliation", stage: input.stage, observations: value, issues, reconciled });
|
|
188
314
|
return { ok: true, issues, reconciled };
|
|
@@ -232,6 +232,7 @@ export function applyVnextMerge(context, input) {
|
|
|
232
232
|
throw new AppError("merge_apply_receipt_invalid", "Existing merge apply receipt does not match the active request", 1, { receipt_path: receiptFile });
|
|
233
233
|
return receipt;
|
|
234
234
|
}
|
|
235
|
+
requireBootstrapCommand(requireRun(context, request.project_id, request.run_id));
|
|
235
236
|
heartbeatLaneLock(context, { projectRoot: input.projectRoot, lane: "merge", workerId: request.executor_work_id, workspacePath: request.target_workspace, ttlSeconds: 7200 });
|
|
236
237
|
assertExecutionBaseline(request);
|
|
237
238
|
input.progress?.(`applying ${request.source_commit} to ${request.target_branch}`);
|
|
@@ -316,7 +317,7 @@ export async function finishVnextMerge(context, input, prepared = prepareVnextMe
|
|
|
316
317
|
if (failed.length) {
|
|
317
318
|
context.db.run("UPDATE merge_requests SET status = 'action_required', last_error_json = ?, updated_at = ? WHERE merge_request_id = ?", [JSON.stringify({ code: "merge_gate_failed", failures: failed }), context.now(), request.merge_request_id]);
|
|
318
319
|
const sourceRepair = failed.every(receipt => checkRepairDisposition(receipt) === "source_repair");
|
|
319
|
-
throw new AppError("merge_gate_failed", "Integrated target checks failed. Only
|
|
320
|
+
throw new AppError("merge_gate_failed", "Integrated target checks failed. Only candidate-source-backed failures permit source repair; tooling or unknown evidence requires diagnosis/settlement first.", 2, { failures: failed, ...(sourceRepair ? { repair_command: repairCommand(context, request, projectRoot), continuation: { ...recoveryEvidenceInput(run.run_root, failed[0].id), command_after_input: finishCommand(context, run.id, request, projectRoot, failed[0].id) } } : { outcome: "blocked", recoverable: false }) });
|
|
320
321
|
}
|
|
321
322
|
const passedRefs = new Set(receipts.flatMap((receipt) => receipt.check_refs));
|
|
322
323
|
const missingRefs = frozenGateRefs(gate).filter((ref) => !passedRefs.has(ref));
|
|
@@ -567,10 +568,14 @@ function protocolIds(home) { const root = path.join(home, "03-plan"); if (!fs.ex
|
|
|
567
568
|
function workspaceRoute(home) { const file = path.join(home, "02-protocolize", "workspace-route.json"); const value = JSON.parse(fs.readFileSync(file, "utf8")); if (!value.policy?.integration_branch)
|
|
568
569
|
throw new AppError("workspace_route_missing", "MERGE requires the frozen integration branch", 1, { file }); return { integration_branch: value.policy.integration_branch }; }
|
|
569
570
|
function executionSettings(run) { return JSON.parse(run.index_json).execution_profile?.settings ?? {}; }
|
|
570
|
-
|
|
571
|
+
function requireBootstrapCommand(run) {
|
|
571
572
|
const command = executionSettings(run).code_bootstrap?.command;
|
|
572
573
|
if (!command)
|
|
573
|
-
throw new AppError("execution_profile_invalid", "MERGE requires
|
|
574
|
+
throw new AppError("execution_profile_invalid", "MERGE requires an evidenced bootstrap command in the frozen RUN profile; configure code_bootstrap before starting a code-ready RUN", 2);
|
|
575
|
+
return command;
|
|
576
|
+
}
|
|
577
|
+
async function runBootstrap(context, run, cwd, mergeId, progress) {
|
|
578
|
+
const command = requireBootstrapCommand(run);
|
|
574
579
|
const result = await runWorkspaceBootstrap(context, { projectId: run.project_id, runId: run.id, runHome: requireHome(run), workspaceRoot: cwd, command, artifactDir: `${stageDir}/${mergeId}/readiness`, progress });
|
|
575
580
|
if (result.exit_code !== 0 || result.error)
|
|
576
581
|
throw new AppError("merge_bootstrap_failed", "Integrated target bootstrap failed; inspect its retained logs", 2, { command, ...result });
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { managedLifecycleCommand } from "./lifecycle-invocations.js";
|
|
2
2
|
import crypto from "node:crypto";
|
|
3
|
-
import { withStageSettlement, prepareFlowRunStageAttachment, prepareFlowRunStageCompletion } from "./runs.js";
|
|
3
|
+
import { withStageSettlement, prepareFlowRunStageAttachment, prepareFlowRunStageCompletion, readFlowRunExecutionBundle } from "./runs.js";
|
|
4
4
|
import fs from "node:fs";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { AppError } from "../shared/errors.js";
|
|
@@ -132,6 +132,7 @@ export function startVnextPlanReview(context, input, prepared = prepareVnextPlan
|
|
|
132
132
|
commitVerificationMatrixStart(context, prepared.matrix, () => attachFlowRunStage(context, attachmentInput, attachment));
|
|
133
133
|
const reportGit = gitFacts(run.workspace_root);
|
|
134
134
|
const next = codeSuccessor(context, run.id, projectRoot);
|
|
135
|
+
const completionBundle = readFlowRunExecutionBundle(context, { projectRoot, runId: run.id });
|
|
135
136
|
return withStageSettlement(context, () => {
|
|
136
137
|
const registered = addWorkBatch(context, { parentWorkId: rootWork, prepared: prepared.codeItems, stage: "code" });
|
|
137
138
|
const report = reportFor(context, { run, mode: effective, requested, outcome: "off", groups: [], batchChecksum, planChecksum, code: registered, now, projectRoot, flow: flowCommand(context), git: reportGit, ...(next.command ? { nextCommand: next.command } : {}) });
|
|
@@ -141,7 +142,7 @@ export function startVnextPlanReview(context, input, prepared = prepareVnextPlan
|
|
|
141
142
|
const preparedReport = prepareStageReport(report);
|
|
142
143
|
acceptStageReport(context, { projectId: project.id, runId: run.id, stage, root, prepared: preparedReport });
|
|
143
144
|
const completionInput = { projectRoot, runId: run.id, stage, status: "done", data: "stage-report.json", dataSchemaId: "dd-flow/stage-report@1", report: "stage-report.md", stageReport: "stage-report.html", dataSha256: preparedReport.sha256, git: reportGit };
|
|
144
|
-
completeFlowRunStage(context, completionInput, prepareFlowRunStageCompletion(context, completionInput));
|
|
145
|
+
completeFlowRunStage(context, completionInput, prepareFlowRunStageCompletion(context, completionInput, completionBundle));
|
|
145
146
|
advanceFlowRun(context, { settlementStage: "plan-review", projectRoot, runId: run.id, status: "running", verdict: "plan_review_off", nextAction: "start_code" });
|
|
146
147
|
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "plan_review_opened", mode: effective, outcome: "off" });
|
|
147
148
|
return { ok: true, run_id: run.id, stage, outcome: "review_off", ...(externalContext ? { external_context: externalContext } : {}), plan_review: { requested_mode: requested, effective_mode: effective, policy_source: policySource(run), reason: "No applicable independent review group." }, next };
|
|
@@ -209,7 +210,7 @@ export function dispatchVnextPlanReview(context, input) {
|
|
|
209
210
|
}
|
|
210
211
|
const created = context.db.all("SELECT work_id, task, status, launch_policy, created_at FROM works WHERE project_id = ? AND run_id = ? AND parent_work_id = ?", [project.id, run.id, parent.work_id]);
|
|
211
212
|
const reviews = groups.map((group) => { const work = created.find((item) => item.task === group.task); if (!work)
|
|
212
|
-
throw new AppError("runtime_missing", "Reviewer Work was not created", 1, { group: group.key }); const ready = work.status === "created" && isReviewWorkReady(context, work.work_id); return { group: group.key, work_id: work.work_id, status: work.status, depends_on: group.depends_on, ready, ...(ready ? { start_command: workStartCommand(context, { ...work, project_id: project.id, run_id: run.id }) } : {}) }; });
|
|
213
|
+
throw new AppError("runtime_missing", "Reviewer Work was not created", 1, { group: group.key }); const ready = work.status === "created" && isReviewWorkReady(context, work.work_id); return { group: group.key, work_id: work.work_id, status: work.status, depends_on: group.depends_on, ready, ...(ready ? { start_command: workStartCommand(context, { ...work, project_id: project.id, run_id: run.id }, { preview: true }) } : {}) }; });
|
|
213
214
|
refreshRunWorkProjection(context, project.id, run.id);
|
|
214
215
|
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "plan_review_dispatched", work_id: parent.work_id, groups: reviews.map((item) => ({ group: item.group, work_id: item.work_id })) });
|
|
215
216
|
const waves = plannedReviewWaves(groups, capacity.availableSlots);
|
|
@@ -329,6 +330,7 @@ function finishPlanReviewOwned(context, input) {
|
|
|
329
330
|
const reportGit = gitFacts(run.workspace_root);
|
|
330
331
|
const next = codeSuccessor(context, run.id, projectRoot);
|
|
331
332
|
const matrix = prepareRunVerificationMatrix(context, { projectId: project.id, runId: run.id, stage, role: "output", semanticFile: decisionPath });
|
|
333
|
+
const completionBundle = readFlowRunExecutionBundle(context, { projectRoot, runId: run.id });
|
|
332
334
|
return withStageSettlement(context, () => {
|
|
333
335
|
const registered = addWorkBatch(context, { parentWorkId: codeParentId, prepared: codeItems, stage: "code" });
|
|
334
336
|
const now = context.now();
|
|
@@ -343,7 +345,7 @@ function finishPlanReviewOwned(context, input) {
|
|
|
343
345
|
acceptStagePacket(context, { projectId: project.id, runId: run.id, stage, prepared: decisionReceipt });
|
|
344
346
|
acceptStageReport(context, { projectId: project.id, runId: run.id, stage, root, prepared: preparedReport });
|
|
345
347
|
const completionInput = { projectRoot, runId: run.id, stage, status: "done", data: "stage-report.json", dataSchemaId: "dd-flow/stage-report@1", report: "stage-report.md", stageReport: "stage-report.html", dataSha256: preparedReport.sha256, git: reportGit };
|
|
346
|
-
completeFlowRunStage(context, completionInput, prepareFlowRunStageCompletion(context, completionInput));
|
|
348
|
+
completeFlowRunStage(context, completionInput, prepareFlowRunStageCompletion(context, completionInput, completionBundle));
|
|
347
349
|
refreshRunWorkProjection(context, project.id, run.id);
|
|
348
350
|
advanceFlowRun(context, { settlementStage: "plan-review", projectRoot, runId: run.id, status: "running", verdict: "plan_review_accepted", nextAction: "start_code" });
|
|
349
351
|
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "plan_review_accepted", work_id: parent.work_id, code: registered });
|
|
@@ -4,6 +4,7 @@ import crypto from "node:crypto";
|
|
|
4
4
|
import fs from "node:fs";
|
|
5
5
|
import os from "node:os";
|
|
6
6
|
import path from "node:path";
|
|
7
|
+
import { parse as parseYaml } from "yaml";
|
|
7
8
|
import { AppError } from "../shared/errors.js";
|
|
8
9
|
import { projectFeatureWorktreePath, resolveProjectRoot } from "../storage/paths.js";
|
|
9
10
|
import { previewNextEntityId } from "./ids.js";
|
|
@@ -189,6 +190,29 @@ export async function finishVnextProtocolize(context, input) {
|
|
|
189
190
|
const resultFile = prepareProtocolizeResultPath(requireRunHome(run), input.resultFile);
|
|
190
191
|
const result = input.result ?? readResult(resultFile, { projectRoot, ddFlowHome: context.ddFlowHome, runId: run.id, runRoot: requireRunHome(run) });
|
|
191
192
|
validateSchema({ schemaName: "vnext-protocolize-result", file: resultFile, data: result, projectRoot, ddFlowHome: context.ddFlowHome, runId: run.id, runRoot: requireRunHome(run) });
|
|
193
|
+
const acceptedIndex = JSON.parse(context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [project.id, run.id]).index_json);
|
|
194
|
+
if (acceptedIndex.stage_runs.some(item => item.stage === stage && item.status === "done")) {
|
|
195
|
+
publishStageReport(context, project.id, run.id, stage, root);
|
|
196
|
+
const file = path.join(root, "stage-report.json");
|
|
197
|
+
if (!fs.existsSync(file))
|
|
198
|
+
throw new AppError("protocolize_report_missing", "Completed PROTOCOLIZE has no accepted report to replay", 1, { run_id: run.id });
|
|
199
|
+
const accepted = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
200
|
+
const authoredSha = accepted.semantic.materialization?.authored_result?.sha256;
|
|
201
|
+
if (authoredSha && authoredSha !== sha256(fs.readFileSync(resultFile)))
|
|
202
|
+
throw new AppError("protocolize_intent_conflict", "Completed PROTOCOLIZE result differs from accepted input", 1, { run_id: run.id });
|
|
203
|
+
const currentStage = acceptedIndex.current_stage ?? [...acceptedIndex.stage_runs].reverse().find(item => item.status === "running" || item.status === "done")?.stage;
|
|
204
|
+
const activeReplay = currentStage === stage && acceptedIndex.status === "running";
|
|
205
|
+
const resolvedPolicy = activeReplay ? requireVnextWorkspaceRoute({ projectRoot, runId: run.id, runHome: requireRunHome(run), workspaceRoot: run.workspace_root, stage: "protocolize" }) : accepted.mechanical?.workspace_route;
|
|
206
|
+
if (activeReplay) {
|
|
207
|
+
if (acceptedIndex.verdict !== "protocolized")
|
|
208
|
+
advanceFlowRun(context, { settlementStage: "protocolize", projectRoot, runId: run.id, status: "running", verdict: "protocolized", nextAction: "start_plan" });
|
|
209
|
+
refreshRunWorkProjection(context, project.id, run.id);
|
|
210
|
+
}
|
|
211
|
+
const next = activeReplay ? stageSuccessor(context, { projectRoot, runId: run.id, stage: "plan", command: `${flowCommand(context)} stage start ${run.id} --stage plan --project-root ${JSON.stringify(projectRoot)} --json`, metadata: { cwd: run.workspace_root, session_mode: resolvedPolicy.stage_session_mode } }) : { kind: "complete", outcome: "already_completed" };
|
|
212
|
+
return { ok: true, outcome: "protocolized", run_id: run.id, protocol_ids: accepted.semantic.acceptance, workspace_root: run.workspace_root, git_policy: resolvedPolicy, next_action: activeReplay ? "await_plan" : acceptedIndex.next_action, ...("command" in next && next.command ? { next_command: next.command } : {}), next,
|
|
213
|
+
artifacts: { result: resultFile, report_json: file, report_markdown: path.join(root, "stage-report.md"), report_html: path.join(root, "stage-report.html"), protocols: accepted.semantic.acceptance.map(id => path.join(run.workspace_root, ".memory-bank/protocol", id, "summary.md")) } };
|
|
214
|
+
}
|
|
215
|
+
const resolvedPolicy = requireVnextWorkspaceRoute({ projectRoot, runId: run.id, runHome: requireRunHome(run), workspaceRoot: run.workspace_root, stage: "protocolize" });
|
|
192
216
|
const acceptedObligations = acceptedSpecifyObligations(context, projectRoot, run.id, path.join(requireRunHome(run), "01-specify", "specify.json"));
|
|
193
217
|
let obligations;
|
|
194
218
|
try {
|
|
@@ -202,11 +226,11 @@ export async function finishVnextProtocolize(context, input) {
|
|
|
202
226
|
throw error;
|
|
203
227
|
throw new AppError(error.code, error.message, error.exitCode, { ...error.details, phase: "prepare", effect: "no_effect", recoverable: true });
|
|
204
228
|
}
|
|
205
|
-
const resolvedPolicy = requireVnextWorkspaceRoute({ projectRoot, runId: run.id, runHome: requireRunHome(run), workspaceRoot: run.workspace_root, stage: "protocolize" });
|
|
206
229
|
const receipt = readWorkspaceRouteReceipt(root);
|
|
207
230
|
const workspaceRoot = run.workspace_root;
|
|
208
231
|
const intent = await prepareProtocolizeIntent(context, run, resultFile, result, obligations);
|
|
209
232
|
publishProtocolizeIntent(context, project.id, intent);
|
|
233
|
+
const materialization = verifyProtocolizeMaterialization(intent, result, path.relative(requireRunHome(run), resultFile));
|
|
210
234
|
const protocolIds = intent.protocol_ids;
|
|
211
235
|
const changedFiles = intent.changed_files;
|
|
212
236
|
for (const protocolId of protocolIds)
|
|
@@ -216,9 +240,10 @@ export async function finishVnextProtocolize(context, input) {
|
|
|
216
240
|
ensureManagedWorktreeRecorded(context, { protocolId, projectRoot, branch: requiredPolicyValue(resolvedPolicy.feature_branch, "feature branch"), base: requiredPolicyValue(resolvedPolicy.base_ref, "base ref"), created: receipt.created, bootstrap: receipt.bootstrap });
|
|
217
241
|
}
|
|
218
242
|
const now = context.now();
|
|
219
|
-
const workSession =
|
|
243
|
+
const workSession = context.db.get("SELECT * FROM work_sessions WHERE work_id = ? AND status = 'running' ORDER BY created_at DESC LIMIT 1", [work.work_id]);
|
|
220
244
|
const routedRun = requireRun(context, projectRoot, run.id);
|
|
221
245
|
const report = stageReport(context, routedRun, resultFile, protocolIds, changedFiles, workSession?.session_id ?? null, now, resolvedPolicy);
|
|
246
|
+
Object.assign(report.semantic, { materialization });
|
|
222
247
|
const reportJson = path.join(root, "stage-report.json");
|
|
223
248
|
const reportMarkdown = path.join(root, "stage-report.md");
|
|
224
249
|
const reportHtml = path.join(root, "stage-report.html");
|
|
@@ -235,6 +260,7 @@ export async function finishVnextProtocolize(context, input) {
|
|
|
235
260
|
const completion = { projectRoot, runId: run.id, stage, status: "done", data: "protocolize-result.json", dataSchemaId: "dd-flow/vnext-protocolize-result@3", report: "stage-report.md", stageReport: "stage-report.html", dataSha256: crypto.createHash("sha256").update(fs.readFileSync(resultFile)).digest("hex") };
|
|
236
261
|
const preparedCompletion = prepareFlowRunStageCompletion(context, completion);
|
|
237
262
|
withStageSettlement(context, () => {
|
|
263
|
+
completeWorkSession(context, work.work_id, resultFile, now);
|
|
238
264
|
acceptStageReport(context, { projectId: project.id, runId: run.id, stage, root, prepared: preparedReport });
|
|
239
265
|
completeFlowRunStage(context, completion, preparedCompletion);
|
|
240
266
|
});
|
|
@@ -367,6 +393,38 @@ function ensureManagedWorktreeRecorded(context, input) {
|
|
|
367
393
|
recordManagedWorktree(context, input);
|
|
368
394
|
}
|
|
369
395
|
function sha256(bytes) { return crypto.createHash("sha256").update(bytes).digest("hex"); }
|
|
396
|
+
export function verifyProtocolizeMaterialization(intent, result, authoredPath) {
|
|
397
|
+
const documents = intent.files.map(file => {
|
|
398
|
+
const target = path.resolve(intent.workspace_root, file.relative);
|
|
399
|
+
inside(path.join(intent.workspace_root, ".memory-bank"), target);
|
|
400
|
+
assertNoSymlinkPath(path.join(intent.workspace_root, ".memory-bank"), target);
|
|
401
|
+
const bytes = fs.readFileSync(target);
|
|
402
|
+
if (sha256(bytes) !== file.after_sha256)
|
|
403
|
+
throw new AppError("protocolize_materialization_invalid", "Published document differs from accepted intent", 1, { path: file.relative });
|
|
404
|
+
return { path: file.relative, sha256: file.after_sha256 };
|
|
405
|
+
});
|
|
406
|
+
const featurePaths = intent.changed_files.filter(file => /\/features\/FT-[^/]+\/index\.md$/.test(file));
|
|
407
|
+
const featurePath = result.feature.action === "not_applicable" ? null : featurePaths.length === 1 ? featurePaths[0] : null;
|
|
408
|
+
if (result.feature.action !== "not_applicable" && !featurePath)
|
|
409
|
+
throw new AppError("protocolize_materialization_invalid", "Accepted feature output is ambiguous or missing", 1);
|
|
410
|
+
const frontmatter = (relative) => {
|
|
411
|
+
const text = fs.readFileSync(path.join(intent.workspace_root, relative), "utf8");
|
|
412
|
+
const match = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(text);
|
|
413
|
+
const value = match ? parseYaml(match[1]) : null;
|
|
414
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
415
|
+
throw new AppError("protocolize_materialization_invalid", "Published document lacks frontmatter", 1, { path: relative });
|
|
416
|
+
return value;
|
|
417
|
+
};
|
|
418
|
+
const edges = featurePath ? intent.protocol_ids.map(protocolId => ({ feature_path: featurePath, protocol_id: protocolId, protocol_path: `.memory-bank/protocol/${protocolId}/summary.md` })) : [];
|
|
419
|
+
for (const edge of edges) {
|
|
420
|
+
const protocols = frontmatter(edge.feature_path).related_protocols;
|
|
421
|
+
const features = frontmatter(edge.protocol_path).related_features;
|
|
422
|
+
if (!Array.isArray(protocols) || !protocols.includes(edge.protocol_id) || !Array.isArray(features) || !features.includes(edge.feature_path))
|
|
423
|
+
throw new AppError("protocolize_materialization_invalid", "Owned feature/protocol links are not reciprocal", 1, edge);
|
|
424
|
+
}
|
|
425
|
+
return { schema_id: "dd-flow/protocolize-materialization@1", run_id: intent.run_id, stage: "protocolize", action: result.feature.action, feature_path: featurePath,
|
|
426
|
+
protocol_ids: intent.protocol_ids, authored_result: { role: "authored_input", run_relative_path: authoredPath, sha256: intent.result_sha256 }, documents, owned_links: edges, status: "passed" };
|
|
427
|
+
}
|
|
370
428
|
async function materialize(projectRoot, runId, runHome, result, obligations, ids, psetId) {
|
|
371
429
|
const psetPath = psetId ? path.join(projectRoot, ".memory-bank", "protocol", "_set", `${psetId}.md`) : null;
|
|
372
430
|
const created = [];
|