ai-engineering-loop 1.0.10 → 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 +18 -3
  2. package/.agents/judge.md +8 -4
  3. package/.agents/workflows/ai-engineering-loop.md +7 -7
  4. package/.claude/agents/devil-advocate.md +17 -2
  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 +31 -32
  10. package/.grok/agents/judge.md +9 -5
  11. package/.grok/commands/ai-engineering-loop.md +1 -1
  12. package/.grok/skills/ai-engineering-loop/SKILL.md +9 -7
  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 +61 -0
  23. package/agents/shared/judge.body.md +38 -0
  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 +8 -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 +117 -0
  49. package/tests/sync-hosts.test.js +119 -0
@@ -79,6 +79,26 @@ test('Grok skill may use spawn_subagent; Claude skill must not', () => {
79
79
  assert.doesNotMatch(claude, /spawn_subagent/);
80
80
  });
81
81
 
82
+ function bodyAfterFrontmatter(content) {
83
+ const match = content.match(/^---\n[\s\S]*?\n---\n+/);
84
+ assert.ok(match, 'missing frontmatter');
85
+ return content.slice(match[0].length).replace(/\s+$/, '');
86
+ }
87
+
88
+ test('DA body is identical on Claude Code, Grok, and Antigravity', () => {
89
+ const shared = readRepo('agents/shared/devil-advocate.body.md').replace(/\s+$/, '');
90
+ for (const rel of ['.claude/agents/devil-advocate.md', '.grok/agents/devil-advocate.md', '.agents/devil-advocate.md']) {
91
+ assert.strictEqual(bodyAfterFrontmatter(readRepo(rel)), shared, rel);
92
+ }
93
+ });
94
+
95
+ test('Judge body is identical on Claude Code, Grok, and Antigravity', () => {
96
+ const shared = readRepo('agents/shared/judge.body.md').replace(/\s+$/, '');
97
+ for (const rel of ['.claude/agents/judge.md', '.grok/agents/judge.md', '.agents/judge.md']) {
98
+ assert.strictEqual(bodyAfterFrontmatter(readRepo(rel)), shared, rel);
99
+ }
100
+ });
101
+
82
102
  test('Judge budget is the same on Claude Code, Grok, and Antigravity', () => {
83
103
  const hosts = [
84
104
  '.claude/agents/judge.md',
@@ -95,6 +115,20 @@ test('Judge budget is the same on Claude Code, Grok, and Antigravity', () => {
95
115
  }
96
116
  });
97
117
 
118
+ test('Parent DA and Judge prompts match across hosts', () => {
119
+ const daPrompt = 'at most 8 tool calls; read the diff file; skip css and generated files';
120
+ const judgePrompt = 'at most 4 tool calls; ledger and contract only; skip css; do not re-review the whole diff';
121
+ for (const rel of [
122
+ '.claude/skills/ai-engineering-loop/SKILL.md',
123
+ '.grok/skills/ai-engineering-loop/SKILL.md',
124
+ '.agents/workflows/ai-engineering-loop.md'
125
+ ]) {
126
+ const text = readRepo(rel);
127
+ assert.match(text, new RegExp(daPrompt.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')), rel);
128
+ assert.match(text, new RegExp(judgePrompt.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')), rel);
129
+ }
130
+ });
131
+
98
132
  test('Antigravity workflow waits and never uses browser_subagent as reviewer', () => {
99
133
  const wf = readRepo('.agents/workflows/ai-engineering-loop.md');
100
134
  parseFrontmatter(wf, 'antigravity workflow');
@@ -110,3 +144,86 @@ test('Claude and Grok parent skills pass Judge a 4-call ledger-only prompt', ()
110
144
  assert.match(claude, /4 tool calls/);
111
145
  assert.match(grok, /4 tool calls/);
112
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
+ });
@@ -0,0 +1,119 @@
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 {
9
+ applyGrokSkillOverlay,
10
+ planHostSync,
11
+ applyHostSync,
12
+ summarizeHostSync
13
+ } = require('../lib/sync-hosts.js');
14
+
15
+ const ROOT = path.join(__dirname, '..');
16
+ const CLI = path.join(ROOT, 'bin', 'ai-engineering-loop.js');
17
+
18
+ function tmpHome() {
19
+ return fs.mkdtempSync(path.join(os.tmpdir(), 'ael-home-'));
20
+ }
21
+
22
+ test('applyGrokSkillOverlay inserts user-invocable once', () => {
23
+ const raw = '---\nname: demo\ndescription: x\n---\n\n# Body\n';
24
+ const once = applyGrokSkillOverlay(raw);
25
+ assert.match(once, /user-invocable: true/);
26
+ const twice = applyGrokSkillOverlay(once);
27
+ assert.strictEqual(twice, once);
28
+ });
29
+
30
+ test('planHostSync skips everything when no host roots exist', () => {
31
+ const home = tmpHome();
32
+ const plan = planHostSync({ packageRoot: ROOT, home });
33
+ assert.ok(plan.length > 0);
34
+ assert.ok(plan.every((item) => item.action === 'skip'));
35
+ const reasons = new Set(plan.map((item) => item.reason));
36
+ assert.ok(reasons.has('host-missing'));
37
+ });
38
+
39
+ test('sync upserts Claude AEL files but does not invent DOT skills', () => {
40
+ const home = tmpHome();
41
+ fs.mkdirSync(path.join(home, '.claude'));
42
+ const results = applyHostSync({ packageRoot: ROOT, home });
43
+ const claude = results.filter((item) => item.id === 'claude');
44
+ assert.ok(claude.every((item) => item.action === 'copy'));
45
+ assert.ok(fs.existsSync(path.join(home, '.claude/skills/ai-engineering-loop/SKILL.md')));
46
+ assert.ok(fs.existsSync(path.join(home, '.claude/agents/devil-advocate.md')));
47
+ assert.ok(fs.existsSync(path.join(home, '.claude/agents/judge.md')));
48
+ assert.ok(fs.existsSync(path.join(home, '.claude/commands/ai-engineering-loop.md')));
49
+ assert.ok(!fs.existsSync(path.join(home, '.claude/skills/dot-dev-workflow/SKILL.md')));
50
+ assert.ok(!fs.existsSync(path.join(home, '.claude/settings.local.json')));
51
+ });
52
+
53
+ test('DOT skills update only when already installed', () => {
54
+ const home = tmpHome();
55
+ const dest = path.join(home, '.claude/skills/dot-dev-workflow/SKILL.md');
56
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
57
+ fs.writeFileSync(dest, 'stale\n');
58
+ const results = applyHostSync({ packageRoot: ROOT, home });
59
+ const dotWf = results.find((item) => item.dest === dest);
60
+ assert.strictEqual(dotWf.action, 'copy');
61
+ assert.match(fs.readFileSync(dest, 'utf8'), /Not a substitute for ai-engineering-loop/);
62
+ });
63
+
64
+ test('Grok skill overlay keeps user-invocable', () => {
65
+ const home = tmpHome();
66
+ fs.mkdirSync(path.join(home, '.grok'));
67
+ const dot = path.join(home, '.grok/skills/dot-dev-workflow/SKILL.md');
68
+ fs.mkdirSync(path.dirname(dot), { recursive: true });
69
+ fs.writeFileSync(dot, '---\nname: dot-dev-workflow\ndescription: old\n---\n\nold\n');
70
+ applyHostSync({ packageRoot: ROOT, home });
71
+ const skill = fs.readFileSync(path.join(home, '.grok/skills/ai-engineering-loop/SKILL.md'), 'utf8');
72
+ const wf = fs.readFileSync(dot, 'utf8');
73
+ assert.match(skill, /^user-invocable: true$/m);
74
+ assert.match(wf, /^user-invocable: true$/m);
75
+ });
76
+
77
+ test('dry-run does not write; second apply is current', () => {
78
+ const home = tmpHome();
79
+ fs.mkdirSync(path.join(home, '.gemini'));
80
+ const dry = applyHostSync({ packageRoot: ROOT, home, dryRun: true });
81
+ const gemini = dry.find((item) => item.id === 'gemini');
82
+ assert.strictEqual(gemini.action, 'copy');
83
+ assert.ok(gemini.dryRun);
84
+ assert.ok(!fs.existsSync(path.join(home, '.gemini/config/skills/ai-engineering-loop/SKILL.md')));
85
+ applyHostSync({ packageRoot: ROOT, home });
86
+ const again = applyHostSync({ packageRoot: ROOT, home });
87
+ assert.strictEqual(summarizeHostSync(again.filter((item) => item.id === 'gemini')).current, 1);
88
+ assert.strictEqual(summarizeHostSync(again.filter((item) => item.id === 'gemini')).copy, 0);
89
+ });
90
+
91
+ test('symlink destinations are left alone', () => {
92
+ const home = tmpHome();
93
+ const skillDir = path.join(home, '.claude/skills/ai-engineering-loop');
94
+ fs.mkdirSync(skillDir, { recursive: true });
95
+ const target = path.join(home, 'outside.md');
96
+ fs.writeFileSync(target, 'keep me\n');
97
+ fs.symlinkSync(target, path.join(skillDir, 'SKILL.md'));
98
+ applyHostSync({ packageRoot: ROOT, home });
99
+ assert.strictEqual(fs.readFileSync(target, 'utf8'), 'keep me\n');
100
+ assert.ok(fs.lstatSync(path.join(skillDir, 'SKILL.md')).isSymbolicLink());
101
+ });
102
+
103
+ test('CLI sync-hosts --dry-run respects AEL_HOME', () => {
104
+ const home = tmpHome();
105
+ fs.mkdirSync(path.join(home, '.agents'));
106
+ const out = execFileSync('node', [CLI, 'sync-hosts', '--dry-run'], {
107
+ cwd: ROOT,
108
+ encoding: 'utf8',
109
+ env: { ...process.env, AEL_HOME: home }
110
+ });
111
+ assert.match(out, /dry-run/);
112
+ assert.match(out, /would-copy|copy:/);
113
+ assert.ok(!fs.existsSync(path.join(home, '.agents/judge.md')));
114
+ });
115
+
116
+ test('CLI help lists sync-hosts', () => {
117
+ const out = execFileSync('node', [CLI, '--help'], { encoding: 'utf8' });
118
+ assert.match(out, /sync-hosts/);
119
+ });