@deksden-com/dd-flow-cli 0.9.0-beta.103 → 0.9.0-beta.105

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 (69) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/build-info.json +3 -3
  3. package/dist/cli/run-cli.js +32 -11
  4. package/dist/harness-runtime/bin/dd-agy.mjs +1 -1
  5. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +2 -2
  6. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +4 -3
  7. package/dist/harness-runtime/lib/delegation-instructions.d.mts +2 -0
  8. package/dist/harness-runtime/lib/delegation-instructions.mjs +8 -1
  9. package/dist/harness-runtime/lib/managed-daemon.mjs +11 -6
  10. package/dist/protocol/local-files.js +18 -9
  11. package/dist/runtime/context.js +2 -2
  12. package/dist/services/cleanup.js +81 -32
  13. package/dist/services/code-checks.js +18 -19
  14. package/dist/services/continuation-outcome.js +1 -1
  15. package/dist/services/controller-fanout.js +3 -3
  16. package/dist/services/eval-snapshots.js +69 -41
  17. package/dist/services/execution-policy.js +12 -1
  18. package/dist/services/hooks.js +150 -42
  19. package/dist/services/lanes.js +23 -18
  20. package/dist/services/lifecycle-command.js +11 -2
  21. package/dist/services/lifecycle-invocations.js +291 -54
  22. package/dist/services/managed-processes.js +35 -23
  23. package/dist/services/merge-queue.js +71 -67
  24. package/dist/services/merge-server.js +3 -11
  25. package/dist/services/migrations.js +28 -12
  26. package/dist/services/plan-runtime.js +8 -4
  27. package/dist/services/projects.js +3 -11
  28. package/dist/services/protocols.js +98 -68
  29. package/dist/services/recovery-snapshot-database.js +43 -8
  30. package/dist/services/run-control-worker.js +15 -32
  31. package/dist/services/run-control.js +3 -17
  32. package/dist/services/run-controller-capture.js +58 -27
  33. package/dist/services/run-controller-process.js +21 -30
  34. package/dist/services/run-controller-recovery.js +36 -37
  35. package/dist/services/run-controller.js +81 -86
  36. package/dist/services/run-fork.js +10 -6
  37. package/dist/services/run-projection.js +19 -64
  38. package/dist/services/run-recovery-runtime.js +24 -14
  39. package/dist/services/run-recovery.js +56 -54
  40. package/dist/services/runs.js +218 -160
  41. package/dist/services/runtime-budget.js +19 -45
  42. package/dist/services/runtime-scope-capture.js +8 -6
  43. package/dist/services/runtime-scope-control.js +49 -39
  44. package/dist/services/runtime-scope-resume.js +134 -122
  45. package/dist/services/runtime-scope-worker.js +48 -49
  46. package/dist/services/scope-writer-cleanup.js +26 -0
  47. package/dist/services/stage-context.js +6 -1
  48. package/dist/services/stage-lifecycle.js +1 -1
  49. package/dist/services/stage-pause.js +13 -29
  50. package/dist/services/stage-report-renderer.js +116 -6
  51. package/dist/services/usage.js +2 -9
  52. package/dist/services/vnext-code-review.js +10 -7
  53. package/dist/services/vnext-code.js +9 -17
  54. package/dist/services/vnext-merge.js +64 -21
  55. package/dist/services/vnext-plan-review.js +43 -30
  56. package/dist/services/vnext-plan.js +28 -24
  57. package/dist/services/vnext-protocolize.js +183 -20
  58. package/dist/services/vnext-specify.js +27 -19
  59. package/dist/services/work-registry.js +229 -150
  60. package/dist/storage/database.js +286 -56
  61. package/dist/storage/foreign-key-transaction.js +39 -0
  62. package/dist/storage/immutable-input.js +45 -0
  63. package/dist/storage/import-guard.js +102 -0
  64. package/dist/storage/native-cleanup.js +52 -0
  65. package/dist/storage/projection-lock.js +73 -0
  66. package/dist/storage/writer-contract.js +3 -3
  67. package/dist/storage/writer-migration.js +24 -41
  68. package/package.json +1 -1
  69. package/tools/repair-paused-run-status.mjs +48 -23
@@ -7,6 +7,7 @@ import { AppError } from "../shared/errors.js";
7
7
  import { entityReferenceVariants } from "../shared/entity-references.js";
8
8
  import { parseJsonObject } from "../shared/json.js";
9
9
  import { canonicalPath, ensureDir, resolveProjectRoot } from "../storage/paths.js";
10
+ import { resolveWorkReference } from "../storage/work-references.js";
10
11
  import { appendAudit } from "./audit.js";
11
12
  import { prepareTextFile, prepareJsonText, prepareError } from "../cli/input-preparation.js";
12
13
  import { nativeSessionIdentity, storageSessionId } from "./session-identity.js";
