@sabaiway/agent-workflow-kit 2.1.0 → 3.1.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 +94 -1
- package/README.md +7 -5
- package/SKILL.md +16 -8
- package/bin/install.mjs +11 -2
- package/bridges/antigravity-cli-bridge/SKILL.md +8 -1
- package/bridges/antigravity-cli-bridge/bin/agy-review-honesty.test.mjs +163 -0
- package/bridges/antigravity-cli-bridge/bin/agy-review-model-screen.test.mjs +76 -0
- package/bridges/antigravity-cli-bridge/bin/agy-review.sh +80 -15
- package/bridges/antigravity-cli-bridge/bin/agy-review.test.mjs +171 -22
- package/bridges/antigravity-cli-bridge/bin/agy.test.mjs +59 -41
- package/bridges/antigravity-cli-bridge/capability.json +3 -2
- package/bridges/codex-cli-bridge/SKILL.md +8 -1
- package/bridges/codex-cli-bridge/bin/codex-exec.test.mjs +67 -22
- package/bridges/codex-cli-bridge/bin/codex-review-honesty.test.mjs +156 -0
- package/bridges/codex-cli-bridge/bin/codex-review.sh +71 -10
- package/bridges/codex-cli-bridge/bin/codex-review.test.mjs +158 -27
- package/bridges/codex-cli-bridge/capability.json +3 -2
- package/capability.json +1 -1
- package/migrations/3.0.0-hardened-core-loop.md +44 -0
- package/migrations/README.md +1 -1
- package/package.json +2 -2
- package/references/hooks/gate-approve.mjs +1 -1
- package/references/modes/bootstrap.md +3 -3
- package/references/modes/commit-guard.md +19 -0
- package/references/modes/core-evidence.md +15 -0
- package/references/modes/coverage-check.md +12 -0
- package/references/modes/doc-parity.md +7 -6
- package/references/modes/gates.md +7 -6
- package/references/modes/grounding.md +1 -2
- package/references/modes/hook.md +1 -1
- package/references/modes/recommendations.md +1 -0
- package/references/modes/review-state.md +3 -3
- package/references/modes/upgrade.md +6 -6
- package/references/modes/velocity.md +2 -2
- package/references/modes/worktrees.md +120 -0
- package/references/scripts/archive-decisions.mjs +3 -3
- package/references/scripts/archive-decisions.test.mjs +5 -5
- package/references/scripts/check-docs-size-cli.test.mjs +41 -0
- package/references/scripts/check-docs-size.mjs +46 -29
- package/references/scripts/install-git-hooks-repo-exec.test.mjs +82 -0
- package/references/scripts/install-git-hooks.mjs +90 -18
- package/references/scripts/install-git-hooks.test.mjs +102 -0
- package/references/scripts/migrate-gates-branches.test.mjs +157 -0
- package/references/scripts/migrate-gates.mjs +395 -0
- package/references/scripts/migrate-gates.test.mjs +284 -0
- package/references/templates/agent_rules.md +2 -2
- package/tools/ack-write.mjs +1 -1
- package/tools/atomic-write.mjs +2 -2
- package/tools/autonomy-config.mjs +1 -1
- package/tools/autonomy-doctor.mjs +2 -2
- package/tools/autonomy-write.mjs +1 -1
- package/tools/bridge-settings-read.mjs +1 -1
- package/tools/bridge-settings.mjs +1 -1
- package/tools/changed-surface.mjs +16 -77
- package/tools/cheap-agents.mjs +2 -2
- package/tools/commands.mjs +26 -10
- package/tools/commit-guard.mjs +167 -0
- package/tools/core-evidence.mjs +914 -0
- package/tools/coverage-check.mjs +260 -0
- package/tools/delegation.mjs +1 -1
- package/tools/detect-backends.mjs +3 -3
- package/tools/doc-parity.mjs +11 -27
- package/tools/engine-source.mjs +1 -1
- package/tools/family-members.mjs +1 -1
- package/tools/family-registry.mjs +1 -1
- package/tools/fs-safe.mjs +1 -1
- package/tools/gate-hook.mjs +1 -1
- package/tools/{seed-gates.mjs → gates-init.mjs} +58 -52
- package/tools/grounding.mjs +19 -96
- package/tools/hide-footprint.mjs +1 -1
- package/tools/inject-methodology.mjs +72 -41
- package/tools/known-footprint.mjs +1 -1
- package/tools/labels.mjs +1 -1
- package/tools/lcov.mjs +6 -10
- package/tools/lens-region.mjs +1 -1
- package/tools/manifest/validate.mjs +19 -0
- package/tools/migrate-adr-store.mjs +5 -5
- package/tools/orchestration-config.mjs +1 -1
- package/tools/orchestration-write.mjs +1 -1
- package/tools/presentation.mjs +1 -1
- package/tools/procedures.mjs +3 -3
- package/tools/recipes.mjs +62 -11
- package/tools/recommendations.mjs +97 -14
- package/tools/release-scan.mjs +10 -3
- package/tools/renderers.mjs +1 -1
- package/tools/review-state.mjs +291 -343
- package/tools/run-gates.mjs +216 -92
- package/tools/sandbox-masks.mjs +14 -14
- package/tools/semver-lite.mjs +1 -1
- package/tools/set-autonomy.mjs +1 -1
- package/tools/set-recipe.mjs +2 -2
- package/tools/setup-backends.mjs +1 -1
- package/tools/surface.mjs +1 -1
- package/tools/uninstall.mjs +1 -1
- package/tools/velocity-profile.mjs +9 -9
- package/tools/view-model.mjs +1 -1
- package/tools/worktrees.mjs +2292 -0
- package/references/modes/fold-completeness.md +0 -30
- package/references/modes/review-ledger.md +0 -34
- package/references/templates/verification-profile.json +0 -10
- package/tools/fold-completeness-run.mjs +0 -1120
- package/tools/fold-completeness.mjs +0 -672
- package/tools/review-ledger-core.mjs +0 -428
- package/tools/review-ledger-write.mjs +0 -647
- package/tools/review-ledger.mjs +0 -630
- package/tools/sarif.mjs +0 -52
- package/tools/verification-profile.mjs +0 -219
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
2
|
+
// gates-init.mjs — the consented docs/ai/gates.json FILL preview (D9; the pure derivation carried
|
|
3
|
+
// from the retired standalone seeder, AD-042/AD-052 invariants intact). Reached ONLY through
|
|
4
|
+
// explicitly-consenting prose (the init/bootstrap flows and the gates.md consent-fill section) —
|
|
5
|
+
// it is NOT a routable mode token, it sits OUTSIDE every velocity allowlist tier (a
|
|
6
|
+
// consent-per-run writer is never pre-approved), and the shipped gates.json TEMPLATE stays EMPTY
|
|
7
|
+
// (AD-021/AD-038): a populated declaration is per-entry maintainer consent recorded through this
|
|
8
|
+
// preview, never auto-seeding. ONE lifecycle contract: this preview runs at INIT; at UPGRADE the
|
|
9
|
+
// only gates.json writer is the consented legacy migration.
|
|
8
10
|
//
|
|
9
11
|
// What it offers (the derivation invariants, test-pinned) — CLOSED-WORLD since AD-052: an entry
|
|
10
12
|
// is offered only when every axis is proven safe by MEMBERSHIP in a finite, test-guarded set,
|
|
@@ -29,7 +31,7 @@
|
|
|
29
31
|
// executes project-controlled tooling (a node_modules/.bin PATH shim intercepts under every
|
|
30
32
|
// form) — the documented residual, disclosed in the preview, bounded by the two consents;
|
|
31
33
|
// • ids derive kebab-case from script names (build:prod → build-prod) and every offered entry
|
|
32
|
-
// passes the runner's validateDeclaration (
|
|
34
|
+
// passes the runner's validateDeclaration (this module imports the validator — NEVER the
|
|
33
35
|
// reverse: run-gates.mjs stays a runner that writes nothing);
|
|
34
36
|
// • the review-state candidate appears ONLY when docs/ai/orchestration.json DECLARES
|
|
35
37
|
// reviewed/council on plan-execution.review — the slot the checker enforces — with the
|
|
@@ -44,7 +46,7 @@
|
|
|
44
46
|
// .workflow-version == lineage-head stamp gate on --apply only (the velocity/gate-hook precedent).
|
|
45
47
|
//
|
|
46
48
|
// Exit codes: 0 done / dry-run; 1 precondition STOP (no deployment, stamp, symlink, malformed
|
|
47
|
-
// declaration, id collision); 2 usage. Dependency-free, Node >=
|
|
49
|
+
// declaration, id collision); 2 usage. Dependency-free, Node >= 22. No side effects on import.
|
|
48
50
|
|
|
49
51
|
import { existsSync, lstatSync, readFileSync } from 'node:fs';
|
|
50
52
|
import { join, resolve, dirname } from 'node:path';
|
|
@@ -58,39 +60,40 @@ const HERE = dirname(fileURLToPath(import.meta.url));
|
|
|
58
60
|
const KIT_ROOT = resolve(HERE, '..');
|
|
59
61
|
const TEMPLATE_PATH = join(KIT_ROOT, 'references', 'templates', 'gates.json');
|
|
60
62
|
const REVIEW_STATE_TOOL = join(KIT_ROOT, 'tools', 'review-state.mjs');
|
|
61
|
-
const
|
|
63
|
+
const COVERAGE_CHECK_TOOL = join(KIT_ROOT, 'tools', 'coverage-check.mjs');
|
|
62
64
|
const STAMP_REL = join('docs', 'ai', '.workflow-version');
|
|
63
65
|
|
|
64
66
|
const EXIT_OK = 0;
|
|
65
67
|
const EXIT_PRECONDITION = 1;
|
|
66
68
|
const EXIT_USAGE = 2;
|
|
67
69
|
|
|
68
|
-
export const
|
|
70
|
+
export const GATES_INIT_STOP = 'GATES_INIT_STOP';
|
|
69
71
|
const stop = (message) =>
|
|
70
|
-
Object.assign(new Error(`[agent-workflow-kit] ${message}`), { name: '
|
|
72
|
+
Object.assign(new Error(`[agent-workflow-kit] ${message}`), { name: 'GatesInitStop', code: GATES_INIT_STOP, exitCode: EXIT_PRECONDITION });
|
|
71
73
|
const usageFail = (message) =>
|
|
72
74
|
Object.assign(new Error(`[agent-workflow-kit] ${message}`), { exitCode: EXIT_USAGE });
|
|
73
75
|
|
|
74
76
|
// The trust-chain disclosure (AD-042) — printed with EVERY preview, token-pinned by the tests: a
|
|
75
77
|
// consent-seeded command becomes auto-approvable only after TWO explicit consents.
|
|
76
78
|
export const TRUST_CHAIN_DISCLOSURE =
|
|
77
|
-
'Disclosure: once the optional approval hook is wired
|
|
78
|
-
'auto-approves byte-exact declared gate commands from the
|
|
79
|
-
'and hook wiring (its own consent) are two separate
|
|
80
|
-
'own tooling, which executes project-controlled code
|
|
81
|
-
'consents bound it).';
|
|
82
|
-
|
|
83
|
-
const USAGE = `usage:
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
derived { id, title, cmd } entries and writes NOTHING. --apply APPENDS exactly the
|
|
87
|
-
entries (--only <id> selects a subset; append-only — existing entries are never
|
|
88
|
-
removed, an id collision is refused). The offer is CLOSED-WORLD: only a
|
|
89
|
-
name (test / lint / type-check / build — never release/publish/deploy,
|
|
90
|
-
write-mode variant) whose BODY is a member of the literal runner
|
|
91
|
-
hook-free \`COREPACK_ENABLE_NETWORK=0 <pm> exec -- <body>\` form for
|
|
92
|
-
|
|
93
|
-
be declared by hand); non-gate-class
|
|
79
|
+
'Disclosure: gates.json is a PRIVILEGED file — once the optional approval hook is wired ' +
|
|
80
|
+
'(/agent-workflow-kit hook), it auto-approves byte-exact declared gate commands from the ' +
|
|
81
|
+
'project root — filling (this consent) and hook wiring (its own consent) are two separate ' +
|
|
82
|
+
'yeses. A script gate runs the project\'s own tooling, which executes project-controlled code ' +
|
|
83
|
+
'(this preview does not sandbox it; the two consents bound it).';
|
|
84
|
+
|
|
85
|
+
const USAGE = `usage: gates-init [--dry-run | --apply] [--only <id>]... [--cwd <dir>] [--help]
|
|
86
|
+
|
|
87
|
+
The consented FILL preview for the project's own docs/ai/gates.json (D9). Default is --dry-run:
|
|
88
|
+
prints the derived { id, title, cmd } entries and writes NOTHING. --apply APPENDS exactly the
|
|
89
|
+
consented entries (--only <id> selects a subset; append-only — existing entries are never
|
|
90
|
+
modified or removed, an id collision is refused). The offer is CLOSED-WORLD: only a
|
|
91
|
+
terminating-class script name (test / lint / type-check / build — never release/publish/deploy,
|
|
92
|
+
never watch/serve, never a write-mode variant) whose BODY is a member of the literal runner
|
|
93
|
+
allowlist is offered, as the hook-free \`COREPACK_ENABLE_NETWORK=0 <pm> exec -- <body>\` form for
|
|
94
|
+
the detected package manager. A gate-class script whose body is NOT in the allowlist is screened
|
|
95
|
+
out with a note naming it (a command you trust can still be declared by hand); non-gate-class
|
|
96
|
+
names are excluded silently, as always.
|
|
94
97
|
${TRUST_CHAIN_DISCLOSURE}`;
|
|
95
98
|
|
|
96
99
|
// ── candidate classification: the NAME screens (the LOCKED derivation invariants) ──────
|
|
@@ -310,12 +313,14 @@ export const reviewStateCandidate = (cwd, deps = {}) => {
|
|
|
310
313
|
}
|
|
311
314
|
};
|
|
312
315
|
|
|
313
|
-
// The conditional
|
|
314
|
-
// review-state candidate (offered ONLY when plan-execution.review is reviewed/council), keyed on
|
|
315
|
-
// same slot, path resolved + QUOTED.
|
|
316
|
-
//
|
|
317
|
-
|
|
318
|
-
|
|
316
|
+
// The conditional COVERAGE-CHECK candidate (D3(a)) — the SAME consent + conditional rule as the
|
|
317
|
+
// review-state candidate (offered ONLY when plan-execution.review is reviewed/council), keyed on
|
|
318
|
+
// the same slot, path resolved + QUOTED. Together they are the canonical core pair `run-gates
|
|
319
|
+
// --final` requires (the checker declared LAST — buildOffer appends it last so a whole-offer
|
|
320
|
+
// apply lands final-ready); review-state gates receipt satisfaction, coverage-check verifies the
|
|
321
|
+
// consumed lcov + the red-proof declarations.
|
|
322
|
+
export const coverageCheckCandidate = (cwd, deps = {}) => {
|
|
323
|
+
const toolPath = deps.coverageCheckTool ?? COVERAGE_CHECK_TOOL;
|
|
319
324
|
try {
|
|
320
325
|
const { config } = loadConfig(resolve(cwd), deps.readFile ?? readFileSync, deps.lstat ?? lstatSync);
|
|
321
326
|
const declared = config?.['plan-execution']?.review;
|
|
@@ -324,15 +329,15 @@ export const reviewLedgerCandidate = (cwd, deps = {}) => {
|
|
|
324
329
|
return {
|
|
325
330
|
candidate: null,
|
|
326
331
|
note:
|
|
327
|
-
`the
|
|
332
|
+
`the coverage-check candidate was withheld: the resolved kit path contains shell ` +
|
|
328
333
|
`metacharacters that do not survive double-quoting (${toolPath}) — declare the gate ` +
|
|
329
|
-
`by hand per references/modes/
|
|
334
|
+
`by hand per references/modes/coverage-check.md`,
|
|
330
335
|
};
|
|
331
336
|
}
|
|
332
337
|
return {
|
|
333
338
|
candidate: {
|
|
334
|
-
id: '
|
|
335
|
-
title: '
|
|
339
|
+
id: 'coverage-check',
|
|
340
|
+
title: 'Changed-line coverage + red-proof verification (the final-run checker)',
|
|
336
341
|
cmd: `node "${toolPath}" --check`,
|
|
337
342
|
},
|
|
338
343
|
note: null,
|
|
@@ -340,7 +345,7 @@ export const reviewLedgerCandidate = (cwd, deps = {}) => {
|
|
|
340
345
|
} catch (err) {
|
|
341
346
|
return {
|
|
342
347
|
candidate: null,
|
|
343
|
-
note: `orchestration config unreadable (${err.message}) — the
|
|
348
|
+
note: `orchestration config unreadable (${err.message}) — the coverage-check candidate was not evaluated`,
|
|
344
349
|
};
|
|
345
350
|
}
|
|
346
351
|
};
|
|
@@ -356,22 +361,23 @@ const assertOnlyIdsOffered = (offer, onlyIds = []) => {
|
|
|
356
361
|
}
|
|
357
362
|
};
|
|
358
363
|
|
|
359
|
-
// The full offer: script entries + the conditional review-state +
|
|
360
|
-
//
|
|
361
|
-
//
|
|
364
|
+
// The full offer: script entries + the conditional review-state + coverage-check candidates
|
|
365
|
+
// (coverage-check LAST — the `run-gates --final` declaration-shape rule requires the checker as
|
|
366
|
+
// the last declared gate, so a whole-offer apply is final-ready by construction). Both key on the
|
|
367
|
+
// same slot (plan-execution.review reviewed/council) but gate distinct axes.
|
|
362
368
|
export const buildOffer = (cwd, deps = {}) => {
|
|
363
369
|
const scripts = deriveScripts(cwd, deps);
|
|
364
370
|
const rs = reviewStateCandidate(cwd, deps);
|
|
365
|
-
const
|
|
366
|
-
const candidates = [rs.candidate,
|
|
371
|
+
const cc = coverageCheckCandidate(cwd, deps);
|
|
372
|
+
const candidates = [rs.candidate, cc.candidate].filter(Boolean);
|
|
367
373
|
return {
|
|
368
374
|
entries: [...scripts.entries, ...candidates],
|
|
369
|
-
notes: [...scripts.notes, rs.note,
|
|
375
|
+
notes: [...scripts.notes, rs.note, cc.note].filter(Boolean),
|
|
370
376
|
};
|
|
371
377
|
};
|
|
372
378
|
|
|
373
379
|
// The RUNNABLE apply invocation for a given project — this tool has no bin and no mode token, so
|
|
374
|
-
// the consent step must print the real command, never a bare `
|
|
380
|
+
// the consent step must print the real command, never a bare `gates-init`. Consent integrity: a
|
|
375
381
|
// previewed --only subset is carried into the hint VERBATIM (ids are shell-safe by construction —
|
|
376
382
|
// kebabIdOf output / the fixed `review-state`), so following the hint can never widen the consent.
|
|
377
383
|
// Paths that do not survive double-quoting (the reviewStateCandidate screen, same pattern) make
|
|
@@ -386,7 +392,7 @@ export const applyInvocationFor = (cwd, onlyIds = []) => {
|
|
|
386
392
|
const GENERIC_APPLY_HINT = 're-run this same command with --apply [--only <id>]...';
|
|
387
393
|
|
|
388
394
|
export const formatPreview = (offer, applyInvocation = null, { explicitOnly = false } = {}) => {
|
|
389
|
-
const lines = ['[agent-workflow-kit] gates
|
|
395
|
+
const lines = ['[agent-workflow-kit] gates fill preview (dry-run — nothing was written):'];
|
|
390
396
|
if (!offer.entries.length) {
|
|
391
397
|
lines.push(' nothing to offer — no seedable terminating verification scripts were found.');
|
|
392
398
|
}
|
|
@@ -418,14 +424,14 @@ const loadExistingDeclaration = (cwd, deps = {}) => {
|
|
|
418
424
|
try {
|
|
419
425
|
return JSON.parse(String(read(full, 'utf8')));
|
|
420
426
|
} catch (err) {
|
|
421
|
-
throw stop(`${GATES_REL}: malformed JSON (${err.message}) — fix it by hand;
|
|
427
|
+
throw stop(`${GATES_REL}: malformed JSON (${err.message}) — fix it by hand; this preview never writes over a declaration it cannot parse`);
|
|
422
428
|
}
|
|
423
429
|
})();
|
|
424
430
|
const gates = (() => {
|
|
425
431
|
try {
|
|
426
432
|
return validateDeclaration(parsed);
|
|
427
433
|
} catch (err) {
|
|
428
|
-
throw stop(`${err.message} — fix it by hand;
|
|
434
|
+
throw stop(`${err.message} — fix it by hand; this preview never writes over an invalid declaration`);
|
|
429
435
|
}
|
|
430
436
|
})();
|
|
431
437
|
return { outcome: 'loaded', readme: typeof parsed._README === 'string' ? parsed._README : undefined, gates };
|
|
@@ -453,7 +459,7 @@ const readStampValue = (cwd, deps = {}) => {
|
|
|
453
459
|
};
|
|
454
460
|
|
|
455
461
|
// ── apply (append exactly the consented entries) ───────────────────────────────────────
|
|
456
|
-
export const
|
|
462
|
+
export const applyFill = ({ cwd, onlyIds = [] }, deps = {}) => {
|
|
457
463
|
assertDocsAiDeployment(cwd, deps, { stop, noun: 'a gate declaration', rel: GATES_REL });
|
|
458
464
|
const stampValue = readStampValue(cwd, deps);
|
|
459
465
|
if (stampValue !== EXPECTED_WORKFLOW_VERSION) {
|
|
@@ -474,7 +480,7 @@ export const applySeed = ({ cwd, onlyIds = [] }, deps = {}) => {
|
|
|
474
480
|
if (collisions.length) {
|
|
475
481
|
throw stop(
|
|
476
482
|
`id collision — already declared in ${GATES_REL}: ${collisions.join(', ')} (append-only: the ` +
|
|
477
|
-
`
|
|
483
|
+
`fill never modifies or removes an existing entry; pick the others with --only, or edit by hand)`,
|
|
478
484
|
);
|
|
479
485
|
}
|
|
480
486
|
|
|
@@ -539,7 +545,7 @@ export const main = (argv = process.argv.slice(2), deps = {}) => {
|
|
|
539
545
|
log(formatPreview(filtered, applyInvocationFor(cwd, args.only), { explicitOnly: args.only.length > 0 }));
|
|
540
546
|
return EXIT_OK;
|
|
541
547
|
}
|
|
542
|
-
const result =
|
|
548
|
+
const result = applyFill({ cwd, onlyIds: args.only }, deps);
|
|
543
549
|
if (result.outcome === 'nothing') {
|
|
544
550
|
log('[agent-workflow-kit] nothing to offer — no seedable terminating verification scripts were found; wrote nothing.');
|
|
545
551
|
for (const note of result.notes) log(` note: ${note}`); // the user learns WHY (same note as the preview)
|
package/tools/grounding.mjs
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
// not-ignored path (a new untracked file would itself move the review fingerprint the facts are
|
|
24
24
|
// about to ground). stdout is the default. It never commits and never runs a subscription CLI.
|
|
25
25
|
//
|
|
26
|
-
// Dependency-free, Node >=
|
|
26
|
+
// Dependency-free, Node >= 22. No side effects on import (the isDirectRun idiom).
|
|
27
27
|
|
|
28
28
|
import { readFileSync, writeFileSync, lstatSync, realpathSync } from 'node:fs';
|
|
29
29
|
import { resolve, join, relative, isAbsolute, dirname, basename, sep } from 'node:path';
|
|
@@ -31,11 +31,6 @@ import { tmpdir } from 'node:os';
|
|
|
31
31
|
import { pathToFileURL } from 'node:url';
|
|
32
32
|
import { spawnSync } from 'node:child_process';
|
|
33
33
|
import { fail } from './orchestration-config.mjs';
|
|
34
|
-
// (e) --ledger-summary (BUGFREE-3 / AD-049): a computed, loop/base-SCOPED review-ledger digest for
|
|
35
|
-
// the facts payload. The review ledger is read-only here — grounding never writes it. ORIGINS +
|
|
36
|
-
// computeTelemetry give the counts; the segment filter gives the scope.
|
|
37
|
-
import { ORIGINS, computeTelemetry, filterSegmentRecords, readLedger, resolveLedgerPath, resolveBase } from './review-ledger.mjs';
|
|
38
|
-
import { plansInFlight } from './review-state.mjs';
|
|
39
34
|
// (f) --autonomy (AD-044 Plan 3): the effective per-project autonomy policy for the facts payload.
|
|
40
35
|
// READ core only — never autonomy-write.mjs (the import-split invariant).
|
|
41
36
|
import { AUTONOMY_REL, loadAutonomy, resolveAutonomy, isSparseSeedConfig } from './autonomy-config.mjs';
|
|
@@ -107,7 +102,7 @@ export const assembleGrounding = ({ constraintsText = null, autonomyText = null,
|
|
|
107
102
|
// Trim `payload` to `budget` bytes, tail-first, with a loud in-band marker. The budget is a HARD
|
|
108
103
|
// ceiling: when it is smaller than the marker itself, the marker is truncated too (the output may
|
|
109
104
|
// never exceed `budget` — a facts file over the reserved share would push the final wrapper prompt
|
|
110
|
-
// past the argv ceiling
|
|
105
|
+
// past the argv ceiling). Returns { text, trimmedBytes }. Never a silent cut.
|
|
111
106
|
export const trimToBudget = (payload, budget) => {
|
|
112
107
|
const bytes = Buffer.byteLength(payload, 'utf8');
|
|
113
108
|
if (bytes <= budget) return { text: payload, trimmedBytes: 0 };
|
|
@@ -119,72 +114,10 @@ export const trimToBudget = (payload, budget) => {
|
|
|
119
114
|
return { text, trimmedBytes: bytes - keep };
|
|
120
115
|
};
|
|
121
116
|
|
|
122
|
-
// ── (e) --ledger-summary: a loop/base-SCOPED review-ledger digest (read-only, computed) ─────────
|
|
123
|
-
// `computeTelemetry` aggregates ALL loops and is not per-round-facts-shaped, so this renders the
|
|
124
|
-
// in-flight SEGMENT only (plan-execution, loop, base) — the telemetry COUNTS filtered to that
|
|
125
|
-
// segment, plus a terse per-round/triage/override render. Deliberately compact + distinct from the
|
|
126
|
-
// review-ledger `--status` human view (that one is indented + segment-grouped): this is a byte-
|
|
127
|
-
// budgeted FACTS block that agy/codex review against, so unrelated loops are excluded by construction.
|
|
128
|
-
|
|
129
|
-
const baseTag = (base) => (base === null ? '(unborn branch)' : String(base).slice(0, 12));
|
|
130
|
-
const countsOf = (obj) => {
|
|
131
|
-
const keys = Object.keys(obj).sort();
|
|
132
|
-
return keys.length === 0 ? '(none)' : keys.map((k) => `${k}:${obj[k]}`).join(' ');
|
|
133
|
-
};
|
|
134
|
-
const summaryRoundLine = (r) =>
|
|
135
|
-
`round ${r.round} — ${r.backends.map((b) => `${b.backend} ${b.degraded ? `degraded(${b.reason})` : `${b.blockers}/${b.majors}/${b.minors} ${b.verdict}`}`).join(', ')}` +
|
|
136
|
-
(r.findings.length ? ` [${r.findings.map((f) => `${f.findingKey}(${f.severity})`).join(', ')}]` : '');
|
|
137
|
-
const summaryTriageLine = (t) => `triage @round ${t.round} — ${t.classifications.map((c) => `${c.findingKey}=${c.class}`).join(', ')}`;
|
|
138
|
-
const summaryOverrideLine = (o) =>
|
|
139
|
-
`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}`;
|
|
140
|
-
const summaryGateRunLine = (g) => `gate-run — status=${g.summary.status} ${g.summary.passed}/${g.summary.gates} green`;
|
|
141
|
-
const summaryRecordLine = (r) =>
|
|
142
|
-
r.kind === 'round' ? summaryRoundLine(r) : r.kind === 'triage' ? summaryTriageLine(r) : r.kind === 'override' ? summaryOverrideLine(r) : summaryGateRunLine(r);
|
|
143
|
-
|
|
144
|
-
// renderLedgerSummary(records, { loop, base }) → the scoped facts section, or '' when the segment
|
|
145
|
-
// holds no records (empty/absent → nothing to ground). Pure: the caller does the I/O.
|
|
146
|
-
export const renderLedgerSummary = (records, { loop, base }) => {
|
|
147
|
-
const segment = filterSegmentRecords(records, { activity: PLAN_EXECUTION, loop, base });
|
|
148
|
-
if (segment.length === 0) return '';
|
|
149
|
-
const t = computeTelemetry(segment, []).loops[0];
|
|
150
|
-
const lines = [
|
|
151
|
-
`## Review-ledger summary — loop ${loop} @ base ${baseTag(base)}`,
|
|
152
|
-
'',
|
|
153
|
-
`rounds ${t.rounds} · divergence-rounds ${t.divergenceRounds}`,
|
|
154
|
-
`finding origins — ${ORIGINS.map((k) => `${k}:${t.origins[k]}`).join(' ')}`,
|
|
155
|
-
`classifications — ${countsOf(t.classifications)}`,
|
|
156
|
-
`backend verdicts — ${Object.keys(t.backendVerdicts).sort().map((b) => `${b}{${countsOf(t.backendVerdicts[b])}}`).join(' · ') || '(none)'}`,
|
|
157
|
-
`overrides — ${countsOf(t.overrides)}`,
|
|
158
|
-
'',
|
|
159
|
-
...segment.map(summaryRecordLine),
|
|
160
|
-
];
|
|
161
|
-
return `${lines.join('\n')}\n`;
|
|
162
|
-
};
|
|
163
|
-
|
|
164
|
-
// resolveLedgerSummary({ cwd, env }) → the scoped section text for the SINGLE in-flight plan-execution
|
|
165
|
-
// segment. A loud STOP unless exactly one plan is in flight (the family's single-plan discipline —
|
|
166
|
-
// never guess which loop to ground). Reads the ledger read-only.
|
|
167
117
|
const gitTop = (cwd) => {
|
|
168
118
|
const r = gitLine(['rev-parse', '--show-toplevel'], cwd);
|
|
169
119
|
return r && r.status === 0 ? r.stdout.replace(/\r?\n$/, '') : cwd;
|
|
170
120
|
};
|
|
171
|
-
export const resolveLedgerSummary = ({ cwd, env }) => {
|
|
172
|
-
const root = gitTop(cwd);
|
|
173
|
-
const plans = plansInFlight(root);
|
|
174
|
-
if (plans.length !== 1) {
|
|
175
|
-
throw fail(1, `--ledger-summary needs exactly one in-flight plan (in flight: ${plans.length ? plans.join(', ') : 'none'}) — resolve to one active plan`);
|
|
176
|
-
}
|
|
177
|
-
const loop = plans[0].replace(/\.md$/, '');
|
|
178
|
-
const base = resolveBase(cwd);
|
|
179
|
-
const ledgerPath = resolveLedgerPath(cwd, env);
|
|
180
|
-
const { records, readError, malformed, malformedReasons } = ledgerPath ? readLedger(ledgerPath) : { records: [], malformed: 0 };
|
|
181
|
-
// Fail CLOSED like every sibling ledger reader (No-silent-failures): an unreadable OR malformed
|
|
182
|
-
// ledger must never silently render an empty/partial digest the reviewer then grounds against — an
|
|
183
|
-
// empty section would be indistinguishable from a legitimately-empty segment.
|
|
184
|
-
if (readError) throw fail(1, `--ledger-summary cannot read the ledger (${readError}) — failing closed; inspect ${ledgerPath}`);
|
|
185
|
-
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('; ')}`);
|
|
186
|
-
return renderLedgerSummary(records, { loop, base });
|
|
187
|
-
};
|
|
188
121
|
|
|
189
122
|
// ── (f) --autonomy: the effective autonomy policy as a computed facts block (AD-044 Plan 3) ─────
|
|
190
123
|
// The ONE shared resolver (resolveAutonomy) renders the FULL effective policy — every red-line and
|
|
@@ -231,17 +164,17 @@ const gitLine = (args, cwd) => {
|
|
|
231
164
|
// EXISTING in-repo file, even gitignored, is a project file — the .env clobber class). Refused:
|
|
232
165
|
// tracked paths, in-repo not-ignored paths (a new untracked file would move the fingerprint),
|
|
233
166
|
// every other outside-repo destination, symlink and existing non-regular leaves, and any
|
|
234
|
-
// unverifiable lstat. The check runs on the REAL destination, never the lexical path
|
|
167
|
+
// unverifiable lstat. The check runs on the REAL destination, never the lexical path:
|
|
235
168
|
// the parent directory is realpath-resolved first, so a symlink can never route the write onto a
|
|
236
169
|
// tracked/in-repo file. Returns { path, kind: 'temp' | 'repo-fresh' } — a repo-fresh caller MUST
|
|
237
|
-
// write with the exclusive flag ('wx'), sealing the guard→write race
|
|
170
|
+
// write with the exclusive flag ('wx'), sealing the guard→write race.
|
|
238
171
|
export const assertScratchDestination = (outPath, cwd) => {
|
|
239
172
|
const lexical = isAbsolute(outPath) ? outPath : resolve(cwd, outPath);
|
|
240
173
|
let leaf = null;
|
|
241
174
|
try {
|
|
242
175
|
leaf = lstatSync(lexical);
|
|
243
176
|
} catch (err) {
|
|
244
|
-
// ONLY an absent leaf is a fresh file; any other lstat failure fails CLOSED (
|
|
177
|
+
// ONLY an absent leaf is a fresh file; any other lstat failure fails CLOSED (this
|
|
245
178
|
// writer is bridge-tier auto-allowable, so an unverifiable leaf must never be written).
|
|
246
179
|
if (err?.code !== 'ENOENT') {
|
|
247
180
|
throw fail(1, `--out cannot inspect the destination (${outPath}: ${err?.code ?? err?.message ?? err}) — refusing to write an unverifiable leaf`);
|
|
@@ -252,7 +185,7 @@ export const assertScratchDestination = (outPath, cwd) => {
|
|
|
252
185
|
throw fail(1, `--out refuses a symlink destination (${outPath}) — the write would follow it onto another file; name the real scratch path`);
|
|
253
186
|
}
|
|
254
187
|
if (leaf != null && !leaf.isFile()) {
|
|
255
|
-
throw fail(1, `--out refuses an existing non-regular destination (${outPath}) — a FIFO/device/directory write would hang or land on a non-file
|
|
188
|
+
throw fail(1, `--out refuses an existing non-regular destination (${outPath}) — a FIFO/device/directory write would hang or land on a non-file; name a fresh or regular scratch path`);
|
|
256
189
|
}
|
|
257
190
|
let realParent;
|
|
258
191
|
try {
|
|
@@ -261,8 +194,8 @@ export const assertScratchDestination = (outPath, cwd) => {
|
|
|
261
194
|
throw fail(1, `--out parent directory does not exist (${dirname(lexical)}) — create the scratch dir first`);
|
|
262
195
|
}
|
|
263
196
|
const full = join(realParent, basename(lexical));
|
|
264
|
-
// An out-of-repo (or no-repo) destination is scratch ONLY under a system temp root
|
|
265
|
-
//
|
|
197
|
+
// An out-of-repo (or no-repo) destination is scratch ONLY under a system temp root: the bridge
|
|
198
|
+
// tier auto-allows this tool with an args wildcard, so "anything outside
|
|
266
199
|
// the repo is scratch" would let an unattended run overwrite e.g. ~/.bashrc promptless.
|
|
267
200
|
// $TMPDIR / os.tmpdir() / /tmp are the scratch surface; everything else refuses loudly.
|
|
268
201
|
const assertTempScratch = () => {
|
|
@@ -282,7 +215,7 @@ export const assertScratchDestination = (outPath, cwd) => {
|
|
|
282
215
|
if (top == null || top.status !== 0) return { path: assertTempScratch().path, kind: 'temp' }; // no git tree → temp-only scratch
|
|
283
216
|
const root = top.stdout.replace(/\r?\n$/, '');
|
|
284
217
|
const rel = relative(root, full);
|
|
285
|
-
// Segment-safe outside test (the Issue-004 class
|
|
218
|
+
// Segment-safe outside test (the Issue-004 class): an in-repo file literally named
|
|
286
219
|
// `..facts` has rel `..facts` — `startsWith('..')` would misread it as outside and BYPASS the
|
|
287
220
|
// tracked/ignored refusals below. Only `..` exactly or a `..${sep}`-prefixed rel is outside.
|
|
288
221
|
if (rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel)) return { path: assertTempScratch().path, kind: 'temp' };
|
|
@@ -294,11 +227,11 @@ export const assertScratchDestination = (outPath, cwd) => {
|
|
|
294
227
|
if (ignored == null || ignored.status !== 0) {
|
|
295
228
|
throw fail(1, `--out refuses an in-repo path that is not gitignored (${rel}) — a new untracked file would move the review fingerprint; use a gitignored path or a location outside the repo`);
|
|
296
229
|
}
|
|
297
|
-
// An in-repo destination must be a FRESH file
|
|
230
|
+
// An in-repo destination must be a FRESH file: even a gitignored existing file is a
|
|
298
231
|
// project file (.env is the canonical victim) — an auto-allowable writer must never clobber one.
|
|
299
232
|
// The rewritable scratch surface is the system temp; in-repo gitignored writes are create-only —
|
|
300
233
|
// the CALLER must open with the exclusive flag ('wx'): the kind seals the pre-check against the
|
|
301
|
-
// guard→write race
|
|
234
|
+
// guard→write race.
|
|
302
235
|
if (leaf != null) {
|
|
303
236
|
throw fail(1, `--out refuses to OVERWRITE an existing in-repo file (${rel}) — even gitignored, it is a project file (the .env class); write rewritable scratch under $TMPDIR//tmp, or remove the file first`);
|
|
304
237
|
}
|
|
@@ -310,7 +243,7 @@ export const assertScratchDestination = (outPath, cwd) => {
|
|
|
310
243
|
const HELP = `grounding — grounded-review facts assembler for the agent-workflow family (AD-038).
|
|
311
244
|
|
|
312
245
|
Usage:
|
|
313
|
-
node grounding.mjs [--constraints] [--autonomy] [--plan <path>] [--
|
|
246
|
+
node grounding.mjs [--constraints] [--autonomy] [--plan <path>] [--reserve-bytes <n>] [--out <path>]
|
|
314
247
|
|
|
315
248
|
--constraints slice the root AGENTS.md "Hard Constraints" section verbatim
|
|
316
249
|
(exactly one matching heading, else a loud STOP)
|
|
@@ -322,10 +255,6 @@ Usage:
|
|
|
322
255
|
--plan <path> extract the plan's decision-bearing sections verbatim + whole:
|
|
323
256
|
"## Approach" + "## Verification" (REQUIRED — STOP if missing),
|
|
324
257
|
"## Decisions (locked)" when present; a duplicate heading is a STOP
|
|
325
|
-
--ledger-summary append a COMPUTED review-ledger digest for the SINGLE in-flight
|
|
326
|
-
plan-execution segment (rounds · origins · classifications · verdicts ·
|
|
327
|
-
overrides + a per-round render) — "computed, not remembered" facts; a
|
|
328
|
-
loud STOP unless exactly one plan is in flight
|
|
329
258
|
--reserve-bytes <n> the artifact share agy-review will add around these facts — the output
|
|
330
259
|
budget becomes AGY_MAX_PROMPT_BYTES − n (loud tail-trim on overflow)
|
|
331
260
|
--out <path> write instead of stdout — system-temp scratch (rewritable), or a FRESH
|
|
@@ -345,12 +274,10 @@ const parseArgs = (argv) => {
|
|
|
345
274
|
let plan = null;
|
|
346
275
|
let out = null;
|
|
347
276
|
let reserve = 0;
|
|
348
|
-
let ledgerSummary = false;
|
|
349
277
|
for (let i = 0; i < argv.length; i += 1) {
|
|
350
278
|
const a = argv[i];
|
|
351
279
|
if (a === '--constraints') constraints = true;
|
|
352
280
|
else if (a === '--autonomy') autonomy = true;
|
|
353
|
-
else if (a === '--ledger-summary') ledgerSummary = true;
|
|
354
281
|
else if (a === '--plan') {
|
|
355
282
|
plan = argv[i + 1];
|
|
356
283
|
if (!plan || plan.startsWith('--')) throw fail(2, '--plan requires a <path>');
|
|
@@ -366,10 +293,10 @@ const parseArgs = (argv) => {
|
|
|
366
293
|
i += 1;
|
|
367
294
|
} else throw fail(2, `unknown argument: ${a}`);
|
|
368
295
|
}
|
|
369
|
-
if (!constraints && !autonomy && plan == null
|
|
370
|
-
throw fail(2, 'nothing to assemble — pass --constraints, --autonomy, --plan <path
|
|
296
|
+
if (!constraints && !autonomy && plan == null) {
|
|
297
|
+
throw fail(2, 'nothing to assemble — pass --constraints, --autonomy, and/or --plan <path>');
|
|
371
298
|
}
|
|
372
|
-
return { constraints, autonomy, plan, out, reserve
|
|
299
|
+
return { constraints, autonomy, plan, out, reserve };
|
|
373
300
|
};
|
|
374
301
|
|
|
375
302
|
const resolveBudget = (env, reserve) => {
|
|
@@ -389,7 +316,7 @@ export const main = (argv, ctx = {}) => {
|
|
|
389
316
|
const env = ctx.env ?? process.env;
|
|
390
317
|
try {
|
|
391
318
|
if (argv.includes('--help') || argv.includes('-h')) return { code: 0, stdout: HELP, stderr: '' };
|
|
392
|
-
const { constraints, autonomy, plan, out, reserve
|
|
319
|
+
const { constraints, autonomy, plan, out, reserve } = parseArgs(argv);
|
|
393
320
|
const budget = resolveBudget(env, reserve);
|
|
394
321
|
|
|
395
322
|
const readOrStop = (path, label) => {
|
|
@@ -401,7 +328,7 @@ export const main = (argv, ctx = {}) => {
|
|
|
401
328
|
};
|
|
402
329
|
const constraintsText = constraints ? readOrStop('AGENTS.md', 'root AGENTS.md') : null;
|
|
403
330
|
const autonomyText = autonomy ? resolveAutonomyFacts({ cwd }) : null;
|
|
404
|
-
// --plan is CONFINED to the git work tree
|
|
331
|
+
// --plan is CONFINED to the git work tree: the bridge tier auto-allows this tool
|
|
405
332
|
// with an args wildcard, so an unattended invocation could otherwise point --plan at ANY
|
|
406
333
|
// readable file and ship it into the facts payload. Plans are in-repo by contract
|
|
407
334
|
// (docs/plans); an outside-tree path is a loud refusal, never a silent read.
|
|
@@ -424,10 +351,6 @@ export const main = (argv, ctx = {}) => {
|
|
|
424
351
|
const parts = [];
|
|
425
352
|
const assembled = assembleGrounding({ constraintsText, autonomyText, planText, planLabel: plan ?? 'plan' });
|
|
426
353
|
if (assembled) parts.push(assembled);
|
|
427
|
-
if (ledgerSummary) {
|
|
428
|
-
const summary = resolveLedgerSummary({ cwd, env });
|
|
429
|
-
if (summary) parts.push(summary);
|
|
430
|
-
}
|
|
431
354
|
const payload = parts.join('\n');
|
|
432
355
|
const { text, trimmedBytes } = trimToBudget(payload, budget);
|
|
433
356
|
const stderr = trimmedBytes > 0
|
|
@@ -437,7 +360,7 @@ export const main = (argv, ctx = {}) => {
|
|
|
437
360
|
if (out != null) {
|
|
438
361
|
const dest = assertScratchDestination(out, cwd);
|
|
439
362
|
// repo-fresh writes are EXCLUSIVE ('wx'): the create-only pre-check would otherwise race a
|
|
440
|
-
// file created between the guard and this write
|
|
363
|
+
// file created between the guard and this write; temp scratch stays rewritable.
|
|
441
364
|
// ctx.writeFile is a TEST seam (the EEXIST race arm is not constructible without it).
|
|
442
365
|
const writeFile = ctx.writeFile ?? writeFileSync;
|
|
443
366
|
try {
|
|
@@ -457,7 +380,7 @@ export const main = (argv, ctx = {}) => {
|
|
|
457
380
|
const isDirectRun = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
458
381
|
if (isDirectRun) {
|
|
459
382
|
const r = main(process.argv.slice(2));
|
|
460
|
-
// Exact writes + a natural exit
|
|
383
|
+
// Exact writes + a natural exit: console.log would append a stray newline to the
|
|
461
384
|
// byte-exact facts payload, and process.exit() can truncate large piped stdout before Node
|
|
462
385
|
// flushes. The payload already ends with '\n' (sliceSection normalizes); only a non-payload
|
|
463
386
|
// message (help / the --out report) gains the terminating newline it lacks.
|
package/tools/hide-footprint.mjs
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
// path). Add-local-first, then DETECT + REPORT the residual legacy global block — removal is the
|
|
18
18
|
// explicit, ASK-gated `--remove-global` (prints a restorable backup), never a default (an arbitrary
|
|
19
19
|
// host's OTHER hidden repo may rely on the same root-anchored global lines). Windows is supported (text
|
|
20
|
-
// edit; forward-slash patterns; CRLF preserved via EOL detection). Dependency-free, Node >=
|
|
20
|
+
// edit; forward-slash patterns; CRLF preserved via EOL detection). Dependency-free, Node >= 22.
|
|
21
21
|
|
|
22
22
|
import {
|
|
23
23
|
readFileSync, writeFileSync, statSync, readdirSync,
|