@stigmer/runner 3.1.3 → 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 (83) hide show
  1. package/dist/.build-fingerprint +1 -1
  2. package/dist/activities/execute-cursor/capture-flow.d.ts +26 -15
  3. package/dist/activities/execute-cursor/capture-flow.js +56 -18
  4. package/dist/activities/execute-cursor/capture-flow.js.map +1 -1
  5. package/dist/activities/execute-cursor/command-provenance.d.ts +11 -25
  6. package/dist/activities/execute-cursor/command-provenance.js +25 -115
  7. package/dist/activities/execute-cursor/command-provenance.js.map +1 -1
  8. package/dist/activities/execute-cursor/index.js +296 -477
  9. package/dist/activities/execute-cursor/index.js.map +1 -1
  10. package/dist/activities/execute-cursor/todo-tracker.d.ts +6 -1
  11. package/dist/activities/execute-cursor/todo-tracker.js +15 -43
  12. package/dist/activities/execute-cursor/todo-tracker.js.map +1 -1
  13. package/dist/activities/execute-cursor/turn-stream.d.ts +141 -0
  14. package/dist/activities/execute-cursor/turn-stream.js +249 -0
  15. package/dist/activities/execute-cursor/turn-stream.js.map +1 -0
  16. package/dist/activities/execute-deep-agent/command-provenance.d.ts +61 -0
  17. package/dist/activities/execute-deep-agent/command-provenance.js +72 -0
  18. package/dist/activities/execute-deep-agent/command-provenance.js.map +1 -0
  19. package/dist/activities/execute-deep-agent/index.js +60 -18
  20. package/dist/activities/execute-deep-agent/index.js.map +1 -1
  21. package/dist/activities/execute-deep-agent/status-builder.js +8 -1
  22. package/dist/activities/execute-deep-agent/status-builder.js.map +1 -1
  23. package/dist/activities/execute-deep-agent/v3-status-builder.js +12 -1
  24. package/dist/activities/execute-deep-agent/v3-status-builder.js.map +1 -1
  25. package/dist/otel.js +10 -0
  26. package/dist/otel.js.map +1 -1
  27. package/dist/shared/filereview/cas-progress.d.ts +63 -0
  28. package/dist/shared/filereview/cas-progress.js +128 -0
  29. package/dist/shared/filereview/cas-progress.js.map +1 -0
  30. package/dist/shared/filereview/cas-substrate.d.ts +29 -0
  31. package/dist/shared/filereview/cas-substrate.js +46 -21
  32. package/dist/shared/filereview/cas-substrate.js.map +1 -1
  33. package/dist/shared/filereview/command-provenance.d.ts +93 -0
  34. package/dist/shared/filereview/command-provenance.js +132 -0
  35. package/dist/shared/filereview/command-provenance.js.map +1 -0
  36. package/dist/shared/filereview/git-substrate.d.ts +9 -3
  37. package/dist/shared/filereview/git-substrate.js.map +1 -1
  38. package/dist/shared/filereview/index.d.ts +4 -3
  39. package/dist/shared/filereview/index.js +3 -2
  40. package/dist/shared/filereview/index.js.map +1 -1
  41. package/dist/shared/filereview/progress.d.ts +105 -34
  42. package/dist/shared/filereview/progress.js +96 -34
  43. package/dist/shared/filereview/progress.js.map +1 -1
  44. package/dist/shared/plan-mode-prompt.d.ts +9 -0
  45. package/dist/shared/plan-mode-prompt.js +18 -0
  46. package/dist/shared/plan-mode-prompt.js.map +1 -1
  47. package/dist/shared/todos.d.ts +56 -0
  48. package/dist/shared/todos.js +98 -0
  49. package/dist/shared/todos.js.map +1 -0
  50. package/dist/shared/tool-row.d.ts +16 -0
  51. package/dist/shared/tool-row.js +31 -0
  52. package/dist/shared/tool-row.js.map +1 -1
  53. package/package.json +2 -2
  54. package/src/__tests__/otel-turn-span.test.ts +61 -0
  55. package/src/activities/execute-cursor/__tests__/progress-substrate.test.ts +169 -0
  56. package/src/activities/execute-cursor/__tests__/turn-stream.test.ts +349 -0
  57. package/src/activities/execute-cursor/capture-flow.ts +71 -25
  58. package/src/activities/execute-cursor/command-provenance.ts +25 -120
  59. package/src/activities/execute-cursor/index.ts +344 -504
  60. package/src/activities/execute-cursor/todo-tracker.ts +17 -59
  61. package/src/activities/execute-cursor/turn-stream.ts +418 -0
  62. package/src/activities/execute-deep-agent/__tests__/command-provenance.test.ts +252 -0
  63. package/src/activities/execute-deep-agent/__tests__/status-builder.test.ts +78 -0
  64. package/src/activities/execute-deep-agent/__tests__/v3-status-builder.test.ts +105 -1
  65. package/src/activities/execute-deep-agent/command-provenance.ts +102 -0
  66. package/src/activities/execute-deep-agent/index.ts +74 -18
  67. package/src/activities/execute-deep-agent/status-builder.ts +9 -0
  68. package/src/activities/execute-deep-agent/v3-status-builder.ts +13 -0
  69. package/src/otel.ts +8 -0
  70. package/src/shared/__tests__/todos.test.ts +216 -0
  71. package/src/shared/filereview/__tests__/cas-progress.test.ts +228 -0
  72. package/src/shared/filereview/__tests__/cas-substrate.test.ts +66 -0
  73. package/src/shared/filereview/__tests__/command-provenance.test.ts +252 -0
  74. package/src/shared/filereview/__tests__/progress.test.ts +112 -10
  75. package/src/shared/filereview/cas-progress.ts +170 -0
  76. package/src/shared/filereview/cas-substrate.ts +69 -24
  77. package/src/shared/filereview/command-provenance.ts +180 -0
  78. package/src/shared/filereview/git-substrate.ts +9 -3
  79. package/src/shared/filereview/index.ts +15 -1
  80. package/src/shared/filereview/progress.ts +171 -47
  81. package/src/shared/plan-mode-prompt.ts +18 -0
  82. package/src/shared/todos.ts +126 -0
  83. package/src/shared/tool-row.ts +34 -0
