@sabaiway/agent-workflow-kit 2.1.0 → 3.0.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.
Files changed (99) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/README.md +6 -5
  3. package/SKILL.md +12 -8
  4. package/bin/install.mjs +1 -1
  5. package/bridges/antigravity-cli-bridge/SKILL.md +8 -1
  6. package/bridges/antigravity-cli-bridge/bin/agy-review-honesty.test.mjs +150 -0
  7. package/bridges/antigravity-cli-bridge/bin/agy-review-model-screen.test.mjs +76 -0
  8. package/bridges/antigravity-cli-bridge/bin/agy-review.sh +80 -15
  9. package/bridges/antigravity-cli-bridge/bin/agy-review.test.mjs +95 -4
  10. package/bridges/antigravity-cli-bridge/capability.json +3 -2
  11. package/bridges/codex-cli-bridge/SKILL.md +8 -1
  12. package/bridges/codex-cli-bridge/bin/codex-review-honesty.test.mjs +143 -0
  13. package/bridges/codex-cli-bridge/bin/codex-review.sh +71 -10
  14. package/bridges/codex-cli-bridge/bin/codex-review.test.mjs +93 -10
  15. package/bridges/codex-cli-bridge/capability.json +3 -2
  16. package/capability.json +1 -1
  17. package/migrations/3.0.0-hardened-core-loop.md +44 -0
  18. package/migrations/README.md +1 -1
  19. package/package.json +2 -2
  20. package/references/hooks/gate-approve.mjs +1 -1
  21. package/references/modes/bootstrap.md +3 -3
  22. package/references/modes/commit-guard.md +19 -0
  23. package/references/modes/core-evidence.md +15 -0
  24. package/references/modes/coverage-check.md +12 -0
  25. package/references/modes/doc-parity.md +7 -6
  26. package/references/modes/gates.md +7 -6
  27. package/references/modes/grounding.md +1 -2
  28. package/references/modes/hook.md +1 -1
  29. package/references/modes/review-state.md +3 -3
  30. package/references/modes/upgrade.md +6 -6
  31. package/references/modes/velocity.md +2 -2
  32. package/references/scripts/archive-decisions.mjs +1 -1
  33. package/references/scripts/install-git-hooks-repo-exec.test.mjs +82 -0
  34. package/references/scripts/install-git-hooks.mjs +90 -18
  35. package/references/scripts/install-git-hooks.test.mjs +102 -0
  36. package/references/scripts/migrate-gates-branches.test.mjs +157 -0
  37. package/references/scripts/migrate-gates.mjs +395 -0
  38. package/references/scripts/migrate-gates.test.mjs +284 -0
  39. package/references/templates/agent_rules.md +2 -2
  40. package/tools/ack-write.mjs +1 -1
  41. package/tools/atomic-write.mjs +2 -2
  42. package/tools/autonomy-config.mjs +1 -1
  43. package/tools/autonomy-doctor.mjs +1 -1
  44. package/tools/autonomy-write.mjs +1 -1
  45. package/tools/bridge-settings-read.mjs +1 -1
  46. package/tools/bridge-settings.mjs +1 -1
  47. package/tools/changed-surface.mjs +8 -69
  48. package/tools/cheap-agents.mjs +2 -2
  49. package/tools/commands.mjs +17 -10
  50. package/tools/commit-guard.mjs +167 -0
  51. package/tools/core-evidence.mjs +914 -0
  52. package/tools/coverage-check.mjs +260 -0
  53. package/tools/delegation.mjs +1 -1
  54. package/tools/detect-backends.mjs +3 -3
  55. package/tools/doc-parity.mjs +11 -27
  56. package/tools/engine-source.mjs +1 -1
  57. package/tools/family-members.mjs +1 -1
  58. package/tools/family-registry.mjs +1 -1
  59. package/tools/fs-safe.mjs +1 -1
  60. package/tools/gate-hook.mjs +1 -1
  61. package/tools/{seed-gates.mjs → gates-init.mjs} +58 -52
  62. package/tools/grounding.mjs +6 -83
  63. package/tools/hide-footprint.mjs +1 -1
  64. package/tools/inject-methodology.mjs +1 -1
  65. package/tools/known-footprint.mjs +1 -1
  66. package/tools/labels.mjs +1 -1
  67. package/tools/lcov.mjs +6 -10
  68. package/tools/lens-region.mjs +1 -1
  69. package/tools/manifest/validate.mjs +19 -0
  70. package/tools/migrate-adr-store.mjs +2 -2
  71. package/tools/orchestration-config.mjs +1 -1
  72. package/tools/orchestration-write.mjs +1 -1
  73. package/tools/presentation.mjs +1 -1
  74. package/tools/procedures.mjs +2 -2
  75. package/tools/recipes.mjs +60 -9
  76. package/tools/recommendations.mjs +63 -7
  77. package/tools/release-scan.mjs +1 -1
  78. package/tools/renderers.mjs +1 -1
  79. package/tools/review-state.mjs +288 -340
  80. package/tools/run-gates.mjs +216 -92
  81. package/tools/sandbox-masks.mjs +1 -1
  82. package/tools/semver-lite.mjs +1 -1
  83. package/tools/set-autonomy.mjs +1 -1
  84. package/tools/set-recipe.mjs +1 -1
  85. package/tools/setup-backends.mjs +1 -1
  86. package/tools/surface.mjs +1 -1
  87. package/tools/uninstall.mjs +1 -1
  88. package/tools/velocity-profile.mjs +2 -2
  89. package/tools/view-model.mjs +1 -1
  90. package/references/modes/fold-completeness.md +0 -30
  91. package/references/modes/review-ledger.md +0 -34
  92. package/references/templates/verification-profile.json +0 -10
  93. package/tools/fold-completeness-run.mjs +0 -1120
  94. package/tools/fold-completeness.mjs +0 -672
  95. package/tools/review-ledger-core.mjs +0 -428
  96. package/tools/review-ledger-write.mjs +0 -647
  97. package/tools/review-ledger.mjs +0 -630
  98. package/tools/sarif.mjs +0 -52
  99. package/tools/verification-profile.mjs +0 -219
