@deksden-com/dd-flow-cli 0.9.0-beta.145 → 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 +71 -0
- package/dist/build-info.json +7 -7
- package/dist/cli/command-inputs.js +6 -2
- package/dist/cli/command-metadata.js +18 -2
- package/dist/cli/diagnostic-invocation.js +46 -0
- package/dist/cli/diagnostic.js +120 -0
- package/dist/cli/help.js +35 -0
- package/dist/cli/memory-flow-admission.js +4 -2
- package/dist/cli/memory-init.js +5 -2
- package/dist/cli/run-cli.js +16 -15
- package/dist/cli.js +6 -0
- package/dist/domain/diagnostic-aspects.js +103 -0
- package/dist/domain/diagnostic-flows.js +8 -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 +306 -34
- 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 +70 -28
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +3 -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/driver-recovery.mjs +9 -8
- package/dist/harness-runtime/lib/managed-daemon.d.mts +1 -1
- package/dist/harness-runtime/lib/managed-daemon.mjs +11 -3
- 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-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/provider-overload-policy.d.mts +1 -1
- package/dist/harness-runtime/lib/provider-overload-policy.mjs +1 -1
- 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/project-flow-pack-manifest.schema.json +1 -1
- package/dist/services/check-environment.js +21 -0
- package/dist/services/check-runner-evidence.js +267 -15
- package/dist/services/code-checks.js +23 -15
- package/dist/services/controller-execution.js +10 -4
- package/dist/services/controller-fanout.js +13 -6
- package/dist/services/controller-memory-worker.js +3 -2
- package/dist/services/dashboard.js +2 -2
- package/dist/services/descriptor-run-settlement.js +5 -3
- package/dist/services/descriptor-run-start.js +22 -3
- package/dist/services/descriptor-stage-coordinator.js +37 -4
- package/dist/services/descriptor-stage-settlement.js +2 -1
- 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/eval-snapshots.js +85 -29
- package/dist/services/execution-bundles.js +19 -7
- package/dist/services/flow-catalog.js +6 -1
- package/dist/services/frozen-engine.js +37 -2
- package/dist/services/hybrid-contracts.js +2 -0
- package/dist/services/installed-flow-source.js +25 -3
- package/dist/services/lifecycle-invocations.js +147 -34
- package/dist/services/memory-init-contexts.js +16 -0
- package/dist/services/memory-init-controller-start.js +12 -4
- package/dist/services/memory-init-lint-capability.js +5 -5
- package/dist/services/memory-init-native-capacity.js +4 -4
- package/dist/services/memory-init-preflight.js +10 -16
- package/dist/services/memory-init-worktrunk-capability.js +3 -7
- package/dist/services/memory-layer.js +4 -3
- package/dist/services/memory-tool-probe.js +25 -0
- package/dist/services/memory-verification-checks.js +15 -4
- package/dist/services/native-daemon-history.js +2 -1
- package/dist/services/provider-capacity.js +43 -38
- package/dist/services/release-cache.js +8 -0
- package/dist/services/run-control.js +2 -1
- package/dist/services/run-controller-adapter.js +2 -1
- package/dist/services/run-controller-process.js +2 -1
- package/dist/services/run-controller-recovery.js +2 -1
- package/dist/services/run-controller.js +85 -15
- package/dist/services/run-fork.js +1 -1
- 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-service.js +2 -1
- package/dist/services/stage-handlers.js +16 -3
- package/dist/services/stage-pause.js +4 -1
- package/dist/services/version-status.js +1 -1
- package/dist/services/vnext-code.js +2 -2
- package/dist/services/vnext-fanout.js +31 -1
- package/dist/services/work-registry.js +37 -6
- package/dist/services/workspace-bootstrap.js +2 -1
- package/package.json +6 -3
|
@@ -11,6 +11,7 @@ import { hybridDigest } from "./hybrid-contracts.js";
|
|
|
11
11
|
import { isMemoryInitResourceStorageFacts } from "./memory-init-resource-storage.js";
|
|
12
12
|
import { memoryInitStartupInputSchemas } from "./memory-init-startup-custody.js";
|
|
13
13
|
import { SETUP_STARTUP_MS } from "../harness-runtime/lib/setup-progress.mjs";
|
|
14
|
+
import { isDiagnosticFlow } from "../domain/diagnostic-flows.js";
|
|
14
15
|
function storageProbeRefuse(reason, probe) {
|
|
15
16
|
const summary = probe ? ` (${probe.error_code ?? probe.signal ?? `exit ${probe.status}`})` : "";
|
|
16
17
|
throw new AppError("frozen_engine_storage_incompatible", `Selected frozen engine cannot admit memory-flow storage${summary}`, 1, { reason, effect: "no_effect", ...(probe ? { probe } : {}) });
|
|
@@ -76,6 +77,37 @@ export class AdmittedFrozenEngine {
|
|
|
76
77
|
refuse("invalid_selection_identity");
|
|
77
78
|
return AdmittedFrozenEngine.fromIdentity(identity, expectedManifestSha256);
|
|
78
79
|
}
|
|
80
|
+
/** Physical relocation preserves the original manifest as provenance. It
|
|
81
|
+
* cannot authorize a different executable or different semantic metadata. */
|
|
82
|
+
static admitRelocation(binding, retained, expectedSha256, target) {
|
|
83
|
+
if (hash(retained) !== expectedSha256 || !shape("run-engine-binding", binding)
|
|
84
|
+
|| !shape("run-engine-binding", target) || binding.run_id !== target.run_id)
|
|
85
|
+
refuse("invalid_relocation_identity");
|
|
86
|
+
const value = json(retained);
|
|
87
|
+
if (!shape("engine-manifest", value))
|
|
88
|
+
refuse("invalid_manifest_shape");
|
|
89
|
+
const original = value;
|
|
90
|
+
if (original.snapshot_root !== binding.engine.snapshot_root
|
|
91
|
+
|| original.package_name !== binding.engine.package_name || original.package_version !== binding.engine.package_version
|
|
92
|
+
|| original.engine_version !== binding.engine.engine_version || original.integrity.checksum !== binding.engine.integrity_checksum
|
|
93
|
+
|| Object.entries(binding.engine).some(([key, value]) => key !== "snapshot_root" && value !== target.engine[key])) {
|
|
94
|
+
refuse("foreign_relocation_identity");
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
const bytes = readBoundedReleaseFile(path.join(target.engine.snapshot_root, "engine.json"), 1024 * 1024);
|
|
98
|
+
const selected = AdmittedFrozenEngine.admit(target, hash(bytes));
|
|
99
|
+
const semantic = (manifest) => Object.fromEntries(Object.entries(manifest)
|
|
100
|
+
.filter(([key]) => !["snapshot_root", "package_root", "installed_at", "install_source"].includes(key)));
|
|
101
|
+
if (hybridDigest(semantic(original)) !== hybridDigest(semantic(selected.manifest())))
|
|
102
|
+
refuse("relocated_manifest_changed");
|
|
103
|
+
return selected;
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
if (error instanceof AppError && error.code === "frozen_engine_invalid")
|
|
107
|
+
throw error;
|
|
108
|
+
return refuse("snapshot_unavailable_or_unreadable");
|
|
109
|
+
}
|
|
110
|
+
}
|
|
79
111
|
static fromIdentity(identity, expectedManifestSha256) {
|
|
80
112
|
try {
|
|
81
113
|
const root = path.resolve(identity.snapshot_root);
|
|
@@ -125,6 +157,9 @@ export class AdmittedFrozenEngine {
|
|
|
125
157
|
admitMemoryUpgradeFlow(source) {
|
|
126
158
|
return this.#admitMemoryFlow(source, "mb-upgrade");
|
|
127
159
|
}
|
|
160
|
+
admitDiagnosticFlow(source, flowId) {
|
|
161
|
+
return this.#admitMemoryFlow(source, flowId);
|
|
162
|
+
}
|
|
128
163
|
#admitMemoryFlow(source, flowId, startupInputSchema) {
|
|
129
164
|
const fail = (reason, details = {}) => {
|
|
130
165
|
throw new AppError("frozen_engine_flow_incompatible", `Selected frozen engine cannot admit ${flowId} handlers`, 1, { ...details, reason, phase: "prepare", effect: "no_effect" });
|
|
@@ -156,7 +191,7 @@ export class AdmittedFrozenEngine {
|
|
|
156
191
|
const flow = source.resolveRegisteredFlow();
|
|
157
192
|
if (flow.descriptor.id !== flowId)
|
|
158
193
|
fail("foreign_flow_source");
|
|
159
|
-
const expected = { schema_id: flowId === "mb-upgrade" ? "dd-flow/memory-upgrade-engine-flow@1"
|
|
194
|
+
const expected = { schema_id: isDiagnosticFlow(flowId) ? "dd-flow/diagnostic-engine-flow@1" : flowId === "mb-upgrade" ? "dd-flow/memory-upgrade-engine-flow@1"
|
|
160
195
|
: startupInputSchema === undefined ? "dd-flow/memory-init-engine-flow@1" : "dd-flow/memory-init-engine-flow@2",
|
|
161
196
|
...(startupInputSchema === undefined ? {} : { startup_input_schema: startupInputSchema }),
|
|
162
197
|
flow_id: flowId, source_sha256: source.sha256,
|
|
@@ -164,7 +199,7 @@ export class AdmittedFrozenEngine {
|
|
|
164
199
|
revalidate();
|
|
165
200
|
const env = Object.fromEntries(Object.entries(process.env).filter(([key]) => !["NODE_OPTIONS", "NODE_PATH"].includes(key) && !key.startsWith("DD_FLOW_")));
|
|
166
201
|
const started = performance.now();
|
|
167
|
-
const result = spawnSync(this.nodeExecutable, [this.entrypoint, "runtime", "flow", "admit", "--operation", flowId === "mb-init" ? "memory-init" : "memory-upgrade", "--json"], { input, cwd: this.#manifest.snapshot_root, env: { ...env, DD_FLOW_ENGINE_MODE: "1", DD_FLOW_ENGINE_HOME: this.#manifest.snapshot_root },
|
|
202
|
+
const result = spawnSync(this.nodeExecutable, [this.entrypoint, "runtime", "flow", "admit", "--operation", isDiagnosticFlow(flowId) ? flowId : flowId === "mb-init" ? "memory-init" : "memory-upgrade", "--json"], { input, cwd: this.#manifest.snapshot_root, env: { ...env, DD_FLOW_ENGINE_MODE: "1", DD_FLOW_ENGINE_HOME: this.#manifest.snapshot_root },
|
|
168
203
|
// This is cold startup of a selected CLI, not an already-loaded handler.
|
|
169
204
|
// Admission is bounded local validation; no provider operation is started.
|
|
170
205
|
timeout: SETUP_STARTUP_MS, maxBuffer: 1024 * 1024, shell: false, windowsHide: true });
|
|
@@ -249,6 +249,8 @@ export function validateHybridContract(name, input, context = {}) {
|
|
|
249
249
|
fail("foreign_bundle_canon", "/canon");
|
|
250
250
|
if (hybridDigest(data.project_pack) !== hybridDigest(context.bundleProvenance.project_pack))
|
|
251
251
|
fail("foreign_bundle_project_pack", "/project_pack");
|
|
252
|
+
if ((data.flow_id === "mb-audit" || data.flow_id === "project-review") !== (data.project_pack === null))
|
|
253
|
+
fail("invalid_bundle_project_pack_role", "/project_pack");
|
|
252
254
|
const memoryFlow = data.flow_id === "mb-init" || data.flow_id === "mb-upgrade";
|
|
253
255
|
if (memoryFlow && context.bootstrapIdentity?.projectRoot !== context.bundleProjectRoot)
|
|
254
256
|
fail("foreign_bootstrap_identity", "/bootstrap_receipts");
|
|
@@ -6,7 +6,9 @@ import { readSetupStatus } from "./setup.js";
|
|
|
6
6
|
import { readBoundedReleaseFile } from "./release-source.js";
|
|
7
7
|
import { hybridDigest, validateHybridShape } from "./hybrid-contracts.js";
|
|
8
8
|
import { admitBundledFlow } from "./stage-handlers.js";
|
|
9
|
+
import { isDiagnosticFlow } from "../domain/diagnostic-flows.js";
|
|
9
10
|
const limit = 1024 * 1024;
|
|
11
|
+
const flowIds = ["mb-init", "mb-upgrade", "mb-audit", "project-review"];
|
|
10
12
|
const hash = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
11
13
|
function refuse(reason) {
|
|
12
14
|
throw new AppError("installed_flow_source_invalid", "Installed flow source is not the selected verified canon", 1, { reason, phase: "prepare", effect: "no_effect" });
|
|
@@ -50,7 +52,7 @@ export class PreparedInstalledFlowSource {
|
|
|
50
52
|
}
|
|
51
53
|
static fromInstalledRelease(home, expectedInstallationSha256, flowId) {
|
|
52
54
|
try {
|
|
53
|
-
if (!path.isAbsolute(home) || !/^[a-f0-9]{64}$/.test(expectedInstallationSha256) || !
|
|
55
|
+
if (!path.isAbsolute(home) || !/^[a-f0-9]{64}$/.test(expectedInstallationSha256) || !flowIds.includes(flowId))
|
|
54
56
|
refuse("invalid_selection");
|
|
55
57
|
const selected = readSetupStatus(home);
|
|
56
58
|
if (!selected.installation || selected.installation_readiness !== "installed")
|
|
@@ -95,6 +97,26 @@ export class PreparedInstalledFlowSource {
|
|
|
95
97
|
folded.set(reference.path.toLowerCase(), reference.path);
|
|
96
98
|
blobs.set(reference.path, Buffer.from(content));
|
|
97
99
|
}
|
|
100
|
+
if (isDiagnosticFlow(flowId)) {
|
|
101
|
+
const support = ["mbb/index.md", "mbb/memory-bank-structure.md", "mbb/frontmatter-standards.md", "mbb/cross-references.md", "mbb/named-deferrals-guide.md",
|
|
102
|
+
...fs.readdirSync(path.join(memoryRoot, "mbb/aspects")).filter(name => name.endsWith(".md")).map(name => `mbb/aspects/${name}`),
|
|
103
|
+
"dd-flow/mb-sdlc/review/aspects.md", "dd-flow/mb-sdlc/review/critics/universal.md", "dd-flow/mb-sdlc/review/critics/architectural-harmonization.md",
|
|
104
|
+
...fs.readdirSync(path.join(flowRoot, "mb-audit/aspects")).filter(name => name.endsWith(".md")).map(name => `dd-flow/mb-audit/aspects/${name}`)];
|
|
105
|
+
for (const logical of support) {
|
|
106
|
+
const content = readBoundedReleaseFile(containedFile(memoryRoot, logical), limit), name = `support/${logical}`;
|
|
107
|
+
if (!content.length || (total += content.length) > 32 * limit || folded.has(name.toLowerCase()))
|
|
108
|
+
refuse("invalid_support_inventory");
|
|
109
|
+
new TextDecoder("utf-8", { fatal: true }).decode(content);
|
|
110
|
+
folded.set(name.toLowerCase(), name);
|
|
111
|
+
blobs.set(name, Buffer.from(content));
|
|
112
|
+
}
|
|
113
|
+
const workSchema = "schemas/diagnostic-work-result.schema.json";
|
|
114
|
+
const content = readBoundedReleaseFile(containedFile(flowRoot, workSchema), limit);
|
|
115
|
+
if (!content.length || (total += content.length) > 32 * limit || folded.has(workSchema.toLowerCase()))
|
|
116
|
+
refuse("invalid_support_inventory");
|
|
117
|
+
JSON.parse(content.toString("utf8"));
|
|
118
|
+
blobs.set(workSchema, Buffer.from(content));
|
|
119
|
+
}
|
|
98
120
|
// Verify compressed/expanded archive identity again after collecting bytes.
|
|
99
121
|
// A coherent descriptor+prompt edit cannot become a new trusted selection.
|
|
100
122
|
const final = readSetupStatus(home);
|
|
@@ -144,7 +166,7 @@ export class PreparedInstalledFlowSource {
|
|
|
144
166
|
|| typeof value.canon.archive_sha256 !== "string" || !/^[a-f0-9]{64}$/.test(value.canon.archive_sha256)
|
|
145
167
|
|| typeof value.canon.version !== "string" || !/^\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?$/.test(value.canon.version)
|
|
146
168
|
|| !value.descriptor || Object.keys(value.descriptor).sort().join() !== "path,sha256"
|
|
147
|
-
|| !
|
|
169
|
+
|| !flowIds.some(id => value.descriptor.path === `${id}/flow-descriptor.json`) || typeof value.descriptor.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(value.descriptor.sha256)
|
|
148
170
|
|| !Array.isArray(value.blobs) || value.blobs.length < 1 || value.blobs.length > 129)
|
|
149
171
|
refuse("invalid_retained_source");
|
|
150
172
|
const blobs = new Map(), folded = new Set();
|
|
@@ -166,7 +188,7 @@ export class PreparedInstalledFlowSource {
|
|
|
166
188
|
const descriptor = blobs.get(value.descriptor.path);
|
|
167
189
|
if (!descriptor || hash(descriptor) !== value.descriptor.sha256)
|
|
168
190
|
refuse("retained_source_changed");
|
|
169
|
-
const flowId = value.descriptor.path.
|
|
191
|
+
const flowId = value.descriptor.path.slice(0, -"/flow-descriptor.json".length);
|
|
170
192
|
const declared = JSON.parse(descriptor.toString("utf8"));
|
|
171
193
|
if (validateHybridShape("flow-descriptor", declared).length || declared.id !== flowId || declared.origin !== "canon")
|
|
172
194
|
refuse("invalid_retained_source");
|
|
@@ -24,6 +24,9 @@ import { runtimeCommand, shellArgument } from "./runtime-command.js";
|
|
|
24
24
|
// Process-local custody: an executing SQL row or ambient variable alone cannot
|
|
25
25
|
// recreate admission after a CLI restart, or in another resource database.
|
|
26
26
|
const currentAdmissions = new WeakMap();
|
|
27
|
+
// A same-stage finish continuation is not authority until its predecessor's
|
|
28
|
+
// outcome commits. Process death before that commit leaves no issued successor.
|
|
29
|
+
const stagedContinuations = new WeakMap();
|
|
27
30
|
export function currentLifecycleAdmission(context, id) {
|
|
28
31
|
const owned = currentAdmissions.get(context.db)?.get(id);
|
|
29
32
|
if (!owned || context.env.DD_FLOW_CURRENT_INVOCATION !== id || owned.pid !== process.pid
|
|
@@ -379,8 +382,9 @@ export function assertLifecycleOutcomes(context, scope) {
|
|
|
379
382
|
AND NOT EXISTS (SELECT 1 FROM lifecycle_invocations newer WHERE newer.scope_json = a.scope_json AND newer.fingerprint = a.fingerprint AND newer.rowid > a.rowid)
|
|
380
383
|
ORDER BY a.rowid`, [scope.projectRoot, scope.runId, scope.generation]);
|
|
381
384
|
for (const row of rows) {
|
|
382
|
-
const
|
|
383
|
-
|
|
385
|
+
const outcome = row.outcome_json ? JSON.parse(row.outcome_json) : null;
|
|
386
|
+
const error = outcome?.error;
|
|
387
|
+
if (typeof error?.code === "string" && !committedSuccessor(context, row, outcome) && !isCorrectableLifecycleRejection(error.code, error.details) && !isRegisteredRepair(context, error.details?.continuation, { projectId: project.id, runId: scope.runId }))
|
|
384
388
|
throw new AppError(error.code, typeof error.message === "string" ? error.message : "Mandatory lifecycle failed", 1, { ...(error.details ?? {}), invocation_id: row.id });
|
|
385
389
|
}
|
|
386
390
|
}
|
|
@@ -489,14 +493,27 @@ export function observeLifecycleCommand(context, command, nativeEvent) {
|
|
|
489
493
|
}
|
|
490
494
|
}
|
|
491
495
|
const registeredContinuation = error && registeredLifecycleContinuation(context, error, anchor.project_id, anchor.run_id);
|
|
492
|
-
const
|
|
493
|
-
&& typeof error.details.rejection_signature === "string"
|
|
496
|
+
const retainedRejections = error && typeof error.details.retry_command === "string"
|
|
494
497
|
&& context.db.get("SELECT 1 FROM sqlite_master WHERE name = 'lifecycle_invocations'")
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
AND json_extract(a.scope_json,'$.
|
|
498
|
-
AND json_extract(a.scope_json,'$.generation') = ? AND json_extract(a.outcome_json,'$.error.
|
|
499
|
-
AND json_extract(a.outcome_json,'$.error.details.
|
|
498
|
+
? context.db.all(`SELECT a.* FROM lifecycle_invocations a JOIN hook_events h ON h.event_key = a.event_key
|
|
499
|
+
WHERE h.id = ? AND h.project_id = ? AND a.status = 'settled'
|
|
500
|
+
AND json_extract(a.scope_json,'$.runId') = ?
|
|
501
|
+
AND json_extract(a.scope_json,'$.generation') = ? AND json_extract(a.outcome_json,'$.error.code') = ?
|
|
502
|
+
AND json_extract(a.outcome_json,'$.error.details.retry_command') = ?`, [anchor.hook_event_id, anchor.project_id, anchor.run_id, anchor.generation, error.code, error.details.retry_command]) : [];
|
|
503
|
+
const projectRoot = context.db.get("SELECT root FROM projects WHERE id = ?", [anchor.project_id])?.root;
|
|
504
|
+
const ownedRejections = retainedRejections.filter(row => {
|
|
505
|
+
try {
|
|
506
|
+
return projectRoot !== undefined && canonicalPath(JSON.parse(row.scope_json).projectRoot, false) === projectRoot;
|
|
507
|
+
}
|
|
508
|
+
catch {
|
|
509
|
+
return false;
|
|
510
|
+
}
|
|
511
|
+
});
|
|
512
|
+
const retainedRejection = ownedRejections.length === 1 ? ownedRejections[0] : undefined;
|
|
513
|
+
// A committed exact successor is authority; reclassifying its sanitized
|
|
514
|
+
// error would discard the original validated check capability again.
|
|
515
|
+
const retainedSuccessor = retainedRejection && (committedSuccessor(context, retainedRejection, JSON.parse(retainedRejection.outcome_json ?? "null"))
|
|
516
|
+
?? (error && currentIssuedCommand(context, retainedRejection.scope_json, String(error.details.retry_command))));
|
|
500
517
|
const hookOnlyArgumentDefect = error && !invocationId && !context.env.DD_FLOW_INVOCATION_SCOPE
|
|
501
518
|
&& !retainedSuccessor
|
|
502
519
|
&& ["missing_required_option", "missing_option_value", "empty_option_value", "invalid_number", "invalid_choice", "duplicate_option"].includes(String(error.details.reason));
|
|
@@ -535,7 +552,7 @@ export function observeLifecycleCommand(context, command, nativeEvent) {
|
|
|
535
552
|
}
|
|
536
553
|
}
|
|
537
554
|
const correctable = error && !hookOnlyArgumentDefect && error.details.recoverable !== false && error.details.effect !== "unknown"
|
|
538
|
-
&& (registeredContinuation || error.details.effect === "no_effect" && isCorrectableLifecycleRejection(error.code, error.details)
|
|
555
|
+
&& (registeredContinuation || retainedSuccessor || error.details.effect === "no_effect" && isCorrectableLifecycleRejection(error.code, error.details)
|
|
539
556
|
&& typeof error.details.retry_command === "string");
|
|
540
557
|
const correlationKey = parsed.kind === "standalone" ? fingerprint(parsed.invocation)
|
|
541
558
|
: JSON.stringify([auxiliary.operation, auxiliary.operation === "stage_block" ? auxiliary.args.positional[2] ?? null : auxiliary.args.options.get("run")?.[0] ?? null, auxiliary.args.options.get("stage")?.[0] ?? null]);
|
|
@@ -645,18 +662,60 @@ export function lifecycleRetryCommands(context, stage) {
|
|
|
645
662
|
.flatMap((row) => {
|
|
646
663
|
try {
|
|
647
664
|
const outcome = row.outcome_json ? JSON.parse(row.outcome_json) : null;
|
|
648
|
-
const retry = outcome?.error?.details?.retry_command;
|
|
665
|
+
const retry = outcome?.error?.details?.retry_command ?? outcome?.result?.next?.finish_command;
|
|
649
666
|
const parsed = invocation(row.command);
|
|
650
667
|
if (typeof retry !== "string" || parsed.operation !== "stage_finish" || commandOption(parsed, "stage") !== stage)
|
|
651
668
|
return [];
|
|
652
|
-
const
|
|
653
|
-
|
|
669
|
+
const successor = outcome?.successor_invocation_id
|
|
670
|
+
? context.db.get("SELECT * FROM lifecycle_invocations WHERE id = ? AND scope_json = ?", [outcome.successor_invocation_id, row.scope_json])
|
|
671
|
+
: currentIssuedCommand(context, row.scope_json, retry);
|
|
672
|
+
return successor?.status === "issued" && successor.fingerprint === scopedCommandFingerprint(context, row.scope_json, retry)
|
|
673
|
+
&& (successor.deadline === null || successor.deadline > Date.now()) ? [publicInvocationCommand(context, successor, successor.command)] : [];
|
|
654
674
|
}
|
|
655
675
|
catch {
|
|
656
676
|
return [];
|
|
657
677
|
}
|
|
658
678
|
}))];
|
|
659
679
|
}
|
|
680
|
+
function currentIssuedCommand(context, scope, command) {
|
|
681
|
+
const rows = context.db.all("SELECT * FROM lifecycle_invocations WHERE scope_json = ? AND fingerprint = ? AND status = 'issued'", [scope, scopedCommandFingerprint(context, scope, command)])
|
|
682
|
+
.filter(row => row.deadline === null || row.deadline > Date.now());
|
|
683
|
+
if (rows.length > 1)
|
|
684
|
+
throw new AppError("invocation_ambiguous", "Multiple current lifecycle successors match this command", 1);
|
|
685
|
+
return rows[0] ?? null;
|
|
686
|
+
}
|
|
687
|
+
function scopedCommandFingerprint(context, scope, command) {
|
|
688
|
+
return fingerprint(invocation(canonicalManagedCommand(context, command, JSON.parse(scope))));
|
|
689
|
+
}
|
|
690
|
+
function committedSuccessor(context, prior, outcome) {
|
|
691
|
+
if (typeof outcome?.successor_invocation_id !== "string" || outcome.successor_invocation_id === prior.id)
|
|
692
|
+
return null;
|
|
693
|
+
const command = outcome.error?.details?.retry_command ?? outcome.result?.next?.finish_command;
|
|
694
|
+
if (typeof command !== "string")
|
|
695
|
+
return null;
|
|
696
|
+
const next = context.db.get("SELECT * FROM lifecycle_invocations WHERE id = ? AND scope_json = ?", [outcome.successor_invocation_id, prior.scope_json]);
|
|
697
|
+
return next && next.status !== "expired" && (next.status !== "issued" || next.deadline === null || next.deadline > Date.now())
|
|
698
|
+
&& next.fingerprint === scopedCommandFingerprint(context, prior.scope_json, command) ? next : null;
|
|
699
|
+
}
|
|
700
|
+
function invocationRecoverySnapshot(context, scope) {
|
|
701
|
+
return JSON.stringify(context.db.get(`SELECT g.generation,g.status,g.recovery_id FROM run_recovery_guards g
|
|
702
|
+
JOIN projects p ON p.id=g.project_id WHERE p.root=? AND g.run_id=? ORDER BY g.generation DESC LIMIT 1`, [scope.projectRoot, scope.runId]) ?? null);
|
|
703
|
+
}
|
|
704
|
+
/** Resolve authority, never reconstruct it from an old prompt or issue on read. */
|
|
705
|
+
export function currentStageFinishCommand(context, stage) {
|
|
706
|
+
const scope = context.env.DD_FLOW_INVOCATION_SCOPE;
|
|
707
|
+
if (!scope || !context.db.get("SELECT 1 FROM sqlite_master WHERE name='lifecycle_invocations'"))
|
|
708
|
+
return null;
|
|
709
|
+
const rows = context.db.all("SELECT * FROM lifecycle_invocations WHERE scope_json = ? AND status = 'issued'", [scope])
|
|
710
|
+
.filter(row => {
|
|
711
|
+
const parsed = invocation(row.command);
|
|
712
|
+
return parsed.operation === "stage_finish" && commandOption(parsed, "stage") === stage
|
|
713
|
+
&& (row.deadline === null || row.deadline > Date.now());
|
|
714
|
+
});
|
|
715
|
+
if (rows.length > 1)
|
|
716
|
+
throw new AppError("invocation_ambiguous", "Stage has multiple current finish authorities", 1, { stage });
|
|
717
|
+
return rows[0] ? publicInvocationCommand(context, rows[0], rows[0].command) : null;
|
|
718
|
+
}
|
|
660
719
|
export function managedInvocationContext(context, input) {
|
|
661
720
|
const env = { ...context.env, DD_FLOW_CURRENT_INVOCATION: undefined, DD_FLOW_INVOCATION_SCOPE: undefined };
|
|
662
721
|
if (!["zcode-acp", "codex-desktop", "grok-acp", "antigravity-cli", "droid-cli", "opencode-server"].includes(input.harness))
|
|
@@ -712,6 +771,21 @@ export function managedLifecycleCommand(context, command, options = {}) {
|
|
|
712
771
|
return publicInvocationCommand(context, retained, command);
|
|
713
772
|
}
|
|
714
773
|
storage(context);
|
|
774
|
+
const predecessorId = context.env.DD_FLOW_CURRENT_INVOCATION;
|
|
775
|
+
const predecessor = predecessorId ? load(context, predecessorId) : null;
|
|
776
|
+
if (predecessor?.status === "executing" && predecessor.scope_json === configured
|
|
777
|
+
&& parsed.operation === "stage_finish" && invocation(predecessor.command).operation === "stage_finish"
|
|
778
|
+
&& commandOption(parsed, "stage") === commandOption(invocation(predecessor.command), "stage")) {
|
|
779
|
+
let staged = stagedContinuations.get(context.db);
|
|
780
|
+
if (!staged)
|
|
781
|
+
stagedContinuations.set(context.db, staged = new Map());
|
|
782
|
+
const prior = staged.get(predecessor.id);
|
|
783
|
+
if (prior && prior.fingerprint !== fingerprint(parsed))
|
|
784
|
+
throw new AppError("invocation_continuation_conflict", "One finish may publish only one exact successor", 1);
|
|
785
|
+
const next = prior ?? prepareIssuedInvocation(command, scope);
|
|
786
|
+
staged.set(predecessor.id, next);
|
|
787
|
+
return publicInvocationCommand(context, next, command);
|
|
788
|
+
}
|
|
715
789
|
return context.db.writeTransaction(() => {
|
|
716
790
|
const prior = options.freshWorkAttempt ? undefined : context.db.get("SELECT * FROM lifecycle_invocations WHERE scope_json = ? AND fingerprint = ? ORDER BY rowid DESC LIMIT 1", [JSON.stringify(scope), fingerprint(parsed)]);
|
|
717
791
|
if (prior && prior.id !== context.env.DD_FLOW_CURRENT_INVOCATION) {
|
|
@@ -792,17 +866,35 @@ export function settleLifecycleRejection(context, id, error) {
|
|
|
792
866
|
// Only this settlement may publish a successor; inherited help is not authority.
|
|
793
867
|
delete error.details.retry_command;
|
|
794
868
|
delete error.details.retry_instruction;
|
|
795
|
-
|
|
869
|
+
// A producer may have rendered help before throwing. Only the decision below
|
|
870
|
+
// may publish replacement authority for a rejected operation.
|
|
871
|
+
stagedContinuations.get(context.db)?.delete(id);
|
|
872
|
+
// Validate process-local receipt capabilities BEFORE diagnostic cloning.
|
|
873
|
+
// Only this local preparation crosses into the SQL commit below; public
|
|
874
|
+
// category fields/serialized receipts cannot recreate the decision.
|
|
875
|
+
const prepared = {
|
|
876
|
+
invocation: JSON.stringify(load(context, id)),
|
|
877
|
+
recovery: invocationRecoverySnapshot(context, JSON.parse(load(context, id).scope_json)),
|
|
878
|
+
retryable: isRetryableLifecycleRejection(error.code, error.details),
|
|
879
|
+
receipts: ["work_checks_failed", "code_gate_failed", "code_review_gate_failed", "merge_gate_failed"].includes(error.code) && Array.isArray(error.details.failures)
|
|
880
|
+
? error.details.failures.map(receipt => ({ receipt, row: context.db.get("SELECT * FROM check_receipts WHERE id = ?", [receipt.id]) })) : []
|
|
881
|
+
};
|
|
882
|
+
if (prepared.receipts.some(({ receipt, row }) => !row || row.status !== receipt.status || row.verification_epoch !== receipt.verification_epoch || row.command !== receipt.command || row.input_hash !== receipt.input_hash))
|
|
883
|
+
prepared.retryable = false;
|
|
884
|
+
const serialized = errorRecord(error);
|
|
885
|
+
const details = { ...serialized.details };
|
|
796
886
|
try {
|
|
797
887
|
context.db.writeTransaction(() => {
|
|
798
888
|
const prior = load(context, id);
|
|
799
|
-
if (prior.status !== "executing")
|
|
800
|
-
|
|
889
|
+
if (prior.status !== "executing" || JSON.stringify(prior) !== prepared.invocation
|
|
890
|
+
|| invocationRecoverySnapshot(context, JSON.parse(prior.scope_json)) !== prepared.recovery
|
|
891
|
+
|| prepared.receipts.some(({ receipt, row }) => JSON.stringify(context.db.get("SELECT * FROM check_receipts WHERE id = ?", [receipt.id])) !== JSON.stringify(row)))
|
|
892
|
+
throw new AppError("invocation_settlement_conflict", "Attempt or bound check receipts changed before settlement", 1, { invocation_id: id });
|
|
801
893
|
const storageRetry = error.code === "storage_write_failed" && error.details.effect === "no_effect"
|
|
802
894
|
&& !context.db.get("SELECT 1 FROM lifecycle_invocations WHERE scope_json = ? AND fingerprint = ? AND json_extract(outcome_json, '$.error.details.storage_retry') = 1 LIMIT 1", [prior.scope_json, prior.fingerprint]);
|
|
803
|
-
if (
|
|
804
|
-
const failures =
|
|
805
|
-
const retry = retryLifecycleInvocationCommand(context, id, failures
|
|
895
|
+
if (prepared.retryable || storageRetry) {
|
|
896
|
+
const failures = prepared.receipts.map(value => value.receipt);
|
|
897
|
+
const retry = retryLifecycleInvocationCommand(context, id, failures[0]?.id);
|
|
806
898
|
details.retry_command = retry;
|
|
807
899
|
details.effect = "no_effect";
|
|
808
900
|
details.recoverable = true;
|
|
@@ -824,7 +916,9 @@ export function settleLifecycleRejection(context, id, error) {
|
|
|
824
916
|
else {
|
|
825
917
|
details.recoverable = false;
|
|
826
918
|
}
|
|
827
|
-
|
|
919
|
+
const successor = stagedContinuations.get(context.db)?.get(id)
|
|
920
|
+
?? (typeof details.retry_command === "string" ? currentIssuedCommand(context, prior.scope_json, details.retry_command) : null);
|
|
921
|
+
settleLifecycleInvocation(context, id, { error: { ...serialized, details }, ...(successor ? { successor_invocation_id: successor.id } : {}) });
|
|
828
922
|
});
|
|
829
923
|
}
|
|
830
924
|
catch (persistence) {
|
|
@@ -963,15 +1057,22 @@ export function prepareLifecycleIssuance(command, value) {
|
|
|
963
1057
|
return { command, scope, parsed };
|
|
964
1058
|
}
|
|
965
1059
|
export function issueLifecycleInvocation(context, command, scope) {
|
|
966
|
-
const
|
|
1060
|
+
const row = prepareIssuedInvocation(command, scope);
|
|
967
1061
|
storage(context);
|
|
1062
|
+
insertIssuedInvocation(context, row);
|
|
1063
|
+
return { id: row.id, command: row.command };
|
|
1064
|
+
}
|
|
1065
|
+
function prepareIssuedInvocation(command, scope) {
|
|
1066
|
+
const { parsed } = prepareLifecycleIssuance(command, scope);
|
|
968
1067
|
const id = crypto.randomUUID();
|
|
969
1068
|
const prepared = commandWithInvocationId(command, id);
|
|
970
1069
|
// Validate the actual inserted argv, including quoted heredoc boundaries.
|
|
971
|
-
const row = { id, command: prepared, scope_json: JSON.stringify(scope), fingerprint: fingerprint(parsed) };
|
|
1070
|
+
const row = { id, command: prepared, scope_json: JSON.stringify(scope), fingerprint: fingerprint(parsed), status: "issued", event_key: null, identity_json: null, deadline: null, outcome_json: null };
|
|
972
1071
|
checkCommand(row, prepared);
|
|
973
|
-
|
|
974
|
-
|
|
1072
|
+
return row;
|
|
1073
|
+
}
|
|
1074
|
+
function insertIssuedInvocation(context, row) {
|
|
1075
|
+
context.db.run("INSERT INTO lifecycle_invocations (id, scope_json, command, fingerprint, status, created_at, updated_at) VALUES (?, ?, ?, ?, 'issued', ?, ?)", [row.id, row.scope_json, row.command, row.fingerprint, context.now(), context.now()]);
|
|
975
1076
|
}
|
|
976
1077
|
/** Prepare outside a write transaction; the Work retry CAS owns issuance. */
|
|
977
1078
|
export function prepareWorkStartAuthority(context, command, previousId) {
|
|
@@ -1935,6 +2036,9 @@ export async function awaitLifecycleInvocation(context, input) {
|
|
|
1935
2036
|
const { recoverCommittedStageResume } = await import("./stage-pause.js");
|
|
1936
2037
|
const result = recoverCommittedStageResume(context, { id: row.id, projectRoot: scope.projectRoot, runId: scope.runId, generation: scope.generation, stage: commandOption(parsed, "stage") ?? "", workId: commandOption(parsed, "work") ?? "" });
|
|
1937
2038
|
if (result) {
|
|
2039
|
+
const project = requireProjectByRoot(context, scope.projectRoot);
|
|
2040
|
+
const { publishStagePacket } = await import("./stage-report-renderer.js");
|
|
2041
|
+
publishStagePacket(context, project.id, scope.runId, commandOption(parsed, "stage") ?? "", String(result.resume_source_path));
|
|
1938
2042
|
const { preparePromptDelivery } = await import("./prompt-delivery.js");
|
|
1939
2043
|
const delivered = preparePromptDelivery(result);
|
|
1940
2044
|
const previousScope = context.env.DD_FLOW_INVOCATION_SCOPE;
|
|
@@ -1948,7 +2052,6 @@ export async function awaitLifecycleInvocation(context, input) {
|
|
|
1948
2052
|
else
|
|
1949
2053
|
context.env.DD_FLOW_INVOCATION_SCOPE = previousScope;
|
|
1950
2054
|
}
|
|
1951
|
-
const project = requireProjectByRoot(context, scope.projectRoot);
|
|
1952
2055
|
const event = row.event_key ? context.db.get("SELECT id FROM hook_events WHERE project_id = ? AND event_key = ?", [project.id, row.event_key]) : null;
|
|
1953
2056
|
if (event)
|
|
1954
2057
|
observeLifecycleCommand(context, row.command, { project_id: project.id, id: event.id })({ result: delivered });
|
|
@@ -2022,14 +2125,24 @@ export async function awaitLifecycleInvocation(context, input) {
|
|
|
2022
2125
|
}
|
|
2023
2126
|
}
|
|
2024
2127
|
export function settleLifecycleInvocation(context, id, outcome) {
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
if (encoded === undefined)
|
|
2028
|
-
throw new AppError("invocation_outcome_invalid", "Invocation outcome must be JSON", 1);
|
|
2029
|
-
const changed = context.db.run("UPDATE lifecycle_invocations SET status = 'settled', outcome_json = ?, updated_at = ? WHERE id = ? AND status = 'executing'", [encoded, context.now(), id]);
|
|
2030
|
-
if (changed.changes !== 1) {
|
|
2128
|
+
const successor = stagedContinuations.get(context.db)?.get(id);
|
|
2129
|
+
context.db.writeTransaction(() => {
|
|
2031
2130
|
const row = load(context, id);
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2131
|
+
const retained = successor && outcome && typeof outcome === "object" ? { ...outcome, successor_invocation_id: successor.id } : outcome;
|
|
2132
|
+
const encoded = JSON.stringify(retained);
|
|
2133
|
+
if (encoded === undefined)
|
|
2134
|
+
throw new AppError("invocation_outcome_invalid", "Invocation outcome must be JSON", 1);
|
|
2135
|
+
if (row.status === "settled") {
|
|
2136
|
+
if (row.outcome_json !== encoded)
|
|
2137
|
+
throw new AppError("invocation_settlement_conflict", "Attempt cannot be settled with a different outcome", 1, { invocation_id: id });
|
|
2138
|
+
return;
|
|
2139
|
+
}
|
|
2140
|
+
if (row.status !== "executing")
|
|
2141
|
+
throw new AppError("invocation_settlement_conflict", "Attempt is no longer executing", 1, { invocation_id: id });
|
|
2142
|
+
if (successor)
|
|
2143
|
+
insertIssuedInvocation(context, successor);
|
|
2144
|
+
if (context.db.run("UPDATE lifecycle_invocations SET status = 'settled', outcome_json = ?, updated_at = ? WHERE id = ? AND status = 'executing'", [encoded, context.now(), id]).changes !== 1)
|
|
2145
|
+
throw new AppError("invocation_settlement_conflict", "Lifecycle outcome changed before commit", 1, { invocation_id: id });
|
|
2146
|
+
});
|
|
2147
|
+
currentAdmissions.get(context.db)?.delete(id);
|
|
2035
2148
|
}
|
|
@@ -7,6 +7,22 @@ import { publishImmutableInput } from "../storage/immutable-input.js";
|
|
|
7
7
|
import { readBoundedReleaseFile } from "./release-source.js";
|
|
8
8
|
import { readMemoryInitStartupInput } from "./memory-init-startup-custody.js";
|
|
9
9
|
import { readUpgradeStorageReadiness } from "./memory-upgrade-storage-readiness.js";
|
|
10
|
+
import { isDiagnosticFlow } from "../domain/diagnostic-flows.js";
|
|
11
|
+
import { readDiagnosticRequest } from "./diagnostic-start.js";
|
|
12
|
+
export function prepareDiagnosticContextsFromRequest(context, bundle, directory) {
|
|
13
|
+
const scope = bundle.execution.snapshot().scope, flowId = bundle.manifest.flow_id;
|
|
14
|
+
if (!isDiagnosticFlow(flowId))
|
|
15
|
+
throw new AppError("diagnostic_context_invalid", "Diagnostic bundle required", 2, { effect: "no_effect" });
|
|
16
|
+
const request = readDiagnosticRequest(context, { projectRoot: scope.projectRoot, requestId: scope.requestId, flowId });
|
|
17
|
+
if (!request || request.execution_sha256 !== bundle.execution.sha256)
|
|
18
|
+
throw new AppError("diagnostic_context_invalid", "Original diagnostic objective required", 2, { effect: "no_effect" });
|
|
19
|
+
return { ...prepareMemoryContexts(bundle, request.objective, directory, flowId), objective: request.objective };
|
|
20
|
+
}
|
|
21
|
+
export function publishDiagnosticContexts(bundle, objective, directory, expectedSha256, expectedOwner) {
|
|
22
|
+
if (!isDiagnosticFlow(bundle.manifest.flow_id))
|
|
23
|
+
throw new AppError("diagnostic_context_invalid", "Diagnostic bundle required", 2, { effect: "no_effect" });
|
|
24
|
+
return publishMemoryContexts(bundle, objective, directory, expectedSha256, expectedOwner, bundle.manifest.flow_id);
|
|
25
|
+
}
|
|
10
26
|
/** Original upgrade intake comes from the independently retained storage
|
|
11
27
|
* selection, including after project publication changes the old tree. */
|
|
12
28
|
export function prepareMemoryUpgradeContextsFromStartup(context, bundle, directory) {
|
|
@@ -11,6 +11,9 @@ import { requireRun } from "./run-controller-state.js";
|
|
|
11
11
|
import { prepareRunControllerLaunch } from "./run-controller.js";
|
|
12
12
|
import { prepareMemoryInitContextsFromStartup, publishMemoryInitContexts, prepareMemoryUpgradeContextsFromStartup, publishMemoryUpgradeContexts } from "./memory-init-contexts.js";
|
|
13
13
|
import { parseRuntimeBudget } from "./runtime-budget.js";
|
|
14
|
+
import { isDiagnosticFlow } from "../domain/diagnostic-flows.js";
|
|
15
|
+
import { startDiagnosticRun } from "./diagnostic-start.js";
|
|
16
|
+
import { prepareDiagnosticContextsFromRequest, publishDiagnosticContexts } from "./memory-init-contexts.js";
|
|
14
17
|
function refuse(reason) {
|
|
15
18
|
throw new AppError("memory_init_controller_start_invalid", "Init controller requires its exact published startup request", 1, { reason, effect: "no_effect", next_action: "reconcile_exact_request" });
|
|
16
19
|
}
|
|
@@ -24,6 +27,9 @@ export function prepareMemoryInitControllerStart(context, scope) {
|
|
|
24
27
|
export function prepareMemoryUpgradeControllerStart(context, scope) {
|
|
25
28
|
return prepareMemoryControllerStart(context, scope, "mb-upgrade");
|
|
26
29
|
}
|
|
30
|
+
export function prepareDiagnosticControllerStart(context, scope, flowId) {
|
|
31
|
+
return prepareMemoryControllerStart(context, scope, flowId);
|
|
32
|
+
}
|
|
27
33
|
function prepareMemoryControllerStart(context, scope, flowId) {
|
|
28
34
|
validateMemoryInitStartupScope(scope);
|
|
29
35
|
if (context.db.isOwnedWriteTransaction?.())
|
|
@@ -41,15 +47,17 @@ function prepareMemoryControllerStart(context, scope, flowId) {
|
|
|
41
47
|
refuse("published_run_required");
|
|
42
48
|
// Restores the exact request and verifies independent SQL/physical root
|
|
43
49
|
// ownership without consulting current canon/profiles or creating a RUN.
|
|
44
|
-
const started = flowId
|
|
50
|
+
const started = isDiagnosticFlow(flowId) ? startDiagnosticRun(context, { projectRoot: scope.projectRoot, requestId: scope.requestId, flowId })
|
|
51
|
+
: flowId === "mb-init" ? startMemoryInitRun(context, scope) : startMemoryUpgradeRun(context, scope);
|
|
45
52
|
const run = requireRun(context, project.id, started.runId);
|
|
46
53
|
const bundle = readFlowRunExecutionBundle(context, { projectRoot: scope.projectRoot, runId: run.id });
|
|
47
54
|
if (!bundle || bundle.sha256 !== started.bundleSha256)
|
|
48
55
|
refuse("retained_bundle_required");
|
|
49
56
|
if (bundle.execution.settings.execution !== "managed")
|
|
50
57
|
refuse("managed_execution_required");
|
|
51
|
-
const contexts = flowId
|
|
52
|
-
:
|
|
58
|
+
const contexts = isDiagnosticFlow(flowId) ? prepareDiagnosticContextsFromRequest(context, bundle, run.run_root)
|
|
59
|
+
: flowId === "mb-init" ? prepareMemoryInitContextsFromStartup(context, bundle, run.run_root)
|
|
60
|
+
: prepareMemoryUpgradeContextsFromStartup(context, bundle, run.run_root);
|
|
53
61
|
const input = { projectRoot: scope.projectRoot, runId: run.id, requestId: scope.requestId,
|
|
54
62
|
contextFile: contexts.file, contextSha256: contexts.sha256 };
|
|
55
63
|
const controllers = context.db.get("SELECT 1 FROM sqlite_schema WHERE type='table' AND name='run_controllers'")
|
|
@@ -83,7 +91,7 @@ function prepareMemoryControllerStart(context, scope, flowId) {
|
|
|
83
91
|
});
|
|
84
92
|
if (!rootOwner || !Number.isSafeInteger(rootOwner.dev) || !Number.isSafeInteger(rootOwner.ino))
|
|
85
93
|
refuse("root_owner_changed");
|
|
86
|
-
const publish = flowId === "mb-init" ? publishMemoryInitContexts : publishMemoryUpgradeContexts;
|
|
94
|
+
const publish = isDiagnosticFlow(flowId) ? publishDiagnosticContexts : flowId === "mb-init" ? publishMemoryInitContexts : publishMemoryUpgradeContexts;
|
|
87
95
|
publish(bundle, contexts.objective, run.run_root, contexts.sha256, rootOwner);
|
|
88
96
|
try {
|
|
89
97
|
return { input, prepared: prepareRunControllerLaunch(context, input) };
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { spawnSync } from "node:child_process";
|
|
5
4
|
import { createHash } from "node:crypto";
|
|
6
5
|
import { AppError } from "../shared/errors.js";
|
|
7
6
|
import { hybridDigest } from "./hybrid-contracts.js";
|
|
8
7
|
import { PreparedMemoryInitTarget } from "./memory-init-preflight.js";
|
|
9
8
|
import { readBoundedReleaseFile } from "./release-source.js";
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
import { runMemoryToolProbe } from "./memory-tool-probe.js";
|
|
10
|
+
function refuse(reason, diagnostics) {
|
|
11
|
+
throw new AppError("memory_init_lint_capability_failed", "Selected mb-lint cannot validate Memory Bank documents", 1, { reason, effect: "no_effect", next_action: "repair_prerequisite_then_retry", ...(diagnostics ? { tool_probe: diagnostics } : {}) });
|
|
12
12
|
}
|
|
13
13
|
const hash = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
14
14
|
const logical = ".memory-bank/index.md";
|
|
@@ -105,9 +105,9 @@ export class PreparedMemoryInitLintCapability {
|
|
|
105
105
|
const probeEnv = Object.fromEntries(Object.entries(env).filter(([key]) => !key.startsWith("GIT_")));
|
|
106
106
|
const run = (status, rule, config) => {
|
|
107
107
|
ownDirectories();
|
|
108
|
-
const result =
|
|
108
|
+
const { result, diagnostics } = runMemoryToolProbe(executable, ["--root", scratch, "--files", logical, "--format", "json", ...(config ? ["--config", config] : [])], { cwd: scratch, env: { ...probeEnv, GIT_OPTIONAL_LOCKS: "0" } });
|
|
109
109
|
if (result.error || result.signal || result.status !== status)
|
|
110
|
-
refuse(config ? "temporary_configuration_not_supported" : "lint_probe_execution_failed");
|
|
110
|
+
refuse(config ? "temporary_configuration_not_supported" : "lint_probe_execution_failed", { ...diagnostics, phase: "lint_probe", expected_status: status });
|
|
111
111
|
ownDirectories();
|
|
112
112
|
const actual = fs.lstatSync(file);
|
|
113
113
|
if (!actual.isFile() || actual.nlink !== 1 || !same(actual, fileOwner)
|
|
@@ -7,7 +7,7 @@ import { hybridDigest } from "./hybrid-contracts.js";
|
|
|
7
7
|
import { resolveHarnessCommand, harnessRuntimeArguments } from "./harness-config.js";
|
|
8
8
|
import { runHarnessAdapter, adapterSessionId } from "./harness-adapter.js";
|
|
9
9
|
import { renderCapacityInstructions } from "../harness-runtime/lib/delegation-instructions.mjs";
|
|
10
|
-
import { normalizeNativeChildren } from "../harness-runtime/lib/native-children.mjs";
|
|
10
|
+
import { normalizeNativeChildren, nativeShutdownConfirmed } from "../harness-runtime/lib/native-children.mjs";
|
|
11
11
|
import { readBoundedReleaseFile } from "./release-source.js";
|
|
12
12
|
import { acquireInstallationLock } from "./installation-lock.js";
|
|
13
13
|
import { inspectDaemonOperation } from "../harness-runtime/lib/daemon-operations.mjs";
|
|
@@ -115,7 +115,7 @@ export async function produceMemoryInitNativeCapacity(input) {
|
|
|
115
115
|
|| hybridDigest(proof.children) !== hybridDigest(children) || proof.capacity !== children.length
|
|
116
116
|
|| children.length < input.maximum || children.length > input.maximum || new Set(children.map(child => child.session_id)).size !== children.length
|
|
117
117
|
|| children.some(child => child.session_id === rootSessionId || child.parent_session_id !== rootSessionId || child.provenance === "parent_mismatch")
|
|
118
|
-
|| cleanup?.stopped !== true || cleanup.settled !== true || cleanup.shutdown_contract !== "dd-flow/daemon-shutdown@
|
|
118
|
+
|| cleanup?.stopped !== true || cleanup.settled !== true || !nativeShutdownConfirmed(cleanup) || cleanup.shutdown_contract !== "dd-flow/daemon-shutdown@2"
|
|
119
119
|
|| typeof start?.daemon_id !== "string" || cleanup.daemon_id !== start.daemon_id
|
|
120
120
|
|| hybridDigest(cleanup) !== hybridDigest(read(path.join(root, "daemon-stop.json"))))
|
|
121
121
|
refuse("probe_not_qualified", "partial");
|
|
@@ -280,8 +280,8 @@ export async function produceMemoryInitNativeCapacity(input) {
|
|
|
280
280
|
}
|
|
281
281
|
}
|
|
282
282
|
const children = rootSessionId && inspected ? normalizeNativeChildren(inspected, rootSessionId) : [];
|
|
283
|
-
const qualified = !failure && cleanup?.stopped === true && cleanup.settled === true
|
|
284
|
-
&& cleanup.shutdown_contract === "dd-flow/daemon-shutdown@
|
|
283
|
+
const qualified = !failure && cleanup?.stopped === true && cleanup.settled === true && nativeShutdownConfirmed(cleanup)
|
|
284
|
+
&& cleanup.shutdown_contract === "dd-flow/daemon-shutdown@2" && typeof cleanup.daemon_id === "string"
|
|
285
285
|
&& children.length === input.maximum && children.length <= 15
|
|
286
286
|
&& children.every(child => child.parent_session_id === rootSessionId && child.session_id !== rootSessionId && child.provenance !== "parent_mismatch");
|
|
287
287
|
const proof = { schema_id: "dd-flow/memory-native-capacity@1", input: identity, root_session_id: rootSessionId,
|