ai-engineering-loop 1.0.11 → 1.0.12

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 (49) hide show
  1. package/.agents/devil-advocate.md +15 -4
  2. package/.agents/judge.md +7 -3
  3. package/.agents/workflows/ai-engineering-loop.md +4 -2
  4. package/.claude/agents/devil-advocate.md +15 -4
  5. package/.claude/agents/judge.md +7 -3
  6. package/.claude/commands/ai-engineering-loop.md +1 -1
  7. package/.claude/skills/ai-engineering-loop/SKILL.md +6 -5
  8. package/.gemini/skills/ai-engineering-loop/SKILL.md +59 -0
  9. package/.grok/agents/devil-advocate.md +15 -4
  10. package/.grok/agents/judge.md +7 -3
  11. package/.grok/commands/ai-engineering-loop.md +1 -1
  12. package/.grok/skills/ai-engineering-loop/SKILL.md +8 -6
  13. package/README.md +31 -3
  14. package/README.npm.md +5 -2
  15. package/adapters/dot/README.md +27 -3
  16. package/adapters/dot/coreview.md +30 -11
  17. package/adapters/dot/mattermost.md +31 -19
  18. package/adapters/dot/skills/dot-dev-skill-router/SKILL.md +55 -0
  19. package/adapters/dot/skills/dot-dev-workflow/SKILL.md +118 -0
  20. package/agents/devil-advocate.md +3 -1
  21. package/agents/maker.md +13 -11
  22. package/agents/shared/devil-advocate.body.md +15 -4
  23. package/agents/shared/judge.body.md +7 -3
  24. package/bin/ai-engineering-loop.js +131 -22
  25. package/core/context-impact-assessment.md +2 -0
  26. package/core/definition-of-done.md +2 -0
  27. package/core/goal-contract.md +18 -4
  28. package/core/grill-policy.md +70 -0
  29. package/core/handoff-policy.md +44 -0
  30. package/core/judge-policy.md +4 -3
  31. package/core/project-initialization.md +2 -1
  32. package/core/repo-config-schema.md +15 -1
  33. package/core/root-cause-analysis.md +30 -0
  34. package/core/verification-loop.md +3 -1
  35. package/examples/initialization/discovery-trace.md +1 -1
  36. package/examples/initialization/generated-context.md +1 -1
  37. package/lib/orchestration.js +20 -2
  38. package/lib/sync-hosts.js +195 -0
  39. package/package.json +2 -1
  40. package/policies/finding-policy.md +7 -1
  41. package/policies/review-budget.md +1 -0
  42. package/policies/tdd-policy.md +32 -0
  43. package/scripts/init.sh +21 -0
  44. package/templates/repo-config/adr-readme.md +33 -0
  45. package/templates/repo-config/glossary.md +18 -0
  46. package/tests/living-context.test.js +46 -0
  47. package/tests/orchestration.test.js +80 -0
  48. package/tests/skill-host-compat.test.js +83 -0
  49. package/tests/sync-hosts.test.js +119 -0
