@deksden-com/dd-flow-cli 0.9.0-beta.133 → 0.9.0-beta.134

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 CHANGED
@@ -1,5 +1,26 @@
1
1
  # @deksden-com/dd-flow-cli
2
2
 
3
+ ## 0.9.0-beta.134
4
+
5
+ ### Patch Changes
6
+
7
+ - 7dae2f0: Freeze explicitly accepted RUN operational decisions and deliver the same verified policy/context to Stage and Work. Preserve their authority across recovery, require new acceptance for imported experiments, and retain native aborted-check reasons.
8
+ - Deliver plan 082 Work authority, native failure evidence and Judge diagnostic repairs; admit the exact CP226 ZCode bridge after root, concurrent-child, same-child continuation and clean shutdown probes.
9
+ - 46b7dbc: Persist stage usage through the shared completion service, including vNext SPECIFY and PROTOCOLIZE. Completed stage records now retain scoped token measurements for console readers; missing measurements remain unavailable.
10
+ - 430a7df: Prepare writable parent directories before handing agents HITL, Work result and CODE blocker input paths. Preserve existing payloads and reject unavailable destinations without publishing unusable instructions.
11
+ - 539de25: Capture bounded observation journals without requiring global runtime silence.
12
+ Retain RUN-scoped productive authority, immutable payload checksums and recovery
13
+ materials. Finalize stop-after captures as candidates after native settlement.
14
+ - 9ca6bb3: Use scoped native progress and inactivity windows instead of productive-work
15
+ lifetime limits. Preserve observation uncertainty and exact operation fences
16
+ across quiet probes and observer replacement; bound utility output and cleanup.
17
+ Separate native-work and control waits across all six bundled adapters, keep
18
+ serving services exempt from output silence, and stop MERGE on confirmed lease
19
+ loss. New runtime contracts are operation-progress@1, operation-errors@2 and
20
+ native-operation-wait@1.
21
+ - 0a8923a: Separate existing controller lease renewal from new launch admission so terminal RUNs can finish boundary capture and native settlement. Make terminal stop retain durable cleanup evidence without reopening productive recovery, and expose exact-owner finalization diagnostics. Full-cycle regressions now check sealed final capture and physical retirement before fixture cleanup.
22
+ - 6b2c491: Preserve unavailable ZCode cache and reasoning counters as null instead of inventing measured zeros. Mixed histories keep optional totals unknown while retaining confirmed input, output, and total tokens.
23
+
3
24
  ## 0.9.0-beta.133
4
25
 
