@stigmer/runner 3.1.2 → 3.1.4

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 (127) hide show
  1. package/dist/.build-fingerprint +1 -1
  2. package/dist/activities/execute-cursor/attachment-resolver.d.ts +1 -1
  3. package/dist/activities/execute-cursor/attachment-resolver.js +1 -1
  4. package/dist/activities/execute-cursor/capture-flow.d.ts +29 -0
  5. package/dist/activities/execute-cursor/capture-flow.js +59 -0
  6. package/dist/activities/execute-cursor/capture-flow.js.map +1 -1
  7. package/dist/activities/execute-cursor/command-provenance.d.ts +11 -25
  8. package/dist/activities/execute-cursor/command-provenance.js +25 -115
  9. package/dist/activities/execute-cursor/command-provenance.js.map +1 -1
  10. package/dist/activities/execute-cursor/index.js +358 -557
  11. package/dist/activities/execute-cursor/index.js.map +1 -1
  12. package/dist/activities/execute-cursor/todo-tracker.d.ts +6 -1
  13. package/dist/activities/execute-cursor/todo-tracker.js +15 -43
  14. package/dist/activities/execute-cursor/todo-tracker.js.map +1 -1
  15. package/dist/activities/execute-cursor/turn-boundary.d.ts +108 -0
  16. package/dist/activities/execute-cursor/turn-boundary.js +163 -0
  17. package/dist/activities/execute-cursor/turn-boundary.js.map +1 -0
  18. package/dist/activities/execute-cursor/turn-stream.d.ts +141 -0
  19. package/dist/activities/execute-cursor/turn-stream.js +249 -0
  20. package/dist/activities/execute-cursor/turn-stream.js.map +1 -0
  21. package/dist/activities/execute-deep-agent/command-provenance.d.ts +61 -0
  22. package/dist/activities/execute-deep-agent/command-provenance.js +72 -0
  23. package/dist/activities/execute-deep-agent/command-provenance.js.map +1 -0
  24. package/dist/activities/execute-deep-agent/index.js +73 -4
  25. package/dist/activities/execute-deep-agent/index.js.map +1 -1
  26. package/dist/activities/execute-deep-agent/status-builder.js +8 -1
  27. package/dist/activities/execute-deep-agent/status-builder.js.map +1 -1
  28. package/dist/activities/execute-deep-agent/streaming-v3.js +6 -1
  29. package/dist/activities/execute-deep-agent/streaming-v3.js.map +1 -1
  30. package/dist/activities/execute-deep-agent/streaming.d.ts +7 -0
  31. package/dist/activities/execute-deep-agent/streaming.js +5 -1
  32. package/dist/activities/execute-deep-agent/streaming.js.map +1 -1
  33. package/dist/activities/execute-deep-agent/v3-status-builder.js +12 -1
  34. package/dist/activities/execute-deep-agent/v3-status-builder.js.map +1 -1
  35. package/dist/otel.js +10 -0
  36. package/dist/otel.js.map +1 -1
  37. package/dist/shared/filereview/capture.d.ts +18 -1
  38. package/dist/shared/filereview/capture.js +105 -28
  39. package/dist/shared/filereview/capture.js.map +1 -1
  40. package/dist/shared/filereview/cas-progress.d.ts +63 -0
  41. package/dist/shared/filereview/cas-progress.js +128 -0
  42. package/dist/shared/filereview/cas-progress.js.map +1 -0
  43. package/dist/shared/filereview/cas-substrate.d.ts +38 -0
  44. package/dist/shared/filereview/cas-substrate.js +48 -15
  45. package/dist/shared/filereview/cas-substrate.js.map +1 -1
  46. package/dist/shared/filereview/command-provenance.d.ts +93 -0
  47. package/dist/shared/filereview/command-provenance.js +132 -0
  48. package/dist/shared/filereview/command-provenance.js.map +1 -0
  49. package/dist/shared/filereview/events.d.ts +16 -0
  50. package/dist/shared/filereview/events.js +37 -0
  51. package/dist/shared/filereview/events.js.map +1 -1
  52. package/dist/shared/filereview/git-substrate.d.ts +48 -0
  53. package/dist/shared/filereview/git-substrate.js +78 -0
  54. package/dist/shared/filereview/git-substrate.js.map +1 -1
  55. package/dist/shared/filereview/index.d.ts +5 -2
  56. package/dist/shared/filereview/index.js +5 -2
  57. package/dist/shared/filereview/index.js.map +1 -1
  58. package/dist/shared/filereview/line-counts.d.ts +48 -0
  59. package/dist/shared/filereview/line-counts.js +72 -0
  60. package/dist/shared/filereview/line-counts.js.map +1 -0
  61. package/dist/shared/filereview/progress.d.ts +167 -0
  62. package/dist/shared/filereview/progress.js +196 -0
  63. package/dist/shared/filereview/progress.js.map +1 -0
  64. package/dist/shared/implement-plan-prompt.d.ts +6 -4
  65. package/dist/shared/implement-plan-prompt.js +38 -6
  66. package/dist/shared/implement-plan-prompt.js.map +1 -1
  67. package/dist/shared/plan-artifact.d.ts +59 -11
  68. package/dist/shared/plan-artifact.js +144 -16
  69. package/dist/shared/plan-artifact.js.map +1 -1
  70. package/dist/shared/plan-mode-prompt.d.ts +15 -4
  71. package/dist/shared/plan-mode-prompt.js +28 -5
  72. package/dist/shared/plan-mode-prompt.js.map +1 -1
  73. package/dist/shared/todos.d.ts +56 -0
  74. package/dist/shared/todos.js +98 -0
  75. package/dist/shared/todos.js.map +1 -0
  76. package/dist/shared/tool-row.d.ts +16 -0
  77. package/dist/shared/tool-row.js +31 -0
  78. package/dist/shared/tool-row.js.map +1 -1
  79. package/package.json +3 -2
  80. package/src/__tests__/otel-turn-span.test.ts +61 -0
  81. package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +7 -0
  82. package/src/activities/execute-cursor/__tests__/delta-enricher.test.ts +68 -0
  83. package/src/activities/execute-cursor/__tests__/progress-substrate.test.ts +169 -0
  84. package/src/activities/execute-cursor/__tests__/turn-boundary.test.ts +280 -0
  85. package/src/activities/execute-cursor/__tests__/turn-stream.test.ts +349 -0
  86. package/src/activities/execute-cursor/attachment-resolver.ts +1 -1
  87. package/src/activities/execute-cursor/capture-flow.ts +78 -0
  88. package/src/activities/execute-cursor/command-provenance.ts +25 -120
  89. package/src/activities/execute-cursor/index.ts +419 -602
  90. package/src/activities/execute-cursor/todo-tracker.ts +17 -59
  91. package/src/activities/execute-cursor/turn-boundary.ts +274 -0
  92. package/src/activities/execute-cursor/turn-stream.ts +418 -0
  93. package/src/activities/execute-deep-agent/__tests__/command-provenance.test.ts +252 -0
  94. package/src/activities/execute-deep-agent/__tests__/prompt-builder.test.ts +11 -0
  95. package/src/activities/execute-deep-agent/__tests__/status-builder.test.ts +78 -0
  96. package/src/activities/execute-deep-agent/__tests__/v3-status-builder.test.ts +105 -1
  97. package/src/activities/execute-deep-agent/command-provenance.ts +102 -0
  98. package/src/activities/execute-deep-agent/index.ts +91 -4
  99. package/src/activities/execute-deep-agent/status-builder.ts +9 -0
  100. package/src/activities/execute-deep-agent/streaming-v3.ts +6 -1
  101. package/src/activities/execute-deep-agent/streaming.ts +12 -1
  102. package/src/activities/execute-deep-agent/v3-status-builder.ts +13 -0
  103. package/src/otel.ts +8 -0
  104. package/src/shared/__tests__/implement-plan-prompt.test.ts +40 -2
  105. package/src/shared/__tests__/plan-artifact.test.ts +142 -9
  106. package/src/shared/__tests__/todos.test.ts +216 -0
  107. package/src/shared/filereview/__tests__/capture.test.ts +182 -1
  108. package/src/shared/filereview/__tests__/cas-progress.test.ts +228 -0
  109. package/src/shared/filereview/__tests__/cas-substrate.test.ts +100 -0
  110. package/src/shared/filereview/__tests__/command-provenance.test.ts +252 -0
  111. package/src/shared/filereview/__tests__/events.test.ts +105 -0
  112. package/src/shared/filereview/__tests__/line-counts.test.ts +73 -0
  113. package/src/shared/filereview/__tests__/progress.test.ts +338 -0
  114. package/src/shared/filereview/capture.ts +134 -30
  115. package/src/shared/filereview/cas-progress.ts +170 -0
  116. package/src/shared/filereview/cas-substrate.ts +79 -18
  117. package/src/shared/filereview/command-provenance.ts +180 -0
  118. package/src/shared/filereview/events.ts +50 -0
  119. package/src/shared/filereview/git-substrate.ts +114 -0
  120. package/src/shared/filereview/index.ts +36 -1
  121. package/src/shared/filereview/line-counts.ts +83 -0
  122. package/src/shared/filereview/progress.ts +310 -0
  123. package/src/shared/implement-plan-prompt.ts +39 -8
  124. package/src/shared/plan-artifact.ts +152 -16
  125. package/src/shared/plan-mode-prompt.ts +28 -5
  126. package/src/shared/todos.ts +126 -0
  127. package/src/shared/tool-row.ts +34 -0
