@stigmer/runner 3.1.1 → 3.1.3
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.d.ts +49 -8
- package/dist/activities/execute-cursor/attachment-resolver.js +83 -26
- package/dist/activities/execute-cursor/attachment-resolver.js.map +1 -1
- package/dist/activities/execute-cursor/capture-flow.d.ts +18 -0
- package/dist/activities/execute-cursor/capture-flow.js +21 -0
- package/dist/activities/execute-cursor/capture-flow.js.map +1 -1
- package/dist/activities/execute-cursor/cas-observations.d.ts +13 -0
- package/dist/activities/execute-cursor/cas-observations.js +39 -5
- package/dist/activities/execute-cursor/cas-observations.js.map +1 -1
- package/dist/activities/execute-cursor/hook-script.js +19 -1
- package/dist/activities/execute-cursor/hook-script.js.map +1 -1
- package/dist/activities/execute-cursor/index.d.ts +5 -0
- package/dist/activities/execute-cursor/index.js +222 -160
- package/dist/activities/execute-cursor/index.js.map +1 -1
- package/dist/activities/execute-cursor/message-translator.js +10 -1
- package/dist/activities/execute-cursor/message-translator.js.map +1 -1
- package/dist/activities/execute-cursor/prompt-builder.d.ts +32 -7
- package/dist/activities/execute-cursor/prompt-builder.js +38 -20
- package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
- package/dist/activities/execute-cursor/skill-resolver.d.ts +1 -10
- package/dist/activities/execute-cursor/skill-resolver.js +3 -55
- package/dist/activities/execute-cursor/skill-resolver.js.map +1 -1
- package/dist/activities/execute-cursor/stigmer-link.d.ts +35 -0
- package/dist/activities/execute-cursor/stigmer-link.js +73 -0
- package/dist/activities/execute-cursor/stigmer-link.js.map +1 -0
- package/dist/activities/execute-cursor/turn-boundary.d.ts +108 -0
- package/dist/activities/execute-cursor/turn-boundary.js +163 -0
- package/dist/activities/execute-cursor/turn-boundary.js.map +1 -0
- package/dist/activities/execute-cursor/workspace-provision.d.ts +3 -2
- package/dist/activities/execute-cursor/workspace-provision.js +5 -3
- package/dist/activities/execute-cursor/workspace-provision.js.map +1 -1
- package/dist/activities/execute-deep-agent/index.js +89 -5
- package/dist/activities/execute-deep-agent/index.js.map +1 -1
- package/dist/activities/execute-deep-agent/prompt-builder.d.ts +17 -0
- package/dist/activities/execute-deep-agent/prompt-builder.js +16 -0
- package/dist/activities/execute-deep-agent/prompt-builder.js.map +1 -1
- package/dist/activities/execute-deep-agent/setup.js +22 -16
- package/dist/activities/execute-deep-agent/setup.js.map +1 -1
- package/dist/activities/execute-deep-agent/streaming-v3.js +6 -1
- package/dist/activities/execute-deep-agent/streaming-v3.js.map +1 -1
- package/dist/activities/execute-deep-agent/streaming.d.ts +7 -0
- package/dist/activities/execute-deep-agent/streaming.js +5 -1
- package/dist/activities/execute-deep-agent/streaming.js.map +1 -1
- package/dist/config.d.ts +7 -0
- package/dist/config.js +9 -0
- package/dist/config.js.map +1 -1
- package/dist/middleware/approval-gate.js +36 -6
- package/dist/middleware/approval-gate.js.map +1 -1
- package/dist/runner-manager.d.ts +2 -0
- package/dist/runner-manager.js +2 -1
- package/dist/runner-manager.js.map +1 -1
- package/dist/runner.d.ts +2 -0
- package/dist/runner.js +2 -1
- package/dist/runner.js.map +1 -1
- package/dist/shared/artifact-storage.d.ts +32 -0
- package/dist/shared/artifact-storage.js +74 -1
- package/dist/shared/artifact-storage.js.map +1 -1
- package/dist/shared/filereview/capture.d.ts +18 -1
- package/dist/shared/filereview/capture.js +105 -28
- package/dist/shared/filereview/capture.js.map +1 -1
- package/dist/shared/filereview/cas-substrate.d.ts +9 -0
- package/dist/shared/filereview/cas-substrate.js +9 -1
- package/dist/shared/filereview/cas-substrate.js.map +1 -1
- package/dist/shared/filereview/events.d.ts +16 -0
- package/dist/shared/filereview/events.js +37 -0
- package/dist/shared/filereview/events.js.map +1 -1
- package/dist/shared/filereview/git-substrate.d.ts +42 -0
- package/dist/shared/filereview/git-substrate.js +78 -0
- package/dist/shared/filereview/git-substrate.js.map +1 -1
- package/dist/shared/filereview/index.d.ts +3 -1
- package/dist/shared/filereview/index.js +3 -1
- package/dist/shared/filereview/index.js.map +1 -1
- package/dist/shared/filereview/line-counts.d.ts +48 -0
- package/dist/shared/filereview/line-counts.js +72 -0
- package/dist/shared/filereview/line-counts.js.map +1 -0
- package/dist/shared/filereview/progress.d.ts +96 -0
- package/dist/shared/filereview/progress.js +134 -0
- package/dist/shared/filereview/progress.js.map +1 -0
- package/dist/shared/implement-plan-prompt.d.ts +44 -0
- package/dist/shared/implement-plan-prompt.js +98 -0
- package/dist/shared/implement-plan-prompt.js.map +1 -0
- package/dist/shared/plan-artifact.d.ts +59 -11
- package/dist/shared/plan-artifact.js +144 -16
- package/dist/shared/plan-artifact.js.map +1 -1
- package/dist/shared/plan-mode-prompt.d.ts +32 -0
- package/dist/shared/plan-mode-prompt.js +56 -0
- package/dist/shared/plan-mode-prompt.js.map +1 -0
- package/dist/shared/status.d.ts +11 -0
- package/dist/shared/status.js +12 -2
- package/dist/shared/status.js.map +1 -1
- package/dist/shared/tool-row.d.ts +30 -1
- package/dist/shared/tool-row.js +53 -4
- package/dist/shared/tool-row.js.map +1 -1
- package/dist/shared/workspace/session-root.d.ts +35 -0
- package/dist/shared/workspace/session-root.js +53 -0
- package/dist/shared/workspace/session-root.js.map +1 -0
- package/dist/shared/workspace/workspace-lock.d.ts +86 -0
- package/dist/shared/workspace/workspace-lock.js +187 -0
- package/dist/shared/workspace/workspace-lock.js.map +1 -0
- package/package.json +8 -5
- package/src/__tests__/config.test.ts +8 -0
- package/src/activities/__tests__/classify-tool-approvals.test.ts +1 -0
- package/src/activities/__tests__/discover-mcp-server.test.ts +1 -0
- package/src/activities/execute-cursor/__tests__/attachment-resolver.test.ts +161 -0
- package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +132 -2
- package/src/activities/execute-cursor/__tests__/cas-observations.test.ts +55 -0
- package/src/activities/execute-cursor/__tests__/delta-enricher.test.ts +68 -0
- package/src/activities/execute-cursor/__tests__/hitl-ledger.test.ts +28 -0
- package/src/activities/execute-cursor/__tests__/hook-script.test.ts +63 -4
- package/src/activities/execute-cursor/__tests__/turn-boundary.test.ts +280 -0
- package/src/activities/execute-cursor/__tests__/workspace-provision.test.ts +36 -2
- package/src/activities/execute-cursor/attachment-resolver.ts +116 -34
- package/src/activities/execute-cursor/capture-flow.ts +32 -0
- package/src/activities/execute-cursor/cas-observations.ts +41 -5
- package/src/activities/execute-cursor/hook-script.ts +19 -1
- package/src/activities/execute-cursor/index.ts +243 -178
- package/src/activities/execute-cursor/message-translator.ts +9 -1
- package/src/activities/execute-cursor/prompt-builder.ts +60 -22
- package/src/activities/execute-cursor/skill-resolver.ts +3 -59
- package/src/activities/execute-cursor/stigmer-link.ts +78 -0
- package/src/activities/execute-cursor/turn-boundary.ts +274 -0
- package/src/activities/execute-cursor/workspace-provision.ts +5 -3
- package/src/activities/execute-deep-agent/__tests__/hitl-resume-approve-all.test.ts +7 -2
- package/src/activities/execute-deep-agent/__tests__/hitl-resume-history.test.ts +7 -2
- package/src/activities/execute-deep-agent/__tests__/index.test.ts +4 -1
- package/src/activities/execute-deep-agent/__tests__/prompt-builder.test.ts +99 -0
- package/src/activities/execute-deep-agent/__tests__/sequential-gate-resume.test.ts +7 -2
- package/src/activities/execute-deep-agent/index.ts +104 -5
- package/src/activities/execute-deep-agent/prompt-builder.ts +39 -0
- package/src/activities/execute-deep-agent/setup.ts +25 -17
- package/src/activities/execute-deep-agent/streaming-v3.ts +6 -1
- package/src/activities/execute-deep-agent/streaming.ts +12 -1
- package/src/config.ts +17 -0
- package/src/middleware/__tests__/approval-gate.test.ts +77 -12
- package/src/middleware/approval-gate.ts +39 -7
- package/src/runner-manager.ts +6 -1
- package/src/runner.ts +6 -1
- package/src/shared/__tests__/artifact-storage.test.ts +111 -1
- package/src/shared/__tests__/implement-plan-prompt.test.ts +86 -0
- package/src/shared/__tests__/plan-artifact.test.ts +142 -9
- package/src/shared/__tests__/secret-leak-scan.test.ts +105 -0
- package/src/shared/__tests__/status.test.ts +7 -2
- package/src/shared/__tests__/tool-row.test.ts +136 -1
- package/src/shared/artifact-storage.ts +83 -1
- package/src/shared/filereview/__tests__/capture.test.ts +182 -1
- package/src/shared/filereview/__tests__/cas-substrate.test.ts +34 -0
- package/src/shared/filereview/__tests__/events.test.ts +105 -0
- package/src/shared/filereview/__tests__/line-counts.test.ts +73 -0
- package/src/shared/filereview/__tests__/progress.test.ts +236 -0
- package/src/shared/filereview/capture.ts +134 -30
- package/src/shared/filereview/cas-substrate.ts +17 -1
- package/src/shared/filereview/events.ts +50 -0
- package/src/shared/filereview/git-substrate.ts +108 -0
- package/src/shared/filereview/index.ts +22 -1
- package/src/shared/filereview/line-counts.ts +83 -0
- package/src/shared/filereview/progress.ts +186 -0
- package/src/shared/implement-plan-prompt.ts +104 -0
- package/src/shared/plan-artifact.ts +152 -16
- package/src/shared/plan-mode-prompt.ts +56 -0
- package/src/shared/status.ts +11 -1
- package/src/shared/tool-row.ts +58 -5
- package/src/shared/workspace/__tests__/session-root.test.ts +42 -0
- package/src/shared/workspace/__tests__/workspace-lock.test.ts +200 -0
- package/src/shared/workspace/session-root.ts +60 -0
- package/src/shared/workspace/workspace-lock.ts +241 -0
|
@@ -77,6 +77,7 @@ import {
|
|
|
77
77
|
type CasPathCapture,
|
|
78
78
|
type CasSnapshotRef,
|
|
79
79
|
} from "./cas-substrate.js";
|
|
80
|
+
import { isSecretLikePath } from "./secret-paths.js";
|
|
80
81
|
|
|
81
82
|
/**
|
|
82
83
|
* Whether a turn runs in apply-then-review CAPTURE mode (file edits flow and are
|
|
@@ -244,21 +245,41 @@ export async function captureCandidateToLedger(opts: {
|
|
|
244
245
|
|
|
245
246
|
const unreviewable = unreviewablePaths ?? [];
|
|
246
247
|
|
|
248
|
+
// Secret handling is three-way across the substrates, and this is the last of
|
|
249
|
+
// the three (the other two happen upstream at the harness gate):
|
|
250
|
+
// 1. A gitignored secret WRITE never flows — hard-blocked at the gate /
|
|
251
|
+
// deny-gate (DD-12/DD-30). It arrives here only as a path in
|
|
252
|
+
// `unreviewablePaths` (recorded by the gate), authored content-less below.
|
|
253
|
+
// 2. A gitignored secret that flowed under the global bypass is withheld from
|
|
254
|
+
// CAS by `partitionIgnoredPathsBySecret` (secret-paths.ts), so its bytes
|
|
255
|
+
// never reach `casCaptures`.
|
|
256
|
+
// 3. A git-TRACKED secret DID flow (the capture-mode gate allows tracked
|
|
257
|
+
// mutations with no secret check) and is present in `gitChanges` with real
|
|
258
|
+
// bytes. We split it out here and author it content-less, so its content
|
|
259
|
+
// never reaches the ledger. Unlike a gitignored secret it HAS a reversible
|
|
260
|
+
// substrate (the git refs), so it is withheld-and-reverted rather than
|
|
261
|
+
// never-applied — same never-persisted guarantee, appropriate mechanism.
|
|
262
|
+
const { safe: safeGitChanges, secret: secretGitChanges } =
|
|
263
|
+
partitionGitChangesBySecret(gitChanges);
|
|
264
|
+
|
|
247
265
|
// A turn that changed nothing (no tracked/ignored change and no blocked path)
|
|
248
|
-
// authors no event.
|
|
266
|
+
// authors no event. A secret-only tracked turn is NOT a no-op — its file is in
|
|
267
|
+
// `gitChanges`, so the guard below is false and it authors a blocked entry.
|
|
249
268
|
if (gitChanges.length === 0 && casFiles.length === 0 && unreviewable.length === 0) {
|
|
250
269
|
return gitChanges;
|
|
251
270
|
}
|
|
252
271
|
|
|
253
|
-
// One combined change set: git-tracked (inline bodies) +
|
|
254
|
-
// secret-blocked (content-less
|
|
255
|
-
// all (buildCandidateCapturedEvent sorts by file
|
|
256
|
-
// and its identity span every substrate.
|
|
257
|
-
// per-file signals up three ways: any
|
|
258
|
-
// PARTIAL_BLOCKED; a set blocked only by
|
|
259
|
-
// (keepable in one acknowledged action); else
|
|
272
|
+
// One combined change set: git-tracked (inline bodies) + git-tracked-secret
|
|
273
|
+
// (content-less) + CAS (blob refs) + gitignored-secret-blocked (content-less).
|
|
274
|
+
// The aggregate digest folds all (buildCandidateCapturedEvent sorts by file
|
|
275
|
+
// digest), so the reviewed diff and its identity span every substrate.
|
|
276
|
+
// deriveDiffCompleteness then rolls the per-file signals up three ways: any
|
|
277
|
+
// content-less (non-binary) entry forces PARTIAL_BLOCKED; a set blocked only by
|
|
278
|
+
// binaries is BINARY_SUMMARY_ONLY (keepable in one acknowledged action); else
|
|
279
|
+
// COMPLETE.
|
|
260
280
|
const captured = [
|
|
261
|
-
...
|
|
281
|
+
...safeGitChanges.map((c) => buildCapturedFileChange(toCapturedChangeInput(changeSetId, c))),
|
|
282
|
+
...secretGitChanges.map((c) => buildCapturedFileChange(trackedSecretChangeInput(changeSetId, c))),
|
|
262
283
|
...casFiles.map((f) => buildCapturedFileChange(casToCapturedChangeInput(changeSetId, f))),
|
|
263
284
|
...unreviewable.map((p) => buildCapturedFileChange(unreviewableChangeInput(changeSetId, p, unreviewableCaptureClass))),
|
|
264
285
|
];
|
|
@@ -622,7 +643,9 @@ function candidateCasRef(changeSet: FileChangeSet): CasSnapshotRef | undefined {
|
|
|
622
643
|
* after bodies are carried as blob REFS (already offloaded to artifact storage
|
|
623
644
|
* by the CAS substrate), never re-inlined — so the CANDIDATE event stays small
|
|
624
645
|
* and the bytes are stored exactly once. Enforcement digests come from the blob
|
|
625
|
-
* content addresses, identical in form to the git path.
|
|
646
|
+
* content addresses, identical in form to the git path. The display line counts
|
|
647
|
+
* ride along explicitly — a ref side has no inline text for the producer to
|
|
648
|
+
* count, so the substrate's capture-time counts are the only honest source.
|
|
626
649
|
*/
|
|
627
650
|
function casToCapturedChangeInput(
|
|
628
651
|
changeSetId: string,
|
|
@@ -634,6 +657,7 @@ function casToCapturedChangeInput(
|
|
|
634
657
|
pathAfter: file.pathAfter,
|
|
635
658
|
kind: file.kind,
|
|
636
659
|
captureClass: file.captureClass,
|
|
660
|
+
lineCounts: file.lineCounts,
|
|
637
661
|
before: file.before
|
|
638
662
|
? {
|
|
639
663
|
kind: "ref",
|
|
@@ -657,37 +681,117 @@ function casToCapturedChangeInput(
|
|
|
657
681
|
}
|
|
658
682
|
|
|
659
683
|
/**
|
|
660
|
-
*
|
|
661
|
-
*
|
|
662
|
-
*
|
|
663
|
-
*
|
|
664
|
-
*
|
|
665
|
-
*
|
|
666
|
-
*
|
|
667
|
-
*
|
|
668
|
-
*
|
|
669
|
-
*
|
|
670
|
-
*
|
|
671
|
-
*
|
|
684
|
+
* The single content-less `SECRET_WITHHELD` producer input — the one shape every
|
|
685
|
+
* secret-blocked change (gitignored OR git-tracked) is authored with, so the two
|
|
686
|
+
* producers can never drift (a divergence in `diffComplete`/`blockedReason` would
|
|
687
|
+
* make the backend gate treat one kind of secret differently from the other).
|
|
688
|
+
*
|
|
689
|
+
* Both sides are absent (empty enforcement digests) and `diffComplete=false`.
|
|
690
|
+
* Being non-binary incomplete with no keepable bytes, it forces the change set to
|
|
691
|
+
* PARTIAL_BLOCKED (never BINARY_SUMMARY_ONLY) — approval is blocked and the path
|
|
692
|
+
* is surfaced honestly, while its CONTENT never enters the ledger or storage.
|
|
693
|
+
* `blockedReason=SECRET_WITHHELD` records the honest cause so the review UI can say
|
|
694
|
+
* *why* rather than a cause-agnostic "unavailable" (doc 15). Callers supply the
|
|
695
|
+
* id / paths / kind / captureClass so each producer keeps its own honest identity.
|
|
672
696
|
*/
|
|
673
|
-
function
|
|
674
|
-
|
|
675
|
-
|
|
697
|
+
function secretWithheldChangeInput(
|
|
698
|
+
id: string,
|
|
699
|
+
pathBefore: string,
|
|
700
|
+
pathAfter: string,
|
|
701
|
+
kind: FileChangeKind,
|
|
676
702
|
captureClass: FileCaptureClass,
|
|
677
703
|
): CapturedChangeInput {
|
|
678
704
|
return {
|
|
679
|
-
id
|
|
680
|
-
pathBefore
|
|
681
|
-
pathAfter
|
|
682
|
-
kind
|
|
705
|
+
id,
|
|
706
|
+
pathBefore,
|
|
707
|
+
pathAfter,
|
|
708
|
+
kind,
|
|
683
709
|
captureClass,
|
|
684
710
|
diffComplete: false,
|
|
685
711
|
blockedReason: FileReviewBlockReason.SECRET_WITHHELD,
|
|
686
712
|
};
|
|
687
713
|
}
|
|
688
714
|
|
|
715
|
+
/**
|
|
716
|
+
* Map a secret-blocked GITIGNORED path (the write never flowed — hard-blocked at
|
|
717
|
+
* the harness gate, design doc 12 / DD-E) to a content-less entry. The write was
|
|
718
|
+
* blocked before it ran, so create-vs-modify is unknown and irrelevant (nothing
|
|
719
|
+
* is ever applied or reconciled for this entry) — kind is MODIFY. `captureClass`
|
|
720
|
+
* is the turn's CAS substrate class (GIT_IGNORED_CAPTURED | NON_GIT_CAS).
|
|
721
|
+
*/
|
|
722
|
+
function unreviewableChangeInput(
|
|
723
|
+
changeSetId: string,
|
|
724
|
+
path: string,
|
|
725
|
+
captureClass: FileCaptureClass,
|
|
726
|
+
): CapturedChangeInput {
|
|
727
|
+
return secretWithheldChangeInput(
|
|
728
|
+
`${changeSetId}:${path}`,
|
|
729
|
+
path,
|
|
730
|
+
path,
|
|
731
|
+
FileChangeKind.MODIFY,
|
|
732
|
+
captureClass,
|
|
733
|
+
);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
/**
|
|
737
|
+
* Map a secret-like GIT-TRACKED change to a content-less entry (DD-26 follow-up
|
|
738
|
+
* #3). Unlike a gitignored secret, a tracked secret write actually FLOWED (the
|
|
739
|
+
* capture-mode gate allows tracked mutations), so it is present in the git diff
|
|
740
|
+
* with real bytes — which must never be persisted into the ledger. We author it
|
|
741
|
+
* content-less here so its CONTENT never reaches the ledger / Temporal history /
|
|
742
|
+
* storage, while the honest kind + paths (a CREATE/MODIFY/DELETE, mirroring
|
|
743
|
+
* {@link toCapturedChangeInput}'s id/path derivation so the resume reconcile
|
|
744
|
+
* lookup matches) and `GIT_TRACKED` provenance are preserved. It is discard-only
|
|
745
|
+
* (PARTIAL_BLOCKED): on resume it can only be rejected, and the git substrate
|
|
746
|
+
* reverts it byte-exact from the baseline ref (the reversible substrate a
|
|
747
|
+
* gitignored secret lacks — so "withhold + revert" upholds never-persisted here
|
|
748
|
+
* without the gate hard-block). A DELETE is handled here too: the git diff
|
|
749
|
+
* resurrects the deleted file's baseline (secret) bytes as the `before` side, a
|
|
750
|
+
* leak only this seam can withhold (the gate sees no content for a delete).
|
|
751
|
+
*/
|
|
752
|
+
function trackedSecretChangeInput(
|
|
753
|
+
changeSetId: string,
|
|
754
|
+
change: GitCapturedChange,
|
|
755
|
+
): CapturedChangeInput {
|
|
756
|
+
const isCreate = change.changeType === FileChangeType.CREATE;
|
|
757
|
+
const isDelete = change.changeType === FileChangeType.DELETE;
|
|
758
|
+
const pathBefore = isCreate ? "" : change.path;
|
|
759
|
+
const pathAfter = isDelete ? "" : change.path;
|
|
760
|
+
return secretWithheldChangeInput(
|
|
761
|
+
`${changeSetId}:${pathAfter || pathBefore}`,
|
|
762
|
+
pathBefore,
|
|
763
|
+
pathAfter,
|
|
764
|
+
toFileChangeKind(change.changeType),
|
|
765
|
+
FileCaptureClass.GIT_TRACKED,
|
|
766
|
+
);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
/**
|
|
770
|
+
* Split a turn's git-tracked changes by the secret gate: a secret-like tracked
|
|
771
|
+
* path (a committed `credentials.json`, `.env.example`, `*.tfvars`, ...) goes to
|
|
772
|
+
* `secret` (authored content-less via {@link trackedSecretChangeInput}); every
|
|
773
|
+
* other tracked change stays in `safe` (its diff is captured normally). Pure and
|
|
774
|
+
* exported for direct unit testing. Keyed on the same {@link isSecretLikePath}
|
|
775
|
+
* classifier the CAS/gitignored path uses ({@link partitionIgnoredPathsBySecret}
|
|
776
|
+
* in secret-paths.ts), so a path is classified identically everywhere. NOTE: the
|
|
777
|
+
* classifier is path-based (DD-12 D2), so a rename to an innocuous name defeats
|
|
778
|
+
* it — an intentional, cross-substrate limitation, not introduced here.
|
|
779
|
+
*/
|
|
780
|
+
export function partitionGitChangesBySecret(changes: readonly GitCapturedChange[]): {
|
|
781
|
+
readonly safe: readonly GitCapturedChange[];
|
|
782
|
+
readonly secret: readonly GitCapturedChange[];
|
|
783
|
+
} {
|
|
784
|
+
const safe: GitCapturedChange[] = [];
|
|
785
|
+
const secret: GitCapturedChange[] = [];
|
|
786
|
+
for (const change of changes) {
|
|
787
|
+
if (isSecretLikePath(change.path)) secret.push(change);
|
|
788
|
+
else safe.push(change);
|
|
789
|
+
}
|
|
790
|
+
return { safe, secret };
|
|
791
|
+
}
|
|
792
|
+
|
|
689
793
|
/** Map the git capture kind to the file-review {@link FileChangeKind}. */
|
|
690
|
-
function toFileChangeKind(changeType: FileChangeType): FileChangeKind {
|
|
794
|
+
export function toFileChangeKind(changeType: FileChangeType): FileChangeKind {
|
|
691
795
|
switch (changeType) {
|
|
692
796
|
case FileChangeType.CREATE:
|
|
693
797
|
return FileChangeKind.ADD;
|
|
@@ -57,6 +57,7 @@ import {
|
|
|
57
57
|
import type { ArtifactStorage } from "../artifact-storage.js";
|
|
58
58
|
import { bytesLookBinary } from "../file-change.js";
|
|
59
59
|
import { sha256Bytes, sha256Hex } from "./digest.js";
|
|
60
|
+
import { countLineChanges, type LineChangeCounts } from "./line-counts.js";
|
|
60
61
|
|
|
61
62
|
/**
|
|
62
63
|
* Reads an artifact's raw bytes back by storage key. Injected so this module
|
|
@@ -91,6 +92,14 @@ export interface CasCapturedFile {
|
|
|
91
92
|
readonly after?: CasBlobRef;
|
|
92
93
|
/** False when a side is binary — the change set cannot be approved as complete. */
|
|
93
94
|
readonly diffComplete: boolean;
|
|
95
|
+
/**
|
|
96
|
+
* Display `+N −M` counted from the in-memory bytes at capture — the only
|
|
97
|
+
* moment a CAS change's text exists outside blob storage. Absent for binary
|
|
98
|
+
* or oversized sides. Carried in memory to the CANDIDATE event only, never
|
|
99
|
+
* persisted in the manifest: the manifest is the reconcile's source of truth,
|
|
100
|
+
* and display decoration does not belong in an enforcement record.
|
|
101
|
+
*/
|
|
102
|
+
readonly lineCounts?: LineChangeCounts;
|
|
94
103
|
}
|
|
95
104
|
|
|
96
105
|
/** The durable manifest for one change set — the reconcile's source of truth. */
|
|
@@ -210,6 +219,7 @@ async function buildCasCapturedFile(
|
|
|
210
219
|
|
|
211
220
|
const isCreate = kind === FileChangeKind.ADD;
|
|
212
221
|
const isDelete = kind === FileChangeKind.DELETE;
|
|
222
|
+
const isBinary = beforeRef?.isBinary || afterRef?.isBinary;
|
|
213
223
|
|
|
214
224
|
return {
|
|
215
225
|
pathBefore: isCreate ? "" : path,
|
|
@@ -220,7 +230,13 @@ async function buildCasCapturedFile(
|
|
|
220
230
|
after: afterRef,
|
|
221
231
|
// Binary on either side means the diff cannot render as text; the change set
|
|
222
232
|
// then cannot be approved as complete (parity with the git substrate).
|
|
223
|
-
diffComplete: !
|
|
233
|
+
diffComplete: !isBinary,
|
|
234
|
+
// 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")),
|
|
224
240
|
};
|
|
225
241
|
}
|
|
226
242
|
|
|
@@ -42,6 +42,7 @@ import {
|
|
|
42
42
|
FileReviewFailureKind,
|
|
43
43
|
} from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
44
44
|
import { aggregateDigest, fileDigest, sha256Bytes } from "./digest.js";
|
|
45
|
+
import { countLineChanges, type LineChangeCounts } from "./line-counts.js";
|
|
45
46
|
import { looksBinary } from "../file-change.js";
|
|
46
47
|
|
|
47
48
|
// The runner is the author of capture/reconcile/failure events. "runner" (not
|
|
@@ -150,6 +151,15 @@ export interface CapturedChangeInput {
|
|
|
150
151
|
* this UNSPECIFIED. See {@link FileReviewBlockReason}.
|
|
151
152
|
*/
|
|
152
153
|
readonly blockedReason?: FileReviewBlockReason;
|
|
154
|
+
/**
|
|
155
|
+
* Display line counts for this change, for producers whose bodies are not
|
|
156
|
+
* inline here (the CAS substrate counts from the in-memory bytes before
|
|
157
|
+
* offloading them as blob refs). Omit for inline content —
|
|
158
|
+
* {@link buildCapturedFileChange} then counts the inline sides itself, with
|
|
159
|
+
* the same shared {@link countLineChanges}. Informational only, never folded
|
|
160
|
+
* into the digests.
|
|
161
|
+
*/
|
|
162
|
+
readonly lineCounts?: LineChangeCounts;
|
|
153
163
|
}
|
|
154
164
|
|
|
155
165
|
/** Normalize the string-shorthand to a {@link CapturedContent}. */
|
|
@@ -175,6 +185,37 @@ export function contentSha256(content: CapturedContent): string {
|
|
|
175
185
|
}
|
|
176
186
|
}
|
|
177
187
|
|
|
188
|
+
/**
|
|
189
|
+
* Count the display `+N −M` from a change's inline sides, or `undefined` when
|
|
190
|
+
* counting is not honest here: a side that exists without inline text (a `ref`
|
|
191
|
+
* whose bytes were already offloaded, or a `binary` side with no body), or
|
|
192
|
+
* inline text that would render as binary (the UI shows "Binary file changed"
|
|
193
|
+
* for it, never a line diff — a count would describe a diff nobody sees).
|
|
194
|
+
* An absent side is fine — it is the empty document of an ADD/DELETE.
|
|
195
|
+
*/
|
|
196
|
+
function countInlineSides(
|
|
197
|
+
before: CapturedContent | undefined,
|
|
198
|
+
after: CapturedContent | undefined,
|
|
199
|
+
): LineChangeCounts | undefined {
|
|
200
|
+
const beforeText = inlineTextOrNull(before);
|
|
201
|
+
const afterText = inlineTextOrNull(after);
|
|
202
|
+
if (beforeText === null || afterText === null) return undefined;
|
|
203
|
+
return countLineChanges(beforeText, afterText);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* A side's countable inline text: the text for a non-binary inline side,
|
|
208
|
+
* `undefined` for an absent side (countable as the empty document), and `null`
|
|
209
|
+
* when the side exists but cannot be counted (ref / binary / binary-looking).
|
|
210
|
+
*/
|
|
211
|
+
function inlineTextOrNull(
|
|
212
|
+
content: CapturedContent | undefined,
|
|
213
|
+
): string | undefined | null {
|
|
214
|
+
if (content === undefined) return undefined;
|
|
215
|
+
if (content.kind !== "inline" || looksBinary(content.text)) return null;
|
|
216
|
+
return content.text;
|
|
217
|
+
}
|
|
218
|
+
|
|
178
219
|
/** Build the proto {@link FileContent} for one content side (inline, ref, or binary). */
|
|
179
220
|
function toFileContent(content: CapturedContent): FileContent {
|
|
180
221
|
if (content.kind === "inline") {
|
|
@@ -212,12 +253,19 @@ function toFileContent(content: CapturedContent): FileContent {
|
|
|
212
253
|
* `file_digest`) over the captured bytes. Content may be inline (git) or a blob
|
|
213
254
|
* ref (CAS) — the digests are identical either way, so the aggregate digest and
|
|
214
255
|
* the reconcile enforcement compose across both substrates.
|
|
256
|
+
*
|
|
257
|
+
* Display line counts are stamped here too — the single seam every substrate
|
|
258
|
+
* flows through — from `input.lineCounts` when the producer counted at its own
|
|
259
|
+
* source (CAS), else counted from the inline sides. Counting happens BEFORE any
|
|
260
|
+
* persist-time offload, so the counts survive a body being elided later. They
|
|
261
|
+
* never enter `fileDigest` (informational, not enforcement).
|
|
215
262
|
*/
|
|
216
263
|
export function buildCapturedFileChange(input: CapturedChangeInput): CapturedFileChange {
|
|
217
264
|
const before = normalizeContent(input.before);
|
|
218
265
|
const after = normalizeContent(input.after);
|
|
219
266
|
const beforeSha256 = before ? contentSha256(before) : "";
|
|
220
267
|
const afterSha256 = after ? contentSha256(after) : "";
|
|
268
|
+
const counts = input.lineCounts ?? countInlineSides(before, after);
|
|
221
269
|
|
|
222
270
|
const fc = create(CapturedFileChangeSchema, {
|
|
223
271
|
id: input.id,
|
|
@@ -229,6 +277,8 @@ export function buildCapturedFileChange(input: CapturedChangeInput): CapturedFil
|
|
|
229
277
|
afterSha256,
|
|
230
278
|
diffComplete: input.diffComplete ?? true,
|
|
231
279
|
blockedReason: input.blockedReason ?? FileReviewBlockReason.UNSPECIFIED,
|
|
280
|
+
linesAdded: counts?.linesAdded ?? 0,
|
|
281
|
+
linesRemoved: counts?.linesRemoved ?? 0,
|
|
232
282
|
fileDigest: fileDigest({
|
|
233
283
|
pathBefore: input.pathBefore,
|
|
234
284
|
pathAfter: input.pathAfter,
|
|
@@ -308,6 +308,81 @@ export async function captureChangeSet(
|
|
|
308
308
|
return { baselineTree, afterTree, changes };
|
|
309
309
|
}
|
|
310
310
|
|
|
311
|
+
/**
|
|
312
|
+
* One file's slim, content-free progress delta: paths + kind + line counts, NO
|
|
313
|
+
* bytes. The mid-run analogue of {@link GitSubstrateChange} (which carries the
|
|
314
|
+
* before/after {@link CapturedContent}); this deliberately carries neither side,
|
|
315
|
+
* so it can never leak content. `linesAdded`/`linesRemoved` are 0 when
|
|
316
|
+
* uncountable (a binary file — numstat reports `-`).
|
|
317
|
+
*/
|
|
318
|
+
export interface GitProgressEntry {
|
|
319
|
+
/** Path before the change (repo-relative). Empty for a CREATE. */
|
|
320
|
+
readonly pathBefore: string;
|
|
321
|
+
/** Path after the change (repo-relative). Empty for a DELETE. */
|
|
322
|
+
readonly pathAfter: string;
|
|
323
|
+
readonly changeType: FileChangeType;
|
|
324
|
+
readonly linesAdded: number;
|
|
325
|
+
readonly linesRemoved: number;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** Result of {@link captureProgressDelta}. */
|
|
329
|
+
export interface ProgressDelta {
|
|
330
|
+
/** The post-diff working-tree sha, for the caller's short-circuit cache. */
|
|
331
|
+
readonly afterTree: string;
|
|
332
|
+
/** One count-only entry per changed file (no content). */
|
|
333
|
+
readonly entries: readonly GitProgressEntry[];
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Capture a NON-AUTHORITATIVE, content-free snapshot of the working-tree delta
|
|
338
|
+
* against `baselineTree` — the substrate for the live "N files changed so far"
|
|
339
|
+
* surface (mid-run live capture). Unlike {@link captureChangeSet} this reads NO
|
|
340
|
+
* file bytes: it stages the working tree into a temp index, writes its tree, and
|
|
341
|
+
* asks git for `--numstat` + `--name-status` only. Cheap enough to run on a
|
|
342
|
+
* debounce during the turn.
|
|
343
|
+
*
|
|
344
|
+
* Short-circuit: returns `undefined` when the working tree is unchanged since
|
|
345
|
+
* `lastTreeSha` (the caller passes back the previous result's `afterTree`), so a
|
|
346
|
+
* quiet turn pays only one `write-tree`, never a diff.
|
|
347
|
+
*
|
|
348
|
+
* Uses a dedicated temp-index label ("progress") so it never collides with the
|
|
349
|
+
* `baseline`/`capture`/`approved` indexes. `--no-renames` matches
|
|
350
|
+
* {@link captureChangeSet} (a rename surfaces as delete + create). A torn read of
|
|
351
|
+
* a file being written mid-turn is acceptable — the snapshot is non-authoritative
|
|
352
|
+
* and self-corrects on the next capture.
|
|
353
|
+
*/
|
|
354
|
+
export async function captureProgressDelta(
|
|
355
|
+
gitRoot: string,
|
|
356
|
+
executionId: string,
|
|
357
|
+
baselineTree: string,
|
|
358
|
+
excludePaths: readonly string[] = [],
|
|
359
|
+
lastTreeSha?: string,
|
|
360
|
+
): Promise<ProgressDelta | undefined> {
|
|
361
|
+
const gitDir = await resolveGitDir(gitRoot);
|
|
362
|
+
const afterTree = await writeWorkingTree(gitRoot, gitDir, "progress", executionId, excludePaths);
|
|
363
|
+
if (lastTreeSha !== undefined && afterTree === lastTreeSha) return undefined;
|
|
364
|
+
|
|
365
|
+
const [nameStatusRaw, numstatRaw] = await Promise.all([
|
|
366
|
+
git(gitRoot, ["diff", "--no-renames", "--name-status", "-z", baselineTree, afterTree]),
|
|
367
|
+
git(gitRoot, ["diff", "--no-renames", "--numstat", "-z", baselineTree, afterTree]),
|
|
368
|
+
]);
|
|
369
|
+
|
|
370
|
+
const counts = parseNumstatZ(numstatRaw);
|
|
371
|
+
const entries: GitProgressEntry[] = [];
|
|
372
|
+
for (const { status, path } of parseNameStatusZ(nameStatusRaw)) {
|
|
373
|
+
const changeType = nameStatusToChangeType(status);
|
|
374
|
+
const count = counts.get(path);
|
|
375
|
+
entries.push({
|
|
376
|
+
pathBefore: changeType === FileChangeType.CREATE ? "" : path,
|
|
377
|
+
pathAfter: changeType === FileChangeType.DELETE ? "" : path,
|
|
378
|
+
changeType,
|
|
379
|
+
linesAdded: count?.added ?? 0,
|
|
380
|
+
linesRemoved: count?.removed ?? 0,
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
return { afterTree, entries };
|
|
384
|
+
}
|
|
385
|
+
|
|
311
386
|
/**
|
|
312
387
|
* Restore the given files to their `baselineTree` (pre-turn) bytes. On resume
|
|
313
388
|
* this reverts the REJECTED/undecided subset so a discarded change snaps back
|
|
@@ -462,6 +537,39 @@ function parseNameStatusZ(raw: string): NameStatusEntry[] {
|
|
|
462
537
|
return entries;
|
|
463
538
|
}
|
|
464
539
|
|
|
540
|
+
/**
|
|
541
|
+
* Parse `git diff --numstat -z` into a path -> {added, removed} map. Each record
|
|
542
|
+
* is a NUL-terminated `<added>\t<removed>\t<path>` triple; a binary file reports
|
|
543
|
+
* `-` for both counts, which we surface as 0 (no count) per the display contract.
|
|
544
|
+
*/
|
|
545
|
+
function parseNumstatZ(raw: string): Map<string, { added: number; removed: number }> {
|
|
546
|
+
const counts = new Map<string, { added: number; removed: number }>();
|
|
547
|
+
for (const record of raw.split("\u0000")) {
|
|
548
|
+
if (!record) continue;
|
|
549
|
+
const tab = record.indexOf("\t");
|
|
550
|
+
const tab2 = record.indexOf("\t", tab + 1);
|
|
551
|
+
if (tab < 0 || tab2 < 0) continue;
|
|
552
|
+
const addedStr = record.slice(0, tab);
|
|
553
|
+
const removedStr = record.slice(tab + 1, tab2);
|
|
554
|
+
const path = record.slice(tab2 + 1);
|
|
555
|
+
if (!path) continue;
|
|
556
|
+
const added = addedStr === "-" ? 0 : Number.parseInt(addedStr, 10) || 0;
|
|
557
|
+
const removed = removedStr === "-" ? 0 : Number.parseInt(removedStr, 10) || 0;
|
|
558
|
+
counts.set(path, { added, removed });
|
|
559
|
+
}
|
|
560
|
+
return counts;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Map a `--name-status` status letter to a {@link FileChangeType} — the same
|
|
565
|
+
* A -> CREATE, D -> DELETE, else MODIFY rule {@link buildCapturedChange} uses.
|
|
566
|
+
*/
|
|
567
|
+
function nameStatusToChangeType(status: string): FileChangeType {
|
|
568
|
+
if (status === "A") return FileChangeType.CREATE;
|
|
569
|
+
if (status === "D") return FileChangeType.DELETE;
|
|
570
|
+
return FileChangeType.MODIFY;
|
|
571
|
+
}
|
|
572
|
+
|
|
465
573
|
/** Read a blob's exact BYTES from a tree, or `undefined` when absent. */
|
|
466
574
|
async function readBlobBytes(
|
|
467
575
|
gitRoot: string,
|
|
@@ -33,7 +33,28 @@ export {
|
|
|
33
33
|
type CaptureResumeResult,
|
|
34
34
|
} from "./capture.js";
|
|
35
35
|
|
|
36
|
-
export {
|
|
36
|
+
export {
|
|
37
|
+
captureProgressDelta,
|
|
38
|
+
isGitWorkTree,
|
|
39
|
+
type GitProgressEntry,
|
|
40
|
+
type ProgressDelta,
|
|
41
|
+
} from "./git-substrate.js";
|
|
42
|
+
|
|
43
|
+
export {
|
|
44
|
+
buildFileChangeProgress,
|
|
45
|
+
captureFileChangeProgress,
|
|
46
|
+
newProgressCaptureState,
|
|
47
|
+
PROGRESS_CAPTURE_MIN_INTERVAL_MS,
|
|
48
|
+
PROGRESS_MAX_ENTRIES,
|
|
49
|
+
shouldCaptureProgress,
|
|
50
|
+
type ProgressCaptureState,
|
|
51
|
+
} from "./progress.js";
|
|
52
|
+
|
|
53
|
+
export {
|
|
54
|
+
countLineChanges,
|
|
55
|
+
LINE_COUNT_MAX_BYTES,
|
|
56
|
+
type LineChangeCounts,
|
|
57
|
+
} from "./line-counts.js";
|
|
37
58
|
|
|
38
59
|
export {
|
|
39
60
|
applyCasApproved,
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capture-time line counting for the file-review subsystem: the `+N −M` stamped
|
|
3
|
+
* onto each {@link CapturedFileChange} so review surfaces can decorate file
|
|
4
|
+
* lists without the file bodies.
|
|
5
|
+
*
|
|
6
|
+
* WHY THE RUNNER COUNTS, AND WHY WITH JSDIFF. The counts exist because the
|
|
7
|
+
* review UI's compact file lists must show per-file magnitude, but by the time
|
|
8
|
+
* the UI renders, large before/after bodies may have been offloaded to artifact
|
|
9
|
+
* storage (status-offload) — a client cannot count what it would first have to
|
|
10
|
+
* download. So the counts are computed once, at capture, where the bytes are
|
|
11
|
+
* always in hand. They are computed with the SAME `diff` (jsdiff) Myers
|
|
12
|
+
* algorithm the SDK's diff renderer uses (`computeDiff` → `structuredPatch`),
|
|
13
|
+
* NOT with `git diff --numstat`: two counting authorities can disagree on edge
|
|
14
|
+
* cases (trailing newlines being the classic), and a list that says `+37 −1`
|
|
15
|
+
* beside a rendered diff showing `+37 −0` is a self-contradicting UI. One
|
|
16
|
+
* algorithm, consistent by construction.
|
|
17
|
+
*
|
|
18
|
+
* The counts are INFORMATIONAL display data only — never an enforcement input,
|
|
19
|
+
* never folded into `file_digest`/`aggregate_digest` (the same contract as
|
|
20
|
+
* `blocked_reason`).
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { structuredPatch } from "diff";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Per-side byte ceiling above which counting is skipped. Myers is O(ND) and a
|
|
27
|
+
* pathological many-MB text pair must never stall the capture activity just to
|
|
28
|
+
* decorate a list; above this the counts stay absent and the UI hides the stat.
|
|
29
|
+
* Generous relative to the 128 KiB per-side inline-persist cap — a file can be
|
|
30
|
+
* several times that and still count quickly — while bounding the worst case.
|
|
31
|
+
*/
|
|
32
|
+
export const LINE_COUNT_MAX_BYTES = 1024 * 1024;
|
|
33
|
+
|
|
34
|
+
/** The `+N −M` pair for one file change. */
|
|
35
|
+
export interface LineChangeCounts {
|
|
36
|
+
readonly linesAdded: number;
|
|
37
|
+
readonly linesRemoved: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Count the added/removed lines between two text sides, exactly as the SDK's
|
|
42
|
+
* diff renderer would show them.
|
|
43
|
+
*
|
|
44
|
+
* An absent side is the empty document — an ADD counts every `after` line as
|
|
45
|
+
* added, a DELETE every `before` line as removed — mirroring how the renderer
|
|
46
|
+
* diffs a create/delete. `` markers are metadata,
|
|
47
|
+
* not content, and are excluded (the same rule as the SDK's `mapPatchHunks`).
|
|
48
|
+
*
|
|
49
|
+
* Returns `undefined` when counting is not possible or meaningful: both sides
|
|
50
|
+
* absent, or either side over {@link LINE_COUNT_MAX_BYTES}. Callers leave the
|
|
51
|
+
* proto counts at zero in that case, which consumers render as "no stat".
|
|
52
|
+
*/
|
|
53
|
+
export function countLineChanges(
|
|
54
|
+
before: string | undefined,
|
|
55
|
+
after: string | undefined,
|
|
56
|
+
): LineChangeCounts | undefined {
|
|
57
|
+
if (before === undefined && after === undefined) return undefined;
|
|
58
|
+
const beforeText = before ?? "";
|
|
59
|
+
const afterText = after ?? "";
|
|
60
|
+
if (
|
|
61
|
+
Buffer.byteLength(beforeText, "utf8") > LINE_COUNT_MAX_BYTES ||
|
|
62
|
+
Buffer.byteLength(afterText, "utf8") > LINE_COUNT_MAX_BYTES
|
|
63
|
+
) {
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Zero context: the +/- line population is independent of context width, and
|
|
68
|
+
// omitting context lines keeps the walk (and the patch object) minimal.
|
|
69
|
+
const patch = structuredPatch("a", "b", beforeText, afterText, "", "", {
|
|
70
|
+
context: 0,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
let linesAdded = 0;
|
|
74
|
+
let linesRemoved = 0;
|
|
75
|
+
for (const hunk of patch.hunks) {
|
|
76
|
+
for (const line of hunk.lines) {
|
|
77
|
+
if (line.startsWith("\\")) continue;
|
|
78
|
+
if (line[0] === "+") linesAdded++;
|
|
79
|
+
else if (line[0] === "-") linesRemoved++;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return { linesAdded, linesRemoved };
|
|
83
|
+
}
|