package/tools/sarif.mjs DELETED
@@ -1,52 +0,0 @@
1
- #!/usr/bin/env node
2
- // sarif.mjs — a dependency-free SARIF (2.1.0) reader for the OPTIONAL advisory findings surface
3
- // (BUGFREE-3, AD-049, step 1.4 — the reviewdog pattern). Findings are ADVISORY ONLY: they are NEVER
4
- // recorded on a fold run record (SARIF stays entirely out of the fold result schema — no v4 quartet
5
- // coupling, no sequencing dependency) and NEVER gate-blocking (fold-completeness --check never reads
6
- // SARIF). A malformed SARIF fails the advisory READ loudly (a nonzero exit on the advisory verb), but
7
- // the fold gate — fold-completeness --check — is unaffected. No side effects on import; Node >= 18.
8
-
9
- export const SARIF_STOP = 'SARIF_STOP';
10
- const stop = (message) => Object.assign(new Error(`[agent-workflow-kit] ${message}`), { name: 'SarifStop', code: SARIF_STOP });
11
-
12
- // parseSarif(text) → { findings: [{ ruleId, level, message, file, line }] }. THROWS on a malformed
13
- // SARIF (not JSON, or no runs[] array) — the advisory read is LOUD, never a silent empty result. A
14
- // well-formed run with zero results yields an empty findings list (a clean advisory).
15
- export const parseSarif = (text) => {
16
- let doc;
17
- try {
18
- doc = JSON.parse(text);
19
- } catch (err) {
20
- throw stop(`SARIF is not valid JSON (${err.message})`);
21
- }
22
- if (!doc || typeof doc !== 'object' || !Array.isArray(doc.runs)) {
23
- throw stop('SARIF has no runs[] array — not a SARIF 2.1.0 document');
24
- }
25
- const findings = [];
26
- for (const run of doc.runs) {
27
- const results = Array.isArray(run?.results) ? run.results : [];
28
- for (const r of results) {
29
- const loc = r?.locations?.[0]?.physicalLocation ?? {};
30
- findings.push({
31
- ruleId: typeof r?.ruleId === 'string' ? r.ruleId : '(no rule)',
32
- level: typeof r?.level === 'string' ? r.level : 'warning',
33
- message: typeof r?.message?.text === 'string' ? r.message.text : '',
34
- file: typeof loc?.artifactLocation?.uri === 'string' ? loc.artifactLocation.uri : null,
35
- line: Number.isInteger(loc?.region?.startLine) ? loc.region.startLine : null,
36
- });
37
- }
38
- }
39
- return { findings };
40
- };
41
-
42
- // renderSarifFindings(findings) → a human advisory block (one line per finding). Empty → a stated
43
- // "no findings" note. Always advisory — the caller never blocks a gate on this.
44
- export const renderSarifFindings = (findings) => {
45
- if (findings.length === 0) return 'SARIF advisory: no findings (advisory only — never gate-blocking).';
46
- const lines = [`SARIF advisory: ${findings.length} finding(s) (advisory only — never gate-blocking):`];
47
- for (const f of findings) {
48
- const at = f.file ? `${f.file}${f.line != null ? `:${f.line}` : ''}` : '(no location)';
49
- lines.push(` [${f.level}] ${f.ruleId} — ${at}${f.message ? ` — ${f.message}` : ''}`);
50
- }
51
- return lines.join('\n');
52
- };
@@ -1,219 +0,0 @@
1
- #!/usr/bin/env node
2
- // verification-profile.mjs — read-only schema/read core for the per-project VERIFICATION PROFILE
3
- // (docs/ai/verification-profile.json), the language-independence contract (BUGFREE-3, AD-049).
4
- // Companion read-core to the fold-completeness runner. NO writer — the runner is the sole
5
- // tree-toucher. An ABSENT profile reproduces today's exact behaviour (V8 coverage + node:test
6
- // TAP-on-stdout); the profile only generalizes the coverage SOURCE, the single-test RESULT FORMAT,
7
- // and an optional SARIF path — coverage/probe INPUTS, never the suite command.
8
- //
9
- // The suite COMMAND is deliberately NOT a profile field: it stays the docs/ai/gates.json unit-tests
10
- // cmd so the fold run and the unit-tests gate share command-identity (the (a) suite-evidence credit
11
- // requires the SAME command).
12
- //
13
- // Path safety (Decision 4; grounding.mjs assertScratchDestination model): every profile-declared
14
- // artifact path MUST be gitignored or outside the work tree, checked on the REALPATH (a symlink leaf
15
- // is refused). An in-tree, not-ignored file the suite writes would move the review fingerprint the
16
- // run binds to; a symlinked path could route the write onto a tracked file. validateProfile fails
17
- // CLOSED on any such path.
18
- //
19
- // Dependency-free, Node >= 18. No side effects on import.
20
-
21
- import { readFileSync, lstatSync, realpathSync } from 'node:fs';
22
- import { join, resolve, relative, isAbsolute, dirname, basename } from 'node:path';
23
- import { spawnSync } from 'node:child_process';
24
- import { fail } from './orchestration-config.mjs';
25
-
26
- // cwd-relative so error prefixes show a path the user can open, never an absolute temp/host path.
27
- export const PROFILE_REL = 'docs/ai/verification-profile.json';
28
- export const PROFILE_SCHEMA_VERSION = 1;
29
-
30
- export const COVERAGE_KINDS = new Set(['v8', 'lcov']);
31
- export const RESULT_FORMATS = new Set(['tap-stdout', 'tap-file', 'junit-xml']);
32
- // File-based formats must be TOLD where to write, so their argv carries {resultPath} (the runner
33
- // substitutes a fresh out-of-tree path per probe and reads THAT back). tap-stdout reads stdout.
34
- export const FILE_BASED_FORMATS = new Set(['tap-file', 'junit-xml']);
35
- export const RESULT_PATH_TOKEN = '{resultPath}';
36
- export const FILE_TOKEN = '{file}';
37
- export const PATTERN_TOKEN = '{pattern}';
38
-
39
- const isPlainObject = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
40
- const isNonEmptyString = (v) => typeof v === 'string' && v.length > 0;
41
- const isNonEmptyStringArray = (v) => Array.isArray(v) && v.length > 0 && v.every((x) => typeof x === 'string');
42
-
43
- // Unknown-key-fails-closed. Returns a reason string or null.
44
- const unknownKeyReason = (obj, allowed, where) => {
45
- for (const k of Object.keys(obj)) {
46
- if (!allowed.has(k)) return `${where}: unknown key "${k}" (allowed: ${[...allowed].join(', ')})`;
47
- }
48
- return null;
49
- };
50
-
51
- const COVERAGE_KEYS = new Set(['kind', 'lcovPath']);
52
- const SINGLE_TEST_KEYS = new Set(['argv', 'resultFormat']);
53
- const FINDINGS_KEYS = new Set(['sarifPath']);
54
- const TOP_KEYS = new Set(['_README', 'schema', 'coverage', 'singleTest', 'findings']);
55
-
56
- const defaultGitLine = (args, cwd) => {
57
- const r = spawnSync('git', args, { cwd, encoding: 'utf8', windowsHide: true });
58
- return r.error || r.status == null ? null : { status: r.status, stdout: r.stdout ?? '' };
59
- };
60
-
61
- // Reason string when the declared path is unsafe, else null. Unsafe = a symlink leaf, or an
62
- // in-work-tree path that is NOT gitignored (writing there moves the review fingerprint; a tracked
63
- // path reads as not-ignored too). Checked on the REAL destination (parent realpath-resolved) so a
64
- // symlinked parent cannot route an "outside/ignored" path back onto a tracked file. Outside any git
65
- // tree → safe. deps.{gitLine,lstat,realpath} are injectable for hermetic tests.
66
- export const declaredPathUnsafeReason = (label, p, cwd, deps = {}) => {
67
- const gitLine = deps.gitLine ?? defaultGitLine;
68
- const lstat = deps.lstat ?? lstatSync;
69
- const realpath = deps.realpath ?? realpathSync;
70
- if (!isNonEmptyString(p)) return `${label} must be a non-empty string`;
71
- const lexical = isAbsolute(p) ? p : resolve(cwd, p);
72
- let leaf = null;
73
- try {
74
- leaf = lstat(lexical);
75
- } catch {
76
- leaf = null; // absent → a fresh file; the parent is realpath-checked below
77
- }
78
- if (leaf && leaf.isSymbolicLink()) {
79
- return `${label} "${p}" is a symlink — refused (the write would follow it onto another file; name the real path)`;
80
- }
81
- let realParent;
82
- try {
83
- realParent = realpath(dirname(lexical));
84
- } catch {
85
- return `${label} "${p}" parent directory does not exist — create the (gitignored) output dir first, or fix the path`;
86
- }
87
- const full = join(realParent, basename(lexical));
88
- const top = gitLine(['rev-parse', '--show-toplevel'], cwd);
89
- if (top == null || top.status !== 0) return null; // not a git work tree → not fingerprint-relevant
90
- // Realpath the repo root too so both sides of relative() are physical — a checkout opened via a
91
- // symlink must not misclassify an in-tree path as OUTSIDE. Fail CLOSED if it can't resolve.
92
- let root;
93
- try {
94
- root = realpath(top.stdout.replace(/\r?\n$/, ''));
95
- } catch {
96
- return `${label} "${p}" — cannot resolve the repo root's real path (fail closed)`;
97
- }
98
- const rel = relative(root, full);
99
- if (rel.startsWith('..') || isAbsolute(rel)) return null; // outside the work tree → safe
100
- const ignored = gitLine(['check-ignore', '-q', '--', rel], root);
101
- if (ignored == null || ignored.status !== 0) {
102
- return `${label} "${p}" is an in-tree path that is not gitignored — the suite writing there would move the review fingerprint the run binds to; gitignore it (or place it outside the repo)`;
103
- }
104
- return null;
105
- };
106
-
107
- // validateProfile(obj, { cwd, gitLine, lstat, realpath }?) → { ok, reason }. Strict schema first;
108
- // then, ONLY when `cwd` is supplied, the declared-path safety check (Decision 4). A pure-schema unit
109
- // test omits `cwd`; loadProfile passes it so malformed OR unsafe both fail closed.
110
- export const validateProfile = (obj, ctx = {}) => {
111
- if (!isPlainObject(obj)) return { ok: false, reason: `${PROFILE_REL}: must be a JSON object` };
112
- const topUnknown = unknownKeyReason(obj, TOP_KEYS, PROFILE_REL);
113
- if (topUnknown) return { ok: false, reason: topUnknown };
114
- if (obj.schema !== PROFILE_SCHEMA_VERSION) return { ok: false, reason: `${PROFILE_REL}: schema must be ${PROFILE_SCHEMA_VERSION}` };
115
- if (obj._README !== undefined && typeof obj._README !== 'string') return { ok: false, reason: `${PROFILE_REL}: "_README" must be a string` };
116
-
117
- // coverage (optional; absent → V8). lcovPath REQUIRED iff lcov, forbidden otherwise.
118
- if (obj.coverage !== undefined) {
119
- if (!isPlainObject(obj.coverage)) return { ok: false, reason: `${PROFILE_REL}: coverage must be an object` };
120
- const ck = unknownKeyReason(obj.coverage, COVERAGE_KEYS, `${PROFILE_REL}: coverage`);
121
- if (ck) return { ok: false, reason: ck };
122
- if (!COVERAGE_KINDS.has(obj.coverage.kind)) return { ok: false, reason: `${PROFILE_REL}: coverage.kind must be one of ${[...COVERAGE_KINDS].join(', ')}` };
123
- if (obj.coverage.kind === 'lcov') {
124
- if (!isNonEmptyString(obj.coverage.lcovPath)) return { ok: false, reason: `${PROFILE_REL}: coverage.lcovPath (a non-empty path) is required when coverage.kind is "lcov"` };
125
- } else if (obj.coverage.lcovPath !== undefined) {
126
- return { ok: false, reason: `${PROFILE_REL}: coverage.lcovPath is only valid when coverage.kind is "lcov"` };
127
- }
128
- }
129
-
130
- // singleTest (optional; absent → default node:test argv + tap-stdout). argv must carry {file} +
131
- // {pattern}; a file-based resultFormat additionally requires {resultPath}.
132
- if (obj.singleTest !== undefined) {
133
- if (!isPlainObject(obj.singleTest)) return { ok: false, reason: `${PROFILE_REL}: singleTest must be an object` };
134
- const sk = unknownKeyReason(obj.singleTest, SINGLE_TEST_KEYS, `${PROFILE_REL}: singleTest`);
135
- if (sk) return { ok: false, reason: sk };
136
- if (!isNonEmptyStringArray(obj.singleTest.argv)) return { ok: false, reason: `${PROFILE_REL}: singleTest.argv must be a non-empty array of strings` };
137
- const argvJoined = obj.singleTest.argv.join(' ');
138
- if (!argvJoined.includes(FILE_TOKEN)) return { ok: false, reason: `${PROFILE_REL}: singleTest.argv must carry a ${FILE_TOKEN} placeholder (the runner substitutes the test file)` };
139
- if (!argvJoined.includes(PATTERN_TOKEN)) return { ok: false, reason: `${PROFILE_REL}: singleTest.argv must carry a ${PATTERN_TOKEN} placeholder (the runner substitutes the test-name pattern)` };
140
- const rf = obj.singleTest.resultFormat;
141
- if (rf !== undefined && !RESULT_FORMATS.has(rf)) return { ok: false, reason: `${PROFILE_REL}: singleTest.resultFormat must be one of ${[...RESULT_FORMATS].join(', ')}` };
142
- if (FILE_BASED_FORMATS.has(rf) && !argvJoined.includes(RESULT_PATH_TOKEN)) {
143
- return { ok: false, reason: `${PROFILE_REL}: singleTest.resultFormat "${rf}" is file-based — singleTest.argv must carry a ${RESULT_PATH_TOKEN} placeholder (the runner substitutes a fresh out-of-tree result path per probe)` };
144
- }
145
- }
146
-
147
- // findings (optional; an empty object is valid — no SARIF path declared).
148
- if (obj.findings !== undefined) {
149
- if (!isPlainObject(obj.findings)) return { ok: false, reason: `${PROFILE_REL}: findings must be an object` };
150
- const fk = unknownKeyReason(obj.findings, FINDINGS_KEYS, `${PROFILE_REL}: findings`);
151
- if (fk) return { ok: false, reason: fk };
152
- if (obj.findings.sarifPath !== undefined && !isNonEmptyString(obj.findings.sarifPath)) {
153
- return { ok: false, reason: `${PROFILE_REL}: findings.sarifPath must be a non-empty string when present` };
154
- }
155
- }
156
-
157
- // Declared-path safety — only when a cwd is supplied (Decision 4).
158
- if (ctx.cwd != null) {
159
- const deps = { gitLine: ctx.gitLine, lstat: ctx.lstat, realpath: ctx.realpath };
160
- if (obj.coverage?.kind === 'lcov') {
161
- const r = declaredPathUnsafeReason('coverage.lcovPath', obj.coverage.lcovPath, ctx.cwd, deps);
162
- if (r) return { ok: false, reason: `${PROFILE_REL}: ${r}` };
163
- }
164
- if (obj.findings?.sarifPath !== undefined) {
165
- const r = declaredPathUnsafeReason('findings.sarifPath', obj.findings.sarifPath, ctx.cwd, deps);
166
- if (r) return { ok: false, reason: `${PROFILE_REL}: ${r}` };
167
- }
168
- }
169
- return { ok: true };
170
- };
171
-
172
- // resolvers — env WINS over the profile (ad-hoc override precedence, Decision 3)
173
-
174
- export const resolveCoverage = (profile) => {
175
- const kind = profile?.coverage?.kind ?? 'v8';
176
- return { kind, lcovPath: kind === 'lcov' ? profile.coverage.lcovPath : null };
177
- };
178
-
179
- // argv precedence: AW_FOLD_BOUND_CMD (applied in the runner's resolveBoundArgv, keeping the
180
- // malformed-override refusal one home) > profile.singleTest.argv > built-in default. This resolver
181
- // returns the PROFILE view only: argv = the profile template or null → runner default.
182
- export const resolveSingleTest = (profile) => ({
183
- argv: profile?.singleTest?.argv ?? null,
184
- resultFormat: profile?.singleTest?.resultFormat ?? 'tap-stdout',
185
- });
186
-
187
- export const resolveSarifPath = (profile) => profile?.findings?.sarifPath ?? null;
188
-
189
- // IO — config errors → loud fail(1); an absent FILE → defaults path, NOT an error
190
-
191
- // loadProfile(cwd, deps?) → { profile, source }. Absent FILE → { profile: null, source: 'none' }.
192
- // A directory / dangling symlink / permission error is PRESENT-but-unreadable → loud fail(1), never
193
- // silently treated as absent. Malformed JSON, schema-invalid, or an unsafe declared path → fail(1).
194
- export const loadProfile = (cwd, deps = {}) => {
195
- const readFile = deps.readFile ?? readFileSync;
196
- const lstat = deps.lstat ?? lstatSync;
197
- const full = join(cwd, PROFILE_REL);
198
- try {
199
- lstat(full);
200
- } catch (err) {
201
- if (err && err.code === 'ENOENT') return { profile: null, source: 'none' };
202
- throw fail(1, `${PROFILE_REL}: unreadable (${(err && err.code) || (err && err.message) || err})`);
203
- }
204
- let raw;
205
- try {
206
- raw = readFile(full, 'utf8');
207
- } catch (err) {
208
- throw fail(1, `${PROFILE_REL}: unreadable (${(err && err.code) || (err && err.message) || err})`);
209
- }
210
- let parsed;
211
- try {
212
- parsed = JSON.parse(raw);
213
- } catch (err) {
214
- throw fail(1, `${PROFILE_REL}: malformed JSON (${err.message})`);
215
- }
216
- const v = validateProfile(parsed, { cwd, gitLine: deps.gitLine, lstat: deps.lstat, realpath: deps.realpath });
217
- if (!v.ok) throw fail(1, v.reason);
218
- return { profile: parsed, source: PROFILE_REL };
219
- };