@deksden-com/dd-flow-cli 0.9.0-beta.152 → 0.9.0-beta.155
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 +23 -0
- package/dist/build-info.json +8 -8
- package/dist/cli/command-inputs.js +33 -3
- package/dist/cli/command-metadata.js +73 -2
- package/dist/cli/delivery.js +108 -0
- package/dist/cli/diagnostic-invocation.js +23 -7
- package/dist/cli/diagnostic.js +14 -3
- package/dist/cli/help.js +117 -1
- package/dist/cli/hook-ingress.js +36 -49
- package/dist/cli/interactive-invocation.js +33 -0
- package/dist/cli/interactive.js +114 -0
- package/dist/cli/memory-fix-invocation.js +52 -0
- package/dist/cli/memory-fix.js +100 -0
- package/dist/cli/memory-flow-admission.js +7 -3
- package/dist/cli/memory-init.js +5 -4
- package/dist/cli/native-hook-ingress.js +5 -1
- package/dist/cli/native-hook-preparation.js +31 -4
- package/dist/cli/run-cli.js +273 -19
- package/dist/cli.js +17 -3
- package/dist/domain/delivery-flows.js +8 -0
- package/dist/domain/distill-flow.js +20 -0
- package/dist/domain/interactive-flow.js +1 -0
- package/dist/domain/memory-fix-flow.js +2 -0
- package/dist/domain/review-fix-flow.js +2 -0
- package/dist/domain/source-snapshot-flows.js +4 -0
- package/dist/harness-runtime/bin/dd-agy.mjs +18 -3
- package/dist/harness-runtime/bin/dd-grok.mjs +15 -5
- package/dist/harness-runtime/bin/dd-zcode.mjs +15 -7
- package/dist/harness-runtime/lib/agy-error-boundary.mjs +18 -10
- package/dist/harness-runtime/lib/agy-native-inspection.mjs +12 -3
- package/dist/harness-runtime/lib/agy-native-inventory.mjs +19 -0
- package/dist/harness-runtime/lib/agy-terminal-envelope.mjs +13 -6
- package/dist/harness-runtime/lib/agy-transcript-stream.mjs +54 -0
- package/dist/harness-runtime/lib/daemon-observation.d.mts +1 -0
- package/dist/harness-runtime/lib/daemon-observation.mjs +12 -4
- package/dist/harness-runtime/lib/daemon-operations.mjs +20 -5
- package/dist/harness-runtime/lib/dd-agy-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +371 -114
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +43 -3
- package/dist/harness-runtime/lib/dd-codex.mjs +36 -4
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +25 -11
- package/dist/harness-runtime/lib/dd-droid.mjs +124 -11
- package/dist/harness-runtime/lib/dd-grok-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +137 -25
- package/dist/harness-runtime/lib/dd-grok.mjs +4 -2
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +114 -16
- package/dist/harness-runtime/lib/dd-opencode.mjs +10 -7
- package/dist/harness-runtime/lib/dd-zcode-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +115 -22
- package/dist/harness-runtime/lib/dd-zcode.mjs +75 -36
- package/dist/harness-runtime/lib/delegation-instructions.d.mts +1 -0
- package/dist/harness-runtime/lib/delegation-instructions.mjs +10 -3
- package/dist/harness-runtime/lib/delivery-command.mjs +84 -0
- package/dist/harness-runtime/lib/driver-recovery.mjs +0 -19
- package/dist/harness-runtime/lib/droid-observation.mjs +9 -5
- package/dist/harness-runtime/lib/lifecycle-contract.d.mts +1 -0
- package/dist/harness-runtime/lib/lifecycle-contract.mjs +2 -1
- package/dist/harness-runtime/lib/native-hook-command.d.mts +3 -2
- package/dist/harness-runtime/lib/native-hook-command.mjs +63 -8
- package/dist/harness-runtime/lib/native-streams.mjs +3 -10
- package/dist/harness-runtime/lib/opencode-plugin.mjs +36 -9
- package/dist/harness-runtime/lib/operation-errors.mjs +13 -0
- package/dist/harness-runtime/lib/operation-progress.mjs +6 -2
- package/dist/harness-runtime/lib/process-json.mjs +4 -5
- package/dist/harness-runtime/lib/tool-observations.mjs +20 -10
- package/dist/harness-runtime/lib/zcode-native-hook-ownership.mjs +109 -0
- package/dist/schemas/code-work-result.schema.json +2 -1
- package/dist/schemas/delivery-stage-result.schema.json +286 -0
- package/dist/schemas/distill-stage-result.schema.json +667 -0
- package/dist/schemas/distill-work-result.schema.json +428 -0
- package/dist/schemas/execution-bundle.schema.json +6 -2
- package/dist/schemas/interactive-stage-result.schema.json +47 -0
- package/dist/schemas/interactive-work-result.schema.json +63 -0
- package/dist/schemas/mb-fix-stage-result.schema.json +93 -0
- package/dist/schemas/project-flow-pack-manifest.schema.json +2 -2
- package/dist/schemas/review-fix-stage-result.schema.json +210 -0
- package/dist/schemas/run-control-receipt.schema.json +1 -0
- package/dist/services/assigned-lifecycle.js +382 -0
- package/dist/services/check-runner-evidence.js +45 -8
- package/dist/services/cli-operation-classifier.js +6 -1
- package/dist/services/code-checks.js +46 -29
- package/dist/services/codex-hook-delivery.js +11 -2
- package/dist/services/controller-execution.js +4 -1
- package/dist/services/controller-fanout.js +9 -4
- package/dist/services/controller-memory-worker.js +6 -4
- package/dist/services/controller-result-authority.js +122 -0
- package/dist/services/dashboard.js +4 -2
- package/dist/services/delivery-access.js +123 -0
- package/dist/services/delivery-approval.js +68 -0
- package/dist/services/delivery-artifact.js +86 -0
- package/dist/services/delivery-checks.js +178 -0
- package/dist/services/delivery-execution.js +181 -0
- package/dist/services/delivery-intents.js +212 -0
- package/dist/services/delivery-operation.js +81 -0
- package/dist/services/delivery-policy.js +91 -0
- package/dist/services/delivery-procedures.js +43 -0
- package/dist/services/delivery-request.js +99 -0
- package/dist/services/delivery-result.js +144 -0
- package/dist/services/delivery-stage-commands.js +26 -0
- package/dist/services/delivery-stage-entry.js +56 -0
- package/dist/services/delivery-stage-handlers.js +135 -0
- package/dist/services/delivery-stage-results.js +94 -0
- package/dist/services/descriptor-coordinator-session.js +3 -1
- package/dist/services/descriptor-run-start.js +24 -4
- package/dist/services/descriptor-stage-coordinator.js +78 -10
- package/dist/services/descriptor-stage-settlement.js +59 -14
- package/dist/services/diagnostic-repair-intake.js +123 -0
- package/dist/services/diagnostic-repair-source-intake.js +102 -0
- package/dist/services/diagnostic-report.js +30 -0
- package/dist/services/diagnostic-run-bundle.js +48 -9
- package/dist/services/diagnostic-snapshot.js +2 -2
- package/dist/services/diagnostic-stage-handlers.js +3 -2
- package/dist/services/diagnostic-stage-results.js +26 -0
- package/dist/services/diagnostic-start.js +56 -6
- package/dist/services/diagnostic-work-collection.js +3 -3
- package/dist/services/distill-document-publication.js +280 -0
- package/dist/services/distill-promotion.js +197 -0
- package/dist/services/distill-publication-custody.js +162 -0
- package/dist/services/distill-stage-handlers.js +249 -0
- package/dist/services/distill-stage-results.js +105 -0
- package/dist/services/distill-user-inputs.js +4 -0
- package/dist/services/distill-work-batch.js +42 -0
- package/dist/services/distill-work-collection.js +74 -0
- package/dist/services/distill-work-registration.js +93 -0
- package/dist/services/distill-work-results.js +98 -0
- package/dist/services/eval-snapshots.js +17 -7
- package/dist/services/external-work-launch.js +8 -2
- package/dist/services/flow-catalog.js +24 -0
- package/dist/services/frozen-engine.js +19 -2
- package/dist/services/generic-execution.js +4 -3
- package/dist/services/harness-adapter.js +2 -14
- package/dist/services/hook-memory-routing.js +1 -1
- package/dist/services/hooks.js +31 -6
- package/dist/services/hybrid-contracts.js +6 -1
- package/dist/services/installed-flow-source.js +37 -2
- package/dist/services/interactive-attached-session.js +215 -0
- package/dist/services/interactive-baseline-custody.js +54 -0
- package/dist/services/interactive-before-versions.js +148 -0
- package/dist/services/interactive-bundle-data.js +85 -0
- package/dist/services/interactive-checks.js +179 -0
- package/dist/services/interactive-execution.js +33 -0
- package/dist/services/interactive-finish-integration.js +236 -0
- package/dist/services/interactive-git-baseline.js +230 -0
- package/dist/services/interactive-git-dispatch.js +278 -0
- package/dist/services/interactive-input-delivery.js +82 -0
- package/dist/services/interactive-mutation-scope.js +231 -0
- package/dist/services/interactive-request.js +287 -0
- package/dist/services/interactive-run-bundle.js +109 -0
- package/dist/services/interactive-stage-entry.js +41 -0
- package/dist/services/interactive-stage-handlers.js +289 -0
- package/dist/services/interactive-stage-observation.js +136 -0
- package/dist/services/interactive-stage-results.js +111 -0
- package/dist/services/interactive-user-inputs.js +142 -0
- package/dist/services/interactive-work-batch.js +43 -0
- package/dist/services/interactive-work-collection.js +119 -0
- package/dist/services/interactive-work-completion.js +180 -0
- package/dist/services/interactive-work-context.js +249 -0
- package/dist/services/interactive-work-history.js +112 -0
- package/dist/services/interactive-work-registration.js +124 -0
- package/dist/services/interactive-work-results.js +115 -0
- package/dist/services/interactive-work-start.js +155 -0
- package/dist/services/lifecycle-invocations.js +214 -24
- package/dist/services/managed-daemon-binding.js +52 -1
- package/dist/services/memory-compose-handler.js +2 -22
- package/dist/services/memory-finish-integration.js +58 -17
- package/dist/services/memory-fix-finish-integration.js +129 -0
- package/dist/services/memory-fix-git-dispatch.js +139 -0
- package/dist/services/memory-fix-intake-custody.js +11 -0
- package/dist/services/memory-fix-intake-settlement.js +127 -0
- package/dist/services/memory-fix-publication.js +196 -0
- package/dist/services/memory-fix-repair-admission.js +116 -0
- package/dist/services/memory-fix-request.js +141 -0
- package/dist/services/memory-fix-run-bundle.js +83 -0
- package/dist/services/memory-fix-scope.js +78 -0
- package/dist/services/memory-fix-sources.js +85 -0
- package/dist/services/memory-fix-stage-entry.js +24 -0
- package/dist/services/memory-fix-stage-handlers.js +282 -0
- package/dist/services/memory-fix-stage-results.js +106 -0
- package/dist/services/memory-fix-workspace.js +127 -0
- package/dist/services/memory-flow-user-inputs.js +88 -0
- package/dist/services/memory-init-contexts.js +55 -5
- package/dist/services/memory-init-controller-start.js +20 -4
- package/dist/services/memory-layer.js +7 -3
- package/dist/services/memory-native-review-copy.js +27 -3
- package/dist/services/memory-repair-document-delta.js +1 -0
- package/dist/services/project-flow-source.js +195 -0
- package/dist/services/project-git-integration.js +92 -0
- package/dist/services/repair-intents.js +2 -7
- package/dist/services/retained-native-hook.js +245 -0
- package/dist/services/review-fix-handoff.js +82 -0
- package/dist/services/review-fix-intake-custody.js +17 -0
- package/dist/services/review-fix-stage-entry.js +43 -0
- package/dist/services/review-fix-stage-handlers.js +176 -0
- package/dist/services/review-fix-stage-results.js +86 -0
- package/dist/services/run-control-worker.js +42 -1
- package/dist/services/run-control.js +40 -5
- package/dist/services/run-controller-adapter.js +37 -3
- package/dist/services/run-controller-process.js +12 -4
- package/dist/services/run-controller-recovery.js +46 -6
- package/dist/services/run-controller-state.js +3 -2
- package/dist/services/run-controller.js +209 -43
- package/dist/services/run-recovery.js +31 -2
- package/dist/services/run-timeline.js +59 -0
- package/dist/services/runs.js +18 -31
- package/dist/services/sessions.js +2 -1
- package/dist/services/stage-handlers.js +55 -3
- package/dist/services/stage-lifecycle.js +4 -3
- package/dist/services/stage-pause.js +26 -9
- package/dist/services/stage-report-renderer.js +2 -6
- package/dist/services/technical-notes.js +31 -0
- package/dist/services/verification-matrix.js +1 -1
- package/dist/services/version-status.js +1 -1
- package/dist/services/vnext-code-review.js +74 -5
- package/dist/services/vnext-code.js +8 -32
- package/dist/services/vnext-fanout.js +53 -8
- package/dist/services/vnext-merge.js +10 -5
- package/dist/services/vnext-plan-review.js +1 -1
- package/dist/services/vnext-plan.js +1 -1
- package/dist/services/vnext-protocolize.js +1 -1
- package/dist/services/vnext-specify.js +10 -5
- package/dist/services/work-registry.js +252 -66
- package/dist/services/worktrees.js +7 -4
- package/dist/storage/database.js +2 -1
- package/dist/verification-components.json +82 -0
- package/package.json +3 -2
- package/tools/verification-jobs.mjs +35 -10
|
@@ -273,6 +273,51 @@ export function readRunnerEvidenceDetailed(root, contract, binding) {
|
|
|
273
273
|
});
|
|
274
274
|
}
|
|
275
275
|
export function readRunnerEvidence(root, contract, binding) { return readRunnerEvidenceDetailed(root, contract, binding).evidence; }
|
|
276
|
+
/** Semantic attribution is diagnostic information, never permission to investigate a failed check. */
|
|
277
|
+
export const checkDiagnosisPolicy = "settled-failure@1";
|
|
278
|
+
export function checkDiagnosisDisposition(receipt) {
|
|
279
|
+
if (receipt.status === "running"
|
|
280
|
+
|| receipt.execution_failures?.some(failure => ["cleanup_unconfirmed", "engine_outcome_unbound"].includes(failure.code)))
|
|
281
|
+
return "settlement_required";
|
|
282
|
+
if (!["passed", "failed", "aborted"].includes(receipt.status))
|
|
283
|
+
return "invalid_binding";
|
|
284
|
+
// Caller supplies the SQL-owned receipt, never a proposed runner report.
|
|
285
|
+
// Missing, malformed or foreign optional reports are diagnostic failures;
|
|
286
|
+
// they cannot change receipt ownership or grant a successful check.
|
|
287
|
+
if (receipt.status === "passed")
|
|
288
|
+
return "passed";
|
|
289
|
+
// New launchers record physical settlement even for spawn errors/signals.
|
|
290
|
+
// Historical aborted receipts with no such proof must first reconcile.
|
|
291
|
+
if (!receipt.execution_failures?.some(failure => failure.code === "check_execution_settled") && !Number.isSafeInteger(receipt.exit_code))
|
|
292
|
+
return "settlement_required";
|
|
293
|
+
return "diagnosis_required";
|
|
294
|
+
}
|
|
295
|
+
export function assertCheckDiagnosisEligible(receipts, _workspaceRoot) {
|
|
296
|
+
const dispositions = receipts.map(receipt => ({ check_receipt_id: receipt.id, disposition: checkDiagnosisDisposition(receipt) }));
|
|
297
|
+
if (!receipts.length || dispositions.some(item => item.disposition !== "diagnosis_required"))
|
|
298
|
+
throw new AppError("check_gate_blocked", "Check diagnosis requires owned, physically settled failed executions", 2, {
|
|
299
|
+
failures: receipts, dispositions, outcome: "blocked", recoverable: false,
|
|
300
|
+
instruction: "Reconcile any running or unknown check process before changing its workspace. Do not treat a foreign receipt or an agent report as proof of completion."
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
/** Human-readable retained facts; tool output remains data, never instructions. */
|
|
304
|
+
export function checkDiagnosisSummary(receipt, workspaceRoot) {
|
|
305
|
+
const available = (file) => {
|
|
306
|
+
try {
|
|
307
|
+
fs.accessSync(file, fs.constants.R_OK);
|
|
308
|
+
return fs.statSync(file).isFile() ? file : `${file} (not a regular file)`;
|
|
309
|
+
}
|
|
310
|
+
catch {
|
|
311
|
+
return `${file} (unavailable; investigate the missing diagnostic, do not invent its content)`;
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
return [`Check: ${receipt.id}`, `Command: ${receipt.command}`, `cwd: ${workspaceRoot}`,
|
|
315
|
+
`Outcome: ${receipt.status}; exit: ${receipt.exit_code ?? "unavailable"}; epoch: ${receipt.verification_epoch}`,
|
|
316
|
+
`Receipt: ${available(receipt.receipt_path)}`, `stdout: ${available(receipt.stdout_path)}`, `stderr: ${available(receipt.stderr_path)}`,
|
|
317
|
+
`Execution facts: ${JSON.stringify(receipt.execution_failures ?? [])}`,
|
|
318
|
+
`Report: ${receipt.runner_evidence_required?.path ? path.join(path.dirname(receipt.receipt_path), "artifacts", receipt.runner_evidence_required.path) : "not declared"}`].join("\n");
|
|
319
|
+
}
|
|
320
|
+
/** Historical attribution reader only. New repair routing uses checkDiagnosisDisposition. */
|
|
276
321
|
export function checkRepairDisposition(receipt) {
|
|
277
322
|
if (receipt.fingerprint_scope != null && receipt.fingerprint_scope !== "check_inputs")
|
|
278
323
|
return "tooling_blocker";
|
|
@@ -297,11 +342,3 @@ export function checkRepairDisposition(receipt) {
|
|
|
297
342
|
return "unclassified";
|
|
298
343
|
return "source_repair";
|
|
299
344
|
}
|
|
300
|
-
export function assertSourceRepairEligible(receipts, workspaceRoot) {
|
|
301
|
-
const dispositions = receipts.map(receipt => {
|
|
302
|
-
const drift = workspaceRoot !== undefined && !currentCandidateLibraryInputsValid(receipt, workspaceRoot);
|
|
303
|
-
return { check_receipt_id: receipt.id, disposition: drift ? "tooling_blocker" : checkRepairDisposition(receipt), ...(drift ? { reason: "candidate_library_inputs_changed" } : {}) };
|
|
304
|
-
});
|
|
305
|
-
if (!receipts.length || dispositions.some(item => item.disposition !== "source_repair"))
|
|
306
|
-
throw new AppError("check_gate_blocked", "Checks require tooling diagnosis or settlement before product repair; retained nonzero is not a candidate-source verdict", 2, { failures: receipts, dispositions, outcome: "blocked", recoverable: false, instruction: "Inspect retained runner evidence. Do not change product code to repair tooling, unknown observation or unclassified failure. Retry only after establishing and correcting the cause." });
|
|
307
|
-
}
|
|
@@ -89,6 +89,10 @@ function isReadOnlyDiagnostic(family, command, action) {
|
|
|
89
89
|
return ["status", "ls", "show"].includes(command ?? "");
|
|
90
90
|
if (family === "stage")
|
|
91
91
|
return command === "fanout" && action === "status";
|
|
92
|
+
if (family === "interactive")
|
|
93
|
+
return command === "inputs" || command === "git-inputs";
|
|
94
|
+
if (family === "delivery")
|
|
95
|
+
return command === "inputs";
|
|
92
96
|
if (family === "lane") {
|
|
93
97
|
return (command === "status" ||
|
|
94
98
|
command === "waiters" ||
|
|
@@ -108,7 +112,8 @@ function isReadOnlyDiagnostic(family, command, action) {
|
|
|
108
112
|
if (family === "dashboard")
|
|
109
113
|
return command === "data" || command === "open";
|
|
110
114
|
if (family === "memory")
|
|
111
|
-
return command === "permissions" && action === "preflight"
|
|
115
|
+
return command === "permissions" && action === "preflight" || command === "distill" && action === "inputs"
|
|
116
|
+
|| command === "fix" && ["context", "scope"].includes(action ?? "");
|
|
112
117
|
if (family === "codex" && command === "hooks")
|
|
113
118
|
return action === "print" || action === "status";
|
|
114
119
|
if (family === "codex" && command === "home")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
|
-
import { candidateAbsencesValid, candidateFailureAbsences, candidateFailureFiles, candidateLibraryInputs, candidateLibraryInputsValid, currentCandidateLibraryInputsValid,
|
|
3
|
-
export {
|
|
2
|
+
import { candidateAbsencesValid, candidateFailureAbsences, candidateFailureFiles, candidateLibraryInputs, candidateLibraryInputsValid, currentCandidateLibraryInputsValid, checkDiagnosisDisposition, readRunnerEvidence, readRunnerEvidenceDetailed, readRunnerProgress, runnerContractsValid, runnerEvidencePass, runnerObservationDurations, runnerProofArtifacts } from "./check-runner-evidence.js";
|
|
3
|
+
export { assertCheckDiagnosisEligible, checkDiagnosisDisposition, checkRepairDisposition } from "./check-runner-evidence.js";
|
|
4
4
|
import { isGitlessWorkspace, ownedWorkspaceHookPath, projectServicePathFilter, workspaceGitFiles } from "./workspace-files.js";
|
|
5
5
|
import { spawn, spawnSync } from "node:child_process";
|
|
6
6
|
import { ObservationClock, validateObservationDuration } from "../harness-runtime/lib/observation-clock.mjs";
|
|
@@ -352,14 +352,16 @@ export class PreparedCheckEvidence {
|
|
|
352
352
|
candidates: receipts.filter(item => item.scope === input.scope && item.work_id === (input.workId ?? null))
|
|
353
353
|
.slice(-16).map(item => ({ id: item.id, input_hash: item.input_hash, profile_hash: item.profile_hash }))
|
|
354
354
|
});
|
|
355
|
-
if (!checkReceiptEvidenceValidForMode(bindReceipt(receipt, group), input.mode) || !currentCandidateLibraryInputsValid(receipt, input.workspaceRoot))
|
|
355
|
+
if (!checkReceiptEvidenceValidForMode(bindReceipt(receipt, group), input.mode) || input.mode !== "diagnostic" && !currentCandidateLibraryInputsValid(receipt, input.workspaceRoot))
|
|
356
356
|
fail();
|
|
357
357
|
passed = passed && receipt.status === "passed";
|
|
358
|
-
|
|
358
|
+
const availableArtifacts = input.mode === "diagnostic" ? availableDiagnosticArtifacts(receipt) : receipt.artifacts;
|
|
359
|
+
for (const artifact of availableArtifacts)
|
|
359
360
|
artifacts.push({ root: "run", path: path.relative(input.runHome, path.join(path.dirname(receipt.receipt_path), "artifacts", artifact.path)).split(path.sep).join("/"), sha256: artifact.sha256 });
|
|
360
361
|
const bytes = fs.readFileSync(receipt.receipt_path);
|
|
361
|
-
|
|
362
|
-
|
|
362
|
+
if (input.mode !== "diagnostic" || fs.existsSync(receipt.completion_path))
|
|
363
|
+
completions.push({ root: "run", path: path.relative(input.runHome, receipt.completion_path).split(path.sep).join("/"), sha256: crypto.createHash("sha256").update(readBoundedReleaseFile(receipt.completion_path, 1024 * 1024)).digest("hex") });
|
|
364
|
+
if (receipt.input_manifest && (input.mode !== "diagnostic" || fs.existsSync(path.join(path.dirname(receipt.receipt_path), receipt.input_manifest.path))))
|
|
363
365
|
completions.push({ root: "run", path: path.relative(input.runHome, path.join(path.dirname(receipt.receipt_path), receipt.input_manifest.path)).split(path.sep).join("/"), sha256: receipt.input_manifest.sha256 });
|
|
364
366
|
return { id: receipt.id, path: receipt.receipt_path, sha256: crypto.createHash("sha256").update(bytes).digest("hex") };
|
|
365
367
|
});
|
|
@@ -562,7 +564,7 @@ async function runCodeChecksOwned(context, input, workspaceGateId, generation, s
|
|
|
562
564
|
const snapshot = workspaceState(input.workspaceRoot, [input.runHome]);
|
|
563
565
|
const group = groups.find(candidate => candidate.declarations.some(check => check.run_at === predecessor.gate)
|
|
564
566
|
&& (checkInputHash(input.workspaceRoot, { ...candidate, gate: predecessor.gate }, profileHash, workspaceInputFingerprint(input.workspaceRoot, snapshot.files, candidate.inputs, [input.runHome]), input.evidenceContext, input.checkEnvironment) === predecessor.input_hash
|
|
565
|
-
||
|
|
567
|
+
|| diagnosisRetryContractMatches(predecessor, candidate, profileHash, checkInputHash(input.workspaceRoot, { ...candidate, gate: predecessor.gate }, profileHash, "", input.evidenceContext, input.checkEnvironment, false), checkInputHash(input.workspaceRoot, { ...candidate, gate: predecessor.gate }, profileHash, "", input.evidenceContext, input.checkEnvironment))
|
|
566
568
|
|| input.retryReason === "check_inputs_changed" && staleRetryContractMatches(predecessor, candidate, profileHash, input)));
|
|
567
569
|
if (!group || retries.has(group))
|
|
568
570
|
throw new AppError("retry_check_invalid", "Retry must match exactly one current execution contract", 2, { check_receipt_id: receiptId });
|
|
@@ -579,7 +581,9 @@ async function runCodeChecksOwned(context, input, workspaceGateId, generation, s
|
|
|
579
581
|
const inputFingerprint = inputFilesFingerprint(inputFiles);
|
|
580
582
|
const inputHash = checkInputHash(input.workspaceRoot, group, profileHash, inputFingerprint, input.evidenceContext, input.checkEnvironment);
|
|
581
583
|
const epoch = crypto.createHash("sha256").update(`${group.gate}\0${inputFingerprint}`).digest("hex");
|
|
582
|
-
|
|
584
|
+
// Retry preserves the declared check/wave, not the failed conditions being
|
|
585
|
+
// corrected. Observed environment still participates in inputHash/PASS reuse.
|
|
586
|
+
const executionContractHash = checkInputHash(input.workspaceRoot, group, profileHash, "", input.evidenceContext, input.checkEnvironment, false);
|
|
583
587
|
const reservationInput = { ...input, retryReceiptIds: retries.has(group) ? [retries.get(group)] : [], generation, group, before, inputFiles, inputFingerprint, executionContractHash, profileHash, inputHash, epoch, startedAt };
|
|
584
588
|
delete reservationInput.retryReceiptId;
|
|
585
589
|
const decision = reserveOrReuseReceipt(context, reservationInput);
|
|
@@ -592,6 +596,7 @@ async function runCodeChecksOwned(context, input, workspaceGateId, generation, s
|
|
|
592
596
|
const { id, evidenceDir, stdoutPath, stderrPath, receiptPath, completionPath } = reserved;
|
|
593
597
|
let resources = {};
|
|
594
598
|
let result = { exitCode: null, error: "check did not start", aborted: true };
|
|
599
|
+
let dispatched = false, physicallySettled = false;
|
|
595
600
|
try {
|
|
596
601
|
fs.mkdirSync(evidenceDir, { recursive: true });
|
|
597
602
|
retainInputManifest(context, reserved);
|
|
@@ -624,6 +629,7 @@ async function runCodeChecksOwned(context, input, workspaceGateId, generation, s
|
|
|
624
629
|
const runnerEnvironment = group.runnerEvidence ? { DD_FLOW_CHECK_RUNNER_EVIDENCE_PATH: path.join(evidenceDir, group.runnerEvidence.path), DD_FLOW_CHECK_RUNNER_PROGRESS_PATH: path.join(evidenceDir, "runner-progress.jsonl"), DD_FLOW_CHECK_INVOCATION_ID: id, DD_FLOW_CHECK_ID: id, DD_FLOW_CHECK_INPUT_HASH: inputHash, DD_FLOW_CHECK_VERIFICATION_EPOCH: epoch } : {};
|
|
625
630
|
if (group.runnerEvidence)
|
|
626
631
|
fs.mkdirSync(path.dirname(path.join(evidenceDir, group.runnerEvidence.path)), { recursive: true });
|
|
632
|
+
dispatched = true;
|
|
627
633
|
result = await runCheck(group.command, input.workspaceRoot, { ...codeExecutionEnvironment(input.workspaceRoot, input.checkEnvironment), ...browserCheckCapabilityEnvironment(context, input.runId, input.runHome), DD_FLOW_EVIDENCE_DIR: evidenceDir, DD_FLOW_CHECK_COMPLETION_FILE: completionPath, ...runnerEnvironment, ...portEnvironment(resources) }, stdout, stderr, (elapsed) => input.progress?.(`check ${index + 1}/${groups.length} still running (${elapsed}s): ${group.command}`), (pid) => {
|
|
628
634
|
confirmedExpiry = confirmManagedProcess(context, { id: managed.id, leaseToken: managed.lease_token, pid, processGroupId: process.platform === "win32" ? null : pid }).lease_expires_at;
|
|
629
635
|
assertRunMutationAllowed(context, input.projectId, input.runId, generation);
|
|
@@ -638,6 +644,7 @@ async function runCodeChecksOwned(context, input, workspaceGateId, generation, s
|
|
|
638
644
|
const current = managedProcessStatus(context).find(item => item.id === managed.id);
|
|
639
645
|
if (processTreeIsAlive(current))
|
|
640
646
|
throw new AppError("check_in_progress", "The check shell exited but its process tree is still live; retain this receipt and its resources until settlement", 2, { process_id: managed.id, check_receipt_id: id });
|
|
647
|
+
physicallySettled = !result.cleanupUnconfirmed;
|
|
641
648
|
allocation.release();
|
|
642
649
|
if (!result.cleanupUnconfirmed)
|
|
643
650
|
finishManagedProcess(context, { id: managed.id, leaseToken: managed.lease_token, state: result.aborted || result.exitCode !== 0 || Boolean(result.error) ? "failed" : "stopped", reason: result.error });
|
|
@@ -664,6 +671,9 @@ async function runCodeChecksOwned(context, input, workspaceGateId, generation, s
|
|
|
664
671
|
const physicalFailures = executionFailures({ fingerprintScope: "check_inputs", exitCode: result.exitCode, launcherError: result.error ?? undefined, cleanupUnconfirmed: result.cleanupUnconfirmed, missing: artifacts.missing, mutations: mutationPaths, invalidRunner: Boolean(group.runnerEvidence && !runnerEvidence) });
|
|
665
672
|
if (result.failure)
|
|
666
673
|
physicalFailures.push(result.failure);
|
|
674
|
+
// This marker is SQL-owned execution metadata, not a semantic runner verdict.
|
|
675
|
+
if (result.aborted && (!dispatched || physicallySettled))
|
|
676
|
+
physicalFailures.push({ code: "check_execution_settled", message: "No owned check process remains; investigate the retained launch or execution failure" });
|
|
667
677
|
const candidateFiles = candidateFailureFiles(runnerEvidence, input.workspaceRoot);
|
|
668
678
|
const candidateAbsences = candidateFailureAbsences(runnerEvidence, input.workspaceRoot);
|
|
669
679
|
const libraryInputs = candidateLibraryInputs(runnerEvidence, input.workspaceRoot);
|
|
@@ -826,14 +836,9 @@ function reserveOrReuseReceipt(context, input) {
|
|
|
826
836
|
const successor = retryIds.size === 1 ? receipts.find((receipt) => receipt.retry_of_receipt_id === [...retryIds][0] && receipt.input_hash === input.inputHash && receipt.profile_hash === input.profileHash) : null;
|
|
827
837
|
const reusableSuccessor = successor?.status === "passed" && checkReceiptEvidenceValid(successor);
|
|
828
838
|
const retryPredecessor = receipts.find(receipt => retryIds.has(receipt.id));
|
|
829
|
-
const sourceCorrection = Boolean(retryPredecessor && (
|
|
839
|
+
const sourceCorrection = Boolean(retryPredecessor && (diagnosisRetryContractMatches(retryPredecessor, input.group, input.profileHash, input.executionContractHash, checkInputHash(input.workspaceRoot, input.group, input.profileHash, "", input.evidenceContext, input.checkEnvironment))
|
|
830
840
|
|| input.retryReason === "check_inputs_changed" && staleRetryContractMatches(retryPredecessor, input.group, input.profileHash, input)));
|
|
831
841
|
const latestContract = receipts.filter(receipt => receipt.command === input.group.command && receipt.profile_hash === input.profileHash && receipt.gate === input.group.gate && receipt.scope === input.scope && receipt.work_id === (input.workId ?? null)).at(-1);
|
|
832
|
-
// Hash files before SQLite's writer reservation. Captured identities remain
|
|
833
|
-
// valid historical evidence, but cannot authorize reuse in a changed install.
|
|
834
|
-
const priorLibrariesCurrent = !prior || currentCandidateLibraryInputsValid(prior, input.workspaceRoot);
|
|
835
|
-
const latestLibrariesCurrent = !latestContract || currentCandidateLibraryInputsValid(latestContract, input.workspaceRoot);
|
|
836
|
-
const libraryDrift = (receipt) => { throw new AppError("check_evidence_stale", "Captured library inputs differ from the current candidate installation; request an explicit restored-condition retry", 2, { reason: "candidate_library_inputs_changed", check_receipt_id: receipt.id, effect: "no_effect" }); };
|
|
837
842
|
return context.db.writeTransaction(() => {
|
|
838
843
|
assertRunMutationAllowed(context, input.projectId, input.runId, input.generation);
|
|
839
844
|
if (JSON.stringify(context.db.all(snapshotSql, [input.projectId, input.runId])) !== snapshot)
|
|
@@ -869,16 +874,10 @@ function reserveOrReuseReceipt(context, input) {
|
|
|
869
874
|
// An unchanged failed contract needs a concrete restored-condition retry,
|
|
870
875
|
// not repeated coordinator finish until it happens to become green.
|
|
871
876
|
if (!retryId && latestContract?.fingerprint_scope === "check_inputs" && latestContract.status === "failed"
|
|
872
|
-
&& latestContract.execution_failures?.some(failure => failure.code === "check_inputs_changed") && latestContract.execution_contract_hash === input.executionContractHash)
|
|
873
|
-
if (!latestLibrariesCurrent)
|
|
874
|
-
libraryDrift(latestContract);
|
|
877
|
+
&& latestContract.execution_failures?.some(failure => failure.code === "check_inputs_changed") && latestContract.execution_contract_hash === input.executionContractHash)
|
|
875
878
|
return { receipt: latestContract };
|
|
876
|
-
|
|
877
|
-
if (prior && ["failed", "aborted"].includes(prior.status) && !retryId) {
|
|
878
|
-
if (!priorLibrariesCurrent)
|
|
879
|
-
libraryDrift(prior);
|
|
879
|
+
if (prior && ["failed", "aborted"].includes(prior.status) && !retryId)
|
|
880
880
|
return { receipt: prior };
|
|
881
|
-
}
|
|
882
881
|
if (prior?.status === "passed" && !retryIds.has(prior.id) && input.group.reusable && reusablePrior)
|
|
883
882
|
return { receipt: prior };
|
|
884
883
|
let ordinal = (context.db.get("SELECT COALESCE(MAX(reservation_sequence), 0) + 1 AS sequence FROM check_receipts WHERE project_id = ? AND run_id = ?", [input.projectId, input.runId])?.sequence ?? 1);
|
|
@@ -895,9 +894,12 @@ function reserveOrReuseReceipt(context, input) {
|
|
|
895
894
|
return { reserved };
|
|
896
895
|
}, "checks.reserveOrReuseReceipt");
|
|
897
896
|
}
|
|
898
|
-
function
|
|
899
|
-
return
|
|
900
|
-
&&
|
|
897
|
+
function diagnosisRetryContractMatches(receipt, group, profileHash, executionContractHash, legacyExecutionContractHash) {
|
|
898
|
+
return checkDiagnosisDisposition(receipt) === "diagnosis_required" && group.command === receipt.command && profileHash === receipt.profile_hash && isDeepStrictEqual(group.inputs, receipt.inputs)
|
|
899
|
+
&& [executionContractHash, legacyExecutionContractHash].includes(receipt.execution_contract_hash ?? "")
|
|
900
|
+
&& group.requiredArtifacts.every(artifact => receipt.required_artifacts.includes(artifact))
|
|
901
|
+
&& isDeepStrictEqual(group.runnerEvidence ?? null, receipt.runner_evidence_required ?? null)
|
|
902
|
+
&& isDeepStrictEqual([...group.ports].sort(), Object.keys(receipt.resources.ports).sort());
|
|
901
903
|
}
|
|
902
904
|
function staleCheckRetryEligible(receipt) {
|
|
903
905
|
if (receipt.fingerprint_scope !== "check_inputs" || receipt.status !== "failed" || receipt.exit_code !== 0 || receipt.retry_reason === "check_inputs_changed"
|
|
@@ -908,7 +910,8 @@ function staleCheckRetryEligible(receipt) {
|
|
|
908
910
|
}
|
|
909
911
|
function staleRetryContractMatches(receipt, group, profileHash, input) {
|
|
910
912
|
return group.reusable && staleCheckRetryEligible(receipt) && isDeepStrictEqual(group.runnerEvidence ?? null, receipt.runner_evidence_required ?? null)
|
|
911
|
-
&&
|
|
913
|
+
&& [checkInputHash(input.workspaceRoot, group, profileHash, "", input.evidenceContext, input.checkEnvironment, false),
|
|
914
|
+
checkInputHash(input.workspaceRoot, group, profileHash, "", input.evidenceContext, input.checkEnvironment)].includes(receipt.execution_contract_hash ?? "");
|
|
912
915
|
}
|
|
913
916
|
function receiptFor(input) { return { id: input.id, local_id: input.localId, declaration_id: input.group.declarations[0].id, check_refs: input.group.declarations.map(checkRef), gate: input.group.gate, scope: input.scope, work_id: input.workId ?? null, command: input.group.command, input_hash: input.inputHash, verification_epoch: input.epoch, retry_of_receipt_id: input.retryOf ?? null, retry_reason: input.retryReason ?? null, status: input.status, ...(input.status === "aborted" ? { abort_reason: input.result.error } : {}), exit_code: input.status === "running" ? null : input.result.exitCode, stdout_path: input.stdoutPath, stderr_path: input.stderrPath, receipt_path: input.receiptPath, completion_path: input.completionPath, required_artifacts: input.group.requiredArtifacts, inputs: input.group.inputs, fingerprint_scope: "check_inputs", input_manifest: input.inputManifest ?? null, execution_contract_hash: input.executionContractHash, workspace_fingerprint: input.before.fingerprint, before_fingerprint: input.inputFingerprint, after_fingerprint: input.status === "running" ? input.inputFingerprint : input.after.fingerprint, profile_hash: input.profileHash, mutation_paths: input.mutationPaths, artifacts: input.artifacts, resources: { ports: input.resources }, started_at: input.startedAt, finished_at: input.status === "running" ? input.startedAt : new Date().toISOString() }; }
|
|
914
917
|
function retainInputManifest(context, reserved) {
|
|
@@ -966,8 +969,6 @@ export function validateFailedCheckRetry(context, input) {
|
|
|
966
969
|
const declared = new Set(input.declarations.flatMap((check) => [check.id, checkRef(check)]));
|
|
967
970
|
if (!receipt.check_refs.some((ref) => declared.has(ref)))
|
|
968
971
|
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 });
|
|
969
|
-
if (receipt.fingerprint_scope === "check_inputs" && receipt.execution_failures?.some(failure => failure.code === "check_inputs_changed") && input.reason !== "check_inputs_changed")
|
|
970
|
-
throw new AppError("retry_check_invalid", "Scoped drift uses its bounded stale transition, not an environment/source retry", 2, { check_receipt_id: receipt.id });
|
|
971
972
|
if (input.reason === "check_inputs_changed" && (!staleCheckRetryEligible(receipt) || receipt.retry_reason === "check_inputs_changed"))
|
|
972
973
|
throw new AppError("retry_check_invalid", "Scoped input retry requires a bound, safely completed stale predecessor and one unconsumed episode", 2, { check_receipt_id: receipt.id });
|
|
973
974
|
return receipt;
|
|
@@ -1326,12 +1327,28 @@ function explicitInputFiles(root, inputs, ignoredRoots, sourceFiles) {
|
|
|
1326
1327
|
visit(path.resolve(root, input), input, new Set());
|
|
1327
1328
|
return [...result.entries()];
|
|
1328
1329
|
}
|
|
1329
|
-
function checkInputHash(workspaceRoot, group, profileHash, inputFingerprint, evidenceContext, checkEnvironment) { return crypto.createHash("sha256").update(JSON.stringify({ workspace_root: path.resolve(workspaceRoot), gate: group.reusable ? null : group.gate, command: group.command, required_artifacts: group.requiredArtifacts, ports: group.ports, inputs: group.inputs, ...(group.runnerEvidence ? { runner_evidence: group.runnerEvidence } : {}), profile_hash: profileHash, environment: environmentFingerprint(workspaceRoot, group.environmentInputs, checkEnvironment), input_fingerprint: inputFingerprint, ...(group.environmentInputs === undefined ? {} : { environment_inputs: group.environmentInputs }), ...(evidenceContext === undefined ? {} : { evidence_context: evidenceContext }) })).digest("hex"); }
|
|
1330
|
+
function checkInputHash(workspaceRoot, group, profileHash, inputFingerprint, evidenceContext, checkEnvironment, observedEnvironment = true) { return crypto.createHash("sha256").update(JSON.stringify({ workspace_root: path.resolve(workspaceRoot), gate: group.reusable ? null : group.gate, command: group.command, required_artifacts: group.requiredArtifacts, ports: group.ports, inputs: group.inputs, ...(group.runnerEvidence ? { runner_evidence: group.runnerEvidence } : {}), profile_hash: profileHash, environment: observedEnvironment ? environmentFingerprint(workspaceRoot, group.environmentInputs, checkEnvironment) : null, input_fingerprint: inputFingerprint, ...(group.environmentInputs === undefined ? {} : { environment_inputs: group.environmentInputs }), ...(evidenceContext === undefined ? {} : { evidence_context: evidenceContext }) })).digest("hex"); }
|
|
1330
1331
|
function derivedReceiptArtifacts(group, receipt) { return [...new Set([...group.requiredArtifacts, ...runnerProofArtifacts(receipt.runner_evidence), ...(receipt.required_artifacts.includes("engine-outcome.json") ? ["engine-outcome.json"] : [])])]; }
|
|
1331
1332
|
function bindReceipt(receipt, group) { return { ...receipt, gate: group.gate, declaration_id: group.declarations[0].id, check_refs: group.declarations.map(checkRef), required_artifacts: derivedReceiptArtifacts(group, receipt), inputs: group.inputs }; }
|
|
1332
1333
|
export function checkReceiptEvidenceValid(receipt) { return checkReceiptEvidenceValidForMode(receipt); }
|
|
1334
|
+
function availableDiagnosticArtifacts(receipt) {
|
|
1335
|
+
const root = path.join(path.dirname(receipt.receipt_path), "artifacts");
|
|
1336
|
+
return receipt.artifacts.flatMap(artifact => {
|
|
1337
|
+
const relative = path.isAbsolute(artifact.path) ? path.relative(root, artifact.path) : artifact.path;
|
|
1338
|
+
const observed = collectCheckArtifacts(root, [relative]);
|
|
1339
|
+
return observed.items.filter(item => item.sha256 === artifact.sha256);
|
|
1340
|
+
});
|
|
1341
|
+
}
|
|
1333
1342
|
function checkReceiptEvidenceValidForMode(receipt, mode) {
|
|
1334
1343
|
try {
|
|
1344
|
+
if (mode === "diagnostic" && receipt.status !== "passed") {
|
|
1345
|
+
if (checkDiagnosisDisposition(receipt) !== "diagnosis_required")
|
|
1346
|
+
return false;
|
|
1347
|
+
// SQL selects this execution. Only its identity must survive in the
|
|
1348
|
+
// diagnostic packet; missing reports/logs cannot erase a failed command.
|
|
1349
|
+
const stored = JSON.parse(readBoundedReleaseFile(receipt.receipt_path, 1024 * 1024).toString("utf8"));
|
|
1350
|
+
return ["id", "scope", "work_id", "command", "input_hash", "verification_epoch", "status", "exit_code", "started_at", "finished_at"].every(field => (stored[field] ?? null) === (receipt[field] ?? null));
|
|
1351
|
+
}
|
|
1335
1352
|
const staleDiagnostic = mode === "diagnostic" && receipt.fingerprint_scope === "check_inputs" && receipt.status === "failed" && Number.isInteger(receipt.exit_code)
|
|
1336
1353
|
&& receipt.before_fingerprint !== receipt.after_fingerprint && receipt.mutation_paths.length > 0 && receipt.execution_failures?.some(failure => failure.code === "check_inputs_changed");
|
|
1337
1354
|
if (receipt.fingerprint_scope != null && receipt.fingerprint_scope !== "check_inputs")
|
|
@@ -19,7 +19,10 @@ export async function verifyCodexHookDelivery(env, payload) {
|
|
|
19
19
|
throw new AppError("native_hook_timeout", "Native hook deadline expired before delivery verification", 1, { phase: "verification", effect: "no_effect", recoverable: false });
|
|
20
20
|
const proof = await callDaemon(env.DD_FLOW_CODEX_STATE_DIR, "hook.verify", {
|
|
21
21
|
daemonId: env.DD_FLOW_DAEMON_ID, rootSessionId: payload.session_id,
|
|
22
|
-
sessionId: payload.agent_id ?? payload.session_id, toolCallId: payload.tool_use_id, turnId: payload.turn_id
|
|
22
|
+
sessionId: payload.agent_id ?? payload.session_id, toolCallId: payload.tool_use_id, turnId: payload.turn_id,
|
|
23
|
+
// The daemon verifies the native delivery before retaining it; the source
|
|
24
|
+
// command cannot itself grant admission to the CLI.
|
|
25
|
+
nativePayload: payload, nativeCommand: command
|
|
23
26
|
}, Math.min(5000, remaining));
|
|
24
27
|
if (typeof proof.projectRoot !== "string"
|
|
25
28
|
|| typeof proof.ddFlowHome !== "string" || !env.DD_FLOW_HOME || path.resolve(env.DD_FLOW_HOME) !== path.resolve(proof.ddFlowHome)
|
|
@@ -27,6 +30,12 @@ export async function verifyCodexHookDelivery(env, payload) {
|
|
|
27
30
|
|| typeof proof.toolCallId !== "string" || !(proof.parentSessionId === null || typeof proof.parentSessionId === "string")) {
|
|
28
31
|
throw new AppError("native_hook_unproven", "Native hook proof differs from the requested project/runtime", 1, { effect: "no_effect" });
|
|
29
32
|
}
|
|
33
|
+
const binding = proof.nativeHookBinding;
|
|
34
|
+
const retained = binding && typeof binding === "object" && !Array.isArray(binding) ? binding : null;
|
|
30
35
|
return { daemonId: proof.daemonId, rootSessionId: proof.rootSessionId, sessionId: proof.sessionId,
|
|
31
|
-
parentSessionId: proof.parentSessionId, toolCallId: proof.toolCallId, projectRoot: proof.projectRoot, command: command
|
|
36
|
+
parentSessionId: proof.parentSessionId, toolCallId: proof.toolCallId, projectRoot: proof.projectRoot, command: command,
|
|
37
|
+
...(retained?.schema_id === "dd-flow/hook-request@1" && retained.source_harness === "codex-desktop"
|
|
38
|
+
&& retained.daemon_id === proof.daemonId && retained.root_provider_session_id === proof.rootSessionId
|
|
39
|
+
&& retained.provider_session_id === proof.sessionId && retained.tool_call_id === proof.toolCallId
|
|
40
|
+
&& retained.native_command === command && typeof retained.request_id === "string" ? { nativeHookBinding: retained } : {}) };
|
|
32
41
|
}
|
|
@@ -5,6 +5,7 @@ import { resolveExecutionPolicy, resolveStageTransition, stageExecutionCwd } fro
|
|
|
5
5
|
import { readFlowRunExecutionBundle } from "./runs.js";
|
|
6
6
|
import { hybridDigest } from "./hybrid-contracts.js";
|
|
7
7
|
import { isDiagnosticFlow } from "../domain/diagnostic-flows.js";
|
|
8
|
+
import { isDeliveryFlow } from "../domain/delivery-flows.js";
|
|
8
9
|
/** Adapt generic routing to shared fanout APIs, not to SDLC settings. No alias,
|
|
9
10
|
* operator file, merge policy or project default participates in this selection.
|
|
10
11
|
*/
|
|
@@ -44,7 +45,9 @@ export function genericControllerRouting(execution, workspaceRoot) {
|
|
|
44
45
|
* Legacy SDLC manifests retain their original settings and routing behavior.
|
|
45
46
|
*/
|
|
46
47
|
export function selectControllerExecution(context, run, manifest) {
|
|
47
|
-
|
|
48
|
+
const boundDistill = run.flow_kind === "mb-distill" && JSON.parse(run.index_json).execution_bundle?.flow_id === "mb-distill";
|
|
49
|
+
const boundDelivery = isDeliveryFlow(run.flow_kind) && JSON.parse(run.index_json).execution_bundle?.flow_id === run.flow_kind;
|
|
50
|
+
if (manifest?.schema_id === "dd-flow/run-controller-manifest@2" || ["mb-init", "mb-upgrade", "mb-fix", "review-fix", "interactive"].includes(run.flow_kind) || isDiagnosticFlow(run.flow_kind) || boundDistill || boundDelivery) {
|
|
48
51
|
const bundle = readFlowRunExecutionBundle(context, { projectRoot: run.project_root, runId: run.id });
|
|
49
52
|
if (!bundle || bundle.descriptor.descriptor.id !== run.flow_kind || bundle.descriptor.descriptor.id === "sdlc")
|
|
50
53
|
throw new AppError("controller_execution_invalid", "Generic controller requires its retained non-SDLC bundle", 1);
|
|
@@ -13,6 +13,7 @@ import { stageAttempt, stageWorkIds } from "./stage-work-graph.js";
|
|
|
13
13
|
import { runtimeCommand } from "./runtime-command.js";
|
|
14
14
|
import { promptDeliveryInstruction } from "./prompt-delivery.js";
|
|
15
15
|
import { currentStageFinishCommand } from "./lifecycle-invocations.js";
|
|
16
|
+
import { acceptedChildWork } from "./controller-result-authority.js";
|
|
16
17
|
function semanticCompletionPrompt(stage, completed, command) {
|
|
17
18
|
const task = stage === "plan-review"
|
|
18
19
|
? "Classify the reviewer findings, apply every accepted correction in the current PLAN-REVIEW Work, and write the authoritative review decision."
|
|
@@ -74,8 +75,11 @@ export async function nextControllerFanout(context, input) {
|
|
|
74
75
|
const issues = reconciled.issues.filter(issue => !["child_check_in_progress", "child_capacity_pending"].includes(issue.code));
|
|
75
76
|
if (issues.length)
|
|
76
77
|
throw new AppError("fanout_reconciliation_required", "Native child outcome requires reconciliation, not replay", 1, { issues });
|
|
77
|
-
|
|
78
|
-
|
|
78
|
+
const unsettled = children.filter(child => child.status === "running" || child.status === "unknown");
|
|
79
|
+
if (unsettled.length) {
|
|
80
|
+
const project = requireProjectByRoot(context, input.projectRoot);
|
|
81
|
+
return { kind: "wait", reason: unsettled.every(child => acceptedChildWork(context, { project_id: project.id, run_id: input.runId }, input.harness, input.sessionId, child.session_id)) ? "accepted_work_native_cleanup" : "native_children_unsettled" };
|
|
82
|
+
}
|
|
79
83
|
fanout = getVnextFanoutStatus(context, input).orchestration;
|
|
80
84
|
}
|
|
81
85
|
if (input.dispatchFence && !fanout.works.failed)
|
|
@@ -155,7 +159,8 @@ export async function nextControllerFanout(context, input) {
|
|
|
155
159
|
const work = choice.work;
|
|
156
160
|
const row = context.db.get("SELECT payload_json,result_schema FROM works WHERE work_id=?", [work.work_id]);
|
|
157
161
|
const payload = JSON.parse(row?.payload_json ?? "null");
|
|
158
|
-
if (payload?.kind === "memory-review" && row?.result_schema === "dd-flow/memory-review-work-result@1"
|
|
162
|
+
if (payload?.kind === "memory-review" && row?.result_schema === "dd-flow/memory-review-work-result@1"
|
|
163
|
+
|| payload?.kind === "interactive-review" && row?.result_schema === "dd-flow/interactive-work-result@1") {
|
|
159
164
|
const copy = prepareMemoryNativeReviewCopy(context, work.work_id, true);
|
|
160
165
|
return { ...work, start_command: `cd '${copy.root.replaceAll("'", "'\\''")}' && ${workStartCommand(context, assertWorkLaunchReady(context, work.work_id))}` };
|
|
161
166
|
}
|
|
@@ -171,7 +176,7 @@ export async function nextControllerFanout(context, input) {
|
|
|
171
176
|
if (!work.work_id || !work.start_command)
|
|
172
177
|
throw new AppError("fanout_contract_invalid", "Ready Work omits its trusted start command", 1);
|
|
173
178
|
const totalWorks = fanout.works.created + fanout.works.running + fanout.works.completed + fanout.works.failed + fanout.works.cancelled;
|
|
174
|
-
return prompt("dispatch_work", renderDelegationInstructions({ harness: input.harness, stage: input.stage, capacity, works: wave, totalWorks, remainingWorks: fanout.works.created - wave.length }));
|
|
179
|
+
return { ...prompt("dispatch_work", renderDelegationInstructions({ harness: input.harness, stage: input.stage, capacity, works: wave, totalWorks, remainingWorks: fanout.works.created - wave.length })), work_ids: wave.map(work => work.work_id) };
|
|
175
180
|
}
|
|
176
181
|
if (fanout.works.running)
|
|
177
182
|
return { kind: "wait", reason: "work_receipts_pending" };
|
|
@@ -6,7 +6,8 @@ import { launchExternalWork } from "./external-work-launch.js";
|
|
|
6
6
|
import { requireProjectByRoot } from "./projects.js";
|
|
7
7
|
import { assertControllerLeaseAllowed } from "./run-controller-process.js";
|
|
8
8
|
import { assertManagedProcessLease } from "./managed-processes.js";
|
|
9
|
-
import {
|
|
9
|
+
import { isSourceSnapshotFlow } from "../domain/source-snapshot-flows.js";
|
|
10
|
+
import { isDeliveryFlow } from "../domain/delivery-flows.js";
|
|
10
11
|
if (!parentPort)
|
|
11
12
|
throw new AppError("controller_memory_task_invalid", "Memory worker requires its controller thread", 1);
|
|
12
13
|
const port = parentPort, cancellation = new AbortController();
|
|
@@ -18,9 +19,10 @@ let reply;
|
|
|
18
19
|
try {
|
|
19
20
|
context = createContext(env, "prepared_existing");
|
|
20
21
|
const project = requireProjectByRoot(context, task.input.projectRoot);
|
|
21
|
-
const run = context.db.get("SELECT flow_kind FROM runs WHERE project_id=? AND id=?", [project.id, task.input.runId]);
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
const run = context.db.get("SELECT flow_kind,index_json FROM runs WHERE project_id=? AND id=?", [project.id, task.input.runId]);
|
|
23
|
+
const delivery = run && isDeliveryFlow(run.flow_kind) && JSON.parse(run.index_json).execution_bundle?.flow_id === run.flow_kind;
|
|
24
|
+
if (!run || !["mb-init", "mb-upgrade", "mb-fix", "interactive"].includes(run.flow_kind) && !isSourceSnapshotFlow(run.flow_kind) && !delivery)
|
|
25
|
+
throw new AppError("controller_memory_task_invalid", "Worker requires the selected memory, interactive, source-snapshot or bound delivery RUN", 1, { effect: "no_effect" });
|
|
24
26
|
if (task.operation === "external") {
|
|
25
27
|
const work = context.db.get("SELECT project_id,run_id FROM works WHERE work_id=?", [task.input.workId]);
|
|
26
28
|
if (!work || work.project_id !== project.id || work.run_id !== task.input.runId)
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { nextAttempt } from "./runs.js";
|
|
2
|
+
import { stageAttempt, stageCycle, stageWorkIds } from "./stage-work-graph.js";
|
|
3
|
+
import { storageSessionId } from "./session-identity.js";
|
|
4
|
+
import { isObservationLoss } from "../harness-runtime/lib/operation-errors.mjs";
|
|
5
|
+
import { terminalProviderRecovery } from "../harness-runtime/lib/provider-overload-policy.mjs";
|
|
6
|
+
import { getVnextFanoutStatus } from "./vnext-fanout.js";
|
|
7
|
+
import { normalizeNativeChildren, nativeTreeReadiness } from "../harness-runtime/lib/native-children.mjs";
|
|
8
|
+
/** Disappearance from a truncated child list is not retirement evidence. */
|
|
9
|
+
export function acceptedChildrenRetired(expected, receipt, sessionId, operationId) {
|
|
10
|
+
const children = normalizeNativeChildren(receipt, sessionId);
|
|
11
|
+
const settledTree = nativeTreeReadiness(receipt, { sessionId, operationId }) === "ready"
|
|
12
|
+
&& Array.isArray(receipt.tree_session_ids) ? receipt.tree_session_ids : [];
|
|
13
|
+
return expected.every(id => {
|
|
14
|
+
const child = children.find(item => item.session_id === id);
|
|
15
|
+
return Boolean(child && ["completed", "failed", "cancelled"].includes(child.status)) || settledTree.includes(id);
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
/** Typed transport/provider failures only; integrity/ownership errors never become notes. */
|
|
19
|
+
export function lateNativeFailure(error, sessionId) {
|
|
20
|
+
const value = error;
|
|
21
|
+
return isObservationLoss(error) || isObservationLoss(value?.details?.cause)
|
|
22
|
+
|| value?.code === "settlement_recovery_required" || terminalProviderRecovery(error, sessionId);
|
|
23
|
+
}
|
|
24
|
+
/** A technical pause is an engine-recorded boundary, never an operator label. */
|
|
25
|
+
export function technicalDependencyControl(context, projectId, runId, controlId) {
|
|
26
|
+
if (!context.db.get("SELECT 1 FROM sqlite_schema WHERE name='run_controller_events'"))
|
|
27
|
+
return false;
|
|
28
|
+
return Boolean(context.db.get(`SELECT 1 FROM run_controller_events e JOIN run_controllers c ON c.controller_id=e.controller_id JOIN run_controls rc ON rc.project_id=c.project_id AND rc.run_id=c.run_id WHERE c.project_id=? AND c.run_id=? AND rc.control_id=? AND rc.action='pause' AND e.event_type='technical_dependency_paused' AND json_extract(e.data_json,'$.control_id')=rc.control_id AND json_extract(e.data_json,'$.schema_id')='dd-flow/controller-dependency@1'`, [projectId, runId, controlId]));
|
|
29
|
+
}
|
|
30
|
+
/** A technical pause may be resumed automatically only for its own accepted Work.
|
|
31
|
+
* This is a read-only predicate and is rechecked under the resume transaction. */
|
|
32
|
+
export function acceptedTechnicalWorkControl(context, row, controlId) {
|
|
33
|
+
try {
|
|
34
|
+
const control = context.db.get("SELECT request_id,recovery_id,status,action FROM run_controls WHERE project_id=? AND run_id=? AND control_id=?", [row.project_id, row.run_id, controlId]);
|
|
35
|
+
if (!control || control.action !== "pause" || control.status !== "settled")
|
|
36
|
+
return false;
|
|
37
|
+
const guard = context.db.get("SELECT recovery_id,generation,status,capture_path FROM run_recovery_guards WHERE project_id=? AND run_id=? ORDER BY generation DESC LIMIT 1", [row.project_id, row.run_id]);
|
|
38
|
+
if (!guard || guard.recovery_id !== control.recovery_id || guard.status !== "sealed" || !guard.capture_path || guard.generation <= row.generation)
|
|
39
|
+
return false;
|
|
40
|
+
const events = context.db.all("SELECT data_json FROM run_controller_events WHERE controller_id=? AND event_type='technical_dependency_paused' AND json_extract(data_json,'$.control_id')=?", [row.controller_id, controlId]);
|
|
41
|
+
if (events.length !== 1)
|
|
42
|
+
return false;
|
|
43
|
+
const event = JSON.parse(events[0].data_json);
|
|
44
|
+
if (event.schema_id !== "dd-flow/controller-dependency@1" || event.request_id !== control.request_id || event.generation !== row.generation || typeof event.source_operation_id !== "string")
|
|
45
|
+
return false;
|
|
46
|
+
const state = JSON.parse(row.state_json), session = state.current_session === null ? null : state.sessions?.[state.current_session];
|
|
47
|
+
if (!session?.id || !session.owner?.owner_id || event.state_dir !== session.state_dir)
|
|
48
|
+
return false;
|
|
49
|
+
const op = context.db.get("SELECT generation,status,input_json FROM run_controller_operations WHERE controller_id=? AND operation_id=?", [row.controller_id, event.source_operation_id]);
|
|
50
|
+
if (!op || op.generation !== row.generation || !["outcome_unknown", "completed"].includes(op.status))
|
|
51
|
+
return false;
|
|
52
|
+
const input = JSON.parse(op.input_json), expected = input.obligation;
|
|
53
|
+
if (input.owner !== session.owner.owner_id || input.state_dir !== session.state_dir || !Array.isArray(input.args) || input.args[0] !== "session" || input.args[1] !== "prompt" || input.args.indexOf("--session-id") < 0 || input.args[input.args.indexOf("--session-id") + 1] !== session.id || !expected || expected.action !== "work" || expected.stage !== state.stage || expected.generation !== row.generation || !Array.isArray(expected.work_ids) || !expected.work_ids.length || !Array.isArray(expected.work_session_ids))
|
|
54
|
+
return false;
|
|
55
|
+
const run = context.db.get("SELECT status,index_json FROM runs WHERE project_id=? AND id=?", [row.project_id, row.run_id]);
|
|
56
|
+
const stage = run && JSON.parse(run.index_json).stage_runs.find(item => item.stage === expected.stage && (item.attempt ?? "try-001") === expected.attempt);
|
|
57
|
+
if (!run || !["running", "paused"].includes(run.status) || stage?.status !== "running" || !controllerObligationAccepted(context, row, expected))
|
|
58
|
+
return false;
|
|
59
|
+
const stageWorks = stageWorkIds(context, row.project_id, row.run_id, expected.stage);
|
|
60
|
+
return expected.work_ids.every(id => stageWorks.has(id) && Boolean(context.db.get("SELECT 1 FROM work_sessions WHERE work_id=? AND status='completed'", [id])))
|
|
61
|
+
&& expected.work_session_ids.every(id => Boolean(context.db.get(`SELECT 1 FROM work_sessions WHERE id=? AND status='completed' AND work_id IN (${expected.work_ids.map(() => "?").join(",")})`, [id, ...expected.work_ids])));
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/** Persist this snapshot with the exact dispatch, never infer success from one arbitrary Work. */
|
|
68
|
+
export function controllerObligation(context, row, stage, action, assignedWorkIds) {
|
|
69
|
+
const ids = [...stageWorkIds(context, row.project_id, row.run_id, stage)];
|
|
70
|
+
const works = ids.length ? context.db.all("SELECT work_id,status,parent_work_id FROM works WHERE project_id=? AND run_id=?", [row.project_id, row.run_id]).filter(work => ids.includes(work.work_id)) : [];
|
|
71
|
+
const targets = action === "work" ? works.filter(work => work.parent_work_id && ["created", "running"].includes(work.status) && (!assignedWorkIds || assignedWorkIds.includes(work.work_id))).map(work => work.work_id) : [];
|
|
72
|
+
const sessions = targets.length ? context.db.all("SELECT ws.id,ws.work_id FROM work_sessions ws JOIN works w ON w.work_id=ws.work_id WHERE w.project_id=? AND w.run_id=? AND ws.status='running'", [row.project_id, row.run_id]).filter(session => targets.includes(session.work_id)).map(session => session.id) : [];
|
|
73
|
+
const run = context.db.get("SELECT index_json,project_root FROM runs WHERE project_id=? AND id=?", [row.project_id, row.run_id]);
|
|
74
|
+
const previous = run ? JSON.parse(run.index_json).stage_runs.find(item => item.stage === stage) : undefined;
|
|
75
|
+
return { stage, attempt: action === "entry" ? nextAttempt(previous) : stageAttempt(context, row.project_id, row.run_id, stage), cycle: stageCycle(context, row.project_id, row.run_id, stage), action, work_ids: targets, work_session_ids: sessions, generation: row.generation, ...(action === "resume" && previous?.pause ? { pause_id: previous.pause.id } : {}), ...(action === "graph" ? { graph_before: ids.sort() } : {}) };
|
|
76
|
+
}
|
|
77
|
+
export function controllerObligationAccepted(context, row, expected) {
|
|
78
|
+
if (expected.generation !== row.generation)
|
|
79
|
+
return false;
|
|
80
|
+
const run = context.db.get("SELECT index_json,project_root FROM runs WHERE project_id=? AND id=?", [row.project_id, row.run_id]);
|
|
81
|
+
if (!run)
|
|
82
|
+
return false;
|
|
83
|
+
const stages = JSON.parse(run.index_json).stage_runs;
|
|
84
|
+
const stage = [...stages].reverse().find(item => item.stage === expected.stage && (item.attempt ?? "try-001") === expected.attempt);
|
|
85
|
+
if (!stage || stageCycle(context, row.project_id, row.run_id, expected.stage) !== expected.cycle)
|
|
86
|
+
return false;
|
|
87
|
+
if (["done", "skipped"].includes(stage.status))
|
|
88
|
+
return true;
|
|
89
|
+
if (expected.action === "resume")
|
|
90
|
+
return Boolean(expected.pause_id && context.db.get("SELECT 1 FROM audit_events WHERE project_id=? AND event_type='flow_run.stage_resumed' AND json_extract(payload_json,'$.run_id')=? AND json_extract(payload_json,'$.stage')=? AND json_extract(payload_json,'$.pause_id')=?", [row.project_id, row.run_id, expected.stage, expected.pause_id]));
|
|
91
|
+
if (expected.action === "entry") {
|
|
92
|
+
// Entry-only is a boundary for fanout stages. A non-fanout entry Turn
|
|
93
|
+
// also owes semantic work: merely starting SPECIFY is not its result.
|
|
94
|
+
if (stage.status === "paused" && stage.pause)
|
|
95
|
+
return Boolean(context.db.get("SELECT 1 FROM works WHERE project_id=? AND run_id=? AND work_id=? AND status='paused'", [row.project_id, row.run_id, stage.pause.work_id]));
|
|
96
|
+
if (stage.status !== "running")
|
|
97
|
+
return false;
|
|
98
|
+
try {
|
|
99
|
+
return Boolean(getVnextFanoutStatus(context, { projectRoot: run.project_root, runId: row.run_id, stage: expected.stage }).orchestration);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (expected.action === "graph") {
|
|
106
|
+
const current = [...stageWorkIds(context, row.project_id, row.run_id, expected.stage)];
|
|
107
|
+
return Array.isArray(expected.graph_before) && current.length > expected.graph_before.length && expected.graph_before.every(id => current.includes(id));
|
|
108
|
+
}
|
|
109
|
+
if (expected.action !== "work" || !expected.work_ids.length)
|
|
110
|
+
return false;
|
|
111
|
+
return expected.work_ids.every(id => context.db.get("SELECT status FROM works WHERE project_id=? AND run_id=? AND work_id=?", [row.project_id, row.run_id, id])?.status === "completed")
|
|
112
|
+
&& expected.work_session_ids.every(id => context.db.get("SELECT status FROM work_sessions WHERE id=?", [id])?.status === "completed");
|
|
113
|
+
}
|
|
114
|
+
/** A native failure cannot revoke an accepted result; unknown association is not success. */
|
|
115
|
+
export function acceptedChildWork(context, row, harness, parent, child) {
|
|
116
|
+
const links = context.db.all(`SELECT ws.work_id,ws.status,w.status AS work_status 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 DESC,ws.rowid DESC`, [row.project_id, row.run_id, storageSessionId({ harness_id: harness, session_id: child }), storageSessionId({ harness_id: harness, session_id: parent })]);
|
|
117
|
+
const current = new Map();
|
|
118
|
+
for (const link of links)
|
|
119
|
+
if (!current.has(link.work_id))
|
|
120
|
+
current.set(link.work_id, link);
|
|
121
|
+
return current.size > 0 && [...current.values()].every(link => link.status === "completed" && link.work_status === "completed");
|
|
122
|
+
}
|
|
@@ -216,10 +216,11 @@ export function autoRefreshDashboards(context, input) {
|
|
|
216
216
|
if (projectRecoveryGuarded(context, project.id))
|
|
217
217
|
return { ok: true, skipped: true, reason: "run_recovery_guarded" };
|
|
218
218
|
const configured = readProjectConfig(context, project.id);
|
|
219
|
-
// Automatic observation must not change a memory
|
|
219
|
+
// Automatic observation must not change a memory/diagnostic baseline or
|
|
220
|
+
// invalidate accepted delivery checks by rewriting workspace projections.
|
|
220
221
|
// Keep projections in Home; do not hide runtime-created paths in Git or
|
|
221
222
|
// weaken source preservation by excluding user-owned .tasks directories.
|
|
222
|
-
const initializing = context.db.get("SELECT 1 FROM runs WHERE project_id=? AND flow_kind IN ('mb-init','mb-upgrade','mb-audit','project-review') AND (id=? OR status IN ('created','running','paused','waiting_for_user','blocked')) LIMIT 1", [project.id, input.runId ?? null]);
|
|
223
|
+
const initializing = context.db.get("SELECT 1 FROM runs WHERE project_id=? AND (flow_kind IN ('mb-init','mb-upgrade','mb-fix','mb-audit','project-review','review-fix','interactive') OR flow_kind='mb-distill' AND json_extract(index_json,'$.execution_bundle.flow_id')='mb-distill' OR flow_kind IN ('release','deploy','publish') AND json_extract(index_json,'$.execution_bundle.flow_id')=flow_kind) AND (id=? OR status IN ('created','running','paused','waiting_for_user','blocked')) LIMIT 1", [project.id, input.runId ?? null]);
|
|
223
224
|
const config = initializing ? { ...configured, dashboard: { ...configured.dashboard,
|
|
224
225
|
markdown_path: path.join(projectDashboardDir(context.ddFlowHome, project.id), "project-dashboard.md") } } : configured;
|
|
225
226
|
if (!config.dashboard.auto_refresh) {
|
|
@@ -1307,6 +1308,7 @@ function normalizeFlowKind(flowKind) {
|
|
|
1307
1308
|
"mb-upgrade",
|
|
1308
1309
|
"mb-audit",
|
|
1309
1310
|
"mb-fix",
|
|
1311
|
+
"review-fix",
|
|
1310
1312
|
"mb-distill",
|
|
1311
1313
|
"mb-upgrade-review",
|
|
1312
1314
|
"mb-sdlc-review",
|