@hellraisercenobit/ai-engineering-gate 0.8.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 (100) hide show
  1. package/README.md +82 -0
  2. package/agents/design-pattern-reviewer.md +40 -0
  3. package/agents/modern-typescript-reviewer.md +24 -0
  4. package/agents/testing-pattern-reviewer.md +23 -0
  5. package/bin/ai-engineering-gate.mjs +6 -0
  6. package/contracts/axi/axi-standard.md +273 -0
  7. package/contracts/axi/pin.json +36 -0
  8. package/contracts/members.json +58 -0
  9. package/contracts/schemas/arbitration.schema.json +45 -0
  10. package/contracts/schemas/decision-envelope.schema.json +215 -0
  11. package/contracts/schemas/declaration.schema.json +190 -0
  12. package/contracts/schemas/dispute.schema.json +67 -0
  13. package/contracts/schemas/evidence-append.schema.json +72 -0
  14. package/contracts/schemas/gate-output.schema.json +296 -0
  15. package/contracts/schemas/marker.schema.json +75 -0
  16. package/contracts/schemas/review-envelope.schema.json +378 -0
  17. package/contracts/schemas/verdict-record.schema.json +191 -0
  18. package/contracts/suite-contract.md +270 -0
  19. package/dist/ai-engineering-gate.mjs +2762 -0
  20. package/package.json +47 -0
  21. package/skills/engineering/review-design-patterns/LICENSE +6 -0
  22. package/skills/engineering/review-design-patterns/SKILL.md +196 -0
  23. package/skills/engineering/review-design-patterns/agents/openai.yaml +3 -0
  24. package/skills/engineering/review-design-patterns/references/smell-signatures.md +117 -0
  25. package/skills/engineering/review-design-patterns/review-design-patterns.yaml +25 -0
  26. package/skills/engineering/review-modern-typescript/LICENSE +21 -0
  27. package/skills/engineering/review-modern-typescript/SKILL.md +88 -0
  28. package/skills/engineering/review-modern-typescript/agents/openai.yaml +3 -0
  29. package/skills/engineering/review-modern-typescript/references/smell-signatures.md +36 -0
  30. package/skills/engineering/review-modern-typescript/review-modern-typescript.yaml +22 -0
  31. package/skills/engineering/review-testing-patterns/SKILL.md +108 -0
  32. package/skills/engineering/review-testing-patterns/agents/openai.yaml +4 -0
  33. package/skills/engineering/review-testing-patterns/references/compiler-profile.mjs +103 -0
  34. package/skills/engineering/review-testing-patterns/references/smell-signatures.md +30 -0
  35. package/skills/engineering/review-testing-patterns/review-testing-patterns.yaml +23 -0
  36. package/skills/engineering/transpose-design-patterns/LICENSE +6 -0
  37. package/skills/engineering/transpose-design-patterns/SKILL.md +231 -0
  38. package/skills/engineering/transpose-design-patterns/agents/openai.yaml +3 -0
  39. package/skills/engineering/transpose-design-patterns/references/arbitration.schema.json +45 -0
  40. package/skills/engineering/transpose-design-patterns/references/decision-envelope.schema.json +215 -0
  41. package/skills/engineering/transpose-design-patterns/references/declaration.schema.json +190 -0
  42. package/skills/engineering/transpose-design-patterns/references/design-decision-record.schema.json +731 -0
  43. package/skills/engineering/transpose-design-patterns/references/dispute.schema.json +67 -0
  44. package/skills/engineering/transpose-design-patterns/references/evidence-append.schema.json +72 -0
  45. package/skills/engineering/transpose-design-patterns/references/marker.schema.json +75 -0
  46. package/skills/engineering/transpose-design-patterns/references/pattern-catalog.md +305 -0
  47. package/skills/engineering/transpose-design-patterns/references/record.example.json +120 -0
  48. package/skills/engineering/transpose-design-patterns/references/review-envelope.schema.json +378 -0
  49. package/skills/engineering/transpose-design-patterns/references/suite-contract.md +270 -0
  50. package/skills/engineering/transpose-design-patterns/references/transpose-angular.md +224 -0
  51. package/skills/engineering/transpose-design-patterns/references/transpose-php.md +561 -0
  52. package/skills/engineering/transpose-design-patterns/references/transpose-quarkus.md +283 -0
  53. package/skills/engineering/transpose-design-patterns/references/transpose-react.md +256 -0
  54. package/skills/engineering/transpose-design-patterns/references/transpose-vanilla.md +204 -0
  55. package/skills/engineering/transpose-design-patterns/references/transpose-vue.md +234 -0
  56. package/skills/engineering/transpose-design-patterns/transpose-design-patterns.yaml +24 -0
  57. package/skills/engineering/transpose-modern-typescript/LICENSE +21 -0
  58. package/skills/engineering/transpose-modern-typescript/SKILL.md +90 -0
  59. package/skills/engineering/transpose-modern-typescript/agents/openai.yaml +3 -0
  60. package/skills/engineering/transpose-modern-typescript/references/arbitration.schema.json +45 -0
  61. package/skills/engineering/transpose-modern-typescript/references/catalog.md +38 -0
  62. package/skills/engineering/transpose-modern-typescript/references/collections.md +50 -0
  63. package/skills/engineering/transpose-modern-typescript/references/compatibility.md +61 -0
  64. package/skills/engineering/transpose-modern-typescript/references/decision-envelope.schema.json +215 -0
  65. package/skills/engineering/transpose-modern-typescript/references/decision-record.schema.json +830 -0
  66. package/skills/engineering/transpose-modern-typescript/references/declaration.schema.json +190 -0
  67. package/skills/engineering/transpose-modern-typescript/references/dispute.schema.json +67 -0
  68. package/skills/engineering/transpose-modern-typescript/references/evidence-append.schema.json +72 -0
  69. package/skills/engineering/transpose-modern-typescript/references/idioms.md +74 -0
  70. package/skills/engineering/transpose-modern-typescript/references/marker.schema.json +75 -0
  71. package/skills/engineering/transpose-modern-typescript/references/platform.md +147 -0
  72. package/skills/engineering/transpose-modern-typescript/references/record.example.json +172 -0
  73. package/skills/engineering/transpose-modern-typescript/references/record.md +37 -0
  74. package/skills/engineering/transpose-modern-typescript/references/review-envelope.schema.json +378 -0
  75. package/skills/engineering/transpose-modern-typescript/references/suite-contract.md +270 -0
  76. package/skills/engineering/transpose-modern-typescript/references/types.md +97 -0
  77. package/skills/engineering/transpose-modern-typescript/transpose-modern-typescript.yaml +22 -0
  78. package/skills/engineering/transpose-testing-patterns/SKILL.md +96 -0
  79. package/skills/engineering/transpose-testing-patterns/agents/openai.yaml +4 -0
  80. package/skills/engineering/transpose-testing-patterns/references/arbitration.schema.json +45 -0
  81. package/skills/engineering/transpose-testing-patterns/references/catalog.md +218 -0
  82. package/skills/engineering/transpose-testing-patterns/references/decision-envelope.schema.json +215 -0
  83. package/skills/engineering/transpose-testing-patterns/references/decision-record.schema.json +857 -0
  84. package/skills/engineering/transpose-testing-patterns/references/declaration.schema.json +190 -0
  85. package/skills/engineering/transpose-testing-patterns/references/detect-adapter.mjs +98 -0
  86. package/skills/engineering/transpose-testing-patterns/references/dispute.schema.json +67 -0
  87. package/skills/engineering/transpose-testing-patterns/references/doubles-data.md +45 -0
  88. package/skills/engineering/transpose-testing-patterns/references/evidence-append.schema.json +72 -0
  89. package/skills/engineering/transpose-testing-patterns/references/journal-event.schema.json +115 -0
  90. package/skills/engineering/transpose-testing-patterns/references/marker.schema.json +75 -0
  91. package/skills/engineering/transpose-testing-patterns/references/record.example.json +230 -0
  92. package/skills/engineering/transpose-testing-patterns/references/record.md +25 -0
  93. package/skills/engineering/transpose-testing-patterns/references/review-envelope.schema.json +378 -0
  94. package/skills/engineering/transpose-testing-patterns/references/suite-contract.md +270 -0
  95. package/skills/engineering/transpose-testing-patterns/references/tdd.md +62 -0
  96. package/skills/engineering/transpose-testing-patterns/references/transpose-codeception.md +84 -0
  97. package/skills/engineering/transpose-testing-patterns/references/transpose-karma-jasmine-angular.md +73 -0
  98. package/skills/engineering/transpose-testing-patterns/references/transpose-vitest.md +111 -0
  99. package/skills/engineering/transpose-testing-patterns/references/typescript.md +42 -0
  100. package/skills/engineering/transpose-testing-patterns/transpose-testing-patterns.yaml +23 -0
