@deksden-com/dd-flow-cli 0.9.0-beta.15 → 0.9.0-beta.17
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 +18 -0
- package/dist/build-info.json +4 -4
- package/dist/cli/help.js +2 -1
- package/dist/cli/run-cli.js +16 -9
- package/dist/schemas/code-work-batch.schema.json +1 -1
- package/dist/services/code-checks.js +33 -3
- package/dist/services/eval-snapshots.js +23 -10
- package/dist/services/hooks.js +5 -1
- package/dist/services/runs.js +69 -8
- package/dist/services/vnext-code-review.js +9 -4
- package/dist/services/vnext-code.js +22 -11
- package/dist/services/vnext-fanout.js +68 -3
- package/dist/services/vnext-merge.js +24 -8
- package/dist/services/work-registry.js +8 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# @deksden-com/dd-flow-cli
|
|
2
2
|
|
|
3
|
+
## 0.9.0-beta.17
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 0dd4da3: Verify current-attempt gate retries, deduplicate equivalent checks across gate references, retain archive locations across interrupted and repeated repairs, and align MERGE environment recovery guidance with source repair.
|
|
8
|
+
|
|
9
|
+
Snapshot SQLite consistently with VACUUM INTO and exclude local environment secrets, dependencies and caches from project/workspace snapshots.
|
|
10
|
+
|
|
11
|
+
Reconcile native child failures only against unique registered Work/Session attempts, retain non-restorable incomplete snapshots for failed evals, and expose unexpected merge-lane release failures for safe retry.
|
|
12
|
+
|
|
13
|
+
## 0.9.0-beta.16
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- Make repair verification causal and retryable, preserve attempt-qualified
|
|
18
|
+
evidence, expose RUN continuation from persisted lifecycle state, and keep
|
|
19
|
+
native child ownership bound to the physical harness session.
|
|
20
|
+
|
|
3
21
|
## 0.9.0-beta.15
|
|
4
22
|
|
|
5
23
|
### Patch Changes
|
package/dist/build-info.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"cli_package": "@deksden-com/dd-flow-cli",
|
|
3
|
-
"cli_version": "0.9.0-beta.
|
|
4
|
-
"cli_commit": "
|
|
5
|
-
"built_at": "2026-09-
|
|
3
|
+
"cli_version": "0.9.0-beta.17",
|
|
4
|
+
"cli_commit": "162dd994d19c0d454a2ad6b7b62abd70c3e848d6",
|
|
5
|
+
"built_at": "2026-09-06T00:16:19.328Z",
|
|
6
6
|
"built_with_canon": {
|
|
7
7
|
"version": "4.0.4",
|
|
8
|
-
"commit": "
|
|
8
|
+
"commit": "ced59c19d0a4062146a1dc4ccb977e10284a2fe4",
|
|
9
9
|
"flow_contract": "dd-flow-canonical-2026-08",
|
|
10
10
|
"repo_root": "/Users/deksden/Documents/_Projects/dd-memorybank",
|
|
11
11
|
"memorybank_root": "/Users/deksden/Documents/_Projects/dd-memorybank/.memory-bank",
|
package/dist/cli/help.js
CHANGED
|
@@ -154,7 +154,7 @@ Usage:
|
|
|
154
154
|
dd-flow run status <RUN-ID|RUN-short-id> --project-root <root> --json
|
|
155
155
|
dd-flow run list --project-root <root> --json
|
|
156
156
|
dd-flow run prepare-vnext-specify --project-root <root> --slug <slug> --json
|
|
157
|
-
dd-flow run snapshot create <RUN-ID|RUN-short-id> (--stage-entry <stage>|--candidate) --project-root <root> --output <directory> --json
|
|
157
|
+
dd-flow run snapshot create <RUN-ID|RUN-short-id> (--stage-entry <stage>|--candidate|--incomplete) --project-root <root> --output <directory> --json
|
|
158
158
|
dd-flow run snapshot restore --snapshot <directory> --project-root <prepared-root> --json
|
|
159
159
|
dd-flow run snapshot bootstrap create --project-root <root> --output <directory> --json
|
|
160
160
|
dd-flow run snapshot bootstrap restore --snapshot <directory> --project-root <prepared-root> --json
|
|
@@ -190,6 +190,7 @@ Usage:
|
|
|
190
190
|
dd-flow stage unblock <RUN-ID|RUN-short-id> --project-root <root> --stage <name> --work <WORK-ID> --json
|
|
191
191
|
dd-flow stage fanout status <RUN-ID|RUN-short-id> --project-root <root> --stage <name> --json
|
|
192
192
|
dd-flow stage fanout dispatch <RUN-ID|RUN-short-id> --project-root <root> --stage <name> --json
|
|
193
|
+
dd-flow stage fanout reconcile <RUN-ID> --project-root <root> --stage <name> --observations-file <native-observations.json> --json
|
|
193
194
|
dd-flow stage finish <RUN-ID|RUN-short-id> --project-root <root> --stage <name> [--semantic-file <RUN-local-json>] --json
|
|
194
195
|
dd-flow stage finish <RUN-ID|RUN-short-id> --project-root <root> --stage specify (--result-file <RUN-local-specify.json>|--result-stdin) --outcome <specified|failed|cancelled> --json
|
|
195
196
|
|
package/dist/cli/run-cli.js
CHANGED
|
@@ -41,7 +41,7 @@ import { addVnextCodeRepair, finishVnextCode, startVnextCode } from "../services
|
|
|
41
41
|
import { addVnextCodeReviewRepair, finishVnextCodeReview, isVnextCodeReviewRun, startVnextCodeReview } from "../services/vnext-code-review.js";
|
|
42
42
|
import { applyVnextMerge, finishVnextMerge, getVnextMergeRequest, isVnextMergeRun, repairVnextMerge, routeVnextMergeRequest, startVnextMerge } from "../services/vnext-merge.js";
|
|
43
43
|
import { mergeServerStatus, serveMergeRequests, stopMergeServer } from "../services/merge-server.js";
|
|
44
|
-
import { getVnextFanoutStatus } from "../services/vnext-fanout.js";
|
|
44
|
+
import { getVnextFanoutStatus, reconcileVnextFanout } from "../services/vnext-fanout.js";
|
|
45
45
|
import { pauseStageForUser, resumeStageAfterUser } from "../services/stage-pause.js";
|
|
46
46
|
import { blockStageForRuntime, unblockStageAfterRuntimeRepair } from "../services/stage-blocker.js";
|
|
47
47
|
import { classifyCliOperation, requiresExplicitUpgradeAuthorization } from "../services/cli-operation-classifier.js";
|
|
@@ -661,12 +661,14 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
|
|
|
661
661
|
const projectRoot = requiredScopeProjectRoot(scopeProjectRoot, "stage fanout");
|
|
662
662
|
if (action === "status")
|
|
663
663
|
return getVnextFanoutStatus(context, { projectRoot, runId, stage });
|
|
664
|
+
if (action === "reconcile")
|
|
665
|
+
return reconcileVnextFanout(context, { projectRoot, runId, stage, observations: JSON.parse(fs.readFileSync(requiredOption(parsed, "observations-file"), "utf8")) });
|
|
664
666
|
if (action === "dispatch") {
|
|
665
667
|
if (stage === "plan-review")
|
|
666
668
|
return dispatchVnextPlanReview(context, { projectRoot, runId });
|
|
667
669
|
return getVnextFanoutStatus(context, { projectRoot, runId, stage });
|
|
668
670
|
}
|
|
669
|
-
throw new AppError("usage", "Use: dd-flow stage fanout <status|dispatch> <RUN> --stage <stage> --project-root <path>", 2);
|
|
671
|
+
throw new AppError("usage", "Use: dd-flow stage fanout <status|dispatch|reconcile> <RUN> --stage <stage> --project-root <path>; reconcile requires --observations-file <native-observations.json>", 2);
|
|
670
672
|
}
|
|
671
673
|
if (family === "run" && command === "capacity") {
|
|
672
674
|
if (requiredPosition(parsed, 0, "capacity action") !== "record")
|
|
@@ -878,6 +880,10 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
|
|
|
878
880
|
const semanticFile = optionalOption(parsed, "semantic-file") ?? optionalOption(parsed, "data");
|
|
879
881
|
const resultFile = optionalOption(parsed, "result-file") ?? semanticFile;
|
|
880
882
|
const resultStdin = hasOption(parsed, "result-stdin");
|
|
883
|
+
const retryCheckId = optionalOption(parsed, "retry-check");
|
|
884
|
+
const retryReason = optionalOption(parsed, "reason");
|
|
885
|
+
if (retryCheckId && !retryReason?.trim())
|
|
886
|
+
throw new AppError("usage", "--retry-check requires --reason explaining the recovered environment", 2);
|
|
881
887
|
if (stage === "specify" && isVnextSpecifyRun(context, { projectRoot, runId })) {
|
|
882
888
|
const outcome = optionalOption(parsed, "outcome");
|
|
883
889
|
if (!outcome || !["specified", "failed", "cancelled"].includes(outcome)) {
|
|
@@ -917,17 +923,17 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
|
|
|
917
923
|
if (semanticFile || resultFile || resultStdin || hasOption(parsed, "outcome")) {
|
|
918
924
|
throw new AppError("usage", "vNext CODE finish accepts only --verification-file", 2);
|
|
919
925
|
}
|
|
920
|
-
return await finishVnextCode(context, { projectRoot, runId, verificationFile: optionalOption(parsed, "verification-file") ?? path.join("<missing>", "code-verification.json"), progress: commandProgress(io, output) });
|
|
926
|
+
return await finishVnextCode(context, { projectRoot, runId, verificationFile: optionalOption(parsed, "verification-file") ?? path.join("<missing>", "code-verification.json"), ...(retryCheckId ? { retryCheckId, retryReason: retryReason } : {}), progress: commandProgress(io, output) });
|
|
921
927
|
}
|
|
922
928
|
if (stage === "code-review" && isVnextCodeReviewRun(context, { projectRoot, runId })) {
|
|
923
929
|
if (resultStdin || semanticFile || resultFile || hasOption(parsed, "outcome"))
|
|
924
930
|
throw new AppError("usage", "vNext CODE-REVIEW uses --decision-file", 2);
|
|
925
|
-
return await finishVnextCodeReview(context, { projectRoot, runId, ...(optionalOption(parsed, "decision-file") ? { decisionFile: optionalOption(parsed, "decision-file") } : {}), progress: commandProgress(io, output) });
|
|
931
|
+
return await finishVnextCodeReview(context, { projectRoot, runId, ...(optionalOption(parsed, "decision-file") ? { decisionFile: optionalOption(parsed, "decision-file") } : {}), ...(retryCheckId ? { retryCheckId, retryReason: retryReason } : {}), progress: commandProgress(io, output) });
|
|
926
932
|
}
|
|
927
933
|
if (stage === "merge" && isVnextMergeRun(context, { projectRoot, runId })) {
|
|
928
934
|
if (resultStdin || semanticFile || resultFile || hasOption(parsed, "outcome"))
|
|
929
935
|
throw new AppError("usage", "vNext MERGE uses the Work result path returned by stage start", 2);
|
|
930
|
-
return await finishVnextMerge(context, { projectRoot, runId, requestId: requiredOption(parsed, "request"), workId: requiredOption(parsed, "work"), progress: commandProgress(io, output) });
|
|
936
|
+
return await finishVnextMerge(context, { projectRoot, runId, requestId: requiredOption(parsed, "request"), workId: requiredOption(parsed, "work"), ...(retryCheckId ? { retryCheckId, retryReason: retryReason } : {}), progress: commandProgress(io, output) });
|
|
931
937
|
}
|
|
932
938
|
if (hasOption(parsed, "outcome") || hasOption(parsed, "result-file") || resultStdin) {
|
|
933
939
|
throw new AppError("usage", "--outcome, --result-file and --result-stdin are only available for vNext SPECIFY", 2);
|
|
@@ -1431,16 +1437,17 @@ function dispatchRun(context, command, parsed) {
|
|
|
1431
1437
|
}
|
|
1432
1438
|
if (action === "create") {
|
|
1433
1439
|
const candidate = parsed.options.has("candidate");
|
|
1440
|
+
const incomplete = parsed.options.has("incomplete");
|
|
1434
1441
|
const stageEntry = optionalOption(parsed, "stage-entry");
|
|
1435
|
-
if (candidate
|
|
1436
|
-
throw new AppError("usage", "Use
|
|
1437
|
-
if (!candidate && stageEntry === undefined)
|
|
1438
|
-
throw new AppError("usage", "Snapshot create requires --candidate or --stage-entry", 2);
|
|
1442
|
+
if (Number(candidate) + Number(incomplete) + Number(stageEntry !== undefined) !== 1)
|
|
1443
|
+
throw new AppError("usage", "Use exactly one of --candidate, --incomplete or --stage-entry", 2);
|
|
1439
1444
|
const input = {
|
|
1440
1445
|
projectRoot: requiredOption(parsed, "project-root"),
|
|
1441
1446
|
runId: requiredPosition(parsed, 1, "run-id"),
|
|
1442
1447
|
output: requiredOption(parsed, "output")
|
|
1443
1448
|
};
|
|
1449
|
+
if (incomplete)
|
|
1450
|
+
return createEvalRunSnapshot(context, { ...input, incomplete: true });
|
|
1444
1451
|
return candidate
|
|
1445
1452
|
? createEvalRunSnapshot(context, { ...input, candidate: true })
|
|
1446
1453
|
: createEvalRunSnapshot(context, { ...input, stageEntry: stageEntry });
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"check": {"type": "object", "additionalProperties": false, "required": ["id", "command", "purpose", "run_at", "availability"], "properties": {"id": {"type": "string", "pattern": "^CHK-[A-Za-z0-9-]+$"}, "command": {"type": "string", "minLength": 1}, "purpose": {"type": "string", "minLength": 1}, "run_at": {"enum": ["work", "code", "readiness", "merge", "release", "external"]}, "availability": {"enum": ["available", "planned"]}, "provided_by": {"type": "string", "minLength": 1}, "definition": {"type": "string", "minLength": 1}, "required_artifacts": {"type": "array", "uniqueItems": true, "items": {"type": "string", "minLength": 1}}, "ports": {"type": "array", "uniqueItems": true, "items": {"type": "string", "pattern": "^[A-Za-z][A-Za-z0-9_]*$"}}}, "allOf": [{"if": {"properties": {"availability": {"const": "planned"}}, "required": ["availability"]}, "then": {"required": ["provided_by", "definition"], "properties": {"command": {"pattern": "^@check/"}}}}]},
|
|
20
20
|
"documentUpdate": {"type": "object", "additionalProperties": false, "required": ["path", "action", "owner", "reason", "baseline_sha256"], "properties": {"path": {"type": "string", "minLength": 1}, "action": {"enum": ["create", "update"]}, "owner": {"type": "string", "minLength": 1}, "reason": {"type": "string", "minLength": 1}, "baseline_sha256": {"type": ["string", "null"], "pattern": "^[a-f0-9]{64}$"}}},
|
|
21
21
|
"reviewFinding": {"type": "object", "additionalProperties": false, "required": ["finding_ref", "priority", "problem", "impact", "required_outcome", "evidence_refs", "obligation_refs", "decision_reason", "check_refs"], "properties": {"finding_ref": {"type": "string", "minLength": 1}, "priority": {"enum": ["p0", "p1", "p2", "p3"]}, "problem": {"type": "string", "minLength": 1}, "impact": {"type": "string", "minLength": 1}, "required_outcome": {"type": "string", "minLength": 1}, "evidence_refs": {"$ref": "#/$defs/strings"}, "obligation_refs": {"$ref": "#/$defs/strings"}, "decision_reason": {"type": "string", "minLength": 1}, "check_refs": {"$ref": "#/$defs/strings"}}},
|
|
22
|
-
"repair": {"type": "object", "additionalProperties": false, "required": ["origin_work_ids"], "anyOf": [{"required": ["check_receipt_id", "failure_receipt_path"]}, {"required": ["review_findings"]}, {"required": ["semantic_unresolved", "verification_path"]}], "properties": {"origin_work_ids": {"$ref": "#/$defs/strings"}, "check_receipt_id": {"type": "string", "minLength": 1}, "failure_receipt_path": {"type": "string", "minLength": 1}, "review_findings": {"type": "array", "minItems": 1, "items": {"$ref": "#/$defs/reviewFinding"}}, "semantic_unresolved": {"$ref": "#/$defs/strings"}, "verification_path": {"type": "string", "minLength": 1}}},
|
|
22
|
+
"repair": {"type": "object", "additionalProperties": false, "required": ["origin_work_ids", "verification_check_refs"], "anyOf": [{"required": ["check_receipt_id", "failure_receipt_path"]}, {"required": ["review_findings"]}, {"required": ["semantic_unresolved", "verification_path"]}], "properties": {"origin_work_ids": {"$ref": "#/$defs/strings"}, "check_receipt_id": {"type": "string", "minLength": 1}, "failure_receipt_path": {"type": "string", "minLength": 1}, "review_findings": {"type": "array", "minItems": 1, "items": {"$ref": "#/$defs/reviewFinding"}}, "semantic_unresolved": {"$ref": "#/$defs/strings"}, "verification_path": {"type": "string", "minLength": 1}, "verification_check_refs": {"type": "array", "minItems": 1, "items": {"$ref": "#/$defs/check"}}}},
|
|
23
23
|
"work": {"type": "object", "additionalProperties": false, "required": ["schema_id", "key", "launch_policy", "source", "task", "semantic_spine", "requirements", "acceptance", "document_updates", "required_read", "discovery_boundary", "planned_write_areas", "checks", "provides_checks", "stop_conditions", "depends_on", "result_schema"], "properties": {"schema_id": {"const": "dd-flow/code-work-packet@5"}, "key": {"type": "string", "minLength": 1}, "launch_policy": {"const": "fresh_agent_required"}, "source": {"$ref": "#/$defs/source"}, "repair": {"$ref": "#/$defs/repair"}, "task": {"type": "string", "minLength": 1}, "semantic_spine": {"$ref": "#/$defs/spine"}, "requirements": {"type": "array", "minItems": 1, "items": {"$ref": "#/$defs/obligation"}}, "acceptance": {"type": "array", "items": {"$ref": "#/$defs/acceptance"}}, "document_updates": {"type": "array", "items": {"$ref": "#/$defs/documentUpdate"}}, "required_read": {"$ref": "#/$defs/strings", "description": "Mandatory starting sources, not a read allowlist."}, "discovery_boundary": {"$ref": "#/$defs/strings", "description": "Likely discovery areas; additional project-local reads are allowed."}, "planned_write_areas": {"type": "array", "items": {"type": "string", "minLength": 1}, "description": "Soft coordination hints only. A worker may change any necessary project file inside the RUN workspace."}, "checks": {"type": "array", "items": {"$ref": "#/$defs/check"}}, "provides_checks": {"type": "array", "items": {"$ref": "#/$defs/check"}}, "stop_conditions": {"$ref": "#/$defs/strings", "description": "Hard semantic or external stop conditions; path prediction drift is not a stop condition."}, "depends_on": {"type": "array", "uniqueItems": true, "items": {"type": "string", "minLength": 1}}, "result_schema": {"const": "dd-flow/code-work-result@3"}}}
|
|
24
24
|
}
|
|
25
25
|
}
|
|
@@ -222,16 +222,46 @@ catch {
|
|
|
222
222
|
} }
|
|
223
223
|
export function unchangedFinalGateFailures(context, input) { const wanted = new Set(input.declarations.flatMap((check) => [check.id, checkRef(check)])); const current = workspaceFingerprint(input.workspaceRoot); const latest = new Map(); for (const receipt of checkReceipts(context, { projectId: input.projectId, runId: input.runId }).filter((item) => item.scope === "aggregate" && item.check_refs.some((ref) => wanted.has(ref))))
|
|
224
224
|
for (const ref of receipt.check_refs)
|
|
225
|
-
latest.set(ref, receipt); return [...new Set(latest.values())].filter((receipt) => receipt.status === "failed" && receipt.before_fingerprint === current); }
|
|
225
|
+
latest.set(ref, receipt); return [...new Set(latest.values())].filter((receipt) => receipt.status === "failed" && receipt.before_fingerprint === current && receipt.id !== input.retryReceiptId); }
|
|
226
|
+
/** Validate an explicit retry; the caller still executes a fresh real gate and retains a new receipt. */
|
|
227
|
+
export function validateFailedCheckRetry(context, input) {
|
|
228
|
+
if (!input.reason?.trim())
|
|
229
|
+
throw new AppError("retry_check_invalid", "--retry-check requires --reason describing the restored environment", 2);
|
|
230
|
+
const receipt = checkReceipts(context, { projectId: input.projectId, runId: input.runId }).find((item) => item.id === input.receiptId);
|
|
231
|
+
if (!receipt || receipt.scope !== "aggregate" || !["failed", "aborted"].includes(receipt.status))
|
|
232
|
+
throw new AppError("retry_check_invalid", "--retry-check must name a failed or aborted aggregate receipt in this RUN", 2, { check_receipt_id: input.receiptId });
|
|
233
|
+
const relative = path.relative(input.artifactRoot, receipt.receipt_path);
|
|
234
|
+
if (path.isAbsolute(relative) || relative === ".." || relative.startsWith(`..${path.sep}`) || /^try-\d+([/\\]|$)/.test(relative))
|
|
235
|
+
throw new AppError("retry_check_invalid", "--retry-check must belong to this current stage attempt, not an archived or different stage", 2, { check_receipt_id: input.receiptId });
|
|
236
|
+
const declared = new Set(input.declarations.flatMap((check) => [check.id, checkRef(check)]));
|
|
237
|
+
if (!receipt.check_refs.some((ref) => declared.has(ref)))
|
|
238
|
+
throw new AppError("retry_check_invalid", "--retry-check does not belong to this stage gate", 2, { check_receipt_id: input.receiptId, check_refs: receipt.check_refs });
|
|
239
|
+
return receipt;
|
|
240
|
+
}
|
|
226
241
|
export function checkReceipts(context, input) {
|
|
227
242
|
const where = input.workId ? "project_id = ? AND run_id = ? AND work_id = ?" : "project_id = ? AND run_id = ?";
|
|
228
243
|
const params = input.workId ? [input.projectId, input.runId, input.workId] : [input.projectId, input.runId];
|
|
229
|
-
return context.db.all(`SELECT id, declaration_id, check_refs_json, gate, scope, work_id, command, input_hash, verification_epoch, status, exit_code, stdout_path, stderr_path, receipt_path, workspace_fingerprint, before_fingerprint, after_fingerprint, profile_hash, mutation_paths_json, artifacts_json, started_at, finished_at FROM check_receipts WHERE ${where} ORDER BY started_at, id`, params).map((row) => ({ ...row, local_id: row.id.slice(row.id.lastIndexOf("/") + 1), check_refs: parseArray(row.check_refs_json, [row.declaration_id]), mutation_paths: parseArray(row.mutation_paths_json, []),
|
|
244
|
+
return context.db.all(`SELECT id, declaration_id, check_refs_json, gate, scope, work_id, command, input_hash, verification_epoch, status, exit_code, stdout_path, stderr_path, receipt_path, workspace_fingerprint, before_fingerprint, after_fingerprint, profile_hash, mutation_paths_json, artifacts_json, started_at, finished_at FROM check_receipts WHERE ${where} ORDER BY started_at, id`, params).map((row) => ({ ...row, local_id: row.id.slice(row.id.lastIndexOf("/") + 1), check_refs: parseArray(row.check_refs_json, [row.declaration_id]), mutation_paths: parseArray(row.mutation_paths_json, []), resources: readReceiptResources(row.receipt_path), ...resolvedReceiptArtifacts(row.receipt_path, JSON.parse(row.artifacts_json)) }));
|
|
245
|
+
}
|
|
246
|
+
/** File bytes remain immutable; only the reader projects their current location. */
|
|
247
|
+
function resolvedReceiptArtifacts(receiptPath, artifacts) {
|
|
248
|
+
let original = {};
|
|
249
|
+
try {
|
|
250
|
+
original = JSON.parse(fs.readFileSync(receiptPath, "utf8"));
|
|
251
|
+
}
|
|
252
|
+
catch { /* Historical missing receipts remain visibly incomplete. */ }
|
|
253
|
+
const currentRoot = path.dirname(receiptPath);
|
|
254
|
+
const oldRoot = original.receipt_path ? path.dirname(original.receipt_path) : currentRoot;
|
|
255
|
+
const relocate = (file) => {
|
|
256
|
+
const relative = path.relative(oldRoot, file);
|
|
257
|
+
return !path.isAbsolute(relative) && relative !== ".." && !relative.startsWith(`..${path.sep}`) ? path.join(currentRoot, relative) : file;
|
|
258
|
+
};
|
|
259
|
+
return { completion_path: path.join(currentRoot, "completion.json"), required_artifacts: original.required_artifacts ?? [], artifacts: artifacts.map((artifact) => ({ ...artifact, path: relocate(artifact.path) })) };
|
|
230
260
|
}
|
|
231
261
|
export function workspaceFingerprint(root) { return workspaceState(root).fingerprint; }
|
|
232
262
|
export function workspaceChangedPaths(root) { const status = spawnSync("git", ["status", "--porcelain=v1", "-z", "--untracked-files=all"], { cwd: root, encoding: "utf8", maxBuffer: 64 * 1024 * 1024 }); if (status.status !== 0)
|
|
233
263
|
return null; return status.stdout.split("\0").filter(Boolean).map((entry) => entry.slice(3)).filter(Boolean).sort(); }
|
|
234
|
-
function deduplicate(checks, commands) { const groups = new Map(); checks.forEach((check, index) => { const artifacts = [...(check.required_artifacts ?? [])].sort(); const ports = [...(check.ports ?? [])].sort(); const key = JSON.stringify([
|
|
264
|
+
function deduplicate(checks, commands) { const groups = new Map(); checks.forEach((check, index) => { const artifacts = [...(check.required_artifacts ?? [])].sort(); const ports = [...(check.ports ?? [])].sort(); const key = JSON.stringify([commands[index], artifacts, ports]); const current = groups.get(key); if (current)
|
|
235
265
|
current.declarations.push(check);
|
|
236
266
|
else
|
|
237
267
|
groups.set(key, { gate: check.run_at, command: commands[index], declarations: [check], requiredArtifacts: artifacts, ports }); }); return [...groups.values()]; }
|
|
@@ -19,7 +19,7 @@ export function createEvalBootstrapSnapshot(context, input) {
|
|
|
19
19
|
if (isInside(physicalPath(context.ddFlowHome), physicalPath(output)))
|
|
20
20
|
throw new AppError("snapshot_output_inside_runtime", "Bootstrap snapshot output must be outside DD_FLOW_HOME", 1, { output });
|
|
21
21
|
fs.mkdirSync(output, { recursive: true });
|
|
22
|
-
fs.cpSync(projectRoot, path.join(output, "project"), { recursive: true, verbatimSymlinks: true, filter:
|
|
22
|
+
fs.cpSync(projectRoot, path.join(output, "project"), { recursive: true, verbatimSymlinks: true, filter: snapshotPayloadAllowed });
|
|
23
23
|
const manifest = {
|
|
24
24
|
schema_id: "dd-eval/bootstrap-snapshot@1",
|
|
25
25
|
stage: "specify",
|
|
@@ -55,7 +55,8 @@ export function createEvalRunSnapshot(context, input) {
|
|
|
55
55
|
const project = requireProjectByRoot(context, projectRoot);
|
|
56
56
|
const status = getFlowRunStatus(context, { projectRoot, runId: input.runId });
|
|
57
57
|
assertDedicatedHome(context, project.id, input.runId);
|
|
58
|
-
if (input.
|
|
58
|
+
if (input.incomplete) { /* Forensic evidence is not an accepted or restorable boundary. */ }
|
|
59
|
+
else if (input.candidate)
|
|
59
60
|
assertTerminalCandidate(status.index.stage_runs ?? []);
|
|
60
61
|
else if (input.stageEntry)
|
|
61
62
|
assertStageEntry(status.index.stage_runs ?? [], input.stageEntry);
|
|
@@ -65,7 +66,7 @@ export function createEvalRunSnapshot(context, input) {
|
|
|
65
66
|
const activeChildren = input.candidate
|
|
66
67
|
? context.db.get("SELECT COUNT(*) AS count FROM works WHERE project_id = ? AND run_id = ? AND parent_work_id IS NOT NULL AND status IN ('created', 'running', 'paused')", [project.id, input.runId])?.count ?? 0
|
|
67
68
|
: context.db.get("SELECT COUNT(*) AS count FROM works WHERE project_id = ? AND run_id = ? AND parent_work_id IS NOT NULL AND (status IN ('running', 'paused') OR (status = 'created' AND (? IS NULL OR COALESCE(result_schema, '') NOT LIKE ?)))", [project.id, input.runId, allowedCreatedResultSchema, allowedCreatedResultSchema ? `${allowedCreatedResultSchema}@%` : ""])?.count ?? 0;
|
|
68
|
-
if (activeChildren > 0)
|
|
69
|
+
if (!input.incomplete && activeChildren > 0)
|
|
69
70
|
throw new AppError("snapshot_not_quiescent", `${input.candidate ? "Candidate" : "Stage-entry"} snapshot requires no active child Work`, 1, { run_id: input.runId, active_children: activeChildren });
|
|
70
71
|
const output = path.resolve(input.output);
|
|
71
72
|
if (fs.existsSync(output))
|
|
@@ -75,7 +76,6 @@ export function createEvalRunSnapshot(context, input) {
|
|
|
75
76
|
const workspaceRoot = String(status.run.workspace_root ?? projectRoot);
|
|
76
77
|
if (isInside(physicalPath(projectRoot), physicalPath(output)) || isInside(physicalPath(workspaceRoot), physicalPath(output)))
|
|
77
78
|
throw new AppError("snapshot_output_inside_project", "Snapshot output must be outside the project and RUN workspace trees", 1, { output });
|
|
78
|
-
context.db.exec("PRAGMA wal_checkpoint(TRUNCATE)");
|
|
79
79
|
fs.mkdirSync(output, { recursive: true });
|
|
80
80
|
fs.cpSync(context.ddFlowHome, path.join(output, "runtime"), {
|
|
81
81
|
recursive: true,
|
|
@@ -83,12 +83,17 @@ export function createEvalRunSnapshot(context, input) {
|
|
|
83
83
|
// Managed checkouts are restored from their Git bundle and workspace
|
|
84
84
|
// payload. Copying their .git indirection into runtime makes the snapshot
|
|
85
85
|
// dependent on the source machine's worktree metadata.
|
|
86
|
-
filter: (source) => !["engines", "checkouts"].includes(path.basename(source))
|
|
86
|
+
filter: (source) => snapshotPayloadAllowed(source) && !["engines", "checkouts"].includes(path.basename(source)) && ![context.db.path, `${context.db.path}-wal`, `${context.db.path}-shm`].includes(source)
|
|
87
87
|
});
|
|
88
|
+
// VACUUM INTO is a consistent SQLite read snapshot, including committed WAL
|
|
89
|
+
// pages. Copying the live database and its WAL separately is not atomic.
|
|
90
|
+
const databaseCopy = path.join(output, "runtime", path.relative(context.ddFlowHome, context.db.path));
|
|
91
|
+
fs.mkdirSync(path.dirname(databaseCopy), { recursive: true });
|
|
92
|
+
context.db.exec(`VACUUM INTO '${databaseCopy.replace(/'/g, "''")}'`);
|
|
88
93
|
fs.cpSync(projectRoot, path.join(output, "project"), {
|
|
89
94
|
recursive: true,
|
|
90
95
|
verbatimSymlinks: true,
|
|
91
|
-
filter:
|
|
96
|
+
filter: snapshotPayloadAllowed
|
|
92
97
|
});
|
|
93
98
|
const projectGit = snapshotProjectGit(projectRoot, output);
|
|
94
99
|
const workspace = snapshotWorkspace(workspaceRoot, projectRoot, output);
|
|
@@ -98,8 +103,9 @@ export function createEvalRunSnapshot(context, input) {
|
|
|
98
103
|
project_id: project.id,
|
|
99
104
|
project_root: projectRoot,
|
|
100
105
|
dd_flow_home: context.ddFlowHome,
|
|
101
|
-
purpose: input.candidate ? "candidate" : "stage_entry",
|
|
102
|
-
stage_entry: input.candidate ? null : input.stageEntry,
|
|
106
|
+
purpose: input.incomplete ? "incomplete" : input.candidate ? "candidate" : "stage_entry",
|
|
107
|
+
stage_entry: input.incomplete || input.candidate ? null : input.stageEntry,
|
|
108
|
+
...(input.incomplete ? { consistency: "sqlite_read_snapshot_files_copied_nonatomically", active_child_work_count: activeChildren, restorable: false } : {}),
|
|
103
109
|
created_at: context.now(),
|
|
104
110
|
runtime_sha256: treeHash(path.join(output, "runtime")),
|
|
105
111
|
project_sha256: treeHash(path.join(output, "project")),
|
|
@@ -121,6 +127,8 @@ function createdWorkSchemaForStage(stage) {
|
|
|
121
127
|
export function restoreEvalRunSnapshot(context, input) {
|
|
122
128
|
const snapshot = path.resolve(input.snapshot);
|
|
123
129
|
const manifest = readManifest(snapshot);
|
|
130
|
+
if (manifest.purpose === "incomplete")
|
|
131
|
+
throw new AppError("snapshot_incomplete_not_restorable", "Incomplete snapshot is forensic evidence, not a stage-entry fixture", 1, { snapshot });
|
|
124
132
|
if (manifest.purpose === "candidate")
|
|
125
133
|
throw new AppError("snapshot_candidate_not_restorable", "A terminal candidate snapshot is evidence, not a stage-entry fixture", 1, { snapshot });
|
|
126
134
|
if (!manifest.stage_entry)
|
|
@@ -285,7 +293,7 @@ function snapshotWorkspace(workspaceRoot, projectRoot, output) {
|
|
|
285
293
|
throw new AppError("snapshot_workspace_invalid", "Feature workspace has no readable HEAD", 1, { workspace_root: workspaceRoot });
|
|
286
294
|
const workspace = path.join(output, "workspace");
|
|
287
295
|
const bundle = path.join(output, "workspace.bundle");
|
|
288
|
-
fs.cpSync(workspaceRoot, workspace, { recursive: true, verbatimSymlinks: true, filter:
|
|
296
|
+
fs.cpSync(workspaceRoot, workspace, { recursive: true, verbatimSymlinks: true, filter: snapshotPayloadAllowed });
|
|
289
297
|
runGit(projectRoot, ["bundle", "create", bundle, branch]);
|
|
290
298
|
return { kind: "git_worktree", source_root: workspaceRoot, branch, base_ref: baseRef, sha256: treeHash(workspace), bundle_sha256: fileHash(bundle) };
|
|
291
299
|
}
|
|
@@ -343,6 +351,11 @@ function runGit(root, args) {
|
|
|
343
351
|
throw new AppError("snapshot_workspace_restore_failed", "Could not restore feature worktree", 1, { stderr: result.stderr.trim(), args });
|
|
344
352
|
}
|
|
345
353
|
function samePath(left, right) { return path.resolve(left) === path.resolve(right); }
|
|
354
|
+
function snapshotPayloadAllowed(source) {
|
|
355
|
+
const name = path.basename(source);
|
|
356
|
+
return ![".git", "node_modules", ".cache", ".npm", ".pnpm-store"].includes(name)
|
|
357
|
+
&& name !== ".env" && !name.startsWith(".env.");
|
|
358
|
+
}
|
|
346
359
|
function treeHash(root) {
|
|
347
360
|
const hash = crypto.createHash("sha256");
|
|
348
361
|
const visit = (dir, relative) => {
|
|
@@ -370,7 +383,7 @@ function readManifest(snapshot) {
|
|
|
370
383
|
if (!fs.existsSync(file))
|
|
371
384
|
throw new AppError("snapshot_missing", "Snapshot manifest is missing", 1, { snapshot });
|
|
372
385
|
const value = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
373
|
-
if (value.schema_id !== "dd-flow/eval-run-snapshot@5" || !value.run_id || !value.project_root || !value.dd_flow_home || typeof value.purpose !== "string" || !["stage_entry", "candidate"].includes(value.purpose) || (value.purpose === "stage_entry" && !value.stage_entry) || (value.purpose
|
|
386
|
+
if (value.schema_id !== "dd-flow/eval-run-snapshot@5" || !value.run_id || !value.project_root || !value.dd_flow_home || typeof value.purpose !== "string" || !["stage_entry", "candidate", "incomplete"].includes(value.purpose) || (value.purpose === "stage_entry" && !value.stage_entry) || (value.purpose !== "stage_entry" && value.stage_entry !== null) || !value.runtime_sha256 || !value.project_sha256 || !value.project_git || !value.workspace)
|
|
374
387
|
throw new AppError("snapshot_invalid", "Snapshot manifest is invalid", 1, { snapshot });
|
|
375
388
|
if (!value.project_git.branch || !value.project_git.head || !value.project_git.bundle_sha256)
|
|
376
389
|
throw new AppError("snapshot_invalid", "Snapshot project Git metadata is incomplete", 1, { snapshot });
|
package/dist/services/hooks.js
CHANGED
|
@@ -309,7 +309,11 @@ export function handleCodexHook(context, input) {
|
|
|
309
309
|
const project = projectForHook(context, input.projectRoot ?? commandProjectRoot, stringValue(payload.cwd));
|
|
310
310
|
if (!project)
|
|
311
311
|
return { ok: true, observed: false, reason: "unrelated_cwd" };
|
|
312
|
-
|
|
312
|
+
// Codex retains the root session_id for a thread-spawned child, while the
|
|
313
|
+
// payload transcript belongs to the current agent_id. Keep lineage separate
|
|
314
|
+
// from ownership: otherwise a child overwrites the root transcript binding.
|
|
315
|
+
const bindingOwnerId = providerSessionId ?? rootProviderSessionId;
|
|
316
|
+
const binding = bindingOwnerId ? upsertSessionBindingFromPayload(context, project, bindingOwnerId, payload) : undefined;
|
|
313
317
|
const storageId = providerSessionId ? storageSessionId(nativeSessionIdentity("codex-desktop", providerSessionId)) : null;
|
|
314
318
|
const parentSessionId = parentProviderSessionId ? storageSessionId(nativeSessionIdentity("codex-desktop", parentProviderSessionId)) : null;
|
|
315
319
|
const observedSession = flowPayload
|
package/dist/services/runs.js
CHANGED
|
@@ -17,6 +17,7 @@ import { bindCurrentEngineToRun } from "./engines.js";
|
|
|
17
17
|
import { executionProfilePath, loadVnextExecutionProfile } from "./vnext-execution-profile.js";
|
|
18
18
|
import { isLegalVnextTransition, vnextStageDirectory, vnextStages } from "../domain/stage-catalog.js";
|
|
19
19
|
import { publicSessionIdentity } from "./session-identity.js";
|
|
20
|
+
import { flowCommand } from "./stage-pause.js";
|
|
20
21
|
const runSchemaId = "dd-flow/flow-run@3";
|
|
21
22
|
const runtimeSchemaId = "dd-flow/flow-run@3";
|
|
22
23
|
const runIdType = "RUN";
|
|
@@ -229,7 +230,33 @@ export function getFlowRunStatus(context, input) {
|
|
|
229
230
|
const run = resolveRun(context, project.id, input.runId);
|
|
230
231
|
const index = authoritativeIndex(run);
|
|
231
232
|
const runtime = readRuntimeSnapshot(run.runtime_path);
|
|
232
|
-
return { ok: true, run: flowRunSummary(run), index, ...(runtime ? { runtime } : {}), flow_guidance: guidanceForRun(context, run, index) };
|
|
233
|
+
return { ok: true, run: flowRunSummary(run), index, continuation: runContinuation(context, run, index), ...(runtime ? { runtime } : {}), flow_guidance: guidanceForRun(context, run, index) };
|
|
234
|
+
}
|
|
235
|
+
/** A read-only projection of the next legal lifecycle action.
|
|
236
|
+
* It is derived from the persisted RUN/Work facts so controllers never infer a
|
|
237
|
+
* transition from a model's prose or a hard-coded stage array. */
|
|
238
|
+
function runContinuation(context, run, index) {
|
|
239
|
+
if (["done", "failed", "cancelled"].includes(run.status))
|
|
240
|
+
return { kind: "terminal", stage: null, attempt: null, work_id: null, reason: run.status, command: null };
|
|
241
|
+
const active = index.stage_runs.find((stage) => !["done", "skipped"].includes(stage.status));
|
|
242
|
+
if (active) {
|
|
243
|
+
const runningWork = context.db.get("SELECT work_id FROM works WHERE project_id = ? AND run_id = ? AND status = 'running' ORDER BY updated_at DESC LIMIT 1", [run.project_id, run.id]);
|
|
244
|
+
const kind = active.status === "running" ? "continue_stage" : active.status === "paused" || active.status === "waiting_for_user" ? "paused" : "blocked";
|
|
245
|
+
return { kind, stage: active.stage, attempt: active.attempt ?? null, work_id: active.pause?.work_id ?? runningWork?.work_id ?? null, reason: active.status, command: null };
|
|
246
|
+
}
|
|
247
|
+
if (run.status === "waiting_for_user" || run.status === "paused")
|
|
248
|
+
return { kind: "paused", stage: index.current_stage ?? null, attempt: null, work_id: null, reason: run.status, command: null };
|
|
249
|
+
if (run.status === "blocked")
|
|
250
|
+
return { kind: "blocked", stage: index.current_stage ?? null, attempt: null, work_id: null, reason: run.status, command: null };
|
|
251
|
+
if (run.flow_kind === "vnext_specify" || run.flow_kind === "vnext_protocolize") {
|
|
252
|
+
const completed = index.stage_runs.filter((stage) => stage.status === "done" || stage.status === "skipped").sort((left, right) => right.order - left.order)[0];
|
|
253
|
+
const next = completed ? vnextStages.find((stage) => stage.id === completed.stage)?.next.find((candidate) => candidate !== "plan-review" || index.settings?.plan_review?.mode !== "off") : "specify";
|
|
254
|
+
if (!next)
|
|
255
|
+
return { kind: "terminal", stage: null, attempt: null, work_id: null, reason: "flow_complete", command: null };
|
|
256
|
+
const command = `${flowCommand(context)} stage start ${run.id} --stage ${next} --project-root ${JSON.stringify(run.project_root)} --json`;
|
|
257
|
+
return { kind: "start_stage", stage: next, attempt: null, work_id: null, reason: "legal_successor", command };
|
|
258
|
+
}
|
|
259
|
+
return { kind: "wait", stage: index.current_stage ?? null, attempt: null, work_id: null, reason: "no_vnext_continuation", command: null };
|
|
233
260
|
}
|
|
234
261
|
export function getFlowRunConfig(context, input) {
|
|
235
262
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
@@ -470,7 +497,7 @@ export function attachFlowRunStage(context, input) {
|
|
|
470
497
|
const existing = index.stage_runs.find((item) => item.stage === stage);
|
|
471
498
|
assertVnextStageStart(run, index, stage, status, existing);
|
|
472
499
|
if (status === "running" && existing) {
|
|
473
|
-
archiveExistingStageAttempt(runArtifactRoot(run), dir);
|
|
500
|
+
archiveExistingStageAttempt(context, project.id, run.id, runArtifactRoot(run), dir);
|
|
474
501
|
}
|
|
475
502
|
const stageRun = {
|
|
476
503
|
...(existing ?? { order: index.stage_runs.length + 1 }),
|
|
@@ -763,6 +790,9 @@ export function prepareVnextMergeSourceRepairAttempt(context, input) {
|
|
|
763
790
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
764
791
|
const run = resolveRun(context, project.id, input.runId);
|
|
765
792
|
const index = authoritativeIndex(run);
|
|
793
|
+
const priorRepair = index.variables?.["merge.source_repair"];
|
|
794
|
+
if (priorRepair?.merge_request_id === input.mergeRequestId && priorRepair.cycle)
|
|
795
|
+
return { run_id: run.id, cycle: priorRepair.cycle };
|
|
766
796
|
const code = index.stage_runs.find((item) => item.stage === "code");
|
|
767
797
|
const review = index.stage_runs.find((item) => item.stage === "code-review");
|
|
768
798
|
const merge = index.stage_runs.find((item) => item.stage === "merge");
|
|
@@ -776,7 +806,7 @@ export function prepareVnextMergeSourceRepairAttempt(context, input) {
|
|
|
776
806
|
}
|
|
777
807
|
const root = runArtifactRoot(run);
|
|
778
808
|
for (const stage of ["code", "code-review", "merge"])
|
|
779
|
-
archiveExistingStageAttempt(root, vnextStageDirectory(stage));
|
|
809
|
+
archiveExistingStageAttempt(context, project.id, run.id, root, vnextStageDirectory(stage));
|
|
780
810
|
const now = context.now();
|
|
781
811
|
const restarted = {
|
|
782
812
|
...code,
|
|
@@ -1391,24 +1421,55 @@ function resolveWorkspaceRoot(workspaceRoot) {
|
|
|
1391
1421
|
}
|
|
1392
1422
|
return fs.realpathSync(absolute);
|
|
1393
1423
|
}
|
|
1394
|
-
|
|
1424
|
+
/** Move one completed stage attempt and update only known durable path columns.
|
|
1425
|
+
* Receipt bytes remain immutable; readers resolve their new location through the DB. */
|
|
1426
|
+
export function archiveExistingStageAttempt(context, projectId, runId, runRoot, stageDir) {
|
|
1395
1427
|
const currentStageDir = path.join(runRoot, stageDir);
|
|
1396
1428
|
if (!fs.existsSync(currentStageDir) || !fs.statSync(currentStageDir).isDirectory()) {
|
|
1397
1429
|
return;
|
|
1398
1430
|
}
|
|
1399
|
-
const
|
|
1400
|
-
|
|
1431
|
+
const journal = path.join(runRoot, `.archive-${stageDir}.json`);
|
|
1432
|
+
const pending = fs.existsSync(journal) ? JSON.parse(fs.readFileSync(journal, "utf8")) : null;
|
|
1433
|
+
const entries = pending?.entries ?? fs.readdirSync(currentStageDir).filter((entry) => !/^try-\d{3}$/.test(entry));
|
|
1434
|
+
if (entries.length === 0 && !pending) {
|
|
1401
1435
|
return;
|
|
1402
1436
|
}
|
|
1403
1437
|
let attempt = 1;
|
|
1404
1438
|
while (fs.existsSync(path.join(currentStageDir, `try-${String(attempt).padStart(3, "0")}`))) {
|
|
1405
1439
|
attempt += 1;
|
|
1406
1440
|
}
|
|
1407
|
-
const archiveDir = path.join(currentStageDir, `try-${String(attempt).padStart(3, "0")}`);
|
|
1441
|
+
const archiveDir = pending?.archiveDir ?? path.join(currentStageDir, `try-${String(attempt).padStart(3, "0")}`);
|
|
1442
|
+
if (!pending) {
|
|
1443
|
+
const stored = context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [projectId, runId]);
|
|
1444
|
+
const prior = stored ? JSON.parse(stored.index_json) : null;
|
|
1445
|
+
writeJsonFile(journal, { archiveDir, entries, stage_run: prior?.stage_runs.find((stage) => stage.dir === stageDir) ?? null });
|
|
1446
|
+
}
|
|
1408
1447
|
ensureDir(archiveDir);
|
|
1409
1448
|
for (const entry of entries) {
|
|
1410
|
-
|
|
1449
|
+
const source = path.join(currentStageDir, entry);
|
|
1450
|
+
const target = path.join(archiveDir, entry);
|
|
1451
|
+
if (fs.existsSync(source)) {
|
|
1452
|
+
if (fs.existsSync(target))
|
|
1453
|
+
throw new AppError("archive_conflict", "Both live and archived artifact exist; preserve both for recovery", 2, { source, target });
|
|
1454
|
+
fs.renameSync(source, target);
|
|
1455
|
+
}
|
|
1456
|
+
else if (!fs.existsSync(target))
|
|
1457
|
+
throw new AppError("archive_artifact_missing", "Artifact is absent from both archive locations", 2, { source, target });
|
|
1411
1458
|
}
|
|
1459
|
+
const oldPrefix = `${currentStageDir}${path.sep}`;
|
|
1460
|
+
const newPrefix = `${archiveDir}${path.sep}`;
|
|
1461
|
+
const rewrite = (table, column, where, values) => {
|
|
1462
|
+
context.db.run(`UPDATE ${table} SET ${column} = ? || substr(${column}, ?) WHERE ${column} LIKE ? AND substr(${column}, ?, 4) != 'try-' AND ${where}`, [newPrefix, oldPrefix.length + 1, `${oldPrefix}%`, oldPrefix.length + 1, ...values]);
|
|
1463
|
+
};
|
|
1464
|
+
const workScope = "work_id IN (SELECT work_id FROM works WHERE project_id = ? AND run_id = ?)";
|
|
1465
|
+
rewrite("work_sessions", "prompt_path", workScope, [projectId, runId]);
|
|
1466
|
+
rewrite("work_sessions", "result_path", workScope, [projectId, runId]);
|
|
1467
|
+
const receiptScope = "project_id = ? AND run_id = ?";
|
|
1468
|
+
for (const column of ["stdout_path", "stderr_path", "receipt_path"])
|
|
1469
|
+
rewrite("check_receipts", column, receiptScope, [projectId, runId]);
|
|
1470
|
+
// Retain the original stage facts with its immutable artifacts; current
|
|
1471
|
+
// stage_runs stays a projection of the current attempt, not a second ledger.
|
|
1472
|
+
fs.renameSync(journal, path.join(archiveDir, "archive.json"));
|
|
1412
1473
|
}
|
|
1413
1474
|
function writeJsonFile(file, value) {
|
|
1414
1475
|
const tmpFile = `${file}.tmp-${process.pid}-${Date.now()}`;
|
|
@@ -11,7 +11,7 @@ import { flowCommand } from "./stage-pause.js";
|
|
|
11
11
|
import { assertStageStartHookEvent } from "./hooks.js";
|
|
12
12
|
import { requireVnextWorkspaceRoute } from "./vnext-workspace-policy.js";
|
|
13
13
|
import { addVnextCodeRepair } from "./vnext-code.js";
|
|
14
|
-
import { finalCodeCheckDeclarations, runCodeChecks, unchangedFinalGateFailures, workspaceFingerprint } from "./code-checks.js";
|
|
14
|
+
import { finalCodeCheckDeclarations, runCodeChecks, unchangedFinalGateFailures, validateFailedCheckRetry, workspaceFingerprint } from "./code-checks.js";
|
|
15
15
|
import { addWorkBatch, bindStageCoordinatorWork, finishWork, refreshRunWorkProjection } from "./work-registry.js";
|
|
16
16
|
import { vnextStageDirectory } from "../domain/stage-catalog.js";
|
|
17
17
|
import { writeStageReport } from "./stage-report-renderer.js";
|
|
@@ -172,9 +172,13 @@ export async function finishVnextCodeReview(context, input) {
|
|
|
172
172
|
const outcome = decision.findings.some((item) => item.disposition === "defer") ? "accepted_with_DEF" : "accepted";
|
|
173
173
|
const repairs = reviewRepairWorks(context, project.id, run.id, root, cycle);
|
|
174
174
|
const finalChecks = finalCodeCheckDeclarations(run.workspace_root, codeWorks(context, project.id, run.id).flatMap((work) => { const payload = readJsonString(work.payload_json); return Array.isArray(payload.checks) ? payload.checks : []; }));
|
|
175
|
-
|
|
175
|
+
if (input.retryCheckId) {
|
|
176
|
+
validateFailedCheckRetry(context, { projectId: project.id, runId: run.id, receiptId: input.retryCheckId, declarations: finalChecks, artifactRoot: path.join(requireHome(run), stageDir), reason: input.retryReason });
|
|
177
|
+
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "check_retry_requested", check_receipt_id: input.retryCheckId, stage, reason: input.retryReason ?? "operator retry" });
|
|
178
|
+
}
|
|
179
|
+
const unchangedFailures = unchangedFinalGateFailures(context, { projectId: project.id, runId: run.id, workspaceRoot: run.workspace_root, declarations: finalChecks, ...(input.retryCheckId ? { retryReceiptId: input.retryCheckId } : {}) });
|
|
176
180
|
if (unchangedFailures.length)
|
|
177
|
-
throw new AppError("code_review_gate_repair_required", "CODE-REVIEW final gate already failed for the unchanged workspace; repair the
|
|
181
|
+
throw new AppError("code_review_gate_repair_required", "CODE-REVIEW final gate already failed for the unchanged workspace; choose source repair or restore the environment and retry that receipt", 2, { outcome: "repair_required", retry_after_workspace_change: true, workspace_fingerprint: unchangedFailures[0].workspace_fingerprint, failures: unchangedFailures, retry_command: `${finishCommand(context, run.id, projectRoot, decisionFile)} --retry-check ${unchangedFailures[0].id} --reason "<environment recovery evidence>"` });
|
|
178
182
|
const receipts = await runCodeChecks(context, { projectId: project.id, runId: run.id, runHome: home, workspaceRoot: run.workspace_root, artifactDir: stageDir, scope: "aggregate", checks: finalChecks, ...(input.progress ? { progress: input.progress } : {}) });
|
|
179
183
|
const failed = receipts.filter((receipt) => receipt.status !== "passed");
|
|
180
184
|
if (failed.length)
|
|
@@ -183,7 +187,8 @@ export async function finishVnextCodeReview(context, input) {
|
|
|
183
187
|
outcome: "repair_required",
|
|
184
188
|
retry_after_workspace_change: true,
|
|
185
189
|
workspace_fingerprint: failed[0].workspace_fingerprint,
|
|
186
|
-
repair_command: `${flowCommand(context)} work repair add --run ${run.id} --from-check ${failed[0].id} --origin-work <WORK-ID> --task-stdin --project-root ${JSON.stringify(projectRoot)} --json
|
|
190
|
+
repair_command: `${flowCommand(context)} work repair add --run ${run.id} --from-check ${failed[0].id} --origin-work <WORK-ID> --task-stdin --project-root ${JSON.stringify(projectRoot)} --json`,
|
|
191
|
+
retry_command: `${finishCommand(context, run.id, projectRoot, decisionFile)} --retry-check ${failed[0].id} --reason "<environment recovery evidence>"`
|
|
187
192
|
});
|
|
188
193
|
const stopTarget = executionStopTarget(run);
|
|
189
194
|
const nextAction = stopTarget === "merge_completed" ? "start_merge" : "code_review_completed";
|
|
@@ -4,7 +4,7 @@ import fs from "node:fs";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { AppError } from "../shared/errors.js";
|
|
6
6
|
import { resolveProjectRoot } from "../storage/paths.js";
|
|
7
|
-
import { aggregateCheckDeclarations, checkReceipts, finalCodeCheckDeclarations, runCodeChecks, unchangedFinalGateFailures, workspaceChangedPaths, workspaceFingerprint } from "./code-checks.js";
|
|
7
|
+
import { aggregateCheckDeclarations, checkReceipts, effectiveCheckDeclarations, finalCodeCheckDeclarations, runCodeChecks, unchangedFinalGateFailures, validateFailedCheckRetry, workspaceChangedPaths, workspaceFingerprint } from "./code-checks.js";
|
|
8
8
|
import { requireProjectByRoot } from "./projects.js";
|
|
9
9
|
import { appendFlowRunTimelineEvent, advanceFlowRun, attachFlowRunStage, completeFlowRunStage, completeFlowRun, getFlowRunVariables, gitFacts } from "./runs.js";
|
|
10
10
|
import { validateSchema } from "./schema-validation.js";
|
|
@@ -157,9 +157,13 @@ export async function finishVnextCode(context, input) {
|
|
|
157
157
|
return { ok: true, run_id: run.id, stage, outcome: "repair_required", verification, repair, instruction: "The semantic verification is not accepted. Run the returned repair Work, then update code-verification.json and invoke this same stage finish command again." };
|
|
158
158
|
}
|
|
159
159
|
const checks = finalCodeCheckDeclarations(run.workspace_root, works.flatMap((work) => packet(work)?.checks ?? []));
|
|
160
|
-
|
|
160
|
+
if (input.retryCheckId) {
|
|
161
|
+
validateFailedCheckRetry(context, { projectId: project.id, runId: run.id, receiptId: input.retryCheckId, declarations: checks, artifactRoot: path.join(requireHome(run), stageDir), reason: input.retryReason });
|
|
162
|
+
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "check_retry_requested", check_receipt_id: input.retryCheckId, stage, reason: input.retryReason ?? "operator retry" });
|
|
163
|
+
}
|
|
164
|
+
const unchangedFailures = unchangedFinalGateFailures(context, { projectId: project.id, runId: run.id, workspaceRoot: run.workspace_root, declarations: checks, ...(input.retryCheckId ? { retryReceiptId: input.retryCheckId } : {}) });
|
|
161
165
|
if (unchangedFailures.length)
|
|
162
|
-
throw new AppError("code_gate_repair_required", "CODE final gate already failed for the unchanged workspace; repair the
|
|
166
|
+
throw new AppError("code_gate_repair_required", "CODE final gate already failed for the unchanged workspace; choose source repair or restore the environment and retry that receipt", 2, { outcome: "repair_required", retry_after_workspace_change: true, workspace_fingerprint: unchangedFailures[0].workspace_fingerprint, failures: unchangedFailures, retry_command: `${finishCommand(context, run.id, projectRoot, input.verificationFile)} --retry-check ${unchangedFailures[0].id} --reason "<environment recovery evidence>"` });
|
|
163
167
|
let receipts = reusableAggregateGateReceipts(context, { projectId: project.id, runId: run.id, workspaceRoot: run.workspace_root, checks });
|
|
164
168
|
if (!receipts) {
|
|
165
169
|
const release = acquireAggregateGateLock(root);
|
|
@@ -187,7 +191,8 @@ export async function finishVnextCode(context, input) {
|
|
|
187
191
|
outcome: "repair_required",
|
|
188
192
|
retry_after_workspace_change: true,
|
|
189
193
|
workspace_fingerprint: failed[0].workspace_fingerprint,
|
|
190
|
-
repair_command: `${flowCommand(context)} work repair add --run ${run.id} --from-check ${failed[0].id} --origin-work <WORK-ID> --task-stdin --project-root ${JSON.stringify(projectRoot)} --json
|
|
194
|
+
repair_command: `${flowCommand(context)} work repair add --run ${run.id} --from-check ${failed[0].id} --origin-work <WORK-ID> --task-stdin --project-root ${JSON.stringify(projectRoot)} --json`,
|
|
195
|
+
retry_command: `${finishCommand(context, run.id, projectRoot, input.verificationFile)} --retry-check ${failed[0].id} --reason "<environment recovery evidence>"`
|
|
191
196
|
});
|
|
192
197
|
}
|
|
193
198
|
const allReceipts = checkReceipts(context, { projectId: project.id, runId: run.id });
|
|
@@ -284,6 +289,11 @@ export function addVnextCodeRepair(context, input) {
|
|
|
284
289
|
const home = requireHome(run);
|
|
285
290
|
if (!input.objective.trim())
|
|
286
291
|
throw new AppError("validation", "Repair objective must not be empty", 2);
|
|
292
|
+
if (input.reviewCycle) {
|
|
293
|
+
const previous = codeWorks(context, project.id, run.id).find((work) => packet(work)?.review_cycle === input.reviewCycle);
|
|
294
|
+
if (previous)
|
|
295
|
+
return { ok: true, run_id: run.id, repair_work_id: previous.work_id, start_command: workStartCommand(context, previous), reused: true };
|
|
296
|
+
}
|
|
287
297
|
if (!input.checkReceiptId && !input.reviewFindings?.length && !input.semanticUnresolved?.length) {
|
|
288
298
|
throw new AppError("validation", "Repair needs failed check evidence, a CODE-REVIEW finding, or an unresolved CODE verification", 2);
|
|
289
299
|
}
|
|
@@ -307,10 +317,10 @@ export function addVnextCodeRepair(context, input) {
|
|
|
307
317
|
// repair that fixes one failed aggregate check can make a second, already
|
|
308
318
|
// declared aggregate failure impossible to repair. The immutable source
|
|
309
319
|
// of repair eligibility is the original CODE graph.
|
|
310
|
-
const declaredChecks =
|
|
320
|
+
const declaredChecks = effectiveCheckDeclarations(run.workspace_root, codeWorks(context, project.id, run.id)
|
|
311
321
|
.map((work) => packet(work))
|
|
312
322
|
.filter((value) => !value.repair)
|
|
313
|
-
.flatMap((value) => value.checks));
|
|
323
|
+
.flatMap((value) => value.checks), ["work", "code", "readiness", "merge"]);
|
|
314
324
|
const failedCheck = receipt ? declaredChecks.find((check) => check.id === receipt.declaration_id) : undefined;
|
|
315
325
|
if (receipt && !failedCheck) {
|
|
316
326
|
throw new AppError("repair_check_declaration_missing", "Repair receipt is not backed by an accepted CODE check declaration", 2, {
|
|
@@ -330,7 +340,11 @@ export function addVnextCodeRepair(context, input) {
|
|
|
330
340
|
origin_work_ids: origins.map((work) => work.work_id),
|
|
331
341
|
...(receipt ? { check_receipt_id: receipt.id, failure_receipt_path: receipt.receipt_path } : {}),
|
|
332
342
|
...(input.reviewFindings?.length ? { review_findings: input.reviewFindings } : {}),
|
|
333
|
-
...(semanticRepair ? { semantic_unresolved: unique(input.semanticUnresolved ?? []), verification_path: input.verificationPath } : {})
|
|
343
|
+
...(semanticRepair ? { semantic_unresolved: unique(input.semanticUnresolved ?? []), verification_path: input.verificationPath } : {}),
|
|
344
|
+
// `run_at` remains the accepted aggregate placement. This separate
|
|
345
|
+
// repair obligation is deliberately executed before this repair Work is
|
|
346
|
+
// accepted, so a local fix cannot close without re-proving its cause.
|
|
347
|
+
verification_check_refs: selectRepairChecks({ ...(failedCheck ? { failedCheck } : {}), ...(input.reviewChecks ? { reviewChecks: input.reviewChecks } : {}), ...(semanticRepair ? { semanticChecks: declaredChecks } : {}) })
|
|
334
348
|
},
|
|
335
349
|
task: input.objective,
|
|
336
350
|
semantic_spine: {
|
|
@@ -384,11 +398,8 @@ export function addVnextCodeRepair(context, input) {
|
|
|
384
398
|
refreshRunWorkProjection(context, project.id, run.id);
|
|
385
399
|
return { ok: true, run_id: run.id, repair_work_id: id, start_command: workStartCommand(context, work) };
|
|
386
400
|
}
|
|
387
|
-
/** Retain the causal declaration for worker context; run_at
|
|
401
|
+
/** Retain the causal declaration for worker context; run_at still records its original gate. */
|
|
388
402
|
export function selectRepairChecks(input) {
|
|
389
|
-
// Gate placement belongs to the accepted PLAN. `runCodeChecks` executes only
|
|
390
|
-
// work-scoped entries, while retaining the aggregate declaration tells the
|
|
391
|
-
// worker exactly what will be rerun by the coordinator.
|
|
392
403
|
const candidates = input.failedCheck ? [input.failedCheck] : input.reviewChecks ?? input.semanticChecks ?? [];
|
|
393
404
|
const checks = candidates.map((check) => ({ ...check, purpose: `${input.semanticChecks ? "Prove the CODE verification repair" : "Prove the CODE-REVIEW repair"}: ${check.purpose}` }));
|
|
394
405
|
return uniqueBy(checks, (check) => check.id);
|
|
@@ -3,8 +3,10 @@ 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 } from "./runs.js";
|
|
7
|
-
import { listWorks } from "./work-registry.js";
|
|
6
|
+
import { getFlowRunVariables, appendFlowRunTimelineEvent } from "./runs.js";
|
|
7
|
+
import { listWorks, failWork, cancelWork } from "./work-registry.js";
|
|
8
|
+
import { storageSessionId } from "./session-identity.js";
|
|
9
|
+
import { reconcileUnfinishedChecks } from "./code-checks.js";
|
|
8
10
|
import { vnextStageDirectory } from "../domain/stage-catalog.js";
|
|
9
11
|
export const subagentCapacityKey = "runtime.subagent.available_slots";
|
|
10
12
|
export function fanoutState(input) {
|
|
@@ -66,7 +68,70 @@ export function getVnextFanoutStatus(context, input) {
|
|
|
66
68
|
run_key: subagentCapacityKey,
|
|
67
69
|
available_slots: capacityKnown ? available : null
|
|
68
70
|
},
|
|
69
|
-
works: { ...counts, ready: ready.works.map((work) => ({ work_id: work.work_id, task: work.task, start_command: work.start_command })) }
|
|
71
|
+
works: { ...counts, ready: ready.works.map((work) => ({ work_id: work.work_id, task: work.task, launch_policy: work.launch_policy, start_command: work.start_command })) }
|
|
70
72
|
}
|
|
71
73
|
};
|
|
72
74
|
}
|
|
75
|
+
/** Explicit controller mutation, separate from read-only fanout status.
|
|
76
|
+
* A native child may be mapped only by its registered identity. Reused native
|
|
77
|
+
* Sessions need a provider execution identity: without one we report ambiguity
|
|
78
|
+
* instead of applying a late event to the newest Work attempt. */
|
|
79
|
+
export async function reconcileVnextFanout(context, input) {
|
|
80
|
+
const status = getVnextFanoutStatus(context, input);
|
|
81
|
+
if (!status.orchestration)
|
|
82
|
+
return { ok: true, issues: [], reconciled: [] };
|
|
83
|
+
const value = input.observations;
|
|
84
|
+
if (!value || typeof value.harness_id !== "string" || !value.harness_id || typeof value.parent_session_id !== "string" || !Array.isArray(value.children)
|
|
85
|
+
|| value.children.some(child => !child || typeof child.session_id !== "string" || !["running", "unknown", "completed", "failed", "cancelled"].includes(String(child.status))))
|
|
86
|
+
throw new AppError("usage", "Expected native observations: harness_id, parent_session_id and children[{session_id,status}]", 2);
|
|
87
|
+
if (new Set(value.children.map(child => child.session_id)).size !== value.children.length)
|
|
88
|
+
throw new AppError("usage", "Native observations must contain each child Session exactly once", 2);
|
|
89
|
+
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
90
|
+
const parent = storageSessionId({ harness_id: value.harness_id, session_id: value.parent_session_id });
|
|
91
|
+
const issues = [], reconciled = [];
|
|
92
|
+
for (const child of value.children) {
|
|
93
|
+
if (child.status === "running" || child.status === "unknown")
|
|
94
|
+
continue;
|
|
95
|
+
const sessionId = storageSessionId({ harness_id: value.harness_id, session_id: String(child.session_id) });
|
|
96
|
+
const links = context.db.all(`SELECT ws.id, ws.work_id, ws.status, w.status AS work_status, w.parent_work_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 WHERE w.project_id = ? AND w.run_id = ? AND ws.session_id = ? AND s.parent_session_id = ? ORDER BY ws.created_at`, [project.id, input.runId, sessionId, parent]);
|
|
97
|
+
const active = links.filter(link => link.status === "running" && link.work_status === "running" && link.parent_work_id === status.orchestration.parent_work_id);
|
|
98
|
+
if (!active.length) {
|
|
99
|
+
if (!links.length)
|
|
100
|
+
issues.push({ code: "child_launch_association_missing", session_id: child.session_id, outcome: child.status });
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (links.length !== 1 || active.length !== 1) {
|
|
104
|
+
issues.push({ code: "child_execution_ambiguous", session_id: child.session_id, work_ids: active.map(link => link.work_id) });
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const link = active[0];
|
|
108
|
+
const pendingCheck = () => context.db.get("SELECT 1 FROM check_receipts WHERE project_id = ? AND run_id = ? AND work_id = ? AND status = 'running' LIMIT 1", [project.id, input.runId, link.work_id]);
|
|
109
|
+
if (pendingCheck()) {
|
|
110
|
+
const run = context.db.get("SELECT workspace_root, run_root FROM runs WHERE project_id = ? AND id = ?", [project.id, input.runId]);
|
|
111
|
+
try {
|
|
112
|
+
reconcileUnfinishedChecks(context, { projectId: project.id, runId: input.runId, workspaceRoot: run.workspace_root, runHome: run.run_root, workId: link.work_id });
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
if (!(error instanceof AppError) || error.code !== "check_in_progress")
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
118
|
+
if (pendingCheck()) {
|
|
119
|
+
issues.push({ code: "child_check_in_progress", work_id: link.work_id });
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (context.db.get("SELECT 1 FROM works WHERE parent_work_id = ? AND status IN ('created','running','paused') LIMIT 1", [link.work_id])) {
|
|
124
|
+
issues.push({ code: "child_descendants_unsettled", work_id: link.work_id });
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (child.status === "completed") {
|
|
128
|
+
issues.push({ code: "execution_ended_without_work_result", work_id: link.work_id, session_id: child.session_id });
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
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 });
|
|
132
|
+
const result = await (child.status === "failed" ? failWork : cancelWork)(context, link.work_id, reason);
|
|
133
|
+
reconciled.push({ work_id: link.work_id, work_session_id: link.id, outcome: child.status, result });
|
|
134
|
+
}
|
|
135
|
+
appendFlowRunTimelineEvent(context, project.id, input.runId, { type: "native_child_reconciliation", stage: input.stage, observations: value, issues, reconciled });
|
|
136
|
+
return { ok: true, issues, reconciled };
|
|
137
|
+
}
|
|
@@ -15,7 +15,7 @@ import { validateSchema } from "./schema-validation.js";
|
|
|
15
15
|
import { writeStageReport } from "./stage-report-renderer.js";
|
|
16
16
|
import { bindStageCoordinatorWork, createChildWork, failWork, finishFanInWork, finishWork, refreshRunWorkProjection, startStageCoordinatorWork } from "./work-registry.js";
|
|
17
17
|
import { addVnextCodeRepair } from "./vnext-code.js";
|
|
18
|
-
import { checkReceipts, effectiveCheckDeclarations, readCodeCheckProfile, runCodeChecks, unchangedFinalGateFailures, workspaceFingerprint } from "./code-checks.js";
|
|
18
|
+
import { checkReceipts, effectiveCheckDeclarations, readCodeCheckProfile, runCodeChecks, unchangedFinalGateFailures, validateFailedCheckRetry, workspaceFingerprint } from "./code-checks.js";
|
|
19
19
|
import { applyExternalStageContext } from "./stage-context.js";
|
|
20
20
|
import { ensureLaneWorkspace, heartbeatLaneLock, releaseLaneLock, waitAcquireLaneLock } from "./lanes.js";
|
|
21
21
|
const stage = "merge";
|
|
@@ -226,14 +226,18 @@ export async function finishVnextMerge(context, input) {
|
|
|
226
226
|
request = requireRequest(context, request.merge_request_id);
|
|
227
227
|
}
|
|
228
228
|
const checks = effectiveMergeChecks(run, request);
|
|
229
|
-
|
|
229
|
+
if (input.retryCheckId) {
|
|
230
|
+
validateFailedCheckRetry(context, { projectId: project.id, runId: run.id, receiptId: input.retryCheckId, declarations: checks, artifactRoot: path.join(requireHome(run), stageDir), reason: input.retryReason });
|
|
231
|
+
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "check_retry_requested", check_receipt_id: input.retryCheckId, stage, reason: input.retryReason ?? "operator retry" });
|
|
232
|
+
}
|
|
233
|
+
const unchanged = unchangedFinalGateFailures(context, { projectId: project.id, runId: run.id, workspaceRoot: request.target_workspace, declarations: checks, ...(input.retryCheckId ? { retryReceiptId: input.retryCheckId } : {}) });
|
|
230
234
|
if (unchanged.length)
|
|
231
|
-
throw new AppError("merge_check_repair_required", "MERGE checks already failed for the unchanged integrated tree", 2, { failures: unchanged });
|
|
235
|
+
throw new AppError("merge_check_repair_required", "MERGE checks already failed for the unchanged integrated tree; choose source repair or restore the environment and retry that receipt", 2, { failures: unchanged, retry_command: `${finishCommand(context, run.id, request, projectRoot)} --retry-check ${unchanged[0].id} --reason "<environment recovery evidence>"` });
|
|
232
236
|
const receipts = request.checkpoint === "checks_passed" || request.checkpoint === "delivered" || request.checkpoint === "finalized" ? checkReceipts(context, { projectId: project.id, runId: run.id, workId: request.executor_work_id }).filter((receipt) => receipt.status === "passed" && receipt.before_fingerprint === workspaceFingerprint(request.target_workspace)) : await runCodeChecks(context, { projectId: project.id, runId: run.id, runHome: requireHome(run), workspaceRoot: request.target_workspace, workId: request.executor_work_id, artifactDir: path.join(stageDir, "works", request.executor_work_id), scope: "aggregate", checks, ...(input.progress ? { progress: input.progress } : {}) });
|
|
233
237
|
const failed = receipts.filter((receipt) => receipt.status !== "passed");
|
|
234
238
|
if (failed.length) {
|
|
235
239
|
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]);
|
|
236
|
-
throw new AppError("
|
|
240
|
+
throw new AppError("merge_gate_failed", "Integrated target checks failed. Classify retained evidence: source defects use source repair; restored environment uses retry.", 2, { failures: failed, repair_command: `${flowCommand(context)} merge repair ${request.merge_request_id} --project-root ${JSON.stringify(projectRoot)} --json`, retry_command: `${finishCommand(context, run.id, request, projectRoot)} --retry-check ${failed[0].id} --reason "<environment recovery evidence>"` });
|
|
237
241
|
}
|
|
238
242
|
const requiredRefs = mergeAcceptanceRefs(run.workspace_root, JSON.parse(request.protocol_ids_json));
|
|
239
243
|
const passedRefs = new Set(receipts.flatMap((receipt) => receipt.check_refs));
|
|
@@ -310,6 +314,11 @@ export async function repairVnextMerge(context, input) {
|
|
|
310
314
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
311
315
|
const project = requireProjectByRoot(context, projectRoot);
|
|
312
316
|
const request = requireRequest(context, input.requestId);
|
|
317
|
+
if (request.project_id === project.id && request.status === "superseded" && errorCode(request) === "merge_source_repair") {
|
|
318
|
+
const saved = JSON.parse(request.last_error_json);
|
|
319
|
+
releaseMergeLane(context, projectRoot, request, `MERGE ${request.merge_request_id} moved to source repair`);
|
|
320
|
+
return { ok: true, run_id: request.run_id, merge_request_id: request.merge_request_id, status: "superseded", source_repair: { repair_work_id: saved.repair_work_id }, next: { kind: "start_stage", stage: "code", command: `${flowCommand(context)} stage start ${request.run_id} --stage code --project-root ${JSON.stringify(projectRoot)} --json --progress-jsonl` } };
|
|
321
|
+
}
|
|
313
322
|
if (request.project_id !== project.id || request.status !== "action_required" || errorCode(request) !== "merge_gate_failed") {
|
|
314
323
|
throw new AppError("invalid_merge_repair_state", "MERGE source repair requires an action-required failed integration gate", 2, { merge_request_id: input.requestId, status: request.status, code: errorCode(request) });
|
|
315
324
|
}
|
|
@@ -322,16 +331,18 @@ export async function repairVnextMerge(context, input) {
|
|
|
322
331
|
throw new AppError("runtime_missing", "MERGE failure has no durable failed receipt", 1, { merge_request_id: request.merge_request_id });
|
|
323
332
|
// Settle the child while its canonical result path still exists; the next
|
|
324
333
|
// step archives the entire MERGE attempt as immutable evidence.
|
|
325
|
-
|
|
334
|
+
const executor = context.db.get("SELECT status FROM works WHERE work_id = ?", [request.executor_work_id]);
|
|
335
|
+
if (executor?.status !== "failed")
|
|
336
|
+
await failWork(context, request.executor_work_id, `Superseded by source repair after failed integration gate ${failed[0]}.`);
|
|
326
337
|
const prepared = prepareVnextMergeSourceRepairAttempt(context, { projectRoot, runId: request.run_id, mergeRequestId: request.merge_request_id });
|
|
327
|
-
const repair = addVnextCodeRepair(context, { projectRoot, runId: request.run_id, checkReceiptId: failed[0], originWorkIds, objective: `Repair source behavior evidenced by failed integration check ${failed[0]}.` });
|
|
338
|
+
const repair = addVnextCodeRepair(context, { projectRoot, runId: request.run_id, checkReceiptId: failed[0], originWorkIds, reviewCycle: `MRR-${request.merge_request_id}`, objective: `Repair source behavior evidenced by failed integration check ${failed[0]}.` });
|
|
328
339
|
const now = context.now();
|
|
329
340
|
context.db.run("UPDATE merge_requests SET status = 'superseded', checkpoint = 'source_repair_required', last_error_json = ?, completed_at = ?, updated_at = ? WHERE merge_request_id = ?", [JSON.stringify({ code: "merge_source_repair", replacement_pending: true, failed_receipt_ids: failed, repair_work_id: repair.repair_work_id }), now, now, request.merge_request_id]);
|
|
330
341
|
releaseMergeLane(context, projectRoot, request, `MERGE ${request.merge_request_id} moved to source repair`);
|
|
331
342
|
appendFlowRunTimelineEvent(context, project.id, request.run_id, { type: "merge_source_repair_created", merge_request_id: request.merge_request_id, repair_work_id: repair.repair_work_id, cycle: prepared.cycle, failed_receipt_ids: failed });
|
|
332
343
|
return { ok: true, run_id: request.run_id, merge_request_id: request.merge_request_id, status: "superseded", source_repair: repair, next: { kind: "start_stage", stage: "code", command: `${flowCommand(context)} stage start ${request.run_id} --stage code --project-root ${JSON.stringify(projectRoot)} --json --progress-jsonl` } };
|
|
333
344
|
}
|
|
334
|
-
function mergePrompt(context, input) { const pause = `${flowCommand(context)} stage pause ${input.run.id} --stage merge --work ${input.request.executor_work_id} --project-root ${JSON.stringify(input.projectRoot)} --question-stdin --json`; const repair = `${flowCommand(context)} merge repair ${input.request.merge_request_id} --project-root ${JSON.stringify(input.projectRoot)} --json`; return ["<stage_identity>", `- RUN: ${input.run.id}`, `- MERGE request: ${input.request.merge_request_id}`, `- Work: ${input.request.executor_work_id}`, "- stage: merge", "</stage_identity>", "", "<trusted_runtime_context>", `- integration workspace: ${input.request.target_workspace}`, `- source workspace: ${input.request.source_workspace}`, `- frozen source commit: ${input.request.source_commit}`, `- target branch: ${input.request.target_branch}`, `- execution target baseline: ${input.request.execution_target_head}`, `- queue route: ${input.request.execution_route}`, `- delivery: ${JSON.stringify(executionSettings(input.run).merge_delivery)}`, `- cleanup: ${JSON.stringify(executionSettings(input.run).merge_cleanup)}`, "These facts and the acquired project integration lane were established by dd-flow. Do not repeat discovery and do not run git merge/rebase/squash yourself.", "</trusted_runtime_context>", "", "<effective_merge_gate>", ...effectiveMergeChecks(input.run, input.request).map((check) => `- ${check.canonical_ref ?? check.id}: ${check.command} — ${check.purpose}`), "</effective_merge_gate>", "", "<execution_contract>", `1. Run this exact standalone command first: ${applyCommand(context, input.request, input.projectRoot)}`, "2. If it reports conflicts, resolve only the actual unmerged paths in the integration workspace. Do not repeat merge apply and do not edit product code, tests, documentation or configuration merely to make a gate pass.", `3. Write the compact semantic result to ${input.resultPath}:`, "```json", JSON.stringify({ schema_id: "dd-flow/merge-result@1", outcome: "completed", summary: "What was integrated.", conflict_resolution: "How material conflicts were resolved, or empty when none.", verification_summary: "Why the integrated result is ready for deterministic checks.", residual_risks: [] }, null, 2), "```", `4. Finish with this exact standalone command and wait for all progress: ${finishCommand(context, input.run.id, input.request, input.projectRoot)}`, `If a gate fails,
|
|
345
|
+
function mergePrompt(context, input) { const pause = `${flowCommand(context)} stage pause ${input.run.id} --stage merge --work ${input.request.executor_work_id} --project-root ${JSON.stringify(input.projectRoot)} --question-stdin --json`; const repair = `${flowCommand(context)} merge repair ${input.request.merge_request_id} --project-root ${JSON.stringify(input.projectRoot)} --json`; return ["<stage_identity>", `- RUN: ${input.run.id}`, `- MERGE request: ${input.request.merge_request_id}`, `- Work: ${input.request.executor_work_id}`, "- stage: merge", "</stage_identity>", "", "<trusted_runtime_context>", `- integration workspace: ${input.request.target_workspace}`, `- source workspace: ${input.request.source_workspace}`, `- frozen source commit: ${input.request.source_commit}`, `- target branch: ${input.request.target_branch}`, `- execution target baseline: ${input.request.execution_target_head}`, `- queue route: ${input.request.execution_route}`, `- delivery: ${JSON.stringify(executionSettings(input.run).merge_delivery)}`, `- cleanup: ${JSON.stringify(executionSettings(input.run).merge_cleanup)}`, "These facts and the acquired project integration lane were established by dd-flow. Do not repeat discovery and do not run git merge/rebase/squash yourself.", "</trusted_runtime_context>", "", "<effective_merge_gate>", ...effectiveMergeChecks(input.run, input.request).map((check) => `- ${check.canonical_ref ?? check.id}: ${check.command} — ${check.purpose}`), "</effective_merge_gate>", "", "<execution_contract>", `1. Run this exact standalone command first: ${applyCommand(context, input.request, input.projectRoot)}`, "2. If it reports conflicts, resolve only the actual unmerged paths in the integration workspace. Do not repeat merge apply and do not edit product code, tests, documentation or configuration merely to make a gate pass.", `3. Write the compact semantic result to ${input.resultPath}:`, "```json", JSON.stringify({ schema_id: "dd-flow/merge-result@1", outcome: "completed", summary: "What was integrated.", conflict_resolution: "How material conflicts were resolved, or empty when none.", verification_summary: "Why the integrated result is ready for deterministic checks.", residual_risks: [] }, null, 2), "```", `4. Finish with this exact standalone command and wait for all progress: ${finishCommand(context, input.run.id, input.request, input.projectRoot)}`, `If a gate fails, read its receipt and logs and determine the cause. For a product defect use ${repair}; it restores the target baseline and opens CODE → independent CODE-REVIEW → replacement MRG. For an environment failure restore the declared environment in this MERGE and use the returned finish command with --retry-check <receipt-id> --reason "<what was restored>". The CLI reruns the real gate and retains the old receipt. Never edit product code in the integration target to make a gate pass.`, "If a material conflict has no reasonable answer in accepted evidence, pause this same Work with the exact heredoc below, ask the returned user_message, then use the exact resume command returned by CLI:", "```sh", stagePauseCommandTemplate(pause), "```", "</execution_contract>", ""].join("\n"); }
|
|
335
346
|
function mergeReport(context, run, request, semantic, receipts) { const now = context.now(); const cleanup = cleanupReceiptPath(run); return { schema_id: "dd-flow/stage-report@2", run_id: run.id, stage, generated_at: now, verdict: "done", summary: semantic.summary, semantic: { result: semantic.summary, acceptance: ["source_commit_frozen", "integration_commit_created", "merge_gate_passed", "delivery_confirmed"], changed_files: [], checks: receipts.map((item) => item.command), evidence: [applyReceiptPath(context, request), ...receipts.map((item) => item.receipt_path), ...(fs.existsSync(cleanup) ? [cleanup] : [])], next_action: "merge_completed", merge: { merge_request_id: request.merge_request_id, work_id: request.executor_work_id, protocols: JSON.parse(request.protocol_ids_json), source_commit: request.source_commit, execution_target_head: request.execution_target_head, accepted_tree: request.accepted_tree, integration_commit: request.integration_commit, route: request.execution_route, delivery: executionSettings(run).merge_delivery, cleanup: executionSettings(run).merge_cleanup, verification_summary: semantic.verification_summary, residual_risks: semantic.residual_risks } }, mechanical: { started_at: request.lock_acquired_at, finished_at: now, git: gitFacts(request.target_workspace), queue: queueStatus(context, request) }, artifacts: { json: "stage-report.json", markdown: "stage-report.md", html: "stage-report.html" }, validation: { status: "passed" } }; }
|
|
336
347
|
function effectiveMergeChecks(run, request) { return readFrozenMergeGate(path.join(requireHome(run), stageDir, "merge-gate.json"), request.merge_request_id).checks; }
|
|
337
348
|
function planChecks(workspace, protocols) { return protocols.flatMap((protocol) => { const file = path.join(workspace, ".memory-bank", "protocol", protocol, "plan.json"); if (!fs.existsSync(file))
|
|
@@ -396,7 +407,12 @@ function releaseMergeLane(context, projectRoot, request, reason) {
|
|
|
396
407
|
try {
|
|
397
408
|
releaseLaneLock(context, { projectRoot, lane: "merge", workerId: request.executor_work_id, workspacePath: request.target_workspace, reason });
|
|
398
409
|
}
|
|
399
|
-
catch {
|
|
410
|
+
catch (error) {
|
|
411
|
+
// An absent/replaced lease is already no longer ours. Storage and other
|
|
412
|
+
// failures must remain visible so a repeated operation can release it.
|
|
413
|
+
if (!(error instanceof AppError) || !["lane_lock_not_active", "lane_lock_owned_by_other_worker"].includes(error.code))
|
|
414
|
+
throw error;
|
|
415
|
+
}
|
|
400
416
|
}
|
|
401
417
|
function queueAhead(context, request) { return context.db.get("SELECT COUNT(*) AS count FROM merge_requests WHERE project_id = ? AND status NOT IN ('completed','failed','cancelled','superseded') AND (created_at < ? OR (created_at = ? AND merge_request_id < ?))", [request.project_id, request.created_at, request.created_at, request.merge_request_id])?.count ?? 0; }
|
|
402
418
|
function queueStatus(context, request) { return { position: queueAhead(context, request) + 1, requests_ahead: queueAhead(context, request), status: request.status, route: request.execution_route }; }
|
|
@@ -340,7 +340,10 @@ async function settle(context, id, status, result, progress) {
|
|
|
340
340
|
readCodeCheckProfile(run.workspace_root);
|
|
341
341
|
coordinationDrift = plannedAreaDrift(packet, result);
|
|
342
342
|
const artifactDir = path.relative(requireRunHome(run), path.dirname(link.result_path));
|
|
343
|
-
|
|
343
|
+
const requiredChecks = packet.repair?.verification_check_refs?.length
|
|
344
|
+
? uniqueChecks([...packet.checks.filter((check) => check.run_at === "work"), ...packet.repair.verification_check_refs])
|
|
345
|
+
: packet.checks.filter((check) => check.run_at === "work");
|
|
346
|
+
receipts = await runCodeChecks(context, { projectId: work.project_id, runId: work.run_id, runHome: requireRunHome(run), workspaceRoot: run.workspace_root, workId: work.work_id, artifactDir, scope: "work", checks: requiredChecks, ...(progress ? { progress } : {}) });
|
|
344
347
|
const failed = receipts.filter((receipt) => receipt.status !== "passed");
|
|
345
348
|
if (failed.length)
|
|
346
349
|
throw new AppError("work_checks_failed", "Work remains running because required checks failed", 2, { work_id: id, failures: failed, all_receipts: receipts });
|
|
@@ -365,7 +368,7 @@ async function settle(context, id, status, result, progress) {
|
|
|
365
368
|
throw error;
|
|
366
369
|
}
|
|
367
370
|
refreshRunWorkProjection(context, work.project_id, work.run_id);
|
|
368
|
-
const newlyReady = 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, start_command: workStartCommand(context, candidate) }));
|
|
371
|
+
const newlyReady = 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) }));
|
|
369
372
|
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 } : {}) });
|
|
370
373
|
for (const ready of newlyReady)
|
|
371
374
|
appendFlowRunTimelineEvent(context, work.project_id, work.run_id, { type: "work_dependency_unblocked", work_id: ready.work_id, completed_dependency: work.work_id });
|
|
@@ -496,7 +499,7 @@ function renderWorkerPrompt(context, work, run, dependencies) {
|
|
|
496
499
|
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>", "");
|
|
497
500
|
if (packet)
|
|
498
501
|
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>", "");
|
|
499
|
-
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>", "", "<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), "Do not create result.json yourself. Send the JSON to dd-flow on stdin; it atomically validates and stores the canonical receipt.", "</result_contract>", ""] : []), "<completion>", "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, "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.", `Finish as one standalone command, piping your JSON object to stdin: ${command} work finish ${work.work_id} --result-stdin --project-root ${JSON.stringify(run.project_root)} --json --progress-jsonl`, `Fail only for an evidenced external or semantic-contract blocker: ${command} work fail ${work.work_id} --reason "receipt path + exact external or semantic blocker" --project-root ${JSON.stringify(run.project_root)} --json`, "</completion>", ""].join("\n");
|
|
502
|
+
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>", "", "<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), "Do not create result.json yourself. Send the JSON to dd-flow on stdin; it atomically validates and 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, "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.", `Finish as one standalone command, piping your JSON object to stdin: ${command} work finish ${work.work_id} --result-stdin --project-root ${JSON.stringify(run.project_root)} --json --progress-jsonl`, `Fail only for an evidenced external or semantic-contract blocker: ${command} work fail ${work.work_id} --reason "receipt path + exact external or semantic blocker" --project-root ${JSON.stringify(run.project_root)} --json`, "</completion>", ""].join("\n");
|
|
500
503
|
}
|
|
501
504
|
export function resultSchemaGuidance(work, runId) {
|
|
502
505
|
const schema = work.result_schema;
|
|
@@ -658,7 +661,7 @@ export function refreshRunWorkProjection(context, projectId, runId) { refreshRun
|
|
|
658
661
|
if (fs.existsSync(obsolete))
|
|
659
662
|
fs.rmSync(obsolete);
|
|
660
663
|
} }
|
|
661
|
-
export function codeWorkGraph(context, projectId, runId) { 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))); 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, start_command: workStartCommand(context, work) })), blocked: works.filter((work) => work.status === "created" && !ready.includes(work)).map((work) => ({ work_id: work.work_id, depends_on: parseDependencies(work) })) }; }
|
|
664
|
+
export function codeWorkGraph(context, projectId, runId) { 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))); 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) })), blocked: works.filter((work) => work.status === "created" && !ready.includes(work)).map((work) => ({ work_id: work.work_id, depends_on: parseDependencies(work) })) }; }
|
|
662
665
|
function parsePayload(work) { if (!work.payload_json)
|
|
663
666
|
return null; try {
|
|
664
667
|
const value = JSON.parse(work.payload_json);
|
|
@@ -667,6 +670,7 @@ function parsePayload(work) { if (!work.payload_json)
|
|
|
667
670
|
catch {
|
|
668
671
|
return null;
|
|
669
672
|
} }
|
|
673
|
+
function uniqueChecks(checks) { return checks.filter((check, index) => checks.findIndex((candidate) => candidate.id === check.id) === index); }
|
|
670
674
|
function codePacket(work) { const value = parsePayload(work); if (value?.schema_id !== "dd-flow/code-work-packet@5")
|
|
671
675
|
return null; return value; }
|
|
672
676
|
function assertNoCycles(nodes) { const local = new Map(nodes.map((node) => [node.id, node.dependencies.filter((dependency) => nodes.some((candidate) => candidate.id === dependency))])); const active = new Set(); const done = new Set(); const visit = (id) => { if (active.has(id))
|