@yagni-app/code-staging 1.2.7-staging.2056.1 → 1.2.7-staging.2062.1
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/extension/pipeline/changeScan.d.ts +10 -3
- package/dist/extension/pipeline/changeScan.js +7 -5
- package/dist/extension/pipeline/cloud.d.ts +4 -2
- package/dist/extension/pipeline/cloud.js +11 -5
- package/dist/extension/pipeline/finish.d.ts +9 -1
- package/dist/extension/pipeline/finish.js +45 -15
- package/dist/extension/pipeline/headlessGo.js +24 -10
- package/dist/extension/pipeline/personas.d.ts +5 -4
- package/dist/extension/pipeline/personas.js +21 -7
- package/dist/extension/pipeline/types.d.ts +6 -0
- package/dist/extension/pipeline/verify.d.ts +27 -8
- package/dist/extension/pipeline/verify.js +25 -14
- package/dist/extension/pipeline/workspace.d.ts +6 -0
- package/dist/extension/pipeline/workspace.js +12 -0
- package/package.json +2 -2
|
@@ -52,11 +52,18 @@ export interface ChangeScanOpts {
|
|
|
52
52
|
/** This session's own credentials, matched by value. */
|
|
53
53
|
sessionSecrets?: readonly string[];
|
|
54
54
|
readFile?: (path: string) => Buffer;
|
|
55
|
+
/**
|
|
56
|
+
* The commit the run started from: what it committed past it is scanned
|
|
57
|
+
* too, since a model that commits leaves nothing on top of HEAD (YAG-1039).
|
|
58
|
+
* Absent: HEAD.
|
|
59
|
+
*/
|
|
60
|
+
base?: string;
|
|
55
61
|
}
|
|
56
62
|
/**
|
|
57
|
-
* Scan what this run added on top of
|
|
58
|
-
* files whole, minus `skip` (the tree's
|
|
59
|
-
* not say what changed: the gate then
|
|
63
|
+
* Scan what this run added on top of its base (HEAD, unless `opts.base`):
|
|
64
|
+
* tracked files' added lines, new files whole, minus `skip` (the tree's
|
|
65
|
+
* pre-run dirt). Null when git could not say what changed: the gate then
|
|
66
|
+
* notes the scan did not run.
|
|
60
67
|
*/
|
|
61
68
|
export declare function scanChange(cwd: string, opts: ChangeScanOpts, skip?: readonly string[], signal?: AbortSignal): Promise<Finding[] | null>;
|
|
62
69
|
//# sourceMappingURL=changeScan.d.ts.map
|
|
@@ -253,9 +253,10 @@ function bytesAreBinary(bytes) {
|
|
|
253
253
|
/** How much of one new file is scanned: a credential sits in source, not past the first megabyte. */
|
|
254
254
|
const MAX_SCANNED_FILE_BYTES = 1024 * 1024;
|
|
255
255
|
/**
|
|
256
|
-
* Scan what this run added on top of
|
|
257
|
-
* files whole, minus `skip` (the tree's
|
|
258
|
-
* not say what changed: the gate then
|
|
256
|
+
* Scan what this run added on top of its base (HEAD, unless `opts.base`):
|
|
257
|
+
* tracked files' added lines, new files whole, minus `skip` (the tree's
|
|
258
|
+
* pre-run dirt). Null when git could not say what changed: the gate then
|
|
259
|
+
* notes the scan did not run.
|
|
259
260
|
*/
|
|
260
261
|
export async function scanChange(cwd, opts, skip = [], signal) {
|
|
261
262
|
// `git status --porcelain` lists an untracked folder as `dir/`, so a skip
|
|
@@ -267,10 +268,11 @@ export async function scanChange(cwd, opts, skip = [], signal) {
|
|
|
267
268
|
has: (path) => skipFiles.has(path) || skipDirs.some((dir) => path.startsWith(dir)),
|
|
268
269
|
};
|
|
269
270
|
const git = (args) => opts.exec(["git", "-c", "core.quotepath=off", ...args], cwd, signal);
|
|
271
|
+
const base = opts.base ?? "HEAD";
|
|
270
272
|
// A deleted binary is no binary in the change: the host lets it through.
|
|
271
273
|
const [numstat, diff, others] = await Promise.all([
|
|
272
|
-
git(["diff",
|
|
273
|
-
git(["diff",
|
|
274
|
+
git(["diff", base, "--numstat", "--no-renames", "--no-ext-diff", "--diff-filter=d"]),
|
|
275
|
+
git(["diff", base, "-U0", "--no-color", "--no-renames", "--no-ext-diff"]),
|
|
274
276
|
git(["ls-files", "--others", "--exclude-standard"]),
|
|
275
277
|
]);
|
|
276
278
|
if (numstat.code !== 0 || diff.code !== 0 || others.code !== 0)
|
|
@@ -190,8 +190,10 @@ export interface CloudCompletion {
|
|
|
190
190
|
* commits its work locally on the branch the host checked out, so nothing is
|
|
191
191
|
* lost when the sandbox goes. `result.json` is written last; the caller mirrors
|
|
192
192
|
* it on the stream. `ok` is a written plan, or a clean candidate whose branch
|
|
193
|
-
* carries this attempt's committed work
|
|
194
|
-
*
|
|
193
|
+
* carries this attempt's committed work: a commit past the execution's base,
|
|
194
|
+
* whoever wrote it (a model that ran `git commit` too, YAG-1039), or an
|
|
195
|
+
* earlier execution's. A clean stop with nothing committed at all delivered
|
|
196
|
+
* nothing.
|
|
195
197
|
*/
|
|
196
198
|
export declare function completeCloudSession(c: CloudCompletion, ran: {
|
|
197
199
|
result: PipelineResult;
|
|
@@ -247,8 +247,10 @@ function findingKey(finding) {
|
|
|
247
247
|
* commits its work locally on the branch the host checked out, so nothing is
|
|
248
248
|
* lost when the sandbox goes. `result.json` is written last; the caller mirrors
|
|
249
249
|
* it on the stream. `ok` is a written plan, or a clean candidate whose branch
|
|
250
|
-
* carries this attempt's committed work
|
|
251
|
-
*
|
|
250
|
+
* carries this attempt's committed work: a commit past the execution's base,
|
|
251
|
+
* whoever wrote it (a model that ran `git commit` too, YAG-1039), or an
|
|
252
|
+
* earlier execution's. A clean stop with nothing committed at all delivered
|
|
253
|
+
* nothing.
|
|
252
254
|
*/
|
|
253
255
|
export async function completeCloudSession(c, ran) {
|
|
254
256
|
const notes = "error" in ran ? [ran.error] : [];
|
|
@@ -334,21 +336,25 @@ export async function completeCloudSession(c, ran) {
|
|
|
334
336
|
rounds: result?.rounds.length ?? 0,
|
|
335
337
|
openPr: false,
|
|
336
338
|
...(c.preRunDirt.length > 0 ? { baselinePaths: c.preRunDirt } : {}),
|
|
339
|
+
...(c.baseSha ? { baseSha: c.baseSha } : {}),
|
|
337
340
|
exec: c.exec,
|
|
338
341
|
});
|
|
339
342
|
committed = finish.committed;
|
|
340
343
|
notes.push(...finish.notes);
|
|
341
344
|
}
|
|
345
|
+
const facts = await gitFacts(c.cwd, c.exec, c.baseSha);
|
|
346
|
+
notes.push(...facts.notes);
|
|
342
347
|
let delivered = outputsWritten;
|
|
343
348
|
if (c.stage !== "plan") {
|
|
344
|
-
|
|
349
|
+
// A commit past the base is this execution's work, whoever wrote it.
|
|
350
|
+
const onBranch = committed ||
|
|
351
|
+
facts.commits.length > 0 ||
|
|
352
|
+
(await attemptCommitted(c.cwd, c.exec, c.identity.attemptId));
|
|
345
353
|
if (onBranch === null) {
|
|
346
354
|
notes.push("git log failed, so the result cannot say whether the branch carries this attempt's work.");
|
|
347
355
|
}
|
|
348
356
|
delivered = onBranch === true;
|
|
349
357
|
}
|
|
350
|
-
const facts = await gitFacts(c.cwd, c.exec, c.baseSha);
|
|
351
|
-
notes.push(...facts.notes);
|
|
352
358
|
// What the final scan and checks found beside the last round's findings; a
|
|
353
359
|
// finding both report is listed once.
|
|
354
360
|
const seen = new Set((result?.findings ?? []).map(findingKey));
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* branch (the tree started clean, so everything is the run's own). In --here
|
|
8
8
|
* mode it commits ONLY when the pre-run baseline was verifiably clean;
|
|
9
9
|
* otherwise it skips with an honest note (never mixing user dirt into an
|
|
10
|
-
* agent commit).
|
|
10
|
+
* agent commit). A run given its base commit (the cloud session) whose work
|
|
11
|
+
* is already committed past it gets the trailer on an empty commit instead.
|
|
11
12
|
* - Commit message: `<type>(<scope>): <title>` + a 2-3 sentence plan summary +
|
|
12
13
|
* the exact provenance trailer (YAGNI-Run / YAGNI-Ticket / YAGNI-Verify /
|
|
13
14
|
* YAGNI-Decisions / YAGNI-Rounds / Co-Authored-By: YAGNI Code).
|
|
@@ -72,6 +73,13 @@ export interface RunFinishOpts extends TrailerInputs {
|
|
|
72
73
|
* lands in the run's provenance commit. Absent ⇒ the whole diff is the run's.
|
|
73
74
|
*/
|
|
74
75
|
baselinePaths?: string[];
|
|
76
|
+
/**
|
|
77
|
+
* The commit the run started from (a cloud session's). When the tree holds
|
|
78
|
+
* nothing of the run's own because the run already committed its work past
|
|
79
|
+
* it (the model ran `git commit`), FINISH puts the trailer on an empty
|
|
80
|
+
* commit on top, so the branch still carries the run's `YAGNI-Run` line.
|
|
81
|
+
*/
|
|
82
|
+
baseSha?: string;
|
|
75
83
|
signal?: AbortSignal;
|
|
76
84
|
exec?: WorktreeExec;
|
|
77
85
|
}
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* branch (the tree started clean, so everything is the run's own). In --here
|
|
8
8
|
* mode it commits ONLY when the pre-run baseline was verifiably clean;
|
|
9
9
|
* otherwise it skips with an honest note (never mixing user dirt into an
|
|
10
|
-
* agent commit).
|
|
10
|
+
* agent commit). A run given its base commit (the cloud session) whose work
|
|
11
|
+
* is already committed past it gets the trailer on an empty commit instead.
|
|
11
12
|
* - Commit message: `<type>(<scope>): <title>` + a 2-3 sentence plan summary +
|
|
12
13
|
* the exact provenance trailer (YAGNI-Run / YAGNI-Ticket / YAGNI-Verify /
|
|
13
14
|
* YAGNI-Decisions / YAGNI-Rounds / Co-Authored-By: YAGNI Code).
|
|
@@ -238,6 +239,27 @@ function tail(output) {
|
|
|
238
239
|
.trim();
|
|
239
240
|
return t;
|
|
240
241
|
}
|
|
242
|
+
/**
|
|
243
|
+
* The paths the run's commits past `baseSha` changed: empty with no base, no
|
|
244
|
+
* commit past it, or a change that nets to nothing; `failed` when git could
|
|
245
|
+
* not say, so a note never reads that as nothing committed.
|
|
246
|
+
*/
|
|
247
|
+
async function committedSince(run, baseSha) {
|
|
248
|
+
if (!baseSha)
|
|
249
|
+
return [];
|
|
250
|
+
const commits = await run(["git", "rev-list", `${baseSha}..HEAD`]);
|
|
251
|
+
if (commits.code !== 0)
|
|
252
|
+
return { failed: tail(commits.output) || "git rev-list failed" };
|
|
253
|
+
if (!commits.output.trim())
|
|
254
|
+
return [];
|
|
255
|
+
const diff = await run(["git", "diff", "--name-only", baseSha, "HEAD"]);
|
|
256
|
+
if (diff.code !== 0)
|
|
257
|
+
return { failed: tail(diff.output) || "git diff failed" };
|
|
258
|
+
return diff.output
|
|
259
|
+
.split("\n")
|
|
260
|
+
.map((line) => line.trim())
|
|
261
|
+
.filter(Boolean);
|
|
262
|
+
}
|
|
241
263
|
/**
|
|
242
264
|
* Run the FINISH stage. NEVER throws; every failure path returns an honest
|
|
243
265
|
* {@link FinishResult} with notes. See the module docblock for the contract.
|
|
@@ -273,33 +295,42 @@ export async function runFinish(opts) {
|
|
|
273
295
|
if (status.code !== 0) {
|
|
274
296
|
return skipped(`FINISH could not read the git status (${tail(status.output) || "git failed"}); nothing was committed.`);
|
|
275
297
|
}
|
|
276
|
-
if (!status.output.trim()) {
|
|
277
|
-
return skipped("FINISH found nothing to commit (the tree is already clean).");
|
|
278
|
-
}
|
|
279
298
|
// The run's OWN paths = everything changed now, minus what was already dirty
|
|
280
299
|
// at pipeline start (bootstrap lockfile churn, or a pre-existing edit). We
|
|
281
300
|
// commit only these, so the provenance commit never folds in non-run work.
|
|
282
301
|
const changed = parseChangedPaths(status.output);
|
|
283
302
|
const baseline = new Set(opts.baselinePaths ?? []);
|
|
284
303
|
const runOwned = baseline.size === 0 ? changed : changed.filter((p) => !baseline.has(p));
|
|
285
|
-
|
|
286
|
-
|
|
304
|
+
// Nothing of the run's own left in the tree, but its work may already be
|
|
305
|
+
// committed past its base: the trailer then goes on an empty commit.
|
|
306
|
+
const committed = runOwned.length === 0 ? await committedSince(run, opts.baseSha) : [];
|
|
307
|
+
if (!Array.isArray(committed)) {
|
|
308
|
+
return skipped(`FINISH could not read what the run committed past its base (${committed.failed}); nothing was committed.`);
|
|
309
|
+
}
|
|
310
|
+
if (runOwned.length === 0 && committed.length === 0) {
|
|
311
|
+
return skipped(status.output.trim()
|
|
312
|
+
? "FINISH found nothing of the run's own to commit (the only changes were already present before the run)."
|
|
313
|
+
: "FINISH found nothing to commit (the tree is already clean).");
|
|
287
314
|
}
|
|
315
|
+
const trailerOnly = runOwned.length === 0;
|
|
288
316
|
// Message derivation off the run's real diff.
|
|
289
|
-
const subject = commitSubject(commitType(opts.ticket, opts.ticketBrief), deriveCommitScope(runOwned), commitTitle(opts.ticket, opts.ticketBrief));
|
|
317
|
+
const subject = commitSubject(commitType(opts.ticket, opts.ticketBrief), deriveCommitScope(trailerOnly ? committed : runOwned), commitTitle(opts.ticket, opts.ticketBrief));
|
|
290
318
|
const summary = summarizePlan(opts.planText, opts.ticket);
|
|
291
319
|
const trailer = provenanceTrailer(opts);
|
|
292
320
|
const message = buildCommitMessage(subject, summary, trailer);
|
|
293
321
|
// No baseline dirt ⇒ everything is the run's own: `git add -A` (preserves
|
|
294
322
|
// rename detection). With a baseline, stage only the run-owned paths so
|
|
295
323
|
// bootstrap/pre-existing dirt never lands (`--` guards flag-like paths).
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
324
|
+
if (!trailerOnly) {
|
|
325
|
+
const add = baseline.size === 0
|
|
326
|
+
? await run(["git", "add", "-A"])
|
|
327
|
+
: await run(["git", "add", "--", ...runOwned]);
|
|
328
|
+
if (add.code !== 0) {
|
|
329
|
+
return skipped(`FINISH could not stage the changes (git add failed: ${tail(add.output)}); nothing was committed.`);
|
|
330
|
+
}
|
|
301
331
|
}
|
|
302
|
-
|
|
332
|
+
const commitArgs = [...(trailerOnly ? ["--allow-empty"] : []), "-m", message];
|
|
333
|
+
let commit = await run(["git", "commit", ...commitArgs]);
|
|
303
334
|
if (commit.code !== 0) {
|
|
304
335
|
// Most likely a missing git identity in a fresh environment (mirrors
|
|
305
336
|
// commitWip): retry once with an explicit YAGNI Code identity.
|
|
@@ -310,8 +341,7 @@ export async function runFinish(opts) {
|
|
|
310
341
|
"-c",
|
|
311
342
|
"user.email=code@yagni.app",
|
|
312
343
|
"commit",
|
|
313
|
-
|
|
314
|
-
message,
|
|
344
|
+
...commitArgs,
|
|
315
345
|
]);
|
|
316
346
|
}
|
|
317
347
|
if (commit.code !== 0) {
|
|
@@ -549,6 +549,23 @@ export async function runHeadlessGo(argv, deps = {}) {
|
|
|
549
549
|
// A cloud session reports the checks of the tree it commits: the gate's last
|
|
550
550
|
// verdict, with the tree it judged.
|
|
551
551
|
const snapshot = deps.snapshotWorkspace ?? snapshotWorkspace;
|
|
552
|
+
// A cloud session's start: the commit it builds on (its result's `baseSha`,
|
|
553
|
+
// which its gate and scan read its change against, since a model may commit
|
|
554
|
+
// its own work) and the tree's pre-run dirt (an install's lockfile churn),
|
|
555
|
+
// which never lands in its commit.
|
|
556
|
+
const startTree = cloud ? await snapshot(cwd, deps.signal) : undefined;
|
|
557
|
+
const baseSha = startTree?.head ?? "";
|
|
558
|
+
const preRunDirt = startTree ? parseChangedPaths(startTree.status) : [];
|
|
559
|
+
const onBase = baseSha ? { base: baseSha } : {};
|
|
560
|
+
// Its reviewer reads the change from that commit too (YAG-1041). A
|
|
561
|
+
// continuation starts past the attempt's earlier commits, so its start is
|
|
562
|
+
// not where the change begins: its reviewer keeps the bare `git diff`.
|
|
563
|
+
const reviewLane = lane && baseSha && !continuation
|
|
564
|
+
? {
|
|
565
|
+
...lane,
|
|
566
|
+
stages: lane.stages.map((s) => (s.id === "review" ? { ...s, diffBase: baseSha } : s)),
|
|
567
|
+
}
|
|
568
|
+
: lane;
|
|
552
569
|
let gate;
|
|
553
570
|
let lastVerify;
|
|
554
571
|
// A cloud session's gate also runs the host's secret and binary scans on
|
|
@@ -556,7 +573,7 @@ export async function runHeadlessGo(argv, deps = {}) {
|
|
|
556
573
|
const exec = deps.exec ?? finishExec;
|
|
557
574
|
const sessionSecrets = [env.YAGNI_TOKEN, env.GH_ENTERPRISE_TOKEN].filter((value) => Boolean(value));
|
|
558
575
|
const scan = cloud
|
|
559
|
-
? (scanCwd, signal, skip) => scanChange(scanCwd, { exec, sessionSecrets }, skip, signal)
|
|
576
|
+
? (scanCwd, signal, skip) => scanChange(scanCwd, { exec, sessionSecrets, ...onBase }, skip, signal)
|
|
560
577
|
: undefined;
|
|
561
578
|
if (gateOpts || baseline || cloud) {
|
|
562
579
|
const made = (deps.makeRunVerify ?? makeRunVerify)({
|
|
@@ -564,7 +581,10 @@ export async function runHeadlessGo(argv, deps = {}) {
|
|
|
564
581
|
...(baseline ? { baseline } : {}),
|
|
565
582
|
...(checksOnBase ? { checksOnBase } : {}),
|
|
566
583
|
...(scan ? { scanChange: scan } : {}),
|
|
567
|
-
|
|
584
|
+
// The one-attempt lane's host runs no checks after the build.
|
|
585
|
+
...(cloud
|
|
586
|
+
? { unreadTestFailureIsFinding: true, hostRechecks: false, ...(baseSha ? { baseSha } : {}) }
|
|
587
|
+
: {}),
|
|
568
588
|
...heard,
|
|
569
589
|
});
|
|
570
590
|
gate = made;
|
|
@@ -595,7 +615,7 @@ export async function runHeadlessGo(argv, deps = {}) {
|
|
|
595
615
|
...(budget ? { budget } : {}),
|
|
596
616
|
...(deps.stages ? { stages: deps.stages } : {}),
|
|
597
617
|
grounded: deps.grounded ?? grounding,
|
|
598
|
-
...(
|
|
618
|
+
...(reviewLane ? { lane: reviewLane } : {}),
|
|
599
619
|
...(mission ? { mission } : {}),
|
|
600
620
|
...(runVerify ? { runVerify } : {}),
|
|
601
621
|
...(baseline ? { runWorkstreamCheck: makeWorkstreamCheck({ baseline }) } : {}),
|
|
@@ -634,12 +654,6 @@ export async function runHeadlessGo(argv, deps = {}) {
|
|
|
634
654
|
emit({ type: "pipeline_log", event, ...(data === undefined ? {} : { data }) });
|
|
635
655
|
},
|
|
636
656
|
};
|
|
637
|
-
// A cloud session's start: the commit it builds on (its result's `baseSha`)
|
|
638
|
-
// and the tree's pre-run dirt (an install's lockfile churn), which never
|
|
639
|
-
// lands in its commit.
|
|
640
|
-
const startTree = cloud ? await snapshot(cwd, deps.signal) : undefined;
|
|
641
|
-
const baseSha = startTree?.head ?? "";
|
|
642
|
-
const preRunDirt = startTree ? parseChangedPaths(startTree.status) : [];
|
|
643
657
|
/** The cloud session's last act (cloud.ts `completeCloudSession`), mirrored on the stream. */
|
|
644
658
|
const completeCloud = async (session, ran) => {
|
|
645
659
|
const verifyGate = gate;
|
|
@@ -668,7 +682,7 @@ export async function runHeadlessGo(argv, deps = {}) {
|
|
|
668
682
|
},
|
|
669
683
|
}
|
|
670
684
|
: {}),
|
|
671
|
-
scan: () => scanChange(cwd, { exec, sessionSecrets }, preRunDirt),
|
|
685
|
+
scan: () => scanChange(cwd, { exec, sessionSecrets, ...onBase }, preRunDirt),
|
|
672
686
|
planPaths: (planText) => baselineDirs(cwd, planPaths(planText), readFile).map((dir) => relative(cwd, dir) || "."),
|
|
673
687
|
exec,
|
|
674
688
|
writeFile: deps.writeFile ?? writeFileSync,
|
|
@@ -50,10 +50,11 @@ export declare function builderPersonaClause(workstream?: PartitionWorkstream, s
|
|
|
50
50
|
* Resolve the system-prompt body for a stage. For non-review stages this is the
|
|
51
51
|
* role body; for the review stage it appends the lens-specific clause (or, with
|
|
52
52
|
* no lens, the one-reviewer clause covering all three) and the required
|
|
53
|
-
* findings output contract so the reviewer's output parses. A
|
|
54
|
-
* `
|
|
55
|
-
* carrying a `
|
|
56
|
-
*
|
|
53
|
+
* findings output contract so the reviewer's output parses. A review carrying
|
|
54
|
+
* a `diffBase` (a cloud session's) diffs from that commit in its first step. A
|
|
55
|
+
* stage carrying a `personaClause` (the fan-out builders) gets it appended
|
|
56
|
+
* last, and one carrying a `persona` (the cloud plan stage's host persona)
|
|
57
|
+
* runs with that in place of its agent's body.
|
|
57
58
|
*
|
|
58
59
|
* `grounded` defaults true (every real /go stage). The M6 eval passes
|
|
59
60
|
* `grounded: false` to select the grounding-free bodies + lens clauses for the
|
|
@@ -86,12 +86,22 @@ What was done.
|
|
|
86
86
|
Any record_decision calls you made, and why.
|
|
87
87
|
## Notes
|
|
88
88
|
Anything the reviewer should know (exact file paths, key functions touched).`;
|
|
89
|
+
/** The reviewer's first step, which a stage's `diffBase` rewrites (see `personaBody`). */
|
|
90
|
+
const REVIEWER_DIFF_STEP = "1. `git diff` to see the change.";
|
|
91
|
+
/**
|
|
92
|
+
* The reviewer's first step on a change read from `base`, the commit a cloud
|
|
93
|
+
* session started on: its model may have committed its work, which a bare
|
|
94
|
+
* `git diff` no longer shows (YAG-1041).
|
|
95
|
+
*/
|
|
96
|
+
function reviewerDiffStepFrom(base) {
|
|
97
|
+
return `1. \`git diff ${base}\` to see the change: everything this session changed since its base commit ${base}, committed or not (a bare \`git diff\` shows only what is not committed yet). \`git log ${base}..HEAD\` lists its commits, and \`git status\` lists new files git does not track yet.`;
|
|
98
|
+
}
|
|
89
99
|
const REVIEWER_BODY = `You are a senior, adversarial code reviewer. Analyze the change for real problems, not style nits. Bash is read-only here (\`git diff\`, \`git log\`, \`git show\`); do NOT modify files or run builds.
|
|
90
100
|
|
|
91
101
|
You are grounded in how THIS company works — that context is your edge over a repo-only reviewer.
|
|
92
102
|
|
|
93
103
|
Strategy:
|
|
94
|
-
|
|
104
|
+
${REVIEWER_DIFF_STEP}
|
|
95
105
|
2. Read the modified files and their surroundings.
|
|
96
106
|
3. Judge the change through your assigned lens (below).`;
|
|
97
107
|
const ADVISOR_BODY = `You are a senior advisor, brought in on ONE hard call by an agent that is already working the problem. You are the strongest model in this system and you are expensive, so you are consulted, not driven — the caller does the work, you supply the judgment.
|
|
@@ -300,7 +310,7 @@ Anything the reviewer should know (exact file paths, key functions touched).`;
|
|
|
300
310
|
const REVIEWER_BLIND = `You are a senior, adversarial code reviewer. Analyze the change for real problems, not style nits. Bash is read-only here (\`git diff\`, \`git log\`, \`git show\`); do NOT modify files or run builds.
|
|
301
311
|
|
|
302
312
|
Strategy:
|
|
303
|
-
|
|
313
|
+
${REVIEWER_DIFF_STEP}
|
|
304
314
|
2. Read the modified files and their surroundings.
|
|
305
315
|
3. Judge the change through your assigned lens (below).`;
|
|
306
316
|
const ADVISOR_BLIND = `You are a senior advisor, brought in on ONE hard call by an agent that is already working the problem. You are the strongest model in this system and you are expensive, so you are consulted, not driven — the caller does the work, you supply the judgment.
|
|
@@ -428,10 +438,11 @@ export function builderPersonaClause(workstream, siblings = []) {
|
|
|
428
438
|
* Resolve the system-prompt body for a stage. For non-review stages this is the
|
|
429
439
|
* role body; for the review stage it appends the lens-specific clause (or, with
|
|
430
440
|
* no lens, the one-reviewer clause covering all three) and the required
|
|
431
|
-
* findings output contract so the reviewer's output parses. A
|
|
432
|
-
* `
|
|
433
|
-
* carrying a `
|
|
434
|
-
*
|
|
441
|
+
* findings output contract so the reviewer's output parses. A review carrying
|
|
442
|
+
* a `diffBase` (a cloud session's) diffs from that commit in its first step. A
|
|
443
|
+
* stage carrying a `personaClause` (the fan-out builders) gets it appended
|
|
444
|
+
* last, and one carrying a `persona` (the cloud plan stage's host persona)
|
|
445
|
+
* runs with that in place of its agent's body.
|
|
435
446
|
*
|
|
436
447
|
* `grounded` defaults true (every real /go stage). The M6 eval passes
|
|
437
448
|
* `grounded: false` to select the grounding-free bodies + lens clauses for the
|
|
@@ -443,7 +454,10 @@ export function personaBody(stage, opts = {}) {
|
|
|
443
454
|
const base = stage.persona?.trim() || bodies[stage.agent];
|
|
444
455
|
if (!base)
|
|
445
456
|
throw new Error(`No persona body for agent "${stage.agent}"`);
|
|
446
|
-
const
|
|
457
|
+
const diffBase = stage.diffBase;
|
|
458
|
+
const parts = [
|
|
459
|
+
diffBase ? base.replace(REVIEWER_DIFF_STEP, () => reviewerDiffStepFrom(diffBase)) : base,
|
|
460
|
+
];
|
|
447
461
|
if (stage.id === "review") {
|
|
448
462
|
const clauses = grounded ? LENS_CLAUSES : BLIND_LENS_CLAUSES;
|
|
449
463
|
if (stage.lens)
|
|
@@ -72,6 +72,12 @@ export interface PipelineStage {
|
|
|
72
72
|
* plan stage takes the host's `plan-persona.md` (Reeve's persona) here.
|
|
73
73
|
*/
|
|
74
74
|
persona?: string;
|
|
75
|
+
/**
|
|
76
|
+
* The commit a review reads the change from (a cloud session's start,
|
|
77
|
+
* YAG-1041): its first step is `git diff <diffBase>`, since a model that
|
|
78
|
+
* commits its work leaves a bare `git diff` empty. Absent: `git diff`.
|
|
79
|
+
*/
|
|
80
|
+
diffBase?: string;
|
|
75
81
|
/**
|
|
76
82
|
* The stage's own clock, in milliseconds (`stageClock.ts`). Past it the
|
|
77
83
|
* child refuses every tool call, so the model writes its answer from what it
|
|
@@ -244,13 +244,19 @@ export declare function parseChangedPaths(status: string): string[];
|
|
|
244
244
|
*/
|
|
245
245
|
export declare function resolvePackageDirs(repoRoot: string, relPaths: string[], readFile: (path: string) => string): string[];
|
|
246
246
|
/**
|
|
247
|
-
* This run's OWN changed
|
|
248
|
-
*
|
|
249
|
-
*
|
|
250
|
-
* changed path predates the run drops out. Pure (the `readFile` seam keeps it so), so
|
|
251
|
-
* an unrelated dirty package can never retarget the gate off this run's real diff.
|
|
247
|
+
* This run's OWN changed paths: the porcelain `status`'s, plus `committed` (what
|
|
248
|
+
* the run already committed past its base, YAG-1039), MINUS `baselinePaths` (the
|
|
249
|
+
* tree's pre-run dirt). Pure, deduped.
|
|
252
250
|
*/
|
|
253
|
-
export declare function
|
|
251
|
+
export declare function ownChangedPaths(status: string, baselinePaths: string[], committed?: string[]): string[];
|
|
252
|
+
/**
|
|
253
|
+
* This run's OWN changed package dirs: the packages of {@link ownChangedPaths}, so a
|
|
254
|
+
* package touched only by `baselinePaths` (the tree's pre-run dirt) drops out. A
|
|
255
|
+
* package with a NEW change stays in scope even if it also had pre-run dirt. Pure
|
|
256
|
+
* (the `readFile` seam keeps it so), so an unrelated dirty package can never
|
|
257
|
+
* retarget the gate off this run's real diff.
|
|
258
|
+
*/
|
|
259
|
+
export declare function ownChangedDirs(repoRoot: string, status: string, baselinePaths: string[], readFile: (path: string) => string, committed?: string[]): string[];
|
|
254
260
|
/**
|
|
255
261
|
* Resolve the verify command(s) for a repo. An explicitly allowed `.yagni-code/verify`
|
|
256
262
|
* override wins (first non-comment, non-empty line, whitespace-split into argv; run
|
|
@@ -495,6 +501,19 @@ export interface MakeRunVerifyOpts {
|
|
|
495
501
|
* Recomputed each round so the scope follows the growing diff.
|
|
496
502
|
*/
|
|
497
503
|
changedDirs?: (cwd: string, signal?: AbortSignal, baselinePaths?: string[]) => Promise<string[]>;
|
|
504
|
+
/**
|
|
505
|
+
* The commit the run started from (the cloud session's): what the run
|
|
506
|
+
* committed past it counts as changed beside the uncommitted tree, in the
|
|
507
|
+
* default `changedDirs` and `changedPaths`, so a model that commits its own
|
|
508
|
+
* work keeps the gate's scope (YAG-1039). Absent: the tree alone.
|
|
509
|
+
*/
|
|
510
|
+
baseSha?: string;
|
|
511
|
+
/**
|
|
512
|
+
* Whether the host runs the approved checks again after the build, which the
|
|
513
|
+
* gate's note of checks it left unrun says. Absent: it does (the mission
|
|
514
|
+
* lane). The one-attempt lane's host does not, so its session passes false.
|
|
515
|
+
*/
|
|
516
|
+
hostRechecks?: boolean;
|
|
498
517
|
exec?: VerifyExec;
|
|
499
518
|
timeoutMs?: number;
|
|
500
519
|
/**
|
|
@@ -520,8 +539,8 @@ export interface MakeRunVerifyOpts {
|
|
|
520
539
|
onFailedChecks?: (results: FailedCheckResult[]) => void;
|
|
521
540
|
/**
|
|
522
541
|
* This run's own changed paths (repo-relative), which scope the CI jobs by
|
|
523
|
-
* their `paths` filters. Default: the same `git status`
|
|
524
|
-
* minus the pre-run dirt.
|
|
542
|
+
* their `paths` filters. Default: the same `git status` (and commits past
|
|
543
|
+
* `baseSha`) the scope reads, minus the pre-run dirt.
|
|
525
544
|
*/
|
|
526
545
|
changedPaths?: (cwd: string, signal?: AbortSignal, baselinePaths?: string[]) => Promise<string[]>;
|
|
527
546
|
/** Repository-checks env override for determinism in tests (default: buildRepoCheckEnv() per run). */
|
|
@@ -65,7 +65,7 @@ import { claimCovers } from "./fanout.js";
|
|
|
65
65
|
import { ciJobInScope } from "./missionChecks.js";
|
|
66
66
|
import { composeAbortSignal } from "./resilience.js";
|
|
67
67
|
import { scrubSecrets } from "./scrubSecrets.js";
|
|
68
|
-
import { snapshotWorkspace } from "./workspace.js";
|
|
68
|
+
import { committedPaths, snapshotWorkspace } from "./workspace.js";
|
|
69
69
|
/**
|
|
70
70
|
* Total wall-clock budget for a single verify run before it is abandoned
|
|
71
71
|
* (fail-open). Ten minutes until 2026-10-09; twenty now, matching the run
|
|
@@ -355,16 +355,23 @@ export function resolvePackageDirs(repoRoot, relPaths, readFile) {
|
|
|
355
355
|
return [...dirs].sort();
|
|
356
356
|
}
|
|
357
357
|
/**
|
|
358
|
-
* This run's OWN changed
|
|
359
|
-
*
|
|
360
|
-
*
|
|
361
|
-
* changed path predates the run drops out. Pure (the `readFile` seam keeps it so), so
|
|
362
|
-
* an unrelated dirty package can never retarget the gate off this run's real diff.
|
|
358
|
+
* This run's OWN changed paths: the porcelain `status`'s, plus `committed` (what
|
|
359
|
+
* the run already committed past its base, YAG-1039), MINUS `baselinePaths` (the
|
|
360
|
+
* tree's pre-run dirt). Pure, deduped.
|
|
363
361
|
*/
|
|
364
|
-
export function
|
|
362
|
+
export function ownChangedPaths(status, baselinePaths, committed = []) {
|
|
365
363
|
const baseline = new Set(baselinePaths);
|
|
366
|
-
|
|
367
|
-
|
|
364
|
+
return [...new Set([...parseChangedPaths(status), ...committed])].filter((p) => !baseline.has(p));
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* This run's OWN changed package dirs: the packages of {@link ownChangedPaths}, so a
|
|
368
|
+
* package touched only by `baselinePaths` (the tree's pre-run dirt) drops out. A
|
|
369
|
+
* package with a NEW change stays in scope even if it also had pre-run dirt. Pure
|
|
370
|
+
* (the `readFile` seam keeps it so), so an unrelated dirty package can never
|
|
371
|
+
* retarget the gate off this run's real diff.
|
|
372
|
+
*/
|
|
373
|
+
export function ownChangedDirs(repoRoot, status, baselinePaths, readFile, committed = []) {
|
|
374
|
+
return resolvePackageDirs(repoRoot, ownChangedPaths(status, baselinePaths, committed), readFile);
|
|
368
375
|
}
|
|
369
376
|
/**
|
|
370
377
|
* The highest-priority package.json script at `pkgDir` that is a string AND not a
|
|
@@ -1409,6 +1416,10 @@ function measuredBaseline(onBase, check) {
|
|
|
1409
1416
|
export function makeRunVerify(opts = {}) {
|
|
1410
1417
|
const detect = opts.detect ?? defaultDetect;
|
|
1411
1418
|
const detectTests = opts.detectTests ?? defaultDetectTests;
|
|
1419
|
+
// What the run committed past its base: a model that commits leaves the tree clean.
|
|
1420
|
+
const committedOf = (cwd, head, signal) => opts.baseSha && head !== opts.baseSha
|
|
1421
|
+
? committedPaths(cwd, opts.baseSha, signal)
|
|
1422
|
+
: Promise.resolve([]);
|
|
1412
1423
|
const changedDirs = opts.changedDirs ??
|
|
1413
1424
|
(async (cwd, signal, baselinePaths) => {
|
|
1414
1425
|
const snapshot = await snapshotWorkspace(cwd, signal);
|
|
@@ -1417,7 +1428,7 @@ export function makeRunVerify(opts = {}) {
|
|
|
1417
1428
|
// This run's OWN changed packages = the current diff minus the pre-run dirt, so
|
|
1418
1429
|
// a stray pre-existing file (or an unrelated dirty package) never pulls its dir
|
|
1419
1430
|
// into scope and the gate verifies what THIS run actually touched.
|
|
1420
|
-
return ownChangedDirs(cwd, snapshot.status, baselinePaths ?? [], (p) => readFileSync(p, "utf8"));
|
|
1431
|
+
return ownChangedDirs(cwd, snapshot.status, baselinePaths ?? [], (p) => readFileSync(p, "utf8"), await committedOf(cwd, snapshot.head, signal));
|
|
1421
1432
|
});
|
|
1422
1433
|
const exec = opts.exec ?? defaultExec;
|
|
1423
1434
|
const timeoutMs = opts.timeoutMs ?? VERIFY_TIMEOUT_MS;
|
|
@@ -1428,8 +1439,7 @@ export function makeRunVerify(opts = {}) {
|
|
|
1428
1439
|
const snapshot = await snapshotWorkspace(cwd, signal);
|
|
1429
1440
|
if (!snapshot.tracked)
|
|
1430
1441
|
return [];
|
|
1431
|
-
|
|
1432
|
-
return parseChangedPaths(snapshot.status).filter((p) => !baseline.has(p));
|
|
1442
|
+
return ownChangedPaths(snapshot.status, baselinePaths ?? [], await committedOf(cwd, snapshot.head, signal));
|
|
1433
1443
|
});
|
|
1434
1444
|
const toolchain = opts.toolchain ?? sessionToolchain;
|
|
1435
1445
|
const treeStatus = opts.treeStatus ??
|
|
@@ -1439,6 +1449,7 @@ export function makeRunVerify(opts = {}) {
|
|
|
1439
1449
|
// keys from the last call it failed in.
|
|
1440
1450
|
let repoChecksSpentMs = 0;
|
|
1441
1451
|
const lastCiFailure = new Map();
|
|
1452
|
+
const hostRechecks = opts.hostRechecks === false ? "" : "; the host still runs the approved checks after the build";
|
|
1442
1453
|
// Each approved check's last run in this build, by command (trimmed).
|
|
1443
1454
|
const lastApproved = new Map();
|
|
1444
1455
|
/**
|
|
@@ -1565,7 +1576,7 @@ export function makeRunVerify(opts = {}) {
|
|
|
1565
1576
|
}
|
|
1566
1577
|
const buildLeft = (opts.repoChecksBuildBudgetMs ?? REPO_CHECKS_BUILD_BUDGET_MS) - repoChecksSpentMs;
|
|
1567
1578
|
if (buildLeft < REPO_CHECK_MIN_START_MS) {
|
|
1568
|
-
reasons.push(
|
|
1579
|
+
reasons.push(`approved checks and CI steps not run again: the build's check budget is spent${hostRechecks}`);
|
|
1569
1580
|
return {
|
|
1570
1581
|
findings,
|
|
1571
1582
|
reasons,
|
|
@@ -1619,7 +1630,7 @@ export function makeRunVerify(opts = {}) {
|
|
|
1619
1630
|
}
|
|
1620
1631
|
}
|
|
1621
1632
|
if (run.notReached > 0) {
|
|
1622
|
-
reasons.push(`${run.notReached} approved check(s) or CI step(s) not reached in the gate's time budget
|
|
1633
|
+
reasons.push(`${run.notReached} approved check(s) or CI step(s) not reached in the gate's time budget${hostRechecks}`);
|
|
1623
1634
|
}
|
|
1624
1635
|
const approvedRan = new Set(run.approvedRuns.values());
|
|
1625
1636
|
const whyNotRun = signal?.aborted
|
|
@@ -29,4 +29,10 @@ export declare function workspaceChanged(before: WorkspaceSnapshot, after: Works
|
|
|
29
29
|
* report `tracked: false` and skip the second call.
|
|
30
30
|
*/
|
|
31
31
|
export declare function snapshotWorkspace(cwd: string, signal?: AbortSignal): Promise<WorkspaceSnapshot>;
|
|
32
|
+
/**
|
|
33
|
+
* The repo-relative paths committed between `baseSha` and HEAD: what a run
|
|
34
|
+
* committed past its base. Empty on any git error, so a caller reads the
|
|
35
|
+
* uncommitted tree alone, as it did before.
|
|
36
|
+
*/
|
|
37
|
+
export declare function committedPaths(cwd: string, baseSha: string, signal?: AbortSignal): Promise<string[]>;
|
|
32
38
|
//# sourceMappingURL=workspace.d.ts.map
|
|
@@ -70,4 +70,16 @@ export async function snapshotWorkspace(cwd, signal) {
|
|
|
70
70
|
const status = await runGit(["status", "--porcelain"], cwd, signal);
|
|
71
71
|
return { tracked: true, head, status };
|
|
72
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* The repo-relative paths committed between `baseSha` and HEAD: what a run
|
|
75
|
+
* committed past its base. Empty on any git error, so a caller reads the
|
|
76
|
+
* uncommitted tree alone, as it did before.
|
|
77
|
+
*/
|
|
78
|
+
export async function committedPaths(cwd, baseSha, signal) {
|
|
79
|
+
const names = await runGit(["diff", "--name-only", baseSha, "HEAD"], cwd, signal);
|
|
80
|
+
return names
|
|
81
|
+
.split("\n")
|
|
82
|
+
.map((line) => line.trim())
|
|
83
|
+
.filter(Boolean);
|
|
84
|
+
}
|
|
73
85
|
//# sourceMappingURL=workspace.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yagni-app/code-staging",
|
|
3
|
-
"version": "1.2.7-staging.
|
|
3
|
+
"version": "1.2.7-staging.2062.1",
|
|
4
4
|
"description": "YAGNI Code: a terminal coding agent that already knows your company. One YAGNI login routes the model and grounds the agent in your team's context.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
6
6
|
"author": "YAGNI, Inc. <jack@yagni.app> (https://yagni.app)",
|
|
@@ -58,5 +58,5 @@
|
|
|
58
58
|
"turndown": "^7.2.4",
|
|
59
59
|
"typebox": "^1.3.15"
|
|
60
60
|
},
|
|
61
|
-
"yagniSourceSha": "
|
|
61
|
+
"yagniSourceSha": "a52f06325dd8542ceb7055b6fc9a7c965c8c77bd"
|
|
62
62
|
}
|