@@ -0,0 +1,2762 @@
1
+ // Generated by scripts/build-gate-bundle.mjs from packages/ai-engineering-gate/src. Do not edit.
2
+
3
+ import { spawnSync } from 'node:child_process';
4
+ import { createHash } from 'node:crypto';
5
+ import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, symlinkSync, writeFileSync } from 'node:fs';
6
+ import { homedir, tmpdir } from 'node:os';
7
+ import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
8
+ import { fileURLToPath } from 'node:url';
9
+
10
+ // packages/ai-engineering-gate/src/answer.mjs
11
+ // Every command answers in one of two shapes, which the renderer and the exit code both read: a refusal
12
+ // carrying the contract's code, or an acceptance carrying the line a human reads plus the payload the
13
+ // JSON surface publishes.
14
+ const refuse = (code, reason, details = [], cause = null) => ({
15
+ ok: false,
16
+ code,
17
+ reason,
18
+ details,
19
+ ...(cause ? { cause } : {}),
20
+ });
21
+
22
+ const accept = (text, payload = {}) => ({ ok: true, text, payload });
23
+
24
+ // packages/ai-engineering-gate/src/hash.mjs
25
+ // Content hashes only. A fingerprint never carries a revision, a timestamp or a path's mtime, so a
26
+ // content-neutral rebase and a re-checkout leave every verdict current.
27
+
28
+ function hashText(text) {
29
+ return `sha256:${createHash('sha256').update(text).digest('hex')}`;
30
+ }
31
+
32
+ function hashFile(path) {
33
+ try {
34
+ if (!statSync(path).isFile()) return null;
35
+ return `sha256:${createHash('sha256').update(readFileSync(path)).digest('hex')}`;
36
+ } catch {
37
+ return null;
38
+ }
39
+ }
40
+
41
+ // Canonical JSON with sorted keys, so two equal documents hash equally whatever their key order.
42
+ function canonicalJson(value) {
43
+ if (value === null || typeof value !== 'object') return JSON.stringify(value) ?? 'null';
44
+ if (Array.isArray(value)) return `[${value.map(canonicalJson).join(',')}]`;
45
+ const entries = Object.keys(value).sort()
46
+ .filter(key => value[key] !== undefined)
47
+ .map(key => `${JSON.stringify(key)}:${canonicalJson(value[key])}`);
48
+ return `{${entries.join(',')}}`;
49
+ }
50
+
51
+ function hashJson(value) {
52
+ return hashText(canonicalJson(value));
53
+ }
54
+
55
+ // One fingerprint from a set of named content hashes. `null` marks a member that does not exist,
56
+ // so a deleted file changes the fingerprint instead of disappearing from it.
57
+ function hashEntries(entries) {
58
+ const ordered = [...entries].sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0));
59
+ return hashText(ordered.map(([name, digest]) => `${name}\u0000${digest ?? 'absent'}`).join('\u0001'));
60
+ }
61
+
62
+ // packages/ai-engineering-gate/src/repo.mjs
63
+ const MARKER_FILE = '.ai-engineering-suite.json';
64
+ const DEFAULT_EVIDENCE_ROOT = join(homedir(), '.ai-engineering-gate', 'evidence');
65
+
66
+ function git(args, cwd) {
67
+ const result = spawnSync('git', args, { cwd, encoding: 'utf8' });
68
+ return {
69
+ ok: result.status === 0,
70
+ stdout: (result.stdout ?? '').trim(),
71
+ stderr: (result.stderr ?? '').trim(),
72
+ };
73
+ }
74
+
75
+ function gitLines(args, cwd) {
76
+ const result = git(args, cwd);
77
+ if (!result.ok) return [];
78
+ return result.stdout.split('\n').map(line => line.trim()).filter(Boolean);
79
+ }
80
+
81
+ function findRepoRoot(cwd) {
82
+ const result = git(['rev-parse', '--show-toplevel'], cwd);
83
+ return result.ok ? result.stdout : cwd;
84
+ }
85
+
86
+ function readMarker(repoRoot) {
87
+ const path = join(repoRoot, MARKER_FILE);
88
+ if (!existsSync(path)) return null;
89
+ return JSON.parse(readFileSync(path, 'utf8'));
90
+ }
91
+
92
+ function expandHome(path) {
93
+ if (path === '~') return homedir();
94
+ if (path.startsWith('~/')) return join(homedir(), path.slice(2));
95
+ return path;
96
+ }
97
+
98
+ function resolveEvidenceRoot({ flag, env, marker, repoRoot }) {
99
+ const chosen = flag ?? env ?? marker?.evidenceRoot;
100
+ if (!chosen) return DEFAULT_EVIDENCE_ROOT;
101
+ const expanded = expandHome(chosen);
102
+ return isAbsolute(expanded) ? expanded : resolve(repoRoot, expanded);
103
+ }
104
+
105
+ // `feature/REF-15-something` names task REF-15, matching the delivery convention. A branch with no
106
+ // ticket key falls back to a slug of the whole branch name, so the gate never goes silent for want
107
+ // of a key, and an explicit flag or environment variable serves a detached pipeline head.
108
+ function resolveTaskKey({ flag, env, repoRoot }) {
109
+ if (flag) return flag;
110
+ if (env) return env;
111
+ const branch = git(['rev-parse', '--abbrev-ref', 'HEAD'], repoRoot).stdout;
112
+ if (!branch || branch === 'HEAD') return 'detached-head';
113
+ const ticket = /(?<![A-Z0-9])([A-Z][A-Z0-9]+-\d+)(?![0-9])/.exec(branch.toUpperCase());
114
+ if (ticket) return ticket[1];
115
+ return branch.replaceAll(/[^A-Za-z0-9]+/g, '-').replaceAll(/^-+|-+$/g, '').toLowerCase() || 'unnamed';
116
+ }
117
+
118
+ // `*` matches inside one segment, `**` across segments, and a bare directory name matches
119
+ // everything under it. Nothing else, so an ignore pattern cannot quietly widen.
120
+ function matchesPattern(path, pattern) {
121
+ const clean = pattern.replace(/^\.\//, '').replace(/\/$/, '');
122
+ if (path === clean || path.startsWith(`${clean}/`)) return true;
123
+ const expression = clean.split('**').map(part => part
124
+ .replaceAll(/[.+^${}()|[\]\\]/g, '\\$&')
125
+ .replaceAll('*', '[^/]*')
126
+ .replaceAll('?', '[^/]')).join('.*');
127
+ return new RegExp(`^${expression}$`, 'u').test(path);
128
+ }
129
+
130
+ function isInside(parent, child) {
131
+ const rel = relative(parent, child);
132
+ return rel === '' || (!rel.startsWith('..') && !isAbsolute(rel));
133
+ }
134
+
135
+ function toRepoRelative(repoRoot, path) {
136
+ const absolute = isAbsolute(path) ? path : resolve(repoRoot, path);
137
+ const rel = relative(repoRoot, absolute);
138
+ return rel.split(sep).join('/');
139
+ }
140
+
141
+ // packages/ai-engineering-gate/src/changeset.mjs
142
+ // A path the gate owns or publishes is never part of the state a verdict binds to: the export
143
+ // directory carries the verdicts themselves, an evidence root inside the checkout is the index, and
144
+ // the marker configures enforcement rather than describing the change.
145
+ function excludedFromChangeSet(context, path) {
146
+ return excluded(context, path);
147
+ }
148
+
149
+ function excluded(context, path) {
150
+ const { marker, repoRoot, evidenceRoot } = context;
151
+ if (path === MARKER_FILE) return true;
152
+ if (marker.exportDirectory && matchesPattern(path, marker.exportDirectory)) return true;
153
+ if (isInside(repoRoot, evidenceRoot) && matchesPattern(path, toRepoRelative(repoRoot, evidenceRoot))) return true;
154
+ return (marker.ignore ?? []).some(pattern => matchesPattern(path, pattern));
155
+ }
156
+
157
+ function mergeBase(repoRoot, base) {
158
+ if (!base) return null;
159
+ const found = git(['merge-base', base, 'HEAD'], repoRoot);
160
+ if (found.ok && found.stdout) return found.stdout;
161
+ const resolved = git(['rev-parse', '--verify', base], repoRoot);
162
+ return resolved.ok ? resolved.stdout : null;
163
+ }
164
+
165
+ // The tracked diff between the merge-base and the worktree, staged, unstaged and untracked files
166
+ // included. A clean HEAD never identifies a dirty worktree, so the untracked half is not optional.
167
+ function changeSet(context, base) {
168
+ const { repoRoot } = context;
169
+ const point = mergeBase(repoRoot, base);
170
+ const names = new Set();
171
+ if (point) {
172
+ for (const name of gitLines(['diff', '--name-only', point], repoRoot)) names.add(name);
173
+ for (const name of gitLines(['diff', '--name-only', '--cached', point], repoRoot)) names.add(name);
174
+ } else {
175
+ for (const name of gitLines(['diff', '--name-only', 'HEAD'], repoRoot)) names.add(name);
176
+ }
177
+ for (const name of gitLines(['ls-files', '--others', '--exclude-standard'], repoRoot)) names.add(name);
178
+ return [...names].filter(name => !excluded(context, name)).sort();
179
+ }
180
+
181
+ function filesUnder(context, repoRoot, relativePath, into) {
182
+ const absolute = join(repoRoot, relativePath);
183
+ if (!existsSync(absolute)) {
184
+ into.set(relativePath, null);
185
+ return;
186
+ }
187
+ if (statSync(absolute).isFile()) {
188
+ if (!excluded(context, relativePath)) into.set(relativePath, hashFile(absolute));
189
+ return;
190
+ }
191
+ for (const entry of readdirSync(absolute, { withFileTypes: true })) {
192
+ if (entry.name === '.git') continue;
193
+ filesUnder(context, repoRoot, `${relativePath.replace(/\/$/, '')}/${entry.name}`, into);
194
+ }
195
+ }
196
+
197
+ // The union of the declared scope paths, the declared configuration files and the change set. A
198
+ // scope entry that names a directory expands to its files, so a new file in scope moves the state.
199
+ function sourceFingerprint(context, declaration, change) {
200
+ const { repoRoot } = context;
201
+ const entries = new Map();
202
+ for (const path of declaration?.scope?.paths ?? []) filesUnder(context, repoRoot, path, entries);
203
+ for (const path of declaration?.scope?.configuration ?? []) filesUnder(context, repoRoot, path, entries);
204
+ for (const path of change) {
205
+ if (entries.has(path)) continue;
206
+ entries.set(path, hashFile(join(repoRoot, path)));
207
+ }
208
+ return hashEntries(entries);
209
+ }
210
+
211
+ // Change-set paths outside every applicable declaration. Never a refusal: the gate does not guess
212
+ // applicability, and the reviewer still sees what the builder did not declare.
213
+ function undeclaredChanges(change, declarations) {
214
+ const scopes = declarations
215
+ .filter(declaration => declaration?.applicability === 'applicable')
216
+ .flatMap(declaration => declaration.scope?.paths ?? []);
217
+ return change.filter(path => !scopes.some(scope => matchesPattern(path, scope)));
218
+ }
219
+
220
+ function coveringDimensions(path, declarationsByDimension) {
221
+ return Object.entries(declarationsByDimension)
222
+ .filter(([, declaration]) => declaration?.applicability === 'applicable'
223
+ && (declaration.scope?.paths ?? []).some(scope => matchesPattern(path, scope)))
224
+ .map(([dimension]) => dimension);
225
+ }
226
+
227
+ // packages/ai-engineering-gate/src/registry.mjs
228
+ const SHARED_SCHEMAS = [
229
+ 'marker', 'declaration', 'decision-envelope', 'review-envelope',
230
+ 'verdict-record', 'dispute', 'arbitration', 'evidence-append', 'gate-output',
231
+ ];
232
+
233
+ // The gate resolves its members and its reference bundles from its own distribution root - the
234
+ // plugin root, the npm package or this checkout - and never from the target project, so a
235
+ // reference fingerprint means the same thing wherever the gate runs. Walk to the outermost
236
+ // `contracts/members.json` so a staged copy inside the package never shadows this checkout.
237
+ function findDistributionRoot() {
238
+ const override = process.env.AI_ENGINEERING_GATE_ROOT;
239
+ if (override) return override;
240
+ let directory = dirname(fileURLToPath(import.meta.url));
241
+ let found = null;
242
+ for (let depth = 0; depth < 12; depth += 1) {
243
+ if (existsSync(join(directory, 'contracts/members.json'))) found = directory;
244
+ const parent = dirname(directory);
245
+ if (parent === directory) break;
246
+ directory = parent;
247
+ }
248
+ if (found) return found;
249
+ throw new Error('cannot resolve the gate distribution root: contracts/members.json not found');
250
+ }
251
+
252
+ function loadRegistry(distributionRoot) {
253
+ const manifest = JSON.parse(readFileSync(join(distributionRoot, 'contracts/members.json'), 'utf8'));
254
+ const schemas = {};
255
+ for (const name of SHARED_SCHEMAS) {
256
+ schemas[name] = JSON.parse(readFileSync(join(distributionRoot, `contracts/schemas/${name}.schema.json`), 'utf8'));
257
+ }
258
+ return { ...manifest, schemas, root: distributionRoot };
259
+ }
260
+
261
+ function gateVersion(distributionRoot) {
262
+ const candidates = [
263
+ join(distributionRoot, 'packages/ai-engineering-gate/package.json'),
264
+ join(distributionRoot, 'package.json'),
265
+ ];
266
+ for (const candidate of candidates) {
267
+ if (!existsSync(candidate)) continue;
268
+ const { version } = JSON.parse(readFileSync(candidate, 'utf8'));
269
+ if (version) return version;
270
+ }
271
+ return '0.0.0';
272
+ }
273
+
274
+ // The dimensions the project registered, resolved against the manifest. An identifier the manifest
275
+ // does not carry is an unresolvable registry member, not a dimension the gate invents.
276
+ function memberAgentType(member) {
277
+ return member.agent.split('/').pop().replace(/\.md$/, '');
278
+ }
279
+
280
+ function registeredMembers(registry, marker) {
281
+ const byName = new Map(registry.members.map(member => [member.dimension, member]));
282
+ const selected = marker.dimensions === 'all'
283
+ ? registry.members
284
+ : marker.dimensions.map(name => {
285
+ const member = byName.get(name);
286
+ if (!member) throw new Error(`the marker registers ${name}, which the member manifest does not`);
287
+ return member;
288
+ });
289
+ for (const member of selected) {
290
+ for (const path of [member.transpose, member.review, member.agent, member.decisionSchema]) {
291
+ if (!existsSync(join(registry.root, path))) {
292
+ throw new Error(`registry member ${member.dimension} does not resolve ${path}`);
293
+ }
294
+ }
295
+ }
296
+ return selected;
297
+ }
298
+
299
+ function memberDecisionSchema(registry, member) {
300
+ return JSON.parse(readFileSync(join(registry.root, member.decisionSchema), 'utf8'));
301
+ }
302
+
303
+ function memberEvidenceSchema(registry, member, kind) {
304
+ const path = member.evidenceSchemas?.[kind];
305
+ if (!path) return null;
306
+ return JSON.parse(readFileSync(join(registry.root, path), 'utf8'));
307
+ }
308
+
309
+ function walkFiles(root, into = []) {
310
+ if (!existsSync(root)) return into;
311
+ if (statSync(root).isFile()) {
312
+ into.push(root);
313
+ return into;
314
+ }
315
+ for (const entry of readdirSync(root, { withFileTypes: true }).sort((a, b) => (a.name < b.name ? -1 : 1))) {
316
+ const path = join(root, entry.name);
317
+ if (entry.isDirectory()) walkFiles(path, into);
318
+ else if (entry.isFile()) into.push(path);
319
+ }
320
+ return into;
321
+ }
322
+
323
+ // Catalog, decision schema, evidence schemas, guides, the review skill's references and the
324
+ // generated contract copy, plus the contract version and the gate version. A catalog edit
325
+ // therefore reopens every verdict that read it.
326
+ function referenceFingerprint(registry, member, version) {
327
+ const entries = [
328
+ ['contract-version', registry.contractVersion],
329
+ ['gate-version', version],
330
+ ];
331
+ for (const bundle of member.referenceBundle) {
332
+ const absolute = join(registry.root, bundle);
333
+ for (const file of walkFiles(absolute)) {
334
+ entries.push([file.slice(registry.root.length + 1), hashFile(file)]);
335
+ }
336
+ }
337
+ return hashEntries(entries);
338
+ }
339
+
340
+ // The bundle the gate distributes is authoritative. When a discoverable installed copy of the same
341
+ // skill differs from it, the state warns rather than guessing which one a reviewer read.
342
+ function installedReferenceMismatch(registry, member) {
343
+ const home = process.env.HOME;
344
+ if (!home) return false;
345
+ const name = member.transpose.split('/').pop();
346
+ for (const base of [join(home, '.claude/skills'), join(home, '.agents/skills')]) {
347
+ const installed = join(base, name, 'references');
348
+ if (!existsSync(installed)) continue;
349
+ const distributed = join(registry.root, member.transpose, 'references');
350
+ for (const file of walkFiles(distributed)) {
351
+ const mirror = join(installed, file.slice(distributed.length + 1));
352
+ if (hashFile(mirror) !== hashFile(file)) return true;
353
+ }
354
+ }
355
+ return false;
356
+ }
357
+
358
+ // packages/ai-engineering-gate/src/store.mjs
359
+ // Seven kinds of document live under a task's evidence directory. The first six are written through
360
+ // a gate command; `index` is the gate's own and nobody else writes it.
361
+ function taskPaths(evidenceRoot, task) {
362
+ const root = join(evidenceRoot, task);
363
+ return {
364
+ root,
365
+ declarations: join(root, 'declarations'),
366
+ records: join(root, 'records'),
367
+ evidence: join(root, 'evidence'),
368
+ disputes: join(root, 'disputes'),
369
+ arbitrations: join(root, 'arbitrations'),
370
+ index: join(root, 'index'),
371
+ };
372
+ }
373
+
374
+ function readJson(path) {
375
+ try {
376
+ return JSON.parse(readFileSync(path, 'utf8'));
377
+ } catch {
378
+ return null;
379
+ }
380
+ }
381
+
382
+ // Write then rename, one document at a time, so concurrent attestations of two dimensions never
383
+ // interleave and a crashed writer leaves no half document behind.
384
+ function writeJsonAtomic(path, value) {
385
+ mkdirSync(join(path, '..'), { recursive: true });
386
+ const temporary = `${path}.${process.pid}.tmp`;
387
+ writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`);
388
+ renameSync(temporary, path);
389
+ return path;
390
+ }
391
+
392
+ function listDirectories(path) {
393
+ if (!existsSync(path)) return [];
394
+ return readdirSync(path, { withFileTypes: true })
395
+ .filter(entry => entry.isDirectory())
396
+ .map(entry => entry.name)
397
+ .sort();
398
+ }
399
+
400
+ function listJsonFiles(path) {
401
+ if (!existsSync(path)) return [];
402
+ return readdirSync(path, { withFileTypes: true })
403
+ .filter(entry => entry.isFile() && entry.name.endsWith('.json') && !entry.name.endsWith('.snapshot.json'))
404
+ .map(entry => entry.name)
405
+ .sort();
406
+ }
407
+
408
+ const revisionNumber = name => Number.parseInt(/^rev-(\d+)\.json$/.exec(name)?.[1] ?? '0', 10);
409
+
410
+ function latestRevision(directory) {
411
+ const files = listJsonFiles(directory).filter(name => revisionNumber(name) > 0);
412
+ if (files.length === 0) return null;
413
+ const name = files.sort((a, b) => revisionNumber(a) - revisionNumber(b)).at(-1);
414
+ const document = readJson(join(directory, name));
415
+ return document && { number: revisionNumber(name), path: join(directory, name), document };
416
+ }
417
+
418
+ function readDeclaration(paths, dimension) {
419
+ return latestRevision(join(paths.declarations, dimension));
420
+ }
421
+
422
+ function declarationRevisions(paths, dimension) {
423
+ const directory = join(paths.declarations, dimension);
424
+ return listJsonFiles(directory)
425
+ .filter(name => revisionNumber(name) > 0)
426
+ .sort((a, b) => revisionNumber(a) - revisionNumber(b))
427
+ .map(name => ({ number: revisionNumber(name), document: readJson(join(directory, name)) }));
428
+ }
429
+
430
+ function writeDeclaration(paths, dimension, number, document) {
431
+ return writeJsonAtomic(join(paths.declarations, dimension, `rev-${number}.json`), document);
432
+ }
433
+
434
+ // A record identifier is `rec-001` per dimension; a revision is `rec-001@2`. Revision 1 opens a new
435
+ // record, a later one names its predecessor and extends that chain, so no revision is overwritten.
436
+ function recordIds(paths, dimension) {
437
+ return listDirectories(join(paths.records, dimension));
438
+ }
439
+
440
+ function readRecord(paths, dimension, recordId) {
441
+ const revision = latestRevision(join(paths.records, dimension, recordId));
442
+ return revision && { ...revision, id: recordId, reference: `${recordId}@${revision.number}` };
443
+ }
444
+
445
+ function readRecordRevision(paths, dimension, recordId, number) {
446
+ const path = join(paths.records, dimension, recordId, `rev-${number}.json`);
447
+ const document = readJson(path);
448
+ return document && { number, path, document, id: recordId, reference: `${recordId}@${number}` };
449
+ }
450
+
451
+ function currentRecords(paths, dimension) {
452
+ return recordIds(paths, dimension)
453
+ .map(id => readRecord(paths, dimension, id))
454
+ .filter(Boolean);
455
+ }
456
+
457
+ function nextRecordId(paths, dimension) {
458
+ const used = recordIds(paths, dimension)
459
+ .map(id => Number.parseInt(/^rec-(\d+)$/.exec(id)?.[1] ?? '0', 10));
460
+ return `rec-${String(Math.max(0, ...used) + 1).padStart(3, '0')}`;
461
+ }
462
+
463
+ function writeRecord(paths, dimension, recordId, number, document) {
464
+ return writeJsonAtomic(join(paths.records, dimension, recordId, `rev-${number}.json`), document);
465
+ }
466
+
467
+ function writeRecordSnapshot(paths, dimension, recordId, number, snapshot) {
468
+ return writeJsonAtomic(join(paths.records, dimension, recordId, `rev-${number}.snapshot.json`), snapshot);
469
+ }
470
+
471
+ function readRecordSnapshot(paths, dimension, recordId, number) {
472
+ return readJson(join(paths.records, dimension, recordId, `rev-${number}.snapshot.json`));
473
+ }
474
+
475
+ function appendEvidence(paths, dimension, document) {
476
+ const directory = join(paths.evidence, dimension);
477
+ const sequence = String(listJsonFiles(directory).length + 1).padStart(4, '0');
478
+ const name = `${sequence}-${document.kind.replaceAll(/[^a-z0-9-]/gi, '-')}.json`;
479
+ return writeJsonAtomic(join(directory, name), document);
480
+ }
481
+
482
+ function evidenceAppends(paths, dimension) {
483
+ const directory = join(paths.evidence, dimension);
484
+ return listJsonFiles(directory).map(name => readJson(join(directory, name))).filter(Boolean);
485
+ }
486
+
487
+ function sequencedWrite(directory, prefix, document) {
488
+ const sequence = String(listJsonFiles(directory).length + 1).padStart(3, '0');
489
+ const id = `${prefix}-${sequence}`;
490
+ writeJsonAtomic(join(directory, `${id}.json`), { ...document, id });
491
+ return id;
492
+ }
493
+
494
+ function sequencedList(directory) {
495
+ return listJsonFiles(directory).map(name => readJson(join(directory, name))).filter(Boolean);
496
+ }
497
+
498
+ function writeVerdict(paths, kind, dimension, document) {
499
+ const directory = join(paths.index, `${kind}s`, dimension);
500
+ const prefix = kind === 'attestation' ? 'att' : 'rep';
501
+ return sequencedWrite(directory, prefix, document);
502
+ }
503
+
504
+ function verdicts(paths, kind, dimension) {
505
+ return sequencedList(join(paths.index, `${kind}s`, dimension));
506
+ }
507
+
508
+ function writeDispute(paths, dimension, document) {
509
+ return sequencedWrite(join(paths.disputes, dimension), 'dis', document);
510
+ }
511
+
512
+ function disputes(paths, dimension) {
513
+ return sequencedList(join(paths.disputes, dimension));
514
+ }
515
+
516
+ function writeArbitration(paths, dimension, document) {
517
+ return sequencedWrite(join(paths.arbitrations, dimension), 'arb', document);
518
+ }
519
+
520
+ function arbitrations(paths, dimension) {
521
+ return sequencedList(join(paths.arbitrations, dimension));
522
+ }
523
+
524
+ function windowPath(paths, dimension) {
525
+ return join(paths.index, 'windows', `${dimension}.json`);
526
+ }
527
+
528
+ function readWindow(paths, dimension) {
529
+ return readJson(windowPath(paths, dimension));
530
+ }
531
+
532
+ function writeWindow(paths, dimension, document) {
533
+ return writeJsonAtomic(windowPath(paths, dimension), document);
534
+ }
535
+
536
+ function closeWindow(paths, dimension, outcome) {
537
+ const open = readWindow(paths, dimension);
538
+ if (!open) return null;
539
+ const directory = join(paths.index, 'closed-windows', dimension);
540
+ sequencedWrite(directory, 'win', { ...open, outcome, closedAt: new Date().toISOString() });
541
+ writeJsonAtomic(windowPath(paths, dimension), { closed: true });
542
+ return open;
543
+ }
544
+
545
+ function closedWindows(paths, dimension) {
546
+ return sequencedList(join(paths.index, 'closed-windows', dimension));
547
+ }
548
+
549
+ function readRounds(paths) {
550
+ return readJson(join(paths.index, 'rounds.json')) ?? { round: 0, conflictRounds: 0, history: [] };
551
+ }
552
+
553
+ function writeRounds(paths, value) {
554
+ return writeJsonAtomic(join(paths.index, 'rounds.json'), value);
555
+ }
556
+
557
+ function readStopSnapshot(paths) {
558
+ return readJson(join(paths.index, 'stop-snapshot.json'));
559
+ }
560
+
561
+ function writeStopSnapshot(paths, value) {
562
+ return writeJsonAtomic(join(paths.index, 'stop-snapshot.json'), value);
563
+ }
564
+
565
+ function invocationHash(command) {
566
+ const tokens = String(command).trim().split(/\s+/).filter(Boolean);
567
+ const gateAt = tokens.findIndex(token => token.includes('ai-engineering-gate'));
568
+ const rest = gateAt >= 0 ? tokens.slice(gateAt + 1) : tokens;
569
+ return hashText(canonicalJson(rest));
570
+ }
571
+
572
+ // Keyed on session, tool-use id and the hash of the gate argv so two same-verb calls
573
+ // stay distinct, and a leftover of another verb cannot be consumed as this one.
574
+ function handoffKey(session, toolUseId, commandHash) {
575
+ return hashText(canonicalJson([session ?? '', toolUseId ?? '', commandHash ?? ''])).slice(7, 39);
576
+ }
577
+
578
+ function writeHandoff(paths, key, document) {
579
+ return writeJsonAtomic(join(paths.index, 'handoffs', `${key}.json`), document);
580
+ }
581
+
582
+ function readHandoff(paths, key) {
583
+ return readJson(join(paths.index, 'handoffs', `${key}.json`));
584
+ }
585
+
586
+ function handoffs(paths) {
587
+ const directory = join(paths.index, 'handoffs');
588
+ return listJsonFiles(directory).map(name => ({ key: name.replace(/\.json$/, ''), document: readJson(join(directory, name)) }));
589
+ }
590
+
591
+ function dropHandoff(paths, key) {
592
+ const path = join(paths.index, 'handoffs', `${key}.json`);
593
+ if (existsSync(path)) writeJsonAtomic(path, { consumed: true, at: new Date().toISOString() });
594
+ }
595
+
596
+ function readBuilders(paths) {
597
+ return readJson(join(paths.index, 'builders.json')) ?? { agents: [], sessions: [] };
598
+ }
599
+
600
+ // The builder identities seen writing declarations, records and evidence. `attest` refuses a caller
601
+ // that appears here, which checks consistency and never proves independence.
602
+ function noteBuilder(paths, identity) {
603
+ const known = readBuilders(paths);
604
+ const agents = [...new Set([...known.agents, identity.agent].filter(Boolean))];
605
+ const sessions = [...new Set([...known.sessions, identity.session].filter(Boolean))];
606
+ writeJsonAtomic(join(paths.index, 'builders.json'), { agents, sessions });
607
+ }
608
+
609
+ // packages/ai-engineering-gate/src/context.mjs
610
+ // Everything a command reads, resolved once. `marked` is false in a repository that carries no
611
+ // marker, and every command then allows and prints nothing that blocks.
612
+ function buildContext(options) {
613
+ const cwd = options.cwd ?? process.cwd();
614
+ const repoRoot = findRepoRoot(cwd);
615
+ const marker = readMarker(repoRoot);
616
+ const distributionRoot = findDistributionRoot();
617
+ const registry = loadRegistry(distributionRoot);
618
+ const version = gateVersion(distributionRoot);
619
+ const context = {
620
+ cwd,
621
+ repoRoot,
622
+ marker,
623
+ marked: marker !== null,
624
+ registry,
625
+ version,
626
+ gateCommand: options.gateCommand ?? gateCommandLine(distributionRoot),
627
+ harness: options.harness ?? null,
628
+ };
629
+ if (!context.marked) return context;
630
+ context.members = registeredMembers(registry, marker);
631
+ context.dimensions = context.members.map(member => member.dimension);
632
+ context.evidenceRoot = resolveEvidenceRoot({
633
+ flag: options.evidenceRoot,
634
+ env: process.env.AI_ENGINEERING_GATE_EVIDENCE_ROOT,
635
+ marker,
636
+ repoRoot,
637
+ });
638
+ context.task = resolveTaskKey({
639
+ flag: options.task,
640
+ // A CI checkout is a detached merge ref, so no branch names the task. One export carries one
641
+ // task, which is a fact the export itself states better than a pipeline variable could.
642
+ env: process.env.AI_ENGINEERING_GATE_TASK ?? (options.fromExport ? soleTask(context.evidenceRoot) : null),
643
+ repoRoot,
644
+ });
645
+ context.paths = taskPaths(context.evidenceRoot, context.task);
646
+ context.conflictRoundCap = marker.conflictRoundCap ?? 3;
647
+ context.roundCap = marker.roundCap ?? 12;
648
+ context.allowReplay = marker.allowReplay !== false;
649
+ context.requireVerifiedIdentity = marker.requireVerifiedIdentity === true;
650
+ return context;
651
+ }
652
+
653
+ function soleTask(evidenceRoot) {
654
+ const tasks = listDirectories(evidenceRoot).filter(name => name !== 'index');
655
+ return tasks.length === 1 ? tasks[0] : null;
656
+ }
657
+
658
+ // The command line the gate prints for the next step. It names the entry point it is running from,
659
+ // so a checkout, a plugin install and an npm install each print something the reader can paste.
660
+ function gateCommandLine(distributionRoot) {
661
+ for (const candidate of ['packages/ai-engineering-gate/bin/ai-engineering-gate.mjs', 'bin/ai-engineering-gate.mjs']) {
662
+ if (existsSync(join(distributionRoot, candidate))) return `node ${join(distributionRoot, candidate)}`;
663
+ }
664
+ return 'ai-engineering-gate';
665
+ }
666
+
667
+ function memberOf(context, dimension) {
668
+ const member = context.members?.find(one => one.dimension === dimension);
669
+ if (!member) throw new Error(`${dimension} is not a dimension this project registers`);
670
+ return member;
671
+ }
672
+
673
+ function declarations(context) {
674
+ const found = {};
675
+ for (const dimension of context.dimensions) {
676
+ found[dimension] = readDeclaration(context.paths, dimension)?.document ?? null;
677
+ }
678
+ return found;
679
+ }
680
+
681
+ // The base a dimension compares against: its declaration's, the marker's default, or none.
682
+ function baseOf(context, declaration) {
683
+ return declaration?.base ?? context.marker.base ?? null;
684
+ }
685
+
686
+ // The three fingerprints, always computed here and never accepted from a document.
687
+ function fingerprintsOf(context, dimension, declaration) {
688
+ const member = memberOf(context, dimension);
689
+ const change = changeSet(context, baseOf(context, declaration));
690
+ return {
691
+ source: sourceFingerprint(context, declaration, change),
692
+ reference: referenceFingerprint(context.registry, member, context.version),
693
+ decision: decisionFingerprint(context, dimension),
694
+ change,
695
+ };
696
+ }
697
+
698
+ // The dimension's declaration and every one of its records, each with its revision number. A
699
+ // re-declaration or a record revision therefore expires the verdicts that read the old ones.
700
+ function decisionFingerprint(context, dimension) {
701
+ const declaration = readDeclaration(context.paths, dimension);
702
+ const entries = [
703
+ ['declaration', declaration ? `${declaration.number}:${hashJson(declaration.document)}` : 'absent'],
704
+ ];
705
+ for (const record of currentRecords(context.paths, dimension)) {
706
+ entries.push([`record:${record.id}`, `${record.number}:${hashJson(record.document)}`]);
707
+ }
708
+ return hashEntries(entries);
709
+ }
710
+
711
+ function undeclaredFor(context) {
712
+ const all = declarations(context);
713
+ const applicable = Object.values(all).filter(one => one?.applicability === 'applicable');
714
+ const base = applicable[0]?.base ?? context.marker.base ?? null;
715
+ return undeclaredChanges(changeSet(context, base), Object.values(all));
716
+ }
717
+
718
+ // packages/ai-engineering-gate/src/identity.mjs
719
+ // Harnesses that expose an agent identifier in hook input inside a subagent call. Elsewhere the
720
+ // identity claim cannot be checked, and the gate says so rather than pretending it verified one.
721
+ const HARNESSES_WITH_AGENT_IDENTITY = new Set(['claude-code']);
722
+ const HANDOFF_LIFETIME_MS = 5 * 60 * 1000;
723
+
724
+ function nonempty(value) {
725
+ return value ? value : null;
726
+ }
727
+
728
+ function sessionIdentifier() {
729
+ return nonempty(process.env.CLAUDE_SESSION_ID)
730
+ ?? nonempty(process.env.CODEX_SESSION_ID)
731
+ ?? nonempty(process.env.AI_ENGINEERING_GATE_SESSION)
732
+ ?? null;
733
+ }
734
+
735
+ function toolUseIdentifier() {
736
+ return nonempty(process.env.CLAUDE_TOOL_USE_ID)
737
+ ?? nonempty(process.env.AI_ENGINEERING_GATE_TOOL_USE_ID)
738
+ ?? null;
739
+ }
740
+
741
+ // The hook sees a session id the invoked process sometimes does not, so both the tool-use key and
742
+ // the session+command-hash key are written; a null-session twin is not, because a leftover of that
743
+ // twin would make a typed command look like an agent.
744
+ function recordHandoff(context, { session, verb, dimension, agent, agentType, toolUseId, command }) {
745
+ const commandHash = invocationHash(command);
746
+ const document = {
747
+ session, verb, dimension, agent, agentType, toolUseId, commandHash,
748
+ harness: context.harness,
749
+ recordedAt: new Date().toISOString(),
750
+ expiresAt: new Date(Date.now() + HANDOFF_LIFETIME_MS).toISOString(),
751
+ consumed: false,
752
+ };
753
+ const keys = [...new Set([
754
+ handoffKey(session, toolUseId, commandHash),
755
+ handoffKey(session, null, commandHash),
756
+ ])];
757
+ for (const key of keys) writeHandoff(context.paths, key, document);
758
+ return keys;
759
+ }
760
+
761
+ function takeHandoff(context) {
762
+ const session = sessionIdentifier();
763
+ const commandHash = invocationHash(process.argv.slice(1).join(' '));
764
+ const keys = [...new Set([
765
+ handoffKey(session, toolUseIdentifier(), commandHash),
766
+ handoffKey(session, null, commandHash),
767
+ ])];
768
+ const found = keys
769
+ .map(key => ({ key, stored: readHandoff(context.paths, key) }))
770
+ .find(one => one.stored && !one.stored.consumed
771
+ && !(one.stored.expiresAt && one.stored.expiresAt < new Date().toISOString()));
772
+ if (!found) return null;
773
+ for (const key of keys) {
774
+ const stored = readHandoff(context.paths, key);
775
+ if (stored) writeHandoff(context.paths, key, { ...stored, consumed: true });
776
+ }
777
+ return found.stored;
778
+ }
779
+
780
+ // Every gate call carries the session identifier. The agent identifier arrives only through the
781
+ // handoff a PreToolUse hook recorded, which is why its absence is the mechanical sign of a
782
+ // human-typed command.
783
+ function captureIdentity(context, verb, dimension) {
784
+ const handoff = takeHandoff(context, verb, dimension);
785
+ const providesAgents = HARNESSES_WITH_AGENT_IDENTITY.has(context.harness ?? handoff?.harness ?? '');
786
+ return {
787
+ session: sessionIdentifier(),
788
+ agent: handoff?.agent ?? null,
789
+ agentType: handoff?.agentType ?? null,
790
+ harness: context.harness ?? handoff?.harness ?? null,
791
+ handoffPresent: Boolean(handoff),
792
+ providesAgents,
793
+ verified: providesAgents && Boolean(handoff?.agent),
794
+ };
795
+ }
796
+
797
+ function stampBuilder(context, identity) {
798
+ noteBuilder(context.paths, identity);
799
+ }
800
+
801
+ function isBuilderIdentity(context, identity) {
802
+ const known = readBuilders(context.paths);
803
+ return Boolean(identity.agent) && known.agents.includes(identity.agent);
804
+ }
805
+
806
+ function storedIdentity(identity) {
807
+ return {
808
+ session: identity.session,
809
+ agent: identity.agent,
810
+ agentType: identity.agentType,
811
+ harness: identity.harness,
812
+ verified: identity.verified,
813
+ };
814
+ }
815
+
816
+ // packages/ai-engineering-gate/src/state.mjs
817
+ const sameFingerprints = (stored, current) => stored.source === current.source
818
+ && stored.reference === current.reference
819
+ && stored.decision === current.decision;
820
+
821
+ function staleCodes(stored, current) {
822
+ const codes = [];
823
+ if (stored.source !== current.source) codes.push('stale-source');
824
+ if (stored.reference !== current.reference) codes.push('stale-reference');
825
+ if (stored.decision !== current.decision) codes.push('stale-decision');
826
+ return codes;
827
+ }
828
+
829
+ // A planned artifact is produced when it exists in the worktree, under the task's evidence, or as an
830
+ // append that names it. Nothing is dispatched to a reviewer to notice an omission a machine can see.
831
+ function missingPlannedArtifacts(context, dimension, records) {
832
+ const appends = evidenceAppends(context.paths, dimension);
833
+ const produced = new Set(appends.map(append => append.produces).filter(Boolean));
834
+ const missing = [];
835
+ for (const record of records) {
836
+ for (const plan of record.document.plans ?? []) {
837
+ if (produced.has(plan.path)) continue;
838
+ if (existsSync(join(context.repoRoot, plan.path))) continue;
839
+ if (existsSync(join(context.paths.root, plan.path))) continue;
840
+ if (!missing.includes(plan.path)) missing.push(plan.path);
841
+ }
842
+ }
843
+ return missing;
844
+ }
845
+
846
+ const findingsOf = report => report?.envelope?.findings ?? [];
847
+
848
+ function remedySatisfied(context, dimension, finding, report, records) {
849
+ const remedy = finding.remedy;
850
+ if (!remedy) return false;
851
+ const appends = evidenceAppends(context.paths, dimension)
852
+ .filter(append => (append.filedAt ?? '') > (report.filedAt ?? ''));
853
+ if (remedy.kind === 'produce') {
854
+ return appends.some(append => append.produces === remedy.artifact)
855
+ || existsSync(join(context.repoRoot, remedy.artifact))
856
+ || existsSync(join(context.paths.root, remedy.artifact));
857
+ }
858
+ if (remedy.kind === 'rerun') {
859
+ return appends.some(append => append.payload?.command === remedy.command);
860
+ }
861
+ if (remedy.kind === 'replay') {
862
+ return appends.some(append => append.replayed === true && append.payload?.scenario === remedy.scenario);
863
+ }
864
+ return false;
865
+ }
866
+
867
+ function judgmentAddressed(context, dimension, finding, report, records, current) {
868
+ const cites = records.some(record => (record.document.revision?.addresses ?? []).includes(finding.id));
869
+ if (!cites) return false;
870
+ return report.fingerprints.source !== current.source || report.fingerprints.decision !== current.decision;
871
+ }
872
+
873
+ // A finding is pending until it is addressed, disputed or closed by arbitration. A changed
874
+ // fingerprint alone addresses nothing: touching a file reopens no review.
875
+ function findingStatus(context, dimension, report, records, current) {
876
+ if (!report) return { pending: [], disputed: [], closed: [] };
877
+ const openDisputes = disputes(context.paths, dimension).filter(one => one.report === report.id);
878
+ const decided = arbitrations(context.paths, dimension);
879
+ const pending = [];
880
+ const disputed = [];
881
+ const closed = [];
882
+ for (const finding of findingsOf(report)) {
883
+ const dispute = openDisputes.find(one => one.finding === finding.id);
884
+ const arbitration = dispute && decided.find(one => one.dispute === dispute.id);
885
+ if (arbitration?.decision === 'uphold') {
886
+ closed.push(finding.id);
887
+ continue;
888
+ }
889
+ if (dispute && !arbitration) {
890
+ disputed.push(finding.id);
891
+ continue;
892
+ }
893
+ const addressed = finding.kind === 'evidence'
894
+ ? remedySatisfied(context, dimension, finding, report, records)
895
+ : judgmentAddressed(context, dimension, finding, report, records, current);
896
+ if (addressed) closed.push(finding.id);
897
+ else pending.push(finding.id);
898
+ }
899
+ return { pending, disputed, closed };
900
+ }
901
+
902
+ function dimensionState(context, dimension) {
903
+ const declarationRevision = readDeclaration(context.paths, dimension);
904
+ const declaration = declarationRevision?.document ?? null;
905
+ const warnings = [];
906
+ if (installedReferenceMismatch(context.registry, context.members.find(one => one.dimension === dimension))) {
907
+ warnings.push('reference-mismatch');
908
+ }
909
+ if (!declaration) {
910
+ return {
911
+ dimension, state: 'undeclared', codes: ['missing-declaration'], warnings, records: 0,
912
+ missingEvidence: [], openFindings: [], declaration: null,
913
+ };
914
+ }
915
+ if (declaration.applicability === 'non-applicable') {
916
+ return {
917
+ dimension,
918
+ state: 'non-applicable',
919
+ applicability: 'non-applicable',
920
+ codes: declaration.reason ? [] : ['missing-reason'],
921
+ warnings,
922
+ records: 0,
923
+ missingEvidence: [],
924
+ openFindings: [],
925
+ declaration,
926
+ };
927
+ }
928
+
929
+ const records = currentRecords(context.paths, dimension);
930
+ const current = fingerprintsOf(context, dimension, declaration);
931
+ const openWindow = (() => {
932
+ const found = readWindow(context.paths, dimension);
933
+ return found && !found.closed ? found : null;
934
+ })();
935
+ const attestations = verdicts(context.paths, 'attestation', dimension);
936
+ const reports = verdicts(context.paths, 'report', dimension);
937
+ const filed = [...attestations, ...reports].sort((a, b) => (a.filedAt < b.filedAt ? -1 : 1));
938
+ const latest = filed.at(-1) ?? null;
939
+ const currentAttestation = attestations.find(one => sameFingerprints(one.fingerprints, current)) ?? null;
940
+ const currentReport = reports.filter(one => sameFingerprints(one.fingerprints, current)).at(-1) ?? null;
941
+ const lastReport = reports.at(-1) ?? null;
942
+ const status = findingStatus(context, dimension, lastReport, records, current);
943
+ const unresolvedDispute = status.disputed.length > 0;
944
+
945
+ const codes = [];
946
+ if (records.length === 0) codes.push('missing-record');
947
+ const missingEvidence = missingPlannedArtifacts(context, dimension, records);
948
+ if (missingEvidence.length > 0) codes.push('missing-evidence');
949
+ if (openWindow) codes.push('review-in-flight');
950
+ if (unresolvedDispute) codes.push('unresolved-dispute');
951
+ if (currentReport && status.pending.concat(status.disputed).length > 0) codes.push('non-sound-review');
952
+ if (status.pending.length > 0) codes.push('remedies-pending');
953
+ if (!currentAttestation) {
954
+ const stale = filed.filter(one => !sameFingerprints(one.fingerprints, current)).at(-1) ?? null;
955
+ if (stale) codes.push(...staleCodes(stale.fingerprints, current));
956
+ else if (!currentReport) codes.push('missing-review');
957
+ }
958
+
959
+ const state = (() => {
960
+ if (unresolvedDispute) return 'disputed';
961
+ if (openWindow) return 'in-review';
962
+ if (currentAttestation) return 'attested';
963
+ if (currentReport) return 'reported';
964
+ if (latest) return 'stale';
965
+ if (records.length > 0) return 'recorded';
966
+ return 'declared';
967
+ })();
968
+
969
+ for (const record of records) {
970
+ const wider = (record.document.scope ?? [])
971
+ .some(path => !(declaration.scope?.paths ?? []).some(scope => matchesPattern(path, scope) || path.startsWith(scope)));
972
+ if (wider && !warnings.includes('scope-wider-than-declaration')) warnings.push('scope-wider-than-declaration');
973
+ const claims = (record.document.cites ?? []).map(citation => `${citation.path}\0${citation.claim}`);
974
+ if (new Set(claims).size !== claims.length && !warnings.includes('duplicate-evidence')) {
975
+ warnings.push('duplicate-evidence');
976
+ }
977
+ }
978
+ if (currentAttestation?.identity?.verified === false) warnings.push('identity-unverified');
979
+ const rounds = readRounds(context.paths);
980
+ if ((rounds.conflicts ?? []).includes(dimension)) warnings.push('cross-dimension-conflict');
981
+
982
+ return {
983
+ dimension,
984
+ state,
985
+ applicability: 'applicable',
986
+ codes: [...new Set(codes)],
987
+ warnings,
988
+ records: records.length,
989
+ missingEvidence,
990
+ openFindings: [...status.pending, ...status.disputed],
991
+ fingerprints: { source: current.source, reference: current.reference, decision: current.decision },
992
+ declaration,
993
+ recordRefs: records.map(record => record.reference),
994
+ releasedWindows: closedWindows(context.paths, dimension).filter(one => one.outcome === 'released').length,
995
+ };
996
+ }
997
+
998
+ function nextAction(context, states, code) {
999
+ if (code === 'missing-review') {
1000
+ const agents = [...new Set(
1001
+ states
1002
+ .filter(state => state.codes.includes(code))
1003
+ .map(state => memberAgentType(memberOf(context, state.dimension))),
1004
+ )];
1005
+ if (agents.length > 0) return `dispatch a review round: launch ${agents.join(', ')}`;
1006
+ }
1007
+ return NEXT_ACTION[code] ?? 'read the full status';
1008
+ }
1009
+
1010
+ const NEXT_ACTION = {
1011
+ 'missing-declaration': 'park for the author: declare the dimension applicable or non-applicable with its reason, request and constraints',
1012
+ 'missing-reason': 'park for the author: re-declare the dimension with the reason it does not apply',
1013
+ 'missing-record': 'park for the author: write a decision record before the first affected write',
1014
+ 'missing-evidence': 'park for the author: file the planned artifacts through `evidence append`',
1015
+ 'missing-review': 'dispatch a review round: launch the registered reviewer',
1016
+ 'non-sound-review': 'park for the author: address or dispute every finding of the report',
1017
+ 'remedies-pending': 'dispatch a review round once the builder executes each remedy, or dispute it with counter-evidence',
1018
+ 'unresolved-dispute': 'park for the user: run `arbitrate` locally; no agent writes an arbitration',
1019
+ 'arbitration-required': 'park for the user: arbitrate the repeating cross-dimension conflict',
1020
+ 'round-cap-reached': 'park for the user: the task reached its total round cap',
1021
+ 'stale-source': 'dispatch a review round: the code moved since the verdict',
1022
+ 'stale-reference': 'dispatch a review round: a catalog, schema or guide moved',
1023
+ 'stale-decision': 'dispatch a review round: a declaration or record moved',
1024
+ 'review-in-flight': 'park for the author: wait for the open review to file or release',
1025
+ 'gate-failure': 'park as infrastructure: the gate itself failed; read the error and fix the setup',
1026
+ };
1027
+
1028
+ function completionOf(context, states) {
1029
+ const rounds = readRounds(context.paths);
1030
+ const codes = states.flatMap(one => one.codes);
1031
+ if ((rounds.conflictRounds ?? 0) >= context.conflictRoundCap) codes.push('arbitration-required');
1032
+ if ((rounds.round ?? 0) >= context.roundCap) codes.push('round-cap-reached');
1033
+ if (context.requireVerifiedIdentity && states.some(one => one.warnings.includes('identity-unverified'))) {
1034
+ codes.push('non-sound-review');
1035
+ }
1036
+ const unique = [...new Set(codes)];
1037
+ return {
1038
+ complete: unique.length === 0,
1039
+ codes: unique,
1040
+ next: unique.map(code => `${code}: ${nextAction(context, states, code)}`),
1041
+ };
1042
+ }
1043
+
1044
+ function suiteState(context) {
1045
+ const states = context.dimensions.map(dimension => dimensionState(context, dimension));
1046
+ const undeclared = undeclaredFor(context);
1047
+ if (undeclared.length > 0) {
1048
+ for (const state of states) {
1049
+ if (state.applicability === 'applicable' && !state.warnings.includes('undeclared-change')) {
1050
+ state.warnings.push('undeclared-change');
1051
+ }
1052
+ }
1053
+ }
1054
+ return { states, undeclaredChanges: undeclared, completion: completionOf(context, states), rounds: readRounds(context.paths) };
1055
+ }
1056
+
1057
+ // A round is ready when every applicable dimension has its records and its planned evidence, no
1058
+ // finding is pending, no dispute is open and no window is in flight.
1059
+ function reviewReady(states) {
1060
+ const applicable = states.filter(one => one.applicability === 'applicable');
1061
+ if (applicable.length === 0) return false;
1062
+ return applicable.every(one => one.records > 0
1063
+ && one.missingEvidence.length === 0
1064
+ && one.openFindings.length === 0
1065
+ && one.state !== 'in-review');
1066
+ }
1067
+
1068
+ function blockingCause(states) {
1069
+ const applicable = states.filter(one => one.applicability === 'applicable');
1070
+ for (const code of ['missing-record', 'missing-evidence', 'remedies-pending', 'unresolved-dispute']) {
1071
+ if (applicable.some(one => one.codes.includes(code))) return code;
1072
+ }
1073
+ return null;
1074
+ }
1075
+
1076
+ // packages/ai-engineering-gate/src/schema.mjs
1077
+ // A JSON Schema 2020-12 subset validator, bundled so the gate carries no runtime dependency.
1078
+ // It covers the keywords the suite's own schemas use and refuses an unknown keyword rather
1079
+ // than ignoring it, so a schema this validator cannot enforce is never silently accepted.
1080
+
1081
+ const KNOWN = new Set([
1082
+ '$schema', '$id', '$defs', '$ref', '$comment', 'title', 'description', 'default', 'examples',
1083
+ 'type', 'const', 'enum', 'required', 'properties', 'patternProperties', 'additionalProperties',
1084
+ 'propertyNames', 'dependentRequired', 'items', 'prefixItems', 'minItems', 'maxItems',
1085
+ 'uniqueItems', 'contains', 'minContains', 'maxContains', 'minLength', 'maxLength', 'pattern',
1086
+ 'minimum', 'maximum', 'exclusiveMinimum', 'exclusiveMaximum', 'multipleOf',
1087
+ 'allOf', 'anyOf', 'oneOf', 'not', 'if', 'then', 'else',
1088
+ ]);
1089
+
1090
+ const typeOf = value => {
1091
+ if (value === null) return 'null';
1092
+ if (Array.isArray(value)) return 'array';
1093
+ if (Number.isInteger(value)) return 'integer';
1094
+ return typeof value;
1095
+ };
1096
+
1097
+ const typeMatches = (value, expected) => {
1098
+ const actual = typeOf(value);
1099
+ if (expected === 'number') return actual === 'number' || actual === 'integer';
1100
+ if (expected === 'integer') return actual === 'integer';
1101
+ return actual === expected;
1102
+ };
1103
+
1104
+ const equals = (a, b) => {
1105
+ if (a === b) return true;
1106
+ if (typeOf(a) !== typeOf(b)) return false;
1107
+ if (Array.isArray(a)) return a.length === b.length && a.every((item, index) => equals(item, b[index]));
1108
+ if (a && typeof a === 'object') {
1109
+ const keys = Object.keys(a);
1110
+ return keys.length === Object.keys(b).length && keys.every(key => equals(a[key], b[key]));
1111
+ }
1112
+ return false;
1113
+ };
1114
+
1115
+ const resolvePointer = (root, reference) => {
1116
+ if (!reference.startsWith('#')) throw new Error(`unsupported schema reference: ${reference}`);
1117
+ let node = root;
1118
+ for (const raw of reference.slice(1).split('/').filter(Boolean)) {
1119
+ const token = decodeURIComponent(raw).replaceAll('~1', '/').replaceAll('~0', '~');
1120
+ node = node?.[token];
1121
+ if (node === undefined) throw new Error(`unresolved schema reference: ${reference}`);
1122
+ }
1123
+ return node;
1124
+ };
1125
+
1126
+ const check = (schema, value, path, root, errors) => {
1127
+ if (schema === true) return;
1128
+ if (schema === false) {
1129
+ errors.push({ path, keyword: 'false', message: 'no value is valid here' });
1130
+ return;
1131
+ }
1132
+ for (const keyword of Object.keys(schema)) {
1133
+ if (!KNOWN.has(keyword)) throw new Error(`unsupported schema keyword: ${keyword}`);
1134
+ }
1135
+ if (schema.$ref !== undefined) check(resolvePointer(root, schema.$ref), value, path, root, errors);
1136
+
1137
+ if (schema.type !== undefined) {
1138
+ const expected = Array.isArray(schema.type) ? schema.type : [schema.type];
1139
+ if (!expected.some(one => typeMatches(value, one))) {
1140
+ errors.push({ path, keyword: 'type', message: `expected ${expected.join(' or ')}, got ${typeOf(value)}` });
1141
+ return;
1142
+ }
1143
+ }
1144
+ if (schema.const !== undefined && !equals(value, schema.const)) {
1145
+ errors.push({ path, keyword: 'const', message: `expected ${JSON.stringify(schema.const)}` });
1146
+ }
1147
+ if (schema.enum !== undefined && !schema.enum.some(one => equals(value, one))) {
1148
+ errors.push({ path, keyword: 'enum', message: `expected one of ${JSON.stringify(schema.enum)}` });
1149
+ }
1150
+
1151
+ if (typeof value === 'string') {
1152
+ if (schema.minLength !== undefined && [...value].length < schema.minLength) {
1153
+ errors.push({ path, keyword: 'minLength', message: `shorter than ${schema.minLength}` });
1154
+ }
1155
+ if (schema.maxLength !== undefined && [...value].length > schema.maxLength) {
1156
+ errors.push({ path, keyword: 'maxLength', message: `longer than ${schema.maxLength}` });
1157
+ }
1158
+ if (schema.pattern !== undefined && !new RegExp(schema.pattern, 'u').test(value)) {
1159
+ errors.push({ path, keyword: 'pattern', message: `does not match ${schema.pattern}` });
1160
+ }
1161
+ }
1162
+
1163
+ if (typeof value === 'number') {
1164
+ if (schema.minimum !== undefined && value < schema.minimum) {
1165
+ errors.push({ path, keyword: 'minimum', message: `below ${schema.minimum}` });
1166
+ }
1167
+ if (schema.maximum !== undefined && value > schema.maximum) {
1168
+ errors.push({ path, keyword: 'maximum', message: `above ${schema.maximum}` });
1169
+ }
1170
+ if (schema.exclusiveMinimum !== undefined && value <= schema.exclusiveMinimum) {
1171
+ errors.push({ path, keyword: 'exclusiveMinimum', message: `not above ${schema.exclusiveMinimum}` });
1172
+ }
1173
+ if (schema.exclusiveMaximum !== undefined && value >= schema.exclusiveMaximum) {
1174
+ errors.push({ path, keyword: 'exclusiveMaximum', message: `not below ${schema.exclusiveMaximum}` });
1175
+ }
1176
+ if (schema.multipleOf !== undefined && value % schema.multipleOf !== 0) {
1177
+ errors.push({ path, keyword: 'multipleOf', message: `not a multiple of ${schema.multipleOf}` });
1178
+ }
1179
+ }
1180
+
1181
+ if (Array.isArray(value)) {
1182
+ const prefix = schema.prefixItems ?? [];
1183
+ prefix.forEach((item, index) => {
1184
+ if (index < value.length) check(item, value[index], `${path}/${index}`, root, errors);
1185
+ });
1186
+ if (schema.items !== undefined) {
1187
+ for (let index = prefix.length; index < value.length; index += 1) {
1188
+ check(schema.items, value[index], `${path}/${index}`, root, errors);
1189
+ }
1190
+ }
1191
+ if (schema.minItems !== undefined && value.length < schema.minItems) {
1192
+ errors.push({ path, keyword: 'minItems', message: `fewer than ${schema.minItems} items` });
1193
+ }
1194
+ if (schema.maxItems !== undefined && value.length > schema.maxItems) {
1195
+ errors.push({ path, keyword: 'maxItems', message: `more than ${schema.maxItems} items` });
1196
+ }
1197
+ if (schema.uniqueItems === true) {
1198
+ const duplicate = value.some((item, index) => value.slice(index + 1).some(other => equals(item, other)));
1199
+ if (duplicate) errors.push({ path, keyword: 'uniqueItems', message: 'items are not unique' });
1200
+ }
1201
+ if (schema.contains !== undefined) {
1202
+ const matches = value.filter(item => valid(schema.contains, item, root)).length;
1203
+ const min = schema.minContains ?? 1;
1204
+ if (matches < min) {
1205
+ errors.push({ path, keyword: 'contains', message: `fewer than ${min} matching items` });
1206
+ }
1207
+ if (schema.maxContains !== undefined && matches > schema.maxContains) {
1208
+ errors.push({ path, keyword: 'maxContains', message: `more than ${schema.maxContains} matching items` });
1209
+ }
1210
+ }
1211
+ }
1212
+
1213
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
1214
+ for (const key of schema.required ?? []) {
1215
+ if (!Object.hasOwn(value, key)) {
1216
+ errors.push({ path, keyword: 'required', message: `missing property ${key}` });
1217
+ }
1218
+ }
1219
+ const declared = new Set(Object.keys(schema.properties ?? {}));
1220
+ for (const [key, child] of Object.entries(schema.properties ?? {})) {
1221
+ if (Object.hasOwn(value, key)) check(child, value[key], `${path}/${key}`, root, errors);
1222
+ }
1223
+ for (const [pattern, child] of Object.entries(schema.patternProperties ?? {})) {
1224
+ const expression = new RegExp(pattern, 'u');
1225
+ for (const key of Object.keys(value)) {
1226
+ if (!expression.test(key)) continue;
1227
+ declared.add(key);
1228
+ check(child, value[key], `${path}/${key}`, root, errors);
1229
+ }
1230
+ }
1231
+ if (schema.additionalProperties !== undefined) {
1232
+ for (const key of Object.keys(value)) {
1233
+ if (declared.has(key)) continue;
1234
+ if (schema.additionalProperties === false) {
1235
+ errors.push({ path: `${path}/${key}`, keyword: 'additionalProperties', message: 'property is not allowed' });
1236
+ } else {
1237
+ check(schema.additionalProperties, value[key], `${path}/${key}`, root, errors);
1238
+ }
1239
+ }
1240
+ }
1241
+ if (schema.propertyNames !== undefined) {
1242
+ for (const key of Object.keys(value)) check(schema.propertyNames, key, `${path}/${key}`, root, errors);
1243
+ }
1244
+ for (const [key, dependents] of Object.entries(schema.dependentRequired ?? {})) {
1245
+ if (!Object.hasOwn(value, key)) continue;
1246
+ for (const dependent of dependents) {
1247
+ if (!Object.hasOwn(value, dependent)) {
1248
+ errors.push({ path, keyword: 'dependentRequired', message: `${key} requires ${dependent}` });
1249
+ }
1250
+ }
1251
+ }
1252
+ }
1253
+
1254
+ for (const child of schema.allOf ?? []) check(child, value, path, root, errors);
1255
+ if (schema.anyOf !== undefined && !schema.anyOf.some(child => valid(child, value, root))) {
1256
+ errors.push({ path, keyword: 'anyOf', message: 'matches no allowed alternative' });
1257
+ }
1258
+ if (schema.oneOf !== undefined) {
1259
+ const matches = schema.oneOf.filter(child => valid(child, value, root)).length;
1260
+ if (matches !== 1) {
1261
+ errors.push({ path, keyword: 'oneOf', message: `matches ${matches} alternatives, expected exactly one` });
1262
+ }
1263
+ }
1264
+ if (schema.not !== undefined && valid(schema.not, value, root)) {
1265
+ errors.push({ path, keyword: 'not', message: 'matches a forbidden shape' });
1266
+ }
1267
+ if (schema.if !== undefined) {
1268
+ const branch = valid(schema.if, value, root) ? schema.then : schema.else;
1269
+ if (branch !== undefined) check(branch, value, path, root, errors);
1270
+ }
1271
+ };
1272
+
1273
+ const valid = (schema, value, root) => {
1274
+ const errors = [];
1275
+ check(schema, value, '', root, errors);
1276
+ return errors.length === 0;
1277
+ };
1278
+
1279
+ function schemaErrors(schema, value) {
1280
+ const errors = [];
1281
+ check(schema, value, '', schema, errors);
1282
+ return errors.map(error => `${error.path || '/'}: ${error.message}`);
1283
+ }
1284
+
1285
+ // packages/ai-engineering-gate/src/validate.mjs
1286
+ // A record is validated twice: against the envelope every dimension shares, and against the
1287
+ // dimension's own schema. The gate reads the envelope; the body stays the dimension's business.
1288
+ function recordErrors(context, dimension, document) {
1289
+ const member = memberOf(context, dimension);
1290
+ return [
1291
+ ...schemaErrors(context.registry.schemas['decision-envelope'], document),
1292
+ ...schemaErrors(memberDecisionSchema(context.registry, member), document),
1293
+ ];
1294
+ }
1295
+
1296
+ function sharedErrors(context, name, document) {
1297
+ return schemaErrors(context.registry.schemas[name], document);
1298
+ }
1299
+
1300
+ function evidenceErrors(context, dimension, document) {
1301
+ const errors = sharedErrors(context, 'evidence-append', document);
1302
+ const schema = memberEvidenceSchema(context.registry, memberOf(context, dimension), document.kind);
1303
+ if (schema) errors.push(...schemaErrors(schema, document.payload));
1304
+ return errors;
1305
+ }
1306
+
1307
+ // A cited path must exist in the worktree or under the task's evidence when the record is validated.
1308
+ // `plans` are deliberately not checked here, so a record can precede the first affected write.
1309
+ function resolveCitation(context, path) {
1310
+ for (const candidate of [join(context.repoRoot, path), join(context.paths.root, path)]) {
1311
+ if (existsSync(candidate)) return candidate;
1312
+ }
1313
+ return null;
1314
+ }
1315
+
1316
+ function danglingCitations(context, document) {
1317
+ return (document.cites ?? [])
1318
+ .map(citation => citation.path)
1319
+ .filter(path => resolveCitation(context, path) === null);
1320
+ }
1321
+
1322
+ function citationSnapshot(context, document) {
1323
+ const cites = {};
1324
+ for (const citation of document.cites ?? []) {
1325
+ const resolved = resolveCitation(context, citation.path);
1326
+ cites[citation.path] = resolved ? hashFile(resolved) : null;
1327
+ }
1328
+ return cites;
1329
+ }
1330
+
1331
+ const CHECKABLE_FACT = /^unknown\b|^unknown$/i;
1332
+
1333
+ function walkStrings(value, path, into) {
1334
+ if (typeof value === 'string') {
1335
+ into.push([path, value]);
1336
+ return into;
1337
+ }
1338
+ if (Array.isArray(value)) {
1339
+ value.forEach((item, index) => walkStrings(item, `${path}/${index}`, into));
1340
+ return into;
1341
+ }
1342
+ if (value && typeof value === 'object') {
1343
+ for (const [key, child] of Object.entries(value)) walkStrings(child, `${path}/${key}`, into);
1344
+ }
1345
+ return into;
1346
+ }
1347
+
1348
+ // A claim of unknown about a checkable fact is accepted only with the sources consulted cited, or
1349
+ // with the paths searched and not found. Nothing else: a fleet declared unknown without opening a
1350
+ // file is what this refuses.
1351
+ function unsourcedUnknowns(document) {
1352
+ const citations = document.cites ?? [];
1353
+ const searched = citations.some(citation => (citation.searched ?? []).length > 0);
1354
+ if (searched) return [];
1355
+ const covered = new Set(citations.flatMap(citation => citation.covers ?? []));
1356
+ return walkStrings(document, '', [])
1357
+ .filter(([path, value]) => CHECKABLE_FACT.test(value.trim()) && !path.startsWith('/cites'))
1358
+ .filter(([path]) => {
1359
+ const segments = path.split('/').filter(Boolean).filter(segment => !/^\d+$/.test(segment));
1360
+ return !segments.some(segment => covered.has(segment)) && !covered.has(path);
1361
+ })
1362
+ .map(([path]) => path);
1363
+ }
1364
+
1365
+ // A revision that announces a correction is refused when no cited artifact changed, so a record
1366
+ // cannot describe work a script did not apply.
1367
+ function revisionChangeErrors(context, dimension, recordId, document) {
1368
+ const number = document.revision.number;
1369
+ if (number < 2) return [];
1370
+ const snapshot = readRecordSnapshot(context.paths, dimension, recordId, number - 1);
1371
+ if (!snapshot) return [`no snapshot for revision ${number - 1} of ${recordId}`];
1372
+ const current = citationSnapshot(context, document);
1373
+ const named = document.revision.changed ?? null;
1374
+ if (named) {
1375
+ return named
1376
+ .filter(path => (snapshot.cites?.[path] ?? null) === (current[path] ?? null))
1377
+ .map(path => `${path} is named as changed and its content is identical`);
1378
+ }
1379
+ const moved = Object.keys(current).some(path => (snapshot.cites?.[path] ?? null) !== current[path]);
1380
+ return moved ? [] : ['no cited artifact differs from the previous revision'];
1381
+ }
1382
+
1383
+ // A dispute needs a pointer to counter-evidence that exists now.
1384
+ function danglingCounterEvidence(context, document) {
1385
+ return (document.counterEvidence ?? [])
1386
+ .map(pointer => pointer.path)
1387
+ .filter(path => resolveCitation(context, path) === null);
1388
+ }
1389
+
1390
+ function ignoredFingerprints(document) {
1391
+ const found = walkStrings(document, '', [])
1392
+ .map(([path]) => path)
1393
+ .filter(path => /fingerprint/i.test(path));
1394
+ const keys = Object.keys(document).filter(key => /fingerprint/i.test(key));
1395
+ return found.length > 0 || keys.length > 0;
1396
+ }
1397
+
1398
+ // packages/ai-engineering-gate/src/cmd-write.mjs
1399
+ function requireDimension(context, dimension) {
1400
+ memberOf(context, dimension);
1401
+ return dimension;
1402
+ }
1403
+
1404
+ // A declaration locks once a record, a report, a window or a dispute depends on it, so a
1405
+ // re-declaration cannot erase a stored verdict. Widening the scope or adding constraints is allowed.
1406
+ function declarationLock(context, dimension, next) {
1407
+ const previous = readDeclaration(context.paths, dimension);
1408
+ if (!previous) return null;
1409
+ const dependents = [
1410
+ currentRecords(context.paths, dimension).length > 0 && 'a decision record',
1411
+ verdicts(context.paths, 'report', dimension).length > 0 && 'a report',
1412
+ verdicts(context.paths, 'attestation', dimension).length > 0 && 'an attestation',
1413
+ readWindow(context.paths, dimension)?.closed === false && 'an open review window',
1414
+ disputes(context.paths, dimension).length > 0 && 'a dispute',
1415
+ ].filter(Boolean);
1416
+ if (dependents.length === 0) return null;
1417
+ if (onlyWidens(previous.document, next)) return null;
1418
+ return dependents;
1419
+ }
1420
+
1421
+ function onlyWidens(previous, next) {
1422
+ if (previous.applicability !== next.applicability) return false;
1423
+ if (previous.request !== next.request) return false;
1424
+ if (previous.reason !== next.reason) return false;
1425
+ if (previous.applicability === 'non-applicable') return false;
1426
+ if (previous.base !== next.base) return false;
1427
+ const kept = (previous.scope?.paths ?? []).every(path => (next.scope?.paths ?? []).includes(path));
1428
+ const keptConfiguration = (previous.scope?.configuration ?? [])
1429
+ .every(path => (next.scope?.configuration ?? []).includes(path));
1430
+ const keptConstraints = (previous.constraints ?? []).every(one => (next.constraints ?? []).includes(one));
1431
+ return kept && keptConfiguration && keptConstraints;
1432
+ }
1433
+
1434
+ function commandDeclare(context, args, document) {
1435
+ const dimension = requireDimension(context, args.dimension);
1436
+ if (document?.dimension !== dimension) {
1437
+ return refuse('invalid-document', `the document declares ${document?.dimension ?? 'no dimension'}, the command declares ${dimension}`);
1438
+ }
1439
+ const errors = sharedErrors(context, 'declaration', document);
1440
+ if (errors.length > 0) return refuse('invalid-document', 'the declaration does not match its schema', errors);
1441
+
1442
+ const revisions = declarationRevisions(context.paths, dimension);
1443
+ const expected = revisions.length + 1;
1444
+ if (document.revision.number !== expected) {
1445
+ return refuse('invalid-document', `the next declaration revision of ${dimension} is ${expected}`);
1446
+ }
1447
+ const locked = declarationLock(context, dimension, document);
1448
+ if (locked) {
1449
+ return refuse('declaration-locked', `${locked.join(' and ')} depends on the declaration of ${dimension}; a revision may only widen its scope or add constraints`);
1450
+ }
1451
+ const identity = captureIdentity(context, 'declare', dimension);
1452
+ stampBuilder(context, identity);
1453
+ const path = writeDeclaration(context.paths, dimension, document.revision.number, document);
1454
+ return accept(
1455
+ `declared ${dimension} ${document.applicability} at revision ${document.revision.number}\nnext: write a decision record before the first affected write`,
1456
+ { document: { kind: 'declaration', dimension, revision: document.revision.number, path } },
1457
+ );
1458
+ }
1459
+
1460
+ function commandRecord(context, args, document) {
1461
+ const dimension = requireDimension(context, args.dimension);
1462
+ if (document?.dimension !== dimension) {
1463
+ return refuse('invalid-document', `the document records ${document?.dimension ?? 'no dimension'}, the command records ${dimension}`);
1464
+ }
1465
+ const declaration = readDeclaration(context.paths, dimension)?.document ?? null;
1466
+ if (!declaration) {
1467
+ return refuse('invalid-document', `declare ${dimension} before recording a decision for it`);
1468
+ }
1469
+ if (declaration.applicability !== 'applicable') {
1470
+ return refuse('invalid-document', `${dimension} is declared non-applicable; a non-applicable dimension has a declaration and nothing else`);
1471
+ }
1472
+ const errors = recordErrors(context, dimension, document);
1473
+ if (errors.length > 0) return refuse('invalid-document', 'the record does not match its envelope or its dimension schema', errors);
1474
+
1475
+ const dangling = danglingCitations(context, document);
1476
+ if (dangling.length > 0) {
1477
+ return refuse('dangling-reference', 'a cited path does not exist in the worktree or under the task evidence', dangling);
1478
+ }
1479
+ const unsourced = unsourcedUnknowns(document);
1480
+ if (unsourced.length > 0) {
1481
+ return refuse('unknown-without-source', 'a claim of unknown about a checkable fact cites no source consulted and no path searched', unsourced);
1482
+ }
1483
+
1484
+ const number = document.revision.number;
1485
+ let recordId;
1486
+ if (number === 1) {
1487
+ recordId = nextRecordId(context.paths, dimension);
1488
+ } else {
1489
+ const reference = /^(rec-\d+)@(\d+)$/.exec(document.revision.previous ?? '');
1490
+ if (!reference) {
1491
+ return refuse('invalid-document', 'revision previous must name the preceding revision as `rec-001@1`');
1492
+ }
1493
+ recordId = reference[1];
1494
+ const existing = readRecord(context.paths, dimension, recordId);
1495
+ if (!existing) return refuse('invalid-document', `${recordId} is not a record of ${dimension} for this task`);
1496
+ if (existing.number !== number - 1 || Number(reference[2]) !== number - 1) {
1497
+ return refuse('invalid-document', `the next revision of ${recordId} is ${existing.number + 1}`);
1498
+ }
1499
+ const changeErrors = revisionChangeErrors(context, dimension, recordId, document);
1500
+ if (changeErrors.length > 0) {
1501
+ return refuse('revision-without-change', 'the revision announces a correction that no cited artifact shows', changeErrors);
1502
+ }
1503
+ }
1504
+
1505
+ const warnings = ignoredFingerprints(document) ? ['fingerprint-ignored'] : [];
1506
+ const identity = captureIdentity(context, 'record', dimension);
1507
+ stampBuilder(context, identity);
1508
+ const path = writeRecord(context.paths, dimension, recordId, number, document);
1509
+ writeRecordSnapshot(context.paths, dimension, recordId, number, {
1510
+ revision: number,
1511
+ cites: citationSnapshot(context, document),
1512
+ builder: storedIdentity(identity),
1513
+ writtenAt: new Date().toISOString(),
1514
+ });
1515
+ const planned = (document.plans ?? []).map(plan => plan.path);
1516
+ const lines = [`recorded ${dimension} ${recordId}@${number}`];
1517
+ if (planned.length > 0) lines.push(`plans: ${planned.join(', ')}`);
1518
+ lines.push('next: write the implementation, then file every planned artifact through `evidence append`');
1519
+ return accept(lines.join('\n'), {
1520
+ document: { kind: 'record', dimension, id: recordId, revision: number, path },
1521
+ warnings,
1522
+ });
1523
+ }
1524
+
1525
+ function commandEvidenceAppend(context, args, document) {
1526
+ const dimension = requireDimension(context, args.dimension);
1527
+ if (document?.dimension !== dimension) {
1528
+ return refuse('invalid-document', `the append names ${document?.dimension ?? 'no dimension'}, the command names ${dimension}`);
1529
+ }
1530
+ const errors = evidenceErrors(context, dimension, document);
1531
+ if (errors.length > 0) return refuse('invalid-document', 'the evidence append does not match its schema', errors);
1532
+ const record = readRecord(context.paths, dimension, document.record.split('@')[0]);
1533
+ if (!record) return refuse('invalid-document', `${document.record} is not a record of ${dimension} for this task`);
1534
+
1535
+ const identity = captureIdentity(context, 'evidence', dimension);
1536
+ stampBuilder(context, identity);
1537
+ const stamped = stampAppend(context, dimension, record, document, identity, false);
1538
+ const stampErrors = sharedErrors(context, 'evidence-append', stamped);
1539
+ if (stampErrors.length > 0) {
1540
+ throw new Error(`the gate stamped an append its own schema refuses: ${stampErrors.join('; ')}`);
1541
+ }
1542
+ const path = appendEvidence(context.paths, dimension, stamped);
1543
+ return accept(`appended ${document.kind} to ${dimension} ${record.reference}`, {
1544
+ document: { kind: 'evidence-append', dimension, path },
1545
+ });
1546
+ }
1547
+
1548
+ // Every append carries the content hashes of the record's planned artifacts, by role, so an event
1549
+ // proves the state of test and production files at the moment it was filed.
1550
+ function stampAppend(context, dimension, record, document, identity, replayed, hashRoot = context.repoRoot) {
1551
+ const roles = {};
1552
+ for (const plan of record.document.plans ?? []) {
1553
+ const digest = hashFile(join(hashRoot, plan.path)) ?? hashFile(join(context.paths.root, plan.path));
1554
+ roles[plan.role] = { ...(roles[plan.role] ?? {}), [plan.path]: digest };
1555
+ }
1556
+ return {
1557
+ ...document,
1558
+ filedAt: new Date().toISOString(),
1559
+ artifactHashes: roles,
1560
+ builder: storedIdentity(identity),
1561
+ replayed,
1562
+ };
1563
+ }
1564
+
1565
+ function commandDispute(context, args, document) {
1566
+ const dimension = requireDimension(context, args.dimension);
1567
+ const errors = sharedErrors(context, 'dispute', document);
1568
+ if (errors.length > 0) return refuse('invalid-document', 'the dispute does not match its schema', errors);
1569
+ if (document.dimension !== dimension) {
1570
+ return refuse('invalid-document', `the dispute names ${document.dimension}, the command names ${dimension}`);
1571
+ }
1572
+ const report = verdicts(context.paths, 'report', dimension).find(one => one.id === document.report);
1573
+ if (!report) return refuse('invalid-document', `${document.report} is not a report of ${dimension} for this task`);
1574
+ const finding = (report.envelope.findings ?? []).find(one => one.id === document.finding);
1575
+ if (!finding) return refuse('invalid-document', `${document.report} carries no finding ${document.finding}`);
1576
+ const dangling = danglingCounterEvidence(context, document);
1577
+ if (dangling.length > 0) {
1578
+ return refuse('dispute-without-evidence', 'the counter-evidence points at a path that does not exist', dangling);
1579
+ }
1580
+ const already = disputes(context.paths, dimension).find(one => one.finding === document.finding
1581
+ && !arbitrations(context.paths, dimension).some(decided => decided.dispute === one.id));
1582
+ if (already) return refuse('invalid-document', `${document.finding} is already disputed as ${already.id}`);
1583
+
1584
+ const identity = captureIdentity(context, 'dispute', dimension);
1585
+ stampBuilder(context, identity);
1586
+ const id = writeDispute(context.paths, dimension, {
1587
+ ...document, filedAt: new Date().toISOString(), builder: storedIdentity(identity),
1588
+ });
1589
+ return accept(
1590
+ `disputed ${dimension} finding ${document.finding} as ${id}\nnext: the user decides; run \`${context.gateCommand} arbitrate --dimension ${dimension} --stdin\` yourself, no agent writes an arbitration`,
1591
+ { document: { kind: 'dispute', dimension, id } },
1592
+ );
1593
+ }
1594
+
1595
+ // Only a human writes an arbitration. Every agent tool call the hook saw left a handoff; a command
1596
+ // the user types passes through no hook and leaves none, and that absence is the mechanical sign.
1597
+ function commandArbitrate(context, args, document) {
1598
+ const dimension = requireDimension(context, args.dimension);
1599
+ const identity = captureIdentity(context, 'arbitrate', dimension);
1600
+ if (identity.handoffPresent) {
1601
+ return refuse('agent-arbitration-refused', 'an agent tool call invoked arbitrate; only a command the user types can write an arbitration');
1602
+ }
1603
+ const errors = sharedErrors(context, 'arbitration', document);
1604
+ if (errors.length > 0) return refuse('invalid-document', 'the arbitration does not match its schema', errors);
1605
+ if (document.dimension !== dimension) {
1606
+ return refuse('invalid-document', `the arbitration names ${document.dimension}, the command names ${dimension}`);
1607
+ }
1608
+ const dispute = disputes(context.paths, dimension).find(one => one.id === document.dispute);
1609
+ if (!dispute) return refuse('invalid-document', `${document.dispute} is not a dispute of ${dimension} for this task`);
1610
+ if (arbitrations(context.paths, dimension).some(one => one.dispute === document.dispute)) {
1611
+ return refuse('invalid-document', `${document.dispute} is already arbitrated`);
1612
+ }
1613
+ const id = writeArbitration(context.paths, dimension, {
1614
+ ...document, finding: dispute.finding, filedAt: new Date().toISOString(),
1615
+ });
1616
+ if (document.decision === 'uphold') {
1617
+ const rounds = readRounds(context.paths);
1618
+ writeRounds(context.paths, {
1619
+ ...rounds, conflictRounds: 0, conflicts: [], upheld: [...(rounds.upheld ?? []), { dimension, arbitration: id }],
1620
+ });
1621
+ }
1622
+ const next = document.decision === 'uphold'
1623
+ ? `next: one fresh review of ${dimension} is allowed on this same state`
1624
+ : `next: execute the remedy or correction of ${dispute.finding}`;
1625
+ return accept(`arbitrated ${document.dispute} as ${document.decision} (${id})\n${next}`, {
1626
+ document: { kind: 'arbitration', dimension, id },
1627
+ });
1628
+ }
1629
+
1630
+ function plannedEvidenceMissing(context, dimension) {
1631
+ return missingPlannedArtifacts(context, dimension, currentRecords(context.paths, dimension));
1632
+ }
1633
+
1634
+ // packages/ai-engineering-gate/src/plan.mjs
1635
+ // The gate never spawns, stops or messages an agent: it prints what to launch. The brief is rendered
1636
+ // from the declaration alone - the requester's wording, the factual constraints, the scope and the
1637
+ // base - so nothing of the builder's rationale reaches a reviewer.
1638
+ function neutralBrief(context, state, view) {
1639
+ const member = memberOf(context, state.dimension);
1640
+ const reviewSkill = member.review.split('/').pop();
1641
+ const declaration = state.declaration;
1642
+ const rounds = readRounds(context.paths);
1643
+ const conflict = (rounds.conflicts ?? []).includes(state.dimension);
1644
+ const lines = [
1645
+ '```text',
1646
+ `You are the fresh ${state.dimension} reviewer. Run /${reviewSkill} yourself; do not delegate.`,
1647
+ 'Read-only: do not modify any file, including code, records, catalogs or fixtures.',
1648
+ `Scope: ${(declaration.scope?.paths ?? []).join(', ')}; repository: ${context.repoRoot}.`,
1649
+ `Base: ${declaration.base}; include relevant uncommitted and new files.`,
1650
+ 'Use git diff and file reads, never git log or PR descriptions.',
1651
+ `Original request: ${declaration.request}`,
1652
+ `Factual constraints: ${(declaration.constraints ?? []).join(' | ') || 'none stated'}.`,
1653
+ `Records: ${state.recordRefs.map(reference => join(context.paths.records, state.dimension, reference.replace('@', '/rev-') + '.json')).join(', ')}`,
1654
+ `Check evidence: ${join(context.paths.evidence, state.dimension)}`,
1655
+ `Undeclared changes: ${view.undeclaredChanges.join(', ') || 'none'}.`,
1656
+ ];
1657
+ if (declaration.scope?.configuration?.length) {
1658
+ lines.push(`Relevant configuration: ${declaration.scope.configuration.join(', ')}.`);
1659
+ }
1660
+ if (state.warnings.includes('duplicate-evidence')) {
1661
+ lines.push('Note: two cited evidence items in one record are identical.');
1662
+ }
1663
+ if (conflict) {
1664
+ lines.push('Note: this dimension turned non-SOUND on a state produced by correcting another; the conflicting findings are in the previous reports.');
1665
+ }
1666
+ for (const arbitration of arbitrationsOf(view, state.dimension)) {
1667
+ lines.push(`Note: the user arbitrated finding ${arbitration.finding} as ${arbitration.decision}: ${arbitration.words}`);
1668
+ }
1669
+ lines.push(`Open your window first: ${context.gateCommand} begin --dimension ${state.dimension}`);
1670
+ lines.push('Report your frozen matrix before opening the records. End with the domain report');
1671
+ lines.push('and one verdict, or explain incomplete execution without attestation.');
1672
+ lines.push('```');
1673
+ return lines.join('\n');
1674
+ }
1675
+
1676
+ const arbitrationsOf = (view, dimension) => (view.arbitrations ?? []).filter(one => one.dimension === dimension);
1677
+
1678
+ function dispatchPlan(context, view) {
1679
+ if (!reviewReady(view.states)) return [];
1680
+ return view.states
1681
+ .filter(state => state.applicability === 'applicable' && state.state !== 'attested')
1682
+ .map(state => {
1683
+ const member = memberOf(context, state.dimension);
1684
+ const fingerprints = fingerprintsOf(context, state.dimension, state.declaration);
1685
+ return {
1686
+ dimension: state.dimension,
1687
+ agent: memberAgentType(member),
1688
+ skill: member.review.split('/').pop(),
1689
+ fingerprints: {
1690
+ source: fingerprints.source,
1691
+ reference: fingerprints.reference,
1692
+ decision: fingerprints.decision,
1693
+ },
1694
+ brief: neutralBrief(context, state, view),
1695
+ };
1696
+ });
1697
+ }
1698
+
1699
+ // packages/ai-engineering-gate/src/cmd-inspect.mjs
1700
+ function suiteView(context) {
1701
+ const view = suiteState(context);
1702
+ view.arbitrations = context.dimensions.flatMap(dimension => arbitrations(context.paths, dimension)
1703
+ .map(one => ({ ...one, dimension })));
1704
+ view.plan = dispatchPlan(context, view);
1705
+ return view;
1706
+ }
1707
+
1708
+ function commandStatus(context) {
1709
+ const view = suiteView(context);
1710
+ return { ok: true, view };
1711
+ }
1712
+
1713
+ function commandCanStop(context) {
1714
+ const view = suiteView(context);
1715
+ return { ok: view.completion.complete, view };
1716
+ }
1717
+
1718
+ function commandCanStopHook(context, reentrant) {
1719
+ const view = suiteView(context);
1720
+ if (view.completion.complete) return { ok: true, view, silent: true };
1721
+ const fingerprint = hashJson(view.states.map(state => ({
1722
+ dimension: state.dimension,
1723
+ fingerprints: state.fingerprints ?? null,
1724
+ codes: state.codes,
1725
+ })));
1726
+ const previous = readStopSnapshot(context.paths);
1727
+ if (reentrant && previous?.fingerprint === fingerprint) return { ok: true, view, silent: true };
1728
+ writeStopSnapshot(context.paths, { fingerprint });
1729
+ return { ok: false, view };
1730
+ }
1731
+
1732
+ // The text form answers the one question a builder asks - what does the gate see right now - while
1733
+ // `--json` stays the single status shape, whose dimensions already carry the same three digests.
1734
+ function commandFingerprint(context, args) {
1735
+ const all = declarations(context);
1736
+ const wanted = args.dimension ? [args.dimension] : context.dimensions;
1737
+ const lines = wanted.map(dimension => {
1738
+ const fingerprints = fingerprintsOf(context, dimension, all[dimension]);
1739
+ return `${dimension}\n source ${fingerprints.source}\n reference ${fingerprints.reference}\n decision ${fingerprints.decision}`;
1740
+ });
1741
+ return { ok: true, text: lines.join('\n'), view: suiteView(context) };
1742
+ }
1743
+
1744
+ const WRITE_FORM = /(^|[\s;&|])(>|>>|tee\b|sed\s+-i|perl\s+-i|mv\b|cp\b)|git\s+(apply|checkout|restore|stash|clean|rm|mv)\b|\bpatch\b/u;
1745
+
1746
+ // `can-write` is mechanical only. It decides by file path, never by the content of a write, and it
1747
+ // never infers applicability for the builder: a path outside every declared scope passes, except
1748
+ // while a review window is open, when the whole change set is frozen.
1749
+ function commandCanWrite(context, args) {
1750
+ const paths = args.paths ?? [];
1751
+ const command = args.command ?? null;
1752
+ const all = declarations(context);
1753
+
1754
+ const openWindows = context.dimensions
1755
+ .map(dimension => ({ dimension, window: readWindow(context.paths, dimension) }))
1756
+ .filter(one => one.window && !one.window.closed)
1757
+ .map(one => one.dimension);
1758
+
1759
+ const indexHit = paths.some(path => isInside(context.paths.index, absolute(context, path)))
1760
+ || (command !== null && command.includes(context.paths.index) && WRITE_FORM.test(command));
1761
+ if (indexHit) {
1762
+ return refuse('invalid-document', `the gate index is written by gate commands only: ${context.paths.index}`);
1763
+ }
1764
+
1765
+ const relatives = paths.map(path => toRepoRelative(context.repoRoot, path));
1766
+ if (openWindows.length > 0) {
1767
+ const hitsChangeSet = relatives.some(path => path && !excludedFromChangeSet(context, path))
1768
+ || (command !== null && WRITE_FORM.test(command));
1769
+ if (hitsChangeSet) {
1770
+ return refuse(
1771
+ 'review-in-flight',
1772
+ `a review is in flight on ${openWindows.join(', ')}; wait for it to file or release before editing the change set`,
1773
+ openWindows,
1774
+ );
1775
+ }
1776
+ }
1777
+
1778
+ const scoped = [];
1779
+ for (const relative of relatives) {
1780
+ for (const dimension of coveringDimensions(relative, all)) {
1781
+ if (!scoped.includes(dimension)) scoped.push(dimension);
1782
+ }
1783
+ }
1784
+ if (command !== null && WRITE_FORM.test(command)) {
1785
+ for (const [dimension, declaration] of Object.entries(all)) {
1786
+ if (declaration?.applicability !== 'applicable') continue;
1787
+ const named = (declaration.scope?.paths ?? []).some(scope => command.includes(scope));
1788
+ if (named && !scoped.includes(dimension)) scoped.push(dimension);
1789
+ }
1790
+ }
1791
+ if (scoped.length === 0) {
1792
+ return accept(paths.length > 0
1793
+ ? `allowed: no declared scope covers ${paths.join(', ')}`
1794
+ : 'allowed: nothing in a declared scope');
1795
+ }
1796
+ const without = scoped.filter(dimension => currentRecords(context.paths, dimension).length === 0);
1797
+ if (without.length > 0) {
1798
+ return refuse(
1799
+ 'invalid-document',
1800
+ `${without.join(' and ')} has no validated decision record and this path is inside its declared scope; record the decision before the first affected write`,
1801
+ without,
1802
+ );
1803
+ }
1804
+ return accept(`allowed: ${scoped.join(', ')} already has a validated decision record`);
1805
+ }
1806
+
1807
+ function absolute(context, path) {
1808
+ return isAbsolute(path) ? path : resolve(context.repoRoot, path);
1809
+ }
1810
+
1811
+ // Paths a harness edit tool names, whatever the tool's own shape.
1812
+ function editedPaths(toolInput) {
1813
+ const found = [];
1814
+ for (const key of ['file_path', 'filePath', 'notebook_path', 'notebookPath', 'path', 'target_file']) {
1815
+ if (typeof toolInput?.[key] === 'string') found.push(toolInput[key]);
1816
+ }
1817
+ for (const edit of toolInput?.edits ?? []) {
1818
+ if (typeof edit?.file_path === 'string') found.push(edit.file_path);
1819
+ if (typeof edit?.path === 'string') found.push(edit.path);
1820
+ }
1821
+ for (const change of toolInput?.changes ?? []) {
1822
+ if (typeof change?.path === 'string') found.push(change.path);
1823
+ }
1824
+ return [...new Set(found)];
1825
+ }
1826
+
1827
+ function shellCommand(toolInput) {
1828
+ for (const key of ['command', 'cmd', 'script']) {
1829
+ if (typeof toolInput?.[key] === 'string') return toolInput[key];
1830
+ }
1831
+ return null;
1832
+ }
1833
+
1834
+ // packages/ai-engineering-gate/src/cmd-review.mjs
1835
+ function reviewReadiness(context, dimension) {
1836
+ const view = suiteState(context);
1837
+ const own = view.states.find(state => state.dimension === dimension);
1838
+ if (!own) return refuse('invalid-document', `${dimension} is not a dimension this project registers`);
1839
+ if (own.applicability !== 'applicable') {
1840
+ return refuse('not-review-ready', `${dimension} is declared non-applicable; there is nothing to review`, [], 'missing-declaration');
1841
+ }
1842
+ const open = readWindow(context.paths, dimension);
1843
+ if (open && !open.closed) {
1844
+ return refuse('review-in-flight', `${dimension} already has an open review window opened at ${open.openedAt}`);
1845
+ }
1846
+ // Corrections are batched: while any dimension of the task has a pending finding, no review reopens.
1847
+ const cause = blockingCause(view.states);
1848
+ if (cause) {
1849
+ const upheld = (view.rounds.upheld ?? []).some(one => one.dimension === dimension);
1850
+ if (!(cause === 'remedies-pending' && upheld)) {
1851
+ const blocking = view.states.filter(state => state.codes.includes(cause)).map(state => state.dimension);
1852
+ return refuse('not-review-ready', `${cause} on ${blocking.join(', ')}`, blocking, cause);
1853
+ }
1854
+ }
1855
+ if ((view.rounds.conflictRounds ?? 0) >= context.conflictRoundCap) {
1856
+ return refuse('not-review-ready', `${dimension} reached the cross-dimension conflict cap; the user arbitrates before another review`, [], 'arbitration-required');
1857
+ }
1858
+ if ((view.rounds.round ?? 0) >= context.roundCap) {
1859
+ return refuse('not-review-ready', `the task reached its total round cap of ${context.roundCap}`, [], 'round-cap-reached');
1860
+ }
1861
+ return { ok: true, view, own };
1862
+ }
1863
+
1864
+ function commandCanReview(context, args) {
1865
+ const dimension = args.dimension;
1866
+ if (!dimension) return accept('allowed: no dimension named, nothing to gate');
1867
+ const readiness = reviewReadiness(context, dimension);
1868
+ if (!readiness.ok) return readiness;
1869
+ return accept(`allowed: ${dimension} is ready for a fresh review`);
1870
+ }
1871
+
1872
+ function commandBegin(context, args) {
1873
+ const dimension = args.dimension;
1874
+ const readiness = reviewReadiness(context, dimension);
1875
+ if (!readiness.ok) return readiness;
1876
+ const identity = captureIdentity(context, 'begin', dimension);
1877
+ if (identity.providesAgents && !identity.agent) {
1878
+ return refuse('identity-refused', 'this harness reports an agent identifier and none reached the gate; a review cannot begin without one');
1879
+ }
1880
+ if (isBuilderIdentity(context, identity)) {
1881
+ return refuse('identity-refused', 'the caller is the builder of this task; a review is dispatched to a fresh context');
1882
+ }
1883
+ const declaration = readDeclaration(context.paths, dimension).document;
1884
+ const fingerprints = fingerprintsOf(context, dimension, declaration);
1885
+ const window = {
1886
+ dimension,
1887
+ openedAt: new Date().toISOString(),
1888
+ identity: storedIdentity(identity),
1889
+ fingerprints: {
1890
+ source: fingerprints.source,
1891
+ reference: fingerprints.reference,
1892
+ decision: fingerprints.decision,
1893
+ },
1894
+ closed: false,
1895
+ };
1896
+ writeWindow(context.paths, dimension, window);
1897
+ const lines = [
1898
+ `window open for ${dimension}`,
1899
+ `state: source ${fingerprints.source} reference ${fingerprints.reference} decision ${fingerprints.decision}`,
1900
+ `records: ${currentRecords(context.paths, dimension).map(record => record.reference).join(', ')}`,
1901
+ ];
1902
+ lines.push(`next: file with \`${context.gateCommand} attest --dimension ${dimension} --stdin\` for SOUND, \`report\` otherwise`);
1903
+ return accept(lines.join('\n'), {
1904
+ document: { kind: 'window', dimension, fingerprints: window.fingerprints },
1905
+ warnings: identity.verified ? [] : ['identity-unverified'],
1906
+ });
1907
+ }
1908
+
1909
+ function fileVerdict(context, args, document, kind) {
1910
+ const dimension = args.dimension;
1911
+ const member = memberOf(context, dimension);
1912
+ const errors = sharedErrors(context, 'review-envelope', document);
1913
+ if (errors.length > 0) return refuse('invalid-document', 'the review envelope does not match its schema', errors);
1914
+ if (document.dimension !== dimension) {
1915
+ return refuse('invalid-document', `the envelope reviews ${document.dimension}, the command reviews ${dimension}`);
1916
+ }
1917
+ if (!member.contractVersions.includes(document.contractVersion)) {
1918
+ return refuse('invalid-document', `${dimension} supports contract ${member.contractVersions.join(' or ')}, the envelope declares ${document.contractVersion}`);
1919
+ }
1920
+ if (kind === 'attestation' && document.verdict !== 'SOUND') {
1921
+ return refuse('invalid-document', 'attest accepts SOUND only; file SMELLS and VIOLATIONS through report');
1922
+ }
1923
+ if (kind === 'report' && document.verdict === 'SOUND') {
1924
+ return refuse('invalid-document', 'report accepts SMELLS and VIOLATIONS; file SOUND through attest');
1925
+ }
1926
+ for (const finding of document.findings) {
1927
+ if (finding.kind === 'evidence' && !finding.remedy) {
1928
+ return refuse('finding-without-remedy', `finding ${finding.id} is about evidence and names no remedy the builder can execute`);
1929
+ }
1930
+ }
1931
+
1932
+ const open = readWindow(context.paths, dimension);
1933
+ if (!open || open.closed) {
1934
+ return refuse('no-window', `no open review window for ${dimension}; run \`${context.gateCommand} begin --dimension ${dimension}\` first`);
1935
+ }
1936
+ const identity = captureIdentity(context, kind === 'attestation' ? 'attest' : 'report', dimension);
1937
+ if (identity.providesAgents && !identity.agent) {
1938
+ return refuse('identity-refused', 'this harness reports an agent identifier and none reached the gate');
1939
+ }
1940
+ if (identity.agent && open.identity.agent && identity.agent !== open.identity.agent) {
1941
+ return refuse('no-window', `the window for ${dimension} was opened by another agent`);
1942
+ }
1943
+ if (isBuilderIdentity(context, identity)) {
1944
+ return refuse('identity-refused', 'the caller is the builder of this task; only a fresh reviewer files a verdict');
1945
+ }
1946
+ if (context.requireVerifiedIdentity && !identity.verified) {
1947
+ return refuse('identity-refused', 'the marker requires a verified agent identity and this harness reports none');
1948
+ }
1949
+
1950
+ const state = dimensionState(context, dimension);
1951
+ const missing = state.missingEvidence;
1952
+ if (missing.length > 0) {
1953
+ return refuse('not-review-ready', 'a planned artifact is still not produced', missing, 'missing-evidence');
1954
+ }
1955
+ const current = fingerprintsOf(context, dimension, state.declaration);
1956
+ const moved = ['source', 'reference', 'decision'].filter(one => current[one] !== open.fingerprints[one]);
1957
+ if (moved.length > 0) {
1958
+ closeWindow(context.paths, dimension, 'voided');
1959
+ return refuse('state-moved', `the ${moved.join(' and ')} fingerprint moved since this review began; the review is void and a fresh one is needed`, moved);
1960
+ }
1961
+
1962
+ const warnings = [];
1963
+ if (ignoredFingerprints(document)) warnings.push('fingerprint-ignored');
1964
+ if (!identity.verified) warnings.push('identity-unverified');
1965
+ if (document.reviewer.agentType && document.reviewer.agentType !== memberAgentType(member)) {
1966
+ warnings.push('identity-unverified');
1967
+ }
1968
+
1969
+ const rounds = readRounds(context.paths);
1970
+ const round = (rounds.round ?? 0) + (rounds.filedOn === current.source ? 0 : 1);
1971
+ const previous = verdicts(context.paths, 'attestation', dimension).at(-1) ?? null;
1972
+ const verdict = {
1973
+ document: kind,
1974
+ documentVersion: '1.0.0',
1975
+ dimension,
1976
+ task: context.task,
1977
+ round,
1978
+ filedAt: new Date().toISOString(),
1979
+ gateVersion: context.version,
1980
+ fingerprints: {
1981
+ source: current.source,
1982
+ reference: current.reference,
1983
+ decision: current.decision,
1984
+ },
1985
+ identity: storedIdentity(identity),
1986
+ envelope: document,
1987
+ ...(kind === 'report' ? { openFindings: document.findings.map(finding => finding.id) } : {}),
1988
+ warnings,
1989
+ };
1990
+ // The gate holds itself to the same schema it holds every writer to, so a drift in what it stores
1991
+ // fails here rather than reappearing as an unreadable index later.
1992
+ const stored = sharedErrors(context, 'verdict-record', { ...verdict, id: 'pending' });
1993
+ if (stored.length > 0) {
1994
+ throw new Error(`the gate composed a ${kind} its own schema refuses: ${stored.join('; ')}`);
1995
+ }
1996
+ const id = writeVerdict(context.paths, kind, dimension, verdict);
1997
+ closeWindow(context.paths, dimension, 'filed');
1998
+
1999
+ // A dimension that was SOUND and turns non-SOUND on a state produced by correcting another is a
2000
+ // cross-dimension conflict. The next briefs carry it and a repeating one goes to the user.
2001
+ const conflict = kind === 'report' && previous !== null;
2002
+ const conflicts = new Set(rounds.conflicts ?? []);
2003
+ if (conflict) conflicts.add(dimension);
2004
+ const sameState = rounds.filedOn === current.source;
2005
+ let conflictRounds = rounds.conflictRounds ?? 0;
2006
+ if (!sameState) {
2007
+ conflictRounds = conflict ? ((rounds.roundConflicted ? conflictRounds : 0) + 1) : 0;
2008
+ } else if (conflict && !rounds.roundConflicted) {
2009
+ conflictRounds += 1;
2010
+ }
2011
+ writeRounds(context.paths, {
2012
+ ...rounds,
2013
+ round,
2014
+ filedOn: current.source,
2015
+ conflicts: [...conflicts],
2016
+ roundConflicted: sameState ? Boolean(rounds.roundConflicted) || conflict : conflict,
2017
+ conflictRounds,
2018
+ history: [...(rounds.history ?? []), { round, dimension, verdict: document.verdict, id }],
2019
+ });
2020
+
2021
+ const lines = [`filed ${kind} ${id} for ${dimension}: ${document.verdict}`];
2022
+ if (kind === 'report') {
2023
+ lines.push(`findings: ${document.findings.map(finding => `${finding.id} (${finding.kind}, ${finding.severity})`).join(', ')}`);
2024
+ lines.push('next: the builder addresses or disputes every finding before any review of this task reopens');
2025
+ }
2026
+ return accept(lines.join('\n'), {
2027
+ document: { kind, dimension, id },
2028
+ warnings: conflict ? [...warnings, 'cross-dimension-conflict'] : warnings,
2029
+ });
2030
+ }
2031
+
2032
+ function commandAttest(context, args, document) {
2033
+ return fileVerdict(context, args, document, 'attestation');
2034
+ }
2035
+
2036
+ function commandReport(context, args, document) {
2037
+ return fileVerdict(context, args, document, 'report');
2038
+ }
2039
+
2040
+ function commandRelease(context, args) {
2041
+ const dimension = args.dimension;
2042
+ if (!dimension) return accept('nothing to release: no dimension named');
2043
+ const released = closeWindow(context.paths, dimension, 'released');
2044
+ if (!released) return accept(`no open window for ${dimension}`);
2045
+ return accept(`released the open review window for ${dimension}\nnext: dispatch a fresh reviewer for ${dimension}`);
2046
+ }
2047
+
2048
+ // packages/ai-engineering-gate/src/cmd-transfer.mjs
2049
+ // The export directory carries the task's documents into the repository so a CI runner can recompute
2050
+ // the fingerprints from a clean checkout. It is excluded from the change set and never fingerprinted.
2051
+ function commandExport(context) {
2052
+ const directory = context.marker.exportDirectory;
2053
+ if (!directory) {
2054
+ return refuse('invalid-document', 'the marker names no exportDirectory; add one before exporting');
2055
+ }
2056
+ const dirty = git(['status', '--porcelain'], context.repoRoot).stdout
2057
+ .split('\n')
2058
+ .map(line => line.slice(3).trim().replace(/^.* -> /, ''))
2059
+ .filter(Boolean)
2060
+ .filter(path => !path.startsWith(directory));
2061
+ const change = dirty.filter(path => !excludedFromChangeSet(context, path));
2062
+ if (change.length > 0) {
2063
+ return refuse(
2064
+ 'state-moved',
2065
+ 'HEAD would recompute different fingerprints than the attestations; commit the change set first',
2066
+ change.slice(0, 20),
2067
+ );
2068
+ }
2069
+ const view = suiteView(context);
2070
+ if (!view.completion.complete) {
2071
+ return refuse('not-review-ready', 'the task is not complete, so its verdicts cannot be published', view.completion.codes);
2072
+ }
2073
+ const target = join(context.repoRoot, directory, context.task);
2074
+ rmSync(target, { recursive: true, force: true });
2075
+ mkdirSync(dirname(target), { recursive: true });
2076
+ cpSync(context.paths.root, target, { recursive: true });
2077
+ const attested = view.states.filter(state => state.applicability === 'applicable').length;
2078
+ return accept(
2079
+ `exported ${attested} attested dimensions of ${context.task} to ${directory}/${context.task}\nnext: commit the export directory and let CI verify it from a clean checkout`,
2080
+ { document: { kind: 'export', path: target } },
2081
+ );
2082
+ }
2083
+
2084
+ // The CI half of the direct-PR route: the same completion logic over exported evidence, plus the two
2085
+ // checks only a clean checkout can make - the gate version the attestations record, and the marker.
2086
+ function exportVerificationErrors(context, base) {
2087
+ const errors = [];
2088
+ for (const dimension of context.dimensions) {
2089
+ for (const attestation of verdicts(context.paths, 'attestation', dimension)) {
2090
+ if (attestation.gateVersion !== context.version) {
2091
+ errors.push(`${dimension}: ${attestation.id} was filed by gate ${attestation.gateVersion} and this gate is ${context.version}`);
2092
+ }
2093
+ }
2094
+ }
2095
+ if (base) {
2096
+ const onBase = git(['cat-file', '-e', `${base}:.ai-engineering-suite.json`], context.repoRoot).ok;
2097
+ if (onBase && !context.marked) {
2098
+ errors.push(`the marker exists on ${base} and not on this head`);
2099
+ }
2100
+ }
2101
+ return errors;
2102
+ }
2103
+
2104
+ function applyExportVerification(context, result, base) {
2105
+ const errors = exportVerificationErrors(context, base);
2106
+ if (errors.length === 0) return result;
2107
+ const versionDrift = errors.some(one => one.includes('was filed by gate'));
2108
+ const codes = new Set(result.view?.completion?.codes ?? []);
2109
+ if (versionDrift) codes.add('stale-reference');
2110
+ const view = result.view && {
2111
+ ...result.view,
2112
+ completion: {
2113
+ complete: false,
2114
+ codes: [...codes],
2115
+ next: result.view.completion.next,
2116
+ },
2117
+ };
2118
+ if (versionDrift) {
2119
+ return { ok: false, view, details: errors };
2120
+ }
2121
+ return {
2122
+ ok: false,
2123
+ view,
2124
+ code: 'invalid-document',
2125
+ reason: 'the exported evidence does not verify against this checkout',
2126
+ details: errors,
2127
+ };
2128
+ }
2129
+
2130
+ // The gate executes the replay, never the builder. An isolated copy at the record's base carries the
2131
+ // planned test artifacts alone, so the red it captures is a fact the gate stamped.
2132
+ function commandReplay(context, args) {
2133
+ if (!context.allowReplay) {
2134
+ return refuse('replay-forbidden', 'the marker forbids the replay remedy; rerun a full cycle instead');
2135
+ }
2136
+ const dimension = args.dimension;
2137
+ const record = readRecord(context.paths, dimension, (args.record ?? '').split('@')[0]);
2138
+ if (!record) return refuse('invalid-document', `${args.record} is not a record of ${dimension} for this task`);
2139
+ if (!args.command) return refuse('invalid-document', 'replay needs the scenario command to run');
2140
+ const plans = record.document.plans ?? [];
2141
+ const tests = plans.filter(plan => plan.role === 'test');
2142
+ const production = plans.filter(plan => plan.role === 'production');
2143
+ if (tests.length === 0 || production.length === 0) {
2144
+ return refuse('invalid-document', 'a replay needs planned artifacts of role test and of role production');
2145
+ }
2146
+ const base = record.document.base;
2147
+ const point = git(['merge-base', base, 'HEAD'], context.repoRoot);
2148
+ if (!point.ok) return refuse('invalid-document', `cannot resolve the record base ${base}`);
2149
+
2150
+ const isolated = mkdtempSync(join(tmpdir(), 'ai-engineering-replay-'));
2151
+ const worktree = join(isolated, 'worktree');
2152
+ const added = git(['worktree', 'add', '--detach', worktree, point.stdout], context.repoRoot);
2153
+ if (!added.ok) {
2154
+ rmSync(isolated, { recursive: true, force: true });
2155
+ return refuse('invalid-document', `cannot create the isolated copy: ${added.stderr}`);
2156
+ }
2157
+ try {
2158
+ for (const name of ['node_modules', '.venv']) {
2159
+ const source = join(context.repoRoot, name);
2160
+ if (existsSync(source) && !existsSync(join(worktree, name))) symlinkSync(source, join(worktree, name));
2161
+ }
2162
+ place(context, worktree, tests);
2163
+ const red = run(args.command, worktree);
2164
+ const identity = captureIdentity(context, 'replay', dimension);
2165
+ const redEvent = appendEvidence(context.paths, dimension, stampAppend(context, dimension, record, {
2166
+ document: 'evidence-append',
2167
+ documentVersion: '1.0.0',
2168
+ dimension,
2169
+ record: record.reference,
2170
+ kind: 'journal-event',
2171
+ payload: {
2172
+ record: record.reference,
2173
+ scenario: args.scenario ?? 'scenario',
2174
+ phase: 'red',
2175
+ command: args.command,
2176
+ exitCode: red.code,
2177
+ output: writeOutput(context, args.scenario, 'red', red.output),
2178
+ cause: firstFailure(red.output),
2179
+ failureClass: 'expected-behavior-missing',
2180
+ },
2181
+ }, identity, true, worktree));
2182
+ place(context, worktree, production);
2183
+ const green = run(args.command, worktree);
2184
+ const greenEvent = appendEvidence(context.paths, dimension, stampAppend(context, dimension, record, {
2185
+ document: 'evidence-append',
2186
+ documentVersion: '1.0.0',
2187
+ dimension,
2188
+ record: record.reference,
2189
+ kind: 'journal-event',
2190
+ payload: {
2191
+ record: record.reference,
2192
+ scenario: args.scenario ?? 'scenario',
2193
+ phase: 'green',
2194
+ command: args.command,
2195
+ exitCode: green.code,
2196
+ output: writeOutput(context, args.scenario, 'green', green.output),
2197
+ },
2198
+ }, identity, true, worktree));
2199
+ const events = [redEvent, greenEvent];
2200
+ const lines = [
2201
+ `replayed ${dimension} ${record.reference} scenario ${args.scenario ?? 'scenario'}`,
2202
+ `red exit ${red.code}, green exit ${green.code}`,
2203
+ ];
2204
+ if (red.code === 0) {
2205
+ lines.push('warn: the test passed without the production change, so it discriminates nothing');
2206
+ }
2207
+ lines.push(`events: ${events.length} appended and stamped by the gate`);
2208
+ return accept(lines.join('\n'), { document: { kind: 'replay', dimension, events } });
2209
+ } finally {
2210
+ git(['worktree', 'remove', '--force', worktree], context.repoRoot);
2211
+ rmSync(isolated, { recursive: true, force: true });
2212
+ }
2213
+ }
2214
+
2215
+ function writeOutput(context, scenario, phase, output) {
2216
+ const relative = join('replays', `${scenario ?? 'scenario'}-${phase}.txt`);
2217
+ writeFileSync(ensure(join(context.paths.root, relative)), output);
2218
+ return relative;
2219
+ }
2220
+
2221
+ function ensure(path) {
2222
+ mkdirSync(dirname(path), { recursive: true });
2223
+ return path;
2224
+ }
2225
+
2226
+ function place(context, worktree, plans) {
2227
+ for (const plan of plans) {
2228
+ const source = join(context.repoRoot, plan.path);
2229
+ if (!existsSync(source)) continue;
2230
+ writeFileSync(ensure(join(worktree, plan.path)), readFileSync(source));
2231
+ }
2232
+ }
2233
+
2234
+ // Variables a caller's own runner uses to talk to its children. Inherited, they change the reporter
2235
+ // and the exit code of the scenario, so a replay run from inside a test suite would read green where
2236
+ // a shell reads red.
2237
+ const RUNNER_PRIVATE = ['NODE_TEST_CONTEXT', 'NODE_V8_COVERAGE', 'VITEST', 'VITEST_WORKER_ID', 'JEST_WORKER_ID'];
2238
+
2239
+ function run(command, cwd) {
2240
+ const env = { ...process.env };
2241
+ for (const name of RUNNER_PRIVATE) delete env[name];
2242
+ const result = spawnSync(command, { cwd, env, shell: true, encoding: 'utf8', timeout: 10 * 60 * 1000 });
2243
+ return {
2244
+ code: result.status ?? 1,
2245
+ output: `$ ${command}\n${result.stdout ?? ''}${result.stderr ?? ''}`,
2246
+ };
2247
+ }
2248
+
2249
+ function firstFailure(output) {
2250
+ const line = output.split('\n').find(one => /fail|error|expected|assert/i.test(one));
2251
+ return (line ?? 'the run failed').trim().slice(0, 300);
2252
+ }
2253
+
2254
+ // packages/ai-engineering-gate/src/hookio.mjs
2255
+ function readStdin() {
2256
+ try {
2257
+ return readFileSync(0, 'utf8');
2258
+ } catch {
2259
+ return '';
2260
+ }
2261
+ }
2262
+
2263
+ function readHookEvent() {
2264
+ const raw = readStdin().trim();
2265
+ if (!raw) return {};
2266
+ try {
2267
+ return JSON.parse(raw);
2268
+ } catch {
2269
+ return {};
2270
+ }
2271
+ }
2272
+
2273
+ function hookAgent(event) {
2274
+ // Codex delivers a shell command at the top level of `beforeShellExecution`, with no tool name, so
2275
+ // the two harnesses converge here rather than in every caller.
2276
+ const shellEvent = typeof event.command === 'string' && !event.tool_name && !event.toolName;
2277
+ return {
2278
+ session: event.session_id ?? event.sessionId ?? event.conversation_id ?? null,
2279
+ agent: event.agent_id ?? event.agentId ?? null,
2280
+ agentType: event.agent_type ?? event.agentType ?? event.subagent_type ?? event.subagentType ?? null,
2281
+ toolUseId: event.tool_use_id ?? event.toolUseId ?? event.call_id ?? null,
2282
+ toolName: event.tool_name ?? event.toolName ?? event.tool ?? (shellEvent ? 'shell' : null),
2283
+ toolInput: event.tool_input ?? event.toolInput ?? (shellEvent ? { command: event.command } : {}),
2284
+ reentrant: event.stop_hook_active === true || event.stopHookActive === true,
2285
+ };
2286
+ }
2287
+
2288
+ const claudeCode = {
2289
+ sessionContext: text => ({
2290
+ hookSpecificOutput: { hookEventName: 'SessionStart', additionalContext: text },
2291
+ }),
2292
+ toolDeny: reason => ({
2293
+ hookSpecificOutput: {
2294
+ hookEventName: 'PreToolUse',
2295
+ permissionDecision: 'deny',
2296
+ permissionDecisionReason: reason,
2297
+ },
2298
+ }),
2299
+ stopBlock: reason => ({ decision: 'block', reason }),
2300
+ };
2301
+
2302
+ // Codex shapes, from its documented output cheat sheet: a permission decision for `preToolUse`,
2303
+ // `beforeShellExecution` and `subagentStart`, and a follow-up message for the `stop` and
2304
+ // `subagentStop` loops. Codex documents no output field that injects context at `sessionStart`, so the
2305
+ // gate answers nothing there and the installer says so rather than pretending the status was delivered.
2306
+ const codex = {
2307
+ sessionContext: () => null,
2308
+ toolDeny: reason => ({ permission: 'deny', agent_message: reason, user_message: reason }),
2309
+ stopBlock: reason => ({ followup_message: reason }),
2310
+ };
2311
+
2312
+ const SHAPES = { 'claude-code': claudeCode, codex };
2313
+
2314
+ const shapeOf = harness => SHAPES[harness] ?? claudeCode;
2315
+
2316
+ const sessionContext = (text, harness) => shapeOf(harness).sessionContext(text);
2317
+ const toolDeny = (reason, harness) => shapeOf(harness).toolDeny(reason);
2318
+ const stopBlock = (reason, harness) => shapeOf(harness).stopBlock(reason);
2319
+
2320
+ // packages/ai-engineering-gate/src/render.mjs
2321
+ // AXI shape: a useful no-argument output, a compact aggregate by default, reasons on request and one
2322
+ // `next` line per thing to do. Detail is behind `--full`, automation behind `--json`.
2323
+ function renderTable(rows, headers) {
2324
+ if (rows.length === 0) return '';
2325
+ const widths = headers.map((header, column) => Math.max(
2326
+ header.length,
2327
+ ...rows.map(row => String(row[column] ?? '').length),
2328
+ ));
2329
+ const line = cells => cells.map((cell, column) => String(cell ?? '').padEnd(widths[column])).join(' ').trimEnd();
2330
+ return [line(headers), ...rows.map(line)].join('\n');
2331
+ }
2332
+
2333
+ function renderStatus(context, view, { full = false } = {}) {
2334
+ const lines = [];
2335
+ lines.push(`task ${context.task} round ${view.rounds.round ?? 0} gate ${context.version}`);
2336
+ lines.push(renderTable(
2337
+ view.states.map(state => [state.dimension, state.state, describe(state)]),
2338
+ ['dimension', 'state', 'why'],
2339
+ ));
2340
+ const warnings = [...new Set(view.states.flatMap(state => state.warnings))];
2341
+ if (warnings.length > 0) lines.push(`warn: ${warnings.join(', ')}`);
2342
+ for (const next of view.completion.next) lines.push(`next: ${next}`);
2343
+ if (view.completion.complete) lines.push('next: nothing; every registered dimension is complete');
2344
+
2345
+ if (!full) {
2346
+ lines.push(`help: ${context.gateCommand} status --full`);
2347
+ return lines.filter(Boolean).join('\n');
2348
+ }
2349
+
2350
+ for (const state of view.states) {
2351
+ lines.push('');
2352
+ lines.push(`## ${state.dimension} - ${state.state}`);
2353
+ if (state.declaration) {
2354
+ lines.push(`applicability: ${state.declaration.applicability} - ${state.declaration.reason}`);
2355
+ if (state.declaration.scope) lines.push(`scope: ${state.declaration.scope.paths.join(', ')}`);
2356
+ }
2357
+ if (state.records) lines.push(`records: ${state.recordRefs.join(', ')}`);
2358
+ if (state.missingEvidence?.length) lines.push(`missing evidence: ${state.missingEvidence.join(', ')}`);
2359
+ if (state.openFindings?.length) lines.push(`open findings: ${state.openFindings.join(', ')}`);
2360
+ if (state.fingerprints) {
2361
+ lines.push(`fingerprints: source ${short(state.fingerprints.source)} reference ${short(state.fingerprints.reference)} decision ${short(state.fingerprints.decision)}`);
2362
+ }
2363
+ if (state.releasedWindows) lines.push(`released windows: ${state.releasedWindows}`);
2364
+ for (const code of state.codes) lines.push(`code: ${code}`);
2365
+ for (const warning of state.warnings) lines.push(`warn: ${warning}`);
2366
+ }
2367
+ if (view.undeclaredChanges.length > 0) {
2368
+ lines.push('');
2369
+ lines.push('## undeclared changes');
2370
+ for (const path of view.undeclaredChanges) lines.push(`- ${path}`);
2371
+ }
2372
+ if (view.plan?.length) {
2373
+ lines.push('');
2374
+ lines.push('## dispatch plan');
2375
+ lines.push('Launch every reviewer below in one turn, then wait for all filings.');
2376
+ for (const entry of view.plan) {
2377
+ lines.push('');
2378
+ lines.push(`### ${entry.dimension} - agent ${entry.agent}`);
2379
+ lines.push(`state: source ${short(entry.fingerprints.source)} reference ${short(entry.fingerprints.reference)} decision ${short(entry.fingerprints.decision)}`);
2380
+ lines.push(entry.brief);
2381
+ }
2382
+ }
2383
+ return lines.filter(line => line !== null).join('\n');
2384
+ }
2385
+
2386
+ const short = digest => (digest ?? '').replace('sha256:', '').slice(0, 12);
2387
+
2388
+ function describe(state) {
2389
+ if (state.state === 'non-applicable') return state.declaration?.reason ?? '';
2390
+ if (state.codes.length > 0) return state.codes.join(', ');
2391
+ return 'complete';
2392
+ }
2393
+
2394
+ function statusJson(context, view, extra = {}) {
2395
+ return {
2396
+ outputVersion: '1.0.0',
2397
+ command: extra.command ?? 'status',
2398
+ gateVersion: context.version,
2399
+ marked: true,
2400
+ ok: extra.ok ?? true,
2401
+ task: context.task,
2402
+ repository: context.repoRoot,
2403
+ evidenceRoot: context.evidenceRoot,
2404
+ gateCommand: context.gateCommand,
2405
+ round: view.rounds.round ?? 0,
2406
+ dimensions: view.states.map(state => ({
2407
+ dimension: state.dimension,
2408
+ state: state.state,
2409
+ ...(state.applicability ? { applicability: state.applicability } : {}),
2410
+ records: state.records,
2411
+ codes: state.codes,
2412
+ warnings: state.warnings,
2413
+ missingEvidence: state.missingEvidence,
2414
+ openFindings: state.openFindings,
2415
+ ...(state.fingerprints ? { fingerprints: state.fingerprints } : {}),
2416
+ })),
2417
+ completion: view.completion,
2418
+ warnings: [...new Set(view.states.flatMap(state => state.warnings))],
2419
+ undeclaredChanges: view.undeclaredChanges,
2420
+ ...(view.plan?.length ? { plan: view.plan } : {}),
2421
+ ...extra.payload,
2422
+ };
2423
+ }
2424
+
2425
+ function silentJson(context, command) {
2426
+ return {
2427
+ outputVersion: '1.0.0',
2428
+ command,
2429
+ gateVersion: context.version,
2430
+ marked: false,
2431
+ ok: true,
2432
+ };
2433
+ }
2434
+
2435
+ // The gate validates its own JSON against the published output schema, so a drift in the machine
2436
+ // surface fails here rather than in a driver.
2437
+ function assertOutputShape(context, payload) {
2438
+ const errors = schemaErrors(context.registry.schemas['gate-output'], payload);
2439
+ if (errors.length > 0) throw new Error(`gate output does not match its schema: ${errors.join('; ')}`);
2440
+ return payload;
2441
+ }
2442
+
2443
+ // packages/ai-engineering-gate/src/main.mjs
2444
+ const DOCUMENT_VERBS = new Set([
2445
+ 'declare', 'record', 'evidence', 'begin', 'attest', 'report', 'dispute', 'arbitrate', 'replay', 'release',
2446
+ ]);
2447
+ const WRITING_COMMANDS = new Set(['declare', 'record', 'evidence append', 'dispute', 'arbitrate']);
2448
+ const EDIT_TOOLS = /^(edit|write|multiedit|multi_edit|notebookedit|notebook_edit|apply_patch|str_replace|create_file)$/i;
2449
+ const SHELL_TOOLS = /^(bash|shell|run_terminal_cmd|terminal|exec)$/i;
2450
+ const DISPATCH_TOOLS = /^(task|agent|dispatch_agent|subagent)$/i;
2451
+
2452
+ const HELP = `ai-engineering-gate - generic enforcement for the transpose/review suite
2453
+
2454
+ usage: ai-engineering-gate [command] [options]
2455
+
2456
+ With no command it prints the compact status of every registered dimension for this task.
2457
+ In a repository that carries no \`.ai-engineering-suite.json\` marker it prints nothing and allows.
2458
+
2459
+ commands
2460
+ status compact state of every registered dimension, plus the next action
2461
+ declare --dimension D --stdin file a declaration before any record
2462
+ record --dimension D --stdin file a decision record; refuses a dangling citation
2463
+ evidence append --dimension D --stdin append a journal event, a check output or a snapshot
2464
+ replay --dimension D --record R --scenario S --command C the gate runs and stamps a red/green pair
2465
+ begin --dimension D open a review window and freeze the state it binds to
2466
+ attest --dimension D --stdin file a SOUND review envelope
2467
+ report --dimension D --stdin file a SMELLS or VIOLATIONS review envelope
2468
+ dispute --dimension D --stdin contest one finding with a pointer to counter-evidence
2469
+ arbitrate --dimension D --stdin the user's decision on a dispute; refused from an agent call
2470
+ release --dimension D abandon an open review window
2471
+ export copy the task's documents into the marker's export directory
2472
+ can-write --path P ... may this path be edited now
2473
+ can-review --dimension D may a fresh review of this dimension start now
2474
+ can-stop is the task complete; the exit code is the publication lock
2475
+ fingerprint [--dimension D] the three fingerprints, as the gate computes them
2476
+
2477
+ options
2478
+ --json stable machine output, versioned and schema-checked
2479
+ --full every reason, the missing evidence and the dispatch plan
2480
+ --hook read the harness event on stdin and answer in its own shape
2481
+ --harness NAME the harness the hook wiring runs under
2482
+ --task KEY the task key, for a pipeline or a detached head
2483
+ --evidence-root PATH where the evidence lives; also the export directory in CI
2484
+ --base REF the comparison base, for CI verification
2485
+ --from-export verify exported evidence: gate version and the marker on the base
2486
+
2487
+ exit codes
2488
+ 0 allowed or valid 2 denied by policy 1 gate or infrastructure failure`;
2489
+
2490
+ function parseArguments(argv) {
2491
+ const args = { paths: [], command: null };
2492
+ const positional = [];
2493
+ for (let index = 0; index < argv.length; index += 1) {
2494
+ const token = argv[index];
2495
+ const next = () => argv[index += 1];
2496
+ if (!token.startsWith('--')) {
2497
+ positional.push(token);
2498
+ continue;
2499
+ }
2500
+ switch (token) {
2501
+ case '--json': args.json = true; break;
2502
+ case '--full': args.full = true; break;
2503
+ case '--hook': args.hook = true; break;
2504
+ case '--stdin': args.stdin = true; break;
2505
+ case '--from-export': args.fromExport = true; break;
2506
+ case '--help': args.help = true; break;
2507
+ case '--version': args.version = true; break;
2508
+ case '--dimension': args.dimension = next(); break;
2509
+ case '--harness': args.harness = next(); break;
2510
+ case '--task': args.task = next(); break;
2511
+ case '--evidence-root': args.evidenceRoot = next(); break;
2512
+ case '--record': args.record = next(); break;
2513
+ case '--scenario': args.scenario = next(); break;
2514
+ case '--base': args.base = next(); break;
2515
+ case '--path': args.paths.push(next()); break;
2516
+ case '--command': args.command = next(); break;
2517
+ default: throw new Error(`unknown option ${token}; run --help`);
2518
+ }
2519
+ }
2520
+ args.name = positional.length === 0
2521
+ ? 'status'
2522
+ : (positional[0] === 'evidence' ? positional.slice(0, 2).join(' ') : positional[0]);
2523
+ args.positional = positional;
2524
+ return args;
2525
+ }
2526
+
2527
+ function documentRefusal(reason) {
2528
+ const error = new Error(reason);
2529
+ error.refusal = refuse('invalid-document', reason);
2530
+ throw error;
2531
+ }
2532
+
2533
+ function readDocument(args) {
2534
+ const raw = readStdin();
2535
+ if (!raw.trim()) {
2536
+ documentRefusal(`${args.name} reads its document on stdin; pipe it and pass --stdin`);
2537
+ }
2538
+ try {
2539
+ return JSON.parse(raw);
2540
+ } catch (error) {
2541
+ documentRefusal(`the document on stdin is not JSON: ${error.message}`);
2542
+ }
2543
+ }
2544
+
2545
+ function requireDimensionFlag(args) {
2546
+ if (!args.dimension) throw new Error(`${args.name} needs --dimension <identifier>`);
2547
+ }
2548
+
2549
+ function runCommand(context, args) {
2550
+ switch (args.name) {
2551
+ case 'status': return commandStatus(context);
2552
+ case 'can-stop': {
2553
+ const result = commandCanStop(context);
2554
+ if (!args.fromExport) return result;
2555
+ return applyExportVerification(context, result, args.base);
2556
+ }
2557
+ case 'can-write': return commandCanWrite(context, args);
2558
+ case 'can-review': return commandCanReview(context, args);
2559
+ case 'fingerprint': return commandFingerprint(context, args);
2560
+ case 'declare': requireDimensionFlag(args); return commandDeclare(context, args, readDocument(args));
2561
+ case 'record': requireDimensionFlag(args); return commandRecord(context, args, readDocument(args));
2562
+ case 'evidence append': requireDimensionFlag(args); return commandEvidenceAppend(context, args, readDocument(args));
2563
+ case 'dispute': requireDimensionFlag(args); return commandDispute(context, args, readDocument(args));
2564
+ case 'arbitrate': requireDimensionFlag(args); return commandArbitrate(context, args, readDocument(args));
2565
+ case 'begin': requireDimensionFlag(args); return commandBegin(context, args);
2566
+ case 'attest': requireDimensionFlag(args); return commandAttest(context, args, readDocument(args));
2567
+ case 'report': requireDimensionFlag(args); return commandReport(context, args, readDocument(args));
2568
+ case 'release': return commandRelease(context, args);
2569
+ case 'replay': requireDimensionFlag(args); return commandReplay(context, args);
2570
+ case 'export': return commandExport(context);
2571
+ default: throw new Error(`unknown command ${args.name}; run --help`);
2572
+ }
2573
+ }
2574
+
2575
+ function render(context, args, result) {
2576
+ if (args.json) {
2577
+ const view = result.view ?? suiteView(context);
2578
+ return JSON.stringify(assertOutputShape(context, statusJson(context, view, {
2579
+ command: args.name,
2580
+ ok: result.ok,
2581
+ payload: {
2582
+ ...(result.code ? { refusal: { code: result.code, reason: result.reason, ...(result.cause ? { cause: result.cause } : {}), ...(result.details?.length ? { details: result.details } : {}) } } : {}),
2583
+ ...(result.payload?.document ? { document: result.payload.document } : {}),
2584
+ },
2585
+ })), null, 2);
2586
+ }
2587
+ if (!result.ok) {
2588
+ const refusal = refusalText(result);
2589
+ return result.view ? `${refusal}\n\n${renderStatus(context, result.view, { full: args.full })}` : refusal;
2590
+ }
2591
+ if (result.text) {
2592
+ const warnings = [...new Set(result.payload?.warnings ?? [])];
2593
+ return [result.text, ...(warnings.length > 0 ? [`warn: ${warnings.join(', ')}`] : [])].join('\n');
2594
+ }
2595
+ return renderStatus(context, result.view, { full: args.full });
2596
+ }
2597
+
2598
+ function refusalText(result) {
2599
+ const lines = [`refused: ${result.code}`, `reason: ${result.reason}`];
2600
+ if (result.cause) lines.push(`cause: ${result.cause}`);
2601
+ for (const detail of result.details ?? []) lines.push(`- ${detail}`);
2602
+ return lines.join('\n');
2603
+ }
2604
+
2605
+ // A hook answers on stdout in the event's own shape and exits 0. In a marked repository a failure of
2606
+ // the gate itself exits 2 with the failure as its reason, which both harnesses treat as blocking; a
2607
+ // failure is never an approval.
2608
+ function runHook(context, args) {
2609
+ const event = readHookEvent();
2610
+ const agent = hookAgent(event);
2611
+ const harness = context.harness;
2612
+ if (args.name === 'status') {
2613
+ const view = suiteView(context);
2614
+ const text = renderStatus(context, view, { full: false });
2615
+ const answer = sessionContext(`Engineering suite gate: ${context.gateCommand}\n${text}`, harness);
2616
+ return { ...(answer ? { stdout: JSON.stringify(answer) } : {}), exitCode: 0 };
2617
+ }
2618
+ if (args.name === 'can-write') {
2619
+ if (DISPATCH_TOOLS.test(agent.toolName ?? '')) return { exitCode: 0 };
2620
+ const paths = EDIT_TOOLS.test(agent.toolName ?? '') ? editedPaths(agent.toolInput) : [];
2621
+ const command = SHELL_TOOLS.test(agent.toolName ?? '') ? shellCommand(agent.toolInput) : null;
2622
+ if (command) noteHandoff(context, agent, command);
2623
+ if (paths.length === 0 && !command) return { exitCode: 0 };
2624
+ const result = commandCanWrite(context, { ...args, paths, command });
2625
+ if (result.ok) return { exitCode: 0 };
2626
+ return { stdout: JSON.stringify(toolDeny(refusalText(result), harness)), exitCode: 0 };
2627
+ }
2628
+ if (args.name === 'can-review') {
2629
+ const reviewer = reviewerFor(context, agent.toolInput);
2630
+ if (!reviewer) return { exitCode: 0 };
2631
+ const result = commandCanReview(context, { ...args, dimension: reviewer });
2632
+ if (result.ok) return { exitCode: 0 };
2633
+ return { stdout: JSON.stringify(toolDeny(refusalText(result), harness)), exitCode: 0 };
2634
+ }
2635
+ if (args.name === 'can-stop') {
2636
+ const result = commandCanStopHook(context, agent.reentrant);
2637
+ if (result.ok) return { exitCode: 0 };
2638
+ // Keep Stop compact. A full status leaks the reviewer brief into the builder conversation.
2639
+ return { stdout: JSON.stringify(stopBlock(renderStatus(context, result.view, { full: false }), harness)), exitCode: 0 };
2640
+ }
2641
+ if (args.name === 'fingerprint') {
2642
+ return { exitCode: 0 };
2643
+ }
2644
+ if (args.name === 'release') {
2645
+ const reviewer = reviewerFor(context, { subagent_type: agent.agentType }) ?? args.dimension;
2646
+ if (!reviewer) return { exitCode: 0 };
2647
+ commandRelease(context, { ...args, dimension: reviewer });
2648
+ return { exitCode: 0 };
2649
+ }
2650
+ throw new Error(`${args.name} has no hook form`);
2651
+ }
2652
+
2653
+ // The agent identifier and agent type exist only in hook input inside a subagent call, so the hook
2654
+ // records them for the gate command that call is about to run.
2655
+ function noteHandoff(context, agent, command) {
2656
+ if (!command.includes('ai-engineering-gate')) return;
2657
+ const verb = /ai-engineering-gate(?:\.mjs)?["']?\s+(\S+)/.exec(command)?.[1];
2658
+ if (!verb || !DOCUMENT_VERBS.has(verb)) return;
2659
+ const dimension = /--dimension\s+([A-Za-z0-9-]+)/.exec(command)?.[1] ?? null;
2660
+ recordHandoff(context, {
2661
+ session: agent.session,
2662
+ verb: verb === 'evidence' ? 'evidence' : verb,
2663
+ dimension,
2664
+ agent: agent.agent,
2665
+ agentType: agent.agentType,
2666
+ toolUseId: agent.toolUseId,
2667
+ command,
2668
+ });
2669
+ }
2670
+
2671
+ function reviewerFor(context, toolInput) {
2672
+ const type = toolInput?.subagent_type ?? toolInput?.agent_type ?? toolInput?.agentType ?? null;
2673
+ if (!type) return null;
2674
+ const member = context.members.find(one => memberAgentType(one) === type);
2675
+ return member?.dimension ?? null;
2676
+ }
2677
+
2678
+ function main(argv) {
2679
+ let args;
2680
+ try {
2681
+ args = parseArguments(argv);
2682
+ } catch (error) {
2683
+ process.stderr.write(`${error.message}\n`);
2684
+ return 1;
2685
+ }
2686
+ if (args.help || args.name === 'help') {
2687
+ process.stdout.write(`${HELP}\n`);
2688
+ return 0;
2689
+ }
2690
+ let context;
2691
+ try {
2692
+ context = buildContext({
2693
+ harness: args.harness,
2694
+ task: args.task,
2695
+ evidenceRoot: args.evidenceRoot,
2696
+ fromExport: args.fromExport,
2697
+ });
2698
+ } catch (error) {
2699
+ process.stderr.write(`gate-failure: ${error.message}\n`);
2700
+ return args.hook ? 2 : 1;
2701
+ }
2702
+ if (args.version) {
2703
+ process.stdout.write(`${context.version}\n`);
2704
+ return 0;
2705
+ }
2706
+ // Silence outside a marked repository, even on failure, so installing the hooks once for every
2707
+ // project costs nothing elsewhere.
2708
+ if (!context.marked) {
2709
+ if (args.json) process.stdout.write(`${JSON.stringify(silentJson(context, args.name), null, 2)}\n`);
2710
+ return 0;
2711
+ }
2712
+ if (args.hook) {
2713
+ try {
2714
+ const answer = runHook(context, args);
2715
+ if (answer.stdout) process.stdout.write(`${answer.stdout}\n`);
2716
+ return answer.exitCode;
2717
+ } catch (error) {
2718
+ process.stderr.write(`gate-failure: ${error.message}\n`);
2719
+ return 2;
2720
+ }
2721
+ }
2722
+ try {
2723
+ const result = runCommand(context, args);
2724
+ const text = render(context, args, result);
2725
+ if (text) process.stdout.write(`${text}\n`);
2726
+ return result.ok ? 0 : 2;
2727
+ } catch (error) {
2728
+ if (error.refusal) {
2729
+ const text = render(context, args, error.refusal);
2730
+ if (text) process.stdout.write(`${text}\n`);
2731
+ return 2;
2732
+ }
2733
+ if (args.json) {
2734
+ process.stdout.write(`${JSON.stringify({
2735
+ outputVersion: '1.0.0',
2736
+ command: args.name,
2737
+ gateVersion: context.version,
2738
+ marked: true,
2739
+ ok: false,
2740
+ completion: { complete: false, codes: ['gate-failure'] },
2741
+ error: error.message,
2742
+ }, null, 2)}\n`);
2743
+ } else {
2744
+ process.stderr.write(`gate-failure: ${error.message}\n`);
2745
+ }
2746
+ return 1;
2747
+ }
2748
+ }
2749
+
2750
+ function invokedAsCli() {
2751
+ if (!process.argv[1]) return false;
2752
+ try {
2753
+ return realpathSync(fileURLToPath(import.meta.url)) === realpathSync(resolve(process.argv[1]));
2754
+ } catch {
2755
+ return false;
2756
+ }
2757
+ }
2758
+
2759
+ if (invokedAsCli()) process.exitCode = main(process.argv.slice(2));
2760
+
2761
+ export { main };
2762
+