@deksden-com/dd-flow-cli 0.9.0-beta.14 → 0.9.0-beta.16
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 +14 -0
- package/README.md +13 -0
- package/dist/build-info.json +4 -4
- package/dist/cli/run-cli.js +17 -8
- package/dist/schemas/code-work-batch.schema.json +1 -1
- package/dist/services/code-checks.js +11 -1
- package/dist/services/hooks.js +5 -1
- package/dist/services/managed-processes.js +52 -12
- package/dist/services/runs.js +44 -5
- package/dist/services/runtime-service.js +92 -0
- package/dist/services/vnext-code-review.js +9 -4
- package/dist/services/vnext-code.js +15 -9
- package/dist/services/vnext-fanout.js +1 -1
- package/dist/services/vnext-merge.js +8 -4
- package/dist/services/work-registry.js +10 -4
- package/dist/services/workspace-bootstrap.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# @deksden-com/dd-flow-cli
|
|
2
2
|
|
|
3
|
+
## 0.9.0-beta.16
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Make repair verification causal and retryable, preserve attempt-qualified
|
|
8
|
+
evidence, expose RUN continuation from persisted lifecycle state, and keep
|
|
9
|
+
native child ownership bound to the physical harness session.
|
|
10
|
+
|
|
11
|
+
## 0.9.0-beta.15
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- Add owned temporary HTTP service supervision with allocated ports and readiness receipts. Prevent duplicate workspace bootstrap processes, retain resources when process ownership is uncertain, and share safe stop behavior between explicit stops and orphan reconciliation.
|
|
16
|
+
|
|
3
17
|
## 0.9.0-beta.14
|
|
4
18
|
|
|
5
19
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# dd-flow-cli
|
|
2
2
|
|
|
3
|
+
## Managed temporary HTTP services
|
|
4
|
+
|
|
5
|
+
For an interactive scenario, `runtime process start --run <RUN-ID>
|
|
6
|
+
--project-root <absolute-project> --command '<project command>' --ports api
|
|
7
|
+
--ready-port api --ready-path /health --json --progress-jsonl` runs a foreground
|
|
8
|
+
supervisor. The command receives `DD_FLOW_PORT_API`; use it instead of a fixed
|
|
9
|
+
port. Additional comma-separated names receive equivalent environment variables.
|
|
10
|
+
The ready event points to a receipt and logs under the RUN's `runtime/` folder.
|
|
11
|
+
Keep the tool invocation alive, run the scenario using the allocated ports,
|
|
12
|
+
then execute the exact `stop_command` in that receipt and await the supervisor.
|
|
13
|
+
Readiness is not a scenario pass. Keep semantic/scenario evidence separately.
|
|
14
|
+
Never use a broad `pkill`/`killall`. A failed owned stop retains resources.
|
|
15
|
+
|
|
3
16
|
`dd-flow-cli` is the mechanical control layer for `dd-flow` workflows.
|
|
4
17
|
|
|
5
18
|
It does not replace Memory Bank prompts and does not make product, design, merge, or verification judgments. Prompts own intent, route selection, planning depth, evidence meaning, and semantic readiness. The CLI owns explicit local state: projects, protocols, Codex session bindings, transitions, worktree records, lanes, locks, merge queue jobs, hook records, and audit events.
|
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.16",
|
|
4
|
+
"cli_commit": "8de503c90a952a8b56300a0c0367dd89be5c2436",
|
|
5
|
+
"built_at": "2026-09-05T22:05:40.605Z",
|
|
6
6
|
"built_with_canon": {
|
|
7
7
|
"version": "4.0.4",
|
|
8
|
-
"commit": "
|
|
8
|
+
"commit": "8e572d151c7f9d22c2b60122c87d9febcbfbde69",
|
|
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/run-cli.js
CHANGED
|
@@ -50,7 +50,8 @@ import { finishStage, startStage } from "../services/stage-lifecycle.js";
|
|
|
50
50
|
import { addWorkBatch, cancelWork, deleteWork, failWork, finishWork, listWorks, mutateWorkDeps, retryWork, showWork, startWork } from "../services/work-registry.js";
|
|
51
51
|
import { createEvalBootstrapSnapshot, createEvalRunSnapshot, prepareVnextSpecifyRun, restoreEvalBootstrapSnapshot, restoreEvalRunSnapshot } from "../services/eval-snapshots.js";
|
|
52
52
|
import { loadExternalStageContext } from "../services/stage-context.js";
|
|
53
|
-
import { confirmManagedProcess, finishManagedProcess, heartbeatManagedProcess, managedProcessStatus, reconcileExpiredManagedProcesses, registerManagedProcess } from "../services/managed-processes.js";
|
|
53
|
+
import { stopManagedProcess, confirmManagedProcess, finishManagedProcess, heartbeatManagedProcess, managedProcessStatus, reconcileExpiredManagedProcesses, registerManagedProcess } from "../services/managed-processes.js";
|
|
54
|
+
import { startRuntimeService } from "../services/runtime-service.js";
|
|
54
55
|
const defaultIo = {
|
|
55
56
|
stdout: process.stdout,
|
|
56
57
|
stderr: process.stderr,
|
|
@@ -517,7 +518,7 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
|
|
|
517
518
|
return getCliVersionReport();
|
|
518
519
|
}
|
|
519
520
|
if (family === "runtime") {
|
|
520
|
-
return await dispatchRuntime(context, command, parsed);
|
|
521
|
+
return await dispatchRuntime(context, command, parsed, commandProgress(io, output));
|
|
521
522
|
}
|
|
522
523
|
if (family === "canon") {
|
|
523
524
|
return dispatchCanon(context, command, parsed);
|
|
@@ -877,6 +878,10 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
|
|
|
877
878
|
const semanticFile = optionalOption(parsed, "semantic-file") ?? optionalOption(parsed, "data");
|
|
878
879
|
const resultFile = optionalOption(parsed, "result-file") ?? semanticFile;
|
|
879
880
|
const resultStdin = hasOption(parsed, "result-stdin");
|
|
881
|
+
const retryCheckId = optionalOption(parsed, "retry-check");
|
|
882
|
+
const retryReason = optionalOption(parsed, "reason");
|
|
883
|
+
if (retryCheckId && !retryReason?.trim())
|
|
884
|
+
throw new AppError("usage", "--retry-check requires --reason explaining the recovered environment", 2);
|
|
880
885
|
if (stage === "specify" && isVnextSpecifyRun(context, { projectRoot, runId })) {
|
|
881
886
|
const outcome = optionalOption(parsed, "outcome");
|
|
882
887
|
if (!outcome || !["specified", "failed", "cancelled"].includes(outcome)) {
|
|
@@ -916,17 +921,17 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
|
|
|
916
921
|
if (semanticFile || resultFile || resultStdin || hasOption(parsed, "outcome")) {
|
|
917
922
|
throw new AppError("usage", "vNext CODE finish accepts only --verification-file", 2);
|
|
918
923
|
}
|
|
919
|
-
return await finishVnextCode(context, { projectRoot, runId, verificationFile: optionalOption(parsed, "verification-file") ?? path.join("<missing>", "code-verification.json"), progress: commandProgress(io, output) });
|
|
924
|
+
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) });
|
|
920
925
|
}
|
|
921
926
|
if (stage === "code-review" && isVnextCodeReviewRun(context, { projectRoot, runId })) {
|
|
922
927
|
if (resultStdin || semanticFile || resultFile || hasOption(parsed, "outcome"))
|
|
923
928
|
throw new AppError("usage", "vNext CODE-REVIEW uses --decision-file", 2);
|
|
924
|
-
return await finishVnextCodeReview(context, { projectRoot, runId, ...(optionalOption(parsed, "decision-file") ? { decisionFile: optionalOption(parsed, "decision-file") } : {}), progress: commandProgress(io, output) });
|
|
929
|
+
return await finishVnextCodeReview(context, { projectRoot, runId, ...(optionalOption(parsed, "decision-file") ? { decisionFile: optionalOption(parsed, "decision-file") } : {}), ...(retryCheckId ? { retryCheckId, retryReason: retryReason } : {}), progress: commandProgress(io, output) });
|
|
925
930
|
}
|
|
926
931
|
if (stage === "merge" && isVnextMergeRun(context, { projectRoot, runId })) {
|
|
927
932
|
if (resultStdin || semanticFile || resultFile || hasOption(parsed, "outcome"))
|
|
928
933
|
throw new AppError("usage", "vNext MERGE uses the Work result path returned by stage start", 2);
|
|
929
|
-
return await finishVnextMerge(context, { projectRoot, runId, requestId: requiredOption(parsed, "request"), workId: requiredOption(parsed, "work"), progress: commandProgress(io, output) });
|
|
934
|
+
return await finishVnextMerge(context, { projectRoot, runId, requestId: requiredOption(parsed, "request"), workId: requiredOption(parsed, "work"), ...(retryCheckId ? { retryCheckId, retryReason: retryReason } : {}), progress: commandProgress(io, output) });
|
|
930
935
|
}
|
|
931
936
|
if (hasOption(parsed, "outcome") || hasOption(parsed, "result-file") || resultStdin) {
|
|
932
937
|
throw new AppError("usage", "--outcome, --result-file and --result-stdin are only available for vNext SPECIFY", 2);
|
|
@@ -1583,10 +1588,14 @@ function dispatchRun(context, command, parsed) {
|
|
|
1583
1588
|
}
|
|
1584
1589
|
throw new AppError("usage", `Unknown run command: ${command ?? "<empty>"}`, 2);
|
|
1585
1590
|
}
|
|
1586
|
-
async function dispatchRuntime(context, command, parsed) {
|
|
1591
|
+
async function dispatchRuntime(context, command, parsed, progress) {
|
|
1587
1592
|
if (command !== "process")
|
|
1588
|
-
throw new AppError("usage", "Usage: dd-flow runtime process <register|confirm|heartbeat|finish|status|reconcile>", 2);
|
|
1593
|
+
throw new AppError("usage", "Usage: dd-flow runtime process <start|stop|register|confirm|heartbeat|finish|status|reconcile>", 2);
|
|
1589
1594
|
const action = requiredPosition(parsed, 0, "runtime process action");
|
|
1595
|
+
if (action === "start")
|
|
1596
|
+
return await startRuntimeService(context, { projectRoot: requiredOption(parsed, "project-root"), runId: requiredOption(parsed, "run"), command: requiredOption(parsed, "command"), ports: requiredOption(parsed, "ports").split(","), readyPort: requiredOption(parsed, "ready-port"), readyPath: optionalOption(parsed, "ready-path") ?? "/", timeoutMs: optionalPositiveNumber(parsed, "ready-timeout-ms", true) ?? 30_000, progress });
|
|
1597
|
+
if (action === "stop")
|
|
1598
|
+
return { ok: true, process: await stopManagedProcess(context, { id: requiredOption(parsed, "id"), leaseToken: requiredOption(parsed, "lease-token"), ...(optionalPositiveNumber(parsed, "grace-ms", true) ? { graceMs: optionalPositiveNumber(parsed, "grace-ms", true) } : {}) }) };
|
|
1590
1599
|
if (action === "register") {
|
|
1591
1600
|
const leaseMs = optionalPositiveNumber(parsed, "lease-ms", true);
|
|
1592
1601
|
const process = registerManagedProcess(context, {
|
|
@@ -1618,7 +1627,7 @@ async function dispatchRuntime(context, command, parsed) {
|
|
|
1618
1627
|
return { ok: true, processes: managedProcessStatus(context) };
|
|
1619
1628
|
if (action === "reconcile")
|
|
1620
1629
|
return { ok: true, processes: await reconcileExpiredManagedProcesses(context, requiredOption(parsed, "owner"), optionalPositiveNumber(parsed, "grace-ms", true)) };
|
|
1621
|
-
throw new AppError("usage", "Usage: dd-flow runtime process <register|confirm|heartbeat|finish|status|reconcile>", 2);
|
|
1630
|
+
throw new AppError("usage", "Usage: dd-flow runtime process <start|stop|register|confirm|heartbeat|finish|status|reconcile>", 2);
|
|
1622
1631
|
}
|
|
1623
1632
|
function dispatchStat(context, command, parsed) {
|
|
1624
1633
|
if (command === "usage") {
|
|
@@ -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,7 +222,17 @@ 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
|
+
const receipt = checkReceipts(context, { projectId: input.projectId, runId: input.runId }).find((item) => item.id === input.receiptId);
|
|
229
|
+
if (!receipt || receipt.scope !== "aggregate" || !["failed", "aborted"].includes(receipt.status))
|
|
230
|
+
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 });
|
|
231
|
+
const declared = new Set(input.declarations.flatMap((check) => [check.id, checkRef(check)]));
|
|
232
|
+
if (!receipt.check_refs.some((ref) => declared.has(ref)))
|
|
233
|
+
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 });
|
|
234
|
+
return receipt;
|
|
235
|
+
}
|
|
226
236
|
export function checkReceipts(context, input) {
|
|
227
237
|
const where = input.workId ? "project_id = ? AND run_id = ? AND work_id = ?" : "project_id = ? AND run_id = ?";
|
|
228
238
|
const params = input.workId ? [input.projectId, input.runId, input.workId] : [input.projectId, input.runId];
|
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
|
|
@@ -7,6 +7,26 @@ export function resourceHome(context) {
|
|
|
7
7
|
return context.env?.DD_FLOW_RESOURCE_HOME ?? context.ddFlowHome ?? "/tmp/dd-flow-runtime";
|
|
8
8
|
}
|
|
9
9
|
export function registerManagedProcess(context, input) {
|
|
10
|
+
if (!input.uniqueActiveOperation)
|
|
11
|
+
return insertManagedProcess(context, input);
|
|
12
|
+
if (!input.operationId)
|
|
13
|
+
throw new Error("Unique managed operation requires operationId");
|
|
14
|
+
const db = registry(context);
|
|
15
|
+
db.exec("BEGIN IMMEDIATE");
|
|
16
|
+
try {
|
|
17
|
+
const existing = db.get("SELECT * FROM managed_processes WHERE operation_id = ? AND state IN ('starting','running','stopping','orphaned')", [input.operationId]);
|
|
18
|
+
if (existing)
|
|
19
|
+
throw Object.assign(new Error("The operation already owns a managed process; observe it instead of spawning a duplicate"), { code: "managed_operation_in_progress", details: { process_id: existing.id, operation_id: input.operationId } });
|
|
20
|
+
const result = insertManagedProcess(context, input);
|
|
21
|
+
db.exec("COMMIT");
|
|
22
|
+
return result;
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
db.exec("ROLLBACK");
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function insertManagedProcess(context, input) {
|
|
10
30
|
const db = registry(context);
|
|
11
31
|
const now = context.now();
|
|
12
32
|
const id = input.id ?? `PROC-${crypto.randomUUID()}`;
|
|
@@ -40,6 +60,9 @@ export function heartbeatManagedProcess(context, input) {
|
|
|
40
60
|
}
|
|
41
61
|
export function finishManagedProcess(context, input) {
|
|
42
62
|
const db = registry(context);
|
|
63
|
+
const existing = requireProcess(db, input.id);
|
|
64
|
+
if (existing.lease_token === input.leaseToken && existing.state === input.state)
|
|
65
|
+
return true;
|
|
43
66
|
const now = context.now();
|
|
44
67
|
const updated = db.run("UPDATE managed_processes SET state = ?, termination_reason = ?, finished_at = ?, updated_at = ?, lease_expires_at = ? WHERE id = ? AND lease_token = ? AND state IN ('starting','running','stopping','orphaned')", [input.state, input.reason ?? null, now, now, now, input.id, input.leaseToken]).changes === 1;
|
|
45
68
|
if (updated)
|
|
@@ -67,8 +90,8 @@ export function processTreeIsAlive(record) {
|
|
|
67
90
|
process.kill(-group, 0);
|
|
68
91
|
return true;
|
|
69
92
|
}
|
|
70
|
-
catch {
|
|
71
|
-
return
|
|
93
|
+
catch (error) {
|
|
94
|
+
return error.code !== "ESRCH";
|
|
72
95
|
}
|
|
73
96
|
}
|
|
74
97
|
return false;
|
|
@@ -111,6 +134,26 @@ export function claimExpiredManagedProcesses(context, ownerId) {
|
|
|
111
134
|
export function managedProcessStatus(context) {
|
|
112
135
|
return registry(context).all("SELECT * FROM managed_processes ORDER BY updated_at DESC, id");
|
|
113
136
|
}
|
|
137
|
+
export async function stopManagedProcess(context, input) {
|
|
138
|
+
const db = registry(context), record = requireProcess(db, input.id);
|
|
139
|
+
if (record.lease_token !== input.leaseToken)
|
|
140
|
+
throw new Error("Managed process lease does not match");
|
|
141
|
+
if (["stopped", "failed"].includes(record.state) && !processTreeIsAlive(record))
|
|
142
|
+
return record;
|
|
143
|
+
db.run("UPDATE managed_processes SET state = 'stopping', updated_at = ? WHERE id = ? AND lease_token = ? AND state IN ('starting','running','orphaned')", [context.now(), record.id, record.lease_token]);
|
|
144
|
+
if (processTreeIsAlive(record)) {
|
|
145
|
+
terminateOwnedProcess(record, "SIGTERM");
|
|
146
|
+
await delay(input.graceMs ?? 1_000);
|
|
147
|
+
if (processTreeIsAlive(record)) {
|
|
148
|
+
terminateOwnedProcess(record, "SIGKILL");
|
|
149
|
+
await delay(250);
|
|
150
|
+
}
|
|
151
|
+
if (processTreeIsAlive(record))
|
|
152
|
+
throw Object.assign(new Error("Owned process tree could not be safely stopped; resources retained"), { code: "process_tree_not_settled", details: { process_id: record.id } });
|
|
153
|
+
}
|
|
154
|
+
finishManagedProcess(context, { id: record.id, leaseToken: record.lease_token, state: "stopped", reason: input.reason ?? "requested_stop" });
|
|
155
|
+
return requireProcess(db, record.id);
|
|
156
|
+
}
|
|
114
157
|
/** Reconcile only records the system owns and has atomically claimed. */
|
|
115
158
|
export async function reconcileExpiredManagedProcesses(context, ownerId, graceMs = 1_000) {
|
|
116
159
|
const claimed = claimExpiredManagedProcesses(context, ownerId);
|
|
@@ -121,18 +164,15 @@ export async function reconcileExpiredManagedProcesses(context, ownerId, graceMs
|
|
|
121
164
|
outcomes.push({ id: processRecord.id, outcome: "already_stopped" });
|
|
122
165
|
continue;
|
|
123
166
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
terminateOwnedProcess(processRecord, "SIGKILL");
|
|
128
|
-
await delay(Math.min(graceMs, 250));
|
|
167
|
+
try {
|
|
168
|
+
await stopManagedProcess(context, { id: processRecord.id, leaseToken: processRecord.lease_token, graceMs, reason: "orphan_reconciled" });
|
|
169
|
+
outcomes.push({ id: processRecord.id, outcome: "stopped" });
|
|
129
170
|
}
|
|
130
|
-
|
|
171
|
+
catch (error) {
|
|
172
|
+
if (error.code !== "process_tree_not_settled")
|
|
173
|
+
throw error;
|
|
131
174
|
outcomes.push({ id: processRecord.id, outcome: "kill_failed" });
|
|
132
|
-
continue;
|
|
133
175
|
}
|
|
134
|
-
finishManagedProcess(context, { id: processRecord.id, leaseToken: processRecord.lease_token, state: "stopped", reason: "orphan_reconciled" });
|
|
135
|
-
outcomes.push({ id: processRecord.id, outcome: "stopped" });
|
|
136
176
|
}
|
|
137
177
|
return outcomes;
|
|
138
178
|
}
|
|
@@ -172,7 +212,7 @@ function releasePortClaims(db, claims) { for (const claim of claims)
|
|
|
172
212
|
function terminateOwnedProcess(record, signal) {
|
|
173
213
|
// Never signal a group merely because its former leader PID was once ours.
|
|
174
214
|
// PID reuse or a departed leader makes group ownership unprovable.
|
|
175
|
-
if (!record.pid || !record.pid_started_at ||
|
|
215
|
+
if (!record.pid || !record.pid_started_at || processStartedAt(record.pid) !== record.pid_started_at)
|
|
176
216
|
return;
|
|
177
217
|
const group = parseMetadata(record.metadata_json).process_group_id;
|
|
178
218
|
try {
|
package/dist/services/runs.js
CHANGED
|
@@ -229,7 +229,33 @@ export function getFlowRunStatus(context, input) {
|
|
|
229
229
|
const run = resolveRun(context, project.id, input.runId);
|
|
230
230
|
const index = authoritativeIndex(run);
|
|
231
231
|
const runtime = readRuntimeSnapshot(run.runtime_path);
|
|
232
|
-
return { ok: true, run: flowRunSummary(run), index, ...(runtime ? { runtime } : {}), flow_guidance: guidanceForRun(context, run, index) };
|
|
232
|
+
return { ok: true, run: flowRunSummary(run), index, continuation: runContinuation(context, run, index), ...(runtime ? { runtime } : {}), flow_guidance: guidanceForRun(context, run, index) };
|
|
233
|
+
}
|
|
234
|
+
/** A read-only projection of the next legal lifecycle action.
|
|
235
|
+
* It is derived from the persisted RUN/Work facts so controllers never infer a
|
|
236
|
+
* transition from a model's prose or a hard-coded stage array. */
|
|
237
|
+
function runContinuation(context, run, index) {
|
|
238
|
+
if (["done", "failed", "cancelled"].includes(run.status))
|
|
239
|
+
return { kind: "terminal", stage: null, attempt: null, work_id: null, reason: run.status, command: null };
|
|
240
|
+
const active = index.stage_runs.find((stage) => !["done", "skipped"].includes(stage.status));
|
|
241
|
+
if (active) {
|
|
242
|
+
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]);
|
|
243
|
+
const kind = active.status === "running" ? "continue_stage" : active.status === "paused" || active.status === "waiting_for_user" ? "paused" : "blocked";
|
|
244
|
+
return { kind, stage: active.stage, attempt: active.attempt ?? null, work_id: active.pause?.work_id ?? runningWork?.work_id ?? null, reason: active.status, command: null };
|
|
245
|
+
}
|
|
246
|
+
if (run.status === "waiting_for_user" || run.status === "paused")
|
|
247
|
+
return { kind: "paused", stage: index.current_stage ?? null, attempt: null, work_id: null, reason: run.status, command: null };
|
|
248
|
+
if (run.status === "blocked")
|
|
249
|
+
return { kind: "blocked", stage: index.current_stage ?? null, attempt: null, work_id: null, reason: run.status, command: null };
|
|
250
|
+
if (run.flow_kind === "vnext_specify" || run.flow_kind === "vnext_protocolize") {
|
|
251
|
+
const completed = index.stage_runs.filter((stage) => stage.status === "done" || stage.status === "skipped").sort((left, right) => right.order - left.order)[0];
|
|
252
|
+
const next = completed ? vnextStages.find((stage) => stage.id === completed.stage)?.next.find((candidate) => candidate !== "plan-review" || index.settings?.plan_review?.mode !== "off") : "specify";
|
|
253
|
+
if (!next)
|
|
254
|
+
return { kind: "terminal", stage: null, attempt: null, work_id: null, reason: "flow_complete", command: null };
|
|
255
|
+
const command = `DD_FLOW_HOME=${JSON.stringify(context.ddFlowHome)} dd-flow stage start ${run.id} --stage ${next} --project-root ${JSON.stringify(run.project_root)} --json`;
|
|
256
|
+
return { kind: "start_stage", stage: next, attempt: null, work_id: null, reason: "legal_successor", command };
|
|
257
|
+
}
|
|
258
|
+
return { kind: "wait", stage: index.current_stage ?? null, attempt: null, work_id: null, reason: "no_vnext_continuation", command: null };
|
|
233
259
|
}
|
|
234
260
|
export function getFlowRunConfig(context, input) {
|
|
235
261
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
@@ -470,7 +496,7 @@ export function attachFlowRunStage(context, input) {
|
|
|
470
496
|
const existing = index.stage_runs.find((item) => item.stage === stage);
|
|
471
497
|
assertVnextStageStart(run, index, stage, status, existing);
|
|
472
498
|
if (status === "running" && existing) {
|
|
473
|
-
archiveExistingStageAttempt(runArtifactRoot(run), dir);
|
|
499
|
+
archiveExistingStageAttempt(context, project.id, run.id, runArtifactRoot(run), dir);
|
|
474
500
|
}
|
|
475
501
|
const stageRun = {
|
|
476
502
|
...(existing ?? { order: index.stage_runs.length + 1 }),
|
|
@@ -776,7 +802,7 @@ export function prepareVnextMergeSourceRepairAttempt(context, input) {
|
|
|
776
802
|
}
|
|
777
803
|
const root = runArtifactRoot(run);
|
|
778
804
|
for (const stage of ["code", "code-review", "merge"])
|
|
779
|
-
archiveExistingStageAttempt(root, vnextStageDirectory(stage));
|
|
805
|
+
archiveExistingStageAttempt(context, project.id, run.id, root, vnextStageDirectory(stage));
|
|
780
806
|
const now = context.now();
|
|
781
807
|
const restarted = {
|
|
782
808
|
...code,
|
|
@@ -1263,7 +1289,7 @@ function upsertStage(index, stageRun) {
|
|
|
1263
1289
|
}
|
|
1264
1290
|
index.stage_runs.sort((a, b) => a.order - b.order);
|
|
1265
1291
|
}
|
|
1266
|
-
function resolveRun(context, projectId, idOrAlias) {
|
|
1292
|
+
export function resolveRun(context, projectId, idOrAlias) {
|
|
1267
1293
|
if (isFullEntityId(idOrAlias)) {
|
|
1268
1294
|
return requireRunById(context, projectId, idOrAlias);
|
|
1269
1295
|
}
|
|
@@ -1391,7 +1417,9 @@ function resolveWorkspaceRoot(workspaceRoot) {
|
|
|
1391
1417
|
}
|
|
1392
1418
|
return fs.realpathSync(absolute);
|
|
1393
1419
|
}
|
|
1394
|
-
|
|
1420
|
+
/** Move one completed stage attempt and update only known durable path columns.
|
|
1421
|
+
* Receipt bytes remain immutable; readers resolve their new location through the DB. */
|
|
1422
|
+
function archiveExistingStageAttempt(context, projectId, runId, runRoot, stageDir) {
|
|
1395
1423
|
const currentStageDir = path.join(runRoot, stageDir);
|
|
1396
1424
|
if (!fs.existsSync(currentStageDir) || !fs.statSync(currentStageDir).isDirectory()) {
|
|
1397
1425
|
return;
|
|
@@ -1409,6 +1437,17 @@ function archiveExistingStageAttempt(runRoot, stageDir) {
|
|
|
1409
1437
|
for (const entry of entries) {
|
|
1410
1438
|
fs.renameSync(path.join(currentStageDir, entry), path.join(archiveDir, entry));
|
|
1411
1439
|
}
|
|
1440
|
+
const oldPrefix = `${currentStageDir}${path.sep}`;
|
|
1441
|
+
const newPrefix = `${archiveDir}${path.sep}`;
|
|
1442
|
+
const rewrite = (table, column, where, values) => {
|
|
1443
|
+
context.db.run(`UPDATE ${table} SET ${column} = ? || substr(${column}, ?) WHERE ${column} LIKE ? AND ${where}`, [newPrefix, oldPrefix.length + 1, `${oldPrefix}%`, ...values]);
|
|
1444
|
+
};
|
|
1445
|
+
const workScope = "work_id IN (SELECT work_id FROM works WHERE project_id = ? AND run_id = ?)";
|
|
1446
|
+
rewrite("work_sessions", "prompt_path", workScope, [projectId, runId]);
|
|
1447
|
+
rewrite("work_sessions", "result_path", workScope, [projectId, runId]);
|
|
1448
|
+
const receiptScope = "project_id = ? AND run_id = ?";
|
|
1449
|
+
for (const column of ["stdout_path", "stderr_path", "receipt_path"])
|
|
1450
|
+
rewrite("check_receipts", column, receiptScope, [projectId, runId]);
|
|
1412
1451
|
}
|
|
1413
1452
|
function writeJsonFile(file, value) {
|
|
1414
1453
|
const tmpFile = `${file}.tmp-${process.pid}-${Date.now()}`;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import crypto from "node:crypto";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { AppError } from "../shared/errors.js";
|
|
5
|
+
import { requireProjectByRoot } from "./projects.js";
|
|
6
|
+
import { resolveRun } from "./runs.js";
|
|
7
|
+
import { codeExecutionEnvironment, runCheck } from "./code-checks.js";
|
|
8
|
+
import { confirmManagedProcess, finishManagedProcess, heartbeatManagedProcess, managedProcessStatus, processTreeIsAlive, registerManagedProcess, reservePorts, resourceHome, stopManagedProcess } from "./managed-processes.js";
|
|
9
|
+
/** Foreground supervisor: the tool may yield while this command owns the service.
|
|
10
|
+
* Its ready receipt is emitted immediately; a separate stop ends this command.
|
|
11
|
+
* Readiness is infrastructure evidence, never a successful scenario verdict. */
|
|
12
|
+
export async function startRuntimeService(context, input) {
|
|
13
|
+
if (!Number.isFinite(input.timeoutMs) || input.timeoutMs <= 0)
|
|
14
|
+
throw new AppError("validation", "Readiness timeout must be positive and finite", 2);
|
|
15
|
+
if (!input.command.trim() || !input.ports.length || input.ports.some(name => !/^[a-z][a-z0-9_]*$/.test(name)) || new Set(input.ports).size !== input.ports.length || !input.ports.includes(input.readyPort) || !input.readyPath.startsWith("/") || input.readyPath.startsWith("//"))
|
|
16
|
+
throw new AppError("validation", "Declare unique lowercase port names, a ready-port from that list and a local /ready-path", 2);
|
|
17
|
+
const project = requireProjectByRoot(context, path.resolve(input.projectRoot));
|
|
18
|
+
const run = resolveRun(context, project.id, input.runId);
|
|
19
|
+
if (!run?.run_root || !run.workspace_root)
|
|
20
|
+
throw new AppError("not_found", "Registered RUN with a materialized workspace is required", 2);
|
|
21
|
+
input = { ...input, runId: run.id };
|
|
22
|
+
const id = `PROC-${crypto.randomUUID()}`;
|
|
23
|
+
const directory = path.join(run.run_root, "runtime", id);
|
|
24
|
+
fs.mkdirSync(directory, { recursive: true });
|
|
25
|
+
const record = registerManagedProcess(context, { id, kind: "runtime-service", ownerId: `run:${input.runId}`, ownerPid: process.pid, runId: input.runId, projectId: project.id, stdoutPath: path.join(directory, "stdout.log"), stderrPath: path.join(directory, "stderr.log"), metadata: { command: input.command } });
|
|
26
|
+
const receiptPath = path.join(directory, "service.json");
|
|
27
|
+
let stdout, stderr;
|
|
28
|
+
let settled = false, ready = false;
|
|
29
|
+
const receipt = { schema_id: "dd-flow/runtime-service@1", process_id: record.id, run_id: input.runId, status: "starting", started_at: context.now(), receipt_path: receiptPath, stdout_path: path.join(directory, "stdout.log"), stderr_path: path.join(directory, "stderr.log") };
|
|
30
|
+
const save = () => { const temporary = `${receiptPath}.${process.pid}.tmp`; fs.writeFileSync(temporary, JSON.stringify(receipt, null, 2)); fs.renameSync(temporary, receiptPath); };
|
|
31
|
+
try {
|
|
32
|
+
const allocation = await reservePorts(context, { ownerId: record.owner_id, processId: record.id, names: input.ports });
|
|
33
|
+
const environment = { ...codeExecutionEnvironment(run.workspace_root), ...Object.fromEntries(Object.entries(allocation.ports).map(([name, port]) => [`DD_FLOW_PORT_${name.toUpperCase()}`, String(port)])), DD_FLOW_EVIDENCE_DIR: directory, DD_FLOW_CHECK_COMPLETION_FILE: path.join(directory, "completion.json") };
|
|
34
|
+
const quoted = (value) => `'${value.replaceAll("'", "'\\''")}'`;
|
|
35
|
+
Object.assign(receipt, { ports: allocation.ports, environment: Object.fromEntries(Object.entries(environment).filter(([key]) => key.startsWith("DD_FLOW_PORT_"))), stop_command: `DD_FLOW_HOME=${quoted(context.ddFlowHome)} DD_FLOW_RESOURCE_HOME=${quoted(resourceHome(context))} dd-flow runtime process stop --id ${record.id} --lease-token ${record.lease_token} --json` });
|
|
36
|
+
save();
|
|
37
|
+
stdout = fs.openSync(String(receipt.stdout_path), "a");
|
|
38
|
+
stderr = fs.openSync(String(receipt.stderr_path), "a");
|
|
39
|
+
const running = runCheck(input.command, run.workspace_root, environment, stdout, stderr, elapsed => input.progress?.(`service ${record.id} alive (${elapsed}s); receipt: ${receiptPath}`), pid => confirmManagedProcess(context, { id: record.id, leaseToken: record.lease_token, pid, processGroupId: process.platform === "win32" ? null : pid }), () => heartbeatManagedProcess(context, { id: record.id, leaseToken: record.lease_token }));
|
|
40
|
+
void running.then(() => { settled = true; });
|
|
41
|
+
const deadline = performance.now() + input.timeoutMs;
|
|
42
|
+
while (!settled && performance.now() < deadline) {
|
|
43
|
+
try {
|
|
44
|
+
const response = await fetch(`http://127.0.0.1:${allocation.ports[input.readyPort]}${input.readyPath}`, { signal: AbortSignal.timeout(1_000), redirect: "error" });
|
|
45
|
+
await response.body?.cancel();
|
|
46
|
+
ready = response.ok;
|
|
47
|
+
}
|
|
48
|
+
catch { /* Startup is observed until the declared deadline. */ }
|
|
49
|
+
if (ready)
|
|
50
|
+
break;
|
|
51
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
52
|
+
}
|
|
53
|
+
if (!ready) {
|
|
54
|
+
await stopManagedProcess(context, { id: record.id, leaseToken: record.lease_token });
|
|
55
|
+
await running;
|
|
56
|
+
throw new AppError("service_not_ready", "Service did not pass its declared HTTP readiness check; inspect retained logs", 2, { receipt_path: receiptPath });
|
|
57
|
+
}
|
|
58
|
+
Object.assign(receipt, { status: "ready", ready_at: context.now() });
|
|
59
|
+
save();
|
|
60
|
+
input.progress?.(`service ready: ${JSON.stringify(receipt)}; keep this supervisor running, use its exact stop command when finished`);
|
|
61
|
+
const outcome = await running;
|
|
62
|
+
const current = managedProcessStatus(context).find(item => item.id === record.id);
|
|
63
|
+
if (current.state === "stopping") {
|
|
64
|
+
const settleBy = performance.now() + 5_000;
|
|
65
|
+
while (processTreeIsAlive(current) && performance.now() < settleBy)
|
|
66
|
+
await new Promise(resolve => setTimeout(resolve, 25));
|
|
67
|
+
}
|
|
68
|
+
if (processTreeIsAlive(current))
|
|
69
|
+
throw new AppError("process_tree_not_settled", "Service still owns live children; retain resources", 2, { process_id: record.id });
|
|
70
|
+
if (!["stopped", "failed"].includes(current.state))
|
|
71
|
+
finishManagedProcess(context, { id: record.id, leaseToken: record.lease_token, state: current.state === "stopping" || outcome.exitCode === 0 ? "stopped" : "failed", reason: current.state === "stopping" ? "requested_stop" : outcome.error });
|
|
72
|
+
Object.assign(receipt, { status: managedProcessStatus(context).find(item => item.id === record.id).state, finished_at: context.now(), exit_code: outcome.exitCode });
|
|
73
|
+
save();
|
|
74
|
+
if (receipt.status === "failed")
|
|
75
|
+
throw new AppError("service_failed", "Service exited unsuccessfully; inspect retained logs", 2, receipt);
|
|
76
|
+
return receipt;
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
const current = managedProcessStatus(context).find(item => item.id === record.id);
|
|
80
|
+
if (!processTreeIsAlive(current))
|
|
81
|
+
finishManagedProcess(context, { id: record.id, leaseToken: record.lease_token, state: "failed", reason: error instanceof Error ? error.message : String(error) });
|
|
82
|
+
Object.assign(receipt, { status: "failed", error: error instanceof Error ? error.message : String(error), cleanup_pending: processTreeIsAlive(current) });
|
|
83
|
+
save();
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
finally {
|
|
87
|
+
if (stdout !== undefined)
|
|
88
|
+
fs.closeSync(stdout);
|
|
89
|
+
if (stderr !== undefined)
|
|
90
|
+
fs.closeSync(stderr);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -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 });
|
|
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, 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 });
|
|
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 });
|
|
@@ -330,7 +335,11 @@ export function addVnextCodeRepair(context, input) {
|
|
|
330
335
|
origin_work_ids: origins.map((work) => work.work_id),
|
|
331
336
|
...(receipt ? { check_receipt_id: receipt.id, failure_receipt_path: receipt.receipt_path } : {}),
|
|
332
337
|
...(input.reviewFindings?.length ? { review_findings: input.reviewFindings } : {}),
|
|
333
|
-
...(semanticRepair ? { semantic_unresolved: unique(input.semanticUnresolved ?? []), verification_path: input.verificationPath } : {})
|
|
338
|
+
...(semanticRepair ? { semantic_unresolved: unique(input.semanticUnresolved ?? []), verification_path: input.verificationPath } : {}),
|
|
339
|
+
// `run_at` remains the accepted aggregate placement. This separate
|
|
340
|
+
// repair obligation is deliberately executed before this repair Work is
|
|
341
|
+
// accepted, so a local fix cannot close without re-proving its cause.
|
|
342
|
+
verification_check_refs: selectRepairChecks({ ...(failedCheck ? { failedCheck } : {}), ...(input.reviewChecks ? { reviewChecks: input.reviewChecks } : {}), ...(semanticRepair ? { semanticChecks: declaredChecks } : {}) })
|
|
334
343
|
},
|
|
335
344
|
task: input.objective,
|
|
336
345
|
semantic_spine: {
|
|
@@ -384,11 +393,8 @@ export function addVnextCodeRepair(context, input) {
|
|
|
384
393
|
refreshRunWorkProjection(context, project.id, run.id);
|
|
385
394
|
return { ok: true, run_id: run.id, repair_work_id: id, start_command: workStartCommand(context, work) };
|
|
386
395
|
}
|
|
387
|
-
/** Retain the causal declaration for worker context; run_at
|
|
396
|
+
/** Retain the causal declaration for worker context; run_at still records its original gate. */
|
|
388
397
|
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
398
|
const candidates = input.failedCheck ? [input.failedCheck] : input.reviewChecks ?? input.semanticChecks ?? [];
|
|
393
399
|
const checks = candidates.map((check) => ({ ...check, purpose: `${input.semanticChecks ? "Prove the CODE verification repair" : "Prove the CODE-REVIEW repair"}: ${check.purpose}` }));
|
|
394
400
|
return uniqueBy(checks, (check) => check.id);
|
|
@@ -66,7 +66,7 @@ export function getVnextFanoutStatus(context, input) {
|
|
|
66
66
|
run_key: subagentCapacityKey,
|
|
67
67
|
available_slots: capacityKnown ? available : null
|
|
68
68
|
},
|
|
69
|
-
works: { ...counts, ready: ready.works.map((work) => ({ work_id: work.work_id, task: work.task, start_command: work.start_command })) }
|
|
69
|
+
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
70
|
}
|
|
71
71
|
};
|
|
72
72
|
}
|
|
@@ -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 });
|
|
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));
|
|
@@ -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 });
|
|
@@ -494,7 +497,9 @@ function renderWorkerPrompt(context, work, run, dependencies) {
|
|
|
494
497
|
const codeContext = packet ? ["<semantic_spine>", JSON.stringify(packet.semantic_spine, null, 2), "</semantic_spine>", "", ...(packet.repair ? ["<repair_context>", JSON.stringify(packet.repair, null, 2), "Read the failed receipt and its linked stdout/stderr before editing. Preserve the accepted origin context and fix only the evidenced failure.", "</repair_context>", ""] : []), "<accepted_requirements>", JSON.stringify(packet.requirements, null, 2), "</accepted_requirements>", "", "<write_boundary_invariant>", "For a mutation guarded by membership, ownership, authorization or parent lifecycle state, preserve that predicate in the write statement or make guard and write one explicit transaction with the needed lock. A prior read may diagnose an error but never proves a later write remains allowed. Apply this to create, update, delete and parent-state mutations.", "</write_boundary_invariant>", "", "<acceptance_context>", "The criteria below are end-to-end context. Complete this Work's semantic contribution and declared checks; another ordered Work may own a different acceptance surface.", JSON.stringify(packet.acceptance, null, 2), "</acceptance_context>", "", "<required_read>", "These are mandatory starting sources, not a read allowlist. Read any additional project files needed to implement the Work correctly.", ...packet.required_read.map((item) => `- ${resolveRunReferences(item, work.run_id, requireRunHome(run))}`), "</required_read>", "", "<discovery_boundary>", "These are likely discovery areas, not a hard boundary. Expand project-local investigation when required and report material additions.", ...packet.discovery_boundary.map((item) => `- ${item}`), "</discovery_boundary>", "", "<planned_write_areas>", "SOFT COORDINATION HINT ONLY. These paths help the coordinator avoid concurrent collisions. They do not grant or deny write permission and do not limit the files needed for this Work. You may create or change any project file under workspace_root that is necessary and in semantic scope; report every actual changed path.", ...(packet.planned_write_areas.length ? packet.planned_write_areas.map((item) => `- ${item}`) : ["- none predicted; derive the necessary files from the task"]), "</planned_write_areas>", "", ...(packet.provides_checks.length ? ["<provided_checks>", ...packet.provides_checks.map((item) => `- ${item.id}: materialize ${item.command}${item.definition ? ` as ${item.definition}` : ""}; it is not usable until this Work finishes.`), "Update the declared project command or alias before Work finish. The CLI verifies materialization at Work finish. Execution occurs at its declared run_at gate, not necessarily in this Work.", "</provided_checks>", ""] : []), "<verification>", ...packet.checks.map((item) => `- ${item.id} at ${item.run_at}: ${item.command} — ${item.purpose}`), "The CLI executes work-scoped checks and retains their receipts. Report semantic evidence only; do not rerun declared checks manually.", "</verification>", "", "<stop_conditions>", ...packet.stop_conditions.map((item) => `- ${item}`), "</stop_conditions>", ""] : [];
|
|
495
498
|
if (packet)
|
|
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)
|
|
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>", "");
|
|
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");
|
|
498
503
|
}
|
|
499
504
|
export function resultSchemaGuidance(work, runId) {
|
|
500
505
|
const schema = work.result_schema;
|
|
@@ -656,7 +661,7 @@ export function refreshRunWorkProjection(context, projectId, runId) { refreshRun
|
|
|
656
661
|
if (fs.existsSync(obsolete))
|
|
657
662
|
fs.rmSync(obsolete);
|
|
658
663
|
} }
|
|
659
|
-
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) })) }; }
|
|
660
665
|
function parsePayload(work) { if (!work.payload_json)
|
|
661
666
|
return null; try {
|
|
662
667
|
const value = JSON.parse(work.payload_json);
|
|
@@ -665,6 +670,7 @@ function parsePayload(work) { if (!work.payload_json)
|
|
|
665
670
|
catch {
|
|
666
671
|
return null;
|
|
667
672
|
} }
|
|
673
|
+
function uniqueChecks(checks) { return checks.filter((check, index) => checks.findIndex((candidate) => candidate.id === check.id) === index); }
|
|
668
674
|
function codePacket(work) { const value = parsePayload(work); if (value?.schema_id !== "dd-flow/code-work-packet@5")
|
|
669
675
|
return null; return value; }
|
|
670
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))
|
|
@@ -27,7 +27,7 @@ export async function runWorkspaceBootstrap(context, input) {
|
|
|
27
27
|
const directory = path.join(input.runHome, input.artifactDir, `bootstrap-${crypto.randomUUID()}`);
|
|
28
28
|
fs.mkdirSync(directory, { recursive: true });
|
|
29
29
|
const stdoutPath = path.join(directory, "stdout.log"), stderrPath = path.join(directory, "stderr.log"), completionPath = path.join(directory, "completion.json");
|
|
30
|
-
const managed = registerManagedProcess(context, { kind: "workspace-bootstrap", ownerId: operationId, ownerPid: process.pid, operationId, projectId: input.projectId, runId: input.runId, stdoutPath, stderrPath, metadata: { command: input.command, workspace_root: input.workspaceRoot } });
|
|
30
|
+
const managed = registerManagedProcess(context, { kind: "workspace-bootstrap", ownerId: operationId, ownerPid: process.pid, operationId, uniqueActiveOperation: true, projectId: input.projectId, runId: input.runId, stdoutPath, stderrPath, metadata: { command: input.command, workspace_root: input.workspaceRoot } });
|
|
31
31
|
const stdout = fs.openSync(stdoutPath, "a"), stderr = fs.openSync(stderrPath, "a");
|
|
32
32
|
try {
|
|
33
33
|
input.progress?.(`workspace bootstrap started: ${input.command}; logs: ${directory}`);
|