@@ -320,6 +320,7 @@ function validateFindingLedger(ledger) {
320
320
  const validSeverities = ['BLOCKER', 'HIGH', 'MEDIUM', 'LOW'];
321
321
  const validValidities = ['VALID', 'INVALID'];
322
322
  const validDispositions = ['STRONG', 'ACCEPTABLE', 'WEAK'];
323
+ const validAxes = ['spec', 'standards'];
323
324
 
324
325
  for (let i = 0; i < ledger.findings.length; i++) {
325
326
  const f = ledger.findings[i];
@@ -327,6 +328,10 @@ function validateFindingLedger(ledger) {
327
328
  return { valid: false, reason: `Finding at index ${i} missing required id, location, failureScenario, or evidence` };
328
329
  }
329
330
 
331
+ if (f.axis != null && !validAxes.includes(f.axis)) {
332
+ return { valid: false, reason: `Invalid axis at index ${i}: ${f.axis}. Must be spec or standards` };
333
+ }
334
+
330
335
  if (!validSeverities.includes(f.severity)) {
331
336
  return { valid: false, reason: `Invalid severity at index ${i}: ${f.severity}. Must be: ${validSeverities.join(', ')}` };
332
337
  }
@@ -348,7 +353,19 @@ function validateFindingLedger(ledger) {
348
353
  }
349
354
 
350
355
  /**
351
- * 8. Compute Judge Verdict based primarily on Validity + Severity
356
+ * Spec VALID BLOCKER/HIGH blocks. Missing axis is spec.
357
+ * Standards VALID BLOCKER/HIGH blocks only when hardConvention is true.
358
+ */
359
+ function isBlockingFinding(finding) {
360
+ if (!finding || finding.validity !== 'VALID') return false;
361
+ if (finding.severity !== 'BLOCKER' && finding.severity !== 'HIGH') return false;
362
+ const axis = finding.axis || 'spec';
363
+ if (axis === 'standards' && finding.hardConvention !== true) return false;
364
+ return true;
365
+ }
366
+
367
+ /**
368
+ * 8. Compute Judge Verdict based primarily on Validity + Severity, then review axis
352
369
  */
353
370
  function computeJudgeVerdict({
354
371
  goalContract,
@@ -380,7 +397,7 @@ function computeJudgeVerdict({
380
397
  continue;
381
398
  }
382
399
 
383
- if (f.severity === 'BLOCKER' || f.severity === 'HIGH') {
400
+ if (isBlockingFinding(f)) {
384
401
  blockingFindings.push(f);
385
402
  } else {
386
403
  acceptableFindings.push(f);
@@ -581,6 +598,7 @@ module.exports = {
581
598
  buildReviewContextBarrier,
582
599
  validateVerificationEvidence,
583
600
  validateFindingLedger,
601
+ isBlockingFinding,
584
602
  computeJudgeVerdict,
585
603
  formatExecutionReport,
586
604
  detectGrokRuntime,
@@ -0,0 +1,195 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const os = require('os');
5
+ const path = require('path');
6
+
7
+ function packageRoot() {
8
+ return path.join(__dirname, '..');
9
+ }
10
+
11
+ function homeDir(env = process.env) {
12
+ if (env.AEL_HOME) return env.AEL_HOME;
13
+ return os.homedir();
14
+ }
15
+
16
+ function applyGrokSkillOverlay(content) {
17
+ const match = content.match(/^---\n([\s\S]*?)\n---\n/);
18
+ if (!match) return content;
19
+ const yaml = match[1];
20
+ if (/^user-invocable:\s*true\s*$/m.test(yaml)) return content;
21
+ const nextYaml = `${yaml.replace(/\s*$/, '')}\nuser-invocable: true`;
22
+ return `---\n${nextYaml}\n---\n${content.slice(match[0].length)}`;
23
+ }
24
+
25
+ function hostFileMap(home) {
26
+ const claude = path.join(home, '.claude');
27
+ const grok = path.join(home, '.grok');
28
+ const gemini = path.join(home, '.gemini');
29
+ const agents = path.join(home, '.agents');
30
+ return [
31
+ {
32
+ id: 'claude',
33
+ root: claude,
34
+ files: [
35
+ { src: '.claude/skills/ai-engineering-loop/SKILL.md', dest: path.join(claude, 'skills/ai-engineering-loop/SKILL.md'), mode: 'upsert' },
36
+ { src: '.claude/agents/devil-advocate.md', dest: path.join(claude, 'agents/devil-advocate.md'), mode: 'upsert' },
37
+ { src: '.claude/agents/judge.md', dest: path.join(claude, 'agents/judge.md'), mode: 'upsert' },
38
+ { src: '.claude/commands/ai-engineering-loop.md', dest: path.join(claude, 'commands/ai-engineering-loop.md'), mode: 'upsert' }
39
+ ]
40
+ },
41
+ {
42
+ id: 'grok',
43
+ root: grok,
44
+ files: [
45
+ { src: '.grok/skills/ai-engineering-loop/SKILL.md', dest: path.join(grok, 'skills/ai-engineering-loop/SKILL.md'), mode: 'upsert', grokSkillOverlay: true },
46
+ { src: '.grok/agents/devil-advocate.md', dest: path.join(grok, 'agents/devil-advocate.md'), mode: 'upsert' },
47
+ { src: '.grok/agents/judge.md', dest: path.join(grok, 'agents/judge.md'), mode: 'upsert' },
48
+ { src: '.grok/commands/ai-engineering-loop.md', dest: path.join(grok, 'commands/ai-engineering-loop.md'), mode: 'upsert' }
49
+ ]
50
+ },
51
+ {
52
+ id: 'gemini',
53
+ root: gemini,
54
+ files: [
55
+ { src: '.gemini/skills/ai-engineering-loop/SKILL.md', dest: path.join(gemini, 'config/skills/ai-engineering-loop/SKILL.md'), mode: 'upsert' }
56
+ ]
57
+ },
58
+ {
59
+ id: 'antigravity',
60
+ root: agents,
61
+ files: [
62
+ { src: '.agents/devil-advocate.md', dest: path.join(agents, 'devil-advocate.md'), mode: 'upsert' },
63
+ { src: '.agents/judge.md', dest: path.join(agents, 'judge.md'), mode: 'upsert' },
64
+ { src: '.agents/workflows/ai-engineering-loop.md', dest: path.join(agents, 'workflows/ai-engineering-loop.md'), mode: 'upsert' }
65
+ ]
66
+ },
67
+ {
68
+ id: 'dot',
69
+ root: null,
70
+ files: [
71
+ { src: 'adapters/dot/skills/dot-dev-skill-router/SKILL.md', dest: path.join(claude, 'skills/dot-dev-skill-router/SKILL.md'), mode: 'update-if-exists', hostRoot: claude },
72
+ { src: 'adapters/dot/skills/dot-dev-workflow/SKILL.md', dest: path.join(claude, 'skills/dot-dev-workflow/SKILL.md'), mode: 'update-if-exists', hostRoot: claude },
73
+ { src: 'adapters/dot/skills/dot-dev-skill-router/SKILL.md', dest: path.join(grok, 'skills/dot-dev-skill-router/SKILL.md'), mode: 'update-if-exists', hostRoot: grok, grokSkillOverlay: true },
74
+ { src: 'adapters/dot/skills/dot-dev-workflow/SKILL.md', dest: path.join(grok, 'skills/dot-dev-workflow/SKILL.md'), mode: 'update-if-exists', hostRoot: grok, grokSkillOverlay: true },
75
+ { src: 'adapters/dot/skills/dot-dev-skill-router/SKILL.md', dest: path.join(gemini, 'config/skills/dot-dev-skill-router/SKILL.md'), mode: 'update-if-exists', hostRoot: gemini },
76
+ { src: 'adapters/dot/skills/dot-dev-workflow/SKILL.md', dest: path.join(gemini, 'config/skills/dot-dev-workflow/SKILL.md'), mode: 'update-if-exists', hostRoot: gemini }
77
+ ]
78
+ }
79
+ ];
80
+ }
81
+
82
+ function destExists(dest) {
83
+ try {
84
+ fs.lstatSync(dest);
85
+ return true;
86
+ } catch (e) {
87
+ return false;
88
+ }
89
+ }
90
+
91
+ function isSymlink(dest) {
92
+ try {
93
+ return fs.lstatSync(dest).isSymbolicLink();
94
+ } catch (e) {
95
+ return false;
96
+ }
97
+ }
98
+
99
+ function planHostSync({ packageRoot: root = packageRoot(), home = homeDir() } = {}) {
100
+ const results = [];
101
+ for (const group of hostFileMap(home)) {
102
+ for (const file of group.files) {
103
+ const srcAbs = path.join(root, file.src);
104
+ const hostRoot = file.hostRoot || group.root;
105
+ const item = {
106
+ id: group.id,
107
+ src: file.src,
108
+ dest: file.dest,
109
+ mode: file.mode
110
+ };
111
+ if (hostRoot && !fs.existsSync(hostRoot)) {
112
+ results.push({ ...item, action: 'skip', reason: 'host-missing' });
113
+ continue;
114
+ }
115
+ if (!fs.existsSync(srcAbs)) {
116
+ results.push({ ...item, action: 'skip', reason: 'src-missing' });
117
+ continue;
118
+ }
119
+ if (isSymlink(file.dest)) {
120
+ results.push({ ...item, action: 'skip', reason: 'symlink' });
121
+ continue;
122
+ }
123
+ if (file.mode === 'update-if-exists' && !destExists(file.dest)) {
124
+ results.push({ ...item, action: 'skip', reason: 'not-installed' });
125
+ continue;
126
+ }
127
+ let content = fs.readFileSync(srcAbs, 'utf8');
128
+ if (file.grokSkillOverlay) content = applyGrokSkillOverlay(content);
129
+ if (destExists(file.dest) && fs.readFileSync(file.dest, 'utf8') === content) {
130
+ results.push({ ...item, action: 'current' });
131
+ continue;
132
+ }
133
+ results.push({ ...item, action: 'copy', content });
134
+ }
135
+ }
136
+ return results;
137
+ }
138
+
139
+ function applyHostSync({ packageRoot: root = packageRoot(), home = homeDir(), dryRun = false } = {}) {
140
+ const plan = planHostSync({ packageRoot: root, home });
141
+ const applied = [];
142
+ for (const item of plan) {
143
+ if (item.action !== 'copy') {
144
+ applied.push(item);
145
+ continue;
146
+ }
147
+ if (!dryRun) {
148
+ fs.mkdirSync(path.dirname(item.dest), { recursive: true });
149
+ fs.writeFileSync(item.dest, item.content);
150
+ }
151
+ applied.push({ ...item, content: undefined, dryRun });
152
+ }
153
+ return applied;
154
+ }
155
+
156
+ function summarizeHostSync(results) {
157
+ const summary = { copy: 0, current: 0, skip: 0 };
158
+ for (const item of results) {
159
+ if (item.action === 'copy') summary.copy += 1;
160
+ else if (item.action === 'current') summary.current += 1;
161
+ else summary.skip += 1;
162
+ }
163
+ return summary;
164
+ }
165
+
166
+ function formatHostSyncReport(results, { version, dryRun = false } = {}) {
167
+ const summary = summarizeHostSync(results);
168
+ const lines = [
169
+ `Host skills sync (v${version || 'unknown'})${dryRun ? ' [dry-run]' : ''}`,
170
+ `- copy: ${summary.copy} current: ${summary.current} skip: ${summary.skip}`
171
+ ];
172
+ for (const item of results) {
173
+ if (item.action === 'skip' && item.reason === 'host-missing') continue;
174
+ if (item.action === 'skip' && item.reason === 'not-installed') continue;
175
+ const rel = item.dest;
176
+ if (item.action === 'copy') lines.push(` ${item.id.padEnd(12)} ${dryRun ? 'would-copy' : 'copied'} ${rel}`);
177
+ else if (item.action === 'current') lines.push(` ${item.id.padEnd(12)} current ${rel}`);
178
+ else lines.push(` ${item.id.padEnd(12)} skip(${item.reason}) ${rel}`);
179
+ }
180
+ if (summary.copy > 0 && !dryRun) {
181
+ lines.push('Skill text already loaded in this session stays stale until you start a new session.');
182
+ }
183
+ return lines.join('\n');
184
+ }
185
+
186
+ module.exports = {
187
+ packageRoot,
188
+ homeDir,
189
+ applyGrokSkillOverlay,
190
+ hostFileMap,
191
+ planHostSync,
192
+ applyHostSync,
193
+ summarizeHostSync,
194
+ formatHostSyncReport
195
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-engineering-loop",
3
- "version": "1.0.11",
3
+ "version": "1.0.12",
4
4
  "description": "A reusable, framework-agnostic AI Engineering Operating System for autonomous coding agents.",
5
5
  "main": "bin/ai-engineering-loop.js",
6
6
  "bin": {
@@ -21,6 +21,7 @@
21
21
  "scripts/",
22
22
  ".agents/",
23
23
  ".grok/",
24
+ ".gemini/skills/",
24
25
  ".claude/agents/",
25
26
  ".claude/commands/",
26
27
  ".claude/skills/",
@@ -20,6 +20,10 @@ id: "<CATEGORY_PREFIX>-<3_DIGIT_NUMBER>" # e.g. COR-001, SEC-002, PERF-001
20
20
  title: "<Short, descriptive summary of the problem>"
21
21
  topic: "<correctness | error_handling | security | concurrency | performance | maintainability | testing_gaps>"
22
22
 
23
+ # Review axis (do not merge Spec with Standards)
24
+ axis: "<spec | standards>" # spec: Goal Contract / runtime defect | standards: conventions.md or smell baseline
25
+ hardConvention: false # true only when conventions.md states a hard rule this hunk violates
26
+
23
27
  # Axis 1: Factual Validity
24
28
  validity: "<VALID | INVALID>" # VALID: Real technical flaw | INVALID: Reviewer hallucination or misunderstanding
25
29
 
@@ -66,5 +70,7 @@ Review findings are hypotheses, not absolute truths.
66
70
  - **`INVALID` (Dismissed)**: The reviewer made an assumption disproved by the codebase, referenced a non-existent API, or flagged intentional behavior.
67
71
 
68
72
  > [!IMPORTANT]
69
- > **Decision Rule**: The Judge Agent evaluates findings primarily on **Validity + Severity**.
73
+ > **Decision Rule**: The Judge Agent evaluates findings primarily on **Validity + Severity**, then on **review axis**.
70
74
  > A reviewer's subjective disposition (`STRONG`, `ACCEPTABLE`, `WEAK`) **never overrides evidence**. An `INVALID` finding cannot block delivery, even if the reviewer labeled it `STRONG` or `BLOCKER`.
75
+ > Spec (`axis: spec`) VALID BLOCKER/HIGH forces `ITERATE`. Standards (`axis: standards`) VALID BLOCKER/HIGH forces `ITERATE` only when `hardConvention: true`. Other Standards findings are tradeoffs. Missing `axis` is treated as `spec`.
76
+ > Do not merge Spec and Standards into one ranking. A change can pass one axis and fail the other.
@@ -22,6 +22,7 @@ Host files (`.claude/agents/`, `.grok/agents/`, `.agents/`) may differ only in Y
22
22
  - At most **8** tool calls, then emit the Finding Ledger.
23
23
  - Read the diff file first. Do not run `git diff` if that path was given.
24
24
  - Open at most **8** files that appear in the diff. Prefer quoting a hunk over opening the whole file.
25
+ - Report Spec and Standards as separate findings. Do not spawn children to split axes. Do not merge the two axes.
25
26
 
26
27
  ## Judge
27
28
 
@@ -0,0 +1,32 @@
1
+ # TDD Policy (Maker, Stages 4–5)
2
+
3
+ Maker implements with a red → green loop at **pre-agreed seams**. Refactoring is not part of this loop; it belongs to a later Goal Contract or to review.
4
+
5
+ Canonical Maker rules: `agents/maker.md`. Verification evidence: `core/verification-loop.md`.
6
+
7
+ ## Seams
8
+
9
+ A seam is the public interface where you observe behavior without reaching inside. Seams are named in the Goal Contract during Stage 1. Prefer existing seams. The ideal number of new seams is zero; the next best is one.
10
+
11
+ No test is written at an unconfirmed seam. If the contract omitted seams and grill was skipped, name them in the plan (Stage 3) and freeze them in the contract before the first red test.
12
+
13
+ ## What a good test is
14
+
15
+ The test reads like a specification of behavior at the seam. Names use `.ai-engineering-loop/glossary.md`. The test survives an internal rewrite. Expected values come from the Goal Contract or a known-good literal, not from re-running the implementation.
16
+
17
+ ## Loop
18
+
19
+ 1. **Red.** Write one failing test for one AC slice. Confirm it fails for the right reason.
20
+ 2. **Green.** Write the smallest production change that passes that test.
21
+ 3. Repeat one slice at a time (vertical). Do not write the whole suite first.
22
+ 4. After slices covering AC-1..N, run the full verification commands in `.ai-engineering-loop/verification.md` and keep the Evidence Contract.
23
+
24
+ ## Forbidden tests
25
+
26
+ - **Implementation-coupled:** mocks internal collaborators, tests private methods, or asserts through a side channel (raw DB) instead of the seam.
27
+ - **Tautological:** expected value is computed the same way as the code (`expect(add(a,b)).toBe(a+b)`).
28
+ - **Horizontal slicing:** all tests first, then all implementation.
29
+
30
+ ## Evidence
31
+
32
+ Stage 5 still requires command, exit code 0, stdout, test counts, and assertionEvidence mapped to AC ids. "We did TDD" is not evidence. A red-then-green story without logs is invalid.
package/scripts/init.sh CHANGED
@@ -81,5 +81,26 @@ cat <<EOF > .ai-engineering-loop/adapter.md
81
81
  - **default_target_branch**: "main"
82
82
  EOF
83
83
 
84
+ cat <<EOF > .ai-engineering-loop/glossary.md
85
+ # Ubiquitous Language
86
+
87
+ One term per concept. Use these words in Goal Contracts, tests, and code names.
88
+
89
+ ## Terms
90
+
91
+ | Term | Meaning | Do not say |
92
+ |---|---|---|
93
+ | Goal Contract | Frozen Stage 1 acceptance document | "the prompt" |
94
+ | Seam | Public interface under test | "the internals" |
95
+ EOF
96
+
97
+ mkdir -p .ai-engineering-loop/adrs
98
+ cat <<EOF > .ai-engineering-loop/adrs/README.md
99
+ # Architecture Decision Records
100
+
101
+ Write one ADR when Stage 1 grill settles a load-bearing choice.
102
+ File name: NNN-short-kebab-title.md
103
+ EOF
104
+
84
105
  echo -e "\033[1;32m✓ .ai-engineering-loop/ successfully initialized!\033[0m"
85
106
  echo -e "You can now run the AI Engineering Loop on this repository."
@@ -0,0 +1,33 @@
1
+ # Architecture Decision Records
2
+
3
+ Hard decisions that would otherwise live only in chat. Write one ADR when Stage 1 grill settles a choice that future agents must not re-litigate.
4
+
5
+ ## When to write
6
+
7
+ - Two viable designs were on the table and one was chosen
8
+ - A boundary, schema, or public seam is now load-bearing
9
+ - The Goal Contract's Technical Constraints need a durable why
10
+
11
+ Do not write an ADR for a typo fix, a local rename, or a test-only change.
12
+
13
+ ## File name
14
+
15
+ `NNN-short-kebab-title.md` in this directory. Increment `NNN`.
16
+
17
+ ## Template
18
+
19
+ ```markdown
20
+ # ADR NNN: <title>
21
+
22
+ ## Status
23
+ Accepted | Superseded by ADR NNN
24
+
25
+ ## Context
26
+ What forced a choice.
27
+
28
+ ## Decision
29
+ What we chose, in glossary terms.
30
+
31
+ ## Consequences
32
+ What becomes easier, harder, or forbidden.
33
+ ```
@@ -0,0 +1,18 @@
1
+ # Ubiquitous Language
2
+
3
+ One term per concept. Agents and humans use these words in Goal Contracts, tests, code names, and review. Prefer a short term over a 20-word paraphrase.
4
+
5
+ Update this file during Stage 1 grill when a term is coined or corrected. Do not invent parallel synonyms.
6
+
7
+ ## Terms
8
+
9
+ | Term | Meaning | Do not say |
10
+ |---|---|---|
11
+ | Goal Contract | Frozen Stage 1 acceptance document | "the prompt", "the ticket vibe" |
12
+ | Seam | Public interface under test | "the internals", "the private helper" |
13
+
14
+ ## How to add a term
15
+
16
+ 1. Can this idea be named in 1–4 words?
17
+ 2. Does an existing term already cover it? Reuse it.
18
+ 3. Add one row. Use the term in the next Goal Contract.
@@ -0,0 +1,46 @@
1
+ const test = require('node:test');
2
+ const assert = require('node:assert');
3
+ const fs = require('fs');
4
+ const os = require('os');
5
+ const path = require('path');
6
+ const { execFileSync } = require('child_process');
7
+
8
+ const CLI = path.join(__dirname, '..', 'bin', 'ai-engineering-loop.js');
9
+
10
+ function tmpRepo() {
11
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ael-'));
12
+ fs.writeFileSync(
13
+ path.join(dir, 'package.json'),
14
+ JSON.stringify({ name: 'fixture', scripts: { test: 'node -e ""' } }, null, 2)
15
+ );
16
+ return dir;
17
+ }
18
+
19
+ test('init writes glossary.md and adrs/README.md', () => {
20
+ const dir = tmpRepo();
21
+ execFileSync('node', [CLI, 'init'], { cwd: dir, encoding: 'utf8' });
22
+ const glossary = path.join(dir, '.ai-engineering-loop', 'glossary.md');
23
+ const adr = path.join(dir, '.ai-engineering-loop', 'adrs', 'README.md');
24
+ assert.ok(fs.existsSync(glossary));
25
+ assert.ok(fs.existsSync(adr));
26
+ assert.match(fs.readFileSync(glossary, 'utf8'), /Ubiquitous Language/);
27
+ assert.match(fs.readFileSync(adr, 'utf8'), /Architecture Decision Records/);
28
+ });
29
+
30
+ test('repair fills missing glossary without overwriting a filled glossary or architecture', () => {
31
+ const dir = tmpRepo();
32
+ execFileSync('node', [CLI, 'init'], { cwd: dir, encoding: 'utf8' });
33
+ const glossary = path.join(dir, '.ai-engineering-loop', 'glossary.md');
34
+ const architecture = path.join(dir, '.ai-engineering-loop', 'architecture.md');
35
+ fs.writeFileSync(glossary, '# Custom glossary\n- Foo: bar\n');
36
+ const archBefore = fs.readFileSync(architecture, 'utf8');
37
+ fs.unlinkSync(glossary);
38
+ execFileSync('node', [CLI, 'init'], { cwd: dir, encoding: 'utf8' });
39
+ assert.ok(fs.existsSync(glossary));
40
+ assert.strictEqual(fs.readFileSync(architecture, 'utf8'), archBefore);
41
+ fs.writeFileSync(glossary, '# Custom glossary\n- Foo: bar\n');
42
+ fs.unlinkSync(architecture);
43
+ execFileSync('node', [CLI, 'init'], { cwd: dir, encoding: 'utf8' });
44
+ assert.match(fs.readFileSync(glossary, 'utf8'), /Foo: bar/);
45
+ assert.ok(fs.existsSync(architecture));
46
+ });
@@ -286,3 +286,83 @@ test('D, F, G. Judge Decision Matrix: VALID BLOCKER forces ITERATE; INVALID does
286
286
  assert.strictEqual(verdictTradeoff.verdict, 'PASS');
287
287
  assert.strictEqual(verdictTradeoff.acceptableTradeoffs.length, 1);
288
288
  });
289
+
290
+ test('Standards axis BLOCKER does not iterate unless hardConvention is true', () => {
291
+ const goalContract = { objective: 'Test' };
292
+ const verificationEvidence = {
293
+ command: 'npm test',
294
+ executionIdentity: 'exec-1',
295
+ startTime: '2026-08-25T10:00:00Z',
296
+ endTime: '2026-08-25T10:00:02Z',
297
+ exitCode: 0,
298
+ stdout: '10 passed',
299
+ timeoutStatus: 'COMPLETED',
300
+ testCounts: { passed: 10, failed: 0 }
301
+ };
302
+
303
+ const smellLedger = {
304
+ findings: [
305
+ {
306
+ id: 'DA-S1',
307
+ axis: 'standards',
308
+ hardConvention: false,
309
+ severity: 'HIGH',
310
+ validity: 'VALID',
311
+ disposition: 'STRONG',
312
+ location: 'src/pay.ts#L4',
313
+ failureScenario: 'Duplicated Code across two hunks',
314
+ evidence: 'Same lock shape in two functions'
315
+ }
316
+ ]
317
+ };
318
+
319
+ const smellVerdict = computeJudgeVerdict({
320
+ goalContract,
321
+ verificationEvidence,
322
+ findingLedger: smellLedger,
323
+ activeIteration: 1
324
+ });
325
+ assert.strictEqual(smellVerdict.verdict, 'PASS');
326
+ assert.strictEqual(smellVerdict.acceptableTradeoffs.length, 1);
327
+
328
+ const hardLedger = {
329
+ findings: [
330
+ {
331
+ id: 'DA-S2',
332
+ axis: 'standards',
333
+ hardConvention: true,
334
+ severity: 'HIGH',
335
+ validity: 'VALID',
336
+ disposition: 'STRONG',
337
+ location: 'src/pay.ts#L4',
338
+ failureScenario: 'conventions.md forbids any',
339
+ evidence: 'any used in public seam',
340
+ concreteAlternativeDiff: '- x: any\n+ x: unknown'
341
+ }
342
+ ]
343
+ };
344
+
345
+ const hardVerdict = computeJudgeVerdict({
346
+ goalContract,
347
+ verificationEvidence,
348
+ findingLedger: hardLedger,
349
+ activeIteration: 1
350
+ });
351
+ assert.strictEqual(hardVerdict.verdict, 'ITERATE');
352
+ assert.strictEqual(hardVerdict.blockingFindings.length, 1);
353
+ });
354
+
355
+ test('Finding ledger rejects unknown axis; missing axis remains valid', () => {
356
+ const base = {
357
+ id: 'DA-1',
358
+ severity: 'LOW',
359
+ validity: 'VALID',
360
+ disposition: 'ACCEPTABLE',
361
+ location: 'src/a.ts#L1',
362
+ failureScenario: 'n',
363
+ evidence: 'e'
364
+ };
365
+ assert.strictEqual(validateFindingLedger({ findings: [base] }).valid, true);
366
+ assert.strictEqual(validateFindingLedger({ findings: [{ ...base, axis: 'spec' }] }).valid, true);
367
+ assert.strictEqual(validateFindingLedger({ findings: [{ ...base, axis: 'style' }] }).valid, false);
368
+ });
@@ -144,3 +144,86 @@ test('Claude and Grok parent skills pass Judge a 4-call ledger-only prompt', ()
144
144
  assert.match(claude, /4 tool calls/);
145
145
  assert.match(grok, /4 tool calls/);
146
146
  });
147
+
148
+ test('Gemini Antigravity skill matches the loop and never uses Grok spawn keys or browser_subagent as reviewer', () => {
149
+ const gemini = readRepo('.gemini/skills/ai-engineering-loop/SKILL.md');
150
+ const { fm } = parseFrontmatter(gemini, 'gemini skill');
151
+ assert.doesNotMatch(fm, /^description:\s*>-?/m);
152
+ assert.doesNotMatch(gemini, /```mermaid/);
153
+ assert.doesNotMatch(gemini, /\$\\/);
154
+ assert.doesNotMatch(gemini, /spawn_subagent/);
155
+ assert.doesNotMatch(gemini, /capability_mode/);
156
+ assert.doesNotMatch(gemini, /resume_from/);
157
+ assert.match(gemini, /browser_subagent/);
158
+ assert.match(gemini, /CONTEXT_ISOLATION_ONLY/);
159
+ assert.match(gemini, /grill-policy/);
160
+ assert.match(gemini, /tdd-policy/);
161
+ assert.match(gemini, /glossary\.md/);
162
+ assert.match(gemini, /Spec and Standards/);
163
+ assert.match(gemini, /task-impact-inquiry/);
164
+ assert.match(gemini, /8 tool calls/);
165
+ assert.match(gemini, /4 tool calls/);
166
+ assert.match(gemini, /TRUE_INDEPENDENT_AGENT/);
167
+ });
168
+
169
+ test('DOT adapter locates task-impact-inquiry on Antigravity Gemini skills, not Claude', () => {
170
+ const readme = readRepo('adapters/dot/README.md');
171
+ assert.match(readme, /~\/\.gemini\/config\/skills/);
172
+ assert.match(readme, /task-impact-inquiry/);
173
+ assert.match(readme, /not from `~\/\.claude\/skills\/`/);
174
+ assert.doesNotMatch(readme, /and `~\/\.claude\/skills\/`/);
175
+ const grill = readRepo('core/grill-policy.md');
176
+ assert.match(grill, /task-impact-inquiry/);
177
+ assert.match(grill, /Do not skip grill/);
178
+ const wf = readRepo('.agents/workflows/ai-engineering-loop.md');
179
+ assert.match(wf, /task-impact-inquiry/);
180
+ });
181
+
182
+ test('DOT router sends commit-bound work to ai-engineering-loop; workflow is Stage 8 only', () => {
183
+ const router = readRepo('adapters/dot/skills/dot-dev-skill-router/SKILL.md');
184
+ const delivery = readRepo('adapters/dot/skills/dot-dev-workflow/SKILL.md');
185
+ const readme = readRepo('adapters/dot/README.md');
186
+ assert.match(router, /ai-engineering-loop/);
187
+ assert.match(router, /Do not use dot-dev-workflow as a parallel engineering OS/);
188
+ assert.doesNotMatch(router, /task-impact-inquiry` → `dot-dev-workflow/);
189
+ assert.match(delivery, /Not a substitute for ai-engineering-loop/);
190
+ assert.match(delivery, /Judge `PASS`/);
191
+ assert.doesNotMatch(delivery, /```mermaid/);
192
+ assert.match(readme, /run `ai-engineering-loop`/);
193
+ assert.match(readme, /Stage 8 delivery only/);
194
+ assert.doesNotMatch(readme, /Pre-commit multi-round adversarial review gate \(Phase 6\)/);
195
+ });
196
+
197
+ test('Host skills Stage 0 runs sync-hosts before status', () => {
198
+ for (const rel of [
199
+ '.claude/skills/ai-engineering-loop/SKILL.md',
200
+ '.grok/skills/ai-engineering-loop/SKILL.md',
201
+ '.gemini/skills/ai-engineering-loop/SKILL.md',
202
+ '.agents/workflows/ai-engineering-loop.md'
203
+ ]) {
204
+ const text = readRepo(rel);
205
+ assert.match(text, /sync-hosts/, rel);
206
+ }
207
+ });
208
+
209
+ test('Host skills absorb grill, TDD, glossary, and two-axis review without splitting the loop', () => {
210
+ for (const rel of [
211
+ '.claude/skills/ai-engineering-loop/SKILL.md',
212
+ '.grok/skills/ai-engineering-loop/SKILL.md',
213
+ '.agents/workflows/ai-engineering-loop.md'
214
+ ]) {
215
+ const text = readRepo(rel);
216
+ assert.match(text, /grill-policy/, rel);
217
+ assert.match(text, /tdd-policy/, rel);
218
+ assert.match(text, /glossary\.md/, rel);
219
+ assert.match(text, /Spec and Standards/, rel);
220
+ assert.match(text, /adapter_type: dot/, rel);
221
+ }
222
+ const da = readRepo('agents/shared/devil-advocate.body.md');
223
+ assert.match(da, /"axis": "spec"/);
224
+ assert.match(da, /hardConvention/);
225
+ assert.match(da, /standards/);
226
+ const judge = readRepo('agents/shared/judge.body.md');
227
+ assert.match(judge, /hardConvention is true/);
228
+ assert.match(judge, /Do not merge Spec and Standards/);
229
+ });