@@ -10,6 +10,8 @@ import {
10
10
  ExecutionPhase,
11
11
  MessageType,
12
12
  ToolCallStatus,
13
+ ToolKind,
14
+ TodoStatus,
13
15
  } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
14
16
  import { StatusBuilder, type StreamEvent, type ApprovalPolicyProvider } from "../status-builder.js";
15
17
  import type { MergedToolPolicy } from "../../../shared/approval-policy.js";
@@ -1852,4 +1854,80 @@ describe("StatusBuilder", () => {
1852
1854
  expect(status.phase).not.toBe(ExecutionPhase.EXECUTION_WAITING_FOR_APPROVAL);
1853
1855
  });
1854
1856
  });
1857
+
1858
+ // ── Todo extraction (write_todos → status.todos) ─────────────────
1859
+ //
1860
+ // v2 parity with V3StatusBuilder: a completed write_todos projects into
1861
+ // status.todos through the same shared mapper; a failed one does not.
1862
+
1863
+ describe("todo extraction", () => {
1864
+ it("projects a completed write_todos into status.todos", () => {
1865
+ const sb = makeBuilder();
1866
+ sb.processEvent(
1867
+ toolStartEvent("run-1", "write_todos", {
1868
+ todos: [
1869
+ { content: "Step one", status: "in_progress" },
1870
+ { content: "Step two", status: "pending" },
1871
+ ],
1872
+ }),
1873
+ );
1874
+ sb.processEvent(toolEndEvent("run-1", "Todos updated"));
1875
+
1876
+ const todos = sb.currentStatus.todos;
1877
+ expect(Object.keys(todos)).toEqual(["todo-0", "todo-1"]);
1878
+ expect(todos["todo-0"].content).toBe("Step one");
1879
+ expect(todos["todo-0"].status).toBe(TodoStatus.TODO_IN_PROGRESS);
1880
+ expect(todos["todo-1"].status).toBe(TodoStatus.TODO_PENDING);
1881
+ });
1882
+
1883
+ it("keeps the write_todos ToolCall in messages stamped ToolKind.TODO", () => {
1884
+ const sb = makeBuilder();
1885
+ sb.processEvent(
1886
+ toolStartEvent("run-1", "write_todos", {
1887
+ todos: [{ content: "A", status: "pending" }],
1888
+ }),
1889
+ );
1890
+ sb.processEvent(toolEndEvent("run-1", "ok"));
1891
+
1892
+ const tc = sb.currentStatus.messages
1893
+ .flatMap((m) => m.toolCalls)
1894
+ .find((t) => t.name === "write_todos");
1895
+ expect(tc).toBeDefined();
1896
+ expect(tc!.toolKind).toBe(ToolKind.TODO);
1897
+ });
1898
+
1899
+ it("does not project on a failed write_todos (Command never ran)", () => {
1900
+ const sb = makeBuilder();
1901
+ sb.processEvent(
1902
+ toolStartEvent("run-1", "write_todos", {
1903
+ todos: [{ content: "A", status: "pending" }],
1904
+ }),
1905
+ );
1906
+ sb.processEvent(toolEndEvent("run-1", { error: "boom" }));
1907
+ expect(Object.keys(sb.currentStatus.todos)).toHaveLength(0);
1908
+ });
1909
+
1910
+ it("full-replaces the map on a subsequent write_todos", () => {
1911
+ const sb = makeBuilder();
1912
+ sb.processEvent(
1913
+ toolStartEvent("run-1", "write_todos", {
1914
+ todos: [
1915
+ { content: "one", status: "completed" },
1916
+ { content: "two", status: "in_progress" },
1917
+ ],
1918
+ }),
1919
+ );
1920
+ sb.processEvent(toolEndEvent("run-1", "ok"));
1921
+ sb.processEvent(
1922
+ toolStartEvent("run-2", "write_todos", {
1923
+ todos: [{ content: "two", status: "completed" }],
1924
+ }),
1925
+ );
1926
+ sb.processEvent(toolEndEvent("run-2", "ok"));
1927
+
1928
+ const todos = sb.currentStatus.todos;
1929
+ expect(Object.keys(todos)).toEqual(["todo-0"]);
1930
+ expect(todos["todo-0"].status).toBe(TodoStatus.TODO_COMPLETED);
1931
+ });
1932
+ });
1855
1933
  });