@@ -549,21 +550,22 @@ function recordZcodeLifecycle(context, input, invocationId) {
549
550
  const sessionId = storageSessionId(nativeSessionIdentity("zcode-acp", providerSessionId));
550
551
  const parentSessionId = parentProviderSessionId ? storageSessionId(nativeSessionIdentity("zcode-acp", parentProviderSessionId)) : null;
551
552
  const observedProfile = objectRecord(ddZcode.observedProfile);
553
+ const observedCwd = stringValue(hook.cwd) && path.isAbsolute(String(hook.cwd)) ? resolveProjectRoot(String(hook.cwd)) : null;
552
554
  let matchKey = lifecycle ? lifecycleMatchKey(lifecycle, expectedRoot) : null;
553
555
  const eventNameForReceipt = invocationId ? "ToolCallObserved" : "PreToolUse";
554
556
  const persist = (scope) => {
555
557
  if (scope)
556
558
  matchKey = lifecycleMatchKey(lifecycleFacts(scope.command), expectedRoot, scope);
557
- assertHookEventReplay(context, { projectId: project.id, eventKey, harness: "zcode-acp", providerSessionId, parentSessionId, daemonId, sessionId, turnId: null, eventName: eventNameForReceipt, toolName: toolName ?? "Bash", matchKey, cwd: expectedRoot });
558
- const payload = { ...hook, session_id: providerSessionId, cwd: expectedRoot, tool_name: toolName ?? "Bash", tool_input: rawInput,
559
+ assertHookEventReplay(context, { projectId: project.id, eventKey, harness: "zcode-acp", providerSessionId, parentSessionId, daemonId, sessionId, turnId: null, eventName: eventNameForReceipt, toolName: toolName ?? "Bash", matchKey, cwd: observedCwd });
560
+ const payload = { ...hook, session_id: providerSessionId, cwd: observedCwd, tool_name: toolName ?? "Bash", tool_input: rawInput,
559
561
  command, provider: stringValue(observedProfile.provider), model: stringValue(observedProfile.model), reasoning: stringValue(observedProfile.reasoning), mode: stringValue(observedProfile.mode) };
560
562
  const inserted = recordHookEvent(context, {
561
563
  projectId: project.id, protocolId: null, harness: "zcode-acp", providerSessionId, parentSessionId,
562
564
  daemonId, sessionId, agentId: stringValue(hook.agent_id) ?? stringValue(hook.agentId) ?? null, turnId: null,
563
565
  eventName: eventNameForReceipt, toolName: toolName ?? "Bash", status: "observed", payload, eventKey, matchKey,
564
- transcriptPath: stringValue(hook.transcript_path) ?? null, cwd: expectedRoot
566
+ transcriptPath: stringValue(hook.transcript_path) ?? null, cwd: observedCwd
565
567
  });
566
- recordFlowSessionObservation(context, { projectId: project.id, sessionId, cwd: expectedRoot, toolName: toolName ?? "Bash", eventKey });
568
+ recordFlowSessionObservation(context, { projectId: project.id, sessionId, cwd: observedCwd, toolName: toolName ?? "Bash", eventKey });
567
569
  const result = { ok: true, observed: inserted, duplicate: !inserted, event_key: eventKey, harness: "zcode-acp", session: nativeSessionIdentity("zcode-acp", providerSessionId), ...(parentProviderSessionId ? { parent_session: nativeSessionIdentity("zcode-acp", parentProviderSessionId) } : {}), daemon_id: daemonId };
568
570
  return { ...result, hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow", updatedInput: { ...rawInput, command: rewritten } } };
569
571
  };
@@ -602,13 +604,14 @@ export function handleGrokEvent(context, input) {
602
604
  return { ok: true, observed: false, reason: "event_not_participating" };
603
605
  const bootstrapStageStart = lifecycle?.bootstrapStageStart;
604
606
  const expectedRoot = resolveProjectRoot(input.projectRoot);
605
- const matchKey = lifecycle ? lifecycleMatchKey(lifecycle, expectedRoot) : null;
607
+ let matchKey = lifecycle ? lifecycleMatchKey(lifecycle, expectedRoot) : null;
606
608
  if (bootstrapStageStart)
607
609
  registerProject(context, { root: expectedRoot });
608
610
  const project = requireProjectByRoot(context, expectedRoot);
609
611
  const meta = objectRecord(hook._meta);
610
612
  const ddGrok = objectRecord(meta.ddGrok);
611
613
  const observedProfile = objectRecord(ddGrok.observedProfile);
614
+ const observedCwd = stringValue(hook.cwd) && path.isAbsolute(String(hook.cwd)) ? resolveProjectRoot(String(hook.cwd)) : null;
612
615
  const rootProviderSessionId = stringValue(ddGrok.rootProviderSessionId);
613
616
  const providerSessionId = stringValue(hook.session_id) ?? stringValue(hook.sessionId);
614
617
  if (!rootProviderSessionId || !providerSessionId)
@@ -622,23 +625,35 @@ export function handleGrokEvent(context, input) {
622
625
  harness: "grok-acp", providerSessionId, toolUseId: hook.tool_use_id ?? hook.toolUseId ?? null,
623
626
  promptId: hook.prompt_id ?? hook.promptId ?? null, command, turn: hook.turn_id ?? hook.turnId ?? null
624
627
  })).digest("hex");
625
- assertHookEventReplay(context, { projectId: project.id, eventKey, harness: "grok-acp", providerSessionId, parentSessionId, daemonId: daemonId ?? null, sessionId, turnId: stringValue(hook.turn_id) ?? stringValue(hook.turnId) ?? null, eventName: "PreToolUse", toolName: toolName ?? "Bash", matchKey, cwd: expectedRoot });
626
- const payload = {
627
- ...hook, session_id: providerSessionId, agent_id: agentId, cwd: expectedRoot, tool_name: toolName ?? "Bash", tool_input: rawInput,
628
- command, provider: stringValue(observedProfile.provider), model: stringValue(observedProfile.model), reasoning: stringValue(observedProfile.reasoning), mode: stringValue(observedProfile.mode), agent_type: stringValue(hook.subagent_type) ?? stringValue(hook.subagentType)
628
+ const persist = (scope) => {
629
+ if (scope)
630
+ matchKey = lifecycleMatchKey(lifecycleFacts(scope.command), expectedRoot, scope);
631
+ assertHookEventReplay(context, { projectId: project.id, eventKey, harness: "grok-acp", providerSessionId, parentSessionId, daemonId: daemonId ?? null, sessionId, turnId: stringValue(hook.turn_id) ?? stringValue(hook.turnId) ?? null, eventName: "PreToolUse", toolName: toolName ?? "Bash", matchKey, cwd: observedCwd });
632
+ const payload = {
633
+ ...hook, session_id: providerSessionId, agent_id: agentId, cwd: observedCwd, tool_name: toolName ?? "Bash", tool_input: rawInput,
634
+ command, provider: stringValue(observedProfile.provider), model: stringValue(observedProfile.model), reasoning: stringValue(observedProfile.reasoning), mode: stringValue(observedProfile.mode), agent_type: stringValue(hook.subagent_type) ?? stringValue(hook.subagentType)
635
+ };
636
+ const inserted = recordHookEvent(context, {
637
+ projectId: project.id, protocolId: null, harness: "grok-acp", providerSessionId, parentSessionId,
638
+ daemonId: daemonId ?? null, sessionId, agentId: agentId ?? null, turnId: stringValue(hook.turn_id) ?? stringValue(hook.turnId) ?? null,
639
+ eventName: "PreToolUse", toolName: toolName ?? "Bash", status: "observed", payload, eventKey, matchKey, transcriptPath: null, cwd: observedCwd
640
+ });
641
+ recordFlowSessionObservation(context, { projectId: project.id, sessionId, cwd: observedCwd, toolName: toolName ?? "Bash", eventKey });
642
+ return inserted;
629
643
  };
630
- const inserted = recordHookEvent(context, {
631
- projectId: project.id, protocolId: null, harness: "grok-acp", providerSessionId, parentSessionId,
632
- daemonId: daemonId ?? null, sessionId, agentId: agentId ?? null, turnId: stringValue(hook.turn_id) ?? stringValue(hook.turnId) ?? null,
633
- eventName: "PreToolUse", toolName: toolName ?? "Bash", status: "observed", payload, eventKey, matchKey, transcriptPath: null, cwd: expectedRoot
634
- });
635
- recordFlowSessionObservation(context, {
636
- projectId: project.id, sessionId,
637
- cwd: expectedRoot, toolName: toolName ?? "Bash", eventKey
644
+ const result = (inserted) => ({ ok: true, observed: inserted, duplicate: !inserted, event_key: eventKey, session: nativeSessionIdentity("grok-acp", providerSessionId),
645
+ ...(isChild ? { parent_session: nativeSessionIdentity("grok-acp", stringValue(ddGrok.parentProviderSessionId) ?? rootProviderSessionId) } : {}), daemon_id: daemonId ?? null });
646
+ if (!lifecycle)
647
+ return { ...result(context.db.writeTransaction(() => persist())), hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow", updatedInput: { ...rawInput, command: commandWithHookEvent(command, eventKey) } } };
648
+ let inserted = false;
649
+ const receipt = observeLifecycleInvocation(context, {
650
+ command, harness: "grok-acp", projectId: project.id,
651
+ identity: { daemonId: daemonId ?? "", rootSessionId: rootProviderSessionId, sessionId: providerSessionId, parentSessionId, toolCallId: eventKey },
652
+ recordReceipt: scope => { inserted = persist(scope); return eventKey; }
638
653
  });
639
- const result = { ok: true, observed: inserted, duplicate: !inserted, event_key: eventKey, session: nativeSessionIdentity("grok-acp", providerSessionId),
640
- ...(isChild ? { parent_session: nativeSessionIdentity("grok-acp", stringValue(ddGrok.parentProviderSessionId) ?? rootProviderSessionId) } : {}), daemon_id: daemonId ?? null };
641
- return { ...result, hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow", updatedInput: { ...rawInput, command: commandWithHookEvent(command, eventKey) } } };
654
+ if (receipt.invocationId)
655
+ return { ...result(inserted), invocation_id: receipt.invocationId, hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow", updatedInput: { ...rawInput, command: commandWithHookEvent(command, eventKey) } } };
656
+ return { ...result(inserted), hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow", updatedInput: { ...rawInput, command: commandWithHookEvent(command, eventKey) } } };
642
657
  }
643
658
  /** Convert a controlled OpenCode tool hook into the shared lifecycle receipt. */
644
659
  export function handleOpenCodeEvent(context, input) {
@@ -654,6 +669,65 @@ export function handleDroidEvent(context, input) {
654
669
  throw new AppError("unsupported_droid_event_schema", "Unsupported Droid event schema", 1);
655
670
  return handleControlledToolEvent(context, input, event, "droid-cli");
656
671
  }
672
+ /** Stable project identity is not the native Session's execution workspace. */
673
+ function controlledLifecycleCwd(context, projectRoot, facts, providerSessionId, auxiliary) {
674
+ if (facts?.bootstrapStageStart)
675
+ return projectRoot;
676
+ const project = context.db.get("SELECT id FROM projects WHERE root = ?", [projectRoot]);
677
+ if (!project)
678
+ return projectRoot;
679
+ let work;
680
+ if (facts?.workId) {
681
+ try {
682
+ work = resolveWorkReference(context, facts.workId, { projectId: project.id });
683
+ }
684
+ catch (error) {
685
+ if (!(error instanceof AppError) || !["not_found", "ambiguous_work_alias"].includes(error.code))
686
+ throw error;
687
+ }
688
+ }
689
+ const reference = work?.run_id ?? facts?.runId ?? auxiliary?.args.options.get("run")?.[0] ?? auxiliary?.args.positional[2];
690
+ if (!reference)
691
+ return projectRoot;
692
+ const run = context.db.get("SELECT id, run_root, workspace_root FROM runs WHERE project_id = ? AND (id = ? OR short_id = ?)", [project.id, reference, reference]);
693
+ if (!run)
694
+ return projectRoot; // Hook ingress does not validate a mistyped CLI target.
695
+ if (work) {
696
+ // PROTOCOLIZE provisions the feature checkout after its coordinator
697
+ // Session has launched in the stable project root. Its transition Work
698
+ // must keep that physical route until the Stage is complete.
699
+ if (context.db.get("SELECT 1 FROM sqlite_schema WHERE type = 'table' AND name = 'stage_work_memberships'")
700
+ && context.db.get("SELECT 1 FROM stage_work_memberships WHERE project_id = ? AND run_id = ? AND work_id = ? AND stage IN ('specify','protocolize') LIMIT 1", [project.id, run.id, work.work_id]))
701
+ return projectRoot;
702
+ const merge = context.db.get("SELECT target_workspace FROM merge_requests WHERE executor_work_id = ? AND run_id = ?", [work.work_id, run.id]);
703
+ if (merge)
704
+ return resolveProjectRoot(merge.target_workspace);
705
+ if (context.db.get("SELECT 1 FROM sqlite_schema WHERE type = 'table' AND name = 'work_launches'")) {
706
+ const launch = context.db.get("SELECT provider_session_id, profile_json FROM work_launches WHERE work_id = ? AND status IN ('launching', 'start_requested')", [work.work_id]);
707
+ if (launch?.provider_session_id === providerSessionId && run.run_root) {
708
+ const copy = JSON.parse(launch.profile_json).review_copy?.root;
709
+ if (copy) {
710
+ const target = path.resolve(run.run_root, copy);
711
+ const relative = path.relative(run.run_root, target);
712
+ if (relative === ".." || relative.startsWith(`..${path.sep}`))
713
+ throw new AppError("read_only_workspace_unbound", "Review copy escapes its owning RUN", 1);
714
+ return resolveProjectRoot(target);
715
+ }
716
+ }
717
+ }
718
+ }
719
+ if (facts?.stageName === "specify" || facts?.stageName === "protocolize")
720
+ return projectRoot;
721
+ if (facts?.stageName === "merge") {
722
+ const pending = context.db.all("SELECT target_workspace FROM merge_requests WHERE project_id = ? AND run_id = ? AND status IN ('dispatching','active','action_required') ORDER BY updated_at DESC", [project.id, run.id]);
723
+ const targets = [...new Set(pending.map(item => resolveProjectRoot(item.target_workspace)))];
724
+ if (targets.length > 1)
725
+ throw new AppError("merge_workspace_ambiguous", "Current MERGE requests disagree on the native execution workspace", 1, { run_id: run.id, target_workspaces: targets });
726
+ if (targets.length === 1)
727
+ return targets[0];
728
+ }
729
+ return resolveProjectRoot(run.workspace_root);
730
+ }
657
731
  function handleControlledToolEvent(context, input, event, harness) {
658
732
  const agy = harness === "antigravity-cli";
659
733
  const droid = harness === "droid-cli";
@@ -676,9 +750,7 @@ function handleControlledToolEvent(context, input, event, harness) {
676
750
  throw new AppError(`${errorPrefix}_session_identity_invalid`, `${label} event is missing trusted physical identity`, 1);
677
751
  }
678
752
  const expectedRoot = resolveProjectRoot(input.projectRoot);
679
- if (resolveProjectRoot(directory) !== expectedRoot) {
680
- throw new AppError(`${errorPrefix}_directory_mismatch`, `${label} Session directory does not match the controlled workspace`, 1, { expected: expectedRoot, actual: directory });
681
- }
753
+ const observedCwd = resolveProjectRoot(directory);
682
754
  if (droid && nativeParentId === providerSessionId)
683
755
  throw new AppError("droid_session_identity_invalid", "A Droid Session cannot parent itself", 1);
684
756
  const sessionId = storageSessionId(nativeSessionIdentity(harness, providerSessionId));
@@ -690,8 +762,8 @@ function handleControlledToolEvent(context, input, event, harness) {
690
762
  const inserted = recordHookEvent(context, {
691
763
  projectId: project.id, protocolId: null, harness, providerSessionId, parentSessionId,
692
764
  daemonId, sessionId, agentId: stringValue(session.agent_id) ?? null, turnId: stringValue(event.message_id) ?? null,
693
- eventName: "PostToolUse", toolName, status: "observed", payload: { session_id: providerSessionId, cwd: expectedRoot, tool_name: toolName, status: objectRecord(event.outcome).status },
694
- eventKey: baseKey, matchKey: null, transcriptPath: droid ? stringValue(event.transcript_path) ?? null : null, cwd: expectedRoot
765
+ eventName: "PostToolUse", toolName, status: "observed", payload: { session_id: providerSessionId, cwd: observedCwd, tool_name: toolName, status: objectRecord(event.outcome).status },
766
+ eventKey: baseKey, matchKey: null, transcriptPath: droid ? stringValue(event.transcript_path) ?? null : null, cwd: observedCwd
695
767
  });
696
768
  return { ok: true, observed: inserted, duplicate: !inserted, event_key: baseKey, phase, harness, session: nativeSessionIdentity(harness, providerSessionId), ...(nativeParentId ? { parent_session: nativeSessionIdentity(harness, nativeParentId) } : {}) };
697
769
  }
@@ -699,10 +771,15 @@ function handleControlledToolEvent(context, input, event, harness) {
699
771
  return { ok: true, observed: false, reason: command ? "non_bash_tool" : "event_not_participating" };
700
772
  }
701
773
  const lifecycle = lifecycleFacts(command);
702
- if (!lifecycle && !continuationCommand(command))
774
+ const auxiliary = continuationCommand(command);
775
+ if (!lifecycle && !auxiliary)
703
776
  return { ok: true, observed: false, reason: "event_not_participating" };
777
+ const expectedCwd = controlledLifecycleCwd(context, expectedRoot, lifecycle, providerSessionId, auxiliary);
778
+ if (observedCwd !== expectedCwd) {
779
+ throw new AppError(`${errorPrefix}_directory_mismatch`, `${label} ${agy ? "launch route" : "Session directory"} does not match the assigned execution workspace`, 1, { expected: expectedCwd, actual: observedCwd, ...(agy ? { actual_provenance: stringValue(event.cwd_provenance) ?? "unknown" } : {}) });
780
+ }
704
781
  const bootstrapStageStart = lifecycle?.bootstrapStageStart;
705
- const matchKey = lifecycle ? lifecycleMatchKey(lifecycle, expectedRoot) : null;
782
+ let matchKey = lifecycle ? lifecycleMatchKey(lifecycle, expectedRoot) : null;
706
783
  if (bootstrapStageStart)
707
784
  registerProject(context, { root: expectedRoot });
708
785
  const project = requireProjectByRoot(context, expectedRoot);
@@ -713,28 +790,55 @@ function handleControlledToolEvent(context, input, event, harness) {
713
790
  }
714
791
  }
715
792
  const agentId = stringValue(session.agent_id);
716
- assertHookEventReplay(context, { projectId: project.id, eventKey: baseKey, harness, providerSessionId, parentSessionId, daemonId, sessionId, turnId: stringValue(event.message_id) ?? null, eventName: "PreToolUse", toolName, matchKey, cwd: expectedRoot });
717
- const payload = {
718
- session_id: providerSessionId, agent_id: agentId, cwd: expectedRoot, tool_name: toolName, tool_input: rawInput, command,
719
- provider: stringValue(profile.provider), model: stringValue(profile.model), reasoning: stringValue(droid ? profile.reasoning : profile.variant),
720
- mode: stringValue(droid ? profile.mode : profile.agent), agent_type: stringValue(droid ? profile.agent_type : profile.agent)
793
+ const persist = (scope) => {
794
+ if (scope)
795
+ matchKey = lifecycleMatchKey(lifecycleFacts(scope.command), expectedRoot, scope);
796
+ assertHookEventReplay(context, { projectId: project.id, eventKey: baseKey, harness, providerSessionId, parentSessionId, daemonId, sessionId, turnId: stringValue(event.message_id) ?? null, eventName: "PreToolUse", toolName, matchKey, cwd: observedCwd });
797
+ const payload = {
798
+ session_id: providerSessionId, agent_id: agentId, cwd: observedCwd, tool_name: toolName, tool_input: rawInput, command,
799
+ ...(agy ? { cwd_provenance: stringValue(event.cwd_provenance) ?? "unknown" } : {}),
800
+ provider: stringValue(profile.provider), model: stringValue(profile.model), reasoning: stringValue(droid ? profile.reasoning : profile.variant),
801
+ mode: stringValue(droid ? profile.mode : profile.agent), agent_type: stringValue(droid ? profile.agent_type : profile.agent)
802
+ };
803
+ const inserted = recordHookEvent(context, {
804
+ projectId: project.id, protocolId: null, harness, providerSessionId, parentSessionId,
805
+ daemonId, sessionId, agentId: agentId ?? null, turnId: stringValue(event.message_id) ?? null, eventName: "PreToolUse", toolName,
806
+ status: "observed", payload, eventKey: baseKey, matchKey, transcriptPath: droid ? stringValue(event.transcript_path) ?? null : null, cwd: observedCwd
807
+ });
808
+ recordFlowSessionObservation(context, { projectId: project.id, sessionId, cwd: observedCwd, toolName, eventKey: baseKey });
809
+ return inserted;
721
810
  };
722
- const inserted = recordHookEvent(context, {
723
- projectId: project.id, protocolId: null, harness, providerSessionId, parentSessionId,
724
- daemonId, sessionId, agentId: agentId ?? null, turnId: stringValue(event.message_id) ?? null, eventName: "PreToolUse", toolName,
725
- status: "observed", payload, eventKey: baseKey, matchKey, transcriptPath: droid ? stringValue(event.transcript_path) ?? null : null, cwd: expectedRoot
726
- });
727
- recordFlowSessionObservation(context, { projectId: project.id, sessionId, cwd: expectedRoot, toolName, eventKey: baseKey });
728
- const result = { ok: true, observed: inserted, duplicate: !inserted, event_key: baseKey, phase, harness, session: nativeSessionIdentity(harness, providerSessionId), ...(nativeParentId ? { parent_session: nativeSessionIdentity(harness, nativeParentId) } : {}), daemon_id: daemonId };
729
- return agy ? result : { ...result, hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow", updatedInput: { ...rawInput, command: commandWithHookEvent(command, baseKey) } } };
811
+ 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 });
812
+ if (agy && lifecycle) {
813
+ let inserted = false;
814
+ const receipt = observeLifecycleInvocation(context, {
815
+ command, harness, projectId: project.id, identity: { daemonId, rootSessionId: nativeParentId ?? providerSessionId, sessionId: providerSessionId, parentSessionId: nativeParentId ?? null, toolCallId: toolCallId },
816
+ recordReceipt: scope => { inserted = persist(scope); return baseKey; }
817
+ });
818
+ return { ...result(inserted), ...(receipt.invocationId ? { invocation_id: receipt.invocationId } : {}) };
819
+ }
820
+ const inserted = context.db.writeTransaction(() => persist());
821
+ return agy ? result(inserted) : { ...result(inserted), hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow", updatedInput: { ...rawInput, command: commandWithHookEvent(command, baseKey) } } };
730
822
  }
731
823
  /** Convert an Antigravity CLI tool hook into the shared lifecycle receipt. */
732
824
  export function handleAgyEvent(context, input) {
733
825
  const event = parseJsonObject(input.stdin || "{}", "Antigravity tool event");
734
826
  if (event.schema_id !== "dd-flow/agy-tool-event@1")
735
827
  throw new AppError("unsupported_agy_event_schema", "Unsupported Antigravity event schema", 1);
736
- const workspacePaths = Array.isArray(event.workspace_paths) ? event.workspace_paths : [];
737
- const directory = workspacePaths.find((value) => typeof value === "string" && path.isAbsolute(value));
828
+ // workspacePaths lists available workspaces; its first element is not a
829
+ // native selected-cwd observation. The daemon's spawn cwd is a separate,
830
+ // launch-bound route, and the list only corroborates its membership.
831
+ const workspacePaths = Array.isArray(event.workspace_paths)
832
+ ? event.workspace_paths.filter((value) => typeof value === "string" && path.isAbsolute(value)) : [];
833
+ const launchCwd = stringValue(event.launch_cwd);
834
+ const qualifiedLaunchCwd = launchCwd && path.isAbsolute(launchCwd) ? launchCwd : undefined;
835
+ const directory = qualifiedLaunchCwd ?? (workspacePaths.length === 1 ? workspacePaths[0] : undefined);
836
+ if (!directory && workspacePaths.length > 1)
837
+ throw new AppError("agy_workspace_route_unproven", "Antigravity did not identify the active route among multiple workspaces", 1, { workspace_paths: workspacePaths });
838
+ if (directory && qualifiedLaunchCwd && workspacePaths.length
839
+ && !workspacePaths.some(value => resolveProjectRoot(value) === resolveProjectRoot(directory))) {
840
+ throw new AppError("agy_workspace_route_unproven", "Antigravity launch cwd is absent from the native workspace list", 1, { launch_cwd: directory, workspace_paths: workspacePaths });
841
+ }
738
842
  const providerSessionId = stringValue(event.conversation_id);
739
843
  const daemonId = stringValue(event.daemon_id);
740
844
  const eventId = stringValue(event.event_id);
@@ -743,6 +847,9 @@ export function handleAgyEvent(context, input) {
743
847
  throw new AppError("agy_session_identity_invalid", "Antigravity event is missing trusted physical identity", 1);
744
848
  const rawInput = objectRecord(event.input);
745
849
  const command = stringValue(rawInput.command) ?? stringValue(rawInput.cmd) ?? stringValue(rawInput.CommandLine);
850
+ if (command && (lifecycleFacts(command) || continuationCommand(command)) && !qualifiedLaunchCwd) {
851
+ throw new AppError("agy_workspace_route_unproven", "Managed Antigravity call has no launch-bound execution cwd", 1, { phase: "prepare", effect: "no_effect", recoverable: false, workspace_paths: workspacePaths });
852
+ }
746
853
  const invalidStep = !Number.isSafeInteger(event.step_index) || Number(event.step_index) < 0;
747
854
  const invalidExecution = event.execution_num !== undefined && (!Number.isSafeInteger(event.execution_num) || Number(event.execution_num) < 0);
748
855
  if (event.phase === "before" && command && (lifecycleFacts(command) || continuationCommand(command)) && (invalidStep || invalidExecution)) {
@@ -753,6 +860,7 @@ export function handleAgyEvent(context, input) {
753
860
  daemon_id: daemonId, tool_call_id: eventId, tool: toolName, input: event.input,
754
861
  message_id: event.step_index === undefined ? null : String(event.step_index),
755
862
  session: { provider_session_id: providerSessionId, parent_provider_session_id: event.parent_conversation_id, directory },
863
+ cwd_provenance: qualifiedLaunchCwd ? "launch_bound" : "workspace_paths_singleton",
756
864
  profile: { provider: objectRecord(event.profile).provider, model: objectRecord(event.profile).model, variant: objectRecord(event.profile).reasoning, agent: objectRecord(event.profile).mode },
757
865
  outcome: { status: stringValue(event.error) ? "error" : "completed" }
758
866
  };
@@ -23,14 +23,13 @@ export function prepareLaneLock(context, input, action) {
23
23
  const { timeoutSeconds, pollIntervalSeconds } = prepareLaneWaitTiming(input);
24
24
  const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
25
25
  const lane = normalizeLane(input.lane);
26
- if (input.workspacePath !== undefined)
27
- requireMatchingLaneWorkspace(context, project.id, lane, input.workspacePath);
28
- else if (["acquire", "wait", "wait-acquire"].includes(action))
26
+ const workspace = input.workspacePath !== undefined ? requireMatchingLaneWorkspace(context, project.id, lane, input.workspacePath) : undefined;
27
+ if (input.workspacePath === undefined && ["acquire", "wait", "wait-acquire"].includes(action))
29
28
  throw new AppError("validation", "Lane lock requires a workspace path", 2);
30
29
  if (action === "acquire" || action === "wait-acquire")
31
30
  assertWorkerMayAcquireLaneLock(context, project.id, lane, input.workerId);
32
31
  const lock = action === "heartbeat" || action === "release" ? requireOwnedActiveLock(context, project.id, lane, input.workerId, input.leaseToken) : undefined;
33
- return { project, lane, ttlSeconds, timeoutSeconds, pollIntervalSeconds, lock };
32
+ return { project, lane, ttlSeconds, timeoutSeconds, pollIntervalSeconds, lock, workspace };
34
33
  }
35
34
  export function getLaneStatus(context, input) {
36
35
  const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
@@ -99,7 +98,12 @@ export function checkLaneWorkspace(context, input) {
99
98
  };
100
99
  }
101
100
  export function acquireLaneLock(context, input) {
102
- const { project, lane, ttlSeconds } = prepareLaneLock(context, input, "acquire");
101
+ const prepared = prepareLaneLock(context, input, "acquire");
102
+ return context.db.writeTransaction(() => acquirePreparedLaneLock(context, input, prepared));
103
+ }
104
+ function acquirePreparedLaneLock(context, input, prepared) {
105
+ const { project, lane, ttlSeconds } = prepared;
106
+ assertPreparedLaneLock(context, prepared, input.workerId);
103
107
  expireStaleLocks(context, project.id);
104
108
  const active = activeLaneLock(context, project.id, lane);
105
109
  if (active) {
@@ -141,6 +145,12 @@ export function acquireLaneLock(context, input) {
141
145
  ...(expired ? { expired_lock_id: expired.id } : {})
142
146
  };
143
147
  }
148
+ function assertPreparedLaneLock(context, prepared, workerId) {
149
+ if (JSON.stringify(laneRecord(context, prepared.project.id, prepared.lane)) !== JSON.stringify(prepared.workspace)
150
+ || context.db.get("SELECT root FROM projects WHERE id = ?", [prepared.project.id])?.root !== prepared.project.root)
151
+ throw new AppError("lane_workspace_changed", "Lane registration changed after workspace preparation", 1);
152
+ assertWorkerMayAcquireLaneLock(context, prepared.project.id, prepared.lane, workerId);
153
+ }
144
154
  export function heartbeatLaneLock(context, input) {
145
155
  const { project, lock: preparedLock, ttlSeconds } = prepareLaneLock(context, input, "heartbeat");
146
156
  const lock = preparedLock;
@@ -524,13 +534,15 @@ function ensureQueuedLaneWaiter(context, input) {
524
534
  return waiter;
525
535
  }
526
536
  function tryAcquireQueuedLaneLock(context, input) {
527
- context.db.beginWriteTransaction();
528
- try {
537
+ const prepared = prepareLaneLock(context, input, "wait-acquire");
538
+ if (prepared.project.id !== input.projectId || prepared.lane !== input.lane)
539
+ throw new AppError("lane_workspace_changed", "Lane identity changed while waiting", 1);
540
+ return context.db.writeTransaction(() => {
541
+ assertPreparedLaneLock(context, prepared, input.workerId);
529
542
  expireStaleLocks(context, input.projectId);
530
543
  expireStaleWaiters(context, input.projectId);
531
544
  const existingLock = activeLaneLock(context, input.projectId, input.lane);
532
545
  if (existingLock?.worker_id === input.workerId) {
533
- context.db.exec("COMMIT");
534
546
  return {
535
547
  acquired: true,
536
548
  payload: {
@@ -548,7 +560,6 @@ function tryAcquireQueuedLaneLock(context, input) {
548
560
  }
549
561
  const latest = latestLaneWaiterForWorker(context, input.projectId, input.lane, input.workerId);
550
562
  if (latest && ["cancelled", "expired"].includes(latest.status) && latest.updated_at >= input.invocationStartedAt) {
551
- context.db.exec("COMMIT");
552
563
  return {
553
564
  acquired: false,
554
565
  payload: {
@@ -572,14 +583,14 @@ function tryAcquireQueuedLaneLock(context, input) {
572
583
  });
573
584
  const head = firstQueuedLaneWaiter(context, input.projectId, input.lane);
574
585
  if (!existingLock && head?.id === waiter.id) {
575
- const lockResult = acquireLaneLock(context, {
586
+ const lockResult = acquirePreparedLaneLock(context, {
576
587
  projectRoot: input.projectRoot,
577
588
  lane: input.lane,
578
589
  workerId: input.workerId,
579
590
  workspacePath: input.workspacePath,
580
591
  ttlSeconds: input.ttlSeconds,
581
592
  reason: input.reason
582
- });
593
+ }, prepared);
583
594
  const now = context.now();
584
595
  context.db.run(`UPDATE lane_waiters
585
596
  SET status = 'acquired', acquired_at = ?, updated_at = ?
@@ -591,7 +602,6 @@ function tryAcquireQueuedLaneLock(context, input) {
591
602
  reason: input.reason,
592
603
  payload: { project_id: input.projectId, lane: input.lane, worker_id: input.workerId, waiter_id: waiter.id }
593
604
  });
594
- context.db.exec("COMMIT");
595
605
  return {
596
606
  acquired: true,
597
607
  payload: {
@@ -608,7 +618,6 @@ function tryAcquireQueuedLaneLock(context, input) {
608
618
  };
609
619
  }
610
620
  const position = queuedWaiterPosition(context, input.projectId, input.lane, waiter.id);
611
- context.db.exec("COMMIT");
612
621
  return {
613
622
  acquired: false,
614
623
  payload: {
@@ -622,11 +631,7 @@ function tryAcquireQueuedLaneLock(context, input) {
622
631
  position
623
632
  }
624
633
  };
625
- }
626
- catch (error) {
627
- context.db.exec("ROLLBACK");
628
- throw error;
629
- }
634
+ });
630
635
  }
631
636
  function resolveExistingPath(value) {
632
637
  return resolveProjectRoot(value);
@@ -160,8 +160,16 @@ function invocationFromSegment(segment, heredoc, wrapped, inheritedEnv) {
160
160
  index += 1;
161
161
  }
162
162
  }
163
- const executable = segment.words[index];
164
- if (!executable || path.basename(executable) !== "dd-flow" && executable !== "$DD_FLOW_BIN")
163
+ let executable = segment.words[index];
164
+ let nodeLauncher = false;
165
+ let launcher;
166
+ if (executable && path.basename(executable) === "node"
167
+ && (path.basename(segment.words[index + 1] ?? "") === "cli.js" || segment.words[index + 1] === "$DD_FLOW_BIN")) {
168
+ launcher = executable;
169
+ executable = segment.words[++index];
170
+ nodeLauncher = true;
171
+ }
172
+ if (!executable || path.basename(executable) !== "dd-flow" && executable !== "$DD_FLOW_BIN" && !(nodeLauncher && path.basename(executable) === "cli.js"))
165
173
  return null;
166
174
  const argv = segment.words.slice(index + 1);
167
175
  const operation = lifecycleOperation(argv);
@@ -170,6 +178,7 @@ function invocationFromSegment(segment, heredoc, wrapped, inheritedEnv) {
170
178
  return {
171
179
  operation,
172
180
  executable,
181
+ ...(launcher ? { launcher } : {}),
173
182
  argv,
174
183
  env,
175
184
  args: parseCommandArgs(argv.slice(operation === "recovery_accept" ? 3 : 2)),