@wildorder/nightshift 0.6.0 → 0.7.0
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/LICENSE +21 -21
- package/dist/author.d.ts +13 -0
- package/dist/author.d.ts.map +1 -1
- package/dist/author.js +46 -13
- package/dist/author.js.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/decide.d.ts +13 -5
- package/dist/decide.d.ts.map +1 -1
- package/dist/decide.js +50 -35
- package/dist/decide.js.map +1 -1
- package/dist/decider-review.d.ts +68 -1
- package/dist/decider-review.d.ts.map +1 -1
- package/dist/decider-review.js +211 -3
- package/dist/decider-review.js.map +1 -1
- package/dist/decision-ledger.d.ts +79 -1
- package/dist/decision-ledger.d.ts.map +1 -1
- package/dist/decision-ledger.js +118 -11
- package/dist/decision-ledger.js.map +1 -1
- package/dist/decision-view.d.ts +41 -0
- package/dist/decision-view.d.ts.map +1 -0
- package/dist/decision-view.js +264 -0
- package/dist/decision-view.js.map +1 -0
- package/dist/findings.d.ts +3 -0
- package/dist/findings.d.ts.map +1 -1
- package/dist/findings.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/manifest.d.ts +2 -2
- package/dist/programs-dir.d.ts +27 -0
- package/dist/programs-dir.d.ts.map +1 -0
- package/dist/programs-dir.js +52 -0
- package/dist/programs-dir.js.map +1 -0
- package/dist/publish.d.ts.map +1 -1
- package/dist/publish.js +8 -29
- package/dist/publish.js.map +1 -1
- package/dist/review-pass.d.ts +33 -1
- package/dist/review-pass.d.ts.map +1 -1
- package/dist/review-pass.js +94 -42
- package/dist/review-pass.js.map +1 -1
- package/dist/run-program.d.ts +25 -3
- package/dist/run-program.d.ts.map +1 -1
- package/dist/run-program.js +382 -42
- package/dist/run-program.js.map +1 -1
- package/package.json +3 -3
- package/templates/AGENTS.md +29 -29
- package/templates/CLAUDE.md +7 -7
- package/templates/vision.md +46 -46
package/dist/run-program.js
CHANGED
|
@@ -7,11 +7,14 @@ import { resolveSummary, summaryContract } from "./agent-summary.js";
|
|
|
7
7
|
import { authorProgram } from "./author.js";
|
|
8
8
|
import { decisionContract, decisionFingerprint, extractDecisions, } from "./decision.js";
|
|
9
9
|
import { appendLedgerEvents, readDecisionLedger, } from "./decision-ledger.js";
|
|
10
|
-
import {
|
|
10
|
+
import { escalatedRecords, renderRecord, } from "./decision-view.js";
|
|
11
|
+
import { reviewDecisions, triageFindings } from "./decider-review.js";
|
|
12
|
+
import { fingerprint } from "./findings.js";
|
|
11
13
|
import { findCycles, stableTopologicalOrder } from "./graph.js";
|
|
12
|
-
import { extractFindings, findingsContract, findingsToLedgerEvents, renderPassReport, reviewerAbsentOutcome, runReviewPass, } from "./review-pass.js";
|
|
14
|
+
import { extractFindings, findingsContract, findingsToLedgerEvents, hasRoutableEvidence, locateInRepo, renderPassReport, reviewerAbsentOutcome, runReviewPass, verifyEvidence, } from "./review-pass.js";
|
|
13
15
|
import { loadManifest, saveManifest, } from "./manifest.js";
|
|
14
16
|
import { detectDefaultBranch, programBranchName } from "./program-branch.js";
|
|
17
|
+
import { restoreProgramsDir, snapshotProgramsDir } from "./programs-dir.js";
|
|
15
18
|
import { CouldNotStartError } from "./exit-codes.js";
|
|
16
19
|
import { runReportPath } from "./report-path.js";
|
|
17
20
|
const execFileAsync = promisify(execFile);
|
|
@@ -140,6 +143,37 @@ function decisionsRuledOnSection(records) {
|
|
|
140
143
|
lines.push("");
|
|
141
144
|
return lines;
|
|
142
145
|
}
|
|
146
|
+
/**
|
|
147
|
+
* A human-decided finding is binding on the next run, same as a
|
|
148
|
+
* human-decided decision — but the label's force is origin-specific, so the
|
|
149
|
+
* brief renders the *instruction* each label binds, never a bare label
|
|
150
|
+
* (SC-16). `fix-now` is a required correction; `accept` forbids reopening
|
|
151
|
+
* the settled limitation; `escalate` is degenerate for a human ruling — the
|
|
152
|
+
* human is the escalation target — so it binds no implementer instruction,
|
|
153
|
+
* exactly as an escalated-but-unruled finding renders none.
|
|
154
|
+
*/
|
|
155
|
+
function findingsRuledOnSection(records) {
|
|
156
|
+
const lines = [];
|
|
157
|
+
for (const record of records) {
|
|
158
|
+
if (record.status !== "human-decided")
|
|
159
|
+
continue;
|
|
160
|
+
if (record.humanChosen === "fix-now") {
|
|
161
|
+
lines.push(`- **${record.subject}** — binding: the human ruled \`fix-now\` — ` +
|
|
162
|
+
`${record.humanReason}. This is a required correction: address ` +
|
|
163
|
+
"the cited defect in this workstream; it is not optional.");
|
|
164
|
+
}
|
|
165
|
+
else if (record.humanChosen === "accept") {
|
|
166
|
+
lines.push(`- **${record.subject}** — binding: the human ruled \`accept\` — ` +
|
|
167
|
+
`${record.humanReason}. This is a settled known limitation: do ` +
|
|
168
|
+
"not reopen, re-litigate, or attempt to fix it.");
|
|
169
|
+
}
|
|
170
|
+
// A human `escalate` ruling (or any other label) binds no implementer
|
|
171
|
+
// instruction — the human is the escalation target, not a fix to build.
|
|
172
|
+
}
|
|
173
|
+
if (lines.length === 0)
|
|
174
|
+
return [];
|
|
175
|
+
return ["## Findings ruled on", "", ...lines, ""];
|
|
176
|
+
}
|
|
143
177
|
function implementerBrief(manifest, workstream, spec, ledger, priorFailure) {
|
|
144
178
|
const roster = manifest.workstreams
|
|
145
179
|
.map((entry) => {
|
|
@@ -149,6 +183,7 @@ function implementerBrief(manifest, workstream, spec, ledger, priorFailure) {
|
|
|
149
183
|
.join("\n");
|
|
150
184
|
const relevantDecisions = ledger.decisions.filter((record) => record.workstream === workstream.id &&
|
|
151
185
|
(record.status === "human-decided" || record.status === "ratified"));
|
|
186
|
+
const relevantFindings = ledger.findings.filter((record) => record.workstream === workstream.id && record.status === "human-decided");
|
|
152
187
|
return [
|
|
153
188
|
`# Workstream ${workstream.id}: ${workstream.name}`,
|
|
154
189
|
"",
|
|
@@ -161,6 +196,7 @@ function implementerBrief(manifest, workstream, spec, ledger, priorFailure) {
|
|
|
161
196
|
roster,
|
|
162
197
|
"",
|
|
163
198
|
...decisionsRuledOnSection(relevantDecisions),
|
|
199
|
+
...findingsRuledOnSection(relevantFindings),
|
|
164
200
|
...(priorFailure
|
|
165
201
|
? [
|
|
166
202
|
"## Previous attempt failed",
|
|
@@ -305,6 +341,18 @@ export async function runProgram(options) {
|
|
|
305
341
|
}
|
|
306
342
|
}
|
|
307
343
|
}
|
|
344
|
+
// Subject ids already sent to the decider this run — shared across the
|
|
345
|
+
// authoring and build stages (a run is one process) and across the
|
|
346
|
+
// decision and finding paths, so "once per subject per run" (SC-10) is
|
|
347
|
+
// honest rather than per-stage. A resumed run starts a fresh set and
|
|
348
|
+
// legitimately re-reviews an earlier run's subjects.
|
|
349
|
+
const reviewed = new Set();
|
|
350
|
+
// Subject ids the decider actually ruled on this run — a strict subset of
|
|
351
|
+
// `reviewed` (a subject can be sent but the invocation fail or return no
|
|
352
|
+
// parseable verdict). This, not `reviewed`, is the run-local basis for the
|
|
353
|
+
// report's "this run" triage ratio — see decider-review.ts's doc comment
|
|
354
|
+
// on `triaged`.
|
|
355
|
+
const triaged = new Set();
|
|
308
356
|
// Authoring runs before building: every workstream whose spec is missing
|
|
309
357
|
// gets one, in dependency order, before anything is implemented. It
|
|
310
358
|
// reloads the manifest afterward because authoring may have merged
|
|
@@ -317,6 +365,8 @@ export async function runProgram(options) {
|
|
|
317
365
|
git,
|
|
318
366
|
log,
|
|
319
367
|
now,
|
|
368
|
+
reviewed,
|
|
369
|
+
triaged,
|
|
320
370
|
});
|
|
321
371
|
manifest = await loadManifest(root, options.programId);
|
|
322
372
|
// Loaded once so every brief in this run projects the same picture of
|
|
@@ -366,13 +416,20 @@ export async function runProgram(options) {
|
|
|
366
416
|
}
|
|
367
417
|
}
|
|
368
418
|
const ledger = await readDecisionLedger(root, options.programId);
|
|
369
|
-
const escalations = ledger
|
|
419
|
+
const escalations = escalatedRecords(ledger);
|
|
370
420
|
const complete = results.every((result) => result.outcome.status === "complete" ||
|
|
371
421
|
result.outcome.status === "skipped");
|
|
372
422
|
manifest.program.status = complete ? "complete" : "partial";
|
|
373
423
|
await saveManifest(root, options.programId, manifest);
|
|
374
424
|
const reportPath = runReportPath(root, options.programId);
|
|
375
|
-
await writeFile(reportPath,
|
|
425
|
+
await writeFile(reportPath,
|
|
426
|
+
// `triaged` is the run-local set of subject ids the decider actually
|
|
427
|
+
// ruled on this run (built up across the authoring and build stages,
|
|
428
|
+
// see its declaration above) — exactly the `triagedThisRun` basis the
|
|
429
|
+
// "this run" triage ratio needs, since the projected ledger carries no
|
|
430
|
+
// run identifier of its own. It excludes ids that were merely sent but
|
|
431
|
+
// whose invocation failed or returned no valid verdict.
|
|
432
|
+
renderRunReport(manifest, results, ledger, triaged, authorResult, now()), "utf8");
|
|
376
433
|
if (isRepository) {
|
|
377
434
|
await git.commitPaths(root, `nightshift(${options.programId}): run report and decision ledger`, ["docs/programs"]);
|
|
378
435
|
}
|
|
@@ -457,18 +514,17 @@ export async function runProgram(options) {
|
|
|
457
514
|
// The manifest's single commit field records the workstream's final
|
|
458
515
|
// verified state — after any kept fix, that is the fix commit, not
|
|
459
516
|
// the earlier green one.
|
|
460
|
-
|
|
517
|
+
let finalCommit = critique?.finalCommit ?? c0;
|
|
461
518
|
// Findings anchor to c0 (the green, pre-critique commit) — the
|
|
462
519
|
// honest rollback point — while the decider below diffs from
|
|
463
520
|
// baseCommit (pre-workstream), so it sees the whole workstream.
|
|
464
521
|
const findingEvents = findingsToLedgerEvents({
|
|
465
522
|
workstreamId: workstream.id,
|
|
466
|
-
findings: critique?.outcome.open ?? [],
|
|
523
|
+
findings: (critique?.outcome.open ?? []).filter(hasRoutableEvidence),
|
|
467
524
|
...(c0 === undefined ? {} : { baseCommit: c0 }),
|
|
468
525
|
now,
|
|
469
526
|
});
|
|
470
527
|
await appendLedgerEvents(root, options.programId, findingEvents);
|
|
471
|
-
const findingDecisions = findingEvents.flatMap((event) => event.kind === "decision-recorded" ? [event.decision] : []);
|
|
472
528
|
// The commit's own sha cannot be part of the tree it commits, so the
|
|
473
529
|
// manifest records it only now — swept forward into whatever commits
|
|
474
530
|
// next. Replay reads the manifest's current state, not the commit
|
|
@@ -477,7 +533,24 @@ export async function runProgram(options) {
|
|
|
477
533
|
if (finalCommit !== undefined)
|
|
478
534
|
workstream.commit = finalCommit;
|
|
479
535
|
await saveManifest(root, options.programId, manifest);
|
|
480
|
-
await reviewWorkstreamDecisions(workstream.id,
|
|
536
|
+
await reviewWorkstreamDecisions(workstream.id, parsed.decisions, baseCommit);
|
|
537
|
+
// Findings are triaged against the pre-workstream diff (baseCommit),
|
|
538
|
+
// the same one the decider reviews decisions against — c0/finalCommit
|
|
539
|
+
// are the fix loop's own rollback anchors, a different thing.
|
|
540
|
+
await reviewWorkstreamFindings(workstream.id, findingEvents, baseCommit);
|
|
541
|
+
// A fix-now triage drives exactly one bounded fix attempt, here —
|
|
542
|
+
// after the triage that produced it, before the run advances. Only
|
|
543
|
+
// reachable when there is a green commit to attempt from and a
|
|
544
|
+
// decider that could have produced a fix-now verdict in the first
|
|
545
|
+
// place (SC-13).
|
|
546
|
+
if (finalCommit !== undefined && decider) {
|
|
547
|
+
const driven = await driveFixNowFindings(workstream, spec, critique?.outcome.open ?? [], finalCommit);
|
|
548
|
+
if (driven !== finalCommit) {
|
|
549
|
+
finalCommit = driven;
|
|
550
|
+
workstream.commit = finalCommit;
|
|
551
|
+
await saveManifest(root, options.programId, manifest);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
481
554
|
if (critique)
|
|
482
555
|
base.testCritique = critique.outcome;
|
|
483
556
|
base.testCritiqueDiffClipped = critique?.diffClipped ?? false;
|
|
@@ -528,10 +601,99 @@ export async function runProgram(options) {
|
|
|
528
601
|
agentRunner,
|
|
529
602
|
git,
|
|
530
603
|
isRepository,
|
|
604
|
+
reviewed,
|
|
605
|
+
triaged,
|
|
606
|
+
now,
|
|
607
|
+
log,
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
function reviewWorkstreamFindings(workstreamId, findings, baseCommit) {
|
|
611
|
+
return triageFindings({
|
|
612
|
+
root,
|
|
613
|
+
programId: options.programId,
|
|
614
|
+
manifest,
|
|
615
|
+
workstreamId,
|
|
616
|
+
findings,
|
|
617
|
+
baseCommit,
|
|
618
|
+
decider,
|
|
619
|
+
agentRunner,
|
|
620
|
+
git,
|
|
621
|
+
isRepository,
|
|
622
|
+
reviewed,
|
|
623
|
+
triaged,
|
|
531
624
|
now,
|
|
532
625
|
log,
|
|
533
626
|
});
|
|
534
627
|
}
|
|
628
|
+
/**
|
|
629
|
+
* Drives the one bounded fix attempt a `fix-now` triage earns (WS-06).
|
|
630
|
+
* Selects findings from the ledger projection — never from any in-memory
|
|
631
|
+
* disposition — so a human-decided finding (status `"human-decided"`, not
|
|
632
|
+
* `"fix-now"`) is never handed to the fix seam (SC-14). A workstream's
|
|
633
|
+
* `fix-now` findings are fixed together, in one `attemptFix` call, then
|
|
634
|
+
* recorded as one `finding-fix-attempted` event per finding id — a ledger
|
|
635
|
+
* entry, never a re-triage (SC-14). Returns the (possibly unchanged) green
|
|
636
|
+
* commit; never throws, never blocks the run (SC-15).
|
|
637
|
+
*/
|
|
638
|
+
async function driveFixNowFindings(workstream, spec, openFindings, greenCommit) {
|
|
639
|
+
const ledger = await readDecisionLedger(root, options.programId);
|
|
640
|
+
const fixNowIds = new Set(ledger.findings
|
|
641
|
+
.filter((record) => record.workstream === workstream.id && record.status === "fix-now")
|
|
642
|
+
.map((record) => record.id));
|
|
643
|
+
if (fixNowIds.size === 0)
|
|
644
|
+
return greenCommit;
|
|
645
|
+
const toFix = [];
|
|
646
|
+
const matchedIds = [];
|
|
647
|
+
for (const candidate of openFindings) {
|
|
648
|
+
const id = fingerprint({ ...candidate, workstreamId: workstream.id });
|
|
649
|
+
if (!fixNowIds.has(id))
|
|
650
|
+
continue;
|
|
651
|
+
toFix.push(candidate);
|
|
652
|
+
matchedIds.push(id);
|
|
653
|
+
}
|
|
654
|
+
// An id triaged fix-now but absent from the open set (should not
|
|
655
|
+
// happen — it is where the id came from) is skipped, fail-open.
|
|
656
|
+
if (toFix.length === 0)
|
|
657
|
+
return greenCommit;
|
|
658
|
+
const fix = await attemptFix({
|
|
659
|
+
root,
|
|
660
|
+
programId: options.programId,
|
|
661
|
+
workstream,
|
|
662
|
+
spec,
|
|
663
|
+
config,
|
|
664
|
+
agentRunner,
|
|
665
|
+
verifyRunner,
|
|
666
|
+
git,
|
|
667
|
+
agent,
|
|
668
|
+
findings: toFix,
|
|
669
|
+
greenCommit,
|
|
670
|
+
label: "fix-now fix",
|
|
671
|
+
log,
|
|
672
|
+
});
|
|
673
|
+
// "kept" only when the attempt both verified clean and actually landed a
|
|
674
|
+
// commit — a clean-but-empty attempt is a decline, not a fix, and must
|
|
675
|
+
// reach the human exactly like a failed verification does.
|
|
676
|
+
const kept = fix.outcome === "kept" && fix.commit !== undefined;
|
|
677
|
+
const note = kept
|
|
678
|
+
? fix.summary
|
|
679
|
+
: fix.outcome === "kept"
|
|
680
|
+
? `${fix.summary} (the attempt verified clean but made no change; no fix landed)`
|
|
681
|
+
: `${fix.summary} (fix failed verification and was discarded: ${fix.failure})`;
|
|
682
|
+
const events = matchedIds.map((id) => ({
|
|
683
|
+
kind: "finding-fix-attempted",
|
|
684
|
+
at: now().toISOString(),
|
|
685
|
+
id,
|
|
686
|
+
outcome: kept ? "kept" : "failed",
|
|
687
|
+
note,
|
|
688
|
+
...(kept && fix.commit !== undefined ? { commit: fix.commit } : {}),
|
|
689
|
+
attemptedBy: "implementer",
|
|
690
|
+
}));
|
|
691
|
+
await appendLedgerEvents(root, options.programId, events);
|
|
692
|
+
log(kept
|
|
693
|
+
? `${workstream.id}: fix-now fix verified and committed`
|
|
694
|
+
: `${workstream.id}: fix-now fix failed and was escalated — ${note}`);
|
|
695
|
+
return kept ? fix.greenCommit : greenCommit;
|
|
696
|
+
}
|
|
535
697
|
}
|
|
536
698
|
/** Undefined means the attempt verified clean; otherwise the diagnosis. */
|
|
537
699
|
async function verifyAttempt(config, verifyRunner, root, agentExitCode) {
|
|
@@ -719,6 +881,55 @@ function testCritiqueFixBrief(workstream, spec, findings) {
|
|
|
719
881
|
summaryContract(),
|
|
720
882
|
].join("\n");
|
|
721
883
|
}
|
|
884
|
+
/**
|
|
885
|
+
* The fix seam, in full: one brief carrying every finding, one implementer
|
|
886
|
+
* invocation, one verification, one commit on green or one reset on
|
|
887
|
+
* failure. Never invokes a reviewer and never loops — the caller owns
|
|
888
|
+
* whether (and how many times) this is called.
|
|
889
|
+
*/
|
|
890
|
+
async function attemptFix(options) {
|
|
891
|
+
const { root, programId, workstream, spec, config, agentRunner, verifyRunner, git, agent, findings, greenCommit, label, } = options;
|
|
892
|
+
const brief = testCritiqueFixBrief(workstream, spec, findings);
|
|
893
|
+
const invocation = await invokeAgent(agentRunner, agent, brief, root);
|
|
894
|
+
const summary = resolveSummary(invocation.output).text;
|
|
895
|
+
const failure = await verifyAttempt(config, verifyRunner, root, invocation.exitCode);
|
|
896
|
+
if (failure === undefined) {
|
|
897
|
+
// The runner's own pending ledger and manifest writes under
|
|
898
|
+
// docs/programs sit uncommitted in the tree until the run's final
|
|
899
|
+
// commit — appended by earlier steps in this very workstream (findings,
|
|
900
|
+
// triage) or by an earlier workstream in the same run. A plain
|
|
901
|
+
// `git add -A` would otherwise sweep that bookkeeping into this fix's
|
|
902
|
+
// commit and make a genuine no-op look like a landed fix. Only a change
|
|
903
|
+
// outside docs/programs counts as the implementer having done anything;
|
|
904
|
+
// with none, skip committing — the pending bookkeeping stays for a
|
|
905
|
+
// later commit to pick up, nothing is lost.
|
|
906
|
+
const dirty = await git.dirtyPaths(root);
|
|
907
|
+
const meaningfulChange = dirty.some((path) => !path.replaceAll("\\", "/").startsWith("docs/programs/"));
|
|
908
|
+
const commit = meaningfulChange
|
|
909
|
+
? await git.commitAll(root, `nightshift(${programId}): ${workstream.id} ${label}`)
|
|
910
|
+
: undefined;
|
|
911
|
+
return {
|
|
912
|
+
outcome: "kept",
|
|
913
|
+
greenCommit: commit ?? greenCommit,
|
|
914
|
+
...(commit === undefined ? {} : { commit }),
|
|
915
|
+
summary,
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
// The same uncommitted bookkeeping the clean path guards against sweeping
|
|
919
|
+
// into a commit is, on this path, what a whole-tree `git reset --hard`
|
|
920
|
+
// would silently throw away: by the time a fix-now attempt runs, this
|
|
921
|
+
// workstream's finding-recorded, finding-triaged and decision-reviewed
|
|
922
|
+
// events are on disk and not yet committed. Resetting without them would
|
|
923
|
+
// erase the finding this very attempt is about to report a failure on —
|
|
924
|
+
// and the `finding-fix-attempted` event appended afterwards would project
|
|
925
|
+
// against nothing, so the escalation would never reach the human (SC-15).
|
|
926
|
+
// Preserve the journal across the rollback exactly as `decide`'s replay
|
|
927
|
+
// does; the run's final commit picks it up.
|
|
928
|
+
const snapshot = await snapshotProgramsDir(root);
|
|
929
|
+
await git.resetHard(root, greenCommit);
|
|
930
|
+
await restoreProgramsDir(root, snapshot);
|
|
931
|
+
return { outcome: "failed", greenCommit, summary, failure };
|
|
932
|
+
}
|
|
722
933
|
/**
|
|
723
934
|
* The test-critique driver: wires WS-01's bounded review loop to the two
|
|
724
935
|
* briefs above, and owns the green-state invariant (SC-07) — a closure
|
|
@@ -733,6 +944,7 @@ async function runTestCritique(options) {
|
|
|
733
944
|
if (!reviewer) {
|
|
734
945
|
return { outcome: reviewerAbsentOutcome(), finalCommit: greenCommit, diffClipped };
|
|
735
946
|
}
|
|
947
|
+
const locate = (file) => locateInRepo(root, file);
|
|
736
948
|
const review = async (_round, priorOpen) => {
|
|
737
949
|
const rawDiff = baseCommit !== undefined ? await git.diffSince(root, baseCommit) : "";
|
|
738
950
|
const diff = clipForReview(rawDiff, "diff");
|
|
@@ -742,24 +954,33 @@ async function runTestCritique(options) {
|
|
|
742
954
|
const brief = testCritiqueReviewerBrief(manifest, workstream, clippedSpec.text, diff.text, priorOpen);
|
|
743
955
|
const invocation = await invokeAgent(agentRunner, reviewer, brief, root);
|
|
744
956
|
const parsed = extractFindings(invocation.output);
|
|
957
|
+
const findings = verifyEvidence(parsed.findings, locate);
|
|
745
958
|
const ran = invocation.exitCode === 0 && hasFindingsBlock(invocation.output);
|
|
746
|
-
return { findings
|
|
959
|
+
return { findings, errors: parsed.errors, ran };
|
|
747
960
|
};
|
|
748
961
|
const respond = async (_round, findings) => {
|
|
749
|
-
const
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
962
|
+
const fix = await attemptFix({
|
|
963
|
+
root,
|
|
964
|
+
programId: manifest.program.id,
|
|
965
|
+
workstream,
|
|
966
|
+
spec,
|
|
967
|
+
config,
|
|
968
|
+
agentRunner,
|
|
969
|
+
verifyRunner,
|
|
970
|
+
git,
|
|
971
|
+
agent,
|
|
972
|
+
findings,
|
|
973
|
+
greenCommit,
|
|
974
|
+
label: "test critique fix",
|
|
975
|
+
log,
|
|
976
|
+
});
|
|
977
|
+
greenCommit = fix.greenCommit;
|
|
978
|
+
if (fix.outcome === "kept") {
|
|
757
979
|
log(`${workstream.id}: test critique fix verified and committed`);
|
|
758
|
-
return { note: `${summary} (fix verified and committed)` };
|
|
980
|
+
return { note: `${fix.summary} (fix verified and committed)` };
|
|
759
981
|
}
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
return { note: `${summary} (fix failed verification and was discarded; the green state was preserved)` };
|
|
982
|
+
log(`${workstream.id}: test critique fix failed verification and was discarded — ${fix.failure}`);
|
|
983
|
+
return { note: `${fix.summary} (fix failed verification and was discarded; the green state was preserved)` };
|
|
763
984
|
};
|
|
764
985
|
const outcome = await runReviewPass({ review, respond });
|
|
765
986
|
return { outcome, finalCommit: greenCommit, diffClipped };
|
|
@@ -795,7 +1016,128 @@ function renderSpecsSection(authorResult) {
|
|
|
795
1016
|
}
|
|
796
1017
|
return lines;
|
|
797
1018
|
}
|
|
798
|
-
|
|
1019
|
+
/**
|
|
1020
|
+
* By subject + message + first evidence location — the dedup key for
|
|
1021
|
+
* collecting minor/advisory findings into known-limitations. Deliberately
|
|
1022
|
+
* looser than `findings.ts`'s `fingerprint` (which is identity across
|
|
1023
|
+
* review rounds): here two textually distinct findings about the same
|
|
1024
|
+
* subject and location are still worth listing once.
|
|
1025
|
+
*/
|
|
1026
|
+
function limitationKey(finding) {
|
|
1027
|
+
const first = finding.evidence[0];
|
|
1028
|
+
const location = first === undefined
|
|
1029
|
+
? ""
|
|
1030
|
+
: first.kind === "location"
|
|
1031
|
+
? `${first.file}:${first.startLine}`
|
|
1032
|
+
: first.kind === "concern"
|
|
1033
|
+
? first.named
|
|
1034
|
+
: `${first.metric}:${first.value}`;
|
|
1035
|
+
return `${finding.subject}|${finding.message}|${location}`;
|
|
1036
|
+
}
|
|
1037
|
+
function renderRawFinding(finding, workstreamLabel, sinceFixed) {
|
|
1038
|
+
const suffix = sinceFixed ? " (raised, since fixed)" : "";
|
|
1039
|
+
return `- **${finding.subject}** (${finding.severity}, ${workstreamLabel}) — ${finding.message}${suffix}`;
|
|
1040
|
+
}
|
|
1041
|
+
/**
|
|
1042
|
+
* A pass's minor/advisory findings, `open` and `resolved` alike (SC-07's
|
|
1043
|
+
* "every ... raised" — a finding the writer fixed was still raised),
|
|
1044
|
+
* deduped within the pass's own union so a finding present in both (should
|
|
1045
|
+
* WS-02's own dedup ever let that happen) is not double-listed.
|
|
1046
|
+
*/
|
|
1047
|
+
function collectPassLimitations(outcome, workstreamLabel) {
|
|
1048
|
+
if (!outcome)
|
|
1049
|
+
return [];
|
|
1050
|
+
const seen = new Set();
|
|
1051
|
+
const lines = [];
|
|
1052
|
+
for (const finding of outcome.open) {
|
|
1053
|
+
if (finding.severity !== "minor" && finding.severity !== "advisory")
|
|
1054
|
+
continue;
|
|
1055
|
+
const key = limitationKey(finding);
|
|
1056
|
+
if (seen.has(key))
|
|
1057
|
+
continue;
|
|
1058
|
+
seen.add(key);
|
|
1059
|
+
lines.push(renderRawFinding(finding, workstreamLabel, false));
|
|
1060
|
+
}
|
|
1061
|
+
for (const finding of outcome.resolved) {
|
|
1062
|
+
if (finding.severity !== "minor" && finding.severity !== "advisory")
|
|
1063
|
+
continue;
|
|
1064
|
+
const key = limitationKey(finding);
|
|
1065
|
+
if (seen.has(key))
|
|
1066
|
+
continue;
|
|
1067
|
+
seen.add(key);
|
|
1068
|
+
lines.push(renderRawFinding(finding, workstreamLabel, true));
|
|
1069
|
+
}
|
|
1070
|
+
return lines;
|
|
1071
|
+
}
|
|
1072
|
+
/**
|
|
1073
|
+
* One aggregated section (SC-07): every minor/advisory finding raised
|
|
1074
|
+
* anywhere in the run, every finding the decider accepted, any finding
|
|
1075
|
+
* still awaiting triage or already ruled on by a human, the outcome of
|
|
1076
|
+
* every fix-now attempt, and the run-local triage ratio. Fail-open
|
|
1077
|
+
* throughout — an empty run renders a plain statement, never nothing.
|
|
1078
|
+
*/
|
|
1079
|
+
function renderKnownLimitations(results, authorResult, ledger, escalated, triagedThisRun, programId) {
|
|
1080
|
+
const lines = ["## Known limitations", ""];
|
|
1081
|
+
const passLimitations = [
|
|
1082
|
+
...results.flatMap((result) => collectPassLimitations(result.testCritique, `${result.id} test critique`)),
|
|
1083
|
+
...authorResult.results.flatMap((entry) => collectPassLimitations(entry.specCritique, `${entry.id} spec critique`)),
|
|
1084
|
+
];
|
|
1085
|
+
const acceptedFindings = ledger.findings.filter((record) => record.status === "accepted");
|
|
1086
|
+
const openFindings = ledger.findings.filter((record) => record.status === "open");
|
|
1087
|
+
const humanDecidedFindings = ledger.findings.filter((record) => record.status === "human-decided");
|
|
1088
|
+
const fixNowFindings = ledger.findings.filter((record) => record.status === "fix-now");
|
|
1089
|
+
const hasFindingsContent = passLimitations.length > 0 ||
|
|
1090
|
+
acceptedFindings.length > 0 ||
|
|
1091
|
+
openFindings.length > 0 ||
|
|
1092
|
+
humanDecidedFindings.length > 0;
|
|
1093
|
+
if (!hasFindingsContent) {
|
|
1094
|
+
lines.push("No known limitations were recorded.", "");
|
|
1095
|
+
}
|
|
1096
|
+
else {
|
|
1097
|
+
if (passLimitations.length > 0) {
|
|
1098
|
+
lines.push("Minor and advisory findings raised during review:", "", ...passLimitations, "");
|
|
1099
|
+
}
|
|
1100
|
+
if (acceptedFindings.length > 0) {
|
|
1101
|
+
lines.push("Accepted as known limitations:", "");
|
|
1102
|
+
for (const record of acceptedFindings) {
|
|
1103
|
+
lines.push(...renderRecord(record, { density: "compact", programId }), "");
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
if (openFindings.length > 0) {
|
|
1107
|
+
lines.push("Recorded, not yet triaged:", "");
|
|
1108
|
+
for (const record of openFindings) {
|
|
1109
|
+
lines.push(...renderRecord(record, { density: "compact", programId }), "");
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
if (humanDecidedFindings.length > 0) {
|
|
1113
|
+
lines.push("Ruled by the human:", "");
|
|
1114
|
+
for (const record of humanDecidedFindings) {
|
|
1115
|
+
lines.push(...renderRecord(record, { density: "compact", programId }), "");
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
if (fixNowFindings.length > 0) {
|
|
1120
|
+
lines.push("Fix-now attempts:", "");
|
|
1121
|
+
for (const record of fixNowFindings) {
|
|
1122
|
+
if (record.fixAttempt?.outcome === "kept") {
|
|
1123
|
+
const commitPart = record.fixAttempt.commit
|
|
1124
|
+
? ` (commit \`${record.fixAttempt.commit}\`)`
|
|
1125
|
+
: "";
|
|
1126
|
+
lines.push(`- **${record.subject}** — fixed in the run${commitPart}: ${record.fixAttempt.note}`, "");
|
|
1127
|
+
}
|
|
1128
|
+
else {
|
|
1129
|
+
lines.push(`- **${record.subject}** — fix-now triaged; outcome pending.`, "");
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
const triagedCount = triagedThisRun.size;
|
|
1134
|
+
const escalatedThisRunCount = escalated.filter((record) => triagedThisRun.has(record.id)).length;
|
|
1135
|
+
lines.push(`The decider triaged ${triagedCount} subject${triagedCount === 1 ? "" : "s"} this run; ` +
|
|
1136
|
+
`${escalatedThisRunCount} ${escalatedThisRunCount === 1 ? "was" : "were"} escalated.`, "");
|
|
1137
|
+
return lines;
|
|
1138
|
+
}
|
|
1139
|
+
export function renderRunReport(manifest, results, ledger, triagedThisRun, authorResult, at) {
|
|
1140
|
+
const programId = manifest.program.id;
|
|
799
1141
|
const built = results.filter((result) => result.outcome.status === "complete" ||
|
|
800
1142
|
result.outcome.status === "skipped").length;
|
|
801
1143
|
const lines = [
|
|
@@ -807,12 +1149,16 @@ function renderRunReport(manifest, results, decisions, escalations, authorResult
|
|
|
807
1149
|
"",
|
|
808
1150
|
...renderSpecsSection(authorResult),
|
|
809
1151
|
];
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
1152
|
+
const escalated = escalatedRecords(ledger);
|
|
1153
|
+
if (escalated.length > 0) {
|
|
1154
|
+
lines.push("## Needs your attention", "", "The decider reviewed these choices and findings and believes you", "might decide differently. Each is self-contained below: why it is", "here, the alternatives, and a command to flip it if you disagree.", "");
|
|
1155
|
+
for (const record of escalated) {
|
|
1156
|
+
lines.push(...renderRecord(record, { density: "full", programId }));
|
|
1157
|
+
if ("origin" in record &&
|
|
1158
|
+
record.origin === "finding" &&
|
|
1159
|
+
record.fixAttempt?.outcome === "failed") {
|
|
1160
|
+
lines.push(`- **Fix attempt:** ${record.fixAttempt.note}`, "");
|
|
1161
|
+
}
|
|
816
1162
|
}
|
|
817
1163
|
}
|
|
818
1164
|
const failures = results.filter((result) => result.outcome.status === "failed" || result.outcome.status === "parked");
|
|
@@ -841,23 +1187,17 @@ function renderRunReport(manifest, results, decisions, escalations, authorResult
|
|
|
841
1187
|
}
|
|
842
1188
|
}
|
|
843
1189
|
lines.push("");
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
.filter((option) => option.label !== record.decision.chosen)
|
|
849
|
-
.map((option) => option.label)
|
|
850
|
-
.join(", ");
|
|
851
|
-
lines.push(`### ${record.decision.title} (${record.id}) — ${record.status}`, "", `- **Workstream:** ${record.workstream}${record.oneWay ? " · **one-way door**" : ""}`, `- **Context:** ${record.decision.context}`, `- **Chosen:** ${record.decision.chosen} — ${record.decision.rationale}`, `- **Alternatives:** ${alternatives === "" ? "(none listed)" : alternatives}`, ...(record.reviewRationale
|
|
852
|
-
? [`- **Decider:** ${record.reviewRationale}`]
|
|
853
|
-
: []), ...(record.baseCommit
|
|
854
|
-
? [`- **Anchor commit:** \`${record.baseCommit}\``]
|
|
855
|
-
: []), "");
|
|
856
|
-
}
|
|
1190
|
+
const settledDecisions = ledger.decisions.filter((record) => record.status !== "escalated");
|
|
1191
|
+
lines.push("## Decisions made along the way", "", "Every judgment call an agent surfaced, with its review status.", "Anything here can be revisited: roll back to the anchor commit and", "re-run, or just say which option you want changed.", "");
|
|
1192
|
+
if (settledDecisions.length === 0) {
|
|
1193
|
+
lines.push("None surfaced.", "");
|
|
857
1194
|
}
|
|
858
1195
|
else {
|
|
859
|
-
|
|
1196
|
+
for (const record of settledDecisions) {
|
|
1197
|
+
lines.push(...renderRecord(record, { density: "compact", programId }), "");
|
|
1198
|
+
}
|
|
860
1199
|
}
|
|
1200
|
+
lines.push(...renderKnownLimitations(results, authorResult, ledger, escalated, triagedThisRun, programId));
|
|
861
1201
|
const decisionErrors = results.flatMap((result) => result.decisionErrors);
|
|
862
1202
|
if (decisionErrors.length > 0) {
|
|
863
1203
|
lines.push("## Decision blocks the runner could not read", "", ...decisionErrors.map((error) => `- ${error}`), "");
|