@@ -1,7 +1,7 @@
1
1
  import { describe, it, expect, beforeEach } from "vitest";
2
2
  import { create } from "@bufbuild/protobuf";
3
3
  import { AgentExecutionStatusSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
4
- import { ExecutionPhase, MessageType, ToolCallStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
4
+ import { ExecutionPhase, MessageType, ToolCallStatus, ToolKind, TodoStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
5
5
  import { V3StatusBuilder } from "../v3-status-builder.js";
6
6
  import { normalize } from "../v3-protocol-normalizer.js";
7
7
  import type { ApprovalPolicyProvider } from "../status-builder.js";
@@ -504,4 +504,108 @@ describe("V3StatusBuilder", () => {
504
504
  expect(sb.currentStatus.messages).toHaveLength(0);
505
505
  });
506
506
  });
507
+
508
+ // ── Todo extraction (write_todos → status.todos) ─────────────────
509
+ //
510
+ // The native harness emits write_todos; projecting it into status.todos is
511
+ // what lights up the client TodoCard at parity with the Cursor harness.
512
+
513
+ describe("todo extraction", () => {
514
+ it("projects a completed write_todos into status.todos", () => {
515
+ const sb = makeBuilder();
516
+ feedAll(sb, [
517
+ makeMessageStart("run-1"),
518
+ makeTextDelta("run-1", "Planning the build."),
519
+ makeMessageFinish("run-1", { usage: { input_tokens: 10, output_tokens: 5 } }),
520
+ makeToolStarted("todo-1", "write_todos", {
521
+ todos: [
522
+ { content: "Step one", status: "in_progress" },
523
+ { content: "Step two", status: "pending" },
524
+ ],
525
+ }),
526
+ makeToolFinished("todo-1", "Todos updated"),
527
+ ]);
528
+
529
+ const todos = sb.currentStatus.todos;
530
+ expect(Object.keys(todos)).toEqual(["todo-0", "todo-1"]);
531
+ expect(todos["todo-0"].content).toBe("Step one");
532
+ expect(todos["todo-0"].status).toBe(TodoStatus.TODO_IN_PROGRESS);
533
+ expect(todos["todo-1"].content).toBe("Step two");
534
+ expect(todos["todo-1"].status).toBe(TodoStatus.TODO_PENDING);
535
+ expect(sb.forceNextUpdate).toBe(true);
536
+ });
537
+
538
+ it("keeps the write_todos ToolCall in messages stamped ToolKind.TODO", () => {
539
+ const sb = makeBuilder();
540
+ feedAll(sb, [
541
+ makeToolStarted("todo-1", "write_todos", {
542
+ todos: [{ content: "Step one", status: "pending" }],
543
+ }),
544
+ makeToolFinished("todo-1", "Todos updated"),
545
+ ]);
546
+
547
+ const toolCalls = sb.currentStatus.messages.flatMap((m) => m.toolCalls);
548
+ const tc = toolCalls.find((t) => t.name === "write_todos");
549
+ expect(tc).toBeDefined();
550
+ expect(tc!.toolKind).toBe(ToolKind.TODO);
551
+ });
552
+
553
+ it("does not project until the call completes", () => {
554
+ const sb = makeBuilder();
555
+ feedAll(sb, [
556
+ makeToolStarted("todo-1", "write_todos", {
557
+ todos: [{ content: "Step one", status: "pending" }],
558
+ }),
559
+ ]);
560
+ // Tool started but not finished — the state Command has not run yet.
561
+ expect(Object.keys(sb.currentStatus.todos)).toHaveLength(0);
562
+
563
+ feedAll(sb, [makeToolFinished("todo-1", "Todos updated")]);
564
+ expect(Object.keys(sb.currentStatus.todos)).toHaveLength(1);
565
+ });
566
+
567
+ it("full-replaces the map on a subsequent write_todos", () => {
568
+ const sb = makeBuilder();
569
+ feedAll(sb, [
570
+ makeToolStarted("todo-1", "write_todos", {
571
+ todos: [
572
+ { content: "Step one", status: "completed" },
573
+ { content: "Step two", status: "in_progress" },
574
+ ],
575
+ }),
576
+ makeToolFinished("todo-1", "ok"),
577
+ makeToolStarted("todo-2", "write_todos", {
578
+ todos: [{ content: "Step two", status: "completed" }],
579
+ }),
580
+ makeToolFinished("todo-2", "ok"),
581
+ ]);
582
+
583
+ const todos = sb.currentStatus.todos;
584
+ expect(Object.keys(todos)).toEqual(["todo-0"]);
585
+ expect(todos["todo-0"].content).toBe("Step two");
586
+ expect(todos["todo-0"].status).toBe(TodoStatus.TODO_COMPLETED);
587
+ });
588
+
589
+ it("does not project a sub-agent's write_todos into parent status.todos", () => {
590
+ const sb = makeBuilder();
591
+ const subNs = ["tools:task-1", "tools:sub-todo"];
592
+ feedAll(sb, [
593
+ // Register a sub-agent so its namespace routes to the SubAgentTracker.
594
+ makeToolStarted("task-1", "task", {
595
+ subagent_type: "worker",
596
+ description: "delegate",
597
+ }),
598
+ // The sub-agent writes todos — must not leak into the parent map.
599
+ makeToolStarted(
600
+ "sub-todo",
601
+ "write_todos",
602
+ { todos: [{ content: "sub step", status: "pending" }] },
603
+ { namespace: subNs },
604
+ ),
605
+ makeToolFinished("sub-todo", "ok", { namespace: subNs }),
606
+ ]);
607
+
608
+ expect(Object.keys(sb.currentStatus.todos)).toHaveLength(0);
609
+ });
610
+ });
507
611
  });
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Approved-command turn provenance (DD-28) — the DEEP-AGENT (native) harness
3
+ * adapter over the shared qualification rule ({@link qualifyTurnCommandProvenance}).
4
+ *
5
+ * The deep-agent's consent model differs from Cursor's in exactly two ways, and
6
+ * this adapter is precisely those two differences:
7
+ *
8
+ * 1. IDENTITY turn scoping (not positional). Cursor scopes a turn by the message
9
+ * index its stream started at. The deep-agent cannot: an approved command
10
+ * executes IN PLACE at its seeded transcript position (it was proposed in a
11
+ * prior segment, seeded into this segment, and updated to COMPLETED by
12
+ * `StatusBuilder.findResumableSeededToolCall`). So "this turn's calls" are
13
+ * those whose id is absent from a pre-stream {@link collectSettledToolCallIds}
14
+ * snapshot, and "executed" is a COMPLETED status.
15
+ *
16
+ * 2. SAME-ROW direct consent (not a grant token). A gated deep-agent tool pauses
17
+ * on a LangGraph interrupt; SubmitApproval writes `approval_action` onto that
18
+ * same row, and the row keeps its id + `approval_action` when it executes on
19
+ * resume. So the executed command IS its own consent row — cite its own id.
20
+ * (The server's PreserveApprovalFields keeps that `approval_action` on the row
21
+ * across the status write regardless of its new status, so the backend's
22
+ * verification finds it.)
23
+ *
24
+ * A turn that delegated ANY sub-agent fails closed (DD-28 D1: "delegated zero
25
+ * sub-agents"). A sub-agent's writes fold into the parent turn's change set
26
+ * (DD-19) but are not attributable to a consented top-level command. The
27
+ * top-level `task` call (a SUBAGENT kind) already trips the shared rule's
28
+ * `!== SHELL` fail-closed; the explicit sub-agent-novelty guard below also covers
29
+ * the (currently-impossible under the replace-per-turn model) case of a sub-agent
30
+ * that mutates across a turn boundary without a fresh top-level `task` row.
31
+ *
32
+ * (Trust boundary + fail-closed contract: see the shared module.)
33
+ */
34
+
35
+ import {
36
+ ApprovalAction,
37
+ ToolCallStatus,
38
+ } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
39
+ import type { AgentExecutionStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
40
+ import type { TurnCommandProvenance } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/filereview_pb";
41
+ import { qualifyTurnCommandProvenance } from "../../shared/filereview/command-provenance.js";
42
+ import { collectSubAgentToolCallIds } from "../../shared/tool-row.js";
43
+
44
+ export interface DeepAgentCommandProvenanceInputs {
45
+ /** The status at capture time — top-level messages + sub-agent executions. */
46
+ readonly status: AgentExecutionStatus;
47
+ /**
48
+ * The top-level tool-call ids that had already SETTLED before this turn's
49
+ * stream (from {@link collectSettledToolCallIds}). A top-level call whose id is
50
+ * absent is this-turn's; the shared rule then keeps only the COMPLETED ones.
51
+ */
52
+ readonly priorSettledToolCallIds: ReadonlySet<string>;
53
+ /**
54
+ * The sub-agent tool-call ids that existed before this turn's stream (from
55
+ * {@link collectSubAgentToolCallIds}). Any growth means a sub-agent acted this
56
+ * turn → fail closed.
57
+ */
58
+ readonly priorSubAgentToolCallIds: ReadonlySet<string>;
59
+ /** True when the pre-armed spec.auto_approve_all bypassed the gate. */
60
+ readonly globalBypass: boolean;
61
+ }
62
+
63
+ /**
64
+ * Derive the {@link TurnCommandProvenance} for a deep-agent turn, or undefined
65
+ * when it does not qualify. Scopes this turn's top-level calls by id-novelty,
66
+ * resolves consent from the row's own server-authored `approval_action`, fails
67
+ * closed on any sub-agent activity, and delegates the DD-28 rule to
68
+ * {@link qualifyTurnCommandProvenance}.
69
+ */
70
+ export function deriveTurnCommandProvenance(
71
+ inputs: DeepAgentCommandProvenanceInputs,
72
+ ): TurnCommandProvenance | undefined {
73
+ const { status, priorSettledToolCallIds, priorSubAgentToolCallIds, globalBypass } = inputs;
74
+
75
+ // DD-28 D1 cond 2: any sub-agent activity this turn disqualifies. A sub-agent
76
+ // that ran this turn contributes at least one tool-call id absent from the
77
+ // pre-stream snapshot.
78
+ for (const id of collectSubAgentToolCallIds(status.subAgentExecutions)) {
79
+ if (!priorSubAgentToolCallIds.has(id)) return undefined;
80
+ }
81
+
82
+ const messages = status.messages;
83
+ // This turn's top-level calls: those not already settled before the stream.
84
+ const turnToolCalls = messages
85
+ .flatMap((m) => m.toolCalls)
86
+ .filter((tc) => !priorSettledToolCallIds.has(tc.id));
87
+
88
+ return qualifyTurnCommandProvenance({
89
+ turnToolCalls,
90
+ messages,
91
+ // A deep-agent command executed iff its row reached COMPLETED this turn.
92
+ isExecutedCommand: (tc) => tc.status === ToolCallStatus.TOOL_CALL_COMPLETED,
93
+ // The gated command carries its own server-authored approval_action, written
94
+ // in place by SubmitApproval and preserved when the row executes on resume.
95
+ resolveDirectConsent: (tc) =>
96
+ tc.approvalAction === ApprovalAction.APPROVE ||
97
+ tc.approvalAction === ApprovalAction.APPROVE_ALL
98
+ ? tc.id
99
+ : undefined,
100
+ globalBypass,
101
+ });
102
+ }
@@ -50,12 +50,28 @@ import {
50
50
  captureBaselineToLedger,
51
51
  captureCandidateToLedger,
52
52
  } from "../../shared/filereview/capture.js";
53
+ import {
54
+ captureFileChangeProgress,
55
+ createGitProgressSubstrate,
56
+ createHybridProgressSubstrate,
57
+ newProgressCaptureState,
58
+ type ProgressSubstrate,
59
+ } from "../../shared/filereview/progress.js";
60
+ import {
61
+ createCasProgressSubstrate,
62
+ type CasTouchedSnapshot,
63
+ } from "../../shared/filereview/cas-progress.js";
53
64
  import { hasCandidateCaptured } from "../../shared/filereview/events.js";
54
65
  import { casBlobReader, type CasPathCapture } from "../../shared/filereview/cas-substrate.js";
55
66
  import { partitionIgnoredPathsBySecret } from "../../shared/filereview/secret-paths.js";
56
67
  import type { CasCaptureObserver } from "./cas-capture-observer.js";
57
- import { collectSubAgentToolCallIds, withholdSecretContentFromMessages } from "../../shared/tool-row.js";
68
+ import {
69
+ collectSettledToolCallIds,
70
+ collectSubAgentToolCallIds,
71
+ withholdSecretContentFromMessages,
72
+ } from "../../shared/tool-row.js";
58
73
  import { stampFlowedFileEditRows, stampFlowedSubAgentFileEditRows } from "./stamp-flowed-rows.js";
74
+ import { deriveTurnCommandProvenance } from "./command-provenance.js";
59
75
 
60
76
  /** The harness id stamped on the deep-agent's file-review ledger events. */
61
77
  const DEEP_AGENT_HARNESS_ID = "deep-agent";
@@ -308,6 +324,12 @@ export function createDeepAgentActivities(config: Config) {
308
324
  // seeds prior sub-agents — but computed, not assumed, so it stays correct
309
325
  // if that ever changes; see collectSubAgentToolCallIds).
310
326
  const priorSubAgentToolCallIds = collectSubAgentToolCallIds(initialStatus.subAgentExecutions);
327
+ // Snapshot the top-level tool-call ids already SETTLED before this turn's
328
+ // stream, so the approved-command provenance (DD-28) scopes itself to THIS
329
+ // turn's executed commands by identity. The deep-agent's approved shell
330
+ // executes in place at its seeded position, so the Cursor positional scope
331
+ // would miss it — see execute-deep-agent/command-provenance.ts.
332
+ const priorSettledToolCallIds = collectSettledToolCallIds(initialStatus.messages);
311
333
  if (setup.captureMode) {
312
334
  captureBaselineTree = await captureBaselineToLedger({
313
335
  status: initialStatus,
@@ -319,6 +341,35 @@ export function createDeepAgentActivities(config: Config) {
319
341
  });
320
342
  }
321
343
 
344
+ // Per-turn state + substrate for mid-run live capture (DD-32 / DD-33). The
345
+ // floor lives in progressState; the substrate is chosen for this turn's
346
+ // workspace shape and owns its own short-circuit cache. An atomic snapshot
347
+ // of the shared observer feeds the CAS/HYBRID substrates — copied
348
+ // synchronously so a concurrent sub-agent write cannot mutate it mid-read.
349
+ const progressState = newProgressCaptureState();
350
+ const casObserver = setup.casObserver;
351
+ const readObserverTouched = (): CasTouchedSnapshot => ({
352
+ before: new Map(casObserver.before),
353
+ blockedSecretPaths: new Set(casObserver.blockedSecretPaths),
354
+ });
355
+ // Git tree -> hybrid (numstat for tracked + observer for gitignored);
356
+ // non-git -> cas over the observer (no baseline tree needed). Undefined
357
+ // outside capture mode (writes are deny-gated, nothing is captured).
358
+ const progressSubstrate: ProgressSubstrate | undefined = !setup.captureMode
359
+ ? undefined
360
+ : setup.gitWorkspace
361
+ ? captureBaselineTree
362
+ ? createHybridProgressSubstrate(
363
+ createGitProgressSubstrate({
364
+ workspaceRoot: gitRoot,
365
+ executionId,
366
+ baselineTree: captureBaselineTree,
367
+ }),
368
+ createCasProgressSubstrate({ workspaceRoot: gitRoot, read: readObserverTouched }),
369
+ )
370
+ : undefined
371
+ : createCasProgressSubstrate({ workspaceRoot: gitRoot, read: readObserverTouched });
372
+
322
373
  const cancellationSignal = Context.current().cancellationSignal;
323
374
 
324
375
  const result: StreamResult = await streamExecution({
@@ -344,6 +395,21 @@ export function createDeepAgentActivities(config: Config) {
344
395
  globalBypass: setup.globalBypass,
345
396
  },
346
397
  streamVersion: setup.streamVersion,
398
+ // Mid-run live capture (DD-32 / DD-33): attach file_change_progress
399
+ // before each scheduled persist, throttled by the floor inside
400
+ // captureFileChangeProgress. The substrate (git / non-git CAS / hybrid)
401
+ // was chosen for this turn above; deep-agent writes no runner-owned gate
402
+ // files into the tree, so the git slice needs no excludePaths — matching
403
+ // its turn-boundary candidate capture.
404
+ beforePersist: async (status) => {
405
+ if (!progressSubstrate) return;
406
+ await captureFileChangeProgress({
407
+ status,
408
+ changeSetId,
409
+ substrate: progressSubstrate,
410
+ state: progressState,
411
+ });
412
+ },
347
413
  });
348
414
 
349
415
  await processPostStream({
@@ -399,6 +465,25 @@ export function createDeepAgentActivities(config: Config) {
399
465
  gitRoot,
400
466
  casCaptureClass,
401
467
  );
468
+ // Approved-command turn facts (DD-28): when every mutation-capable call
469
+ // this turn was a consented shell command, attach the provenance so the
470
+ // backend can verify the cited consent rows and auto-keep the set
471
+ // instead of arming a second review gate. Fail-closed: any non-qualifying
472
+ // turn attaches nothing and reviews manually exactly as before. Attached
473
+ // only when captureCandidateToLedger actually authors a CANDIDATE.
474
+ const commandProvenance = deriveTurnCommandProvenance({
475
+ status: initialStatus,
476
+ priorSettledToolCallIds,
477
+ priorSubAgentToolCallIds,
478
+ globalBypass: setup.globalBypass,
479
+ });
480
+ if (commandProvenance) {
481
+ console.log(
482
+ `[ExecuteDeepAgent] capture: turn qualifies for approved-command auto-keep ` +
483
+ `(consent rows: ${commandProvenance.consentToolCallIds.join(",") || "(auto_approve_all)"}); ` +
484
+ `attaching provenance to candidate (execution=${executionId})`,
485
+ );
486
+ }
402
487
  await captureCandidateToLedger({
403
488
  status: initialStatus,
404
489
  gitRoot,
@@ -411,6 +496,7 @@ export function createDeepAgentActivities(config: Config) {
411
496
  unreviewablePaths,
412
497
  unreviewableCaptureClass: casCaptureClass,
413
498
  gitWorkspace: setup.gitWorkspace,
499
+ commandProvenance,
414
500
  });
415
501
  // Review is pending iff a CANDIDATE was actually authored (the seam
416
502
  // drops no-op captures), so a turn that only touched-then-unchanged an
@@ -556,9 +642,10 @@ export function createDeepAgentActivities(config: Config) {
556
642
  }
557
643
 
558
644
  // Plan mode: the agent's final message is the plan. Publish it as a
559
- // first-class plan.md artifact so the UI can render a reviewable Plan
560
- // card and a follow-up Implement run can reference it. Read-only mode
561
- // produces no file to auto-publish, so this is the only artifact path.
645
+ // first-class plan artifact (named from the plan's title) so the UI can
646
+ // render a reviewable Plan card and a follow-up Implement run can
647
+ // reference it. Read-only mode produces no file to auto-publish, so
648
+ // this is the only artifact path.
562
649
  // Requires artifact storage; with none (proxy misconfig) the plan text
563
650
  // still lives in the final message — only the reviewable artifact is
564
651
  // skipped (mirrors Cursor's storage-guarded plan publish).
@@ -24,9 +24,11 @@ import {
24
24
  ExecutionPhase,
25
25
  MessageType,
26
26
  ToolCallStatus,
27
+ ToolKind,
27
28
  } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
28
29
  import { resolveApprovalMessage as resolveApprovalMsg } from "../../shared/approval-policy.js";
29
30
  import { classifyTool } from "../../shared/tool-kind.js";
31
+ import { applyTodoUpdate } from "../../shared/todos.js";
30
32
  import { ExecutionState } from "./execution-state.js";
31
33
  import { utcTimestamp } from "../../shared/status.js";
32
34
  import type { ExecutionStatusWriter } from "./execution-status-writer.js";
@@ -348,6 +350,13 @@ export class StatusBuilder {
348
350
  // (see v3 builder note). Truncating here would corrupt image base64.
349
351
  tc.status = ToolCallStatus.TOOL_CALL_COMPLETED;
350
352
  tc.result = extractToolResult(event.data);
353
+
354
+ // Project a completed to-do write into status.todos — success branch only,
355
+ // since a failed write_todos never ran its state-mutating Command. Shares
356
+ // the mapper and rationale documented in V3StatusBuilder.handleToolFinished.
357
+ if (tc.toolKind === ToolKind.TODO) {
358
+ applyTodoUpdate(this.state.proto.todos, tc.args?.todos, { merge: false });
359
+ }
351
360
  }
352
361
 
353
362
  tc.completedAt = utcTimestamp();
@@ -118,10 +118,15 @@ export async function streamExecutionV3(
118
118
  }
119
119
 
120
120
  statusBuilder.syncSubAgentExecutions();
121
+ // Mid-run live capture (DD-32): attach file_change_progress to the live
122
+ // status before it is persisted. Injected so this loop stays ignorant of
123
+ // file-review specifics; a no-op when the hook is absent or nothing changed.
124
+ const statusToPersist = statusBuilder.currentStatus;
125
+ await deps.beforePersist?.(statusToPersist);
121
126
  const signal = await persistStatus(
122
127
  client,
123
128
  executionId,
124
- statusBuilder.currentStatus,
129
+ statusToPersist,
125
130
  { offload, retry: retryOptions },
126
131
  );
127
132
  scheduler.markUpdateSent(eventsProcessed);
@@ -83,6 +83,13 @@ export interface StreamDependencies {
83
83
  readonly approvalProvider?: ApprovalPolicyProvider;
84
84
  /** Streaming protocol version. Defaults to "v2" if unset. */
85
85
  readonly streamVersion?: "v2" | "v3";
86
+ /**
87
+ * Optional hook invoked with the live status just before each scheduled
88
+ * persist. Used by mid-run live capture (DD-32) to attach file_change_progress.
89
+ * Kept as an injected callback (closing over the turn's baseline tree / change
90
+ * set id in index.ts) so this loop stays ignorant of file-review specifics.
91
+ */
92
+ readonly beforePersist?: (status: AgentExecutionStatus) => Promise<void>;
86
93
  }
87
94
 
88
95
  export interface StreamResult {
@@ -189,10 +196,14 @@ async function streamExecutionV2(
189
196
  statusBuilder.clearForceFlag();
190
197
  }
191
198
 
199
+ // Mid-run live capture (DD-32): attach file_change_progress to the live
200
+ // status before persisting (see streaming-v3.ts for the rationale).
201
+ const statusToPersist = statusBuilder.currentStatus;
202
+ await deps.beforePersist?.(statusToPersist);
192
203
  const signal = await persistStatus(
193
204
  client,
194
205
  executionId,
195
- statusBuilder.currentStatus,
206
+ statusToPersist,
196
207
  { offload, retry: retryOptions },
197
208
  );
198
209
  scheduler.markUpdateSent(eventsProcessed);
@@ -23,9 +23,11 @@ import {
23
23
  ExecutionPhase,
24
24
  MessageType,
25
25
  ToolCallStatus,
26
+ ToolKind,
26
27
  } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
27
28
  import { resolveApprovalMessage as resolveApprovalMsg } from "../../shared/approval-policy.js";
28
29
  import { classifyTool } from "../../shared/tool-kind.js";
30
+ import { applyTodoUpdate } from "../../shared/todos.js";
29
31
  import { ExecutionState } from "./execution-state.js";
30
32
  import { utcTimestamp } from "../../shared/status.js";
31
33
  import type { ExecutionStatusWriter } from "./execution-status-writer.js";
@@ -340,6 +342,17 @@ export class V3StatusBuilder implements ExecutionStatusWriter {
340
342
  this.state.toolStartTimes.delete(callId);
341
343
  this.toolArgBuffers.delete(callId);
342
344
 
345
+ // Project a completed to-do write into status.todos. deepagents' write_todos
346
+ // runs its state-mutating Command when the tool node COMPLETES, so we mirror
347
+ // it here (not at tool-start) — a call cancelled before finishing correctly
348
+ // projects nothing. Keyed on the harness-agnostic ToolKind.TODO (stamped at
349
+ // tool-start) and fed by the same shared mapper the Cursor tracker uses;
350
+ // deepagents always full-replaces (no merge field). The tool call itself
351
+ // stays in messages (the client filters ToolKind.TODO from the thread).
352
+ if (tc.toolKind === ToolKind.TODO) {
353
+ applyTodoUpdate(this.state.proto.todos, tc.args?.todos, { merge: false });
354
+ }
355
+
343
356
  this._forceNextUpdate = true;
344
357
  }
345
358
 
package/src/otel.ts CHANGED
@@ -125,12 +125,20 @@ export async function startCursorTurnSpan(attrs: {
125
125
  [ATTR_SESSION_ID]: attrs.sessionId,
126
126
  },
127
127
  });
128
+ // Guard against a double end / a late setTokens: the turn span is ended from
129
+ // the activity's finally (finishTurnTelemetry), and OTel warns when a span is
130
+ // ended twice or mutated after end. Making this a no-op keeps the caller free
131
+ // to end idempotently on every exit path.
132
+ let ended = false;
128
133
  return {
129
134
  setTokens(input: number, output: number) {
135
+ if (ended) return;
130
136
  if (input) span.setAttribute(ATTR_LLM_INPUT_TOKENS, input);
131
137
  if (output) span.setAttribute(ATTR_LLM_OUTPUT_TOKENS, output);
132
138
  },
133
139
  end() {
140
+ if (ended) return;
141
+ ended = true;
134
142
  span.end();
135
143
  },
136
144
  };
@@ -5,7 +5,7 @@ import {
5
5
  } from "../implement-plan-prompt.js";
6
6
 
7
7
  describe("findApprovedPlanPath", () => {
8
- it("finds the plan by its canonical filename among attachment paths", () => {
8
+ it("finds the plan by the legacy filename among attachment paths", () => {
9
9
  expect(
10
10
  findApprovedPlanPath([
11
11
  ".stigmer/inputs/data.csv",
@@ -14,15 +14,28 @@ describe("findApprovedPlanPath", () => {
14
14
  ).toBe(".stigmer/inputs/plan.md");
15
15
  });
16
16
 
17
+ it("finds a title-named <slug>_<id>.plan.md plan", () => {
18
+ expect(
19
+ findApprovedPlanPath([
20
+ ".stigmer/inputs/data.csv",
21
+ ".stigmer/inputs/plan-card-ux-cleanup_a1b2c3d4.plan.md",
22
+ ]),
23
+ ).toBe(".stigmer/inputs/plan-card-ux-cleanup_a1b2c3d4.plan.md");
24
+ });
25
+
17
26
  it("returns undefined when no plan attachment resolved", () => {
18
27
  expect(findApprovedPlanPath([".stigmer/inputs/data.csv"])).toBeUndefined();
19
28
  expect(findApprovedPlanPath([])).toBeUndefined();
20
29
  });
21
30
 
22
- it("matches the basename exactly — a plan-adjacent filename is not the plan", () => {
31
+ it("matches the plan convention only — a plan-adjacent filename is not the plan", () => {
23
32
  expect(
24
33
  findApprovedPlanPath([".stigmer/inputs/my-plan.md.bak"]),
25
34
  ).toBeUndefined();
35
+ // A file merely containing "plan" is not a plan artifact.
36
+ expect(
37
+ findApprovedPlanPath([".stigmer/inputs/myplan.md"]),
38
+ ).toBeUndefined();
26
39
  });
27
40
  });
28
41
 
@@ -45,4 +58,29 @@ describe("buildImplementPlanDirective", () => {
45
58
  expect(directive).toContain("conversation above");
46
59
  expect(directive).not.toContain("plan.md");
47
60
  });
61
+
62
+ // Tier 3 (plan-driven build progress): both variants instruct the agent to
63
+ // derive its to-do list from the plan and keep it current — the agent's todo
64
+ // tool is the single writer of status.todos, so this instruction is the
65
+ // entire plan→progress linkage.
66
+ it("instructs plan-derived progress tracking in the attached variant", () => {
67
+ const directive = buildImplementPlanDirective(".stigmer/inputs/plan.md");
68
+
69
+ expect(directive).toContain("to-do list");
70
+ expect(directive).toContain("break the plan into");
71
+ expect(directive).toContain("in progress");
72
+ expect(directive).toContain("completed");
73
+ });
74
+
75
+ it("instructs plan-derived progress tracking in the conversation-only variant", () => {
76
+ const directive = buildImplementPlanDirective(undefined);
77
+
78
+ expect(directive).toContain("to-do list");
79
+ expect(directive).toContain("break the plan into");
80
+ expect(directive).toContain("in progress");
81
+ expect(directive).toContain("completed");
82
+ // The progress block must never name the file — this variant exists
83
+ // because no plan file materialized.
84
+ expect(directive).not.toContain("plan.md");
85
+ });
48
86
  });