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.
- package/.agents/devil-advocate.md +18 -3
- package/.agents/judge.md +8 -4
- package/.agents/workflows/ai-engineering-loop.md +7 -7
- package/.claude/agents/devil-advocate.md +17 -2
- package/.claude/agents/judge.md +7 -3
- package/.claude/commands/ai-engineering-loop.md +1 -1
- package/.claude/skills/ai-engineering-loop/SKILL.md +6 -5
- package/.gemini/skills/ai-engineering-loop/SKILL.md +59 -0
- package/.grok/agents/devil-advocate.md +31 -32
- package/.grok/agents/judge.md +9 -5
- package/.grok/commands/ai-engineering-loop.md +1 -1
- package/.grok/skills/ai-engineering-loop/SKILL.md +9 -7
- package/README.md +31 -3
- package/README.npm.md +5 -2
- package/adapters/dot/README.md +27 -3
- package/adapters/dot/coreview.md +30 -11
- package/adapters/dot/mattermost.md +31 -19
- package/adapters/dot/skills/dot-dev-skill-router/SKILL.md +55 -0
- package/adapters/dot/skills/dot-dev-workflow/SKILL.md +118 -0
- package/agents/devil-advocate.md +3 -1
- package/agents/maker.md +13 -11
- package/agents/shared/devil-advocate.body.md +61 -0
- package/agents/shared/judge.body.md +38 -0
- package/bin/ai-engineering-loop.js +131 -22
- package/core/context-impact-assessment.md +2 -0
- package/core/definition-of-done.md +2 -0
- package/core/goal-contract.md +18 -4
- package/core/grill-policy.md +70 -0
- package/core/handoff-policy.md +44 -0
- package/core/judge-policy.md +4 -3
- package/core/project-initialization.md +2 -1
- package/core/repo-config-schema.md +15 -1
- package/core/root-cause-analysis.md +30 -0
- package/core/verification-loop.md +3 -1
- package/examples/initialization/discovery-trace.md +1 -1
- package/examples/initialization/generated-context.md +1 -1
- package/lib/orchestration.js +20 -2
- package/lib/sync-hosts.js +195 -0
- package/package.json +2 -1
- package/policies/finding-policy.md +7 -1
- package/policies/review-budget.md +8 -0
- package/policies/tdd-policy.md +32 -0
- package/scripts/init.sh +21 -0
- package/templates/repo-config/adr-readme.md +33 -0
- package/templates/repo-config/glossary.md +18 -0
- package/tests/living-context.test.js +46 -0
- package/tests/orchestration.test.js +80 -0
- package/tests/skill-host-compat.test.js +117 -0
- 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
|
+
});
|