@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
@@ -189,6 +189,66 @@ export async function snapshotCasChangeSet(opts: {
189
189
  };
190
190
  }
191
191
 
192
+ /**
193
+ * The display-facing classification of one CAS path's before/after bytes: the
194
+ * change kind, whether either side is binary, and the `+N −M` line counts.
195
+ */
196
+ export interface CasChangeClassification {
197
+ /** Path before the change (workspace-relative). Empty for ADD. */
198
+ readonly pathBefore: string;
199
+ /** Path after the change. Empty for DELETE. */
200
+ readonly pathAfter: string;
201
+ readonly kind: FileChangeKind;
202
+ /** A NUL byte on either side — the diff cannot render as text. */
203
+ readonly isBinary: boolean;
204
+ /** `+N −M` from the in-memory bytes; absent for a binary or oversized side. */
205
+ readonly lineCounts?: LineChangeCounts;
206
+ }
207
+
208
+ /**
209
+ * Classify one CAS path's before/after bytes. Returns `undefined` for a
210
+ * non-change — both sides absent, or a touch that left the bytes unchanged —
211
+ * exactly as the git substrate's diff omits unchanged files.
212
+ *
213
+ * This is the SINGLE classification authority for a CAS before/after pair: the
214
+ * turn-boundary capture ({@link buildCasCapturedFile}) and the mid-run progress
215
+ * producer ({@link ./cas-progress.js}) both route through it, so the live "N
216
+ * files changed so far" strip and the reviewed change set can never disagree on
217
+ * an edge case (the trailing-newline class of bug {@link ./line-counts.js}
218
+ * warns about). Kind, no-op detection, binary detection, and counting live here
219
+ * and nowhere else.
220
+ */
221
+ export function classifyCasChange(
222
+ path: string,
223
+ beforeBuf: Buffer | null,
224
+ afterBuf: Buffer | null,
225
+ ): CasChangeClassification | undefined {
226
+ if (beforeBuf === null && afterBuf === null) return undefined;
227
+ // A touch that did not change the bytes is not a reviewable change.
228
+ if (beforeBuf && afterBuf && beforeBuf.equals(afterBuf)) return undefined;
229
+
230
+ const kind =
231
+ beforeBuf === null
232
+ ? FileChangeKind.ADD
233
+ : afterBuf === null
234
+ ? FileChangeKind.DELETE
235
+ : FileChangeKind.MODIFY;
236
+ const isBinary =
237
+ (beforeBuf !== null && bytesLookBinary(beforeBuf)) ||
238
+ (afterBuf !== null && bytesLookBinary(afterBuf));
239
+
240
+ return {
241
+ pathBefore: kind === FileChangeKind.ADD ? "" : path,
242
+ pathAfter: kind === FileChangeKind.DELETE ? "" : path,
243
+ kind,
244
+ isBinary,
245
+ // A binary change has no text line diff to count.
246
+ lineCounts: isBinary
247
+ ? undefined
248
+ : countLineChanges(beforeBuf?.toString("utf8"), afterBuf?.toString("utf8")),
249
+ };
250
+ }
251
+
192
252
  /**
193
253
  * Store one path's before/after blobs and classify the change. Returns
194
254
  * `undefined` for a no-op (both sides absent, or an unchanged touch).
@@ -199,44 +259,29 @@ async function buildCasCapturedFile(
199
259
  capture: CasPathCapture,
200
260
  ): Promise<CasCapturedFile | undefined> {
201
261
  const { path, before, after, captureClass } = capture;
202
- if (before === null && after === null) return undefined;
203
-
204
262
  const beforeBuf = before === null ? null : Buffer.from(before);
205
263
  const afterBuf = after === null ? null : Buffer.from(after);
206
264
 
207
- // A touch that did not change the bytes is not a reviewable change.
208
- if (beforeBuf && afterBuf && beforeBuf.equals(afterBuf)) return undefined;
265
+ const classification = classifyCasChange(path, beforeBuf, afterBuf);
266
+ if (!classification) return undefined;
209
267
 
210
268
  const beforeRef = beforeBuf ? await storeBlob(storage, executionId, beforeBuf) : undefined;
211
269
  const afterRef = afterBuf ? await storeBlob(storage, executionId, afterBuf) : undefined;
212
270
 
213
- const kind =
214
- beforeRef === undefined
215
- ? FileChangeKind.ADD
216
- : afterRef === undefined
217
- ? FileChangeKind.DELETE
218
- : FileChangeKind.MODIFY;
219
-
220
- const isCreate = kind === FileChangeKind.ADD;
221
- const isDelete = kind === FileChangeKind.DELETE;
222
- const isBinary = beforeRef?.isBinary || afterRef?.isBinary;
223
-
224
271
  return {
225
- pathBefore: isCreate ? "" : path,
226
- pathAfter: isDelete ? "" : path,
227
- kind,
272
+ pathBefore: classification.pathBefore,
273
+ pathAfter: classification.pathAfter,
274
+ kind: classification.kind,
228
275
  captureClass,
229
276
  before: beforeRef,
230
277
  after: afterRef,
231
278
  // Binary on either side means the diff cannot render as text; the change set
232
279
  // then cannot be approved as complete (parity with the git substrate).
233
- diffComplete: !isBinary,
280
+ diffComplete: !classification.isBinary,
234
281
  // Display counts are taken NOW, while the text bytes are still in memory —
235
- // after this they exist only as offloaded blobs. A binary change has no
236
- // line diff to count.
237
- lineCounts: isBinary
238
- ? undefined
239
- : countLineChanges(beforeBuf?.toString("utf8"), afterBuf?.toString("utf8")),
282
+ // after this they exist only as offloaded blobs (the same counts the mid-run
283
+ // progress producer derives via classifyCasChange).
284
+ lineCounts: classification.lineCounts,
240
285
  };
241
286
  }
242
287
 
@@ -0,0 +1,180 @@
1
+ /**
2
+ * The DD-28 approved-command turn qualification rule — the harness-agnostic core
3
+ * of the auto-keep policy's runner-side facts.
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 and attaches them to the CANDIDATE_CAPTURED event as
11
+ * {@link TurnCommandProvenance}.
12
+ *
13
+ * This module owns the *qualification rule* both harnesses share; each harness
14
+ * supplies only what genuinely differs — how "this turn's tool calls" are scoped,
15
+ * and how a single executed command's DIRECT consent row is identified — via
16
+ * {@link CommandProvenanceCoreInputs}. Keeping the rule in one place is what makes
17
+ * the Cursor and deep-agent qualifications provably identical (a second copy would
18
+ * drift on edge cases — the divergence the AI-Engineer role warns against).
19
+ *
20
+ * TRUST BOUNDARY. This module asserts turn FACTS (which tools ran this turn),
21
+ * the same trust level as the captured bytes themselves. It never asserts
22
+ * CONSENT: a resolved consent id merely POINTS at a transcript row whose
23
+ * `approval_action` was authored by the server's SubmitApproval (and is
24
+ * preserved against runner writes) — the backend re-verifies every claimed row
25
+ * against that server-owned record before authoring the policy decision, so a
26
+ * runner cannot mint authorization it was never given.
27
+ *
28
+ * FAIL-CLOSED. Every uncertainty disqualifies (returns undefined → the set
29
+ * reviews manually, exactly as before DD-28): a file-tool call, an MCP tool, a
30
+ * sub-agent delegation, an unrecognized tool name, or an executed shell command
31
+ * with no provable consent source. Being conservative here costs only an extra
32
+ * review; being permissive would silently waive one.
33
+ */
34
+
35
+ import { create } from "@bufbuild/protobuf";
36
+ import type { AgentMessage, ToolCall } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
37
+ import { TurnCommandProvenanceSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/filereview_pb";
38
+ import type { TurnCommandProvenance } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/filereview_pb";
39
+ import { ApprovalAction, ToolKind } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
40
+ import { classifyTool, toolApprovalCategory } from "../tool-kind.js";
41
+ import { isToolCallRowHidden } from "../tool-row.js";
42
+
43
+ /**
44
+ * Tool kinds that can never mutate the workspace: their presence in a turn is
45
+ * irrelevant to change-set provenance. Everything OUTSIDE this set is either a
46
+ * consent-mapped shell command or a disqualifier — never silently ignored.
47
+ */
48
+ export const NON_MUTATING_KINDS: ReadonlySet<ToolKind> = new Set([
49
+ ToolKind.FILE_READ,
50
+ ToolKind.SEARCH,
51
+ ToolKind.LIST,
52
+ ToolKind.FETCH,
53
+ ToolKind.WEB_SEARCH,
54
+ ToolKind.THINK,
55
+ ToolKind.TODO,
56
+ ]);
57
+
58
+ /**
59
+ * The harness-supplied inputs to the shared DD-28 qualification rule. The two
60
+ * function inputs are exactly the two things that differ between harnesses
61
+ * (turn scoping is expressed by which calls appear in {@link turnToolCalls}).
62
+ */
63
+ export interface CommandProvenanceCoreInputs {
64
+ /**
65
+ * This turn's tool calls, already scoped by the harness (Cursor: the positional
66
+ * slice from the turn's first streamed message; deep-agent: the calls whose id
67
+ * is new since a pre-stream settled snapshot). Order is preserved but irrelevant.
68
+ */
69
+ readonly turnToolCalls: readonly ToolCall[];
70
+ /**
71
+ * The WHOLE transcript, for the APPROVE_ALL lease lookup — a lease is
72
+ * run-lifetime and its granting row usually lives in a prior turn.
73
+ */
74
+ readonly messages: readonly AgentMessage[];
75
+ /**
76
+ * Whether a (shell) tool call actually EXECUTED this turn, as opposed to being
77
+ * a pending/denied gate that produced no mutation. A non-executed shell neither
78
+ * needs consent nor disqualifies (Cursor: not in the denial ledger; deep-agent:
79
+ * status is COMPLETED).
80
+ */
81
+ readonly isExecutedCommand: (tc: ToolCall) => boolean;
82
+ /**
83
+ * The tool-call id of the row whose server-authored `approval_action` DIRECTLY
84
+ * authorized this executed command, or undefined when none does (then the lease
85
+ * / global-bypass fallbacks apply). Cursor maps a reinvocation grant token back
86
+ * to its consent row; the deep-agent's gated command IS its own consent row
87
+ * (same id, `approval_action` written in place).
88
+ */
89
+ readonly resolveDirectConsent: (tc: ToolCall) => string | undefined;
90
+ /** True when the pre-armed spec.auto_approve_all bypassed the gate. */
91
+ readonly globalBypass: boolean;
92
+ }
93
+
94
+ /**
95
+ * Apply the DD-28 qualification rule to a harness-scoped turn, returning the
96
+ * {@link TurnCommandProvenance} to attach to the candidate — or undefined when
97
+ * the turn does not qualify (any non-shell mutation-capable call, any unknown
98
+ * tool, any executed command without a provable consent source, or no executed
99
+ * consented command at all).
100
+ */
101
+ export function qualifyTurnCommandProvenance(
102
+ inputs: CommandProvenanceCoreInputs,
103
+ ): TurnCommandProvenance | undefined {
104
+ const { turnToolCalls, messages, isExecutedCommand, resolveDirectConsent, globalBypass } = inputs;
105
+
106
+ const consentIds = new Set<string>();
107
+ let authorizedByAutoApproveAll = false;
108
+ let executedCommandCount = 0;
109
+
110
+ for (const tc of turnToolCalls) {
111
+ // A hidden row is a collapsed twin/reaction — it never executed.
112
+ if (isToolCallRowHidden(tc)) continue;
113
+
114
+ const kind = classifyTool(tc.name, tc.mcpServerSlug);
115
+ if (NON_MUTATING_KINDS.has(kind)) continue;
116
+
117
+ if (kind !== ToolKind.SHELL) {
118
+ // A file tool (mixed turn), a sub-agent (unattributable mutations), an
119
+ // MCP tool (opaque side effects), or an unrecognized name — the turn's
120
+ // mutations cannot be attributed to consented commands. Fail closed.
121
+ return undefined;
122
+ }
123
+
124
+ if (!isExecutedCommand(tc)) {
125
+ // A pending / denied shell — this turn's gate, not a mutation source.
126
+ continue;
127
+ }
128
+ executedCommandCount++;
129
+
130
+ const consentId = resolveDirectConsent(tc);
131
+ if (consentId) {
132
+ consentIds.add(consentId);
133
+ continue;
134
+ }
135
+ const leaseConsentId = findLeaseConsentId(messages, tc.name);
136
+ if (leaseConsentId) {
137
+ consentIds.add(leaseConsentId);
138
+ continue;
139
+ }
140
+ if (globalBypass) {
141
+ authorizedByAutoApproveAll = true;
142
+ continue;
143
+ }
144
+ // An executed shell command with no grant, no lease, and no bypass should
145
+ // be impossible (every un-consented shell is gated); if it ever happens,
146
+ // the honest answer is a manual review, not a waived one.
147
+ return undefined;
148
+ }
149
+
150
+ // A turn that executed no consented command has nothing to attribute the
151
+ // change set to — whatever changed came from somewhere else. Manual review.
152
+ if (executedCommandCount === 0) return undefined;
153
+
154
+ return create(TurnCommandProvenanceSchema, {
155
+ consentToolCallIds: [...consentIds],
156
+ authorizedByAutoApproveAll,
157
+ });
158
+ }
159
+
160
+ /**
161
+ * Find the transcript row whose APPROVE_ALL authored the run-lifetime lease
162
+ * covering `toolName`'s category — the consent row a lease-executed command
163
+ * cites. Searched across the whole transcript (a lease usually originates in a
164
+ * prior turn); the first (earliest) APPROVE_ALL of the category is the lease's
165
+ * origin. Returns undefined when no lease row exists for the category.
166
+ */
167
+ export function findLeaseConsentId(
168
+ messages: readonly AgentMessage[],
169
+ toolName: string,
170
+ ): string | undefined {
171
+ const category = toolApprovalCategory(toolName);
172
+ if (!category) return undefined;
173
+ for (const msg of messages) {
174
+ for (const tc of msg.toolCalls) {
175
+ if (tc.approvalAction !== ApprovalAction.APPROVE_ALL) continue;
176
+ if (toolApprovalCategory(tc.name) === category) return tc.id;
177
+ }
178
+ }
179
+ return undefined;
180
+ }
@@ -325,8 +325,14 @@ export interface GitProgressEntry {
325
325
  readonly linesRemoved: number;
326
326
  }
327
327
 
328
- /** Result of {@link captureProgressDelta}. */
329
- export interface ProgressDelta {
328
+ /**
329
+ * Result of {@link captureProgressDelta} — the git-numstat delta. Distinct from
330
+ * the substrate-neutral `ProgressDelta` (in {@link ./progress.js}) that the
331
+ * git/cas/hybrid substrates emit: this carries the working-tree sha (for the
332
+ * short-circuit) and git's `FileChangeType` entries; the neutral one carries
333
+ * proto `FileChangeKind` entries and an honest total.
334
+ */
335
+ export interface GitProgressDelta {
330
336
  /** The post-diff working-tree sha, for the caller's short-circuit cache. */
331
337
  readonly afterTree: string;
332
338
  /** One count-only entry per changed file (no content). */
@@ -357,7 +363,7 @@ export async function captureProgressDelta(
357
363
  baselineTree: string,
358
364
  excludePaths: readonly string[] = [],
359
365
  lastTreeSha?: string,
360
- ): Promise<ProgressDelta | undefined> {
366
+ ): Promise<GitProgressDelta | undefined> {
361
367
  const gitDir = await resolveGitDir(gitRoot);
362
368
  const afterTree = await writeWorkingTree(gitRoot, gitDir, "progress", executionId, excludePaths);
363
369
  if (lastTreeSha !== undefined && afterTree === lastTreeSha) return undefined;
@@ -36,20 +36,32 @@ export {
36
36
  export {
37
37
  captureProgressDelta,
38
38
  isGitWorkTree,
39
+ type GitProgressDelta,
39
40
  type GitProgressEntry,
40
- type ProgressDelta,
41
41
  } from "./git-substrate.js";
42
42
 
43
43
  export {
44
44
  buildFileChangeProgress,
45
45
  captureFileChangeProgress,
46
+ createGitProgressSubstrate,
47
+ createHybridProgressSubstrate,
46
48
  newProgressCaptureState,
47
49
  PROGRESS_CAPTURE_MIN_INTERVAL_MS,
48
50
  PROGRESS_MAX_ENTRIES,
49
51
  shouldCaptureProgress,
52
+ type ProgressCapture,
50
53
  type ProgressCaptureState,
54
+ type ProgressDelta,
55
+ type ProgressEntry,
56
+ type ProgressSubstrate,
51
57
  } from "./progress.js";
52
58
 
59
+ export {
60
+ createCasProgressSubstrate,
61
+ type CasTouchedReader,
62
+ type CasTouchedSnapshot,
63
+ } from "./cas-progress.js";
64
+
53
65
  export {
54
66
  countLineChanges,
55
67
  LINE_COUNT_MAX_BYTES,
@@ -61,12 +73,14 @@ export {
61
73
  casBlobKey,
62
74
  casBlobReader,
63
75
  casManifestKey,
76
+ classifyCasChange,
64
77
  loadCasManifest,
65
78
  restoreCasToBaseline,
66
79
  snapshotCasChangeSet,
67
80
  type BlobReader,
68
81
  type CasBlobRef,
69
82
  type CasCapturedFile,
83
+ type CasChangeClassification,
70
84
  type CasManifest,
71
85
  type CasPathCapture,
72
86
  type CasSnapshotRef,