5
26
  ### Patch Changes
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "cli_package": "@deksden-com/dd-flow-cli",
3
- "cli_version": "0.9.0-beta.133",
4
- "cli_commit": "43d88ec57e1b100fa29f31be925d6abfc976e7a2",
5
- "built_at": "2026-10-12T17:50:10.957Z",
3
+ "cli_version": "0.9.0-beta.134",
4
+ "cli_commit": "b243f4dac38629b7f0398b3e0b1613904f4aebff",
5
+ "built_at": "2026-10-13T11:24:52.803Z",
6
6
  "built_with_canon": {
7
7
  "version": "4.1.2",
8
8
  "commit": "2ba0b9ceedf8e4544ad9566c240ef37ea5e8d099",
@@ -14,6 +14,7 @@ import { checkObservedProfile, observeModel, observedFields } from "./model-obse
14
14
  import { commandText } from "./process-json.mjs";
15
15
  import { readNativeLines, streamNativeDiagnostics, nativeObservationError } from "./native-streams.mjs";
16
16
  import { operationProgressReporter } from "./operation-progress.mjs";
17
+ import { safeNativeFailure } from "./native-children.mjs";
17
18
 
18
19
  import { readZcodeUsage } from "./zcode-usage.mjs";
19
20
  const ZCODE_HARNESS_CONTRACT = "dd-zcode-harness@1";
@@ -569,7 +570,25 @@ export async function readZcodeSubagents(bridge, sessionId, timeoutMs = 30_000,
569
570
  cursor = page.ended.nextCursor;
570
571
  if (!cursor) {
571
572
  if (ended.size !== first.ended.total) fail("ZCode child directory is incomplete", "native_topology_incomplete");
572
- return { ...first, ended: { total: first.ended.total, items: [...ended.values()] } };
573
+ const items = [];
574
+ for (const child of ended.values()) {
575
+ if (child.status !== "failed" || !Number.isSafeInteger(child.startedAt) || child.startedAt <= 0
576
+ || !Number.isSafeInteger(child.endedAt) || child.endedAt < child.startedAt || child.endedAt > 8.64e15) { items.push(child); continue; }
577
+ // Closed children must never be loaded merely to explain their failure.
578
+ let terminal;
579
+ try { terminal = await bridge.request("zcode/session/retainedTerminal", { sessionId: child.childSessionId, startedAt: child.startedAt, endedAt: child.endedAt }, timeoutMs); }
580
+ catch (error) { items.push({ ...child, terminal_metadata_unavailable: error.code ?? "terminal_metadata_unavailable" }); continue; }
581
+ if (terminal?.available !== true) { items.push({ ...child, terminal_metadata_unavailable: "terminal_metadata_unavailable" }); continue; }
582
+ const observedAt = Date.parse(terminal.terminal_observed_at);
583
+ if (terminal.sessionId !== child.childSessionId || typeof terminal.terminal_turn_id !== "string" || !terminal.terminal_turn_id
584
+ || terminal.startedAt !== child.startedAt || terminal.endedAt !== child.endedAt
585
+ || !Number.isFinite(observedAt) || observedAt < child.startedAt || observedAt > child.endedAt) fail("ZCode terminal failure metadata has foreign identity", "native_child_turn_conflict");
586
+ const providerError = safeNativeFailure(terminal.provider_error);
587
+ items.push({ ...child, terminal_turn_id: terminal.terminal_turn_id, terminal_observed_at: terminal.terminal_observed_at,
588
+ ...(providerError ? { provider_error: providerError } : {}) });
589
+ clock.sample(child.childSessionId);
590
+ }
591
+ return { ...first, ended: { total: first.ended.total, items } };
573
592
  }
574
593
  if (typeof cursor !== "string" || cursors.has(cursor) || !Number.isSafeInteger(first.revision)) fail("ZCode child pagination has no advancing, versioned cursor", "native_topology_incomplete");
575
594
  cursors.add(cursor);
@@ -923,6 +942,7 @@ export function zcodeLifecycleQualification(sha256, bridgeCommit, harnessContrac
923
942
  ["b9e0f5106dddb439b76186278bd9be5f45dbf3ef", "dd-zcode-harness@2"],
924
943
  ["636b14181d5c9a95476055079836fd6b58169f58", "dd-zcode-harness@2"],
925
944
  ["4621fabd3f517ecdd349dfebebae9077c01d2f2b", "dd-zcode-harness@2"],
945
+ ["9c5ca6cc81c4eb5065e43f8b7ab6b7cc5ab7ae47", "dd-zcode-harness@2"],
926
946
  ]);
927
947
  const qualified = qualifiedBridgeCommits.get(bridgeCommit) === harnessContract;
928
948
  return { status: qualified ? "qualified" : "unqualified", mechanism: "cli-invocation@1", sha256,
@@ -1,4 +1,5 @@
1
1
  export const NATIVE_CHILD_CONTRACT_VERSION: "native-children@1";
2
+ export function safeNativeFailure(value: unknown): Record<string, unknown> | undefined;
2
3
  export interface NativeChild {
3
4
  session_id: string;
4
5
  parent_session_id: string;
@@ -3,6 +3,22 @@ import { errorRecord } from "./operation-errors.mjs";
3
3
 
4
4
  function nativeFailure(code, message, exitCode, details) { return Object.assign(new Error(message), { code, exitCode, details }); }
5
5
 
6
+ /** Retain typed failure facts, never provider headers, request bodies or free-form messages. */
7
+ export function safeNativeFailure(value, depth = 0) {
8
+ if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
9
+ const result = {};
10
+ for (const key of ["type", "code", "source", "reason", "errorPhase", "exceptionKind", "providerId", "modelId", "providerKind", "transport", "retryable", "willRetry", "statusCode", "resets_at", "retryAfter", "retry_after", "retryAfterMs", "resetAt"]) {
11
+ const scalar = value[key];
12
+ if (typeof scalar === "boolean" || typeof scalar === "number" && Number.isFinite(scalar)
13
+ || typeof scalar === "string" && scalar.length <= 256 && /^[\w.:/ -]+$/.test(scalar)) result[key] = scalar;
14
+ }
15
+ if (typeof value.codexErrorInfo === "string" && /^[\w-]{1,128}$/.test(value.codexErrorInfo)) result.codexErrorInfo = value.codexErrorInfo;
16
+ else if (value.codexErrorInfo && typeof value.codexErrorInfo.code === "string" && /^[\w-]{1,128}$/.test(value.codexErrorInfo.code)) result.codexErrorInfo = { code: value.codexErrorInfo.code };
17
+ const attribution = depth === 0 && value.attribution && safeNativeFailure(value.attribution, 1);
18
+ if (attribution && Object.keys(attribution).length) result.attribution = attribution;
19
+ return Object.keys(result).length ? result : undefined;
20
+ }
21
+
6
22
 
7
23
  /** Normalize provider-observed topology only; never parse assistant prose. */
8
24
  export function normalizeNativeChildren(receipt, rootSessionId) {
@@ -1,11 +1,13 @@
1
1
  import { AppError, errorRecord } from "../shared/errors.js";
2
- import { normalizeNativeChildren } from "../harness-runtime/lib/native-children.mjs";
2
+ import { normalizeNativeChildren, safeNativeFailure } from "../harness-runtime/lib/native-children.mjs";
3
3
  import { getVnextFanoutStatus, observeVnextNativeChildren, reconcileVnextFanout, subagentCapacityKey } from "./vnext-fanout.js";
4
4
  import { getFlowRunVariables } from "./runs.js";
5
5
  import { dispatchVnextPlanReview } from "./vnext-plan-review.js";
6
6
  import { resolveExecutionPolicy } from "./execution-policy.js";
7
7
  import { lifecycleCommandCompletionInstruction, renderDelegationInstructions, stageLifecycleInstruction } from "../harness-runtime/lib/delegation-instructions.mjs";
8
8
  import { assertWorkLaunchReady } from "./work-registry.js";
9
+ import { requireProjectByRoot } from "./projects.js";
10
+ import { stageWorkIds } from "./stage-work-graph.js";
9
11
  function semanticCompletionPrompt(stage, completed) {
10
12
  const task = stage === "plan-review"
11
13
  ? "Classify the reviewer findings, apply every accepted correction in the current PLAN-REVIEW Work, and write the authoritative review decision."
@@ -74,8 +76,23 @@ export async function nextControllerFanout(context, input) {
74
76
  const prompt = (action, text) => ({ kind: "prompt", action, prompt: action === "dispatch_work" ? text : `${text}\n\n${lifecycleCommandCompletionInstruction}`, fingerprint: JSON.stringify({ stage: input.stage, parent: fanout.parent_work_id, state: fanout.state, graph: fanout.graph_revision }) });
75
77
  if (fanout.state === "coordinator_required")
76
78
  return prompt("materialize_graph", `Continue the current ${input.stage} Stage using its authoritative packet. Materialize only its required Work graph with real dependencies. Do not launch children, finish the Stage, or start a successor. Return after registering the graph; the runtime will dispatch its ready Work.`);
77
- if (fanout.works.failed || fanout.works.cancelled)
78
- return prompt("repair_or_block", `Some ${input.stage} child Work failed or was cancelled. Inspect its recorded receipts. Choose a legal scoped repair/retry or block the Stage with the concrete reason. Do not relaunch settled siblings or infer success from a child that omitted work finish.`);
79
+ if (fanout.works.failed || fanout.works.cancelled) {
80
+ const projectId = requireProjectByRoot(context, input.projectRoot).id;
81
+ const members = stageWorkIds(context, projectId, input.runId, input.stage);
82
+ const failures = context.db.all("SELECT work_id,status,result FROM works WHERE project_id = ? AND run_id = ? AND parent_work_id = ? AND status IN ('failed','cancelled')", [projectId, input.runId, fanout.parent_work_id]).filter(work => members.has(work.work_id)).map(work => {
83
+ let receipt = {};
84
+ try {
85
+ const value = work.result ? JSON.parse(work.result) : null;
86
+ if (value && typeof value === "object" && !Array.isArray(value))
87
+ receipt = value;
88
+ }
89
+ catch { /* Missing legacy metadata is unknown. */ }
90
+ const providerError = safeNativeFailure(receipt.provider_error);
91
+ return { work_id: work.work_id, status: work.status, ...(providerError ? { provider_error: providerError } : {}),
92
+ ...Object.fromEntries(["terminal_turn_id", "terminal_observed_at"].filter(key => typeof receipt[key] === "string" && receipt[key].length <= 256).map(key => [key, receipt[key]])) };
93
+ });
94
+ throw new AppError("fanout_child_failed", "A child Work failed or was cancelled; no retry authority has been issued", 1, { stage: input.stage, run_id: input.runId, parent_work_id: fanout.parent_work_id, failed: fanout.works.failed, cancelled: fanout.works.cancelled, failures, effect: "unknown" });
95
+ }
79
96
  if (fanout.works.ready.length) {
80
97
  const choices = fanout.works.ready.map(work => {
81
98
  const payload = context.db.get("SELECT payload_json FROM works WHERE work_id = ?", [work.work_id])?.payload_json;
@@ -18,6 +18,11 @@ import { registerProject, requireProjectByRoot } from "./projects.js";
18
18
  import { activeFlowSessionsForProject, recordFlowSessionObservation } from "./sessions.js";
19
19
  const defaultProfileName = "default";
20
20
  const maxSanitizedSummaryLength = 1600;
21
+ function lifecycleObservationResult(receipt) {
22
+ return { observed: receipt.disposition === "admitted", duplicate: receipt.duplicate,
23
+ event_key: receipt.authoritativeEventKey, current_event_key: receipt.currentEventKey,
24
+ authoritative_event_key: receipt.authoritativeEventKey, receipt_disposition: receipt.disposition };
25
+ }
21
26
  export const codexHomeSharedEntries = [
22
27
  "auth.json",
23
28
  "auth.bk",
@@ -410,6 +415,7 @@ export function handleCodexHook(context, input) {
410
415
  throw new AppError("codex_identity_missing", "Managed Codex lifecycle hook has no complete native invocation identity", 1, { invocation_id: invocationId });
411
416
  const executionCwd = assignedLifecycleCwd(context, project.root, lifecycle, providerSessionId, command, stringValue(payload.cwd), { daemonId, rootSessionId: rootProviderSessionId, sessionId: providerSessionId, parentSessionId: parentProviderSessionId, toolCallId }, "codex-desktop");
412
417
  const receipt = observeLifecycleInvocation(context, {
418
+ currentEventKey: eventKey,
413
419
  ...(invocationId ? { id: invocationId } : {}),
414
420
  command,
415
421
  harness: "codex-desktop",
@@ -421,12 +427,11 @@ export function handleCodexHook(context, input) {
421
427
  const stored = context.db.get("SELECT protocol_id FROM hook_events WHERE project_id = ? AND event_key = ?", [project.id, receipt.eventKey]);
422
428
  const admittedInvocationId = receipt.invocationId ?? invocationId;
423
429
  if (admittedInvocationId)
424
- return { ...common, protocol_id: stored?.protocol_id ?? null, observed: !receipt.duplicate, duplicate: receipt.duplicate, event_key: receipt.eventKey, invocation_id: admittedInvocationId };
430
+ return { ...common, protocol_id: stored?.protocol_id ?? null, ...lifecycleObservationResult(receipt), invocation_id: admittedInvocationId };
425
431
  return {
426
432
  ...common,
427
433
  protocol_id: stored?.protocol_id ?? null,
428
- observed: !receipt.duplicate,
429
- duplicate: receipt.duplicate,
434
+ ...lifecycleObservationResult(receipt),
430
435
  hookSpecificOutput: {
431
436
  hookEventName: "PreToolUse",
432
437
  permissionDecision: "allow",
@@ -588,6 +593,7 @@ function recordZcodeLifecycle(context, input, invocationId, nativePreTool = fals
588
593
  let persistedResult;
589
594
  const executionCwd = assignedLifecycleCwd(context, expectedRoot, lifecycle, providerSessionId, command, observedCwd ?? undefined, { daemonId, rootSessionId: rootProviderSessionId, sessionId: providerSessionId, parentSessionId: parentProviderSessionId ?? null, toolCallId: toolUseId }, "zcode-acp");
590
595
  const receipt = observeLifecycleInvocation(context, {
596
+ currentEventKey: eventKey,
591
597
  ...(invocationId ? { id: invocationId } : {}), command,
592
598
  harness: "zcode-acp",
593
599
  projectId: project.id,
@@ -595,7 +601,7 @@ function recordZcodeLifecycle(context, input, invocationId, nativePreTool = fals
595
601
  identity: { daemonId, rootSessionId: rootProviderSessionId, sessionId: providerSessionId, parentSessionId: parentProviderSessionId ?? null, toolCallId: toolUseId },
596
602
  recordReceipt: scope => { persistedResult = persist(scope); return eventKey; }
597
603
  });
598
- if (receipt.duplicate) {
604
+ if (receipt.disposition === "replay") {
599
605
  // Keep first admission immutable. A second transport is independently
600
606
  // retained corroboration, never another claim or an invented ACP cwd.
601
607
  const retained = context.db.get("SELECT event_name, match_key, harness, provider_session_id, parent_session_id, daemon_id FROM hook_events WHERE project_id = ? AND event_key = ?", [project.id, receipt.eventKey]);
@@ -615,7 +621,7 @@ function recordZcodeLifecycle(context, input, invocationId, nativePreTool = fals
615
621
  }
616
622
  const admittedInvocationId = receipt.invocationId ?? invocationId;
617
623
  if (admittedInvocationId)
618
- return { ok: true, observed: !receipt.duplicate, duplicate: receipt.duplicate, event_key: receipt.eventKey, invocation_id: admittedInvocationId };
624
+ return { ok: true, ...lifecycleObservationResult(receipt), invocation_id: admittedInvocationId };
619
625
  // Without an issued managed attempt this is still ordinary hook evidence.
620
626
  // Preserve persist()'s replay result instead of overwriting it with the
621
627
  // admission layer's non-authoritative duplicate flag.
@@ -685,14 +691,13 @@ export function handleGrokEvent(context, input) {
685
691
  let inserted = false;
686
692
  const executionCwd = assignedLifecycleCwd(context, expectedRoot, lifecycle, providerSessionId, command, observedCwd ?? undefined, { daemonId: daemonId ?? "", rootSessionId: rootProviderSessionId, sessionId: providerSessionId, parentSessionId, toolCallId: eventKey }, "grok-acp");
687
693
  const receipt = observeLifecycleInvocation(context, {
694
+ currentEventKey: eventKey,
688
695
  command, harness: "grok-acp", projectId: project.id,
689
696
  ...(executionCwd ? { executionCwd } : {}),
690
697
  identity: { daemonId: daemonId ?? "", rootSessionId: rootProviderSessionId, sessionId: providerSessionId, parentSessionId, toolCallId: eventKey },
691
698
  recordReceipt: scope => { inserted = persist(scope); return eventKey; }
692
699
  });
693
- if (receipt.invocationId)
694
- return { ...result(inserted), invocation_id: receipt.invocationId, hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow", updatedInput: { ...rawInput, command: commandWithHookEvent(command, eventKey) } } };
695
- return { ...result(inserted), hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow", updatedInput: { ...rawInput, command: commandWithHookEvent(command, eventKey) } } };
700
+ return { ...result(inserted), ...lifecycleObservationResult(receipt), ...(receipt.invocationId ? { invocation_id: receipt.invocationId } : {}), hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow", updatedInput: { ...rawInput, command: receipt.eventKey ? commandWithHookEvent(command, receipt.eventKey) : command } } };
696
701
  }
697
702
  /** Convert a controlled OpenCode tool hook into the shared lifecycle receipt. */
698
703
  export function handleOpenCodeEvent(context, input) {
@@ -900,11 +905,12 @@ function handleControlledToolEvent(context, input, event, harness) {
900
905
  if (lifecycle) {
901
906
  let inserted = false;
902
907
  const receipt = observeLifecycleInvocation(context, {
908
+ currentEventKey: baseKey,
903
909
  command, harness, projectId: project.id, ...(!shellRefusal ? { executionCwd: expectedCwd } : {}), identity: { daemonId, rootSessionId: nativeParentId ?? providerSessionId, sessionId: providerSessionId, parentSessionId: nativeParentId ?? null, toolCallId: toolCallId },
904
910
  recordReceipt: scope => { inserted = persist(scope); return baseKey; }
905
911
  });
906
- const admitted = { ...result(inserted), ...(receipt.invocationId ? { invocation_id: receipt.invocationId } : {}) };
907
- return agy || shellRefusal ? admitted : { ...admitted, hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow", updatedInput: { ...rawInput, command: commandWithHookEvent(command, baseKey) } } };
912
+ const admitted = { ...result(inserted), ...lifecycleObservationResult(receipt), ...(receipt.invocationId ? { invocation_id: receipt.invocationId } : {}) };
913
+ return agy || shellRefusal ? admitted : { ...admitted, hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow", updatedInput: { ...rawInput, command: receipt.eventKey ? commandWithHookEvent(command, receipt.eventKey) : command } } };
908
914
  }
909
915
  const inserted = context.db.writeTransaction(() => persist());
910
916
  return agy ? result(inserted) : { ...result(inserted), hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow", updatedInput: { ...rawInput, command: commandWithHookEvent(command, baseKey) } } };
@@ -60,6 +60,11 @@ export function assertAuxiliaryCommandScope(context, command, projectRoot) {
60
60
  }
61
61
  throw new AppError("invocation_scope_unavailable", "Auxiliary command has no live managed daemon binding", 1, { phase: "prepare", effect: "no_effect" });
62
62
  }
63
+ function sameNativeCall(left, right) {
64
+ const a = JSON.parse(left), b = JSON.parse(right);
65
+ // Parent and execution route are immutable facts, not a different call ID.
66
+ return [0, 1, 2, 4].every(index => a[index] === b[index]);
67
+ }
63
68
  // Conclusive result/check rejections, never transport, ownership, registry or
64
69
  // unknown-effect failures. These validators reject before accepting the result.
65
70
  export function isRetryableLifecycleRejection(code, details = {}) {
@@ -567,6 +572,8 @@ export function managedLifecycleCommand(context, command, options = {}) {
567
572
  const scope = JSON.parse(configured);
568
573
  command = canonicalManagedCommand(context, command, scope);
569
574
  const parsed = invocation(command);
575
+ if (options.freshWorkAttempt && !["work_finish", "work_fail"].includes(parsed.operation))
576
+ throw new AppError("invocation_command_mismatch", "Fresh Work packet may only issue Work settlement commands", 1);
570
577
  const suppliedId = commandOption(parsed, "invocation-id");
571
578
  // An invocation id is authority, never decorative text. In particular,
572
579
  // callers that add arguments after rendering must not be able to reuse an
@@ -590,7 +597,7 @@ export function managedLifecycleCommand(context, command, options = {}) {
590
597
  }
591
598
  storage(context);
592
599
  return context.db.writeTransaction(() => {
593
- const prior = context.db.get("SELECT * FROM lifecycle_invocations WHERE scope_json = ? AND fingerprint = ? ORDER BY rowid DESC LIMIT 1", [JSON.stringify(scope), fingerprint(parsed)]);
600
+ 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)]);
594
601
  if (prior && prior.id !== context.env.DD_FLOW_CURRENT_INVOCATION) {
595
602
  const replacesAnswer = options.replaceUnobservedAnswer && parsed.operation === "stage_resume"
596
603
  && (prior.status === "settled" || commandOption(invocation(prior.command), "answer-file") !== commandOption(parsed, "answer-file"));
@@ -817,6 +824,39 @@ export function issueLifecycleInvocation(context, command, scope) {
817
824
  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()]);
818
825
  return { id, command: prepared };
819
826
  }
827
+ /** Prepare outside a write transaction; the Work retry CAS owns issuance. */
828
+ export function prepareWorkStartAuthority(context, command, previousId) {
829
+ const prior = previousId ? load(context, previousId) : null;
830
+ const configured = context.env.DD_FLOW_INVOCATION_SCOPE;
831
+ if (!configured && !prior)
832
+ return null;
833
+ const scope = JSON.parse(configured ?? prior.scope_json);
834
+ if (prior && prior.scope_json !== JSON.stringify(scope))
835
+ throw new AppError("invocation_scope_mismatch", "Work start belongs to another controller scope", 1);
836
+ command = canonicalManagedCommand(context, command, scope);
837
+ const { parsed } = prepareLifecycleIssuance(command, scope);
838
+ if (parsed.operation !== "work_start")
839
+ throw new AppError("invocation_command_mismatch", "Work authority must start its Work", 1);
840
+ if (prior && prior.fingerprint !== fingerprint(parsed))
841
+ throw new AppError("invocation_command_mismatch", "Retained Work start authority belongs to a different Work", 1, { invocation_id: prior.id });
842
+ assertLifecycleInvocationCurrent(context, scope, command);
843
+ return { command, scope };
844
+ }
845
+ /** Caller owns the retry CAS. Preserve settled outcomes; never steal an
846
+ * observed/executing terminal operation from the previous Work attempt. */
847
+ export function retireWorkPacketAuthorities(context, workId) {
848
+ if (!context.db.get("SELECT 1 FROM sqlite_master WHERE name = 'lifecycle_packet_assignments'"))
849
+ return;
850
+ const attempts = context.db.all("SELECT DISTINCT li.* FROM lifecycle_invocations li JOIN lifecycle_packet_assignments pa ON pa.invocation_id=li.id JOIN work_sessions ws ON pa.packet_sha256=json_extract(ws.start_receipt_json,'$.prompt_sha256') WHERE ws.work_id=?", [workId]);
851
+ for (const attempt of attempts) {
852
+ if (!["work_finish", "work_fail"].includes(invocation(attempt.command).operation))
853
+ continue;
854
+ if (["observed", "executing"].includes(attempt.status))
855
+ throw new AppError("work_retry_incomplete", "Previous Work settlement still has native authority in flight", 1, { work_id: workId, invocation_id: attempt.id });
856
+ if (attempt.status === "issued")
857
+ context.db.run("UPDATE lifecycle_invocations SET status='expired',updated_at=? WHERE id=? AND status='issued'", [context.now(), attempt.id]);
858
+ }
859
+ }
820
860
  function commandWithInvocationId(command, id) {
821
861
  const at = shellSuffixOffset(command);
822
862
  const suffix = command.slice(at);
@@ -836,8 +876,10 @@ function assertRenderableInvocation(row) {
836
876
  function renderInvocationCommand(row, command) {
837
877
  assertRenderableInvocation(row);
838
878
  const parsed = invocation(command);
839
- if (commandOption(parsed, "invocation-id"))
840
- throw new AppError("invocation_command_invalid", "Rendered lifecycle command already has an invocation id", 1);
879
+ if (commandOption(parsed, "invocation-id")) {
880
+ checkCommand(row, command);
881
+ return command;
882
+ }
841
883
  const rendered = commandWithInvocationId(command, row.id);
842
884
  checkCommand(row, rendered);
843
885
  return rendered;
@@ -1175,6 +1217,18 @@ export function isControllerManagedInvocation(context, projectRoot, runId) {
1175
1217
  /** recordReceipt is a synchronous SQL-only hook ingress callback. Native
1176
1218
  * adapters must prepare any filesystem or process evidence before calling. */
1177
1219
  export function observeLifecycleInvocation(context, input) {
1220
+ const priorReceipt = input.currentEventKey && input.projectId
1221
+ && context.db.get("SELECT 1 FROM hook_events WHERE project_id = ? AND event_key = ?", [input.projectId, input.currentEventKey]);
1222
+ const result = observeLifecycleInvocationReceipt(context, input);
1223
+ // Hook-only calls have no invocation row, but their immutable receipt still
1224
+ // distinguishes a repeated native delivery from a new observation.
1225
+ if (priorReceipt && result.disposition === "admitted" && result.eventKey === input.currentEventKey) {
1226
+ result.disposition = "replay";
1227
+ result.duplicate = true;
1228
+ }
1229
+ return { ...result, currentEventKey: input.currentEventKey ?? null, authoritativeEventKey: result.eventKey ?? null };
1230
+ }
1231
+ function observeLifecycleInvocationReceipt(context, input) {
1178
1232
  storage(context);
1179
1233
  const projectRoot = input.projectId ? context.db.get("SELECT root FROM projects WHERE id = ?", [input.projectId])?.root : undefined;
1180
1234
  const provenScope = projectRoot && input.harness ? managedNativeInvocationScope(context, projectRoot, input.identity, input.harness) : null;
@@ -1246,7 +1300,7 @@ export function observeLifecycleInvocation(context, input) {
1246
1300
  }
1247
1301
  if (analysis.kind !== "none")
1248
1302
  throw new AppError("lifecycle_shell_syntax_invalid", "This shell form cannot bind exactly one lifecycle execution; use the exact issued lifecycle command without the unsupported shell context", 1, { phase: "prepare", effect: "no_effect", recoverable: true, disposition: "pre_cli_shell_refusal", reason: analysis.kind === "compound" ? analysis.reason : "unsupported_lifecycle_command", event_key: eventKey, ...issuedBarrier });
1249
- return { eventKey, duplicate: false, invocationId: null };
1303
+ return { eventKey, duplicate: false, disposition: "admitted", invocationId: null };
1250
1304
  }
1251
1305
  // Admit the one parsed leaf, never execute or fingerprint the prefix/suffix.
1252
1306
  const leafCommand = analysis.invocation.command;
@@ -1269,7 +1323,7 @@ export function observeLifecycleInvocation(context, input) {
1269
1323
  return false;
1270
1324
  }
1271
1325
  if (row.identity_json && row.identity_json !== deliveredIdentity
1272
- && JSON.stringify(JSON.parse(row.identity_json).slice(0, 5)) === JSON.stringify(JSON.parse(deliveredIdentity).slice(0, 5))) {
1326
+ && sameNativeCall(row.identity_json, deliveredIdentity)) {
1273
1327
  throw new AppError("invocation_identity_mismatch", "The same native tool call changed its bound execution route", 1, { phase: "prepare", effect: "no_effect", recoverable: false });
1274
1328
  }
1275
1329
  if (row.status === "executing" && row.identity_json !== deliveredIdentity)
@@ -1302,7 +1356,7 @@ export function observeLifecycleInvocation(context, input) {
1302
1356
  if (retainedOwner !== ownerSnapshot())
1303
1357
  throw new AppError("invocation_scope_unproven", "Native owner changed before hook-only recovery receipt", 1, { effect: "no_effect" });
1304
1358
  const eventKey = input.recordReceipt();
1305
- return { eventKey, duplicate: false, invocationId: null };
1359
+ return { eventKey, duplicate: false, disposition: "admitted", invocationId: null };
1306
1360
  });
1307
1361
  }
1308
1362
  const resolved = input.id === initial.id ? {} : { invocationId: initial.id };
@@ -1313,7 +1367,7 @@ export function observeLifecycleInvocation(context, input) {
1313
1367
  // reattach. Do not manufacture a new receipt or reject that read as a second
1314
1368
  // executor; the CLI returns the immutable stored outcome without dispatch.
1315
1369
  if (initial.status === "settled")
1316
- return { eventKey: initial.event_key, duplicate: true, ...resolved };
1370
+ return { eventKey: initial.event_key, duplicate: true, disposition: initial.identity_json === deliveredIdentity ? "replay" : "settled_read", ...resolved };
1317
1371
  const project = requireProjectByRoot(context, scope.projectRoot);
1318
1372
  const identity = input.identity;
1319
1373
  const identityJson = deliveredIdentity;
@@ -1324,16 +1378,16 @@ export function observeLifecycleInvocation(context, input) {
1324
1378
  if (row.scope_json !== initial.scope_json)
1325
1379
  throw new AppError("invocation_scope_mismatch", "Lifecycle scope changed before native receipt", 1, { effect: "no_effect" });
1326
1380
  if (row.status === "settled")
1327
- return { eventKey: row.event_key, duplicate: true, ...resolved };
1381
+ return { eventKey: row.event_key, duplicate: true, disposition: row.identity_json === identityJson ? "replay" : "settled_read", ...resolved };
1328
1382
  // Observation records native facts only. CLI owns deadline, argument and
1329
1383
  // RUN-state validation, including decisions about retrying an invocation.
1330
1384
  if (row.status === "expired" || (row.deadline !== null && row.deadline <= Date.now() && !row.event_key)) {
1331
- return { eventKey: row.event_key, duplicate: true, ...resolved };
1385
+ return { eventKey: row.event_key, duplicate: true, disposition: "expired", ...resolved };
1332
1386
  }
1333
1387
  if (row.identity_json === identityJson) {
1334
- return { eventKey: row.event_key, duplicate: true, ...resolved };
1388
+ return { eventKey: row.event_key, duplicate: true, disposition: "replay", ...resolved };
1335
1389
  }
1336
- if (row.identity_json && JSON.stringify(JSON.parse(row.identity_json).slice(0, 5)) === JSON.stringify(JSON.parse(identityJson).slice(0, 5))) {
1390
+ if (row.identity_json && sameNativeCall(row.identity_json, identityJson)) {
1337
1391
  throw new AppError("invocation_identity_mismatch", "The same native tool call changed its bound execution route", 1, { phase: "prepare", effect: "no_effect", recoverable: false });
1338
1392
  }
1339
1393
  const eventKey = input.recordReceipt({ projectRoot: scope.projectRoot, runId: scope.runId, command: initial.command });
@@ -1353,7 +1407,7 @@ export function observeLifecycleInvocation(context, input) {
1353
1407
  const updated = context.db.run("UPDATE lifecycle_invocations SET identity_json = ?, event_key = ?, status = 'observed', updated_at = ? WHERE id = ? AND status IN ('issued', 'observed')", [identityJson, eventKey, context.now(), initial.id]);
1354
1408
  // Once execution has been claimed, later PreToolUse observations are
1355
1409
  // retained as hook evidence but cannot replace the executor's receipt.
1356
- return { eventKey: updated.changes === 1 ? eventKey : row.event_key, duplicate: updated.changes !== 1, ...resolved };
1410
+ return { eventKey: updated.changes === 1 ? eventKey : row.event_key, duplicate: updated.changes !== 1, disposition: updated.changes === 1 ? "admitted" : "in_flight", ...resolved };
1357
1411
  });
1358
1412
  }
1359
1413
  /** Correlate exact native argv for diagnostics, independently of assignment
@@ -11,6 +11,7 @@ import { reconcileUnfinishedChecks } from "./code-checks.js";
11
11
  import { vnextStageDirectory } from "../domain/stage-catalog.js";
12
12
  import { stageWorkIds, stageGraphScope } from "./stage-work-graph.js";
13
13
  import { eligibleCodexChildOverload } from "./codex-capacity.js";
14
+ import { safeNativeFailure } from "../harness-runtime/lib/native-children.mjs";
14
15
  export const subagentCapacityKey = "runtime.subagent.available_slots";
15
16
  /** External concurrency is policy, never evidence of native child qualification. */
16
17
  export function getStageFanoutCapacity(context, input) {
@@ -175,7 +176,11 @@ export async function reconcileVnextFanout(context, input) {
175
176
  issues.push({ code: "execution_ended_without_work_result", work_id: link.work_id, session_id: child.session_id });
176
177
  continue;
177
178
  }
178
- const reason = JSON.stringify({ source: "native_child_observation", harness_id: value.harness_id, session_id: child.session_id, work_session_id: link.id, outcome: child.status });
179
+ const providerError = safeNativeFailure(child.provider_error);
180
+ const reason = JSON.stringify({ source: "native_child_observation", harness_id: value.harness_id, session_id: child.session_id, work_session_id: link.id, outcome: child.status,
181
+ ...(typeof child.terminal_turn_id === "string" ? { terminal_turn_id: child.terminal_turn_id } : {}),
182
+ ...(typeof child.terminal_observed_at === "string" ? { terminal_observed_at: child.terminal_observed_at } : {}),
183
+ ...(providerError ? { provider_error: providerError } : {}) });
179
184
  const result = await (child.status === "failed" ? failWork : cancelWork)(context, link.work_id, reason);
180
185
  reconciled.push({ work_id: link.work_id, work_session_id: link.id, outcome: child.status, result });
181
186
  }
@@ -1,4 +1,4 @@
1
- import { bindLifecyclePacketAssignments, managedLifecycleCommand } from "./lifecycle-invocations.js";
1
+ import { bindLifecyclePacketAssignments, managedLifecycleCommand, prepareWorkStartAuthority, issueLifecycleInvocation, retireWorkPacketAuthorities } from "./lifecycle-invocations.js";
2
2
  import { bindStageWork, currentStageWorkIds, stageWorkIds } from "./stage-work-graph.js";
3
3
  import { runtimeCommand } from "./runtime-command.js";
4
4
  import crypto from "node:crypto";
@@ -449,7 +449,38 @@ export function shortWorkId(id) { return shortEntityReference(id); }
449
449
  * PATH is not inherited; using the shared lifecycle command keeps them on the
450
450
  * same captured runtime as their parent stage.
451
451
  */
452
- export function workStartCommand(context, work) { const run = requireRun(context, work.project_id, work.run_id); const recovery = recoveryGuard(context, work.project_id, work.run_id); return managedLifecycleCommand(context, runtimeCommand(context, ["work", "start", shortWorkId(work.work_id), "--project-root", run.project_root, ...(recovery ? ["--recovery-id", recovery.recovery_id] : []), "--json"])); }
452
+ function rawWorkStartCommand(context, work) {
453
+ const run = requireRun(context, work.project_id, work.run_id), recovery = recoveryGuard(context, work.project_id, work.run_id);
454
+ return runtimeCommand(context, ["work", "start", shortWorkId(work.work_id), "--project-root", run.project_root, ...(recovery ? ["--recovery-id", recovery.recovery_id] : []), "--json"]);
455
+ }
456
+ export function workStartCommand(context, work) {
457
+ const command = rawWorkStartCommand(context, work);
458
+ const retained = context.db.get("SELECT status,current_start_invocation_id FROM works WHERE work_id = ?", [work.work_id]);
459
+ if (retained?.current_start_invocation_id) {
460
+ const prepared = prepareWorkStartAuthority(context, command, retained.current_start_invocation_id);
461
+ return managedLifecycleCommand({ ...context, env: { ...context.env, DD_FLOW_INVOCATION_SCOPE: JSON.stringify(prepared.scope) } }, `${command} --invocation-id ${retained.current_start_invocation_id}`);
462
+ }
463
+ // Legacy terminal/running Work is historical, never a fresh dispatch.
464
+ if (retained?.status !== "created")
465
+ return managedLifecycleCommand(context, command);
466
+ const prepared = prepareWorkStartAuthority(context, command);
467
+ if (!prepared)
468
+ return command;
469
+ if (!context.db.writable || context.env.DD_FLOW_INVOCATION_READONLY === "1")
470
+ throw new AppError("invocation_command_unprepared", "Work start authority has not been prepared", 1);
471
+ const id = context.db.writeTransaction(() => {
472
+ assertRunMutationAllowed(context, work.project_id, work.run_id, prepared.scope.generation);
473
+ const current = context.db.get("SELECT status,current_start_invocation_id FROM works WHERE work_id = ?", [work.work_id]);
474
+ if (current?.current_start_invocation_id)
475
+ return current.current_start_invocation_id;
476
+ if (current?.status !== "created")
477
+ throw new AppError("work_retry_conflict", "Work changed before start issuance", 1);
478
+ const issued = issueLifecycleInvocation(context, prepared.command, prepared.scope);
479
+ context.db.run("UPDATE works SET current_start_invocation_id = ? WHERE work_id = ?", [issued.id, work.work_id]);
480
+ return issued.id;
481
+ });
482
+ return managedLifecycleCommand(context, `${command} --invocation-id ${id}`);
483
+ }
453
484
  export function assertWorkLaunchReady(context, id) {
454
485
  ensureWorkRegistry(context);
455
486
  const work = requireWork(context, id);
@@ -639,7 +670,10 @@ export function startBoundWork(context, work, run, claimIdentity, hookEventId, s
639
670
  if (JSON.stringify(dependencyResults) !== JSON.stringify(preparedDependencies) || JSON.stringify(recovery) !== JSON.stringify(preparedRecovery))
640
671
  throw new AppError("work_not_ready", "Work dependencies or recovery segment changed while preparing its packet", 2, { work_id: current.work_id });
641
672
  const assignments = new Set();
642
- const commands = workerLifecycleCommands({ ...context, captureLifecycleAssignment: (id) => assignments.add(id) }, current, workerRun);
673
+ const currentStart = context.db.get("SELECT current_start_invocation_id FROM works WHERE work_id = ?", [work.work_id])?.current_start_invocation_id;
674
+ if (currentStart && context.env.DD_FLOW_CURRENT_INVOCATION && currentStart !== context.env.DD_FLOW_CURRENT_INVOCATION)
675
+ throw new AppError("work_generation_stale", "Work start authority belongs to an earlier attempt", 1, { work_id: work.work_id });
676
+ const commands = workerLifecycleCommands({ ...context, captureLifecycleAssignment: (id) => assignments.add(id) }, current, workerRun, true);
643
677
  const prompt = promptTemplate.replace(finishMarker, commands.finish).replace(failMarker, commands.fail);
644
678
  bindLifecyclePacketAssignments(context, prompt, assignments);
645
679
  const workContext = { schema_id: "dd-flow/work-context@1", run_id: current.run_id, work_id: current.work_id, parent_work_id: current.parent_work_id, project_root: run.project_root, workspace_root: workerRun.workspace_root, run_root: requireRunHome(run), depends_on: parseDependencies(current), launch_policy: current.launch_policy, result_schema: current.result_schema, read_only: readOnly, ...(recovery ? { recovery } : {}), ...(readOnly ? { workspace_fingerprint: fingerprint, ...(reviewInput ? { review_input: reviewInput } : {}), ...(reviewCopy ? { review_copy: reviewCopy } : {}) } : {}) };
@@ -771,7 +805,7 @@ export function prepareWorkRetry(context, id, reason) {
771
805
  throw new AppError("work_retry_invalid", "Retry journal is not valid JSON", 1);
772
806
  }
773
807
  const item = value;
774
- if (!item || typeof item !== "object" || item.work_id !== work.work_id || typeof item.archive !== "string" || !/^ATT-\d{3,}$/.test(item.archive) || !Array.isArray(item.entries) || item.entries.some(file => !["prompt.md", "result.json", "context.json"].includes(file)) || new Set(item.entries).size !== item.entries.length || !(item.result_sha256 === null || typeof item.result_sha256 === "string" && /^[a-f0-9]{64}$/.test(item.result_sha256)))
808
+ if (!item || typeof item !== "object" || item.work_id !== work.work_id || typeof item.archive !== "string" || !/^ATT-\d{3,}$/.test(item.archive) || !Array.isArray(item.entries) || item.entries.some(file => !["prompt.md", "result.json", "context.json", "result-input.json"].includes(file)) || new Set(item.entries).size !== item.entries.length || !(item.result_sha256 === null || typeof item.result_sha256 === "string" && /^[a-f0-9]{64}$/.test(item.result_sha256)))
775
809
  throw new AppError("work_retry_invalid", "Retry journal does not identify this Work", 1);
776
810
  pending = item;
777
811
  }
@@ -781,7 +815,7 @@ export function prepareWorkRetry(context, id, reason) {
781
815
  let number = 1;
782
816
  while (fs.existsSync(path.join(attempts, `ATT-${String(number).padStart(3, "0")}`)))
783
817
  number += 1;
784
- const prepared = pending ?? { work_id: work.work_id, archive: `ATT-${String(number).padStart(3, "0")}`, entries: ["prompt.md", "result.json", "context.json"].filter(file => fs.existsSync(path.join(directory, file))), result_sha256: work.result === null ? null : crypto.createHash("sha256").update(work.result).digest("hex") };
818
+ const prepared = pending ?? { work_id: work.work_id, archive: `ATT-${String(number).padStart(3, "0")}`, entries: ["prompt.md", "result.json", "context.json", "result-input.json"].filter(file => fs.existsSync(path.join(directory, file))), result_sha256: work.result === null ? null : crypto.createHash("sha256").update(work.result).digest("hex") };
785
819
  const archive = path.join(attempts, prepared.archive);
786
820
  if (work.status === "failed") {
787
821
  if (prepared.result_sha256 !== (work.result === null ? null : crypto.createHash("sha256").update(work.result).digest("hex")))
@@ -805,6 +839,8 @@ export function retryWork(context, id, reason) {
805
839
  // live path can be archived for the next attempt.
806
840
  materializePendingWorkResults(context, pending.project_id, pending.run_id, pending.work_id);
807
841
  const { work, run, directory, journal, pending: retainedJournal, prepared, archive } = prepareWorkRetry(context, id, reason);
842
+ const previousStart = context.db.get("SELECT current_start_invocation_id FROM works WHERE work_id = ?", [id])?.current_start_invocation_id;
843
+ const startAuthority = work.status === "failed" ? prepareWorkStartAuthority(context, rawWorkStartCommand(context, work), previousStart) : null;
808
844
  if (work.status === "failed") {
809
845
  // The durable journal and archive are projections of the failed SQL
810
846
  // receipt. They must finish before the short transaction opens a new
@@ -836,15 +872,18 @@ export function retryWork(context, id, reason) {
836
872
  }
837
873
  context.db.writeTransaction(() => {
838
874
  const current = requireWork(context, work.work_id);
839
- assertRunMutationAllowed(context, current.project_id, current.run_id);
875
+ assertRunMutationAllowed(context, current.project_id, current.run_id, startAuthority?.scope.generation);
840
876
  if (current.status !== "failed" || current.result !== work.result || current.updated_at !== work.updated_at)
841
877
  throw new AppError("work_retry_conflict", "Failed Work receipt changed during retry", 1);
878
+ retireWorkPacketAuthorities(context, work.work_id);
842
879
  for (const column of ["prompt_path", "result_path"]) {
843
880
  const file = column === "prompt_path" ? "prompt.md" : "result.json";
844
881
  context.db.run(`UPDATE work_sessions SET ${column} = ? WHERE work_id = ? AND ${column} = ?`, [path.join(archive, file), work.work_id, path.join(directory, file)]);
845
882
  }
846
883
  if (context.db.run("UPDATE works SET status = 'created', result = NULL, started_at = NULL, completed_at = NULL, updated_at = ? WHERE work_id = ? AND status = 'failed'", [context.now(), work.work_id]).changes !== 1)
847
884
  throw new AppError("work_retry_conflict", "Failed Work changed during retry", 1);
885
+ const startId = startAuthority ? issueLifecycleInvocation(context, startAuthority.command, startAuthority.scope).id : null;
886
+ context.db.run("UPDATE works SET current_start_invocation_id = ? WHERE work_id = ?", [startId, work.work_id]);
848
887
  });
849
888
  }
850
889
  fs.rmSync(journal, { force: true });
@@ -1138,8 +1177,12 @@ async function settle(context, id, status, result, progress, retry) {
1138
1177
  const control = runControlDirective(context, work.project_id, work.run_id);
1139
1178
  const newlyReady = control ? [] : context.db.all(`SELECT ${workColumns} FROM works WHERE project_id = ? AND run_id = ? AND status = 'created' ORDER BY created_at, work_id`, [work.project_id, work.run_id]).filter((candidate) => isReady(context, candidate)).map((candidate) => ({ work_id: candidate.work_id, launch_policy: candidate.launch_policy, start_command: workStartCommand(context, candidate) }));
1140
1179
  appendFlowRunTimelineEvent(context, work.project_id, work.run_id, { type: `work_${status}`, work_id: work.work_id, session_id: link?.session_id ?? null, ...(coordinationDrift.length ? { coordination_drift: coordinationDrift } : {}) });
1141
- for (const ready of newlyReady)
1142
- appendFlowRunTimelineEvent(context, work.project_id, work.run_id, { type: "work_dependency_unblocked", work_id: ready.work_id, completed_dependency: work.work_id });
1180
+ if (status === "completed")
1181
+ for (const ready of newlyReady) {
1182
+ const candidate = requireWork(context, ready.work_id);
1183
+ if (parseDependencies(candidate).includes(work.work_id))
1184
+ appendFlowRunTimelineEvent(context, work.project_id, work.run_id, { type: "work_dependency_unblocked", work_id: ready.work_id, completed_dependency: work.work_id });
1185
+ }
1143
1186
  return { ok: true, ...(control ? { control } : {}), work_id: id, status, checks: receipts, coordination: { planned_write_areas: codePacket(work)?.planned_write_areas ?? [], drift: coordinationDrift }, newly_ready: newlyReady, graph: codeWorkGraph(context, work.project_id, work.run_id), usage: { status: "provisional", reason: "final usage is controller-owned" } };
1144
1187
  }
1145
1188
  function recoveryPrompt(recovery) { return ["<recovery_identity>", `- recovery_id: ${recovery.recoveryId}`, `- execution_generation: ${recovery.generation}`, "This Work was interrupted and is now an authorized recovery segment of the same assignment.", "</recovery_identity>", "", "<recovery_instructions>", `Read the retained pre-interruption evidence at ${recovery.evidencePath}.`, "Before changing files or rerunning a command, inspect the workspace, receipts, logs, and partial artifacts. Do not assume the last command succeeded or failed merely because its reply was lost.", "Preserve accepted work. Do not repeat completed Work, create undeclared Work, alter the original requirements, or edit dd-flow runtime state/evidence.", "Observe an existing check or finish instead of duplicating it. Retry only through an exact runtime retry command after establishing that its prior outcome permits it.", "Record the concrete unresolved operation rather than guessing if an external effect cannot be reconciled.", "</recovery_instructions>"].join("\n"); }
@@ -1291,11 +1334,11 @@ function renderWorkerPrompt(context, work, run, dependencies, commands, workspac
1291
1334
  const sources = matrix?.matrix.sources.filter(source => ["specify", "protocolize", "protocolize_report"].includes(source.role)) ?? [];
1292
1335
  return ["<accepted_decision_precedence>", `Read ${path.join(requireRunHome(run), "01-specify", "specify.json")} and later accepted RUN decisions relevant to this Work.`, ...sources.map(source => `Accepted ${source.role}: ${path.join(source.root === "run" ? requireRunHome(run) : run.workspace_root, source.path)} (sha256 ${source.sha256}). PROTOCOLIZE includes the effective amendments; preserve SPECIFY non-goals.`), acceptedDecisionPrecedence, "</accepted_decision_precedence>", "", body, verificationMatrixInstruction(matrix?.metadata ?? null, requireRunHome(run), `assigned Work ${work.work_id}`)].join("\n");
1293
1336
  }
1294
- function workerLifecycleCommands(context, work, run) {
1337
+ function workerLifecycleCommands(context, work, run, freshWorkAttempt = false) {
1295
1338
  const resultInputPath = path.join(requireRunHome(run), "works", work.work_id, "result-input.json");
1296
1339
  return {
1297
- finish: managedLifecycleCommand(context, runtimeCommand(context, ["work", "finish", work.work_id, "--result-file", resultInputPath, "--project-root", run.project_root, "--json", "--progress-jsonl"])),
1298
- fail: managedLifecycleCommand(context, runtimeCommand(context, ["work", "fail", shortWorkId(work.work_id), "--reason", "receipt path + exact external or semantic blocker", "--project-root", run.project_root, "--json"]))
1340
+ finish: managedLifecycleCommand(context, runtimeCommand(context, ["work", "finish", work.work_id, "--result-file", resultInputPath, "--project-root", run.project_root, "--json", "--progress-jsonl"]), { freshWorkAttempt }),
1341
+ fail: managedLifecycleCommand(context, runtimeCommand(context, ["work", "fail", shortWorkId(work.work_id), "--reason", "receipt path + exact external or semantic blocker", "--project-root", run.project_root, "--json"]), { freshWorkAttempt })
1299
1342
  };
1300
1343
  }
1301
1344
  function renderWorkerPromptBody(context, work, run, dependencies, commands) {
@@ -301,6 +301,9 @@ export function ensureVnextWorkStorage(db) {
301
301
  // A committed Work start is authoritative even if publishing its prompt
302
302
  // files is interrupted after commit. Keep the exact bytes for repair.
303
303
  db.writeTransaction(() => {
304
+ const workColumns = db.all("PRAGMA table_info(works)");
305
+ if (!workColumns.some(column => column.name === "current_start_invocation_id"))
306
+ db.execSchema("ALTER TABLE works ADD COLUMN current_start_invocation_id TEXT");
304
307
  const columns = db.all("PRAGMA table_info(work_sessions)");
305
308
  if (!columns.some((column) => column.name === "start_receipt_json"))
306
309
  db.execSchema("ALTER TABLE work_sessions ADD COLUMN start_receipt_json TEXT");
@@ -1558,6 +1561,7 @@ function migrate(db) {
1558
1561
  ensureColumn(db, "runs", "layout_version", "ALTER TABLE runs ADD COLUMN layout_version TEXT");
1559
1562
  ensureColumn(db, "runs", "artifact_root_kind", "ALTER TABLE runs ADD COLUMN artifact_root_kind TEXT");
1560
1563
  ensureColumn(db, "works", "payload_json", "ALTER TABLE works ADD COLUMN payload_json TEXT");
1564
+ ensureColumn(db, "works", "current_start_invocation_id", "ALTER TABLE works ADD COLUMN current_start_invocation_id TEXT");
1561
1565
  ensureColumn(db, "work_sessions", "start_receipt_json", "ALTER TABLE work_sessions ADD COLUMN start_receipt_json TEXT");
1562
1566
  ensureColumn(db, "work_sessions", "start_publication_status", "ALTER TABLE work_sessions ADD COLUMN start_publication_status TEXT");
1563
1567
  ensureColumn(db, "work_sessions", "result_receipt_json", "ALTER TABLE work_sessions ADD COLUMN result_receipt_json TEXT");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deksden-com/dd-flow-cli",
3
- "version": "0.9.0-beta.133",
3
+ "version": "0.9.0-beta.134",
4
4
  "description": "Mechanical runtime CLI for dd-flow workflows.",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@10.23.0",