@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
@@ -36,9 +36,15 @@ import {
36
36
  type CaptureResumeResult,
37
37
  } from "../../shared/filereview/capture.js";
38
38
  import {
39
- captureFileChangeProgress,
40
- type ProgressCaptureState,
39
+ createGitProgressSubstrate,
40
+ createHybridProgressSubstrate,
41
+ type ProgressSubstrate,
41
42
  } from "../../shared/filereview/progress.js";
43
+ import {
44
+ createCasProgressSubstrate,
45
+ type CasTouchedReader,
46
+ type CasTouchedSnapshot,
47
+ } from "../../shared/filereview/cas-progress.js";
42
48
  import { hasCandidateCaptured } from "../../shared/filereview/events.js";
43
49
  import { partitionIgnoredPathsBySecret } from "../../shared/filereview/secret-paths.js";
44
50
  import { casBlobReader, type CasPathCapture } from "../../shared/filereview/cas-substrate.js";
@@ -202,31 +208,71 @@ export async function captureTurnToLedger(opts: {
202
208
  }
203
209
 
204
210
  /**
205
- * Mid-run: attach the live "N files changed so far" snapshot onto
206
- * `status.file_change_progress` (DD-32), throttled by the floor + tree-sha
207
- * short-circuit inside {@link captureFileChangeProgress}. The Cursor adapter binds
208
- * `CURSOR_RUNNER_OWNED_PATHS` so the progress diff excludes the gate's own files —
209
- * the SAME exclusion {@link captureTurnToLedger} uses for the turn-boundary
210
- * candidate, so the live count and the reviewed set agree. Content-free and
211
- * secret-safe; a no-op when nothing changed since the last capture.
211
+ * Choose the mid-run progress substrate for this turn's workspace shape (DD-32 /
212
+ * DD-33), returning `undefined` when there is nothing to observe (not capture
213
+ * mode, or no workspace). Built ONCE per turn — the substrate owns its own
214
+ * short-circuit cache across the loop's persists; the floor lives in the caller's
215
+ * `ProgressCaptureState`. The live count and the turn-boundary reviewed set agree
216
+ * by construction: the git slice binds the SAME `CURSOR_RUNNER_OWNED_PATHS`
217
+ * exclusion {@link captureTurnToLedger} uses, and the CAS slice reads the SAME
218
+ * hook sidecar ({@link readCasObservations}) the boundary reads.
219
+ *
220
+ * - git tree, no storage -> git substrate only (tracked-file `--numstat`);
221
+ * - git tree, storage (HYBRID) -> git + CAS (the gitignored writes the hook staged);
222
+ * - non-git workspace -> CAS only (every tool-mediated write is CAS-staged).
223
+ *
224
+ * The CAS slice needs the hook sidecar, which exists only when `captureIgnored`
225
+ * is on (storage configured). A non-git capture already required storage
226
+ * (`deriveCaptureMode`), so its sidecar is always present; a git tree with no
227
+ * storage simply gets the git-only substrate (parity with the pre-DD-33 behavior).
212
228
  */
213
- export async function captureProgressToStatus(opts: {
214
- readonly status: AgentExecutionStatus;
215
- readonly gitRoot: string;
229
+ export function buildCursorProgressSubstrate(opts: {
230
+ readonly captureMode: boolean;
231
+ readonly gitWorkspace: boolean;
232
+ readonly workspaceRoot: string | undefined;
233
+ readonly baselineTree: string | undefined;
216
234
  readonly executionId: string;
217
- readonly changeSetId: string;
218
- readonly baselineTree: string;
219
- readonly state: ProgressCaptureState;
220
- }): Promise<void> {
221
- await captureFileChangeProgress({
222
- status: opts.status,
223
- gitRoot: opts.gitRoot,
224
- executionId: opts.executionId,
225
- changeSetId: opts.changeSetId,
226
- baselineTree: opts.baselineTree,
227
- excludePaths: CURSOR_RUNNER_OWNED_PATHS,
228
- state: opts.state,
229
- });
235
+ readonly hitlDir: string | undefined;
236
+ readonly storage: ArtifactStorage | undefined;
237
+ }): ProgressSubstrate | undefined {
238
+ const { captureMode, gitWorkspace, workspaceRoot, baselineTree, executionId, hitlDir, storage } = opts;
239
+ if (!captureMode || !workspaceRoot) return undefined;
240
+
241
+ const casReader: CasTouchedReader | undefined =
242
+ hitlDir && storage ? createSidecarTouchedReader(hitlDir) : undefined;
243
+
244
+ if (gitWorkspace) {
245
+ if (!baselineTree) return undefined;
246
+ const git = createGitProgressSubstrate({
247
+ workspaceRoot,
248
+ executionId,
249
+ baselineTree,
250
+ excludePaths: CURSOR_RUNNER_OWNED_PATHS,
251
+ });
252
+ return casReader
253
+ ? createHybridProgressSubstrate(git, createCasProgressSubstrate({ workspaceRoot, read: casReader }))
254
+ : git;
255
+ }
256
+
257
+ // Non-git: every tool-mediated write is CAS-staged; there is no git slice.
258
+ return casReader ? createCasProgressSubstrate({ workspaceRoot, read: casReader }) : undefined;
259
+ }
260
+
261
+ /**
262
+ * A {@link CasTouchedReader} over the Cursor hook's on-disk sidecar. Each capture
263
+ * reads the current observations ({@link readCasObservations}) and maps them to
264
+ * the substrate's snapshot shape. Reads are bounded by the caller's capture floor
265
+ * (default 2s), and the sidecar is small (one small marker + before-blob per
266
+ * touched gitignored path), so re-reading per capture is cheap and always
267
+ * reflects sub-agent writes staged since the last one.
268
+ */
269
+ function createSidecarTouchedReader(hitlDir: string): CasTouchedReader {
270
+ return async (): Promise<CasTouchedSnapshot> => {
271
+ const { captured, secretPaths } = await readCasObservations(hitlDir);
272
+ const before = new Map<string, Uint8Array | null>();
273
+ for (const c of captured) before.set(c.path, c.before);
274
+ return { before, blockedSecretPaths: new Set(secretPaths) };
275
+ };
230
276
  }
231
277
 
232
278
  /**
@@ -1,54 +1,21 @@
1
1
  /**
2
- * Approved-command turn provenance (DD-28): the runner-owned qualification for
3
- * the auto-keep policy.
2
+ * Approved-command turn provenance (DD-28) the CURSOR harness adapter over the
3
+ * shared qualification rule ({@link qualifyTurnCommandProvenance}).
4
4
  *
5
- * A turn whose ONLY mutation source was shell commands the human had already
6
- * authorized should not re-gate its file effects at the turn-boundary review
7
- * the user consented to the command, and the command's file effects are the
8
- * consented outcome. The server cannot derive this itself (tool calls carry no
9
- * turn marker), so the runner the only component that owns turn scoping —
10
- * derives the facts here and attaches them to the CANDIDATE_CAPTURED event as
11
- * {@link TurnCommandProvenance}.
5
+ * The Cursor harness scopes a turn POSITIONALLY (messages from the turn's first
6
+ * streamed index onward) and maps an executed command to its consent row through
7
+ * the reinvocation grant tokens the deny-gate minted. Both mappings are Cursor's
8
+ * own; the DD-28 rule they feed lives in `shared/filereview/command-provenance.ts`
9
+ * so the Cursor and deep-agent qualifications cannot drift.
12
10
  *
13
- * TRUST BOUNDARY. This module asserts turn FACTS (which tools ran this turn),
14
- * the same trust level as the captured bytes themselves. It never asserts
15
- * CONSENT: `consentToolCallIds` merely POINT at transcript rows whose
16
- * `approval_action` was authored by the server's SubmitApproval (and is
17
- * preserved against runner writes) — the backend re-verifies every claimed row
18
- * against that server-owned record before authoring the policy decision, so a
19
- * runner cannot mint authorization it was never given.
20
- *
21
- * FAIL-CLOSED. Every uncertainty disqualifies (returns undefined → the set
22
- * reviews manually, exactly as before DD-28): a file-tool call, an MCP tool, a
23
- * sub-agent delegation, an unrecognized tool name, or an executed shell command
24
- * with no provable consent source. Being conservative here costs only an extra
25
- * review; being permissive would silently waive one.
11
+ * (Trust boundary + fail-closed contract: see the shared module.)
26
12
  */
27
13
 
28
- import { create } from "@bufbuild/protobuf";
29
14
  import type { AgentMessage } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
30
- import { TurnCommandProvenanceSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/filereview_pb";
31
15
  import type { TurnCommandProvenance } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/filereview_pb";
32
- import { ApprovalAction, ToolKind } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
33
- import { classifyTool, toolApprovalCategory } from "../../shared/tool-kind.js";
34
- import { isToolCallRowHidden } from "../../shared/tool-row.js";
16
+ import { qualifyTurnCommandProvenance } from "../../shared/filereview/command-provenance.js";
35
17
  import { toolCallIdentityToken } from "./message-translator.js";
36
18
 
37
- /**
38
- * Tool kinds that can never mutate the workspace: their presence in a turn is
39
- * irrelevant to change-set provenance. Everything OUTSIDE this set is either a
40
- * consent-mapped shell command or a disqualifier — never silently ignored.
41
- */
42
- const NON_MUTATING_KINDS: ReadonlySet<ToolKind> = new Set([
43
- ToolKind.FILE_READ,
44
- ToolKind.SEARCH,
45
- ToolKind.LIST,
46
- ToolKind.FETCH,
47
- ToolKind.WEB_SEARCH,
48
- ToolKind.THINK,
49
- ToolKind.TODO,
50
- ]);
51
-
52
19
  export interface CommandProvenanceInputs {
53
20
  /**
54
21
  * The full transcript at capture time. Turn scoping is positional: messages
@@ -79,90 +46,28 @@ export interface CommandProvenanceInputs {
79
46
 
80
47
  /**
81
48
  * Derive the {@link TurnCommandProvenance} for the turn, or undefined when the
82
- * turn does not qualify (any non-shell mutation-capable call, any unknown tool,
83
- * any executed command without a provable consent source, or no executed
84
- * consented command at all).
49
+ * turn does not qualify. Scopes this turn's tool calls positionally, resolves
50
+ * consent through the deny-gate grant tokens, and delegates the DD-28 rule to
51
+ * {@link qualifyTurnCommandProvenance}.
85
52
  */
86
53
  export function deriveTurnCommandProvenance(
87
54
  inputs: CommandProvenanceInputs,
88
55
  ): TurnCommandProvenance | undefined {
89
56
  const { messages, turnStartIndex, deniedTokens, grantTokenToConsentId, globalBypass } = inputs;
90
57
 
91
- const consentIds = new Set<string>();
92
- let authorizedByAutoApproveAll = false;
93
- let executedCommandCount = 0;
94
-
95
- for (let i = turnStartIndex; i < messages.length; i++) {
96
- for (const tc of messages[i].toolCalls) {
97
- // A hidden row is a collapsed twin/reaction — it never executed.
98
- if (isToolCallRowHidden(tc)) continue;
99
-
100
- const kind = classifyTool(tc.name, tc.mcpServerSlug);
101
- if (NON_MUTATING_KINDS.has(kind)) continue;
102
-
103
- if (kind !== ToolKind.SHELL) {
104
- // A file tool (mixed turn), a sub-agent (unattributable mutations), an
105
- // MCP tool (opaque side effects), or an unrecognized name — the turn's
106
- // mutations cannot be attributed to consented commands. Fail closed.
107
- return undefined;
108
- }
58
+ // This turn's tool calls are the positional tail from the first message this
59
+ // turn's stream produced; everything before was seeded from prior turns.
60
+ const turnToolCalls = messages.slice(turnStartIndex).flatMap((m) => m.toolCalls);
109
61
 
110
- const token = toolCallIdentityToken(tc);
111
- if (deniedTokens.has(token)) {
112
- // This turn's pending gate — denied by the hook, never executed.
113
- continue;
114
- }
115
- executedCommandCount++;
116
-
117
- const consentId = grantTokenToConsentId.get(token);
118
- if (consentId) {
119
- consentIds.add(consentId);
120
- continue;
121
- }
122
- const leaseConsentId = findLeaseConsentId(messages, tc.name);
123
- if (leaseConsentId) {
124
- consentIds.add(leaseConsentId);
125
- continue;
126
- }
127
- if (globalBypass) {
128
- authorizedByAutoApproveAll = true;
129
- continue;
130
- }
131
- // An executed shell command with no grant, no lease, and no bypass should
132
- // be impossible (the hook gates every un-consented shell); if it ever
133
- // happens, the honest answer is a manual review, not a waived one.
134
- return undefined;
135
- }
136
- }
137
-
138
- // A turn that executed no consented command has nothing to attribute the
139
- // change set to — whatever changed came from somewhere else. Manual review.
140
- if (executedCommandCount === 0) return undefined;
141
-
142
- return create(TurnCommandProvenanceSchema, {
143
- consentToolCallIds: [...consentIds],
144
- authorizedByAutoApproveAll,
62
+ return qualifyTurnCommandProvenance({
63
+ turnToolCalls,
64
+ messages,
65
+ // A shell whose identity token is in the denial ledger was denied by the
66
+ // hook — this turn's pending gate, never executed.
67
+ isExecutedCommand: (tc) => !deniedTokens.has(toolCallIdentityToken(tc)),
68
+ // A grant token maps back to the consent row (the row carrying the
69
+ // server-authored approval_action) that authorized this reinvocation.
70
+ resolveDirectConsent: (tc) => grantTokenToConsentId.get(toolCallIdentityToken(tc)),
71
+ globalBypass,
145
72
  });
146
73
  }
147
-
148
- /**
149
- * Find the transcript row whose APPROVE_ALL authored the run-lifetime lease
150
- * covering `toolName`'s category — the consent row a lease-executed command
151
- * cites. Searched across the whole transcript (a lease usually originates in a
152
- * prior turn); the first (earliest) APPROVE_ALL of the category is the lease's
153
- * origin. Returns undefined when no lease row exists for the category.
154
- */
155
- function findLeaseConsentId(
156
- messages: readonly AgentMessage[],
157
- toolName: string,
158
- ): string | undefined {
159
- const category = toolApprovalCategory(toolName);
160
- if (!category) return undefined;
161
- for (const msg of messages) {
162
- for (const tc of msg.toolCalls) {
163
- if (tc.approvalAction !== ApprovalAction.APPROVE_ALL) continue;
164
- if (toolApprovalCategory(tc.name) === category) return tc.id;
165
- }
166
- }
167
- return undefined;
168
- }