@stigmer/runner 3.1.3 → 3.1.5
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.
- package/dist/.build-fingerprint +1 -1
- package/dist/activities/execute-cursor/attachment-resolver.js +1 -1
- package/dist/activities/execute-cursor/attachment-resolver.js.map +1 -1
- package/dist/activities/execute-cursor/capture-flow.d.ts +26 -15
- package/dist/activities/execute-cursor/capture-flow.js +56 -18
- package/dist/activities/execute-cursor/capture-flow.js.map +1 -1
- package/dist/activities/execute-cursor/command-provenance.d.ts +11 -25
- package/dist/activities/execute-cursor/command-provenance.js +25 -115
- package/dist/activities/execute-cursor/command-provenance.js.map +1 -1
- package/dist/activities/execute-cursor/index.js +297 -478
- package/dist/activities/execute-cursor/index.js.map +1 -1
- package/dist/activities/execute-cursor/skill-resolver.js +1 -1
- package/dist/activities/execute-cursor/skill-resolver.js.map +1 -1
- package/dist/activities/execute-cursor/todo-tracker.d.ts +6 -1
- package/dist/activities/execute-cursor/todo-tracker.js +15 -43
- package/dist/activities/execute-cursor/todo-tracker.js.map +1 -1
- package/dist/activities/execute-cursor/turn-stream.d.ts +141 -0
- package/dist/activities/execute-cursor/turn-stream.js +249 -0
- package/dist/activities/execute-cursor/turn-stream.js.map +1 -0
- package/dist/activities/execute-deep-agent/command-provenance.d.ts +61 -0
- package/dist/activities/execute-deep-agent/command-provenance.js +72 -0
- package/dist/activities/execute-deep-agent/command-provenance.js.map +1 -0
- package/dist/activities/execute-deep-agent/index.js +88 -20
- package/dist/activities/execute-deep-agent/index.js.map +1 -1
- package/dist/activities/execute-deep-agent/status-builder.js +8 -1
- package/dist/activities/execute-deep-agent/status-builder.js.map +1 -1
- package/dist/activities/execute-deep-agent/v3-status-builder.js +12 -1
- package/dist/activities/execute-deep-agent/v3-status-builder.js.map +1 -1
- package/dist/activities/execute-deep-agent/writeback-coordinator.js +5 -1
- package/dist/activities/execute-deep-agent/writeback-coordinator.js.map +1 -1
- package/dist/otel.js +10 -0
- package/dist/otel.js.map +1 -1
- package/dist/shared/filereview/cas-progress.d.ts +63 -0
- package/dist/shared/filereview/cas-progress.js +128 -0
- package/dist/shared/filereview/cas-progress.js.map +1 -0
- package/dist/shared/filereview/cas-substrate.d.ts +29 -0
- package/dist/shared/filereview/cas-substrate.js +46 -21
- package/dist/shared/filereview/cas-substrate.js.map +1 -1
- package/dist/shared/filereview/command-provenance.d.ts +93 -0
- package/dist/shared/filereview/command-provenance.js +132 -0
- package/dist/shared/filereview/command-provenance.js.map +1 -0
- package/dist/shared/filereview/git-substrate.d.ts +9 -3
- package/dist/shared/filereview/git-substrate.js +4 -0
- package/dist/shared/filereview/git-substrate.js.map +1 -1
- package/dist/shared/filereview/index.d.ts +4 -3
- package/dist/shared/filereview/index.js +3 -2
- package/dist/shared/filereview/index.js.map +1 -1
- package/dist/shared/filereview/progress.d.ts +105 -34
- package/dist/shared/filereview/progress.js +96 -34
- package/dist/shared/filereview/progress.js.map +1 -1
- package/dist/shared/plan-mode-prompt.d.ts +9 -0
- package/dist/shared/plan-mode-prompt.js +18 -0
- package/dist/shared/plan-mode-prompt.js.map +1 -1
- package/dist/shared/todos.d.ts +56 -0
- package/dist/shared/todos.js +98 -0
- package/dist/shared/todos.js.map +1 -0
- package/dist/shared/tool-row.d.ts +16 -0
- package/dist/shared/tool-row.js +31 -0
- package/dist/shared/tool-row.js.map +1 -1
- package/dist/shared/workspace/git-identity.d.ts +36 -0
- package/dist/shared/workspace/git-identity.js +39 -0
- package/dist/shared/workspace/git-identity.js.map +1 -0
- package/dist/shared/workspace/local-backend.d.ts +6 -2
- package/dist/shared/workspace/local-backend.js +6 -2
- package/dist/shared/workspace/local-backend.js.map +1 -1
- package/dist/shared/workspace/stigmer-link.d.ts +54 -0
- package/dist/shared/workspace/stigmer-link.js +92 -0
- package/dist/shared/workspace/stigmer-link.js.map +1 -0
- package/dist/shared/workspace/types.d.ts +6 -4
- package/package.json +2 -2
- package/src/__tests__/otel-turn-span.test.ts +61 -0
- package/src/activities/execute-cursor/__tests__/progress-substrate.test.ts +169 -0
- package/src/activities/execute-cursor/__tests__/turn-stream.test.ts +349 -0
- package/src/activities/execute-cursor/attachment-resolver.ts +1 -1
- package/src/activities/execute-cursor/capture-flow.ts +71 -25
- package/src/activities/execute-cursor/command-provenance.ts +25 -120
- package/src/activities/execute-cursor/index.ts +345 -505
- package/src/activities/execute-cursor/skill-resolver.ts +1 -1
- package/src/activities/execute-cursor/todo-tracker.ts +17 -59
- package/src/activities/execute-cursor/turn-stream.ts +418 -0
- package/src/activities/execute-deep-agent/__tests__/command-provenance.test.ts +252 -0
- package/src/activities/execute-deep-agent/__tests__/status-builder.test.ts +78 -0
- package/src/activities/execute-deep-agent/__tests__/v3-status-builder.test.ts +105 -1
- package/src/activities/execute-deep-agent/__tests__/writeback-coordinator.test.ts +30 -3
- package/src/activities/execute-deep-agent/command-provenance.ts +102 -0
- package/src/activities/execute-deep-agent/index.ts +107 -20
- package/src/activities/execute-deep-agent/status-builder.ts +9 -0
- package/src/activities/execute-deep-agent/v3-status-builder.ts +13 -0
- package/src/activities/execute-deep-agent/writeback-coordinator.ts +5 -1
- package/src/otel.ts +8 -0
- package/src/shared/__tests__/todos.test.ts +216 -0
- package/src/shared/filereview/__tests__/cas-progress.test.ts +228 -0
- package/src/shared/filereview/__tests__/cas-substrate.test.ts +66 -0
- package/src/shared/filereview/__tests__/command-provenance.test.ts +252 -0
- package/src/shared/filereview/__tests__/progress.test.ts +112 -10
- package/src/shared/filereview/cas-progress.ts +170 -0
- package/src/shared/filereview/cas-substrate.ts +69 -24
- package/src/shared/filereview/command-provenance.ts +180 -0
- package/src/shared/filereview/git-substrate.ts +13 -3
- package/src/shared/filereview/index.ts +15 -1
- package/src/shared/filereview/progress.ts +171 -47
- package/src/shared/plan-mode-prompt.ts +18 -0
- package/src/shared/todos.ts +126 -0
- package/src/shared/tool-row.ts +34 -0
- package/src/shared/workspace/__tests__/git-identity.test.ts +124 -0
- package/src/shared/workspace/__tests__/stigmer-link.test.ts +173 -0
- package/src/shared/workspace/git-identity.ts +41 -0
- package/src/shared/workspace/local-backend.ts +6 -2
- package/src/shared/workspace/stigmer-link.ts +97 -0
- package/src/shared/workspace/types.ts +6 -4
- package/dist/activities/execute-cursor/stigmer-link.d.ts +0 -35
- package/dist/activities/execute-cursor/stigmer-link.js +0 -73
- package/dist/activities/execute-cursor/stigmer-link.js.map +0 -1
- package/src/activities/execute-cursor/stigmer-link.ts +0 -78
|
@@ -20,12 +20,18 @@ import {
|
|
|
20
20
|
snapshotBaseline,
|
|
21
21
|
dropCaptureRefs,
|
|
22
22
|
type GitProgressEntry,
|
|
23
|
-
type
|
|
23
|
+
type GitProgressDelta,
|
|
24
24
|
} from "../git-substrate.js";
|
|
25
25
|
import {
|
|
26
26
|
buildFileChangeProgress,
|
|
27
|
+
createGitProgressSubstrate,
|
|
28
|
+
createHybridProgressSubstrate,
|
|
27
29
|
PROGRESS_MAX_ENTRIES,
|
|
28
30
|
shouldCaptureProgress,
|
|
31
|
+
type ProgressCapture,
|
|
32
|
+
type ProgressDelta,
|
|
33
|
+
type ProgressEntry,
|
|
34
|
+
type ProgressSubstrate,
|
|
29
35
|
} from "../progress.js";
|
|
30
36
|
|
|
31
37
|
const execFileAsync = promisify(execFile);
|
|
@@ -49,7 +55,7 @@ async function writeBytes(rel: string, bytes: Uint8Array): Promise<void> {
|
|
|
49
55
|
await writeFile(join(repo, rel), bytes);
|
|
50
56
|
}
|
|
51
57
|
|
|
52
|
-
function entryFor(delta:
|
|
58
|
+
function entryFor(delta: GitProgressDelta, path: string): GitProgressEntry | undefined {
|
|
53
59
|
return delta.entries.find((e) => e.pathAfter === path || e.pathBefore === path);
|
|
54
60
|
}
|
|
55
61
|
|
|
@@ -157,23 +163,28 @@ describe("captureProgressDelta (real git repo)", () => {
|
|
|
157
163
|
});
|
|
158
164
|
|
|
159
165
|
describe("buildFileChangeProgress (pure)", () => {
|
|
160
|
-
function delta(entries:
|
|
161
|
-
return {
|
|
166
|
+
function delta(entries: ProgressEntry[], totalFilesChanged?: number): ProgressDelta {
|
|
167
|
+
return totalFilesChanged === undefined ? { entries } : { entries, totalFilesChanged };
|
|
162
168
|
}
|
|
163
|
-
function entry(
|
|
169
|
+
function entry(
|
|
170
|
+
path: string,
|
|
171
|
+
added: number,
|
|
172
|
+
removed: number,
|
|
173
|
+
kind: FileChangeKind = FileChangeKind.MODIFY,
|
|
174
|
+
): ProgressEntry {
|
|
164
175
|
return {
|
|
165
|
-
pathBefore: path,
|
|
176
|
+
pathBefore: kind === FileChangeKind.ADD ? "" : path,
|
|
166
177
|
pathAfter: path,
|
|
167
|
-
|
|
178
|
+
kind,
|
|
168
179
|
linesAdded: added,
|
|
169
180
|
linesRemoved: removed,
|
|
170
181
|
};
|
|
171
182
|
}
|
|
172
183
|
|
|
173
|
-
it("sums aggregate counts and
|
|
184
|
+
it("sums aggregate counts and carries the per-entry kind through unchanged", () => {
|
|
174
185
|
const progress = buildFileChangeProgress(
|
|
175
186
|
delta([
|
|
176
|
-
|
|
187
|
+
entry("a.ts", 5, 0, FileChangeKind.ADD),
|
|
177
188
|
entry("b.ts", 2, 3),
|
|
178
189
|
]),
|
|
179
190
|
CHANGE_SET_ID,
|
|
@@ -187,6 +198,18 @@ describe("buildFileChangeProgress (pure)", () => {
|
|
|
187
198
|
expect(progress.capturedAt).not.toBe("");
|
|
188
199
|
});
|
|
189
200
|
|
|
201
|
+
it("reports totalFilesChanged when a substrate capped its reads, else the entry count", () => {
|
|
202
|
+
// git substrate leaves totalFilesChanged undefined -> filesChanged = entries.length.
|
|
203
|
+
const gitLike = buildFileChangeProgress(delta([entry("a.ts", 1, 0)]), CHANGE_SET_ID);
|
|
204
|
+
expect(gitLike.filesChanged).toBe(1);
|
|
205
|
+
|
|
206
|
+
// cas substrate read only a bounded prefix -> filesChanged reflects the honest
|
|
207
|
+
// total even though fewer entries were emitted.
|
|
208
|
+
const casLike = buildFileChangeProgress(delta([entry("a.ts", 1, 0)], 7), CHANGE_SET_ID);
|
|
209
|
+
expect(casLike.filesChanged).toBe(7);
|
|
210
|
+
expect(casLike.entries).toHaveLength(1);
|
|
211
|
+
});
|
|
212
|
+
|
|
190
213
|
it("zeroes counts for secret-like paths (path visible, magnitude withheld)", () => {
|
|
191
214
|
const progress = buildFileChangeProgress(
|
|
192
215
|
delta([entry(".env", 10, 2), entry("app.ts", 4, 1)]),
|
|
@@ -203,7 +226,7 @@ describe("buildFileChangeProgress (pure)", () => {
|
|
|
203
226
|
});
|
|
204
227
|
|
|
205
228
|
it("caps entries but keeps files_changed and totals honest over all files", () => {
|
|
206
|
-
const entries:
|
|
229
|
+
const entries: ProgressEntry[] = [];
|
|
207
230
|
for (let i = 0; i < PROGRESS_MAX_ENTRIES + 25; i++) {
|
|
208
231
|
entries.push(entry(`f${i}.ts`, 1, 1));
|
|
209
232
|
}
|
|
@@ -234,3 +257,82 @@ describe("shouldCaptureProgress (pure)", () => {
|
|
|
234
257
|
expect(shouldCaptureProgress(1000, 3001, 2000)).toBe(true);
|
|
235
258
|
});
|
|
236
259
|
});
|
|
260
|
+
|
|
261
|
+
describe("createGitProgressSubstrate (real git repo)", () => {
|
|
262
|
+
it("reports changed:true then reuses the cached full delta on an unchanged tree", async () => {
|
|
263
|
+
const baseline = await snapshotBaseline(repo, EXEC_ID);
|
|
264
|
+
const sub = createGitProgressSubstrate({
|
|
265
|
+
workspaceRoot: repo,
|
|
266
|
+
executionId: EXEC_ID,
|
|
267
|
+
baselineTree: baseline,
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
await write("src/new.ts", "a\nb\n");
|
|
271
|
+
const first = await sub.capture();
|
|
272
|
+
expect(first.changed).toBe(true);
|
|
273
|
+
expect(first.delta.entries.length).toBeGreaterThan(0);
|
|
274
|
+
|
|
275
|
+
// Nothing moved since the last capture: the tree-sha short-circuits, but the
|
|
276
|
+
// full delta is still returned (changed:false) so a hybrid can merge it.
|
|
277
|
+
const second = await sub.capture();
|
|
278
|
+
expect(second.changed).toBe(false);
|
|
279
|
+
expect(second.delta.entries).toEqual(first.delta.entries);
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
/** A fake substrate returning a fixed capture — for merge/short-circuit logic. */
|
|
284
|
+
function fakeSubstrate(capture: ProgressCapture): ProgressSubstrate {
|
|
285
|
+
return { capture: () => Promise.resolve(capture) };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function progressEntry(path: string): ProgressEntry {
|
|
289
|
+
return { pathBefore: "", pathAfter: path, kind: FileChangeKind.ADD, linesAdded: 1, linesRemoved: 0 };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
describe("createHybridProgressSubstrate", () => {
|
|
293
|
+
it("concatenates disjoint git + cas slices and sums the honest totals", async () => {
|
|
294
|
+
const git = fakeSubstrate({
|
|
295
|
+
delta: { entries: [progressEntry("tracked.ts")] }, // git: total = entries.length
|
|
296
|
+
changed: true,
|
|
297
|
+
});
|
|
298
|
+
const cas = fakeSubstrate({
|
|
299
|
+
delta: { entries: [progressEntry(".env.local")], totalFilesChanged: 3 }, // cas capped
|
|
300
|
+
changed: true,
|
|
301
|
+
});
|
|
302
|
+
const hybrid = createHybridProgressSubstrate(git, cas);
|
|
303
|
+
|
|
304
|
+
const { delta, changed } = await hybrid.capture();
|
|
305
|
+
expect(changed).toBe(true);
|
|
306
|
+
expect(delta.entries.map((e) => e.pathAfter)).toEqual(["tracked.ts", ".env.local"]);
|
|
307
|
+
// 1 (git entries) + 3 (cas honest total) = 4.
|
|
308
|
+
expect(delta.totalFilesChanged).toBe(4);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it("still emits the full git slice when only the cas slice changed", async () => {
|
|
312
|
+
// Git unchanged returns its cached full delta with changed:false; the hybrid
|
|
313
|
+
// must keep it (a `ProgressDelta | undefined` contract would have dropped it).
|
|
314
|
+
const git = fakeSubstrate({
|
|
315
|
+
delta: { entries: [progressEntry("tracked.ts")] },
|
|
316
|
+
changed: false,
|
|
317
|
+
});
|
|
318
|
+
const cas = fakeSubstrate({
|
|
319
|
+
delta: { entries: [progressEntry("build/out.js")], totalFilesChanged: 1 },
|
|
320
|
+
changed: true,
|
|
321
|
+
});
|
|
322
|
+
const hybrid = createHybridProgressSubstrate(git, cas);
|
|
323
|
+
|
|
324
|
+
const { delta, changed } = await hybrid.capture();
|
|
325
|
+
expect(changed).toBe(true);
|
|
326
|
+
expect(delta.entries.map((e) => e.pathAfter)).toEqual(["tracked.ts", "build/out.js"]);
|
|
327
|
+
expect(delta.totalFilesChanged).toBe(2);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it("reports changed:false only when NEITHER slice moved", async () => {
|
|
331
|
+
const git = fakeSubstrate({ delta: { entries: [] }, changed: false });
|
|
332
|
+
const cas = fakeSubstrate({ delta: { entries: [], totalFilesChanged: 0 }, changed: false });
|
|
333
|
+
const hybrid = createHybridProgressSubstrate(git, cas);
|
|
334
|
+
|
|
335
|
+
const { changed } = await hybrid.capture();
|
|
336
|
+
expect(changed).toBe(false);
|
|
337
|
+
});
|
|
338
|
+
});
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mid-run progress substrate for the CAS (content-addressed) domain — the
|
|
3
|
+
* non-git and gitignored half of DD-33. The git substrate ({@link ./progress.js}
|
|
4
|
+
* `createGitProgressSubstrate`) covers git-tracked changes cheaply via
|
|
5
|
+
* `--numstat`; this covers the paths git cannot see, sourced from the same
|
|
6
|
+
* per-turn observer the turn-boundary CAS capture reads:
|
|
7
|
+
* - a non-git workspace: EVERY tool-mediated write (the whole change set), and
|
|
8
|
+
* - a git tree's gitignored writes (the CAS half of a HYBRID turn).
|
|
9
|
+
*
|
|
10
|
+
* COST MODEL (why this is not as cheap as git)
|
|
11
|
+
* --------------------------------------------
|
|
12
|
+
* `git --numstat` yields exact `+N −M` with zero byte reads; CAS has no such free
|
|
13
|
+
* lunch — kind, no-op exclusion, and counts all need the after-bytes. So we bound
|
|
14
|
+
* the work: only the sorted read-PREFIX (up to {@link PROGRESS_MAX_ENTRIES}) has
|
|
15
|
+
* its after-bytes read and counted; the tail contributes to `totalFilesChanged`
|
|
16
|
+
* (the honest count) only. `files_changed` is therefore net-exact for a normal
|
|
17
|
+
* turn (≤ the budget) and an upper bound beyond it — matching git where git is
|
|
18
|
+
* free, diverging only where the substrate cannot be. A `size+mtime` signature
|
|
19
|
+
* (the stats are collected anyway) short-circuits an unchanged capture.
|
|
20
|
+
*
|
|
21
|
+
* CONSISTENCY & SECRET SAFETY
|
|
22
|
+
* ---------------------------
|
|
23
|
+
* Counts come from {@link classifyCasChange} — the SAME classifier the reviewed
|
|
24
|
+
* set uses — so the strip and the change set never disagree. Secret-like paths
|
|
25
|
+
* are excluded up front via {@link partitionIgnoredPathsBySecret} (and zeroed
|
|
26
|
+
* again in `buildFileChangeProgress` as a backstop); no file bodies are ever
|
|
27
|
+
* carried on the wire. Only tool-mediated writes are observed, exactly the scope
|
|
28
|
+
* of the non-git / HYBRID turn-boundary capture — no new divergence.
|
|
29
|
+
*
|
|
30
|
+
* @since File-Change HITL Redesign (non-git + hybrid mid-run progress / DD-33)
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import { readFile, stat } from "node:fs/promises";
|
|
34
|
+
import { join } from "node:path";
|
|
35
|
+
import { FileChangeKind } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
36
|
+
import { classifyCasChange } from "./cas-substrate.js";
|
|
37
|
+
import { LINE_COUNT_MAX_BYTES } from "./line-counts.js";
|
|
38
|
+
import {
|
|
39
|
+
PROGRESS_MAX_ENTRIES,
|
|
40
|
+
type ProgressCapture,
|
|
41
|
+
type ProgressDelta,
|
|
42
|
+
type ProgressEntry,
|
|
43
|
+
type ProgressSubstrate,
|
|
44
|
+
} from "./progress.js";
|
|
45
|
+
import { partitionIgnoredPathsBySecret } from "./secret-paths.js";
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* A snapshot of the turn's CAS-observed paths, read on each capture:
|
|
49
|
+
* - `before`: first-touched paths' pre-turn bytes (`null` = the path did not
|
|
50
|
+
* exist before → an ADD). The KEYS are the touched-path set.
|
|
51
|
+
* - `blockedSecretPaths`: paths the gate hard-blocked as secret-like — excluded
|
|
52
|
+
* from capture.
|
|
53
|
+
*
|
|
54
|
+
* Deep-agent supplies a synchronous copy of its live `CasCaptureObserver`; Cursor
|
|
55
|
+
* supplies a reader over the hook's on-disk sidecar. The reader MUST return an
|
|
56
|
+
* atomic snapshot (copy the live map before any await) so a concurrent sub-agent
|
|
57
|
+
* write cannot mutate it mid-capture.
|
|
58
|
+
*/
|
|
59
|
+
export interface CasTouchedSnapshot {
|
|
60
|
+
readonly before: ReadonlyMap<string, Uint8Array | null>;
|
|
61
|
+
readonly blockedSecretPaths: ReadonlySet<string>;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Produces a {@link CasTouchedSnapshot} for one capture. */
|
|
65
|
+
export type CasTouchedReader = () => CasTouchedSnapshot | Promise<CasTouchedSnapshot>;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The CAS progress substrate. Reads the touched set on each capture, computes
|
|
69
|
+
* kind + `+N −M` for the bounded read-prefix via {@link classifyCasChange}, and
|
|
70
|
+
* reports `totalFilesChanged` over the full capturable set. Owns a `size+mtime`
|
|
71
|
+
* signature so an unchanged capture returns `changed:false` (the cached delta is
|
|
72
|
+
* reused, letting the hybrid still merge it with a changed git slice).
|
|
73
|
+
*/
|
|
74
|
+
export function createCasProgressSubstrate(opts: {
|
|
75
|
+
readonly workspaceRoot: string;
|
|
76
|
+
readonly read: CasTouchedReader;
|
|
77
|
+
/** Read budget / display cap; defaults to {@link PROGRESS_MAX_ENTRIES}. Injectable for tests. */
|
|
78
|
+
readonly maxEntries?: number;
|
|
79
|
+
}): ProgressSubstrate {
|
|
80
|
+
const maxEntries = opts.maxEntries ?? PROGRESS_MAX_ENTRIES;
|
|
81
|
+
let cachedFull: ProgressDelta = { entries: [], totalFilesChanged: 0 };
|
|
82
|
+
let lastSignature: string | undefined;
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
async capture(): Promise<ProgressCapture> {
|
|
86
|
+
const snapshot = await opts.read();
|
|
87
|
+
const { capturablePaths } = partitionIgnoredPathsBySecret(
|
|
88
|
+
snapshot.before.keys(),
|
|
89
|
+
snapshot.blockedSecretPaths,
|
|
90
|
+
);
|
|
91
|
+
// Deterministic order so the read prefix and entry order are stable.
|
|
92
|
+
const sorted = [...capturablePaths].sort();
|
|
93
|
+
const prefix = sorted.slice(0, maxEntries);
|
|
94
|
+
|
|
95
|
+
const entries: ProgressEntry[] = [];
|
|
96
|
+
// The signature keys on the capturable count (catches a new touch) plus the
|
|
97
|
+
// read prefix's size+mtime (catches a content edit to a shown file). A
|
|
98
|
+
// change to an UNshown tail file has no display effect, so it is not keyed.
|
|
99
|
+
const sigParts: string[] = [`${capturablePaths.length}`];
|
|
100
|
+
|
|
101
|
+
for (const relPath of prefix) {
|
|
102
|
+
const abs = join(opts.workspaceRoot, relPath);
|
|
103
|
+
const st = await statOrNull(abs);
|
|
104
|
+
sigParts.push(`${relPath}\u0000${st ? `${st.size}:${st.mtimeMs}` : "\u2205"}`);
|
|
105
|
+
|
|
106
|
+
const beforeBytes = snapshot.before.get(relPath) ?? null;
|
|
107
|
+
const beforeBuf = beforeBytes === null ? null : Buffer.from(beforeBytes);
|
|
108
|
+
|
|
109
|
+
// Oversized after: derive the kind without reading (bound the I/O) and
|
|
110
|
+
// leave counts at zero — the same "no stat" shape a binary/oversized side
|
|
111
|
+
// takes in the reviewed set. A same-size no-op cannot be excluded here; an
|
|
112
|
+
// accepted overstatement only for very large files.
|
|
113
|
+
if (st && st.size > LINE_COUNT_MAX_BYTES) {
|
|
114
|
+
const kind = beforeBuf === null ? FileChangeKind.ADD : FileChangeKind.MODIFY;
|
|
115
|
+
entries.push({
|
|
116
|
+
pathBefore: kind === FileChangeKind.ADD ? "" : relPath,
|
|
117
|
+
pathAfter: relPath,
|
|
118
|
+
kind,
|
|
119
|
+
linesAdded: 0,
|
|
120
|
+
linesRemoved: 0,
|
|
121
|
+
});
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const afterBytes = st ? await readFileOrNull(abs) : null;
|
|
126
|
+
const afterBuf = afterBytes === null ? null : Buffer.from(afterBytes);
|
|
127
|
+
const cls = classifyCasChange(relPath, beforeBuf, afterBuf);
|
|
128
|
+
if (!cls) continue; // no-op (unchanged touch) or both sides absent
|
|
129
|
+
entries.push({
|
|
130
|
+
pathBefore: cls.pathBefore,
|
|
131
|
+
pathAfter: cls.pathAfter,
|
|
132
|
+
kind: cls.kind,
|
|
133
|
+
linesAdded: cls.lineCounts?.linesAdded ?? 0,
|
|
134
|
+
linesRemoved: cls.lineCounts?.linesRemoved ?? 0,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Honest total: the capturable set minus the no-ops detected in the read
|
|
139
|
+
// prefix. Net-exact for a turn within the read budget; an upper bound beyond
|
|
140
|
+
// it (tail no-ops are not read, so cannot be excluded).
|
|
141
|
+
const prefixNoOps = prefix.length - entries.length;
|
|
142
|
+
const totalFilesChanged = capturablePaths.length - prefixNoOps;
|
|
143
|
+
|
|
144
|
+
const signature = sigParts.join("|");
|
|
145
|
+
if (signature === lastSignature) {
|
|
146
|
+
return { delta: cachedFull, changed: false };
|
|
147
|
+
}
|
|
148
|
+
lastSignature = signature;
|
|
149
|
+
cachedFull = { entries, totalFilesChanged };
|
|
150
|
+
return { delta: cachedFull, changed: true };
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function statOrNull(abs: string): Promise<{ size: number; mtimeMs: number } | null> {
|
|
156
|
+
try {
|
|
157
|
+
const s = await stat(abs);
|
|
158
|
+
return { size: s.size, mtimeMs: s.mtimeMs };
|
|
159
|
+
} catch {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function readFileOrNull(abs: string): Promise<Uint8Array | null> {
|
|
165
|
+
try {
|
|
166
|
+
return await readFile(abs);
|
|
167
|
+
} catch {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
@@ -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
|
-
|
|
208
|
-
if (
|
|
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:
|
|
226
|
-
pathAfter:
|
|
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
|
|
236
|
-
//
|
|
237
|
-
lineCounts:
|
|
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
|
+
}
|
|
@@ -97,6 +97,10 @@ export function captureRef(executionId: string): string {
|
|
|
97
97
|
* garbage collection — but `git commit-tree` still requires an author/committer,
|
|
98
98
|
* and the repo may have no user.name/email configured. Supplied via env so we
|
|
99
99
|
* never mutate the repo's config.
|
|
100
|
+
*
|
|
101
|
+
* Deliberately distinct from the public agent identity in
|
|
102
|
+
* `../workspace/git-identity.ts`, which authors the write-back commits that
|
|
103
|
+
* land on real branches and pull requests.
|
|
100
104
|
*/
|
|
101
105
|
const SNAPSHOT_IDENTITY_ENV: Record<string, string> = {
|
|
102
106
|
GIT_AUTHOR_NAME: "stigmer-runner",
|
|
@@ -325,8 +329,14 @@ export interface GitProgressEntry {
|
|
|
325
329
|
readonly linesRemoved: number;
|
|
326
330
|
}
|
|
327
331
|
|
|
328
|
-
/**
|
|
329
|
-
|
|
332
|
+
/**
|
|
333
|
+
* Result of {@link captureProgressDelta} — the git-numstat delta. Distinct from
|
|
334
|
+
* the substrate-neutral `ProgressDelta` (in {@link ./progress.js}) that the
|
|
335
|
+
* git/cas/hybrid substrates emit: this carries the working-tree sha (for the
|
|
336
|
+
* short-circuit) and git's `FileChangeType` entries; the neutral one carries
|
|
337
|
+
* proto `FileChangeKind` entries and an honest total.
|
|
338
|
+
*/
|
|
339
|
+
export interface GitProgressDelta {
|
|
330
340
|
/** The post-diff working-tree sha, for the caller's short-circuit cache. */
|
|
331
341
|
readonly afterTree: string;
|
|
332
342
|
/** One count-only entry per changed file (no content). */
|
|
@@ -357,7 +367,7 @@ export async function captureProgressDelta(
|
|
|
357
367
|
baselineTree: string,
|
|
358
368
|
excludePaths: readonly string[] = [],
|
|
359
369
|
lastTreeSha?: string,
|
|
360
|
-
): Promise<
|
|
370
|
+
): Promise<GitProgressDelta | undefined> {
|
|
361
371
|
const gitDir = await resolveGitDir(gitRoot);
|
|
362
372
|
const afterTree = await writeWorkingTree(gitRoot, gitDir, "progress", executionId, excludePaths);
|
|
363
373
|
if (lastTreeSha !== undefined && afterTree === lastTreeSha) return undefined;
|