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
package/lib/orchestration.js
CHANGED
|
@@ -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
|
-
*
|
|
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
|
|
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.
|
|
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.
|
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
Applies to Devil's Advocate and Judge on Claude Code, Grok CLI, and Antigravity. Parent always waits; children never run in the background.
|
|
4
4
|
|
|
5
|
+
The **agent body** (instructions + JSON output) is identical on all three hosts. Source of truth:
|
|
6
|
+
|
|
7
|
+
- `agents/shared/devil-advocate.body.md`
|
|
8
|
+
- `agents/shared/judge.body.md`
|
|
9
|
+
|
|
10
|
+
Host files (`.claude/agents/`, `.grok/agents/`, `.agents/`) may differ only in YAML frontmatter (tool names). Tests fail if a host body drifts.
|
|
11
|
+
|
|
5
12
|
## Shared spawn rules
|
|
6
13
|
|
|
7
14
|
- Write `git diff` to `.ai-engineering-loop/tasks/current.diff` before review.
|
|
@@ -15,6 +22,7 @@ Applies to Devil's Advocate and Judge on Claude Code, Grok CLI, and Antigravity.
|
|
|
15
22
|
- At most **8** tool calls, then emit the Finding Ledger.
|
|
16
23
|
- Read the diff file first. Do not run `git diff` if that path was given.
|
|
17
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.
|
|
18
26
|
|
|
19
27
|
## Judge
|
|
20
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
|
+
});
|