@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
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
// migrate-gates.test.mjs — spec for the consented legacy gates.json migration (strip-the-kit D8).
|
|
2
|
+
// The migration is ATOMIC and COMPLETE: canonical legacy entries removed + the unit-tests cmd
|
|
3
|
+
// extended with the lcov reporters + the coverage-check gate added LAST; customized entries are
|
|
4
|
+
// NEVER auto-touched (loud report + recovery); preview writes NOTHING; apply is tmp+rename.
|
|
5
|
+
|
|
6
|
+
import { describe, it } from 'node:test';
|
|
7
|
+
import assert from 'node:assert/strict';
|
|
8
|
+
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync, symlinkSync } from 'node:fs';
|
|
9
|
+
import { tmpdir } from 'node:os';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import { spawnSync } from 'node:child_process';
|
|
12
|
+
import {
|
|
13
|
+
LEGACY_FORMS,
|
|
14
|
+
UNIT_TESTS_COVERAGE_FLAGS,
|
|
15
|
+
RETIRED_STORE_BASENAMES,
|
|
16
|
+
findRetiredStores,
|
|
17
|
+
buildMigrationPlan,
|
|
18
|
+
resultingGates,
|
|
19
|
+
formatPreview,
|
|
20
|
+
main,
|
|
21
|
+
} from './migrate-gates.mjs';
|
|
22
|
+
|
|
23
|
+
const KIT_TOOLS = mkdtempSync(join(tmpdir(), 'migrate-gates-kit-'));
|
|
24
|
+
writeFileSync(join(KIT_TOOLS, 'coverage-check.mjs'), '// the installed checker the migration points at\n');
|
|
25
|
+
|
|
26
|
+
const mkProject = (gates) => {
|
|
27
|
+
const root = mkdtempSync(join(tmpdir(), 'migrate-gates-'));
|
|
28
|
+
mkdirSync(join(root, 'docs', 'ai'), { recursive: true });
|
|
29
|
+
writeFileSync(join(root, 'docs', 'ai', 'gates.json'), `${JSON.stringify({ _README: 'mine', gates }, null, 2)}\n`);
|
|
30
|
+
return root;
|
|
31
|
+
};
|
|
32
|
+
const gatesOf = (root) => JSON.parse(readFileSync(join(root, 'docs', 'ai', 'gates.json'), 'utf8')).gates;
|
|
33
|
+
const quiet = () => {
|
|
34
|
+
const out = [];
|
|
35
|
+
const err = [];
|
|
36
|
+
return { log: (l) => out.push(String(l)), error: (l) => err.push(String(l)), out, err };
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const LEGACY_LEDGER = { id: 'review-ledger', title: 'L', cmd: 'node "/kit/tools/review-ledger.mjs" --check' };
|
|
40
|
+
const LEGACY_FOLD = { id: 'fold-completeness', title: 'F', cmd: 'node /kit/tools/fold-completeness.mjs --check' };
|
|
41
|
+
const UNIT = { id: 'unit-tests', title: 'U', cmd: 'node --test tools/*.test.mjs' };
|
|
42
|
+
const CUSTOM = { id: 'my-ledger-wrap', title: 'C', cmd: 'node scripts/wrap.mjs && node /kit/tools/review-ledger.mjs --check' };
|
|
43
|
+
|
|
44
|
+
describe('migrate-gates — the pure migration plan', () => {
|
|
45
|
+
it('matches BOTH documented legacy forms (quoted and bare paths) and removes them', () => {
|
|
46
|
+
for (const form of LEGACY_FORMS) assert.ok(form.re instanceof RegExp);
|
|
47
|
+
const { plan } = buildMigrationPlan([LEGACY_LEDGER, LEGACY_FOLD], KIT_TOOLS);
|
|
48
|
+
assert.deepEqual(plan.filter((r) => r.action === 'remove').map((r) => r.entry.id), ['review-ledger', 'fold-completeness']);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('extends the canonical unit-tests cmd with the lcov reporters (flags inserted after `node --test`)', () => {
|
|
52
|
+
const { plan, unitTestsExtended } = buildMigrationPlan([UNIT], KIT_TOOLS);
|
|
53
|
+
assert.ok(unitTestsExtended);
|
|
54
|
+
const extended = plan.find((r) => r.action === 'extend').entry;
|
|
55
|
+
assert.equal(extended.cmd, `node --test ${UNIT_TESTS_COVERAGE_FLAGS} tools/*.test.mjs`);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('an already-extended unit-tests cmd is left alone (idempotent)', () => {
|
|
59
|
+
const done = { id: 'unit-tests', title: 'U', cmd: `node --test ${UNIT_TESTS_COVERAGE_FLAGS} tools/*.test.mjs` };
|
|
60
|
+
const { plan } = buildMigrationPlan([done], KIT_TOOLS);
|
|
61
|
+
assert.equal(plan.find((r) => r.entry.id === 'unit-tests').action, 'keep');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('adds the coverage-check gate LAST with the RESOLVED quoted path; never a second one', () => {
|
|
65
|
+
const { plan } = buildMigrationPlan([UNIT], KIT_TOOLS);
|
|
66
|
+
const result = resultingGates(plan);
|
|
67
|
+
const last = result[result.length - 1];
|
|
68
|
+
assert.equal(last.id, 'coverage-check');
|
|
69
|
+
assert.equal(last.cmd, `node "${join(KIT_TOOLS, 'coverage-check.mjs')}" --check`);
|
|
70
|
+
const again = buildMigrationPlan(result, KIT_TOOLS);
|
|
71
|
+
assert.ok(!again.plan.some((r) => r.action === 'add'), 'a declaration already carrying the checker gains no duplicate');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('a CUSTOMIZED dead-tool reference (compound form) is kept untouched and reported', () => {
|
|
75
|
+
const analysis = buildMigrationPlan([CUSTOM], KIT_TOOLS);
|
|
76
|
+
assert.equal(analysis.plan.find((r) => r.entry.id === 'my-ledger-wrap').action, 'keep');
|
|
77
|
+
assert.deepEqual(analysis.customized.map((g) => g.id), ['my-ledger-wrap']);
|
|
78
|
+
const preview = formatPreview(analysis, 'APPLY');
|
|
79
|
+
assert.match(preview, /CUSTOMIZED \(untouched\): my-ledger-wrap/);
|
|
80
|
+
assert.match(preview, /do NOT install the commit guard/);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe('migrate-gates — the canonical anchor + final-capability validation (round-1 folds)', () => {
|
|
85
|
+
it('a canonical checker NOT in the last position is MOVED last (never left mid-list)', () => {
|
|
86
|
+
const canonical = { id: 'coverage-check', title: 'CC', cmd: `node "${join(KIT_TOOLS, 'coverage-check.mjs')}" --check` };
|
|
87
|
+
const { plan } = buildMigrationPlan([canonical, UNIT], KIT_TOOLS);
|
|
88
|
+
const result = resultingGates(plan);
|
|
89
|
+
assert.equal(result[result.length - 1].id, 'coverage-check', 'the canonical checker ends up LAST');
|
|
90
|
+
assert.ok(plan.some((r) => r.action === 'move' && r.entry.id === 'coverage-check'), 'the reorder is an explicit move action');
|
|
91
|
+
assert.ok(!plan.some((r) => r.action === 'add'), 'no duplicate checker is added');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('a LOOKALIKE checker cmd is CUSTOMIZED (never counted canonical) and the canonical one is still added', () => {
|
|
95
|
+
const lookalike = { id: 'cov', title: 'C', cmd: 'node scripts/coverage-check.mjs --check' };
|
|
96
|
+
const analysis = buildMigrationPlan([lookalike, UNIT], KIT_TOOLS);
|
|
97
|
+
assert.ok(analysis.customized.some((g) => g.id === 'cov'), 'the lookalike is reported customized');
|
|
98
|
+
const result = resultingGates(analysis.plan);
|
|
99
|
+
assert.equal(result[result.length - 1].id, 'coverage-check', 'the REAL canonical checker is added last');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('the result is judged final-capable ONLY with a canonical review-state present; missing → a LOUD warning with the candidate line, never "final-run-capable"', () => {
|
|
103
|
+
const analysis = buildMigrationPlan([UNIT], KIT_TOOLS);
|
|
104
|
+
assert.equal(analysis.finalCapable, false, 'no review-state → not final-capable');
|
|
105
|
+
const preview = formatPreview(analysis, 'APPLY');
|
|
106
|
+
assert.match(preview, /review-state/, 'the warning names the missing core check');
|
|
107
|
+
assert.match(preview, /"id": "review-state"|review-state\.mjs/, 'the paste-ready candidate is carried');
|
|
108
|
+
assert.doesNotMatch(preview, /already final-run-capable/);
|
|
109
|
+
const withRs = buildMigrationPlan(
|
|
110
|
+
[UNIT, { id: 'review-state', title: 'RS', cmd: `node "${join(KIT_TOOLS, 'review-state.mjs')}" --check` }],
|
|
111
|
+
KIT_TOOLS,
|
|
112
|
+
);
|
|
113
|
+
assert.equal(withRs.finalCapable, true);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('a NON-canonical unit-tests cmd (npm test / wrapper) is CUSTOMIZED with the full flag set as the recovery', () => {
|
|
117
|
+
const npmTest = { id: 'unit-tests', title: 'U', cmd: 'npm test' };
|
|
118
|
+
const analysis = buildMigrationPlan([npmTest], KIT_TOOLS);
|
|
119
|
+
assert.equal(analysis.plan.find((r) => r.entry.id === 'unit-tests').action, 'keep');
|
|
120
|
+
assert.ok(analysis.customized.some((g) => g.id === 'unit-tests'), 'a non-canonical suite cmd is customized');
|
|
121
|
+
const preview = formatPreview(analysis, 'APPLY');
|
|
122
|
+
assert.match(preview, /--experimental-test-coverage/, 'the recovery carries the full flag set');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('a PARTIALLY-flagged unit-tests cmd is CUSTOMIZED (a lone coverage flag never reads as configured)', () => {
|
|
126
|
+
const partial = { id: 'unit-tests', title: 'U', cmd: 'node --test --experimental-test-coverage tools/*.test.mjs' };
|
|
127
|
+
const analysis = buildMigrationPlan([partial], KIT_TOOLS);
|
|
128
|
+
assert.equal(analysis.plan.find((r) => r.entry.id === 'unit-tests').action, 'keep');
|
|
129
|
+
assert.ok(analysis.customized.some((g) => g.id === 'unit-tests'), 'the half-wired cmd is customized, never silently left');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('a kitTools path with DQ-unsafe characters is a loud STOP before any write', () => {
|
|
133
|
+
const root = mkProject([UNIT]);
|
|
134
|
+
const evil = mkdtempSync(join(tmpdir(), 'migrate-gates-$evil-'));
|
|
135
|
+
writeFileSync(join(evil, 'coverage-check.mjs'), '// lookalike\n');
|
|
136
|
+
const io = quiet();
|
|
137
|
+
assert.equal(main(['--cwd', root, '--kit-tools', evil, '--apply'], io), 1);
|
|
138
|
+
assert.match(io.err.join('\n'), /double-quot|shell metacharacter/i);
|
|
139
|
+
rmSync(evil, { recursive: true, force: true });
|
|
140
|
+
rmSync(root, { recursive: true, force: true });
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('CUSTOMIZED warnings + the guard warning print even when apply has nothing else to do', () => {
|
|
144
|
+
const root = mkProject([CUSTOM]);
|
|
145
|
+
const io = quiet();
|
|
146
|
+
assert.equal(main(['--cwd', root, '--kit-tools', KIT_TOOLS, '--apply'], io), 0);
|
|
147
|
+
const text = io.out.join('\n');
|
|
148
|
+
assert.match(text, /CUSTOMIZED \(untouched\): my-ledger-wrap/, 'the customized row survives the no-op path');
|
|
149
|
+
assert.match(text, /do NOT install the commit guard/i, 'the guard warning survives the no-op path');
|
|
150
|
+
rmSync(root, { recursive: true, force: true });
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('a SYMLINKED docs (or docs/ai) PARENT is a STOP on preview AND apply (never written through)', () => {
|
|
154
|
+
const outside = mkdtempSync(join(tmpdir(), 'migrate-gates-target-'));
|
|
155
|
+
mkdirSync(join(outside, 'ai'), { recursive: true });
|
|
156
|
+
writeFileSync(join(outside, 'ai', 'gates.json'), `${JSON.stringify({ gates: [UNIT] })}\n`);
|
|
157
|
+
const root = mkdtempSync(join(tmpdir(), 'migrate-gates-symparent-'));
|
|
158
|
+
symlinkSync(outside, join(root, 'docs'));
|
|
159
|
+
for (const argv of [['--cwd', root, '--kit-tools', KIT_TOOLS], ['--cwd', root, '--kit-tools', KIT_TOOLS, '--apply']]) {
|
|
160
|
+
const io = quiet();
|
|
161
|
+
assert.equal(main(argv, io), 1, `must STOP: ${argv.join(' ')}`);
|
|
162
|
+
assert.match(io.err.join('\n'), /symlink/i);
|
|
163
|
+
}
|
|
164
|
+
rmSync(root, { recursive: true, force: true });
|
|
165
|
+
rmSync(outside, { recursive: true, force: true });
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe('migrate-gates — preview writes NOTHING; apply is atomic and complete', () => {
|
|
170
|
+
it('the dry-run default leaves gates.json byte-identical and prints the plan + the apply hint', () => {
|
|
171
|
+
const root = mkProject([LEGACY_LEDGER, UNIT]);
|
|
172
|
+
const before = readFileSync(join(root, 'docs', 'ai', 'gates.json'), 'utf8');
|
|
173
|
+
const io = quiet();
|
|
174
|
+
assert.equal(main(['--cwd', root, '--kit-tools', KIT_TOOLS], io), 0);
|
|
175
|
+
assert.equal(readFileSync(join(root, 'docs', 'ai', 'gates.json'), 'utf8'), before, 'dry-run must write nothing');
|
|
176
|
+
const text = io.out.join('\n');
|
|
177
|
+
assert.match(text, /REMOVE review-ledger/);
|
|
178
|
+
assert.match(text, /EXTEND unit-tests/);
|
|
179
|
+
assert.match(text, /ADD coverage-check/);
|
|
180
|
+
assert.match(text, /apply exactly this migration: node "\/[^"]*migrate-gates\.mjs" --kit-tools/, 'the apply hint is a REAL runnable path (never a file: URL)');
|
|
181
|
+
rmSync(root, { recursive: true, force: true });
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('--apply lands the full migration: legacy gone, unit-tests extended, coverage-check LAST', () => {
|
|
185
|
+
const root = mkProject([UNIT, LEGACY_LEDGER, LEGACY_FOLD]);
|
|
186
|
+
const io = quiet();
|
|
187
|
+
assert.equal(main(['--cwd', root, '--kit-tools', KIT_TOOLS, '--apply'], io), 0);
|
|
188
|
+
const gates = gatesOf(root);
|
|
189
|
+
assert.deepEqual(gates.map((g) => g.id), ['unit-tests', 'coverage-check']);
|
|
190
|
+
assert.match(gates[0].cmd, /--experimental-test-coverage/);
|
|
191
|
+
assert.equal(gates[gates.length - 1].id, 'coverage-check');
|
|
192
|
+
rmSync(root, { recursive: true, force: true });
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('an ALREADY-migrated declaration is a stated no-op on apply', () => {
|
|
196
|
+
const root = mkProject([UNIT, LEGACY_LEDGER]);
|
|
197
|
+
assert.equal(main(['--cwd', root, '--kit-tools', KIT_TOOLS, '--apply'], quiet()), 0);
|
|
198
|
+
const io = quiet();
|
|
199
|
+
assert.equal(main(['--cwd', root, '--kit-tools', KIT_TOOLS, '--apply'], io), 0);
|
|
200
|
+
assert.match(io.out.join('\n'), /nothing to migrate/);
|
|
201
|
+
rmSync(root, { recursive: true, force: true });
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('a MISSING gates.json is a stated no-op; a MALFORMED one is a loud STOP (never written over)', () => {
|
|
205
|
+
const empty = mkdtempSync(join(tmpdir(), 'migrate-gates-none-'));
|
|
206
|
+
const io = quiet();
|
|
207
|
+
assert.equal(main(['--cwd', empty, '--kit-tools', KIT_TOOLS], io), 0);
|
|
208
|
+
assert.match(io.out.join('\n'), /nothing to migrate/);
|
|
209
|
+
rmSync(empty, { recursive: true, force: true });
|
|
210
|
+
|
|
211
|
+
const root = mkProject([]);
|
|
212
|
+
writeFileSync(join(root, 'docs', 'ai', 'gates.json'), '{ not json');
|
|
213
|
+
const io2 = quiet();
|
|
214
|
+
assert.equal(main(['--cwd', root, '--kit-tools', KIT_TOOLS, '--apply'], io2), 1);
|
|
215
|
+
assert.match(io2.err.join('\n'), /malformed JSON/);
|
|
216
|
+
rmSync(root, { recursive: true, force: true });
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('a SYMLINKED gates.json is a STOP on preview AND apply', () => {
|
|
220
|
+
const root = mkdtempSync(join(tmpdir(), 'migrate-gates-link-'));
|
|
221
|
+
mkdirSync(join(root, 'docs', 'ai'), { recursive: true });
|
|
222
|
+
writeFileSync(join(root, 'real.json'), '{"gates":[]}\n');
|
|
223
|
+
symlinkSync(join(root, 'real.json'), join(root, 'docs', 'ai', 'gates.json'));
|
|
224
|
+
for (const argv of [['--cwd', root, '--kit-tools', KIT_TOOLS], ['--cwd', root, '--kit-tools', KIT_TOOLS, '--apply']]) {
|
|
225
|
+
const io = quiet();
|
|
226
|
+
assert.equal(main(argv, io), 1);
|
|
227
|
+
assert.match(io.err.join('\n'), /symlink/);
|
|
228
|
+
}
|
|
229
|
+
rmSync(root, { recursive: true, force: true });
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('retired git-dir stores: previewed as CLEAN rows (nothing deleted), unlinked on apply, ENOENT a no-op', () => {
|
|
233
|
+
const root = mkProject([LEGACY_LEDGER]);
|
|
234
|
+
const g = (...a) => spawnSync('git', a, { cwd: root, encoding: 'utf8' });
|
|
235
|
+
g('init', '-q');
|
|
236
|
+
for (const name of RETIRED_STORE_BASENAMES) writeFileSync(join(root, '.git', name), '{"dead":1}\n');
|
|
237
|
+
assert.equal(findRetiredStores(root).length, RETIRED_STORE_BASENAMES.length, 'all three retired basenames are found');
|
|
238
|
+
const io = quiet();
|
|
239
|
+
assert.equal(main(['--cwd', root, '--kit-tools', KIT_TOOLS], io), 0);
|
|
240
|
+
assert.match(io.out.join('\n'), /CLEAN .*agent-workflow-review-ledger\.v5-orphans\.jsonl/, 'the v5-orphans archive is previewed too');
|
|
241
|
+
assert.equal(findRetiredStores(root).length, RETIRED_STORE_BASENAMES.length, 'the dry-run deleted NOTHING');
|
|
242
|
+
const io2 = quiet();
|
|
243
|
+
assert.equal(main(['--cwd', root, '--kit-tools', KIT_TOOLS, '--apply'], io2), 0);
|
|
244
|
+
assert.deepEqual(findRetiredStores(root), [], 'apply unlinked every retired store');
|
|
245
|
+
assert.match(io2.out.join('\n'), /cleaned .*agent-workflow-fold-completeness\.jsonl/);
|
|
246
|
+
// A second apply: stores gone, declaration migrated — a stated no-op (ENOENT never errors).
|
|
247
|
+
const io3 = quiet();
|
|
248
|
+
assert.equal(main(['--cwd', root, '--kit-tools', KIT_TOOLS, '--apply'], io3), 0);
|
|
249
|
+
assert.match(io3.out.join('\n'), /nothing to migrate/);
|
|
250
|
+
rmSync(root, { recursive: true, force: true });
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('stores alone (an already-migrated declaration) still get cleaned on apply', () => {
|
|
254
|
+
const root = mkProject([UNIT]);
|
|
255
|
+
const g = (...a) => spawnSync('git', a, { cwd: root, encoding: 'utf8' });
|
|
256
|
+
g('init', '-q');
|
|
257
|
+
assert.equal(main(['--cwd', root, '--kit-tools', KIT_TOOLS, '--apply'], quiet()), 0); // migrate first
|
|
258
|
+
writeFileSync(join(root, '.git', RETIRED_STORE_BASENAMES[0]), '{"dead":1}\n');
|
|
259
|
+
const io = quiet();
|
|
260
|
+
assert.equal(main(['--cwd', root, '--kit-tools', KIT_TOOLS, '--apply'], io), 0);
|
|
261
|
+
assert.match(io.out.join('\n'), /cleaned .*agent-workflow-review-ledger\.jsonl/);
|
|
262
|
+
assert.deepEqual(findRetiredStores(root), []);
|
|
263
|
+
rmSync(root, { recursive: true, force: true });
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('outside a git tree the store cleanup is a silent no-op (no crash, gates.json still migrates)', () => {
|
|
267
|
+
const root = mkProject([LEGACY_LEDGER, UNIT]);
|
|
268
|
+
const io = quiet();
|
|
269
|
+
assert.equal(main(['--cwd', root, '--kit-tools', KIT_TOOLS, '--apply'], io), 0);
|
|
270
|
+
assert.deepEqual(gatesOf(root).map((g2) => g2.id), ['unit-tests', 'coverage-check']);
|
|
271
|
+
rmSync(root, { recursive: true, force: true });
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('--kit-tools is REQUIRED and must contain the checker (no runtime guessing)', () => {
|
|
275
|
+
const root = mkProject([UNIT]);
|
|
276
|
+
const io = quiet();
|
|
277
|
+
assert.equal(main(['--cwd', root], io), 2);
|
|
278
|
+
assert.match(io.err.join('\n'), /--kit-tools/);
|
|
279
|
+
const io2 = quiet();
|
|
280
|
+
assert.equal(main(['--cwd', root, '--kit-tools', root], io2), 1);
|
|
281
|
+
assert.match(io2.err.join('\n'), /coverage-check\.mjs/);
|
|
282
|
+
rmSync(root, { recursive: true, force: true });
|
|
283
|
+
});
|
|
284
|
+
});
|
|
@@ -76,7 +76,7 @@ Apply these when authoring a plan, reviewing, folding a finding, or editing code
|
|
|
76
76
|
- **Per-round emission.** Every review round emits **{round N · finding-origin tally · per-backend verdict}** so the crossover is a computed, visible signal, not a remembered rule.
|
|
77
77
|
- **Recipe fidelity.** Council runs every backend the recipe names, **every round**; silently dropping a ready backend for quota/convenience is a forbidden downgrade — an unavailable backend is a LOUD, stated degrade, never a quiet drop.
|
|
78
78
|
- **ExitPlanMode ≠ execute.** A harness "approved — start coding" prompt authorizes the PLAN only; this methodology overrides it. Continue into execution only as a DELIBERATE transition after the plan + cold-start prompt exist, never an implicit slide.
|
|
79
|
-
- **Cost lanes.** Route every step to the **cheapest adequate executor** — L0 deterministic script (the batched gate matrix over `gates.json`, the rotation `--check`s) · L1 cheap subagent (extraction/drafting only; the orchestrator verifies) · L2 subscription bridge · L3 frontier judgment. A step with **no named guardrail does not move down** a lane, and the **red lines never move down** (council review models · real code · ADR/handover/changelog-entry wording · persuasive copy · go/no-go · the approval asks). Own-error repair: salvage recorded state first (L0/L1, batched), never frontier re-derivation. **Prompt economy:** read-only fan-out (research/sweeps/extraction) runs ONLY on restricted-tool vehicles — a full-tool subagent for read-only work is a forbidden lane downgrade (invisible prompt-flood + blast radius), and a subagent is never told to shell out for facts obtainable read-only; the orchestrator's own shell form is ONE plain pipeline per call (a `;`/`&&` chain or env-prefixed invocation never matches a prefix allow rule); a fan-out launcher that gates per call yields to the agent-spawn lane — capability-gated: without restricted-tool vehicles (generic full-tool spawning does not count), read-only research stays in the orchestrator's own context, never a vehicle mandate a host cannot satisfy. Judgment, code, synthesis stay at the frontier lane (a task that genuinely runs/writes keeps a full-tool subagent); honest limit: no deterministic gate classifies a dispatch — canon at the point of use + placed vehicles + the retro loop. **Writer economy:** a stage's repeated WRITER commands batch
|
|
79
|
+
- **Cost lanes.** Route every step to the **cheapest adequate executor** — L0 deterministic script (the batched gate matrix over `gates.json`, the rotation `--check`s) · L1 cheap subagent (extraction/drafting only; the orchestrator verifies) · L2 subscription bridge · L3 frontier judgment. A step with **no named guardrail does not move down** a lane, and the **red lines never move down** (council review models · real code · ADR/handover/changelog-entry wording · persuasive copy · go/no-go · the approval asks). Own-error repair: salvage recorded state first (L0/L1, batched), never frontier re-derivation. **Prompt economy:** read-only fan-out (research/sweeps/extraction) runs ONLY on restricted-tool vehicles — a full-tool subagent for read-only work is a forbidden lane downgrade (invisible prompt-flood + blast radius), and a subagent is never told to shell out for facts obtainable read-only; the orchestrator's own shell form is ONE plain pipeline per call (a `;`/`&&` chain or env-prefixed invocation never matches a prefix allow rule); a fan-out launcher that gates per call yields to the agent-spawn lane — capability-gated: without restricted-tool vehicles (generic full-tool spawning does not count), read-only research stays in the orchestrator's own context, never a vehicle mandate a host cannot satisfy. Judgment, code, synthesis stay at the frontier lane (a task that genuinely runs/writes keeps a full-tool subagent); honest limit: no deterministic gate classifies a dispatch — canon at the point of use + placed vehicles + the retro loop. **Writer economy:** a stage's repeated WRITER commands batch — evidence declarations ride consecutive plain invocations of ONE allow-listed tool, other stage writers combine via one launcher per stage; never an unbatched writer scatter (each gated write is its own prompt).
|
|
80
80
|
|
|
81
81
|
---
|
|
82
82
|
|
|
@@ -93,7 +93,7 @@ Split a complex task across sessions for **focus and review hygiene**, not becau
|
|
|
93
93
|
## 4. User Interaction
|
|
94
94
|
|
|
95
95
|
1. **Don't rush to commit.** Prepare changes, run local quality checks, report progress with test outcomes.
|
|
96
|
-
2. **Get explicit approval** before
|
|
96
|
+
2. **Get explicit approval** before COMMITTING or moving to the next phase — staging is reversible loop-work (the final-run ordering stages first, reviews the staged tree, then asks ONCE at the commit; a separate staging ask is a useless approval).
|
|
97
97
|
|
|
98
98
|
---
|
|
99
99
|
|
package/tools/ack-write.mjs
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
// • atomic — exclusive-create *.tmp + rename (no partial-write state); last-writer-wins;
|
|
17
17
|
// • never commits.
|
|
18
18
|
//
|
|
19
|
-
// Dependency-free beyond the kit's own atomic-write core + the shared ack constants, Node >=
|
|
19
|
+
// Dependency-free beyond the kit's own atomic-write core + the shared ack constants, Node >= 22. No
|
|
20
20
|
// side effects on import (the isDirectRun idiom).
|
|
21
21
|
|
|
22
22
|
import { lstatSync, readFileSync } from 'node:fs';
|
package/tools/atomic-write.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// by (containment ROOT, absolute target, body, stop identity) so every consumer runs the same guarded
|
|
4
4
|
// flow with zero drift:
|
|
5
5
|
// • writeDocsAiFileAtomic — a file under a project's docs/ai/ (deployment-gated to cwd):
|
|
6
|
-
// orchestration-write.mjs → docs/ai/orchestration.json;
|
|
6
|
+
// orchestration-write.mjs → docs/ai/orchestration.json; gates-init.mjs → docs/ai/gates.json.
|
|
7
7
|
// • writeHostConfigFileAtomic — a file under a host config dir OUTSIDE any project tree
|
|
8
8
|
// (bridges 2.3.0, D6): ${XDG_CONFIG_HOME:-~/.config}/agent-workflow/bridge-settings.conf. The
|
|
9
9
|
// host dir is CREATED if absent (a host config SHOULD materialize), unlike the docs/ai gate
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
// - tmp cleaned up on any failure after its creation.
|
|
20
20
|
// - LAST-WRITER-WINS: local, single-user; no cross-process lock (documented, not silently assumed).
|
|
21
21
|
//
|
|
22
|
-
// Dependency-free, Node >=
|
|
22
|
+
// Dependency-free, Node >= 22. Every fs primitive is injectable (deps.*) so the guards are
|
|
23
23
|
// unit-testable. NEVER imported by a read-only module (procedures.mjs — pinned by an import guard).
|
|
24
24
|
|
|
25
25
|
import { lstatSync, writeFileSync, renameSync, rmSync, mkdirSync } from 'node:fs';
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
//
|
|
17
17
|
// This module performs NO filesystem WRITES — only reads (loadAutonomy). The single fs-writer lives in
|
|
18
18
|
// autonomy-write.mjs, which no read-only module imports, so the read surface stays fs-write-free.
|
|
19
|
-
// Pure-where-possible (fs injectable), dependency-free, Node >=
|
|
19
|
+
// Pure-where-possible (fs injectable), dependency-free, Node >= 22. No side effects on import.
|
|
20
20
|
|
|
21
21
|
import { readFileSync, lstatSync } from 'node:fs';
|
|
22
22
|
import { join } from 'node:path';
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
// a PATH shadow, never fixes it; (iii) the harness permission prompt shows the --apply tuple, not
|
|
36
36
|
// the absolute-path command — bridged by the apply-time re-print of the exact resolved command.
|
|
37
37
|
//
|
|
38
|
-
// Dependency-free, Node >=
|
|
38
|
+
// Dependency-free, Node >= 22. No side effects on import (the isDirectRun idiom).
|
|
39
39
|
|
|
40
40
|
import { spawnSync } from 'node:child_process';
|
|
41
41
|
import { closeSync, lstatSync, openSync } from 'node:fs';
|
|
@@ -254,7 +254,7 @@ const renderDiagnosis = ({ plan, probeResult, log }) => {
|
|
|
254
254
|
}
|
|
255
255
|
};
|
|
256
256
|
|
|
257
|
-
// The untrusted degrade every lane runs FIRST
|
|
257
|
+
// The untrusted degrade every lane runs FIRST: while any sandbox binary resolves only
|
|
258
258
|
// outside the trusted dirs, no offer, no consent invitation, and no privileged path may proceed —
|
|
259
259
|
// an install cannot make such a host verify-ready.
|
|
260
260
|
const refuseUntrusted = ({ plan, log }) => {
|
package/tools/autonomy-write.mjs
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// core (tools/atomic-write.mjs, AD-042) and is consumed here with this module's own STOP identity, so
|
|
11
11
|
// the public API + error contract are its own (this file's tests pin them).
|
|
12
12
|
//
|
|
13
|
-
// Dependency-free, Node >=
|
|
13
|
+
// Dependency-free, Node >= 22. Every fs primitive is injectable (deps.*) so the guards are unit-testable.
|
|
14
14
|
|
|
15
15
|
import { AUTONOMY_REL, serializeAutonomy } from './autonomy-config.mjs';
|
|
16
16
|
import { writeDocsAiFileAtomic } from './atomic-write.mjs';
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// lines, the manifest-as-source registry (the union of the bundled bridges' `settings` blocks), and
|
|
7
7
|
// the effective value of each knob (env > file > built-in default). It NEVER writes.
|
|
8
8
|
//
|
|
9
|
-
// Dependency-free, Node >=
|
|
9
|
+
// Dependency-free, Node >= 22. No side effects on import.
|
|
10
10
|
|
|
11
11
|
import { readFileSync, statSync } from 'node:fs';
|
|
12
12
|
import { homedir } from 'node:os';
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
// bundle (cannot determine the allowlist). main(argv, ctx) → { code, stdout, stderr }; getenv / home /
|
|
22
22
|
// bundleRoot / fs are injectable for host-independent tests.
|
|
23
23
|
//
|
|
24
|
-
// Dependency-free, Node >=
|
|
24
|
+
// Dependency-free, Node >= 22. No side effects on import (the isDirectRun idiom).
|
|
25
25
|
|
|
26
26
|
import { pathToFileURL } from 'node:url';
|
|
27
27
|
import { settingValueValid } from './manifest/validate.mjs';
|
|
@@ -1,20 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// changed-surface.mjs — the NEUTRAL shared read-only core
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// fold-ledger read path lives beside it: fold-completeness.mjs already imports review-ledger.mjs,
|
|
7
|
-
// so the review-ledger telemetry must reach the fold ledger WITHOUT importing back the other
|
|
8
|
-
// direction — it imports THIS module instead.
|
|
2
|
+
// changed-surface.mjs — the NEUTRAL shared read-only core: ONE home for the changed-surface
|
|
3
|
+
// computation the coverage checker consumes (the closed classification + new-side line numbering),
|
|
4
|
+
// plus the shared fail-closed knob parser and the strict N/N probe-verdict algebra the
|
|
5
|
+
// core-evidence red-proof observer reads.
|
|
9
6
|
//
|
|
10
7
|
// Import-graph invariant (pinned by import-split tests): this module imports NOTHING from the
|
|
11
|
-
// family — node built-ins only. Everyone may import it; it imports no one
|
|
12
|
-
// review-ledger.mjs → changed-surface.mjs ← fold-completeness.mjs ← fold-completeness-run.mjs
|
|
13
|
-
// review-ledger-write.mjs → changed-surface.mjs (the writer NEVER imports the runner — the
|
|
14
|
-
// sole-tree-toucher boundary, codex R2)
|
|
8
|
+
// family — node built-ins only. Everyone may import it; it imports no one.
|
|
15
9
|
//
|
|
16
10
|
// Read-only: never writes, never commits. It DOES spawn read-only `git` queries (diff/ls-files/
|
|
17
|
-
// rev-parse). Dependency-free
|
|
11
|
+
// rev-parse). Dependency-free. No side effects on import.
|
|
18
12
|
|
|
19
13
|
import { readFileSync, lstatSync } from 'node:fs';
|
|
20
14
|
import { join } from 'node:path';
|
|
@@ -43,7 +37,7 @@ export const classifyChangedPath = (rel) => {
|
|
|
43
37
|
|
|
44
38
|
// Strip the quotes and decode escapes BYTE-wise (octal escapes are UTF-8 bytes) — an unparsed
|
|
45
39
|
// quoted path compares unequal to its classifier/testId form and would silently escape the
|
|
46
|
-
// coverage/cap surface (
|
|
40
|
+
// coverage/cap surface (AD-047).
|
|
47
41
|
const CQUOTE_SIMPLE = { n: 10, t: 9, r: 13, f: 12, v: 11, b: 8, a: 7, '"': 34, '\\': 92 };
|
|
48
42
|
export const unquoteDiffPath = (p) => {
|
|
49
43
|
if (!(p.length >= 2 && p.startsWith('"') && p.endsWith('"'))) return p;
|
|
@@ -53,7 +47,7 @@ export const unquoteDiffPath = (p) => {
|
|
|
53
47
|
const c = inner[i];
|
|
54
48
|
if (c !== '\\') {
|
|
55
49
|
// Consume a full CODE POINT — 16-bit-unit iteration would split a surrogate pair (an
|
|
56
|
-
// unescaped non-BMP char, reachable under core.quotepath=false) into replacement bytes
|
|
50
|
+
// unescaped non-BMP char, reachable under core.quotepath=false) into replacement bytes.
|
|
57
51
|
const ch = String.fromCodePoint(inner.codePointAt(i));
|
|
58
52
|
for (const b of Buffer.from(ch, 'utf8')) bytes.push(b);
|
|
59
53
|
i += ch.length - 1;
|
|
@@ -95,7 +89,7 @@ export const parseUnifiedDiff = (diffText) => {
|
|
|
95
89
|
}
|
|
96
90
|
if (inHeader && line.startsWith('--- ')) continue;
|
|
97
91
|
if (inHeader && line.startsWith('+++ ')) {
|
|
98
|
-
const p = unquoteDiffPath(line.slice(4).replace(/[\t\r]+$/, '')); // TAB/CR are git artifacts, never filename bytes
|
|
92
|
+
const p = unquoteDiffPath(line.slice(4).replace(/[\t\r]+$/, '')); // TAB/CR are git artifacts, never filename bytes
|
|
99
93
|
current = p === '/dev/null' ? null : p.startsWith('b/') ? p.slice(2) : p;
|
|
100
94
|
inHeader = false;
|
|
101
95
|
continue;
|
|
@@ -120,7 +114,7 @@ export const parseUnifiedDiff = (diffText) => {
|
|
|
120
114
|
// ── the changed surface (git-driven; the ONE computation both consumers read) ─────────────────────
|
|
121
115
|
|
|
122
116
|
// The one diff-invocation shape every surface pass uses. The a/ b/ prefixes are pinned EXPLICITLY
|
|
123
|
-
//
|
|
117
|
+
// A user's global diff.noprefix=true would otherwise drop them and the parsers would eat
|
|
124
118
|
// a real directory named "a" — user git config must never bend the parse.
|
|
125
119
|
export const DIFF_FLAGS = ['--unified=0', '--no-color', '--no-ext-diff', '--no-renames', '--src-prefix=a/', '--dst-prefix=b/'];
|
|
126
120
|
|
|
@@ -129,11 +123,6 @@ const gitStdout = (args, cwd) => {
|
|
|
129
123
|
const r = runGit(args, cwd);
|
|
130
124
|
return r.error || r.status > 1 ? null : r.stdout;
|
|
131
125
|
};
|
|
132
|
-
const gitLine = (args, cwd) => {
|
|
133
|
-
const r = runGit(args, cwd);
|
|
134
|
-
if (r.error || r.status !== 0) return null;
|
|
135
|
-
return r.stdout.replace(/\r?\n$/, '');
|
|
136
|
-
};
|
|
137
126
|
// Both leaf guards are exported for the unit tests of exactly their fail-closed edges (the
|
|
138
127
|
// hashFileBytes precedent, AD-047) — internal consumers stay this module's own passes.
|
|
139
128
|
export const readFileSafe = (path) => {
|
|
@@ -145,7 +134,7 @@ export const readFileSafe = (path) => {
|
|
|
145
134
|
};
|
|
146
135
|
// A changed LEAF is read only if it is a REGULAR file. lstat (no-follow): a symlinked or non-regular
|
|
147
136
|
// leaf must NEVER be read/followed — following it could read outside the work tree or HANG on a
|
|
148
|
-
// FIFO/device (
|
|
137
|
+
// FIFO/device (AD-046). A non-regular leaf fails closed (routed to `unsupported`).
|
|
149
138
|
export const isRegularLeaf = (abs) => {
|
|
150
139
|
try {
|
|
151
140
|
return lstatSync(abs).isFile();
|
|
@@ -161,13 +150,13 @@ export const isRegularLeaf = (abs) => {
|
|
|
161
150
|
// pure deletions cost nothing, subtractive folds stay free); an untracked file is wholly new, so all
|
|
162
151
|
// its lines are "changed". `unsupportedLines` is the D4 cap's view of the SAME pass: unsupported
|
|
163
152
|
// SOURCE files carry their new-side lines too (excluding them would gift a large-TS-fold bypass,
|
|
164
|
-
//
|
|
153
|
+
// BUGFREE-2 D4); an unreadable/non-regular leaf counts 0 lines but stays LISTED (the
|
|
165
154
|
// coverage gate still fails closed on it). excluded-test and out-of-domain never carry lines.
|
|
166
155
|
export const computeChangedSurface = (root) => {
|
|
167
156
|
// Unborn branch (no HEAD yet): the plain diff alone misses files STAGED for the initial commit —
|
|
168
157
|
// they sit in the index, so they are neither worktree-vs-index changes nor untracked. Merge the
|
|
169
158
|
// --cached diff (index vs the empty tree) with the plain one; parseUnifiedDiff unions per-file
|
|
170
|
-
// lines across concatenated diffs
|
|
159
|
+
// lines across concatenated diffs.
|
|
171
160
|
const trackedDiff = gitStdout(['diff', 'HEAD', ...DIFF_FLAGS], root)
|
|
172
161
|
?? `${gitStdout(['diff', '--cached', ...DIFF_FLAGS], root) ?? ''}\n${gitStdout(['diff', ...DIFF_FLAGS], root) ?? ''}`;
|
|
173
162
|
const trackedLines = parseUnifiedDiff(trackedDiff);
|
|
@@ -215,22 +204,10 @@ export const computeChangedSurface = (root) => {
|
|
|
215
204
|
return { assessable, unsupported: [...unsupportedLines.keys()].sort(), outOfDomain: outOfDomain.sort(), unsupportedLines };
|
|
216
205
|
};
|
|
217
206
|
|
|
218
|
-
// countCapLines(surface) → the D4 counted magnitude: new-side changed lines of assessable AND
|
|
219
|
-
// unsupported SOURCE files (the two counted classes, pinned by named tests); excluded-test and
|
|
220
|
-
// out-of-domain never count; pure deletions already cost nothing (new-side lines only). Stated
|
|
221
|
-
// consequence, INTENTIONAL (agy R1): under --no-renames a rename of a large file costs its full
|
|
222
|
-
// length — the single-home invariant outweighs a rename special case.
|
|
223
|
-
export const countCapLines = ({ assessable, unsupportedLines }) => {
|
|
224
|
-
let counted = 0;
|
|
225
|
-
for (const lines of assessable.values()) counted += lines.length;
|
|
226
|
-
for (const lines of unsupportedLines.values()) counted += lines.length;
|
|
227
|
-
return counted;
|
|
228
|
-
};
|
|
229
|
-
|
|
230
207
|
// ── the shared fail-closed positive-integer knob parser (AD-047 precedent) ───────────────────────
|
|
231
208
|
|
|
232
209
|
// Zero / negative / fractional / non-numeric values are refusals by name — the parseInt(...)||default
|
|
233
|
-
// idiom would silently accept bad truthy values (
|
|
210
|
+
// idiom would silently accept bad truthy values (AD-046). Unset → the default; set → a
|
|
234
211
|
// positive integer, exactly. `makeError` lets each caller throw its OWN typed STOP.
|
|
235
212
|
export const parsePositiveIntKnob = (env, name, fallback, makeError = (m) => new Error(m)) => {
|
|
236
213
|
const raw = env[name];
|
|
@@ -241,8 +218,8 @@ export const parsePositiveIntKnob = (env, name, fallback, makeError = (m) => new
|
|
|
241
218
|
return Number.parseInt(raw, 10);
|
|
242
219
|
};
|
|
243
220
|
|
|
244
|
-
// ── the
|
|
245
|
-
//
|
|
221
|
+
// ── the probe-verdict algebra (AD-047; the SINGLE home — the core-evidence red-proof observer
|
|
222
|
+
// consumes it) ────────────────────────────────────────────────────────────────────────────────────
|
|
246
223
|
|
|
247
224
|
// probeVerdict(entry) → 'green' | 'red' | 'quarantine' | 'unresolvable'. RED/GREEN are strict N/N
|
|
248
225
|
// verdicts; any timeout, mixed outcome, or partial resolution is QUARANTINE — it never converts and
|
|
@@ -254,41 +231,3 @@ export const probeVerdict = (t) => {
|
|
|
254
231
|
if (t.timeouts > 0 || unresolved > 0 || (t.greens > 0 && t.reds > 0)) return 'quarantine';
|
|
255
232
|
return t.greens === t.runs ? 'green' : 'red';
|
|
256
233
|
};
|
|
257
|
-
|
|
258
|
-
// ── the fold-result ledger locator + a tolerant row reader (the D8 telemetry read path) ──────────
|
|
259
|
-
|
|
260
|
-
export const RESULTS_BASENAME = 'agent-workflow-fold-completeness.jsonl';
|
|
261
|
-
|
|
262
|
-
// The result-ledger path: AW_FOLD_RESULTS overrides (mirrors AW_REVIEW_LEDGER); else <git dir>/basename.
|
|
263
|
-
export const resolveResultsPath = (cwd, env = process.env) => {
|
|
264
|
-
if (env.AW_FOLD_RESULTS) return env.AW_FOLD_RESULTS;
|
|
265
|
-
const gitDir = gitLine(['rev-parse', '--absolute-git-dir'], cwd);
|
|
266
|
-
return gitDir == null ? null : join(gitDir, RESULTS_BASENAME);
|
|
267
|
-
};
|
|
268
|
-
|
|
269
|
-
// readJsonlRows(path) → { rows, badLines, readError? } — the TOLERANT parse the telemetry counts
|
|
270
|
-
// ride on (counts only, no judgment — D8): a row is any parsed JSON object; a bad line is counted,
|
|
271
|
-
// never dropped silently. Schema validation stays with each ledger's own reader — telemetry must
|
|
272
|
-
// not fork a second validator (drift) nor import one across the cycle boundary.
|
|
273
|
-
export const readJsonlRows = (path, readFile = readFileSync) => {
|
|
274
|
-
let raw;
|
|
275
|
-
try {
|
|
276
|
-
raw = readFile(path, 'utf8');
|
|
277
|
-
} catch (err) {
|
|
278
|
-
if (err && err.code === 'ENOENT') return { rows: [], badLines: 0 };
|
|
279
|
-
return { rows: [], badLines: 0, readError: (err && err.code) || (err && err.message) || 'read failed' };
|
|
280
|
-
}
|
|
281
|
-
const rows = [];
|
|
282
|
-
let badLines = 0;
|
|
283
|
-
for (const line of raw.split('\n')) {
|
|
284
|
-
if (line.trim() === '') continue;
|
|
285
|
-
try {
|
|
286
|
-
const parsed = JSON.parse(line);
|
|
287
|
-
if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)) rows.push(parsed);
|
|
288
|
-
else badLines += 1;
|
|
289
|
-
} catch {
|
|
290
|
-
badLines += 1;
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
return { rows, badLines };
|
|
294
|
-
};
|
package/tools/cheap-agents.mjs
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
//
|
|
24
24
|
// Exit codes: 0 done / dry-run (incl. preserved customizations — a user's file is a legitimate
|
|
25
25
|
// state, not an error); 1 precondition STOP (stamp, symlink, missing bundle); 2 usage.
|
|
26
|
-
// Dependency-free, Node >=
|
|
26
|
+
// Dependency-free, Node >= 22. No side effects on import.
|
|
27
27
|
|
|
28
28
|
import { existsSync, lstatSync, mkdirSync, readFileSync, writeFileSync, readdirSync } from 'node:fs';
|
|
29
29
|
import { join, resolve, dirname } from 'node:path';
|
|
@@ -34,7 +34,7 @@ const HERE = dirname(fileURLToPath(import.meta.url));
|
|
|
34
34
|
export const AGENTS_DIR = '.claude/agents';
|
|
35
35
|
export const CLAUDE_DIR = '.claude';
|
|
36
36
|
export const WORKFLOW_STAMP = 'docs/ai/.workflow-version';
|
|
37
|
-
export const EXPECTED_WORKFLOW_VERSION = '
|
|
37
|
+
export const EXPECTED_WORKFLOW_VERSION = '3.0.0';
|
|
38
38
|
export const BUNDLED_AGENTS_DIR = resolve(HERE, '..', 'references', 'agents');
|
|
39
39
|
|
|
40
40
|
const EXIT_OK = 0;
|