@deksden-com/dd-flow-cli 0.9.0-beta.161 → 0.9.0-beta.162

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,11 @@
1
1
  # @deksden-com/dd-flow-cli
2
2
 
3
+ ## 0.9.0-beta.162
4
+
5
+ ### Patch Changes
6
+
7
+ - Bind check-recovery help to its single committed evidence-file successor and canonical RUN path. Strip stale executable retry promises in common and hook-only fatal, repeated and persistence outcomes, including response overlays. Retain proven native assignment and directory refusal diagnostics without granting authority or masking their primary typed error (plan116).
8
+
3
9
  ## 0.9.0-beta.161
4
10
 
5
11
  ### Patch Changes
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "cli_package": "@deksden-com/dd-flow-cli",
3
- "cli_version": "0.9.0-beta.161",
4
- "cli_commit": "367b9cb844aa34fd36d0a00cf059160377d10bdd",
5
- "built_at": "2026-10-29T17:36:11.881Z",
3
+ "cli_version": "0.9.0-beta.162",
4
+ "cli_commit": "d78bcd1bb6f510fee45a3d77463b172cc92420dd",
5
+ "built_at": "2026-10-29T23:21:33.965Z",
6
6
  "built_with_canon": {
7
7
  "version": "4.1.4",
8
8
  "commit": "7cc415b3ef3ce37c9624c7cee51023e66f67cd57",
@@ -8,7 +8,7 @@ import { assertAuxiliaryCommandScope, cliManagedInvocationScope, manualRecoveryI
8
8
  import path from "node:path";
9
9
  import { createHash, randomUUID } from "node:crypto";
10
10
  import { quote } from "shell-quote";
11
- import { observeLifecycleCommand, observedLifecycleNativeEvent } from "../services/lifecycle-invocations.js";
11
+ import { observeLifecycleCommand, observedLifecycleNativeEvent, removeRetryPromises } from "../services/lifecycle-invocations.js";
12
12
  import { verifyCodexHookDelivery } from "../services/codex-hook-delivery.js";
13
13
  import { assertLifecycleInvocationCurrent, awaitLifecycleInvocation, bindLifecyclePublishedPacket, expandLifecycleInvocationArgs, issueLifecycleInvocation, prepareLifecycleIssuance, lifecycleInvocationScope, observedLifecycleDiagnostic, observedLifecycleInvocation, resolveObservedLifecycleArgs, retainObservedLifecycleSemantics, settleLifecycleInvocation, settleLifecyclePreparationRejection, settleLifecycleRejection, validateLifecyclePathAliases } from "../services/lifecycle-invocations.js";
14
14
  import { readAcceptedAssignedWork } from "../services/assigned-lifecycle.js";
@@ -316,6 +316,8 @@ async function runCliRequest(args, io, env) {
316
316
  const diagnostic = env.DD_FLOW_DAEMON_ID && lifecycleCommand.kind === "standalone"
317
317
  ? observedLifecycleDiagnostic(routerContext, publicCommand, env.DD_FLOW_DAEMON_ID, observedInvocation?.id) : null;
318
318
  if (diagnostic && (!nativeEvent || Number(diagnostic.hook_event_id) > nativeEvent.id)) {
319
+ if (["invocation_assignment_missing", "invocation_directory_mismatch", "opencode_directory_mismatch", "agy_directory_mismatch", "droid_directory_mismatch"].includes(String(diagnostic.code)))
320
+ throw new AppError(String(diagnostic.code), String(diagnostic.reason), 1, { ...diagnostic, phase: "prepare", effect: "no_effect", recoverable: false });
319
321
  throw new AppError("lifecycle_shell_syntax_invalid", `Native observer rejected the shell form: ${String(diagnostic.reason ?? "unsupported shell composition")}. Write structured input to a file and run the returned dd-flow command separately.`, 2, { ...diagnostic, phase: "prepare", effect: "no_effect", recoverable: true });
320
322
  }
321
323
  let manualRecoveryHookEventKey;
@@ -550,8 +552,7 @@ async function runCliRequest(args, io, env) {
550
552
  if (invocationSettlement && error.details.effect !== "committed") {
551
553
  try {
552
554
  if (isAppError(error)) {
553
- delete error.details.retry_command;
554
- delete error.details.retry_instruction;
555
+ removeRetryPromises(error.details);
555
556
  // The common settlement classifies original validated proof before
556
557
  // errorRecord clones diagnostic objects and loses capability identity.
557
558
  settleLifecycleRejection(invocationSettlement.context, invocationSettlement.id, error);
@@ -560,8 +561,10 @@ async function runCliRequest(args, io, env) {
560
561
  settleLifecycleInvocation(invocationSettlement.context, invocationSettlement.id, { error: { code: "unexpected", message: String(error), exitCode: 1, details: {} } });
561
562
  }
562
563
  catch (persistenceError) {
563
- if (isAppError(error))
564
+ if (isAppError(error)) {
564
565
  error.details.recoverable = false;
566
+ removeRetryPromises(error.details);
567
+ }
565
568
  io.stderr.write(`Invocation settlement persistence failed: ${String(persistenceError)}\n`);
566
569
  }
567
570
  }
@@ -14,7 +14,7 @@ import { prepareTextFile, prepareJsonText, prepareError } from "../cli/input-pre
14
14
  import { nativeHookNonParticipation } from "../cli/native-hook-preparation.js";
15
15
  import { nativeSessionIdentity, storageSessionId } from "./session-identity.js";
16
16
  import { assertRecoveryHookCurrent } from "./hook-recovery-guard.js";
17
- import { observeLifecycleInvocation, observedLifecycleInvocation, managedNativeInvocationScope } from "./lifecycle-invocations.js";
17
+ import { observeLifecycleInvocation, observedLifecycleInvocation, managedNativeInvocationScope, lifecycleDiagnosticFingerprint } from "./lifecycle-invocations.js";
18
18
  import { commandHasOption, commandOption, commandPosition, shellSuffixOffset, parseLifecycleCommand, lifecycleCommandFromPayload, continuationCommand } from "./lifecycle-command.js";
19
19
  import { registerProject, requireProjectByRoot } from "./project-registry.js";
20
20
  import { activeFlowSessionsForProject, recordFlowSessionObservation } from "./hook-session-observation.js";
@@ -421,7 +421,7 @@ export function handleCodexHook(context, input) {
421
421
  throw new AppError("codex_identity_missing", "Managed Codex lifecycle hook has no complete native invocation identity", 1, { invocation_id: invocationId });
422
422
  let executionCwd;
423
423
  try {
424
- executionCwd = assignedLifecycleCwd(context, project.root, lifecycle, providerSessionId, command, stringValue(payload.cwd), { daemonId, rootSessionId: rootProviderSessionId, sessionId: providerSessionId, parentSessionId: parentProviderSessionId, toolCallId }, "codex-desktop");
424
+ executionCwd = assignedLifecycleCwd(context, project.root, lifecycle, providerSessionId, command, stringValue(payload.cwd), { daemonId, rootSessionId: rootProviderSessionId, sessionId: providerSessionId, parentSessionId: parentProviderSessionId, toolCallId }, "codex-desktop", () => { persist(); return eventKey; });
425
425
  }
426
426
  catch (error) {
427
427
  // A proven external Work is not its Stage coordinator. Deny the tool
@@ -614,7 +614,7 @@ function recordZcodeLifecycle(context, input, invocationId, nativePreTool = fals
614
614
  return context.db.writeTransaction(() => persist());
615
615
  }
616
616
  let persistedResult;
617
- const executionCwd = assignedLifecycleCwd(context, expectedRoot, lifecycle, providerSessionId, command, observedCwd ?? undefined, { daemonId, rootSessionId: rootProviderSessionId, sessionId: providerSessionId, parentSessionId: parentProviderSessionId ?? null, toolCallId: toolUseId }, "zcode-acp");
617
+ const executionCwd = assignedLifecycleCwd(context, expectedRoot, lifecycle, providerSessionId, command, observedCwd ?? undefined, { daemonId, rootSessionId: rootProviderSessionId, sessionId: providerSessionId, parentSessionId: parentProviderSessionId ?? null, toolCallId: toolUseId }, "zcode-acp", () => { persist(); return eventKey; });
618
618
  const receipt = observeLifecycleInvocation(context, {
619
619
  currentEventKey: eventKey,
620
620
  ...(invocationId ? { id: invocationId } : {}), command,
@@ -715,7 +715,7 @@ export function handleGrokEvent(context, input) {
715
715
  return { ...result(context.db.writeTransaction(() => persist())), hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow", updatedInput: { ...rawInput, command: commandWithHookEvent(command, eventKey) } } };
716
716
  }
717
717
  let inserted = false;
718
- const executionCwd = assignedLifecycleCwd(context, expectedRoot, lifecycle, providerSessionId, command, observedCwd ?? undefined, { daemonId: daemonId ?? "", rootSessionId: rootProviderSessionId, sessionId: providerSessionId, parentSessionId, toolCallId: eventKey }, "grok-acp");
718
+ const executionCwd = assignedLifecycleCwd(context, expectedRoot, lifecycle, providerSessionId, command, observedCwd ?? undefined, { daemonId: daemonId ?? "", rootSessionId: rootProviderSessionId, sessionId: providerSessionId, parentSessionId, toolCallId: eventKey }, "grok-acp", () => { persist(); return eventKey; });
719
719
  const receipt = observeLifecycleInvocation(context, {
720
720
  currentEventKey: eventKey,
721
721
  command, harness: "grok-acp", projectId: project.id,
@@ -751,7 +751,7 @@ function requiresAssignedLifecycleCwd(facts) {
751
751
  // their tools/checks receive owned paths, not the shell's working directory.
752
752
  return !facts || !["stage_finish", "stage_pause", "stage_resume", "work_finish", "work_fail"].includes(facts.invocation.operation);
753
753
  }
754
- function assignedLifecycleCwd(context, projectRoot, facts, providerSessionId, command, observedCwd, identity, harness) {
754
+ function assignedLifecycleCwd(context, projectRoot, facts, providerSessionId, command, observedCwd, identity, harness, recordRefusal) {
755
755
  const auxiliary = continuationCommand(command);
756
756
  if ((!facts && !auxiliary) || facts?.analysis.supported === false)
757
757
  return undefined;
@@ -764,8 +764,12 @@ function assignedLifecycleCwd(context, projectRoot, facts, providerSessionId, co
764
764
  }
765
765
  const constraint = proven ?? (identity ? { projectRoot, daemonId: identity.daemonId, rootSessionId: identity.rootSessionId } : undefined);
766
766
  const issuedRunId = facts && !facts.runId && !facts.workId && !facts.bootstrapStageStart && constraint ? observedLifecycleInvocation(context, command, constraint, identity ? { identity } : undefined)?.scope.runId : null;
767
- if (facts?.invocation.operation.startsWith("stage_") && !facts.bootstrapStageStart && !facts.runId && !issuedRunId)
768
- throw new AppError("invocation_assignment_missing", "Short lifecycle call has no unique issued RUN assignment; its workspace cannot be inferred", 1, { phase: "prepare", effect: "no_effect", recoverable: false });
767
+ if (facts?.invocation.operation.startsWith("stage_") && !facts.bootstrapStageStart && !facts.runId && !issuedRunId) {
768
+ const error = assignmentMissing();
769
+ if (proven && recordRefusal)
770
+ retainAssignmentRefusal(context, projectRoot, command, recordRefusal, error);
771
+ throw error;
772
+ }
769
773
  if (!requiresAssignedLifecycleCwd(facts))
770
774
  return undefined;
771
775
  const expected = controlledLifecycleCwd(context, projectRoot, facts, providerSessionId, auxiliary, issuedRunId, observedCwd);
@@ -774,14 +778,38 @@ function assignedLifecycleCwd(context, projectRoot, facts, providerSessionId, co
774
778
  if (!expected)
775
779
  return undefined;
776
780
  const route = facts?.invocation.expectedCwd ?? auxiliary?.expectedCwd;
777
- if (route && resolveProjectRoot(route) !== expected)
778
- throw new AppError("invocation_directory_mismatch", "Literal lifecycle route differs from the assigned workspace", 1, { expected, actual: route, actual_provenance: "shell_literal_route", phase: "prepare", effect: "no_effect", recoverable: false });
779
- if (!route && observedCwd && resolveProjectRoot(observedCwd) !== expected)
780
- throw new AppError("invocation_directory_mismatch", "Native lifecycle directory differs from the assigned workspace", 1, { expected, actual: observedCwd, actual_provenance: "native_tool_cwd", phase: "prepare", effect: "no_effect", recoverable: false });
781
+ const directoryError = route && resolveProjectRoot(route) !== expected
782
+ ? new AppError("invocation_directory_mismatch", "Literal lifecycle route differs from the assigned workspace", 1, { expected, actual: route, actual_provenance: "shell_literal_route", phase: "prepare", effect: "no_effect", recoverable: false })
783
+ : !route && observedCwd && resolveProjectRoot(observedCwd) !== expected
784
+ ? new AppError("invocation_directory_mismatch", "Native lifecycle directory differs from the assigned workspace", 1, { expected, actual: observedCwd, actual_provenance: "native_tool_cwd", phase: "prepare", effect: "no_effect", recoverable: false }) : null;
785
+ if (directoryError) {
786
+ if (proven && recordRefusal)
787
+ retainAssignmentRefusal(context, projectRoot, command, recordRefusal, directoryError);
788
+ throw directoryError;
789
+ }
781
790
  // ACP without native cwd still binds assigned authority; CLI confirms the
782
791
  // physical process directory, rather than inventing native observation.
783
792
  return expected;
784
793
  }
794
+ function assignmentMissing() {
795
+ return new AppError("invocation_assignment_missing", "Short lifecycle call has no unique issued RUN assignment; its workspace cannot be inferred", 1, { phase: "prepare", effect: "no_effect", recoverable: false });
796
+ }
797
+ /** A proven native refusal is diagnosis, never an executable assignment. */
798
+ function retainAssignmentRefusal(context, projectRoot, command, record, error) {
799
+ const commandFingerprint = lifecycleDiagnosticFingerprint(command);
800
+ if (!commandFingerprint)
801
+ return;
802
+ const project = requireProjectByRoot(context, projectRoot);
803
+ context.db.writeTransaction(() => {
804
+ const eventKey = record();
805
+ const row = context.db.get("SELECT outcome_json FROM hook_events WHERE project_id = ? AND event_key = ?", [project.id, eventKey]);
806
+ if (row?.outcome_json)
807
+ return;
808
+ context.db.run("UPDATE hook_events SET outcome_json = ? WHERE project_id = ? AND event_key = ? AND outcome_json IS NULL", [JSON.stringify({ lifecycle_observation: {
809
+ ...error.details, code: error.code, reason: error.message, command_fingerprint: commandFingerprint, phase: "prepare", effect: "no_effect", recoverable: false
810
+ } }), project.id, eventKey]);
811
+ });
812
+ }
785
813
  function controlledLifecycleCwd(context, projectRoot, facts, providerSessionId, auxiliary, issuedRunId, observedCwd) {
786
814
  if (facts?.bootstrapStageStart)
787
815
  return projectRoot;
@@ -913,19 +941,18 @@ function handleControlledToolEvent(context, input, event, harness) {
913
941
  const provenScope = lifecycle && !shellRefusal ? managedNativeInvocationScope(context, expectedRoot, nativeIdentity, harness) : null;
914
942
  const issuedRunId = lifecycle && !lifecycle.runId && !lifecycle.workId && !lifecycle.bootstrapStageStart
915
943
  ? observedLifecycleInvocation(context, command, provenScope ?? { projectRoot: expectedRoot, daemonId, rootSessionId: nativeIdentity.rootSessionId }, { identity: nativeIdentity })?.scope.runId : null;
916
- if (!shellRefusal && lifecycle && lifecycle.invocation.operation.startsWith("stage_") && !lifecycle.bootstrapStageStart && !lifecycle.runId && !issuedRunId) {
917
- throw new AppError("invocation_assignment_missing", "Short lifecycle call has no unique issued RUN assignment; its workspace cannot be inferred", 1, { effect: "no_effect", recoverable: false });
918
- }
944
+ const missingAssignment = !shellRefusal && lifecycle && lifecycle.invocation.operation.startsWith("stage_") && !lifecycle.bootstrapStageStart && !lifecycle.runId && !issuedRunId;
919
945
  // These controlled platforms additionally qualify their native launch
920
946
  // route against the stable project root when no CLI target is assigned.
921
947
  const workspaceBound = requiresAssignedLifecycleCwd(lifecycle);
922
948
  const expectedCwd = shellRefusal || !workspaceBound ? observedCwd : controlledLifecycleCwd(context, expectedRoot, lifecycle, providerSessionId, auxiliary, issuedRunId, observedCwd) ?? expectedRoot;
923
949
  const executionRoute = shellRefusal || !workspaceBound ? undefined : lifecycle?.invocation.expectedCwd ?? auxiliary?.expectedCwd;
950
+ let directoryError;
924
951
  if (executionRoute && resolveProjectRoot(executionRoute) !== expectedCwd) {
925
- throw new AppError(`${errorPrefix}_directory_mismatch`, "Literal shell cd route does not match the assigned lifecycle workspace", 1, { expected: expectedCwd, actual: executionRoute, actual_provenance: "shell_literal_route", effect: "no_effect", recoverable: false });
952
+ directoryError = new AppError(`${errorPrefix}_directory_mismatch`, "Literal shell cd route does not match the assigned lifecycle workspace", 1, { expected: expectedCwd, actual: executionRoute, actual_provenance: "shell_literal_route", effect: "no_effect", recoverable: false });
926
953
  }
927
954
  if (workspaceBound && !executionRoute && observedCwd !== expectedCwd) {
928
- throw new AppError(`${errorPrefix}_directory_mismatch`, `${label} ${agy ? "launch route" : "Session directory"} does not match the assigned execution workspace`, 1, { expected: expectedCwd, actual: observedCwd, effect: "no_effect", recoverable: false, ...(agy ? { actual_provenance: stringValue(event.cwd_provenance) ?? "unknown" } : {}) });
955
+ directoryError = new AppError(`${errorPrefix}_directory_mismatch`, `${label} ${agy ? "launch route" : "Session directory"} does not match the assigned execution workspace`, 1, { expected: expectedCwd, actual: observedCwd, effect: "no_effect", recoverable: false, ...(agy ? { actual_provenance: stringValue(event.cwd_provenance) ?? "unknown" } : {}) });
929
956
  }
930
957
  const bootstrapStageStart = !shellRefusal && lifecycle?.bootstrapStageStart;
931
958
  let matchKey = lifecycle && !shellRefusal ? lifecycleMatchKey(lifecycle, expectedRoot) : null;
@@ -959,6 +986,17 @@ function handleControlledToolEvent(context, input, event, harness) {
959
986
  return inserted;
960
987
  };
961
988
  const result = (inserted) => ({ ok: true, observed: inserted, duplicate: !inserted, event_key: baseKey, phase, harness, session: nativeSessionIdentity(harness, providerSessionId), ...(nativeParentId ? { parent_session: nativeSessionIdentity(harness, nativeParentId) } : {}), daemon_id: daemonId });
989
+ if (directoryError) {
990
+ if (provenScope)
991
+ retainAssignmentRefusal(context, expectedRoot, command, () => { persist(); return baseKey; }, directoryError);
992
+ throw directoryError;
993
+ }
994
+ if (missingAssignment) {
995
+ const error = assignmentMissing();
996
+ if (provenScope)
997
+ retainAssignmentRefusal(context, expectedRoot, command, () => { persist(); return baseKey; }, error);
998
+ throw error;
999
+ }
962
1000
  if (lifecycle) {
963
1001
  let inserted = false;
964
1002
  const receipt = observeLifecycleInvocation(context, {
@@ -20,7 +20,7 @@ import { isRegisteredRepair } from "./repair-receipt.js";
20
20
  import { stageAttempt, stageWorkIds } from "./stage-work-graph.js";
21
21
  import { contextualPathOptionsForLifecycle } from "../cli/command-inputs.js";
22
22
  import { canonicalPath } from "../storage/paths.js";
23
- import { runtimeCommand, shellArgument } from "./runtime-command.js";
23
+ import { runtimeCommand, shellArgument, checkRetryArgs, recoveryEvidenceInput } from "./runtime-command.js";
24
24
  import { assignedLifecycleBinding, claimAssignedLifecycle, currentAssignedLifecycle, bindAssignedCoordinatorPacket, inheritAssignedPackets, retainControllerStageAssignment, admitAssignedAuxiliary, bindAssignedStageResume, readAcceptedAssignedWork, assertTerminalSettlementUnreserved } from "./assigned-lifecycle.js";
25
25
  import { appendAudit } from "./audit.js";
26
26
  import { recordTechnicalNote } from "./technical-notes.js";
@@ -628,8 +628,7 @@ export function observeLifecycleCommand(context, command, nativeEvent) {
628
628
  }
629
629
  else {
630
630
  error.details.recoverable = false;
631
- delete error.details.retry_command;
632
- delete error.details.retry_instruction;
631
+ removeRetryPromises(error.details);
633
632
  if (inputKey)
634
633
  error.details.repeated_without_progress = true;
635
634
  else
@@ -641,8 +640,7 @@ export function observeLifecycleCommand(context, command, nativeEvent) {
641
640
  }
642
641
  if (error && !auxiliary && record.disposition === "fatal") {
643
642
  error.details.recoverable = false;
644
- delete error.details.retry_command;
645
- delete error.details.retry_instruction;
643
+ removeRetryPromises(error.details);
646
644
  }
647
645
  if (previous?.storage_retry === 1)
648
646
  Object.assign(record, { storage_retry: 1, work_id: previous.work_id });
@@ -663,9 +661,10 @@ export function observeLifecycleCommand(context, command, nativeEvent) {
663
661
  throw persistence;
664
662
  if (error) {
665
663
  error.details.recoverable = false;
666
- delete error.details.retry_command;
667
- delete error.details.retry_instruction;
664
+ removeRetryPromises(error.details);
668
665
  }
666
+ if (outcome.error)
667
+ removeRetryPromises(outcome.error.details);
669
668
  // Diagnostic evidence is not replay authority. If SQL is unavailable,
670
669
  // the controller still sees this failure and fences instead of waiting
671
670
  // for a model to report it or invent a new invocation.
@@ -686,10 +685,10 @@ export function observeLifecycleCommand(context, command, nativeEvent) {
686
685
  if (outcome.error && error) {
687
686
  outcome.error.code = error.code;
688
687
  outcome.error.message = error.message;
688
+ removeRetryPromises(outcome.error.details);
689
689
  Object.assign(outcome.error.details, error.details);
690
690
  if (record.disposition === "fatal") {
691
- delete outcome.error.details.retry_command;
692
- delete outcome.error.details.retry_instruction;
691
+ removeRetryPromises(outcome.error.details);
693
692
  }
694
693
  }
695
694
  };
@@ -892,16 +891,16 @@ export function managedLifecycleCommand(context, command, options = {}) {
892
891
  return publicInvocationCommand(context, load(context, issued.id), command);
893
892
  });
894
893
  }
895
- export function retryLifecycleInvocationCommand(context, id, checkReceiptId) {
894
+ export function retryLifecycleInvocationCommand(context, id, checkReceiptId, needsEvidenceInput = false) {
896
895
  const prior = load(context, id);
897
896
  if (prior.status !== "executing")
898
897
  throw new AppError("invocation_retry_not_authorized", "Only the executing lifecycle operation can publish its next attempt", 1);
899
- return successorLifecycleInvocationCommand(context, prior, checkReceiptId);
898
+ return successorLifecycleInvocationCommand(context, prior, checkReceiptId, needsEvidenceInput);
900
899
  }
901
900
  /** A predecessor may publish exactly one successor only while the caller has
902
901
  * proved that no lifecycle effect was admitted. Shared by CLI argument
903
902
  * validation and conclusive lifecycle/result rejection, never by hooks. */
904
- function successorLifecycleInvocationCommand(context, prior, checkReceiptId) {
903
+ function successorLifecycleInvocationCommand(context, prior, checkReceiptId, needsEvidenceInput = false) {
905
904
  const parsed = invocation(prior.command);
906
905
  const argv = [...parsed.argv];
907
906
  const at = argv.indexOf("--invocation-id");
@@ -925,7 +924,12 @@ function successorLifecycleInvocationCommand(context, prior, checkReceiptId) {
925
924
  throw new AppError("invocation_command_invalid", "Retained retry option cannot be rewritten unambiguously", 1);
926
925
  retained = retained.replace(fragments[0], "");
927
926
  }
928
- command = retained.replace(` --invocation-id ${prior.id}`, ` --retry-check ${shellArgument(checkReceiptId)} --reason ${shellArgument("Recheck after diagnosis and correction of the failed check")}`);
927
+ const scope = JSON.parse(prior.scope_json);
928
+ if (needsEvidenceInput && !scope.runId)
929
+ throw new AppError("runtime_missing", "Check recovery requires a bound RUN", 1);
930
+ const retryArgs = needsEvidenceInput ? checkRetryArgs(context, scope.projectRoot, scope.runId, checkReceiptId)
931
+ : ["--retry-check", checkReceiptId, "--reason", "Recheck after diagnosis and correction of the failed check"];
932
+ command = retained.replace(` --invocation-id ${prior.id}`, ` ${retryArgs.map(shellArgument).join(" ")}`);
929
933
  }
930
934
  const successor = managedLifecycleCommand({ ...context, env: { ...context.env, DD_FLOW_INVOCATION_SCOPE: prior.scope_json, DD_FLOW_CURRENT_INVOCATION: prior.id } }, command);
931
935
  const next = stagedContinuations.get(context.db)?.get(prior.id) ?? currentIssuedCommand(context, prior.scope_json, successor, prior.id);
@@ -946,6 +950,15 @@ function settleUnadmittedInvocation(context, prior, error) {
946
950
  return details;
947
951
  }
948
952
  const receiptTimeout = (id) => ({ code: "invocation_receipt_timeout", message: "No native confirmation before the receipt deadline; lifecycle state was not changed", exitCode: 1, details: { invocation_id: id } });
953
+ export function removeRetryPromises(details) {
954
+ delete details.retry_command;
955
+ delete details.retry_instruction;
956
+ if (details.continuation && typeof details.continuation === "object" && !Array.isArray(details.continuation)) {
957
+ const continuation = { ...details.continuation };
958
+ delete continuation.command_after_input;
959
+ details.continuation = continuation;
960
+ }
961
+ }
949
962
  /** Settle a conclusive rejection and publish its only legal successor atomically.
950
963
  * A crash must leave either the old attempt executing, or both the settled
951
964
  * outcome and its successor; never a usable successor beside an unknown effect. */
@@ -962,8 +975,7 @@ export function settleLifecycleRejection(context, id, error) {
962
975
  }
963
976
  storage(context);
964
977
  // Only this settlement may publish a successor; inherited help is not authority.
965
- delete error.details.retry_command;
966
- delete error.details.retry_instruction;
978
+ removeRetryPromises(error.details);
967
979
  // A producer may have rendered help before throwing. Only the decision below
968
980
  // may publish replacement authority for a rejected operation.
969
981
  stagedContinuations.get(context.db)?.delete(id);
@@ -992,8 +1004,15 @@ export function settleLifecycleRejection(context, id, error) {
992
1004
  && !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]);
993
1005
  if (prepared.retryable || storageRetry) {
994
1006
  const failures = prepared.receipts.map(value => value.receipt);
995
- const retry = retryLifecycleInvocationCommand(context, id, failures[0]?.id);
1007
+ const continuation = details.continuation;
1008
+ const needsInput = failures.length > 0 && continuation?.kind === "needs_input";
1009
+ const retry = retryLifecycleInvocationCommand(context, id, failures[0]?.id, needsInput);
996
1010
  details.retry_command = retry;
1011
+ if (needsInput) {
1012
+ const scope = JSON.parse(prior.scope_json);
1013
+ const run = context.db.get("SELECT run_root FROM runs WHERE project_root = ? AND id = ?", [scope.projectRoot, scope.runId]);
1014
+ details.continuation = { ...continuation, ...recoveryEvidenceInput(run.run_root, failures[0].id), command_after_input: retry };
1015
+ }
997
1016
  details.effect = "no_effect";
998
1017
  details.recoverable = true;
999
1018
  if (storageRetry)
@@ -1866,6 +1885,10 @@ export function observedLifecycleNativeEvent(context, command, daemonId) {
1866
1885
  // never choose the latest of several identical native calls.
1867
1886
  return matches.length === 1 ? { project_id: matches[0].project_id, id: matches[0].id } : null;
1868
1887
  }
1888
+ export function lifecycleDiagnosticFingerprint(command) {
1889
+ const analysis = parseLifecycleCommand(command);
1890
+ return analysis.kind === "standalone" ? fingerprint(analysis.invocation) : null;
1891
+ }
1869
1892
  /** Keep shell diagnostics distinct from genuinely absent native delivery. */
1870
1893
  export function observedLifecycleDiagnostic(context, command, daemonId, invocationId) {
1871
1894
  const analysis = parseLifecycleCommand(command);
@@ -2086,8 +2109,7 @@ function commandWithObservedWorkSemantics(row, argv) {
2086
2109
  return command;
2087
2110
  }
2088
2111
  export function settleLifecyclePreparationRejection(context, id, error, preparedCommand) {
2089
- delete error.details.retry_command;
2090
- delete error.details.retry_instruction;
2112
+ removeRetryPromises(error.details);
2091
2113
  try {
2092
2114
  return context.db.writeTransaction(() => {
2093
2115
  let row = load(context, id);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deksden-com/dd-flow-cli",
3
- "version": "0.9.0-beta.161",
3
+ "version": "0.9.0-beta.162",
4
4
  "description": "Mechanical runtime CLI for dd-flow workflows.",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@10.23.0",