@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.
Files changed (101) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/dist/build-info.json +7 -7
  3. package/dist/cli/command-inputs.js +6 -2
  4. package/dist/cli/command-metadata.js +18 -2
  5. package/dist/cli/diagnostic-invocation.js +46 -0
  6. package/dist/cli/diagnostic.js +120 -0
  7. package/dist/cli/help.js +35 -0
  8. package/dist/cli/memory-flow-admission.js +4 -2
  9. package/dist/cli/memory-init.js +5 -2
  10. package/dist/cli/run-cli.js +16 -15
  11. package/dist/cli.js +6 -0
  12. package/dist/domain/diagnostic-aspects.js +103 -0
  13. package/dist/domain/diagnostic-flows.js +8 -0
  14. package/dist/harness-runtime/lib/agy-native-inspection.mjs +216 -0
  15. package/dist/harness-runtime/lib/agy-native_pb.js +117 -0
  16. package/dist/harness-runtime/lib/daemon-operations.d.mts +1 -0
  17. package/dist/harness-runtime/lib/daemon-operations.mjs +84 -10
  18. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +306 -34
  19. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +27 -13
  20. package/dist/harness-runtime/lib/dd-codex.mjs +4 -0
  21. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +4 -3
  22. package/dist/harness-runtime/lib/dd-droid.mjs +70 -28
  23. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +3 -3
  24. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +66 -20
  25. package/dist/harness-runtime/lib/dd-opencode.mjs +30 -3
  26. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +7 -6
  27. package/dist/harness-runtime/lib/driver-recovery.mjs +9 -8
  28. package/dist/harness-runtime/lib/managed-daemon.d.mts +1 -1
  29. package/dist/harness-runtime/lib/managed-daemon.mjs +11 -3
  30. package/dist/harness-runtime/lib/native-children.d.mts +8 -1
  31. package/dist/harness-runtime/lib/native-children.mjs +114 -8
  32. package/dist/harness-runtime/lib/native-request-proof.mjs +1 -1
  33. package/dist/harness-runtime/lib/observation-clock.d.mts +2 -2
  34. package/dist/harness-runtime/lib/observation-clock.mjs +7 -4
  35. package/dist/harness-runtime/lib/provider-overload-policy.d.mts +1 -1
  36. package/dist/harness-runtime/lib/provider-overload-policy.mjs +1 -1
  37. package/dist/harness-runtime/lib/tool-observations.mjs +1 -1
  38. package/dist/schemas/diagnostic-stage-result.schema.json +354 -0
  39. package/dist/schemas/diagnostic-work-result.schema.json +296 -0
  40. package/dist/schemas/execution-bundle.schema.json +7 -1
  41. package/dist/schemas/project-flow-pack-manifest.schema.json +1 -1
  42. package/dist/services/check-environment.js +21 -0
  43. package/dist/services/check-runner-evidence.js +267 -15
  44. package/dist/services/code-checks.js +23 -15
  45. package/dist/services/controller-execution.js +10 -4
  46. package/dist/services/controller-fanout.js +13 -6
  47. package/dist/services/controller-memory-worker.js +3 -2
  48. package/dist/services/dashboard.js +2 -2
  49. package/dist/services/descriptor-run-settlement.js +5 -3
  50. package/dist/services/descriptor-run-start.js +22 -3
  51. package/dist/services/descriptor-stage-coordinator.js +37 -4
  52. package/dist/services/descriptor-stage-settlement.js +2 -1
  53. package/dist/services/diagnostic-evidence.js +73 -0
  54. package/dist/services/diagnostic-report-publication.js +72 -0
  55. package/dist/services/diagnostic-report.js +67 -0
  56. package/dist/services/diagnostic-run-bundle.js +74 -0
  57. package/dist/services/diagnostic-snapshot.js +159 -0
  58. package/dist/services/diagnostic-stage-handlers.js +108 -0
  59. package/dist/services/diagnostic-stage-results.js +132 -0
  60. package/dist/services/diagnostic-start.js +84 -0
  61. package/dist/services/diagnostic-work-batch.js +34 -0
  62. package/dist/services/diagnostic-work-collection.js +97 -0
  63. package/dist/services/diagnostic-work-registration.js +101 -0
  64. package/dist/services/diagnostic-work-results.js +93 -0
  65. package/dist/services/eval-snapshots.js +85 -29
  66. package/dist/services/execution-bundles.js +19 -7
  67. package/dist/services/flow-catalog.js +6 -1
  68. package/dist/services/frozen-engine.js +37 -2
  69. package/dist/services/hybrid-contracts.js +2 -0
  70. package/dist/services/installed-flow-source.js +25 -3
  71. package/dist/services/lifecycle-invocations.js +147 -34
  72. package/dist/services/memory-init-contexts.js +16 -0
  73. package/dist/services/memory-init-controller-start.js +12 -4
  74. package/dist/services/memory-init-lint-capability.js +5 -5
  75. package/dist/services/memory-init-native-capacity.js +4 -4
  76. package/dist/services/memory-init-preflight.js +10 -16
  77. package/dist/services/memory-init-worktrunk-capability.js +3 -7
  78. package/dist/services/memory-layer.js +4 -3
  79. package/dist/services/memory-tool-probe.js +25 -0
  80. package/dist/services/memory-verification-checks.js +15 -4
  81. package/dist/services/native-daemon-history.js +2 -1
  82. package/dist/services/provider-capacity.js +43 -38
  83. package/dist/services/release-cache.js +8 -0
  84. package/dist/services/run-control.js +2 -1
  85. package/dist/services/run-controller-adapter.js +2 -1
  86. package/dist/services/run-controller-process.js +2 -1
  87. package/dist/services/run-controller-recovery.js +2 -1
  88. package/dist/services/run-controller.js +85 -15
  89. package/dist/services/run-fork.js +1 -1
  90. package/dist/services/runs.js +6 -1
  91. package/dist/services/runtime-scope-resume.js +12 -6
  92. package/dist/services/runtime-scope-stop.js +3 -2
  93. package/dist/services/runtime-service.js +2 -1
  94. package/dist/services/stage-handlers.js +16 -3
  95. package/dist/services/stage-pause.js +4 -1
  96. package/dist/services/version-status.js +1 -1
  97. package/dist/services/vnext-code.js +2 -2
  98. package/dist/services/vnext-fanout.js +31 -1
  99. package/dist/services/work-registry.js +37 -6
  100. package/dist/services/workspace-bootstrap.js +2 -1
  101. 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) || !["mb-init", "mb-upgrade"].includes(flowId))
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
- || !["mb-init/flow-descriptor.json", "mb-upgrade/flow-descriptor.json"].includes(value.descriptor.path) || typeof value.descriptor.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(value.descriptor.sha256)
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.startsWith("mb-upgrade/") ? "mb-upgrade" : "mb-init";
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 error = (row.outcome_json ? JSON.parse(row.outcome_json) : null)?.error;
383
- if (typeof error?.code === "string" && !isCorrectableLifecycleRejection(error.code, error.details) && !isRegisteredRepair(context, error.details?.continuation, { projectId: project.id, runId: scope.runId }))
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 retainedSuccessor = error && typeof error.details.retry_command === "string"
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
- && context.db.get(`SELECT 1 FROM lifecycle_invocations a JOIN hook_events h ON h.event_key = a.event_key
496
- JOIN projects p ON p.id = h.project_id WHERE h.id = ? AND h.project_id = ? AND a.status = 'settled'
497
- AND json_extract(a.scope_json,'$.projectRoot') = p.root AND json_extract(a.scope_json,'$.runId') = ?
498
- AND json_extract(a.scope_json,'$.generation') = ? AND json_extract(a.outcome_json,'$.error.details.retry_command') = ?
499
- AND json_extract(a.outcome_json,'$.error.details.rejection_signature') = ?`, [anchor.hook_event_id, anchor.project_id, anchor.run_id, anchor.generation, error.details.retry_command, error.details.rejection_signature]);
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 latest = context.db.get("SELECT * FROM lifecycle_invocations WHERE scope_json = ? AND fingerprint = ? ORDER BY rowid DESC LIMIT 1", [row.scope_json, row.fingerprint]);
653
- return latest?.status === "issued" && (latest.deadline === null || latest.deadline > Date.now()) ? [retry] : [];
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
- const details = { ...error.details };
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
- throw new AppError("invocation_settlement_conflict", "Attempt is no longer executing", 1, { invocation_id: id });
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 (isRetryableLifecycleRejection(error.code, error.details) || storageRetry) {
804
- const failures = ["work_checks_failed", "code_gate_failed", "code_review_gate_failed", "merge_gate_failed"].includes(error.code) ? error.details.failures : undefined;
805
- const retry = retryLifecycleInvocationCommand(context, id, failures?.[0]?.id);
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
- settleLifecycleInvocation(context, id, { error: { ...error, details } });
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 { parsed } = prepareLifecycleIssuance(command, scope);
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
- context.db.run("INSERT INTO lifecycle_invocations (id, scope_json, command, fingerprint, status, created_at, updated_at) VALUES (?, ?, ?, ?, 'issued', ?, ?)", [id, row.scope_json, prepared, row.fingerprint, context.now(), context.now()]);
974
- return { id, command: prepared };
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
- currentAdmissions.get(context.db)?.delete(id);
2026
- const encoded = JSON.stringify(outcome);
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
- if (row.status !== "settled" || row.outcome_json !== encoded)
2033
- throw new AppError("invocation_settlement_conflict", "Attempt cannot be settled with a different outcome", 1, { invocation_id: id });
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 === "mb-init" ? startMemoryInitRun(context, scope) : startMemoryUpgradeRun(context, scope);
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 === "mb-init" ? prepareMemoryInitContextsFromStartup(context, bundle, run.run_root)
52
- : prepareMemoryUpgradeContextsFromStartup(context, bundle, run.run_root);
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
- function refuse(reason) {
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" });
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 = spawnSync(executable, ["--root", scratch, "--files", logical, "--format", "json", ...(config ? ["--config", config] : [])], { cwd: scratch, env: { ...probeEnv, GIT_OPTIONAL_LOCKS: "0" }, shell: false, windowsHide: true, timeout: 5000, maxBuffer: 256 * 1024 });
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@1"
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@1" && typeof cleanup.daemon_id === "string"
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,