@@ -0,0 +1,249 @@
1
+ /**
2
+ * The Cursor harness's stream phase — the single per-event consumption loop that
3
+ * turns a live Cursor SDK run into the streamed transcript, mid-run progress, and
4
+ * the flags that decide how the turn ends.
5
+ *
6
+ * Extracted from the activity entry point (index.ts) so that BOTH the primary
7
+ * turn and the two recovery retries (poisoned-handle / transport-timeout) drive
8
+ * the exact same code. The retries previously re-implemented a stripped "bare"
9
+ * loop that dropped live persist, DD-32/DD-33 mid-run progress, sub-agent
10
+ * tracking, the first-denial early stop, and correct pause/stall handling — so a
11
+ * retry froze the UI and mis-handled a mid-retry pause. One loop removes that
12
+ * drift by construction.
13
+ *
14
+ * This mirrors the deep-agent harness's `streamExecution` seam
15
+ * (execute-deep-agent/streaming.ts): injected `heartbeat`/`isCancelled` so it
16
+ * runs without Temporal, a structural `StreamableRun` so it runs without the live
17
+ * SDK, and a result (the terminal reason) instead of internal terminal mapping.
18
+ * The one deliberate difference: this consumer REPORTS why the stream ended and
19
+ * leaves the terminal MAPPING to the activity, because Cursor's mapping is
20
+ * entangled with worker-shutdown-vs-pause disambiguation, the turn boundary,
21
+ * structured output, and recovery retries — all of which must stay in index.ts.
22
+ */
23
+ import { create } from "@bufbuild/protobuf";
24
+ import { CancelledFailure } from "@temporalio/activity";
25
+ import { ExecutionControlSignal } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
26
+ import { StreamingUsageSummarySchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/usage_pb";
27
+ import { startStallWatchdog, StallTimeoutError, } from "../../shared/stall-watchdog.js";
28
+ import { shouldPersistStreamingStatus } from "./persist-decision.js";
29
+ import { readDenialLedger } from "./approval-state.js";
30
+ import { captureFileChangeProgress, } from "../../shared/filereview/progress.js";
31
+ export function newTurnStreamState() {
32
+ return {
33
+ pauseDetected: false,
34
+ stallDetected: false,
35
+ stallError: undefined,
36
+ firstDenialDetected: false,
37
+ denialLedgerDirty: false,
38
+ denialCancelSettled: undefined,
39
+ platformStopSignaled: false,
40
+ streamErrorMessage: undefined,
41
+ lastToolName: undefined,
42
+ eventCount: 0,
43
+ firstTurnAttributionLogged: false,
44
+ stallWatchdog: undefined,
45
+ };
46
+ }
47
+ /**
48
+ * Build the shared onDelta callback. The Cursor SDK's fine-grained delta channel
49
+ * carries token usage, live shell output, and precise tool-call timings; it also
50
+ * fires far more often than discrete stream events, so it is where the stall
51
+ * timer is reset during a long model generation. A heartbeat CancelledFailure
52
+ * here flags the pause so the loop and epilogue treat it as a user pause (the
53
+ * bare retry loops swallowed this — the cause of the mid-retry mislabel).
54
+ */
55
+ export function makeCursorTurnOnDelta(deps) {
56
+ const { usageAccumulator, deltaEnricher, heartbeat, promptEstimatedTokens, executionId, state } = deps;
57
+ return ({ update }) => {
58
+ // Reset the stall timer on the delta channel too: a long model generation
59
+ // emits token deltas but few discrete stream events, so resetting only in the
60
+ // stream loop would false-positive a stall.
61
+ state.stallWatchdog?.recordActivity();
62
+ if (update.type === "turn-ended" && update.usage) {
63
+ usageAccumulator.addTurn(update.usage);
64
+ if (!state.firstTurnAttributionLogged) {
65
+ state.firstTurnAttributionLogged = true;
66
+ const sdkInputTokens = update.usage.inputTokens ?? 0;
67
+ const cursorOverhead = Math.max(0, sdkInputTokens - promptEstimatedTokens);
68
+ console.log(`ExecuteCursor context attribution (first turn): execution=${executionId}, ` +
69
+ `sdkInputTokens=${sdkInputTokens}, stigmerPreamble=${promptEstimatedTokens}, ` +
70
+ `cursorOverhead=${cursorOverhead} (estimated)`);
71
+ }
72
+ }
73
+ deltaEnricher.processDelta(update);
74
+ try {
75
+ heartbeat();
76
+ }
77
+ catch (hbErr) {
78
+ if (hbErr instanceof CancelledFailure) {
79
+ state.pauseDetected = true;
80
+ return;
81
+ }
82
+ throw hbErr;
83
+ }
84
+ };
85
+ }
86
+ /**
87
+ * Consume a Cursor SDK run to completion (or to a clean early stop), driving the
88
+ * transcript, todos, sub-agent tracking, live persist, and DD-32/DD-33 mid-run
89
+ * progress, and reporting WHY the stream ended.
90
+ *
91
+ * Arms + stops its own stall watchdog (stored on `state.stallWatchdog` so the
92
+ * shared onDelta can reset it and the activity's finally can stop it as a
93
+ * backstop). Mutates `deps.status` + `deps.state` in place. The caller owns the
94
+ * post-stream finalize and the terminal mapping.
95
+ */
96
+ export async function consumeCursorTurnStream(run, deps) {
97
+ const { status, accumulator, todoTracker, deltaEnricher, eventRecorder, scheduler, usageAccumulator, progressSubstrate, progressState, changeSetId, hitlDir, executionId, stallTimeoutMs, persist, heartbeat, isCancelled, state, } = deps;
98
+ // Arm the stall watchdog now that the run exists. The activity's periodic
99
+ // heartbeat proves the process is alive, not that the agent is progressing: if
100
+ // the stream wedges (a tool call or model connection that never returns), no
101
+ // event/delta arrives and the turn would hang forever. On stall we end the run
102
+ // cleanly via the SDK's run.cancel() (guarded by supports("cancel")), which
103
+ // unblocks the for-await; the caller then reports EXECUTION_FAILED with a
104
+ // recognizable, actionable message.
105
+ state.stallWatchdog = startStallWatchdog(stallTimeoutMs, (idleMs) => {
106
+ state.stallDetected = true;
107
+ state.stallError = new StallTimeoutError(idleMs, state.lastToolName ? `last tool: ${state.lastToolName}` : undefined);
108
+ console.warn(`ExecuteCursor stall detected: execution=${executionId}, idleMs=${idleMs}, lastTool=${state.lastToolName ?? "none"}`);
109
+ if (run.supports?.("cancel")) {
110
+ void run.cancel().catch((cancelErr) => {
111
+ console.warn(`ExecuteCursor run.cancel() after stall failed (non-fatal): execution=${executionId}, ` +
112
+ `error=${cancelErr instanceof Error ? cancelErr.message : cancelErr}`);
113
+ });
114
+ }
115
+ });
116
+ try {
117
+ for await (const event of run.stream()) {
118
+ if (state.pauseDetected || isCancelled()) {
119
+ state.pauseDetected = true;
120
+ break;
121
+ }
122
+ if (state.stallDetected)
123
+ break;
124
+ // Progress: reset the stall timer on every stream event.
125
+ state.stallWatchdog.recordActivity();
126
+ if (event.type === "tool_call" && typeof event.name === "string") {
127
+ state.lastToolName = event.name;
128
+ }
129
+ eventRecorder?.record(event, state.eventCount);
130
+ accumulator.processEvent(event);
131
+ todoTracker.processEvent(event);
132
+ if (event.type === "tool_call" && event.name === "task") {
133
+ accumulator.trackSubAgentExecution(event);
134
+ }
135
+ // First-denial stop (HITL clean pause). In CAPTURE mode this fires only for
136
+ // an IRREVERSIBLE tool the hook still gates (shell, MCP, or a gitignored
137
+ // write/delete) — file edits flow freely and are captured at the turn
138
+ // boundary, so they never enter the ledger. In the deny-gate FALLBACK
139
+ // (non-git workspace) it fires for every gated file edit too. Either way:
140
+ // the preToolUse hook appends to the denial ledger the instant it gates a
141
+ // tool — before Cursor surfaces the failure to the model — and the fs
142
+ // watcher flips denialLedgerDirty the moment that write lands. Confirming
143
+ // the flag with a read on the very next event (of ANY type — thinking
144
+ // deltas arrive within milliseconds) ends the turn before the model's
145
+ // reaction can persist: waiting for the next tool_call event let the full
146
+ // post-denial reaction (thinking, narration, a workaround shell) stream and
147
+ // persist live (production case aex_01kwj07f7g23c3wp9sn8496z5g). The
148
+ // tool_call-event read stays as the backstop for platforms where fs.watch
149
+ // is unreliable; the current event was already accumulated above, so the
150
+ // anchor's own row is always present for the turn-boundary gate overlay.
151
+ if (!state.firstDenialDetected && hitlDir && (state.denialLedgerDirty || event.type === "tool_call")) {
152
+ state.denialLedgerDirty = false;
153
+ const denials = await readDenialLedger(hitlDir);
154
+ if (denials.length > 0) {
155
+ state.firstDenialDetected = true;
156
+ console.log(`ExecuteCursor first denial detected (${denials.length} ledger ` +
157
+ `entr${denials.length === 1 ? "y" : "ies"}); stopping turn to pause ` +
158
+ `cleanly for approval: execution=${executionId}`);
159
+ if (run.supports?.("cancel")) {
160
+ // Kept (not fire-and-forget): awaited timeboxed by the turn boundary
161
+ // so the ledger read and tree capture see a stopped agent.
162
+ state.denialCancelSettled = run.cancel().then(() => { }, (cancelErr) => {
163
+ console.warn(`ExecuteCursor run.cancel() after first denial failed (non-fatal): ` +
164
+ `execution=${executionId}, ` +
165
+ `error=${cancelErr instanceof Error ? cancelErr.message : cancelErr}`);
166
+ });
167
+ }
168
+ break;
169
+ }
170
+ }
171
+ deltaEnricher.applyEnrichments(status.messages);
172
+ state.eventCount++;
173
+ if (event.type === "status") {
174
+ console.log(`ExecuteCursor stream status: execution=${executionId}, status=${JSON.stringify(event)}`);
175
+ const statusEvent = event;
176
+ if (statusEvent.status === "ERROR" && statusEvent.message) {
177
+ state.streamErrorMessage = statusEvent.message;
178
+ }
179
+ }
180
+ const shouldPersist = shouldPersistStreamingStatus({
181
+ deltaEnricherDirty: deltaEnricher.isDirty,
182
+ todosDirty: todoTracker.isDirty,
183
+ contentDirty: accumulator.isDirty,
184
+ }, scheduler, state.eventCount);
185
+ if (usageAccumulator.hasTurns) {
186
+ status.streamingUsage = create(StreamingUsageSummarySchema, usageAccumulator.snapshot());
187
+ }
188
+ if (shouldPersist) {
189
+ // Sync sub-agent executions into status before every persist so the live
190
+ // UI reflects delegation (including the IN_PROGRESS state) while the
191
+ // parent is still running — matching the native harness.
192
+ status.subAgentExecutions = accumulator.subAgentExecutions;
193
+ // Mid-run live capture (DD-32 / DD-33): attach the "N files changed so
194
+ // far" snapshot onto status.file_change_progress, throttled internally by
195
+ // the floor. Never authoritative — the turn-boundary candidate remains
196
+ // the reviewed diff.
197
+ if (progressSubstrate) {
198
+ await captureFileChangeProgress({
199
+ status,
200
+ changeSetId,
201
+ substrate: progressSubstrate,
202
+ state: progressState,
203
+ });
204
+ }
205
+ const signal = await persist(status);
206
+ deltaEnricher.markPersisted();
207
+ todoTracker.markPersisted();
208
+ accumulator.markPersisted();
209
+ scheduler.markUpdateSent(state.eventCount);
210
+ heartbeat();
211
+ if (signal === ExecutionControlSignal.STOP) {
212
+ state.platformStopSignaled = true;
213
+ console.warn(`ExecuteCursor platform stop signal received: execution=${executionId}`);
214
+ }
215
+ }
216
+ if (state.platformStopSignaled) {
217
+ console.log(`ExecuteCursor stopping stream due to platform stop signal: execution=${executionId}`);
218
+ break;
219
+ }
220
+ }
221
+ }
222
+ catch (streamErr) {
223
+ // run.cancel() — from the stall watchdog or the first-denial stop — can make
224
+ // the stream iterator reject as it tears down; that is the expected teardown
225
+ // for both, so swallow it and fall through. Anything else is a genuine stream
226
+ // failure — rethrow it to the activity's error handler.
227
+ if (!state.stallDetected && !state.firstDenialDetected)
228
+ throw streamErr;
229
+ console.warn(`ExecuteCursor stream ended via cancel: execution=${executionId}, ` +
230
+ `stall=${state.stallDetected}, firstDenial=${state.firstDenialDetected}`);
231
+ }
232
+ finally {
233
+ state.stallWatchdog.stop();
234
+ }
235
+ // Report why the stream ended, in the same precedence the activity's epilogue
236
+ // applies: a stall (an EXECUTION_FAILED terminal) outranks everything; a
237
+ // platform stop and a first denial are distinct proceed/return outcomes; a
238
+ // pause is the fallback for a cancellation with no other cause.
239
+ if (state.stallDetected)
240
+ return "stalled";
241
+ if (state.platformStopSignaled)
242
+ return "platform-stop";
243
+ if (state.firstDenialDetected)
244
+ return "first-denial";
245
+ if (state.pauseDetected || isCancelled())
246
+ return "paused";
247
+ return "completed";
248
+ }
249
+ //# sourceMappingURL=turn-stream.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"turn-stream.js","sourceRoot":"","sources":["../../../src/activities/execute-cursor/turn-stream.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,61 @@
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
+ import type { AgentExecutionStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
35
+ import type { TurnCommandProvenance } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/filereview_pb";
36
+ export interface DeepAgentCommandProvenanceInputs {
37
+ /** The status at capture time — top-level messages + sub-agent executions. */
38
+ readonly status: AgentExecutionStatus;
39
+ /**
40
+ * The top-level tool-call ids that had already SETTLED before this turn's
41
+ * stream (from {@link collectSettledToolCallIds}). A top-level call whose id is
42
+ * absent is this-turn's; the shared rule then keeps only the COMPLETED ones.
43
+ */
44
+ readonly priorSettledToolCallIds: ReadonlySet<string>;
45
+ /**
46
+ * The sub-agent tool-call ids that existed before this turn's stream (from
47
+ * {@link collectSubAgentToolCallIds}). Any growth means a sub-agent acted this
48
+ * turn → fail closed.
49
+ */
50
+ readonly priorSubAgentToolCallIds: ReadonlySet<string>;
51
+ /** True when the pre-armed spec.auto_approve_all bypassed the gate. */
52
+ readonly globalBypass: boolean;
53
+ }
54
+ /**
55
+ * Derive the {@link TurnCommandProvenance} for a deep-agent turn, or undefined
56
+ * when it does not qualify. Scopes this turn's top-level calls by id-novelty,
57
+ * resolves consent from the row's own server-authored `approval_action`, fails
58
+ * closed on any sub-agent activity, and delegates the DD-28 rule to
59
+ * {@link qualifyTurnCommandProvenance}.
60
+ */
61
+ export declare function deriveTurnCommandProvenance(inputs: DeepAgentCommandProvenanceInputs): TurnCommandProvenance | undefined;
@@ -0,0 +1,72 @@
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
+ import { ApprovalAction, ToolCallStatus, } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
35
+ import { qualifyTurnCommandProvenance } from "../../shared/filereview/command-provenance.js";
36
+ import { collectSubAgentToolCallIds } from "../../shared/tool-row.js";
37
+ /**
38
+ * Derive the {@link TurnCommandProvenance} for a deep-agent turn, or undefined
39
+ * when it does not qualify. Scopes this turn's top-level calls by id-novelty,
40
+ * resolves consent from the row's own server-authored `approval_action`, fails
41
+ * closed on any sub-agent activity, and delegates the DD-28 rule to
42
+ * {@link qualifyTurnCommandProvenance}.
43
+ */
44
+ export function deriveTurnCommandProvenance(inputs) {
45
+ const { status, priorSettledToolCallIds, priorSubAgentToolCallIds, globalBypass } = inputs;
46
+ // DD-28 D1 cond 2: any sub-agent activity this turn disqualifies. A sub-agent
47
+ // that ran this turn contributes at least one tool-call id absent from the
48
+ // pre-stream snapshot.
49
+ for (const id of collectSubAgentToolCallIds(status.subAgentExecutions)) {
50
+ if (!priorSubAgentToolCallIds.has(id))
51
+ return undefined;
52
+ }
53
+ const messages = status.messages;
54
+ // This turn's top-level calls: those not already settled before the stream.
55
+ const turnToolCalls = messages
56
+ .flatMap((m) => m.toolCalls)
57
+ .filter((tc) => !priorSettledToolCallIds.has(tc.id));
58
+ return qualifyTurnCommandProvenance({
59
+ turnToolCalls,
60
+ messages,
61
+ // A deep-agent command executed iff its row reached COMPLETED this turn.
62
+ isExecutedCommand: (tc) => tc.status === ToolCallStatus.TOOL_CALL_COMPLETED,
63
+ // The gated command carries its own server-authored approval_action, written
64
+ // in place by SubmitApproval and preserved when the row executes on resume.
65
+ resolveDirectConsent: (tc) => tc.approvalAction === ApprovalAction.APPROVE ||
66
+ tc.approvalAction === ApprovalAction.APPROVE_ALL
67
+ ? tc.id
68
+ : undefined,
69
+ globalBypass,
70
+ });
71
+ }
72
+ //# sourceMappingURL=command-provenance.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"command-provenance.js","sourceRoot":"","sources":["../../../src/activities/execute-deep-agent/command-provenance.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,OAAO,EACL,cAAc,EACd,cAAc,GACf,MAAM,8DAA8D,CAAC;AAGtE,OAAO,EAAE,4BAA4B,EAAE,MAAM,+CAA+C,CAAC;AAC7F,OAAO,EAAE,0BAA0B,EAAE,MAAM,0BAA0B,CAAC;AAqBtE;;;;;;GAMG;AACH,MAAM,UAAU,2BAA2B,CACzC,MAAwC;IAExC,MAAM,EAAE,MAAM,EAAE,uBAAuB,EAAE,wBAAwB,EAAE,YAAY,EAAE,GAAG,MAAM,CAAC;IAE3F,8EAA8E;IAC9E,2EAA2E;IAC3E,uBAAuB;IACvB,KAAK,MAAM,EAAE,IAAI,0BAA0B,CAAC,MAAM,CAAC,kBAAkB,CAAC,EAAE,CAAC;QACvE,IAAI,CAAC,wBAAwB,CAAC,GAAG,CAAC,EAAE,CAAC;YAAE,OAAO,SAAS,CAAC;IAC1D,CAAC;IAED,MAAM,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC;IACjC,4EAA4E;IAC5E,MAAM,aAAa,GAAG,QAAQ;SAC3B,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;SAC3B,MAAM,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,uBAAuB,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;IAEvD,OAAO,4BAA4B,CAAC;QAClC,aAAa;QACb,QAAQ;QACR,yEAAyE;QACzE,iBAAiB,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,MAAM,KAAK,cAAc,CAAC,mBAAmB;QAC3E,6EAA6E;QAC7E,4EAA4E;QAC5E,oBAAoB,EAAE,CAAC,EAAE,EAAE,EAAE,CAC3B,EAAE,CAAC,cAAc,KAAK,cAAc,CAAC,OAAO;YAC5C,EAAE,CAAC,cAAc,KAAK,cAAc,CAAC,WAAW;YAC9C,CAAC,CAAC,EAAE,CAAC,EAAE;YACP,CAAC,CAAC,SAAS;QACf,YAAY;KACb,CAAC,CAAC;AACL,CAAC"}
@@ -33,12 +33,14 @@ import { processPostStream } from "./post-stream.js";
33
33
  import { resolveResumeInput } from "./hitl.js";
34
34
  import { captureApprovalArtifacts } from "./approval-file-change.js";
35
35
  import { applyCaptureDecisions, captureBaselineToLedger, captureCandidateToLedger, } from "../../shared/filereview/capture.js";
36
- import { captureFileChangeProgress, newProgressCaptureState, } from "../../shared/filereview/progress.js";
36
+ import { captureFileChangeProgress, createGitProgressSubstrate, createHybridProgressSubstrate, newProgressCaptureState, } from "../../shared/filereview/progress.js";
37
+ import { createCasProgressSubstrate, } from "../../shared/filereview/cas-progress.js";
37
38
  import { hasCandidateCaptured } from "../../shared/filereview/events.js";
38
39
  import { casBlobReader } from "../../shared/filereview/cas-substrate.js";
39
40
  import { partitionIgnoredPathsBySecret } from "../../shared/filereview/secret-paths.js";
40
- import { collectSubAgentToolCallIds, withholdSecretContentFromMessages } from "../../shared/tool-row.js";
41
+ import { collectSettledToolCallIds, collectSubAgentToolCallIds, withholdSecretContentFromMessages, } from "../../shared/tool-row.js";
41
42
  import { stampFlowedFileEditRows, stampFlowedSubAgentFileEditRows } from "./stamp-flowed-rows.js";
43
+ import { deriveTurnCommandProvenance } from "./command-provenance.js";
42
44
  /** The harness id stamped on the deep-agent's file-review ledger events. */
43
45
  const DEEP_AGENT_HARNESS_ID = "deep-agent";
44
46
  export function createDeepAgentActivities(config) {
@@ -263,6 +265,12 @@ export function createDeepAgentActivities(config) {
263
265
  // seeds prior sub-agents — but computed, not assumed, so it stays correct
264
266
  // if that ever changes; see collectSubAgentToolCallIds).
265
267
  const priorSubAgentToolCallIds = collectSubAgentToolCallIds(initialStatus.subAgentExecutions);
268
+ // Snapshot the top-level tool-call ids already SETTLED before this turn's
269
+ // stream, so the approved-command provenance (DD-28) scopes itself to THIS
270
+ // turn's executed commands by identity. The deep-agent's approved shell
271
+ // executes in place at its seeded position, so the Cursor positional scope
272
+ // would miss it — see execute-deep-agent/command-provenance.ts.
273
+ const priorSettledToolCallIds = collectSettledToolCallIds(initialStatus.messages);
266
274
  if (setup.captureMode) {
267
275
  captureBaselineTree = await captureBaselineToLedger({
268
276
  status: initialStatus,
@@ -273,14 +281,31 @@ export function createDeepAgentActivities(config) {
273
281
  gitWorkspace: setup.gitWorkspace,
274
282
  });
275
283
  }
276
- // Per-turn state for mid-run live capture (DD-32): the last progress tree
277
- // sha (short-circuit) + last capture time (floor), threaded across the
278
- // streaming loop's persists via the beforePersist hook below. The capture
279
- // flags are hoisted so the deferred hook does not depend on `setup`
280
- // (non-null here, but not narrowable inside a later-invoked closure).
284
+ // Per-turn state + substrate for mid-run live capture (DD-32 / DD-33). The
285
+ // floor lives in progressState; the substrate is chosen for this turn's
286
+ // workspace shape and owns its own short-circuit cache. An atomic snapshot
287
+ // of the shared observer feeds the CAS/HYBRID substrates copied
288
+ // synchronously so a concurrent sub-agent write cannot mutate it mid-read.
281
289
  const progressState = newProgressCaptureState();
282
- const progressCaptureMode = setup.captureMode;
283
- const progressGitWorkspace = setup.gitWorkspace;
290
+ const casObserver = setup.casObserver;
291
+ const readObserverTouched = () => ({
292
+ before: new Map(casObserver.before),
293
+ blockedSecretPaths: new Set(casObserver.blockedSecretPaths),
294
+ });
295
+ // Git tree -> hybrid (numstat for tracked + observer for gitignored);
296
+ // non-git -> cas over the observer (no baseline tree needed). Undefined
297
+ // outside capture mode (writes are deny-gated, nothing is captured).
298
+ const progressSubstrate = !setup.captureMode
299
+ ? undefined
300
+ : setup.gitWorkspace
301
+ ? captureBaselineTree
302
+ ? createHybridProgressSubstrate(createGitProgressSubstrate({
303
+ workspaceRoot: gitRoot,
304
+ executionId,
305
+ baselineTree: captureBaselineTree,
306
+ }), createCasProgressSubstrate({ workspaceRoot: gitRoot, read: readObserverTouched }))
307
+ : undefined
308
+ : createCasProgressSubstrate({ workspaceRoot: gitRoot, read: readObserverTouched });
284
309
  const cancellationSignal = Context.current().cancellationSignal;
285
310
  const result = await streamExecution({
286
311
  agentGraph: setup.agentGraph,
@@ -305,20 +330,19 @@ export function createDeepAgentActivities(config) {
305
330
  globalBypass: setup.globalBypass,
306
331
  },
307
332
  streamVersion: setup.streamVersion,
308
- // Mid-run live capture (DD-32): attach file_change_progress before each
309
- // scheduled persist. Git capture mode only (a pinned baseline exists);
310
- // deep-agent writes no runner-owned gate files into the tree, so no
311
- // excludePaths matching its turn-boundary candidate capture.
333
+ // Mid-run live capture (DD-32 / DD-33): attach file_change_progress
334
+ // before each scheduled persist, throttled by the floor inside
335
+ // captureFileChangeProgress. The substrate (git / non-git CAS / hybrid)
336
+ // was chosen for this turn above; deep-agent writes no runner-owned gate
337
+ // files into the tree, so the git slice needs no excludePaths — matching
338
+ // its turn-boundary candidate capture.
312
339
  beforePersist: async (status) => {
313
- if (!progressCaptureMode || !progressGitWorkspace || !captureBaselineTree) {
340
+ if (!progressSubstrate)
314
341
  return;
315
- }
316
342
  await captureFileChangeProgress({
317
343
  status,
318
- gitRoot,
319
- executionId,
320
344
  changeSetId,
321
- baselineTree: captureBaselineTree,
345
+ substrate: progressSubstrate,
322
346
  state: progressState,
323
347
  });
324
348
  },
@@ -366,6 +390,23 @@ export function createDeepAgentActivities(config) {
366
390
  ? FileCaptureClass.GIT_IGNORED_CAPTURED
367
391
  : FileCaptureClass.NON_GIT_CAS;
368
392
  const { casCaptures, unreviewablePaths } = await buildCasTurnCaptures(setup.casObserver, gitRoot, casCaptureClass);
393
+ // Approved-command turn facts (DD-28): when every mutation-capable call
394
+ // this turn was a consented shell command, attach the provenance so the
395
+ // backend can verify the cited consent rows and auto-keep the set
396
+ // instead of arming a second review gate. Fail-closed: any non-qualifying
397
+ // turn attaches nothing and reviews manually exactly as before. Attached
398
+ // only when captureCandidateToLedger actually authors a CANDIDATE.
399
+ const commandProvenance = deriveTurnCommandProvenance({
400
+ status: initialStatus,
401
+ priorSettledToolCallIds,
402
+ priorSubAgentToolCallIds,
403
+ globalBypass: setup.globalBypass,
404
+ });
405
+ if (commandProvenance) {
406
+ console.log(`[ExecuteDeepAgent] capture: turn qualifies for approved-command auto-keep ` +
407
+ `(consent rows: ${commandProvenance.consentToolCallIds.join(",") || "(auto_approve_all)"}); ` +
408
+ `attaching provenance to candidate (execution=${executionId})`);
409
+ }
369
410
  await captureCandidateToLedger({
370
411
  status: initialStatus,
371
412
  gitRoot,
@@ -378,6 +419,7 @@ export function createDeepAgentActivities(config) {
378
419
  unreviewablePaths,
379
420
  unreviewableCaptureClass: casCaptureClass,
380
421
  gitWorkspace: setup.gitWorkspace,
422
+ commandProvenance,
381
423
  });
382
424
  // Review is pending iff a CANDIDATE was actually authored (the seam
383
425
  // drops no-op captures), so a turn that only touched-then-unchanged an