@deksden-com/dd-flow-cli 0.9.0-beta.144 → 0.9.0-beta.149
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 +130 -0
- package/README.md +13 -8
- package/dist/build-info.json +7 -7
- package/dist/cli/command-inputs.js +13 -3
- package/dist/cli/command-metadata.js +51 -16
- package/dist/cli/diagnostic-invocation.js +46 -0
- package/dist/cli/diagnostic.js +120 -0
- package/dist/cli/help.js +67 -1
- package/dist/cli/hook-ingress.js +27 -3
- package/dist/cli/memory-flow-admission.js +9 -3
- package/dist/cli/memory-init-invocation.js +10 -3
- package/dist/cli/memory-init.js +77 -68
- package/dist/cli/memory-storage-admission.js +3 -2
- package/dist/cli/memory-storage-qualification.js +105 -0
- package/dist/cli/memory-storage-transition.js +123 -0
- package/dist/cli/memory-upgrade.js +189 -0
- package/dist/cli/native-hook-ingress.js +14 -4
- package/dist/cli/native-hook-preparation.js +55 -0
- package/dist/cli/response-file.js +2 -1
- package/dist/cli/run-cli.js +79 -40
- package/dist/cli/setup.js +2 -1
- package/dist/cli.js +18 -0
- package/dist/domain/diagnostic-aspects.js +103 -0
- package/dist/domain/diagnostic-flows.js +8 -0
- package/dist/domain/stage-handler-identities.js +8 -0
- package/dist/harness-runtime/bin/dd-agy.mjs +3 -3
- package/dist/harness-runtime/bin/dd-grok.mjs +3 -3
- package/dist/harness-runtime/bin/dd-zcode.mjs +5 -5
- package/dist/harness-runtime/lib/agy-error-boundary.mjs +58 -0
- package/dist/harness-runtime/lib/agy-native-inspection.mjs +216 -0
- package/dist/harness-runtime/lib/agy-native_pb.js +117 -0
- package/dist/harness-runtime/lib/daemon-operations.d.mts +1 -0
- package/dist/harness-runtime/lib/daemon-operations.mjs +84 -10
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +366 -52
- package/dist/harness-runtime/lib/dd-agy.mjs +17 -3
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +27 -13
- package/dist/harness-runtime/lib/dd-codex.mjs +4 -0
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +4 -3
- package/dist/harness-runtime/lib/dd-droid.mjs +71 -29
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +73 -16
- package/dist/harness-runtime/lib/dd-grok.mjs +6 -3
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +66 -20
- package/dist/harness-runtime/lib/dd-opencode.mjs +30 -3
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +7 -6
- package/dist/harness-runtime/lib/dd-zcode.mjs +44 -14
- package/dist/harness-runtime/lib/delegation-instructions.mjs +1 -1
- package/dist/harness-runtime/lib/driver-recovery.mjs +9 -8
- package/dist/harness-runtime/lib/entity-references.d.mts +2 -0
- package/dist/harness-runtime/lib/entity-references.mjs +9 -0
- package/dist/harness-runtime/lib/managed-daemon.d.mts +1 -1
- package/dist/harness-runtime/lib/managed-daemon.mjs +24 -9
- package/dist/harness-runtime/lib/native-children.d.mts +8 -1
- package/dist/harness-runtime/lib/native-children.mjs +114 -8
- package/dist/harness-runtime/lib/native-hook-command.mjs +4 -4
- package/dist/harness-runtime/lib/native-request-proof.mjs +1 -1
- package/dist/harness-runtime/lib/observation-clock.d.mts +2 -2
- package/dist/harness-runtime/lib/observation-clock.mjs +7 -4
- package/dist/harness-runtime/lib/opencode-plugin.mjs +3 -3
- package/dist/harness-runtime/lib/operation-errors.mjs +1 -1
- package/dist/harness-runtime/lib/provider-overload-policy.d.mts +5 -3
- package/dist/harness-runtime/lib/provider-overload-policy.mjs +20 -7
- package/dist/harness-runtime/lib/runner-events.mjs +3 -1
- package/dist/harness-runtime/lib/setup-progress.mjs +8 -6
- package/dist/harness-runtime/lib/tool-observations.mjs +1 -1
- package/dist/schemas/diagnostic-stage-result.schema.json +354 -0
- package/dist/schemas/diagnostic-work-result.schema.json +296 -0
- package/dist/schemas/execution-bundle.schema.json +7 -1
- package/dist/schemas/mb-upgrade-stage-result.schema.json +863 -0
- package/dist/schemas/memory-repair-work-result.schema.json +9 -2
- package/dist/schemas/project-flow-pack-manifest.schema.json +1 -1
- package/dist/schemas/release-impact.schema.json +3 -3
- package/dist/schemas/stage-start-response.schema.json +4 -0
- package/dist/services/accepted-intent.js +13 -1
- package/dist/services/canon.js +43 -7
- package/dist/services/check-environment.js +21 -0
- package/dist/services/check-runner-evidence.js +267 -15
- package/dist/services/cli-operation-classifier.js +1 -1
- package/dist/services/cli-upgrade-allowlist.js +1 -1
- package/dist/services/code-checks.js +34 -19
- package/dist/services/compatibility-preflight.js +5 -2
- package/dist/services/controller-execution.js +10 -4
- package/dist/services/controller-fanout.js +34 -10
- package/dist/services/controller-memory-task.js +45 -0
- package/dist/services/controller-memory-worker.js +49 -0
- package/dist/services/dashboard.js +2 -2
- package/dist/services/descriptor-run-settlement.js +5 -3
- package/dist/services/descriptor-run-start.js +46 -20
- package/dist/services/descriptor-stage-coordinator.js +66 -13
- package/dist/services/descriptor-stage-settlement.js +2 -1
- package/dist/services/descriptor-stage-start.js +40 -4
- package/dist/services/diagnostic-evidence.js +73 -0
- package/dist/services/diagnostic-report-publication.js +72 -0
- package/dist/services/diagnostic-report.js +67 -0
- package/dist/services/diagnostic-run-bundle.js +74 -0
- package/dist/services/diagnostic-snapshot.js +159 -0
- package/dist/services/diagnostic-stage-handlers.js +108 -0
- package/dist/services/diagnostic-stage-results.js +132 -0
- package/dist/services/diagnostic-start.js +84 -0
- package/dist/services/diagnostic-work-batch.js +34 -0
- package/dist/services/diagnostic-work-collection.js +97 -0
- package/dist/services/diagnostic-work-registration.js +101 -0
- package/dist/services/diagnostic-work-results.js +93 -0
- package/dist/services/engines.js +64 -14
- package/dist/services/eval-snapshots.js +186 -119
- package/dist/services/execution-bundles.js +19 -7
- package/dist/services/external-work-launch.js +5 -3
- package/dist/services/flow-catalog.js +13 -4
- package/dist/services/frozen-engine.js +148 -11
- package/dist/services/hook-memory-routing.js +188 -0
- package/dist/services/hook-recovery-guard.js +14 -0
- package/dist/services/hook-session-observation.js +24 -0
- package/dist/services/hooks.js +27 -8
- package/dist/services/hybrid-contracts.js +4 -13
- package/dist/services/hybrid-digest.js +14 -0
- package/dist/services/installed-flow-source.js +47 -4
- package/dist/services/lifecycle-invocations.js +179 -38
- package/dist/services/memory-accepted-composition.js +5 -4
- package/dist/services/memory-completed-correction-wave.js +23 -15
- package/dist/services/memory-compose-handler.js +163 -21
- package/dist/services/memory-composition-acceptance.js +186 -10
- package/dist/services/memory-composition-graph.js +42 -11
- package/dist/services/memory-composition-registration.js +23 -8
- package/dist/services/memory-composition-work-custody.js +14 -9
- package/dist/services/memory-composition-work-results.js +15 -3
- package/dist/services/memory-corrected-review-batch.js +10 -3
- package/dist/services/memory-corrected-review-findings.js +4 -2
- package/dist/services/memory-corrected-review-inputs.js +10 -5
- package/dist/services/memory-corrected-review-receipt.js +17 -9
- package/dist/services/memory-corrected-review-registration.js +3 -2
- package/dist/services/memory-correction-check-receipt.js +20 -4
- package/dist/services/memory-custody-snapshot.js +36 -0
- package/dist/services/memory-finish-evidence.js +44 -22
- package/dist/services/memory-finish-handler.js +37 -23
- package/dist/services/memory-finish-integration.js +218 -48
- package/dist/services/memory-finish-readiness.js +66 -19
- package/dist/services/memory-git-integration.js +87 -18
- package/dist/services/memory-historical-corrected-review.js +21 -15
- package/dist/services/memory-historical-original-review.js +22 -9
- package/dist/services/memory-historical-repair-registration.js +14 -8
- package/dist/services/memory-historical-second-review.js +30 -11
- package/dist/services/memory-init-contexts.js +44 -2
- package/dist/services/memory-init-controller-start.js +27 -8
- package/dist/services/memory-init-git-policy.js +50 -21
- package/dist/services/memory-init-lint-capability.js +24 -6
- package/dist/services/memory-init-native-capacity.js +17 -4
- package/dist/services/memory-init-preflight.js +21 -17
- package/dist/services/memory-init-request-candidate.js +73 -17
- package/dist/services/memory-init-request-input.js +23 -7
- package/dist/services/memory-init-run-bundle.js +49 -12
- package/dist/services/memory-init-storage.js +17 -7
- package/dist/services/memory-init-worktrunk-capability.js +3 -7
- package/dist/services/memory-layer.js +71 -6
- package/dist/services/memory-lint-config.js +111 -2
- package/dist/services/memory-native-review-copy.js +5 -67
- package/dist/services/memory-normal-flow-entry.js +12 -0
- package/dist/services/memory-ordinary-defaults.js +22 -5
- package/dist/services/memory-preflight-handler.js +15 -0
- package/dist/services/memory-repair-current-view.js +1 -1
- package/dist/services/memory-repair-document-delta.js +70 -33
- package/dist/services/memory-repair-history.js +12 -7
- package/dist/services/memory-repair-physical-binding.js +1 -119
- package/dist/services/memory-repair-publication-plan.js +5 -3
- package/dist/services/memory-repair-publication-source.js +1 -1
- package/dist/services/memory-repair-publication.js +3 -2
- package/dist/services/memory-repair-registration.js +2 -1
- package/dist/services/memory-repair-work-baseline.js +4 -2
- package/dist/services/memory-repair-work-completion.js +32 -12
- package/dist/services/memory-repair-work-results.js +21 -2
- package/dist/services/memory-repair-workspace.js +11 -2
- package/dist/services/memory-repair-writer-copy.js +2 -1
- package/dist/services/memory-result-binding.js +78 -12
- package/dist/services/memory-retained-review-authority.js +17 -8
- package/dist/services/memory-retained-second-baseline.js +1 -1
- package/dist/services/memory-retained-second-repairs.js +12 -6
- package/dist/services/memory-review-correction-plan.js +47 -12
- package/dist/services/memory-review-handler.js +17 -17
- package/dist/services/memory-review-inputs.js +55 -9
- package/dist/services/memory-review-limits.js +10 -0
- package/dist/services/memory-review-receipt.js +2 -2
- package/dist/services/memory-review-registration.js +13 -4
- package/dist/services/memory-review-work-custody.js +3 -1
- package/dist/services/memory-review-work-input.js +26 -7
- package/dist/services/memory-review-work-results.js +26 -4
- package/dist/services/memory-second-corrected-review-inputs.js +5 -3
- package/dist/services/memory-second-correction-plan.js +2 -1
- package/dist/services/memory-second-repair-launch.js +4 -4
- package/dist/services/memory-second-repair-source.js +1 -1
- package/dist/services/memory-stage-dispatch.js +38 -1
- package/dist/services/memory-stage-results.js +113 -22
- package/dist/services/memory-tool-probe.js +25 -0
- package/dist/services/memory-upgrade-analysis-handler.js +243 -0
- package/dist/services/memory-upgrade-bootstrap-readiness.js +251 -0
- package/dist/services/memory-upgrade-evidence.js +100 -0
- package/dist/services/memory-upgrade-inventory.js +442 -0
- package/dist/services/memory-upgrade-publication-custody.js +73 -0
- package/dist/services/memory-upgrade-publication.js +413 -0
- package/dist/services/memory-upgrade-selection.js +430 -0
- package/dist/services/memory-upgrade-start.js +60 -0
- package/dist/services/memory-upgrade-storage-readiness.js +340 -0
- package/dist/services/memory-verification-acceptance.js +38 -17
- package/dist/services/memory-verification-checks.js +92 -27
- package/dist/services/memory-verification-receipt.js +6 -5
- package/dist/services/memory-verify-handler.js +15 -13
- package/dist/services/memory-work-result-json.js +12 -0
- package/dist/services/memory-work-result-preparation.js +5 -4
- package/dist/services/merge-server.js +4 -2
- package/dist/services/migrations.js +127 -98
- package/dist/services/native-daemon-history.js +2 -1
- package/dist/services/physical-startup.js +45 -0
- package/dist/services/project-registry.js +97 -0
- package/dist/services/project-summary.js +1 -1
- package/dist/services/projects.js +3 -93
- package/dist/services/prompt-delivery.js +71 -0
- package/dist/services/provider-capacity.js +112 -46
- package/dist/services/recovery-snapshot-database.js +32 -4
- package/dist/services/release-cache.js +8 -0
- package/dist/services/repair-intents.js +1 -12
- package/dist/services/repair-receipt.js +13 -0
- package/dist/services/run-control-worker.js +35 -18
- package/dist/services/run-control.js +2 -1
- package/dist/services/run-controller-adapter.js +2 -1
- package/dist/services/run-controller-process.js +4 -7
- package/dist/services/run-controller-recovery.js +8 -2
- package/dist/services/run-controller.js +215 -94
- package/dist/services/run-fork.js +1 -1
- package/dist/services/run-recovery-guard.js +30 -0
- package/dist/services/run-recovery.js +11 -44
- package/dist/services/run-start-custody.js +17 -5
- package/dist/services/runs.js +6 -1
- package/dist/services/runtime-scope-resume.js +12 -6
- package/dist/services/runtime-scope-stop.js +3 -2
- package/dist/services/runtime-scope-worker.js +46 -19
- package/dist/services/runtime-service.js +2 -1
- package/dist/services/sessions.js +3 -24
- package/dist/services/setup.js +63 -13
- package/dist/services/stage-handlers.js +22 -7
- package/dist/services/stage-pause.js +111 -44
- package/dist/services/version-status.js +1 -1
- package/dist/services/vnext-code.js +2 -2
- package/dist/services/vnext-fanout.js +51 -9
- package/dist/services/vnext-plan-review.js +3 -5
- package/dist/services/work-registry.js +117 -39
- package/dist/services/workspace-bootstrap.js +2 -1
- package/dist/services/workspace-files.js +1 -1
- package/dist/shared/entity-references.js +1 -8
- package/dist/storage/data-preservation.js +65 -0
- package/dist/storage/database.js +53 -19
- package/dist/storage/import-guard.js +84 -27
- package/dist/storage/memory-upgrade-schema-bootstrap.js +38 -0
- package/dist/storage/paired-writer-migration.js +424 -0
- package/dist/storage/writer-contract.js +11 -4
- package/dist/storage/writer-migration.js +313 -52
- package/dist/storage/writer-restore.js +217 -0
- package/package.json +13 -6
|
@@ -10,7 +10,7 @@ export function isUpgradeAllowlisted(args) {
|
|
|
10
10
|
return true;
|
|
11
11
|
if (family === "engine" && command === "resolve")
|
|
12
12
|
return true;
|
|
13
|
-
if (family === "migration" && ["impact", "plan", "report", "verify", "apply"].includes(command ?? ""))
|
|
13
|
+
if (family === "migration" && ["impact", "readiness", "plan", "report", "verify", "apply"].includes(command ?? ""))
|
|
14
14
|
return true;
|
|
15
15
|
if (family === "memory" && command === "permissions" && parsed.positional[2] === "preflight")
|
|
16
16
|
return true;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
|
-
import { candidateFailureFiles, checkRepairDisposition, readRunnerEvidence, readRunnerProgress, runnerContractsValid, runnerEvidencePass, runnerObservationDurations, runnerProofArtifacts } from "./check-runner-evidence.js";
|
|
2
|
+
import { candidateAbsencesValid, candidateFailureAbsences, candidateFailureFiles, checkRepairDisposition, readRunnerEvidence, readRunnerProgress, runnerContractsValid, runnerEvidencePass, runnerObservationDurations, runnerProofArtifacts } from "./check-runner-evidence.js";
|
|
3
3
|
export { assertSourceRepairEligible, 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";
|
|
@@ -16,6 +16,7 @@ import { confirmManagedProcess, finishManagedProcess, renewManagedProcess, manag
|
|
|
16
16
|
import { admitRuntimeCheck, runtimeBudgetForRun } from "./runtime-budget.js";
|
|
17
17
|
import { getResourceDatabase } from "../storage/database.js";
|
|
18
18
|
import { readBoundedReleaseFile } from "./release-source.js";
|
|
19
|
+
import { ownedCheckEnvironment, validateCheckEnvironment } from "./check-environment.js";
|
|
19
20
|
const profileRelativePath = path.join(".memory-bank", "spec", "engineering", "code-check-profile.json");
|
|
20
21
|
// Receipt reservation precedes child spawn so an interrupted CLI leaves an
|
|
21
22
|
// auditable attempt. A second caller must not mistake that small handoff window
|
|
@@ -61,10 +62,7 @@ export function terminalCheckSettled(context, record) {
|
|
|
61
62
|
}
|
|
62
63
|
}
|
|
63
64
|
export function codeExecutionEnvironment(workspaceRoot, checkEnvironment) {
|
|
64
|
-
|
|
65
|
-
|| Object.keys(checkEnvironment).length !== 1 || typeof checkEnvironment.CODEX_HOME !== "string"
|
|
66
|
-
|| checkEnvironment.CODEX_HOME.length > 4096 || checkEnvironment.CODEX_HOME.includes("\0") || !path.isAbsolute(checkEnvironment.CODEX_HOME)))
|
|
67
|
-
throw new AppError("invalid_check_environment", "Owned check environment must name one absolute Codex home", 2, { effect: "no_effect" });
|
|
65
|
+
const selected = validateCheckEnvironment(checkEnvironment);
|
|
68
66
|
const suffix = crypto.createHash("sha256").update(path.resolve(workspaceRoot)).digest("hex").slice(0, 12);
|
|
69
67
|
// The managed CLI's Node must win over a provider app's PATH: a Rosetta
|
|
70
68
|
// provider can otherwise run x64 checks against arm64-installed modules.
|
|
@@ -72,8 +70,7 @@ export function codeExecutionEnvironment(workspaceRoot, checkEnvironment) {
|
|
|
72
70
|
delete environment.NODE_PATH;
|
|
73
71
|
for (const key of ["DD_FLOW_CHECK_RUNNER_EVIDENCE_PATH", "DD_FLOW_CHECK_RUNNER_PROGRESS_PATH", "DD_FLOW_CHECK_INVOCATION_ID", "DD_FLOW_CHECK_ID", "DD_FLOW_CHECK_INPUT_HASH", "DD_FLOW_CHECK_VERIFICATION_EPOCH", "DD_FLOW_CHECK_COMMAND"])
|
|
74
72
|
delete environment[key];
|
|
75
|
-
|
|
76
|
-
environment.CODEX_HOME = checkEnvironment.CODEX_HOME;
|
|
73
|
+
Object.assign(environment, selected);
|
|
77
74
|
return environment;
|
|
78
75
|
}
|
|
79
76
|
export function workspaceOperationId(workspaceRoot) { return `workspace:${crypto.createHash("sha256").update(path.resolve(workspaceRoot)).digest("hex")}`; }
|
|
@@ -302,6 +299,9 @@ export class PreparedCheckEvidence {
|
|
|
302
299
|
Object.freeze(this);
|
|
303
300
|
}
|
|
304
301
|
static prepare(context, input) {
|
|
302
|
+
const checkEnvironment = ownedCheckEnvironment(context.env, input.checkEnvironment);
|
|
303
|
+
if (checkEnvironment)
|
|
304
|
+
input = { ...input, checkEnvironment };
|
|
305
305
|
validateCheckEvidenceContext(input.evidenceContext);
|
|
306
306
|
const fail = () => { throw new AppError("check_evidence_stale", "Current declared checks lack owned passed evidence", 2, { effect: "no_effect" }); };
|
|
307
307
|
if (!input.checks.length || (input.scope === "work") !== Boolean(input.workId) || input.mode !== undefined && input.mode !== "diagnostic")
|
|
@@ -343,7 +343,13 @@ export class PreparedCheckEvidence {
|
|
|
343
343
|
const references = deduplicate(checks, commands).map(group => {
|
|
344
344
|
const inputHash = checkInputHash(input.workspaceRoot, group, profileHash, workspaceInputFingerprint(input.workspaceRoot, state.files, group.inputs, [input.runHome]), input.evidenceContext, input.checkEnvironment);
|
|
345
345
|
const receipt = receipts.filter(item => item.input_hash === inputHash && item.profile_hash === profileHash && item.scope === input.scope && item.work_id === (input.workId ?? null)).at(-1);
|
|
346
|
-
if (!receipt
|
|
346
|
+
if (!receipt)
|
|
347
|
+
throw new AppError("check_evidence_stale", "Current declared checks lack owned passed evidence", 2, {
|
|
348
|
+
effect: "no_effect", reason: "input_identity_mismatch", expected_input_hash: inputHash,
|
|
349
|
+
candidates: receipts.filter(item => item.scope === input.scope && item.work_id === (input.workId ?? null))
|
|
350
|
+
.slice(-16).map(item => ({ id: item.id, input_hash: item.input_hash, profile_hash: item.profile_hash }))
|
|
351
|
+
});
|
|
352
|
+
if (!checkReceiptEvidenceValidForMode(bindReceipt(receipt, group), input.mode))
|
|
347
353
|
fail();
|
|
348
354
|
passed = passed && receipt.status === "passed";
|
|
349
355
|
for (const artifact of receipt.artifacts)
|
|
@@ -482,6 +488,9 @@ function checkEvidenceSqlSnapshot(context, input) {
|
|
|
482
488
|
}));
|
|
483
489
|
}
|
|
484
490
|
export async function runCodeChecks(context, input) {
|
|
491
|
+
const checkEnvironment = ownedCheckEnvironment(context.env, input.checkEnvironment);
|
|
492
|
+
if (checkEnvironment)
|
|
493
|
+
input = { ...input, checkEnvironment };
|
|
485
494
|
validateCheckEvidenceContext(input.evidenceContext);
|
|
486
495
|
const generation = assertRunMutationAllowed(context, input.projectId, input.runId);
|
|
487
496
|
let gate;
|
|
@@ -652,8 +661,9 @@ async function runCodeChecksOwned(context, input, workspaceGateId, generation, s
|
|
|
652
661
|
if (result.failure)
|
|
653
662
|
physicalFailures.push(result.failure);
|
|
654
663
|
const candidateFiles = candidateFailureFiles(runnerEvidence, input.workspaceRoot);
|
|
664
|
+
const candidateAbsences = candidateFailureAbsences(runnerEvidence, input.workspaceRoot);
|
|
655
665
|
if (group.runnerEvidence)
|
|
656
|
-
retainExecutionOutcome(evidenceDir, { id, command: group.command, input_hash: inputHash, verification_epoch: epoch, fingerprint_scope: "check_inputs", input_manifest: reserved.inputManifest ?? null, execution_contract_hash: executionContractHash }, input.workspaceRoot, physicalFailures, candidateFiles);
|
|
666
|
+
retainExecutionOutcome(evidenceDir, { id, command: group.command, input_hash: inputHash, verification_epoch: epoch, fingerprint_scope: "check_inputs", input_manifest: reserved.inputManifest ?? null, execution_contract_hash: executionContractHash }, input.workspaceRoot, physicalFailures, candidateFiles, candidateAbsences);
|
|
657
667
|
const requiredArtifacts = [...new Set([...group.requiredArtifacts, ...runnerProofArtifacts(runnerEvidence), ...(group.runnerEvidence ? ["engine-outcome.json"] : [])])];
|
|
658
668
|
artifacts = collectCheckArtifacts(evidenceDir, requiredArtifacts);
|
|
659
669
|
const status = result.aborted ? "aborted" : result.exitCode === 0 && !result.error && artifacts.complete && mutationPaths.length === 0 && (!group.runnerEvidence || runnerEvidencePass(runnerEvidence)) ? "passed" : "failed";
|
|
@@ -661,6 +671,7 @@ async function runCodeChecksOwned(context, input, workspaceGateId, generation, s
|
|
|
661
671
|
receipt.required_artifacts = requiredArtifacts;
|
|
662
672
|
receipt.candidate_root = input.workspaceRoot;
|
|
663
673
|
receipt.candidate_files = candidateFiles;
|
|
674
|
+
receipt.candidate_absences = candidateAbsences;
|
|
664
675
|
receipt.execution_failures = physicalFailures;
|
|
665
676
|
context.db.run("UPDATE check_receipts SET execution_failures_json = ? WHERE project_id = ? AND id = ? AND status = 'running'", [JSON.stringify(physicalFailures), input.projectId, id]);
|
|
666
677
|
const completion = readCompletion(completionPath);
|
|
@@ -748,6 +759,7 @@ export function reconcileUnfinishedChecks(context, input) {
|
|
|
748
759
|
if (scoped && !completionSnapshot)
|
|
749
760
|
physicalFailures.push({ code: "completion_snapshot_unknown", message: "Detached check has no bound scoped completion snapshot; current inputs cannot prove inputs at command completion" });
|
|
750
761
|
let candidateFiles = candidateFailureFiles(runnerEvidence, input.workspaceRoot);
|
|
762
|
+
let candidateAbsences = candidateFailureAbsences(runnerEvidence, input.workspaceRoot);
|
|
751
763
|
if (receipt?.runner_evidence_required) {
|
|
752
764
|
const evidenceDir = path.join(path.dirname(pendingReceipt.receipt_path), "artifacts");
|
|
753
765
|
if (fs.existsSync(path.join(evidenceDir, "engine-outcome.json"))) {
|
|
@@ -757,13 +769,14 @@ export function reconcileUnfinishedChecks(context, input) {
|
|
|
757
769
|
if (proven.candidate_root) {
|
|
758
770
|
physicalFailures.push(...(proven.execution_failures ?? []));
|
|
759
771
|
candidateFiles = proven.candidate_files ?? candidateFiles;
|
|
772
|
+
candidateAbsences = proven.candidate_absences ?? [];
|
|
760
773
|
}
|
|
761
774
|
else {
|
|
762
775
|
physicalFailures.push({ code: "engine_outcome_unbound", message: "Existing execution outcome is not bound to retained inventory; prior launcher/cleanup outcome is unknown" });
|
|
763
776
|
fs.renameSync(path.join(evidenceDir, "engine-outcome.json"), path.join(evidenceDir, `engine-outcome-unconfirmed-${crypto.randomUUID()}.json`));
|
|
764
777
|
}
|
|
765
778
|
}
|
|
766
|
-
retainExecutionOutcome(evidenceDir, receipt, input.workspaceRoot, physicalFailures, candidateFiles);
|
|
779
|
+
retainExecutionOutcome(evidenceDir, receipt, input.workspaceRoot, physicalFailures, candidateFiles, candidateAbsences);
|
|
767
780
|
receipt.required_artifacts = [...new Set([...(receipt.required_artifacts ?? []), ...runnerProofArtifacts(runnerEvidence), "engine-outcome.json"])];
|
|
768
781
|
artifacts = collectCheckArtifacts(evidenceDir, receipt.required_artifacts);
|
|
769
782
|
}
|
|
@@ -776,6 +789,7 @@ export function reconcileUnfinishedChecks(context, input) {
|
|
|
776
789
|
const repaired = { ...receipt, ...(receipt.runner_evidence_required ? { runner_evidence: runnerEvidence ?? null } : {}), completion_path: receipt.completion_path || path.join(path.dirname(pendingReceipt.receipt_path), "completion.json"), required_artifacts: receipt.required_artifacts ?? [], ...(status === "aborted" ? { abort_reason: "completion_marker_missing" } : {}), status, exit_code: exitCode, after_fingerprint: afterFingerprint, mutation_paths: mutationPaths, artifacts: artifacts.items, finished_at: finishedAt };
|
|
777
790
|
repaired.candidate_root = input.workspaceRoot;
|
|
778
791
|
repaired.candidate_files = candidateFiles;
|
|
792
|
+
repaired.candidate_absences = candidateAbsences;
|
|
779
793
|
repaired.execution_failures = physicalFailures;
|
|
780
794
|
writeReceipt(pendingReceipt.receipt_path, repaired);
|
|
781
795
|
if (status === "failed")
|
|
@@ -974,12 +988,12 @@ function resolvedReceiptArtifacts(receiptPath, artifacts, binding) {
|
|
|
974
988
|
if (retainedOutcome.complete && retainedOutcome.items.every(item => artifacts.some(artifact => relocate(artifact.path) === item.path && artifact.sha256 === item.sha256))) {
|
|
975
989
|
try {
|
|
976
990
|
const value = JSON.parse(fs.readFileSync(outcomePath, "utf8"));
|
|
977
|
-
if (value.schema_id === "dd-flow/check-execution-outcome@1" && value.id === binding.id && value.command === binding.command && value.input_hash === binding.input_hash && value.verification_epoch === binding.verification_epoch && (value.fingerprint_scope ?? null) === (binding.fingerprint_scope ?? null) && (value.execution_contract_hash ?? null) === (binding.execution_contract_hash ?? null) && JSON.stringify(value.input_manifest ?? null) === JSON.stringify(binding.input_manifest ?? null) && typeof value.candidate_root === "string" && path.isAbsolute(value.candidate_root) && (value.candidate_files === undefined || Array.isArray(value.candidate_files) && value.candidate_files.length <= 64 && value.candidate_files.every((file) => typeof file === "string" && path.isAbsolute(file) && Buffer.byteLength(file) <= 4096)) && Array.isArray(value.execution_failures) && value.execution_failures.every((failure) => failure && typeof failure === "object" && typeof failure.code === "string" && typeof failure.message === "string"))
|
|
991
|
+
if (value.schema_id === "dd-flow/check-execution-outcome@1" && value.id === binding.id && value.command === binding.command && value.input_hash === binding.input_hash && value.verification_epoch === binding.verification_epoch && (value.fingerprint_scope ?? null) === (binding.fingerprint_scope ?? null) && (value.execution_contract_hash ?? null) === (binding.execution_contract_hash ?? null) && JSON.stringify(value.input_manifest ?? null) === JSON.stringify(binding.input_manifest ?? null) && typeof value.candidate_root === "string" && path.isAbsolute(value.candidate_root) && (value.candidate_files === undefined || Array.isArray(value.candidate_files) && value.candidate_files.length <= 64 && value.candidate_files.every((file) => typeof file === "string" && path.isAbsolute(file) && Buffer.byteLength(file) <= 4096)) && (value.candidate_absences === undefined || candidateAbsencesValid(value.candidate_absences)) && Array.isArray(value.execution_failures) && value.execution_failures.every((failure) => failure && typeof failure === "object" && typeof failure.code === "string" && typeof failure.message === "string"))
|
|
978
992
|
outcome = value;
|
|
979
993
|
}
|
|
980
994
|
catch { /* Missing physical proof cannot grant attribution. */ }
|
|
981
995
|
}
|
|
982
|
-
return { completion_path: path.join(currentRoot, "completion.json"), required_artifacts: original.required_artifacts ?? [], inputs: original.inputs ?? [], artifacts: artifacts.map((artifact) => ({ ...artifact, path: relocate(artifact.path) })), ...(original.runner_evidence_required ? { runner_evidence_required: original.runner_evidence_required, runner_evidence: evidence } : {}), execution_failures: outcome.execution_failures ?? [], ...(outcome.candidate_root ? { candidate_root: outcome.candidate_root } : {}), ...(outcome.candidate_files ? { candidate_files: outcome.candidate_files } : {}) };
|
|
996
|
+
return { completion_path: path.join(currentRoot, "completion.json"), required_artifacts: original.required_artifacts ?? [], inputs: original.inputs ?? [], artifacts: artifacts.map((artifact) => ({ ...artifact, path: relocate(artifact.path) })), ...(original.runner_evidence_required ? { runner_evidence_required: original.runner_evidence_required, runner_evidence: evidence } : {}), execution_failures: outcome.execution_failures ?? [], ...(outcome.candidate_root ? { candidate_root: outcome.candidate_root } : {}), ...(outcome.candidate_files ? { candidate_files: outcome.candidate_files } : {}), ...(outcome.candidate_absences ? { candidate_absences: outcome.candidate_absences } : {}) };
|
|
983
997
|
}
|
|
984
998
|
function executionFailures(input) {
|
|
985
999
|
const failures = [];
|
|
@@ -997,18 +1011,18 @@ function executionFailures(input) {
|
|
|
997
1011
|
failures.push({ code: "invalid_runner_evidence", message: "Required runner evidence is unavailable or invalid" });
|
|
998
1012
|
return failures;
|
|
999
1013
|
}
|
|
1000
|
-
function retainExecutionOutcome(evidenceDir, binding, candidateRoot, failures, candidateFiles) {
|
|
1014
|
+
function retainExecutionOutcome(evidenceDir, binding, candidateRoot, failures, candidateFiles, candidateAbsences = []) {
|
|
1001
1015
|
const temporary = path.join(evidenceDir, `.engine-outcome-${crypto.randomUUID()}.tmp`);
|
|
1002
1016
|
if (!fs.lstatSync(evidenceDir).isDirectory() || fs.lstatSync(evidenceDir).isSymbolicLink())
|
|
1003
1017
|
throw new AppError("check_evidence_stale", "Check evidence directory was replaced", 1);
|
|
1004
|
-
fs.writeFileSync(temporary, JSON.stringify({ schema_id: "dd-flow/check-execution-outcome@1", id: binding.id, command: binding.command, input_hash: binding.input_hash, verification_epoch: binding.verification_epoch, fingerprint_scope: binding.fingerprint_scope ?? null, input_manifest: binding.input_manifest ?? null, execution_contract_hash: binding.execution_contract_hash ?? null, candidate_root: candidateRoot, candidate_files: candidateFiles, execution_failures: failures }), { flag: "wx", mode: 0o600 });
|
|
1018
|
+
fs.writeFileSync(temporary, JSON.stringify({ schema_id: "dd-flow/check-execution-outcome@1", id: binding.id, command: binding.command, input_hash: binding.input_hash, verification_epoch: binding.verification_epoch, fingerprint_scope: binding.fingerprint_scope ?? null, input_manifest: binding.input_manifest ?? null, execution_contract_hash: binding.execution_contract_hash ?? null, candidate_root: candidateRoot, candidate_files: candidateFiles, candidate_absences: candidateAbsences, execution_failures: failures }), { flag: "wx", mode: 0o600 });
|
|
1005
1019
|
fs.renameSync(temporary, path.join(evidenceDir, "engine-outcome.json"));
|
|
1006
1020
|
}
|
|
1007
1021
|
export function workspaceFingerprint(root) { return workspaceState(root).fingerprint; }
|
|
1008
1022
|
/** One NUL-aware source of truth for paths differing from HEAD. Rename/copy
|
|
1009
1023
|
* records carry a second path; retain both because the old side is a deletion. */
|
|
1010
1024
|
export function workspaceChangedPaths(root) {
|
|
1011
|
-
const status = spawnSync("git", ["status", "--porcelain=v1", "-z", "--untracked-files=all"], { cwd: root, encoding: "utf8", maxBuffer: 64 * 1024 * 1024 });
|
|
1025
|
+
const status = spawnSync("git", ["status", "--porcelain=v1", "-z", "--untracked-files=all"], { cwd: root, env: { ...process.env, LC_ALL: "C" }, encoding: "utf8", maxBuffer: 64 * 1024 * 1024 });
|
|
1012
1026
|
// A workspace can be intentionally gitless. Keep the same conservative
|
|
1013
1027
|
// file inventory used for fingerprints instead of making result validation
|
|
1014
1028
|
// fail before the work contract can report its actual error.
|
|
@@ -1041,7 +1055,7 @@ export function ignoredWorkspacePaths(root, paths) {
|
|
|
1041
1055
|
const normalized = [...new Set(paths.map((item) => normalizeWorkspacePath(root, item)))];
|
|
1042
1056
|
if (!normalized.length)
|
|
1043
1057
|
return [];
|
|
1044
|
-
const status = spawnSync("git", ["check-ignore", "-z", "--stdin"], { cwd: root, encoding: "utf8", input: `${normalized.join("\0")}\0`, maxBuffer: 64 * 1024 * 1024 });
|
|
1058
|
+
const status = spawnSync("git", ["check-ignore", "-z", "--stdin"], { cwd: root, env: { ...process.env, LC_ALL: "C" }, encoding: "utf8", input: `${normalized.join("\0")}\0`, maxBuffer: 64 * 1024 * 1024 });
|
|
1045
1059
|
// The canonical owned hook has one meaning across Git status, check cache
|
|
1046
1060
|
// and MERGE, without changing a sibling worktree's shared Git excludes.
|
|
1047
1061
|
const gitless = isGitlessWorkspace(root, status);
|
|
@@ -1150,12 +1164,13 @@ function environmentFingerprint(workspaceRoot, inputs, checkEnvironment) {
|
|
|
1150
1164
|
// and tool selection; omit only known process/control bookkeeping.
|
|
1151
1165
|
const bookkeeping = new Set(["PWD", "OLDPWD", "SHLVL", "_", "DD_FLOW_DAEMON_ID",
|
|
1152
1166
|
"DD_FLOW_CURRENT_INVOCATION", "DD_FLOW_INVOCATION_READONLY", "DD_FLOW_INVOCATION_SCOPE", "DD_FLOW_CODEX_STATE_DIR",
|
|
1167
|
+
"DD_FLOW_HOOK_DEADLINE_AT", // Per-call transport budget, not a project check input.
|
|
1153
1168
|
"DD_FLOW_RUNTIME_OWNER", "DD_FLOW_CONTROLLER_TOKEN", "DD_FLOW_CONTROL_TOKEN", "DD_FLOW_SCOPE_CONTROL_TOKEN",
|
|
1154
1169
|
"DD_FLOW_BIN", "DD_FLOW_ENGINE_HOME", "DD_FLOW_RUNTIME_BUDGET", "DD_FLOW_WORK_ID", "DD_FLOW_SESSION_ID", "CODEX_THREAD_ID", "DD_EVAL_OPERATION_ID",
|
|
1155
|
-
"CODEX_PERMISSION_PROFILE", "CODEX_SESSION_ID", "CODEX_VERSION"]);
|
|
1170
|
+
"CODEX_PERMISSION_PROFILE", "CODEX_SESSION_ID", "CODEX_VERSION", "CODEX_TOOL_CALL_ID"]);
|
|
1156
1171
|
return crypto.createHash("sha256").update(JSON.stringify(Object.entries(environment)
|
|
1157
1172
|
.filter(([name, value]) => value !== undefined && !bookkeeping.has(name)
|
|
1158
|
-
&& (inputs === undefined || inputs.some(input => input.endsWith("*") ? name.startsWith(input.slice(0, -1)) : name === input)))
|
|
1173
|
+
&& (name === "PLAYWRIGHT_BROWSERS_PATH" || inputs === undefined || inputs.some(input => input.endsWith("*") ? name.startsWith(input.slice(0, -1)) : name === input)))
|
|
1159
1174
|
.sort(([left], [right]) => left.localeCompare(right)))).digest("hex");
|
|
1160
1175
|
}
|
|
1161
1176
|
function acquireWorkspaceGate(context, input) {
|
|
@@ -1330,7 +1345,7 @@ function checkReceiptEvidenceValidForMode(receipt, mode) {
|
|
|
1330
1345
|
// Receipt annotations are not execution authority. Compare the SQL-owned
|
|
1331
1346
|
// failures with the independently hash/binding-checked engine sidecar.
|
|
1332
1347
|
const hasEngineOutcome = receipt.required_artifacts.includes("engine-outcome.json") || receipt.artifacts.some(artifact => artifact.path === "engine-outcome.json" || artifact.path === path.join(path.dirname(receipt.receipt_path), "artifacts", "engine-outcome.json"));
|
|
1333
|
-
if (hasEngineOutcome && !isDeepStrictEqual(retainedProof.execution_failures ?? [], receipt.execution_failures ?? []))
|
|
1348
|
+
if (hasEngineOutcome && (!isDeepStrictEqual(retainedProof.execution_failures ?? [], receipt.execution_failures ?? []) || !isDeepStrictEqual(retainedProof.candidate_absences ?? [], receipt.candidate_absences ?? [])))
|
|
1334
1349
|
return false;
|
|
1335
1350
|
const root = path.join(path.dirname(receipt.receipt_path), "artifacts");
|
|
1336
1351
|
if (stored.runner_evidence_required && mode !== "diagnostic" && !runnerEvidencePass(readRunnerEvidence(root, stored.runner_evidence_required, { invocation_id: receipt.id, check_id: receipt.id, command: receipt.command, input_hash: receipt.input_hash, verification_epoch: receipt.verification_epoch })))
|
|
@@ -13,7 +13,7 @@ export async function preflightCliCompatibility(context, args, env = process.env
|
|
|
13
13
|
if (resolvedProjectRoot === null && parsed.positional[0] === "runtime" && parsed.positional[1] === "process"
|
|
14
14
|
&& !parsed.options.has("project-root") && !parsed.options.has("root"))
|
|
15
15
|
return { ok: true, classification, compatibility: null };
|
|
16
|
-
const { resolveOperationProjectRoot, selectEngine } = await import("./engines.js");
|
|
16
|
+
const { resolveOperationProjectRoot, selectEngine, selectBoundExecutingEngine } = await import("./engines.js");
|
|
17
17
|
const projectRoot = resolvedProjectRoot ?? resolveOperationProjectRoot(context, args);
|
|
18
18
|
if (!projectRoot) {
|
|
19
19
|
return { ok: true, classification, compatibility: null };
|
|
@@ -21,7 +21,10 @@ export async function preflightCliCompatibility(context, args, env = process.env
|
|
|
21
21
|
// Compatibility checks the executing build/manifest identity. Routing and
|
|
22
22
|
// binding validate snapshot bytes before dispatch; maintenance must not
|
|
23
23
|
// rehash the dependency closure or spawn health probes on every lease ACK.
|
|
24
|
-
|
|
24
|
+
// Read-only probes may intentionally inspect a copied RUN before its new
|
|
25
|
+
// executor is bound. Writer authority is neither required nor granted here.
|
|
26
|
+
const selection = (classification.mode === "read_only_diagnostics" ? null : selectBoundExecutingEngine(context, args, projectRoot, env))
|
|
27
|
+
?? selectEngine(context, { projectRoot, env }, { allowCurrentInProcess: true, verifyCurrentContent: false }, classification);
|
|
25
28
|
const compatibility = compatibilityReport(selection, classification);
|
|
26
29
|
if (classification.mode === "read_only_diagnostics" || classification.mode === "mb_upgrade") {
|
|
27
30
|
return { ok: true, classification, compatibility };
|
|
@@ -4,10 +4,11 @@ import { FrozenGenericExecution } from "./generic-execution.js";
|
|
|
4
4
|
import { resolveExecutionPolicy, resolveStageTransition, stageExecutionCwd } from "./execution-policy.js";
|
|
5
5
|
import { readFlowRunExecutionBundle } from "./runs.js";
|
|
6
6
|
import { hybridDigest } from "./hybrid-contracts.js";
|
|
7
|
+
import { isDiagnosticFlow } from "../domain/diagnostic-flows.js";
|
|
7
8
|
/** Adapt generic routing to shared fanout APIs, not to SDLC settings. No alias,
|
|
8
9
|
* operator file, merge policy or project default participates in this selection.
|
|
9
10
|
*/
|
|
10
|
-
export function genericControllerRouting(execution) {
|
|
11
|
+
export function genericControllerRouting(execution, workspaceRoot) {
|
|
11
12
|
if (!(execution instanceof FrozenGenericExecution) || execution.settings.execution !== "managed") {
|
|
12
13
|
throw new AppError("controller_execution_invalid", "Managed controller requires managed frozen execution", 1, { effect: "no_effect" });
|
|
13
14
|
}
|
|
@@ -33,14 +34,17 @@ export function genericControllerRouting(execution) {
|
|
|
33
34
|
execution.stage(fromStage);
|
|
34
35
|
return { ...resolveExecutionPolicy({ ddFlowHome: "", policy, profiles, stage }), session_mode: selected.session_mode };
|
|
35
36
|
},
|
|
36
|
-
cwd(stage, _mergeTargetWorkspace,
|
|
37
|
+
cwd(stage, _mergeTargetWorkspace, currentRun) {
|
|
38
|
+
execution.stage(stage);
|
|
39
|
+
return path.resolve(currentRun?.workspace_root ?? workspaceRoot ?? snapshot.scope.workspace);
|
|
40
|
+
}
|
|
37
41
|
};
|
|
38
42
|
}
|
|
39
43
|
/** Restore execution from RUN authority, never from a generic manifest copy.
|
|
40
44
|
* Legacy SDLC manifests retain their original settings and routing behavior.
|
|
41
45
|
*/
|
|
42
46
|
export function selectControllerExecution(context, run, manifest) {
|
|
43
|
-
if (manifest?.schema_id === "dd-flow/run-controller-manifest@2" || ["mb-init", "mb-upgrade"].includes(run.flow_kind)) {
|
|
47
|
+
if (manifest?.schema_id === "dd-flow/run-controller-manifest@2" || ["mb-init", "mb-upgrade"].includes(run.flow_kind) || isDiagnosticFlow(run.flow_kind)) {
|
|
44
48
|
const bundle = readFlowRunExecutionBundle(context, { projectRoot: run.project_root, runId: run.id });
|
|
45
49
|
if (!bundle || bundle.descriptor.descriptor.id !== run.flow_kind || bundle.descriptor.descriptor.id === "sdlc")
|
|
46
50
|
throw new AppError("controller_execution_invalid", "Generic controller requires its retained non-SDLC bundle", 1);
|
|
@@ -51,7 +55,9 @@ export function selectControllerExecution(context, run, manifest) {
|
|
|
51
55
|
|| manifest.request?.project_root !== run.project_root || manifest.request?.run_id !== run.id)) {
|
|
52
56
|
throw new AppError("controller_execution_invalid", "Controller execution binding differs from retained RUN authority", 1);
|
|
53
57
|
}
|
|
54
|
-
|
|
58
|
+
// Bundle admission has already checked the current RUN workspace against
|
|
59
|
+
// its trusted relocation receipts; frozen scope remains historical input.
|
|
60
|
+
return { ...genericControllerRouting(bundle.execution, run.workspace_root), bundle, binding, legacy: undefined };
|
|
55
61
|
}
|
|
56
62
|
const index = JSON.parse(run.index_json);
|
|
57
63
|
const legacy = manifest ? manifest.execution_profile : index.execution_profile;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import path from "node:path";
|
|
1
2
|
import { AppError, errorRecord } from "../shared/errors.js";
|
|
2
3
|
import { normalizeNativeChildren, safeNativeFailure } from "../harness-runtime/lib/native-children.mjs";
|
|
3
4
|
import { getVnextFanoutStatus, observeVnextNativeChildren, reconcileVnextFanout, getStageFanoutCapacity, verifiedNativeWorkFailure } from "./vnext-fanout.js";
|
|
@@ -10,7 +11,9 @@ import { registerMemoryRepairBaselineFromStage, reserveMemoryRepairWriterCopyFro
|
|
|
10
11
|
import { requireProjectByRoot } from "./projects.js";
|
|
11
12
|
import { stageAttempt, stageWorkIds } from "./stage-work-graph.js";
|
|
12
13
|
import { runtimeCommand } from "./runtime-command.js";
|
|
13
|
-
|
|
14
|
+
import { promptDeliveryInstruction } from "./prompt-delivery.js";
|
|
15
|
+
import { currentStageFinishCommand } from "./lifecycle-invocations.js";
|
|
16
|
+
function semanticCompletionPrompt(stage, completed, command) {
|
|
14
17
|
const task = stage === "plan-review"
|
|
15
18
|
? "Classify the reviewer findings, apply every accepted correction in the current PLAN-REVIEW Work, and write the authoritative review decision."
|
|
16
19
|
: stage === "code-review"
|
|
@@ -19,7 +22,7 @@ function semanticCompletionPrompt(stage, completed) {
|
|
|
19
22
|
return [
|
|
20
23
|
`All declared ${stage} child Work has settled (${completed} completed).`,
|
|
21
24
|
task,
|
|
22
|
-
|
|
25
|
+
`Then invoke this exact currently issued Stage finish command:\n\n${command}\n\nRefresh the authoritative verification evidence first; an earlier rejected or successful nonterminal command only reads its retained outcome.`,
|
|
23
26
|
"If that command returns either outcome = repair_required with a repair Work ID, or error.details.continuation.kind = repair_required, the repair Work is already registered. End this Turn so the controller can dispatch it. Otherwise follow the command's actual result.",
|
|
24
27
|
"Do not create speculative repair Work or start a successor Stage."
|
|
25
28
|
].join(" ");
|
|
@@ -29,17 +32,20 @@ export function controllerStageEntryPrompt(stage, command) {
|
|
|
29
32
|
return [
|
|
30
33
|
`Start the assigned ${stage} Stage now. Your first tool call must be this exact standalone command:`,
|
|
31
34
|
command,
|
|
32
|
-
|
|
35
|
+
promptDeliveryInstruction,
|
|
33
36
|
"If the returned packet has orchestration.kind = work_fanout, this Turn is only Stage entry: acknowledge the packet and end your Turn immediately. The packet describes the whole Stage, but its graph/dispatch/finish instructions belong to later controller continuations. Do not list, start, launch or finish Work, create children, change files, run checks, or finish the Stage in this entry Turn. The controller will inspect the graph and send the next assignment with exact issued commands (or dispatch external workers itself).",
|
|
34
37
|
"Otherwise perform this Stage's semantic work and stop at its boundary or a declared HITL boundary. Do not start a successor Stage.",
|
|
35
38
|
stageLifecycleInstruction
|
|
36
39
|
].join("\n\n");
|
|
37
40
|
}
|
|
38
|
-
export function controllerStageResumePrompt(
|
|
41
|
+
export function controllerStageResumePrompt(packet, resumedPromptPath) {
|
|
39
42
|
return [
|
|
40
|
-
`The
|
|
41
|
-
|
|
42
|
-
|
|
43
|
+
`The accepted canonical answer for pause ${packet.pause_id} (${packet.stage}, ${packet.work_id}) is retained at this absolute read-only path: ${JSON.stringify(packet.answer_file)} (sha256 ${packet.answer_sha256}). Do not replace it with a placeholder or ask the user to supply it again. A null pause.answer_path before resume does not mean the answer is missing.`,
|
|
44
|
+
resumedPromptPath
|
|
45
|
+
? `This exact answer was already committed. Do not invoke stage resume again. Read the recovered authoritative continuation at ${JSON.stringify(resumedPromptPath)} and perform its current Stage instructions.`
|
|
46
|
+
: `Only while this exact pause still awaits its answer, your first tool call must be this exact standalone command. It applies the already accepted bound answer file:\n\n${packet.command}`,
|
|
47
|
+
...(packet.resume_source_path ? [`If this pause has already resumed, do not invoke stage resume again. Read the complete recovered result.worker_prompt_markdown in ${JSON.stringify(packet.resume_source_path)} instead, and follow that authoritative continuation. The controller restores this committed packet before dispatch. A different pause requires its own accepted answer.`] : []),
|
|
48
|
+
`Do not alter the answer. ${promptDeliveryInstruction} Follow the returned packet, then stop at this Stage boundary.`,
|
|
43
49
|
stageLifecycleInstruction
|
|
44
50
|
].join("\n\n");
|
|
45
51
|
}
|
|
@@ -173,8 +179,8 @@ export async function nextControllerFanout(context, input) {
|
|
|
173
179
|
throw new AppError("fanout_graph_stalled", "Created Work has no ready or running dependency path", 1);
|
|
174
180
|
if (input.stage === "review") {
|
|
175
181
|
const projectId = requireProjectByRoot(context, input.projectRoot).id;
|
|
176
|
-
const run = context.db.get("SELECT flow_kind FROM runs WHERE project_id=? AND id=?", [projectId, input.runId]);
|
|
177
|
-
if (run?.flow_kind === "mb-init") {
|
|
182
|
+
const run = context.db.get("SELECT flow_kind,run_root,run_dir FROM runs WHERE project_id=? AND id=?", [projectId, input.runId]);
|
|
183
|
+
if (run?.flow_kind === "mb-init" || run?.flow_kind === "mb-upgrade") {
|
|
178
184
|
const attempt = stageAttempt(context, projectId, input.runId, "review");
|
|
179
185
|
const repair = context.db.get(`SELECT CASE event_type WHEN 'memory.second_repair_registration' THEN 2 ELSE 1 END AS wave
|
|
180
186
|
FROM audit_events WHERE project_id=? AND event_type IN ('memory.repair_registration','memory.second_repair_registration')
|
|
@@ -186,9 +192,27 @@ export async function nextControllerFanout(context, input) {
|
|
|
186
192
|
const args = ["--run", input.runId, "--project-root", input.projectRoot, "--wave", String(repair.wave), "--json"];
|
|
187
193
|
return prompt("semantic_completion", `Correction wave ${repair.wave} Work has settled. Before Stage finish, run ${runtimeCommand(context, ["memory", "corrections", "checks", ...args])}, then ${runtimeCommand(context, ["memory", "review", "selective-register", ...args])}. Read both complete responses, then end this Turn so the controller can dispatch the independent selective reviewers. Do not finish the Stage or treat repair Work completion as independent acceptance.`);
|
|
188
194
|
}
|
|
195
|
+
const wave = repair?.wave ?? 0;
|
|
196
|
+
const runHome = run.run_root ?? run.run_dir;
|
|
197
|
+
if (!runHome || !path.isAbsolute(runHome))
|
|
198
|
+
throw new AppError("runtime_missing", "Memory review collection requires its retained absolute RUN home", 1);
|
|
199
|
+
const collectionArgs = ["--run", input.runId, "--project-root", input.projectRoot, ...(wave ? ["--wave", String(wave)] : []),
|
|
200
|
+
"--json", "--progress-jsonl", "--response-file", path.join(runHome, `memory-review-collection-wave-${wave}.json`)];
|
|
201
|
+
const collect = runtimeCommand(context, ["memory", "review", wave ? "selective-collect" : "collect", ...collectionArgs]);
|
|
202
|
+
return prompt("semantic_completion", [
|
|
203
|
+
`All declared memory review Work has settled. First run ${collect}. Read the complete canonical collection from its response_file; do not decide acceptance from a truncated preview or reviewer counts.`,
|
|
204
|
+
"Only if the collection outcome is accepted (projection.outcome = accepted), integrate its accepted results into the original Stage proposal and invoke the exact Stage finish command from the authoritative packet.",
|
|
205
|
+
wave < 2
|
|
206
|
+
? `If the outcome is needs_repair, use the returned canonical findings and correction_plan_binding to author substantive repair tasks with exact owned write_paths. Register that plan with the packet's issued memory corrections register command for wave ${wave + 1}, then end this Turn so the controller can dispatch it. Do not invoke Stage finish before corrections, checks and independent selective review complete.`
|
|
207
|
+
: "If the outcome is needs_repair or blocked, the two correction waves are exhausted. Use the issued pause command for the concrete unresolved blocker; do not mark it accepted.",
|
|
208
|
+
"For blocked collection, use the issued pause command. Do not calculate finding hashes, merge or drop findings, edit project files in the coordinator, create speculative Work or start a successor Stage."
|
|
209
|
+
].join("\n\n"));
|
|
189
210
|
}
|
|
190
211
|
}
|
|
191
|
-
|
|
212
|
+
const finish = currentStageFinishCommand(context, input.stage);
|
|
213
|
+
if (!finish)
|
|
214
|
+
throw new AppError("controller_finish_authority_missing", "Settled child Work has no current issued Stage finish; reconcile lifecycle authority", 1, { stage: input.stage });
|
|
215
|
+
return prompt("semantic_completion", semanticCompletionPrompt(input.stage, fanout.works.completed, finish));
|
|
192
216
|
}
|
|
193
217
|
/** Shared pinned runtime normalization; preserve structured errors at the TS boundary. */
|
|
194
218
|
export function controllerNativeChildren(receipt, rootSessionId) {
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Worker } from "node:worker_threads";
|
|
2
|
+
import { AppError } from "../shared/errors.js";
|
|
3
|
+
/** Keep synchronous custody/copy work off the controller's lease event loop.
|
|
4
|
+
* The thread remains inside its registered process; OS-level stop owns both.
|
|
5
|
+
* Do not resolve until exit, including after cancellation or a typed failure. */
|
|
6
|
+
export function runControllerMemoryTask(context, task, signal) {
|
|
7
|
+
const aborted = () => new AppError("controller_memory_task_aborted", "Memory operation was cancelled; reconcile its retained effects", 1, { effect: "unknown" });
|
|
8
|
+
if (signal?.aborted)
|
|
9
|
+
return Promise.reject(aborted());
|
|
10
|
+
return new Promise((resolve, reject) => {
|
|
11
|
+
// Both source tests and installed runtimes use the package's compiled worker.
|
|
12
|
+
const worker = new Worker(new URL("../../dist/services/controller-memory-worker.js", import.meta.url), {
|
|
13
|
+
workerData: { task, env: { ...context.env, DD_FLOW_HOME: context.ddFlowHome } }
|
|
14
|
+
});
|
|
15
|
+
let reply, failure;
|
|
16
|
+
const abort = () => worker.postMessage({ abort: true });
|
|
17
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
18
|
+
if (signal?.aborted)
|
|
19
|
+
abort();
|
|
20
|
+
worker.on("message", (value) => {
|
|
21
|
+
const invalid = value && typeof value === "object" && ("error" in value
|
|
22
|
+
? !value.error || typeof value.error.code !== "string" || typeof value.error.message !== "string" || !Number.isInteger(value.error.exitCode)
|
|
23
|
+
|| !value.error.details || typeof value.error.details !== "object" || Array.isArray(value.error.details) || "result" in value
|
|
24
|
+
: !("result" in value) || !value.result || typeof value.result !== "object" || Array.isArray(value.result));
|
|
25
|
+
if (reply || !value || typeof value !== "object" || invalid) {
|
|
26
|
+
failure = new AppError("controller_memory_task_invalid", "Memory worker returned an invalid terminal reply", 1, { effect: "unknown" });
|
|
27
|
+
}
|
|
28
|
+
else
|
|
29
|
+
reply = value;
|
|
30
|
+
});
|
|
31
|
+
worker.once("error", error => { failure = error; });
|
|
32
|
+
worker.once("exit", code => {
|
|
33
|
+
signal?.removeEventListener("abort", abort);
|
|
34
|
+
if (failure)
|
|
35
|
+
return reject(failure);
|
|
36
|
+
if (reply && "error" in reply)
|
|
37
|
+
return reject(new AppError(reply.error.code, reply.error.message, reply.error.exitCode, reply.error.details));
|
|
38
|
+
if (signal?.aborted)
|
|
39
|
+
return reject(aborted());
|
|
40
|
+
if (code !== 0 || !reply || !("result" in reply))
|
|
41
|
+
return reject(new AppError("controller_memory_task_failed", "Memory worker exited without its terminal result", 1, { exit_code: code, effect: "unknown" }));
|
|
42
|
+
resolve(reply.result);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { parentPort, workerData } from "node:worker_threads";
|
|
2
|
+
import { createContext } from "../runtime/context.js";
|
|
3
|
+
import { AppError, errorRecord } from "../shared/errors.js";
|
|
4
|
+
import { nextControllerFanout } from "./controller-fanout.js";
|
|
5
|
+
import { launchExternalWork } from "./external-work-launch.js";
|
|
6
|
+
import { requireProjectByRoot } from "./projects.js";
|
|
7
|
+
import { assertControllerLeaseAllowed } from "./run-controller-process.js";
|
|
8
|
+
import { assertManagedProcessLease } from "./managed-processes.js";
|
|
9
|
+
import { isDiagnosticFlow } from "../domain/diagnostic-flows.js";
|
|
10
|
+
if (!parentPort)
|
|
11
|
+
throw new AppError("controller_memory_task_invalid", "Memory worker requires its controller thread", 1);
|
|
12
|
+
const port = parentPort, cancellation = new AbortController();
|
|
13
|
+
port.on("message", message => { if (message?.abort === true)
|
|
14
|
+
cancellation.abort(); });
|
|
15
|
+
const { task, env } = workerData;
|
|
16
|
+
let context;
|
|
17
|
+
let reply;
|
|
18
|
+
try {
|
|
19
|
+
context = createContext(env, "prepared_existing");
|
|
20
|
+
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
|
+
if (!run || !["mb-init", "mb-upgrade"].includes(run.flow_kind) && !isDiagnosticFlow(run.flow_kind))
|
|
23
|
+
throw new AppError("controller_memory_task_invalid", "Worker requires the selected memory or diagnostic RUN", 1, { effect: "no_effect" });
|
|
24
|
+
if (task.operation === "external") {
|
|
25
|
+
const work = context.db.get("SELECT project_id,run_id FROM works WHERE work_id=?", [task.input.workId]);
|
|
26
|
+
if (!work || work.project_id !== project.id || work.run_id !== task.input.runId)
|
|
27
|
+
throw new AppError("controller_memory_task_invalid", "External Work belongs to another RUN", 1, { effect: "no_effect" });
|
|
28
|
+
}
|
|
29
|
+
const owner = env.DD_FLOW_CONTROLLER_TOKEN ? context.db.get("SELECT * FROM run_controllers WHERE project_id=? AND run_id=? AND owner_token=?", [project.id, task.input.runId, env.DD_FLOW_CONTROLLER_TOKEN]) : undefined;
|
|
30
|
+
if (!owner)
|
|
31
|
+
throw new AppError("controller_owner_mismatch", "Memory task lacks its managed controller owner", 1, { effect: "no_effect" });
|
|
32
|
+
assertControllerLeaseAllowed(context, owner);
|
|
33
|
+
assertManagedProcessLease(context, { id: owner.process_id, leaseToken: owner.process_lease_token, pid: process.pid });
|
|
34
|
+
cancellation.signal.throwIfAborted();
|
|
35
|
+
if (task.operation === "fanout")
|
|
36
|
+
reply = { result: await nextControllerFanout(context, task.input) };
|
|
37
|
+
else if (task.operation === "external")
|
|
38
|
+
reply = { result: await launchExternalWork(context, { ...task.input, signal: cancellation.signal }) };
|
|
39
|
+
else
|
|
40
|
+
throw new AppError("controller_memory_task_invalid", "Unknown memory worker operation", 1, { effect: "no_effect" });
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
reply = { error: errorRecord(error) };
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
context?.db.close?.();
|
|
47
|
+
}
|
|
48
|
+
port.postMessage(reply);
|
|
49
|
+
port.close();
|
|
@@ -216,10 +216,10 @@ 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
|
|
219
|
+
// Automatic observation must not change a memory or diagnostic source baseline.
|
|
220
220
|
// Keep projections in Home; do not hide runtime-created paths in Git or
|
|
221
221
|
// 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
|
|
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
223
|
const config = initializing ? { ...configured, dashboard: { ...configured.dashboard,
|
|
224
224
|
markdown_path: path.join(projectDashboardDir(context.ddFlowHome, project.id), "project-dashboard.md") } } : configured;
|
|
225
225
|
if (!config.dashboard.auto_refresh) {
|
|
@@ -62,17 +62,19 @@ export class PreparedDescriptorRunSettlement {
|
|
|
62
62
|
const scope = { projectId: project.id, runId: input.runId, stage: input.stage };
|
|
63
63
|
const matchKeys = entityReferenceVariants(input.runId).map(reference => stageLifecycleMatchKey("stage_finish", reference, input.stage, input.projectRoot));
|
|
64
64
|
const snapshot = context.db.readSnapshot(() => ({ owner: selected(context, scope, bundle, matchKeys), generation: assertRunMutationAllowed(context, scope.projectId, scope.runId) }));
|
|
65
|
-
const
|
|
65
|
+
const operational = assertPreparedRunBundleSqlAuthority(context, scope, bundle);
|
|
66
|
+
const git = snapshot.owner.prior?.git ?? gitFacts(operational.workspace_root ?? operational.project_root);
|
|
66
67
|
return new PreparedDescriptorRunSettlement(scope, bundle, snapshot.owner, git, snapshot.generation, matchKeys);
|
|
67
68
|
}
|
|
68
69
|
apply(context, assertHandlerCustody) {
|
|
69
70
|
if (typeof assertHandlerCustody !== "function")
|
|
70
71
|
refuse("handler_fence_required");
|
|
71
|
-
const
|
|
72
|
+
const operational = assertPreparedRunBundleSqlAuthority(context, this.#scope, this.#bundle);
|
|
73
|
+
const bundle = readFlowRunExecutionBundle(context, { projectRoot: operational.project_root, runId: this.#scope.runId });
|
|
72
74
|
if (!bundle || bundle.sha256 !== this.#bundle.sha256)
|
|
73
75
|
refuse("bundle_changed");
|
|
74
76
|
const observed = context.db.readSnapshot(() => selected(context, this.#scope, this.#bundle, this.#matchKeys));
|
|
75
|
-
if (!observed.prior && hybridDigest(gitFacts(
|
|
77
|
+
if (!observed.prior && hybridDigest(gitFacts(operational.workspace_root ?? operational.project_root)) !== hybridDigest(this.#git))
|
|
76
78
|
refuse("git_observation_changed");
|
|
77
79
|
const receipt = { run_id: this.#scope.runId, work_id: this.#owner.rootId, binding: this.#owner.binding, git: this.#git };
|
|
78
80
|
const changed = withStageSettlement(context, () => {
|