@sabaiway/agent-workflow-kit 1.38.0 → 1.40.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/CHANGELOG.md +108 -0
- package/README.md +6 -5
- package/SKILL.md +7 -3
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/modes/bootstrap.md +1 -1
- package/references/modes/doc-parity.md +18 -0
- package/references/modes/fold-completeness.md +10 -6
- package/references/modes/gates.md +5 -3
- package/references/modes/grounding.md +4 -3
- package/references/modes/review-ledger.md +10 -6
- package/references/modes/review-state.md +1 -0
- package/references/modes/upgrade.md +3 -1
- package/references/modes/velocity.md +1 -1
- package/references/scripts/archive-decisions.mjs +39 -2
- package/references/scripts/archive-decisions.test.mjs +118 -1
- package/references/scripts/check-docs-size.mjs +50 -24
- package/references/scripts/check-docs-size.test.mjs +73 -1
- package/references/templates/agent_rules.md +1 -0
- package/references/templates/verification-profile.json +10 -0
- package/tools/changed-surface.mjs +294 -0
- package/tools/commands.mjs +10 -3
- package/tools/doc-parity.mjs +139 -0
- package/tools/fold-completeness-run.mjs +518 -252
- package/tools/fold-completeness.mjs +184 -66
- package/tools/grounding.mjs +92 -5
- package/tools/lcov.mjs +127 -0
- package/tools/procedures.mjs +1 -1
- package/tools/review-ledger-write.mjs +253 -50
- package/tools/review-ledger.mjs +382 -53
- package/tools/review-state.mjs +69 -3
- package/tools/run-gates.mjs +101 -11
- package/tools/sarif.mjs +52 -0
- package/tools/verification-profile.mjs +219 -0
|
@@ -14,19 +14,23 @@
|
|
|
14
14
|
// here). The gate is plan-EXECUTION-scoped, filtered to the in-flight plan's filename stem:
|
|
15
15
|
// exit 0 when the resolved plan-execution.review recipe is solo (configured, or degraded there);
|
|
16
16
|
// when no plan is in flight; when the tree is clean; when the cwd is not a git work tree;
|
|
17
|
-
// and when the in-flight plan-execution
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
17
|
+
// and when the in-flight plan-execution SEGMENT — (loop, base = git rev-parse HEAD),
|
|
18
|
+
// schema v3+ (v4 adds the (a) suite-execution evidence + the (c) reattest custody anchor,
|
|
19
|
+
// BUGFREE-3/AD-049), AD-048 D7 — has a CURRENT run record (kind-aware: the latest RUN, never a
|
|
20
|
+
// later red-probe or reattest) whose BOTH bindings match — the tree fingerprint AND the SEGMENT's
|
|
21
|
+
// sorted fixable-bug testId set recorded in the run — with, per bound testId: an
|
|
22
|
+
// N/N-green probe (D4), an observed-red receipt in this SEGMENT (a committed phase's
|
|
23
|
+
// obligations closed with its commit; a receipt never crosses a commit boundary), that receipt
|
|
21
24
|
// PRECEDING the latest run in ledger order (anti-post-hoc), and content CUSTODY — the run's
|
|
22
|
-
// recorded test-file hash equals the latest custody-eligible red-probe hash on that
|
|
23
|
-
// (eligible =
|
|
25
|
+
// recorded test-file hash equals the latest custody-eligible red-probe OR reattest hash on that
|
|
26
|
+
// file (eligible = its own testId is bound AND it precedes the run, D5; a green-only append is
|
|
27
|
+
// re-anchored by a recorded --reattest, not a red-proof waiver); plus 0
|
|
24
28
|
// uncovered changed lines, 0 changed unsupported-source files, a recorded tamper surface
|
|
25
29
|
// with every tampered test-surface file covered by a recorded oracle-change override
|
|
26
30
|
// (review ledger v3), and the reserved EMPTY mutation shape (no mutation ships). A
|
|
27
31
|
// recorded red-proof override waives the receipt + custody proof for exactly the testId
|
|
28
32
|
// it names (D7) — never the N/N-green requirement, never QUARANTINE.
|
|
29
|
-
// exit 1 for any DIRTY in-flight plan-execution
|
|
33
|
+
// exit 1 for any DIRTY in-flight plan-execution segment lacking such a current run record — including
|
|
30
34
|
// the stale-fingerprint case (a tree edit moves the fingerprint), the same-fingerprint/
|
|
31
35
|
// new-testId case (a triage recorded after the run moves the bound-testId set, Decision 9),
|
|
32
36
|
// and a schema-1 (or tamper-less) record as the loop's latest run (an older runner; D2) —
|
|
@@ -58,24 +62,41 @@
|
|
|
58
62
|
// free, Node >= 18. No side effects on import (the isDirectRun idiom).
|
|
59
63
|
|
|
60
64
|
import { readFileSync } from 'node:fs';
|
|
61
|
-
import { join } from 'node:path';
|
|
62
65
|
import { pathToFileURL } from 'node:url';
|
|
63
66
|
import { spawnSync } from 'node:child_process';
|
|
64
67
|
import { detectBackends } from './detect-backends.mjs';
|
|
65
68
|
import { resolveActivityRecipe, planRecipe, DISPLAY_ALIASES } from './recipes.mjs';
|
|
66
69
|
import { CONFIG_REL, fail, loadConfig } from './orchestration-config.mjs';
|
|
67
70
|
import { computeTreeFingerprint, isTreeClean, plansInFlight } from './review-state.mjs';
|
|
68
|
-
import { resolveLedgerPath, readLedger,
|
|
69
|
-
|
|
70
|
-
|
|
71
|
+
import { resolveLedgerPath, resolveBase, readLedger, filterSegmentRecords, collectOverrides, isWellFormedTestId, splitTestId } from './review-ledger.mjs';
|
|
72
|
+
// The fold-ledger locator and the D4 probe-verdict algebra live in the NEUTRAL shared module
|
|
73
|
+
// (BUGFREE-2 / AD-048, D8): review-ledger.mjs telemetry reads the fold ledger through THAT module —
|
|
74
|
+
// importing this checker from there would close an import cycle (this file imports review-ledger).
|
|
75
|
+
// Re-exported here so the runner and every existing consumer keep their one entry point.
|
|
76
|
+
import { probeVerdict, RESULTS_BASENAME, resolveResultsPath } from './changed-surface.mjs';
|
|
77
|
+
|
|
78
|
+
export { probeVerdict, RESULTS_BASENAME, resolveResultsPath };
|
|
71
79
|
// SCHEMA v2 (BUGFREE-1 / AD-047): records gain a kind discriminator — `run` (the fold-completeness
|
|
72
80
|
// run, now with per-testId rerun counts + the test file's content hash) | `red-probe` (the
|
|
73
|
-
// observed-red receipt --red mints).
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
|
|
78
|
-
|
|
81
|
+
// observed-red receipt --red mints). SCHEMA v3 (BUGFREE-2 / AD-048, D7): records carry `base` — the
|
|
82
|
+
// SEGMENT frame — and the gate scopes bound testIds, receipts, custody, and tamper to
|
|
83
|
+
// (loop, base = current HEAD): a committed phase's custody obligations close with its commit (the
|
|
84
|
+
// cross-phase-churn override class dies). RESULT_SCHEMA_VERSION is what the WRITER emits; the
|
|
85
|
+
// reader tolerates every SUPPORTED version under its own per-version rules (the review-ledger
|
|
86
|
+
// v1→v2 precedent), so v1/v2 ledgers never retroactively become malformed — but only a v3 record
|
|
87
|
+
// can enter a segment, so a dirty segment whose loop holds only pre-v3 records fails with a reason
|
|
88
|
+
// naming the schema upgrade (D7 legacy rule).
|
|
89
|
+
// SCHEMA v4 (BUGFREE-3 / AD-049): a v4 run record carries a `suite` execution-evidence block (the (a)
|
|
90
|
+
// one-suite-run credit — cmd + exit + pre/post fingerprints) AND a new record kind `reattest` joins
|
|
91
|
+
// `run`/`red-probe`: an operator-minted, RECORDED custody re-attestation that anchors a bound test
|
|
92
|
+
// file's custody at its CURRENT bytes WITHOUT fabricating a red-observe (the honest replacement for
|
|
93
|
+
// mis-using red-proof on a green-only test-file append, item (c)). A placed pre-v4 checker (SUPPORTED
|
|
94
|
+
// = {1,2,3}) rejects a v4 record outright — the version-skew guard; v1/v2/v3 rules are untouched.
|
|
95
|
+
export const RESULT_SCHEMA_VERSION = 4;
|
|
96
|
+
export const SUPPORTED_RESULT_SCHEMAS = new Set([1, 2, 3, 4]);
|
|
97
|
+
// The v4 record kind (c): a recorded re-attestation. Named as a const so the runner, the checker, and
|
|
98
|
+
// the doc-parity lint share one token (no drift).
|
|
99
|
+
export const REATTEST_KIND = 'reattest';
|
|
79
100
|
const ACTIVITY = 'plan-execution';
|
|
80
101
|
const SLOT = 'review';
|
|
81
102
|
|
|
@@ -89,22 +110,17 @@ const gitLine = (args, cwd) => {
|
|
|
89
110
|
|
|
90
111
|
const gitRoot = (cwd) => gitLine(['rev-parse', '--show-toplevel'], cwd);
|
|
91
112
|
|
|
92
|
-
// The result-ledger path: AW_FOLD_RESULTS overrides (mirrors AW_REVIEW_LEDGER); else <git dir>/basename.
|
|
93
|
-
export const resolveResultsPath = (cwd, env = process.env) => {
|
|
94
|
-
if (env.AW_FOLD_RESULTS) return env.AW_FOLD_RESULTS;
|
|
95
|
-
const gitDir = gitLine(['rev-parse', '--absolute-git-dir'], cwd);
|
|
96
|
-
return gitDir == null ? null : join(gitDir, RESULTS_BASENAME);
|
|
97
|
-
};
|
|
98
|
-
|
|
99
113
|
// ── the bound fixable-bug testId set (the SINGLE source of truth — runner + checker share it) ────
|
|
100
114
|
|
|
101
|
-
// collectBoundTestIds(reviewRecords, { activity, loop }) → the sorted, de-duplicated testIds
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
|
|
115
|
+
// collectBoundTestIds(reviewRecords, { activity, loop, base }) → the sorted, de-duplicated testIds
|
|
116
|
+
// of the SEGMENT's fixable-bug classifications (D7: pass base — a committed segment's folds are
|
|
117
|
+
// closed obligations; `base: undefined` would match nothing, never legacy records). Pure over
|
|
118
|
+
// review-ledger records. Both the runner (which records the set as a binding) and the checker
|
|
119
|
+
// (which recomputes it for the staleness check) call THIS, so the two can never drift (Decision 9:
|
|
120
|
+
// same-fingerprint/new-testId is stale).
|
|
121
|
+
export const collectBoundTestIds = (reviewRecords, { activity = ACTIVITY, loop, base } = {}) => {
|
|
106
122
|
const ids = new Set();
|
|
107
|
-
for (const r of
|
|
123
|
+
for (const r of filterSegmentRecords(reviewRecords, { activity, loop, base })) {
|
|
108
124
|
if (r.kind !== 'triage') continue;
|
|
109
125
|
for (const c of r.classifications) if (c.class === 'fixable-bug' && typeof c.testId === 'string' && c.testId.length > 0) ids.add(c.testId);
|
|
110
126
|
}
|
|
@@ -120,11 +136,18 @@ const isStringArray = (v) => Array.isArray(v) && v.every((x) => typeof x === 'st
|
|
|
120
136
|
|
|
121
137
|
const HASH_RE = /^[0-9a-f]{64}$/; // sha-256 hex — the content-custody hash shape
|
|
122
138
|
|
|
123
|
-
// The shared record frame (
|
|
139
|
+
// The shared record frame (every version, both kinds). v3 adds the SEGMENT frame: base is REQUIRED
|
|
140
|
+
// (null on an unborn branch); a v1/v2 record never carries it — an old record never grows new
|
|
141
|
+
// surface (the review-ledger D2 discipline).
|
|
124
142
|
const validateFrame = (obj) => {
|
|
125
143
|
if (!isNonEmptyString(obj.loop)) return 'missing loop';
|
|
126
144
|
if (!(obj.fingerprint === null || isNonEmptyString(obj.fingerprint))) return 'fingerprint must be null or a non-empty string';
|
|
127
145
|
if (!isNonEmptyString(obj.timestamp)) return 'missing timestamp';
|
|
146
|
+
if (obj.schema >= 3) {
|
|
147
|
+
if (!(obj.base === null || isNonEmptyString(obj.base))) return 'a v3 record requires base — null (unborn branch) or the HEAD commit the dirty tree sits on';
|
|
148
|
+
} else if (obj.base !== undefined) {
|
|
149
|
+
return `base is a v3 frame field — a schema-${obj.schema} record never carries it`;
|
|
150
|
+
}
|
|
128
151
|
return null;
|
|
129
152
|
};
|
|
130
153
|
|
|
@@ -190,6 +213,30 @@ const validateRedProbe = (obj) => {
|
|
|
190
213
|
return null;
|
|
191
214
|
};
|
|
192
215
|
|
|
216
|
+
// The (c) re-attest receipt (v4 only): the operator asserts a bound test file's CURRENT bytes are a
|
|
217
|
+
// legitimate custody anchor (a green-only append after a --red receipt — there is no red to observe).
|
|
218
|
+
// It mirrors the red-probe receipt MINUS the observation counts: testId + the file's current hash.
|
|
219
|
+
// Minted only by the runner's --reattest verb (self-discipline, the same trust model as the overrides).
|
|
220
|
+
const validateReattest = (obj) => {
|
|
221
|
+
if (!isWellFormedTestId(obj.testId)) return 'reattest testId must be "<test-file>#<test-name-pattern>" (a "#" separator, both halves non-empty)';
|
|
222
|
+
if (!(typeof obj.fileHash === 'string' && HASH_RE.test(obj.fileHash))) return 'reattest fileHash must be a 64-hex content hash';
|
|
223
|
+
return null;
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
// The (a) suite-execution evidence (v4 run records): the ONE suite spawn per fingerprint, recorded so
|
|
227
|
+
// run-gates --record can CREDIT the unit-tests gate from it instead of re-spawning. Here only the SHAPE
|
|
228
|
+
// is checked (a non-empty cmd, an integer-or-null exit, null-or-string before/after fingerprints); the
|
|
229
|
+
// credit's fingerprint/tree/cmd-identity/exit-0 conditions live in the ledger writer (review-ledger.mjs).
|
|
230
|
+
const validateSuiteEvidence = (suite) => {
|
|
231
|
+
if (!isPlainObject(suite)) return 'a v4 run records its suite-execution evidence ({ cmd, exit, fingerprintBefore, fingerprintAfter })';
|
|
232
|
+
if (!isNonEmptyString(suite.cmd)) return 'suite.cmd must be a non-empty string';
|
|
233
|
+
if (!(suite.exit === null || Number.isInteger(suite.exit))) return 'suite.exit must be an integer or null';
|
|
234
|
+
for (const k of ['fingerprintBefore', 'fingerprintAfter']) {
|
|
235
|
+
if (!(suite[k] === null || isNonEmptyString(suite[k]))) return `suite.${k} must be null or a non-empty fingerprint string`;
|
|
236
|
+
}
|
|
237
|
+
return null;
|
|
238
|
+
};
|
|
239
|
+
|
|
193
240
|
// validateRunRecord(obj) → { ok, reason }. Per-version, per-kind (D2): v1 records (no kind) keep the
|
|
194
241
|
// AD-046 single-run rules; v2 records carry the kind discriminator. The `reason` names the exact
|
|
195
242
|
// failed check so the malformed-line surface and the per-check named tests can assert it. Mutation
|
|
@@ -208,19 +255,35 @@ export const validateRunRecord = (obj) => {
|
|
|
208
255
|
if (obj.kind === 'run') {
|
|
209
256
|
const r = validateRunBody(obj, validateV2Entry);
|
|
210
257
|
if (r) return { ok: false, reason: r };
|
|
211
|
-
// The tamper surface
|
|
212
|
-
//
|
|
213
|
-
//
|
|
258
|
+
// The tamper surface is OPTIONAL on a v2 read — a record written by the pre-tamper v2 runner
|
|
259
|
+
// stays readable (never retroactively malformed); the GATE handles the staleness. A v3 runner
|
|
260
|
+
// ALWAYS records it, so a v3 run without one is not this runner's output. When present, the
|
|
261
|
+
// shape is exact.
|
|
262
|
+
if (obj.schema >= 3 && obj.tamper === undefined) return { ok: false, reason: 'a v3 run records its tamper surface' };
|
|
214
263
|
if (obj.tamper !== undefined && (!isPlainObject(obj.tamper) || !isStringArray(obj.tamper.tampered))) {
|
|
215
264
|
return { ok: false, reason: 'tamper.tampered must be an array of strings (when the tamper surface is recorded)' };
|
|
216
265
|
}
|
|
266
|
+
// v4 (a): a v4 runner ALWAYS records the suite-execution evidence, so a v4 run without one is not
|
|
267
|
+
// this runner's output (the tamper-on-v3 precedent). v1/v2/v3 runs never carry it.
|
|
268
|
+
if (obj.schema >= 4) {
|
|
269
|
+
const sr = validateSuiteEvidence(obj.suite);
|
|
270
|
+
if (sr) return { ok: false, reason: sr };
|
|
271
|
+
} else if (obj.suite !== undefined) {
|
|
272
|
+
return { ok: false, reason: `suite is a v4 field — a schema-${obj.schema} run never carries it` };
|
|
273
|
+
}
|
|
217
274
|
return { ok: true };
|
|
218
275
|
}
|
|
219
276
|
if (obj.kind === 'red-probe') {
|
|
220
277
|
const r = validateRedProbe(obj);
|
|
221
278
|
return r ? { ok: false, reason: r } : { ok: true };
|
|
222
279
|
}
|
|
223
|
-
|
|
280
|
+
if (obj.kind === REATTEST_KIND) {
|
|
281
|
+
// (c): a v4-only record kind — a v1/v2/v3 record never carries it (the version-skew guard).
|
|
282
|
+
if (!(obj.schema >= 4)) return { ok: false, reason: `${REATTEST_KIND} is a v4 record kind — a schema-${obj.schema} record never carries it` };
|
|
283
|
+
const r = validateReattest(obj);
|
|
284
|
+
return r ? { ok: false, reason: r } : { ok: true };
|
|
285
|
+
}
|
|
286
|
+
return { ok: false, reason: `kind must be "run", "red-probe", or "${REATTEST_KIND}" (got ${JSON.stringify(obj.kind)})` };
|
|
224
287
|
};
|
|
225
288
|
|
|
226
289
|
// readResults(path) → { records, malformed, malformedReasons, readError }. Absent file → empty (no run
|
|
@@ -255,11 +318,20 @@ export const readResults = (path, readFile = readFileSync) => {
|
|
|
255
318
|
// (latest is last).
|
|
256
319
|
export const filterLoopResults = (records, loop) => records.filter((r) => r.loop === loop);
|
|
257
320
|
|
|
321
|
+
// filterSegmentResults(records, loop, base) → the result records of ONE segment (D7), order
|
|
322
|
+
// preserved. STRICT: only a v3+ record carries base, so pre-v3 history can never enter a segment
|
|
323
|
+
// (readable, never re-scoped — the review-ledger filterSegmentRecords discipline).
|
|
324
|
+
export const filterSegmentResults = (records, loop, base) =>
|
|
325
|
+
filterLoopResults(records, loop).filter((r) => r.schema >= 3 && r.base === base);
|
|
326
|
+
|
|
258
327
|
// ── kind-aware selectors (shared: the runner and the checker read the ledger through THESE) ──────
|
|
259
328
|
|
|
260
329
|
// A v1 record IS a run (the kindless AD-046 shape); a v2 record is a run only under kind:"run".
|
|
261
330
|
export const isRunRecord = (r) => r.schema === 1 || r.kind === 'run';
|
|
262
331
|
export const isRedProbeRecord = (r) => r.schema >= 2 && r.kind === 'red-probe';
|
|
332
|
+
// (c) v4: a recorded custody re-attestation. Never read as "the latest run" (latestRunRecord uses
|
|
333
|
+
// isRunRecord); consumed by the custody chain as a valid anchor for exactly the file it names.
|
|
334
|
+
export const isReattestRecord = (r) => r.schema >= 4 && r.kind === REATTEST_KIND;
|
|
263
335
|
|
|
264
336
|
// latestRunRecord(loopRecords) → { record, index } | null over ONE loop's ordered records. Kind-aware
|
|
265
337
|
// (codex R2): a red-probe appended after a run must never be read as the loop's "latest run" — the
|
|
@@ -271,16 +343,42 @@ export const latestRunRecord = (loopRecords) => {
|
|
|
271
343
|
return null;
|
|
272
344
|
};
|
|
273
345
|
|
|
274
|
-
// probeVerdict
|
|
275
|
-
//
|
|
276
|
-
//
|
|
277
|
-
|
|
278
|
-
//
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
346
|
+
// probeVerdict — the D4 verdict algebra, ONE home (now the neutral changed-surface.mjs; re-exported
|
|
347
|
+
// above): the runner (--red mints only on 'red'), this checker (the gate passes only on 'green'),
|
|
348
|
+
// and the review-ledger telemetry (quarantine counts) all read the SAME algebra.
|
|
349
|
+
|
|
350
|
+
// foldSuiteCredit({ cwd, env, fingerprint }) → { credited, evidence, reason }. Read half of the (a)
|
|
351
|
+
// credit: may run-gates --record credit the unit-tests gate from the latest segment fold run's suite
|
|
352
|
+
// evidence instead of re-spawning? STRICT — the evidence must be fingerprint-bound (before === after
|
|
353
|
+
// === the current fingerprint, which also proves the tree unchanged) and exit 0. The cmd-identity check
|
|
354
|
+
// (suite.cmd === the gate cmd — the --only-subset defense) is the caller's. Any mismatch → credited:false.
|
|
355
|
+
// ENV residual (bounded, documented — maintainer-signed against Decision 7): the fold V8 suite runs under
|
|
356
|
+
// NODE_V8_COVERAGE, a plain gate spawn does not. Both now strip NODE_TEST_CONTEXT (run-gates.mjs
|
|
357
|
+
// spawnGateViaBash mirrors the fold suite's childTestEnv), so that divergence — the real vacuous-skip
|
|
358
|
+
// false-green — is closed. The ONE remaining delta is NODE_V8_COVERAGE: a test that FAILS under coverage
|
|
359
|
+
// exits nonzero and never credits (the exit-0 gate above), so only a test that PASSES *only* under
|
|
360
|
+
// coverage instrumentation (e.g. asserting process.env.NODE_V8_COVERAGE is set) could credit a green the
|
|
361
|
+
// plain gate would fail — an AD-047-class residual, unclosable without the double spawn (a) removes.
|
|
362
|
+
export const foldSuiteCredit = ({ cwd = process.cwd(), env = process.env, fingerprint } = {}) => {
|
|
363
|
+
const plans = plansInFlight(gitRoot(cwd) ?? cwd);
|
|
364
|
+
if (plans.length !== 1) return { credited: false, reason: 'no single in-flight plan (cannot resolve the loop)' };
|
|
365
|
+
const loop = plans[0].replace(/\.md$/, '');
|
|
366
|
+
const base = resolveBase(cwd);
|
|
367
|
+
const resultsPath = resolveResultsPath(cwd, env);
|
|
368
|
+
// Fail CLOSED on an unreadable / malformed fold ledger — the SAME posture decideCheck takes (a dropped
|
|
369
|
+
// line could hide a defect); a partially-trusted ledger never credits.
|
|
370
|
+
const read = resultsPath ? readResults(resultsPath) : { records: [], malformed: 0 };
|
|
371
|
+
if (read.readError) return { credited: false, reason: `the fold ledger is unreadable (${read.readError}) — fail closed, re-spawn` };
|
|
372
|
+
if (read.malformed > 0) return { credited: false, reason: `the fold ledger has ${read.malformed} malformed line(s) — a partially-read ledger never credits (fail closed), re-spawn` };
|
|
373
|
+
const sel = latestRunRecord(filterSegmentResults(read.records, loop, base));
|
|
374
|
+
if (sel == null) return { credited: false, reason: 'no fold run recorded for the current segment' };
|
|
375
|
+
const { suite } = sel.record;
|
|
376
|
+
if (!suite) return { credited: false, reason: 'the fold run carries no suite evidence (a pre-v4 record)' };
|
|
377
|
+
if (fingerprint == null || suite.fingerprintBefore !== fingerprint || suite.fingerprintAfter !== fingerprint) {
|
|
378
|
+
return { credited: false, reason: 'the fold suite evidence is not bound to the current tree (the tree moved) — re-spawn' };
|
|
379
|
+
}
|
|
380
|
+
if (suite.exit !== 0) return { credited: false, reason: `the fold suite exited ${suite.exit} (a nonzero/red suite never credits a green gate-run) — re-spawn` };
|
|
381
|
+
return { credited: true, evidence: suite };
|
|
284
382
|
};
|
|
285
383
|
|
|
286
384
|
// ── the check + report core ─────────────────────────────────────────────────────────────────────
|
|
@@ -304,6 +402,7 @@ export const buildFoldState = ({ cwd, env = process.env, detect = detectBackends
|
|
|
304
402
|
const plans = plansInFlight(root);
|
|
305
403
|
const fingerprint = computeTreeFingerprint(cwd);
|
|
306
404
|
const clean = fingerprint == null ? null : isTreeClean(cwd);
|
|
405
|
+
const base = resolveBase(cwd);
|
|
307
406
|
const resultsPath = resolveResultsPath(cwd, env);
|
|
308
407
|
const resultRead = resultsPath ? readResults(resultsPath) : { records: [], malformed: 0, malformedReasons: [] };
|
|
309
408
|
const reviewPath = resolveLedgerPath(cwd, env);
|
|
@@ -315,6 +414,7 @@ export const buildFoldState = ({ cwd, env = process.env, detect = detectBackends
|
|
|
315
414
|
plans,
|
|
316
415
|
fingerprint,
|
|
317
416
|
clean,
|
|
417
|
+
base,
|
|
318
418
|
resultsPath,
|
|
319
419
|
resultRecords: resultRead.records,
|
|
320
420
|
resultMalformed: resultRead.malformed,
|
|
@@ -353,20 +453,28 @@ export const decideCheck = (state) => {
|
|
|
353
453
|
if (state.reviewMalformed > 0) return { code: 1, reason: `the review ledger has ${state.reviewMalformed} malformed line(s) — failing closed; inspect ${state.reviewPath}` };
|
|
354
454
|
|
|
355
455
|
const loop = state.plans[0].replace(/\.md$/, '');
|
|
356
|
-
|
|
456
|
+
// SEGMENT scope (D7): the gate judges the current segment — (loop, base = the HEAD the dirty
|
|
457
|
+
// tree sits on). A committed phase's runs, receipts, custody chains, and tamper obligations are
|
|
458
|
+
// closed history; only v3 records at the current base enter.
|
|
459
|
+
const loopResults = filterSegmentResults(state.resultRecords, loop, state.base);
|
|
357
460
|
const sel = latestRunRecord(loopResults);
|
|
358
|
-
if (sel == null)
|
|
461
|
+
if (sel == null) {
|
|
462
|
+
const allLoop = filterLoopResults(state.resultRecords, loop);
|
|
463
|
+
const legacy = allLoop.length > 0 && allLoop.every((r) => !(r.schema >= 3))
|
|
464
|
+
? ' (the loop has only pre-v3 records, which never enter a segment — the schema upgrade requires a fresh run)'
|
|
465
|
+
: '';
|
|
466
|
+
return { code: 1, reason: `dirty plan-execution loop "${loop}" but no fold-completeness run recorded for the current segment — run fold-completeness-run.mjs${legacy}` };
|
|
467
|
+
}
|
|
359
468
|
const latest = sel.record;
|
|
360
|
-
//
|
|
361
|
-
//
|
|
362
|
-
//
|
|
363
|
-
|
|
364
|
-
if (latest.tamper === undefined) return { code: 1, reason: `the loop's latest run has no recorded tamper surface (an older runner) — re-run fold-completeness-run.mjs` };
|
|
469
|
+
// No per-record schema re-checks here (agy Phase-2 nit): only a VALID v3 record can be a segment
|
|
470
|
+
// member — the reader binned anything older or tamper-less before this point (a v1 never carries
|
|
471
|
+
// base; a v3 run without a tamper surface is malformed), and a malformed line already failed the
|
|
472
|
+
// gate closed above.
|
|
365
473
|
|
|
366
474
|
// Decision 9 — the double binding. A tree edit moves the fingerprint; a new fixable-bug triage moves
|
|
367
475
|
// the bound-testId set. Either mismatch is STALE (the run no longer describes the committable tree).
|
|
368
476
|
if (latest.fingerprint !== state.fingerprint) return { code: 1, reason: `no fold-completeness run for the current tree (the tree was edited after the run) — re-run fold-completeness-run.mjs after the last edit` };
|
|
369
|
-
const currentBound = collectBoundTestIds(state.reviewRecords, { activity: ACTIVITY, loop });
|
|
477
|
+
const currentBound = collectBoundTestIds(state.reviewRecords, { activity: ACTIVITY, loop, base: state.base });
|
|
370
478
|
if (!sameSet(latest.boundTestIds, currentBound)) return { code: 1, reason: `a fixable-bug testId was triaged after the run (the bound-testId set changed) — re-run fold-completeness-run.mjs` };
|
|
371
479
|
// Fail CLOSED if the run's probe set does not cover its bound-testId set EXACTLY. A well-formed
|
|
372
480
|
// runner record always probes every bound testId (one testIds[] entry per boundTestId), but a record
|
|
@@ -402,6 +510,10 @@ export const decideCheck = (state) => {
|
|
|
402
510
|
const probes = new Map(latest.testIds.map((t) => [t.id, t]));
|
|
403
511
|
const boundSet = new Set(latest.boundTestIds);
|
|
404
512
|
const receipts = loopResults.map((r, i) => ({ r, i })).filter(({ r }) => isRedProbeRecord(r));
|
|
513
|
+
// Custody ANCHORS (c): a red-probe OR a recorded re-attestation on the file re-anchors custody at
|
|
514
|
+
// its hash — the honest replacement for a red-proof waiver on a green-only append. The OBSERVED-RED
|
|
515
|
+
// requirement below stays red-probe-only (a re-attest is not a red observation).
|
|
516
|
+
const custodyAnchors = loopResults.map((r, i) => ({ r, i })).filter(({ r }) => isRedProbeRecord(r) || isReattestRecord(r));
|
|
405
517
|
for (const id of latest.boundTestIds) {
|
|
406
518
|
const t = probes.get(id);
|
|
407
519
|
const verdict = probeVerdict(t);
|
|
@@ -417,13 +529,14 @@ export const decideCheck = (state) => {
|
|
|
417
529
|
if (own.length === 0) return { code: 1, reason: `no observed-red receipt for ${id} — a test never seen failing proves nothing about the fix. BEFORE folding a fix, run: node fold-completeness-run.mjs --red "${id}"` };
|
|
418
530
|
if (!own.some(({ i }) => i < sel.index)) return { code: 1, reason: `the observed-red receipt for ${id} was minted AFTER the loop's latest run — a post-hoc red proves nothing; run fold-completeness-run.mjs once more (a fresh run after the receipt)` };
|
|
419
531
|
const { file } = splitTestId(id);
|
|
420
|
-
// Custody eligibility (D5, codex R3): the anchor is the LATEST
|
|
421
|
-
// testId is in the bound set AND which precedes the latest run — an unbound throwaway
|
|
422
|
-
// or one minted post-run, never re-attests a file.
|
|
423
|
-
|
|
532
|
+
// Custody eligibility (D5, codex R3): the anchor is the LATEST red-probe OR re-attest on that FILE
|
|
533
|
+
// whose own testId is in the bound set AND which precedes the latest run — an unbound throwaway
|
|
534
|
+
// receipt, or one minted post-run, never re-attests a file. A green-only append after a red-probe
|
|
535
|
+
// re-anchors here via a recorded --reattest (item (c)), not a red-proof waiver.
|
|
536
|
+
const eligible = custodyAnchors.filter(({ r, i }) => i < sel.index && boundSet.has(r.testId) && splitTestId(r.testId).file === file);
|
|
424
537
|
const anchor = eligible[eligible.length - 1];
|
|
425
538
|
if (!anchor || t.fileHash == null || anchor.r.fileHash !== t.fileHash) {
|
|
426
|
-
return { code: 1, reason: `custody broken for ${id}: the test file ${file} no longer matches its last observed-red content —
|
|
539
|
+
return { code: 1, reason: `custody broken for ${id}: the test file ${file} no longer matches its last observed-red/re-attested content — after a green-only append, re-attest the file (node fold-completeness-run.mjs --reattest "<a bound testId in ${file}>"); after a real edit, re-observe red (--red "<the file's newest testId>"), or record a red-proof override if the red is genuinely unestablishable` };
|
|
427
540
|
}
|
|
428
541
|
}
|
|
429
542
|
if (latest.coverage.uncoveredChanged.length > 0) return { code: 1, reason: `uncovered changed line(s) — changed code no test executed: ${latest.coverage.uncoveredChanged.map(renderUncovered).join(', ')}` };
|
|
@@ -480,9 +593,9 @@ const formatHuman = (state, check) => {
|
|
|
480
593
|
lines.push(` result ledger: ${state.resultsPath ?? '(unresolvable — no git dir)'} (${state.resultRecords.length} record(s)${state.resultMalformed ? `, ${state.resultMalformed} malformed — inspect the file` : ''})`);
|
|
481
594
|
if (state.plans.length === 1) {
|
|
482
595
|
const loop = state.plans[0].replace(/\.md$/, '');
|
|
483
|
-
const
|
|
484
|
-
const sel = latestRunRecord(
|
|
485
|
-
if (sel) lines.push(runLine(sel.record,
|
|
596
|
+
const segResults = filterSegmentResults(state.resultRecords, loop, state.base); // the current segment (D7)
|
|
597
|
+
const sel = latestRunRecord(segResults); // kind-aware: never render a red-probe as "the run"
|
|
598
|
+
if (sel) lines.push(runLine(sel.record, segResults.filter(isRedProbeRecord)));
|
|
486
599
|
}
|
|
487
600
|
lines.push(` check: ${check.code === 0 ? 'PASS' : 'FAIL'} — ${check.reason}`);
|
|
488
601
|
return lines.join('\n');
|
|
@@ -495,15 +608,20 @@ Usage:
|
|
|
495
608
|
|
|
496
609
|
Reads the result ledger the runner writes (<git dir>/${RESULTS_BASENAME}; AW_FOLD_RESULTS overrides),
|
|
497
610
|
resolves the effective ${ACTIVITY}.${SLOT} recipe, recomputes the canonical uncommitted-state
|
|
498
|
-
fingerprint, and decides whether the in-flight plan-execution
|
|
611
|
+
fingerprint, and decides whether the in-flight plan-execution SEGMENT's changed code is pinned by
|
|
612
|
+
tests — (loop, base = git rev-parse HEAD), schema v3+ (v4 adds the suite-execution evidence + the
|
|
613
|
+
reattest custody anchor, BUGFREE-3/AD-049; AD-048 D7): bound testIds, receipts, custody, and tamper
|
|
614
|
+
all filter to the current segment; a committed phase's obligations close with its commit.
|
|
499
615
|
|
|
500
616
|
--status (default) → the human report: resolved recipe, plan-in-flight, the latest run summary
|
|
501
617
|
(per-testId D4 verdicts + rerun counts), the loop's red-probe receipts, verdict.
|
|
502
618
|
--check → the gate exit code. The normative exit contract lives in the tool header (the single home):
|
|
503
|
-
exit 0 for solo / no plan in flight / a clean tree / not-a-git-tree / a CURRENT run whose
|
|
504
|
-
AND bound-testId set both match, with — per bound testId — an N/N-green probe,
|
|
505
|
-
receipt that PRECEDES the run, and content custody (run hash == the
|
|
506
|
-
|
|
619
|
+
exit 0 for solo / no plan in flight / a clean tree / not-a-git-tree / a CURRENT segment run whose
|
|
620
|
+
fingerprint AND segment bound-testId set both match, with — per bound testId — an N/N-green probe,
|
|
621
|
+
an observed-red receipt in this segment that PRECEDES the run, and content custody (run hash == the
|
|
622
|
+
latest custody-eligible red-probe OR reattest hash on that file — a green-only append is re-anchored
|
|
623
|
+
by a recorded --reattest, not a red-proof waiver); plus 0 uncovered changed lines, 0 changed
|
|
624
|
+
unsupported source, every tampered
|
|
507
625
|
test-surface file covered by a recorded oracle-change override, and the reserved empty mutation
|
|
508
626
|
shape. A recorded red-proof override (review-ledger-write override) waives receipt + custody for
|
|
509
627
|
exactly its testId — never green-N/N, never QUARANTINE. exit 1 otherwise (stale/missing/older-runner
|
package/tools/grounding.mjs
CHANGED
|
@@ -30,6 +30,13 @@ import { resolve, join, relative, isAbsolute, dirname, basename } from 'node:pat
|
|
|
30
30
|
import { pathToFileURL } from 'node:url';
|
|
31
31
|
import { spawnSync } from 'node:child_process';
|
|
32
32
|
import { fail } from './orchestration-config.mjs';
|
|
33
|
+
// (e) --ledger-summary (BUGFREE-3 / AD-049): a computed, loop/base-SCOPED review-ledger digest for
|
|
34
|
+
// the facts payload. The review ledger is read-only here — grounding never writes it. ORIGINS +
|
|
35
|
+
// computeTelemetry give the counts; the segment filter gives the scope.
|
|
36
|
+
import { ORIGINS, computeTelemetry, filterSegmentRecords, readLedger, resolveLedgerPath, resolveBase } from './review-ledger.mjs';
|
|
37
|
+
import { plansInFlight } from './review-state.mjs';
|
|
38
|
+
|
|
39
|
+
const PLAN_EXECUTION = 'plan-execution';
|
|
33
40
|
|
|
34
41
|
// The agy single-argv byte contract (mirrors agy-review.sh — the wrapper is the enforcement home).
|
|
35
42
|
export const DEFAULT_MAX_PROMPT_BYTES = 120000;
|
|
@@ -104,6 +111,73 @@ export const trimToBudget = (payload, budget) => {
|
|
|
104
111
|
return { text, trimmedBytes: bytes - keep };
|
|
105
112
|
};
|
|
106
113
|
|
|
114
|
+
// ── (e) --ledger-summary: a loop/base-SCOPED review-ledger digest (read-only, computed) ─────────
|
|
115
|
+
// `computeTelemetry` aggregates ALL loops and is not per-round-facts-shaped, so this renders the
|
|
116
|
+
// in-flight SEGMENT only (plan-execution, loop, base) — the telemetry COUNTS filtered to that
|
|
117
|
+
// segment, plus a terse per-round/triage/override render. Deliberately compact + distinct from the
|
|
118
|
+
// review-ledger `--status` human view (that one is indented + segment-grouped): this is a byte-
|
|
119
|
+
// budgeted FACTS block that agy/codex review against, so unrelated loops are excluded by construction.
|
|
120
|
+
|
|
121
|
+
const baseTag = (base) => (base === null ? '(unborn branch)' : String(base).slice(0, 12));
|
|
122
|
+
const countsOf = (obj) => {
|
|
123
|
+
const keys = Object.keys(obj).sort();
|
|
124
|
+
return keys.length === 0 ? '(none)' : keys.map((k) => `${k}:${obj[k]}`).join(' ');
|
|
125
|
+
};
|
|
126
|
+
const summaryRoundLine = (r) =>
|
|
127
|
+
`round ${r.round} — ${r.backends.map((b) => `${b.backend} ${b.degraded ? `degraded(${b.reason})` : `${b.blockers}/${b.majors}/${b.minors} ${b.verdict}`}`).join(', ')}` +
|
|
128
|
+
(r.findings.length ? ` [${r.findings.map((f) => `${f.findingKey}(${f.severity})`).join(', ')}]` : '');
|
|
129
|
+
const summaryTriageLine = (t) => `triage @round ${t.round} — ${t.classifications.map((c) => `${c.findingKey}=${c.class}`).join(', ')}`;
|
|
130
|
+
const summaryOverrideLine = (o) =>
|
|
131
|
+
`override @round ${o.round} [${o.scope}] — ${o.scope === 'oracle-change' ? o.files.join(', ') : o.scope === 'size-cap' ? `sanctioned ${o.sanctionedLines} lines` : o.testId}: ${o.reason}`;
|
|
132
|
+
const summaryGateRunLine = (g) => `gate-run — status=${g.summary.status} ${g.summary.passed}/${g.summary.gates} green`;
|
|
133
|
+
const summaryRecordLine = (r) =>
|
|
134
|
+
r.kind === 'round' ? summaryRoundLine(r) : r.kind === 'triage' ? summaryTriageLine(r) : r.kind === 'override' ? summaryOverrideLine(r) : summaryGateRunLine(r);
|
|
135
|
+
|
|
136
|
+
// renderLedgerSummary(records, { loop, base }) → the scoped facts section, or '' when the segment
|
|
137
|
+
// holds no records (empty/absent → nothing to ground). Pure: the caller does the I/O.
|
|
138
|
+
export const renderLedgerSummary = (records, { loop, base }) => {
|
|
139
|
+
const segment = filterSegmentRecords(records, { activity: PLAN_EXECUTION, loop, base });
|
|
140
|
+
if (segment.length === 0) return '';
|
|
141
|
+
const t = computeTelemetry(segment, []).loops[0];
|
|
142
|
+
const lines = [
|
|
143
|
+
`## Review-ledger summary — loop ${loop} @ base ${baseTag(base)}`,
|
|
144
|
+
'',
|
|
145
|
+
`rounds ${t.rounds} · divergence-rounds ${t.divergenceRounds}`,
|
|
146
|
+
`finding origins — ${ORIGINS.map((k) => `${k}:${t.origins[k]}`).join(' ')}`,
|
|
147
|
+
`classifications — ${countsOf(t.classifications)}`,
|
|
148
|
+
`backend verdicts — ${Object.keys(t.backendVerdicts).sort().map((b) => `${b}{${countsOf(t.backendVerdicts[b])}}`).join(' · ') || '(none)'}`,
|
|
149
|
+
`overrides — ${countsOf(t.overrides)}`,
|
|
150
|
+
'',
|
|
151
|
+
...segment.map(summaryRecordLine),
|
|
152
|
+
];
|
|
153
|
+
return `${lines.join('\n')}\n`;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
// resolveLedgerSummary({ cwd, env }) → the scoped section text for the SINGLE in-flight plan-execution
|
|
157
|
+
// segment. A loud STOP unless exactly one plan is in flight (the family's single-plan discipline —
|
|
158
|
+
// never guess which loop to ground). Reads the ledger read-only.
|
|
159
|
+
const gitTop = (cwd) => {
|
|
160
|
+
const r = gitLine(['rev-parse', '--show-toplevel'], cwd);
|
|
161
|
+
return r && r.status === 0 ? r.stdout.replace(/\r?\n$/, '') : cwd;
|
|
162
|
+
};
|
|
163
|
+
export const resolveLedgerSummary = ({ cwd, env }) => {
|
|
164
|
+
const root = gitTop(cwd);
|
|
165
|
+
const plans = plansInFlight(root);
|
|
166
|
+
if (plans.length !== 1) {
|
|
167
|
+
throw fail(1, `--ledger-summary needs exactly one in-flight plan (in flight: ${plans.length ? plans.join(', ') : 'none'}) — resolve to one active plan`);
|
|
168
|
+
}
|
|
169
|
+
const loop = plans[0].replace(/\.md$/, '');
|
|
170
|
+
const base = resolveBase(cwd);
|
|
171
|
+
const ledgerPath = resolveLedgerPath(cwd, env);
|
|
172
|
+
const { records, readError, malformed, malformedReasons } = ledgerPath ? readLedger(ledgerPath) : { records: [], malformed: 0 };
|
|
173
|
+
// Fail CLOSED like every sibling ledger reader (No-silent-failures): an unreadable OR malformed
|
|
174
|
+
// ledger must never silently render an empty/partial digest the reviewer then grounds against — an
|
|
175
|
+
// empty section would be indistinguishable from a legitimately-empty segment.
|
|
176
|
+
if (readError) throw fail(1, `--ledger-summary cannot read the ledger (${readError}) — failing closed; inspect ${ledgerPath}`);
|
|
177
|
+
if (malformed > 0) throw fail(1, `--ledger-summary: the ledger has ${malformed} malformed line(s) — failing closed (a dropped line could hide a round/finding): ${malformedReasons.join('; ')}`);
|
|
178
|
+
return renderLedgerSummary(records, { loop, base });
|
|
179
|
+
};
|
|
180
|
+
|
|
107
181
|
// ── the --out destination guard (gitignored / out-of-repo scratch ONLY) ────────────────
|
|
108
182
|
|
|
109
183
|
const gitLine = (args, cwd) => {
|
|
@@ -157,13 +231,17 @@ export const assertScratchDestination = (outPath, cwd) => {
|
|
|
157
231
|
const HELP = `grounding — grounded-review facts assembler for the agent-workflow family (AD-038).
|
|
158
232
|
|
|
159
233
|
Usage:
|
|
160
|
-
node grounding.mjs [--constraints] [--plan <path>] [--reserve-bytes <n>] [--out <path>]
|
|
234
|
+
node grounding.mjs [--constraints] [--plan <path>] [--ledger-summary] [--reserve-bytes <n>] [--out <path>]
|
|
161
235
|
|
|
162
236
|
--constraints slice the root AGENTS.md "Hard Constraints" section verbatim
|
|
163
237
|
(exactly one matching heading, else a loud STOP)
|
|
164
238
|
--plan <path> extract the plan's decision-bearing sections verbatim + whole:
|
|
165
239
|
"## Approach" + "## Verification" (REQUIRED — STOP if missing),
|
|
166
240
|
"## Decisions (locked)" when present; a duplicate heading is a STOP
|
|
241
|
+
--ledger-summary append a COMPUTED review-ledger digest for the SINGLE in-flight
|
|
242
|
+
plan-execution segment (rounds · origins · classifications · verdicts ·
|
|
243
|
+
overrides + a per-round render) — "computed, not remembered" facts; a
|
|
244
|
+
loud STOP unless exactly one plan is in flight
|
|
167
245
|
--reserve-bytes <n> the artifact share agy-review will add around these facts — the output
|
|
168
246
|
budget becomes AGY_MAX_PROMPT_BYTES − n (loud tail-trim on overflow)
|
|
169
247
|
--out <path> write instead of stdout — gitignored / out-of-repo scratch ONLY
|
|
@@ -181,9 +259,11 @@ const parseArgs = (argv) => {
|
|
|
181
259
|
let plan = null;
|
|
182
260
|
let out = null;
|
|
183
261
|
let reserve = 0;
|
|
262
|
+
let ledgerSummary = false;
|
|
184
263
|
for (let i = 0; i < argv.length; i += 1) {
|
|
185
264
|
const a = argv[i];
|
|
186
265
|
if (a === '--constraints') constraints = true;
|
|
266
|
+
else if (a === '--ledger-summary') ledgerSummary = true;
|
|
187
267
|
else if (a === '--plan') {
|
|
188
268
|
plan = argv[i + 1];
|
|
189
269
|
if (!plan || plan.startsWith('--')) throw fail(2, '--plan requires a <path>');
|
|
@@ -199,8 +279,8 @@ const parseArgs = (argv) => {
|
|
|
199
279
|
i += 1;
|
|
200
280
|
} else throw fail(2, `unknown argument: ${a}`);
|
|
201
281
|
}
|
|
202
|
-
if (!constraints && plan == null) throw fail(2, 'nothing to assemble — pass --constraints and/or --
|
|
203
|
-
return { constraints, plan, out, reserve };
|
|
282
|
+
if (!constraints && plan == null && !ledgerSummary) throw fail(2, 'nothing to assemble — pass --constraints, --plan <path>, and/or --ledger-summary');
|
|
283
|
+
return { constraints, plan, out, reserve, ledgerSummary };
|
|
204
284
|
};
|
|
205
285
|
|
|
206
286
|
const resolveBudget = (env, reserve) => {
|
|
@@ -220,7 +300,7 @@ export const main = (argv, ctx = {}) => {
|
|
|
220
300
|
const env = ctx.env ?? process.env;
|
|
221
301
|
try {
|
|
222
302
|
if (argv.includes('--help') || argv.includes('-h')) return { code: 0, stdout: HELP, stderr: '' };
|
|
223
|
-
const { constraints, plan, out, reserve } = parseArgs(argv);
|
|
303
|
+
const { constraints, plan, out, reserve, ledgerSummary } = parseArgs(argv);
|
|
224
304
|
const budget = resolveBudget(env, reserve);
|
|
225
305
|
|
|
226
306
|
const readOrStop = (path, label) => {
|
|
@@ -233,7 +313,14 @@ export const main = (argv, ctx = {}) => {
|
|
|
233
313
|
const constraintsText = constraints ? readOrStop('AGENTS.md', 'root AGENTS.md') : null;
|
|
234
314
|
const planText = plan != null ? readOrStop(plan, 'plan file') : null;
|
|
235
315
|
|
|
236
|
-
const
|
|
316
|
+
const parts = [];
|
|
317
|
+
const assembled = assembleGrounding({ constraintsText, planText, planLabel: plan ?? 'plan' });
|
|
318
|
+
if (assembled) parts.push(assembled);
|
|
319
|
+
if (ledgerSummary) {
|
|
320
|
+
const summary = resolveLedgerSummary({ cwd, env });
|
|
321
|
+
if (summary) parts.push(summary);
|
|
322
|
+
}
|
|
323
|
+
const payload = parts.join('\n');
|
|
237
324
|
const { text, trimmedBytes } = trimToBudget(payload, budget);
|
|
238
325
|
const stderr = trimmedBytes > 0
|
|
239
326
|
? `[grounding] TRIM: assembled ${Buffer.byteLength(payload, 'utf8')} bytes > budget ${budget} (AGY_MAX_PROMPT_BYTES minus --reserve-bytes ${reserve}) — dropped ${trimmedBytes} tail bytes; the cut is marked in-band.`
|