ai-engineering-loop 1.0.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.
- package/LICENSE +21 -0
- package/README.md +272 -0
- package/adapters/dot/README.md +55 -0
- package/adapters/dot/coreview.md +88 -0
- package/adapters/dot/gitlab.md +128 -0
- package/adapters/dot/mattermost.md +102 -0
- package/adapters/dot/multi-branch.md +89 -0
- package/agents/devil-advocate.md +111 -0
- package/agents/judge.md +69 -0
- package/agents/maker.md +66 -0
- package/bin/ai-engineering-loop.js +633 -0
- package/core/configuration-precedence.md +102 -0
- package/core/context-impact-assessment.md +127 -0
- package/core/context-refresh-policy.md +116 -0
- package/core/definition-of-done.md +79 -0
- package/core/escalation-policy.md +99 -0
- package/core/goal-contract.md +88 -0
- package/core/iteration-policy.md +108 -0
- package/core/judge-policy.md +123 -0
- package/core/project-initialization.md +97 -0
- package/core/repo-config-schema.md +72 -0
- package/core/verification-loop.md +97 -0
- package/docs/antigravity-feasibility.md +90 -0
- package/docs/migration-plan.md +70 -0
- package/examples/backend-api/payment-idempotency/README.md +33 -0
- package/examples/backend-api/payment-idempotency/goal-contract.md +33 -0
- package/examples/backend-api/payment-idempotency/judge-verdict.md +28 -0
- package/examples/backend-api/payment-idempotency/review-findings.md +50 -0
- package/examples/dot/attendance-confirmation/README.md +22 -0
- package/examples/dot/attendance-confirmation/delivery-report.md +51 -0
- package/examples/dot/attendance-confirmation/goal-contract.md +39 -0
- package/examples/dot/attendance-confirmation/judge-verdict.md +43 -0
- package/examples/dot/attendance-confirmation/review-findings.md +57 -0
- package/examples/initialization/README.md +19 -0
- package/examples/initialization/discovery-trace.md +63 -0
- package/examples/initialization/generated-context.md +110 -0
- package/examples/mobile-app/offline-sync-queue/README.md +33 -0
- package/examples/mobile-app/offline-sync-queue/goal-contract.md +32 -0
- package/examples/mobile-app/offline-sync-queue/judge-verdict.md +27 -0
- package/examples/mobile-app/offline-sync-queue/review-findings.md +30 -0
- package/package.json +31 -0
- package/policies/discovery-safety-policy.md +51 -0
- package/policies/evidence-policy.md +70 -0
- package/policies/finding-policy.md +102 -0
- package/policies/no-progress-policy.md +92 -0
- package/profiles/README.md +42 -0
- package/profiles/backend-api.md +64 -0
- package/profiles/library.md +51 -0
- package/profiles/mobile-app.md +59 -0
- package/profiles/monorepo.md +46 -0
- package/profiles/web-app.md +65 -0
- package/scripts/init.sh +85 -0
- package/templates/repo-config/adapter.md +11 -0
- package/templates/repo-config/architecture.md +15 -0
- package/templates/repo-config/config.md +11 -0
- package/templates/repo-config/conventions.md +16 -0
- package/templates/repo-config/verification.md +13 -0
|
@@ -0,0 +1,633 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* AI Engineering Loop — Deterministic CLI Bootstrap & Living Context Engine
|
|
5
|
+
* Repository: https://github.com/egagofur/ai-engineering-loop
|
|
6
|
+
*
|
|
7
|
+
* Architecture Principle:
|
|
8
|
+
* The CLI handles deterministic repository discovery, context initialization,
|
|
9
|
+
* baseline metadata tracking (metadata.json), status checks, and non-destructive drift refreshes.
|
|
10
|
+
* The AI Agent handles task reasoning, RCA, implementation, review, judging, and impact assessment.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const crypto = require('crypto');
|
|
16
|
+
const { execSync } = require('child_process');
|
|
17
|
+
|
|
18
|
+
const VERSION = '1.0.0';
|
|
19
|
+
const CWD = process.cwd();
|
|
20
|
+
const CONTEXT_DIR = path.join(CWD, '.ai-engineering-loop');
|
|
21
|
+
|
|
22
|
+
// Core files in .ai-engineering-loop/
|
|
23
|
+
const REQUIRED_FILES = [
|
|
24
|
+
'metadata.json',
|
|
25
|
+
'config.md',
|
|
26
|
+
'architecture.md',
|
|
27
|
+
'conventions.md',
|
|
28
|
+
'verification.md',
|
|
29
|
+
'adapter.md'
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Colorized console helpers
|
|
34
|
+
*/
|
|
35
|
+
const log = {
|
|
36
|
+
info: (msg) => console.log('\x1b[36m%s\x1b[0m', msg),
|
|
37
|
+
success: (msg) => console.log('\x1b[32m%s\x1b[0m', msg),
|
|
38
|
+
warn: (msg) => console.log('\x1b[33m%s\x1b[0m', msg),
|
|
39
|
+
error: (msg) => console.log('\x1b[31m%s\x1b[0m', msg),
|
|
40
|
+
bold: (msg) => console.log('\x1b[1m%s\x1b[0m', msg),
|
|
41
|
+
dim: (msg) => console.log('\x1b[2m%s\x1b[0m', msg)
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Safe file reader
|
|
46
|
+
*/
|
|
47
|
+
function readFileSafe(filePath) {
|
|
48
|
+
try {
|
|
49
|
+
return fs.readFileSync(filePath, 'utf8');
|
|
50
|
+
} catch (e) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Compute SHA256 checksum of a file
|
|
57
|
+
*/
|
|
58
|
+
function getFileChecksum(filePath) {
|
|
59
|
+
const content = readFileSafe(filePath);
|
|
60
|
+
if (!content) return null;
|
|
61
|
+
return crypto.createHash('sha256').update(content).digest('hex').slice(0, 16);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Get current git HEAD revision (Level 0 signal)
|
|
66
|
+
*/
|
|
67
|
+
function getGitRevision(rootDir) {
|
|
68
|
+
try {
|
|
69
|
+
return execSync('git rev-parse HEAD', { cwd: rootDir, stdio: ['pipe', 'pipe', 'ignore'] })
|
|
70
|
+
.toString()
|
|
71
|
+
.trim();
|
|
72
|
+
} catch (e) {
|
|
73
|
+
return 'untracked';
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Repository Discovery Engine
|
|
79
|
+
*/
|
|
80
|
+
function analyzeRepository(rootDir) {
|
|
81
|
+
const discovery = {
|
|
82
|
+
projectName: path.basename(rootDir),
|
|
83
|
+
isMonorepo: false,
|
|
84
|
+
profile: 'backend-api',
|
|
85
|
+
languages: [],
|
|
86
|
+
frameworks: [],
|
|
87
|
+
packageManager: 'npm',
|
|
88
|
+
scripts: {
|
|
89
|
+
testUnit: 'npm test',
|
|
90
|
+
testAll: 'npm test',
|
|
91
|
+
typecheck: 'npx tsc --noEmit',
|
|
92
|
+
lint: 'npx eslint --fix',
|
|
93
|
+
build: 'npm run build',
|
|
94
|
+
e2e: null
|
|
95
|
+
},
|
|
96
|
+
adapter: {
|
|
97
|
+
type: 'standard',
|
|
98
|
+
repoSlug: null,
|
|
99
|
+
defaultBranch: 'main',
|
|
100
|
+
ciProvider: 'none'
|
|
101
|
+
},
|
|
102
|
+
topLevelDirs: [],
|
|
103
|
+
manifestChecksums: {},
|
|
104
|
+
evidence: []
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
// Inspect directory structure
|
|
108
|
+
try {
|
|
109
|
+
const entries = fs.readdirSync(rootDir, { withFileTypes: true });
|
|
110
|
+
discovery.topLevelDirs = entries
|
|
111
|
+
.filter((e) => e.isDirectory() && !e.name.startsWith('.') && e.name !== 'node_modules')
|
|
112
|
+
.map((e) => e.name);
|
|
113
|
+
} catch (e) {}
|
|
114
|
+
|
|
115
|
+
// 1. Monorepo & Topology Detection
|
|
116
|
+
const hasApps = fs.existsSync(path.join(rootDir, 'apps'));
|
|
117
|
+
const hasPackages = fs.existsSync(path.join(rootDir, 'packages'));
|
|
118
|
+
const hasPnpmWorkspace = fs.existsSync(path.join(rootDir, 'pnpm-workspace.yaml'));
|
|
119
|
+
const hasTurbo = fs.existsSync(path.join(rootDir, 'turbo.json'));
|
|
120
|
+
const hasNx = fs.existsSync(path.join(rootDir, 'nx.json'));
|
|
121
|
+
const hasLerna = fs.existsSync(path.join(rootDir, 'lerna.json'));
|
|
122
|
+
const hasGoWork = fs.existsSync(path.join(rootDir, 'go.work'));
|
|
123
|
+
|
|
124
|
+
if (hasPnpmWorkspace || hasTurbo || hasNx || hasLerna || hasGoWork || (hasApps && hasPackages)) {
|
|
125
|
+
discovery.isMonorepo = true;
|
|
126
|
+
discovery.profile = 'monorepo';
|
|
127
|
+
discovery.evidence.push('Topology: Monorepo workspace detected');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// 2. Package Managers & Manifests
|
|
131
|
+
if (fs.existsSync(path.join(rootDir, 'pnpm-lock.yaml')) || hasPnpmWorkspace) {
|
|
132
|
+
discovery.packageManager = 'pnpm';
|
|
133
|
+
} else if (fs.existsSync(path.join(rootDir, 'yarn.lock'))) {
|
|
134
|
+
discovery.packageManager = 'yarn';
|
|
135
|
+
} else if (fs.existsSync(path.join(rootDir, 'bun.lockb')) || fs.existsSync(path.join(rootDir, 'bun.lock'))) {
|
|
136
|
+
discovery.packageManager = 'bun';
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Track manifest checksums
|
|
140
|
+
const pkgPath = path.join(rootDir, 'package.json');
|
|
141
|
+
if (fs.existsSync(pkgPath)) {
|
|
142
|
+
discovery.manifestChecksums['package.json'] = getFileChecksum(pkgPath);
|
|
143
|
+
discovery.languages.push('TypeScript / JavaScript');
|
|
144
|
+
discovery.evidence.push('Manifest: package.json');
|
|
145
|
+
try {
|
|
146
|
+
const pkg = JSON.parse(readFileSafe(pkgPath) || '{}');
|
|
147
|
+
const allDeps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
|
|
148
|
+
|
|
149
|
+
if (allDeps['next']) { discovery.frameworks.push('Next.js'); if (!discovery.isMonorepo) discovery.profile = 'web-app'; }
|
|
150
|
+
if (allDeps['react']) { discovery.frameworks.push('React'); if (!discovery.isMonorepo) discovery.profile = 'web-app'; }
|
|
151
|
+
if (allDeps['vue']) { discovery.frameworks.push('Vue'); if (!discovery.isMonorepo) discovery.profile = 'web-app'; }
|
|
152
|
+
if (allDeps['svelte'] || allDeps['@sveltejs/kit']) { discovery.frameworks.push('Svelte'); if (!discovery.isMonorepo) discovery.profile = 'web-app'; }
|
|
153
|
+
if (allDeps['@nestjs/core']) { discovery.frameworks.push('NestJS'); if (!discovery.isMonorepo) discovery.profile = 'backend-api'; }
|
|
154
|
+
if (allDeps['express']) { discovery.frameworks.push('Express'); if (!discovery.isMonorepo) discovery.profile = 'backend-api'; }
|
|
155
|
+
if (allDeps['fastify']) { discovery.frameworks.push('Fastify'); if (!discovery.isMonorepo) discovery.profile = 'backend-api'; }
|
|
156
|
+
if (allDeps['@prisma/client'] || allDeps['prisma']) { discovery.frameworks.push('Prisma ORM'); }
|
|
157
|
+
|
|
158
|
+
// Script mapping
|
|
159
|
+
const scripts = pkg.scripts || {};
|
|
160
|
+
const pm = discovery.packageManager;
|
|
161
|
+
if (scripts['test:unit']) discovery.scripts.testUnit = `${pm} run test:unit`;
|
|
162
|
+
else if (scripts['test']) discovery.scripts.testUnit = `${pm} test`;
|
|
163
|
+
|
|
164
|
+
if (scripts['typecheck']) discovery.scripts.typecheck = `${pm} run typecheck`;
|
|
165
|
+
else if (scripts['type-check']) discovery.scripts.typecheck = `${pm} run type-check`;
|
|
166
|
+
else if (scripts['check']) discovery.scripts.typecheck = `${pm} run check`;
|
|
167
|
+
|
|
168
|
+
if (scripts['lint']) discovery.scripts.lint = `${pm} run lint`;
|
|
169
|
+
if (scripts['build']) discovery.scripts.build = `${pm} run build`;
|
|
170
|
+
if (scripts['test:e2e']) discovery.scripts.e2e = `${pm} run test:e2e`;
|
|
171
|
+
} catch (e) {}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Go
|
|
175
|
+
const goModPath = path.join(rootDir, 'go.mod');
|
|
176
|
+
if (fs.existsSync(goModPath)) {
|
|
177
|
+
discovery.manifestChecksums['go.mod'] = getFileChecksum(goModPath);
|
|
178
|
+
discovery.languages.push('Go');
|
|
179
|
+
if (!discovery.isMonorepo) discovery.profile = 'backend-api';
|
|
180
|
+
discovery.scripts.testUnit = 'go test -v ./...';
|
|
181
|
+
discovery.scripts.build = 'go build ./...';
|
|
182
|
+
discovery.scripts.typecheck = 'go vet ./...';
|
|
183
|
+
discovery.evidence.push('Manifest: go.mod');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Rust
|
|
187
|
+
const cargoPath = path.join(rootDir, 'Cargo.toml');
|
|
188
|
+
if (fs.existsSync(cargoPath)) {
|
|
189
|
+
discovery.manifestChecksums['Cargo.toml'] = getFileChecksum(cargoPath);
|
|
190
|
+
discovery.languages.push('Rust');
|
|
191
|
+
if (!discovery.isMonorepo) discovery.profile = 'library';
|
|
192
|
+
discovery.scripts.testUnit = 'cargo test';
|
|
193
|
+
discovery.scripts.build = 'cargo build';
|
|
194
|
+
discovery.scripts.typecheck = 'cargo check';
|
|
195
|
+
discovery.scripts.lint = 'cargo clippy';
|
|
196
|
+
discovery.evidence.push('Manifest: Cargo.toml');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Python
|
|
200
|
+
const pyprojPath = path.join(rootDir, 'pyproject.toml');
|
|
201
|
+
const reqPath = path.join(rootDir, 'requirements.txt');
|
|
202
|
+
if (fs.existsSync(pyprojPath) || fs.existsSync(reqPath)) {
|
|
203
|
+
if (fs.existsSync(pyprojPath)) discovery.manifestChecksums['pyproject.toml'] = getFileChecksum(pyprojPath);
|
|
204
|
+
if (fs.existsSync(reqPath)) discovery.manifestChecksums['requirements.txt'] = getFileChecksum(reqPath);
|
|
205
|
+
discovery.languages.push('Python');
|
|
206
|
+
if (!discovery.isMonorepo) discovery.profile = 'backend-api';
|
|
207
|
+
discovery.scripts.testUnit = 'pytest';
|
|
208
|
+
discovery.scripts.typecheck = 'mypy .';
|
|
209
|
+
discovery.scripts.lint = 'ruff check .';
|
|
210
|
+
discovery.evidence.push('Manifest: pyproject.toml / requirements.txt');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Flutter / Mobile
|
|
214
|
+
const pubspecPath = path.join(rootDir, 'pubspec.yaml');
|
|
215
|
+
if (fs.existsSync(pubspecPath)) {
|
|
216
|
+
discovery.manifestChecksums['pubspec.yaml'] = getFileChecksum(pubspecPath);
|
|
217
|
+
discovery.languages.push('Dart / Flutter');
|
|
218
|
+
discovery.profile = 'mobile-app';
|
|
219
|
+
discovery.scripts.testUnit = 'flutter test';
|
|
220
|
+
discovery.scripts.typecheck = 'dart analyze';
|
|
221
|
+
discovery.scripts.build = 'flutter build bundle';
|
|
222
|
+
discovery.evidence.push('Manifest: pubspec.yaml');
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// 3. Adapter / CI Detection
|
|
226
|
+
if (fs.existsSync(path.join(rootDir, '.github', 'workflows'))) {
|
|
227
|
+
discovery.adapter.ciProvider = 'GitHub Actions';
|
|
228
|
+
discovery.adapter.type = 'github';
|
|
229
|
+
} else if (fs.existsSync(path.join(rootDir, '.gitlab-ci.yml'))) {
|
|
230
|
+
discovery.adapter.ciProvider = 'GitLab CI';
|
|
231
|
+
discovery.adapter.type = 'gitlab';
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Check git remote if git folder exists
|
|
235
|
+
const gitConfigStr = readFileSafe(path.join(rootDir, '.git', 'config'));
|
|
236
|
+
if (gitConfigStr) {
|
|
237
|
+
const urlMatch = gitConfigStr.match(/url\s*=\s*(.*)/);
|
|
238
|
+
if (urlMatch) {
|
|
239
|
+
discovery.adapter.repoSlug = urlMatch[1].trim();
|
|
240
|
+
if (discovery.adapter.repoSlug.includes('gitlab.dot.co.id')) {
|
|
241
|
+
discovery.adapter.type = 'dot';
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return discovery;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Generate Context Files (Including metadata.json Baseline)
|
|
251
|
+
*/
|
|
252
|
+
function generateContextFiles(rootDir, discovery, trigger = 'init', impact = 'INITIAL_BOOTSTRAP') {
|
|
253
|
+
const targetDir = path.join(rootDir, '.ai-engineering-loop');
|
|
254
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
255
|
+
|
|
256
|
+
const currentRevision = getGitRevision(rootDir);
|
|
257
|
+
|
|
258
|
+
// 0. metadata.json (Baseline)
|
|
259
|
+
const metadataJson = {
|
|
260
|
+
contextVersion: '1.0.0',
|
|
261
|
+
generatedAt: new Date().toISOString(),
|
|
262
|
+
repositoryRevision: currentRevision,
|
|
263
|
+
projectProfile: discovery.profile,
|
|
264
|
+
manifestChecksums: discovery.manifestChecksums,
|
|
265
|
+
lastReconciliation: {
|
|
266
|
+
timestamp: new Date().toISOString(),
|
|
267
|
+
trigger,
|
|
268
|
+
impact
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
fs.writeFileSync(path.join(targetDir, 'metadata.json'), JSON.stringify(metadataJson, null, 2) + '\n');
|
|
272
|
+
|
|
273
|
+
// 1. config.md
|
|
274
|
+
const configMd = `# Project Configuration
|
|
275
|
+
|
|
276
|
+
## Metadata
|
|
277
|
+
- **project_name**: "${discovery.projectName}"
|
|
278
|
+
- **project_profile**: "${discovery.profile}" # Archetype from profiles/${discovery.profile}.md
|
|
279
|
+
- **languages**:
|
|
280
|
+
${discovery.languages.map((l) => ` - ${l}`).join('\n') || ' - Unspecified'}
|
|
281
|
+
- **frameworks**:
|
|
282
|
+
${discovery.frameworks.map((f) => ` - ${f}`).join('\n') || ' - Standard'}
|
|
283
|
+
- **package_manager**: "${discovery.packageManager}"
|
|
284
|
+
- **default_base_branch**: "${discovery.adapter.defaultBranch}"
|
|
285
|
+
|
|
286
|
+
## Observed Evidence
|
|
287
|
+
${discovery.evidence.map((e) => `- ${e}`).join('\n')}
|
|
288
|
+
`;
|
|
289
|
+
fs.writeFileSync(path.join(targetDir, 'config.md'), configMd);
|
|
290
|
+
|
|
291
|
+
// 2. architecture.md
|
|
292
|
+
const archMd = `# Project Architecture
|
|
293
|
+
|
|
294
|
+
## System Overview
|
|
295
|
+
Discovered architecture for ${discovery.projectName} (${discovery.profile}).
|
|
296
|
+
|
|
297
|
+
## Discovered Top-Level Directories
|
|
298
|
+
${discovery.topLevelDirs.map((d) => `- \`${d}/\``).join('\n') || '- Flat directory layout'}
|
|
299
|
+
|
|
300
|
+
## Boundary Invariants
|
|
301
|
+
- Preserve existing component boundaries and modular encapsulation.
|
|
302
|
+
- Zero circular dependencies across packages or modules.
|
|
303
|
+
- Changes must be surgical and adhere to existing architecture patterns.
|
|
304
|
+
|
|
305
|
+
## Evidence & Confidence
|
|
306
|
+
- Observed from: Directory scan, package manifests
|
|
307
|
+
- Confidence: HIGH
|
|
308
|
+
`;
|
|
309
|
+
fs.writeFileSync(path.join(targetDir, 'architecture.md'), archMd);
|
|
310
|
+
|
|
311
|
+
// 3. conventions.md
|
|
312
|
+
const convMd = `# Project Conventions
|
|
313
|
+
|
|
314
|
+
## Code Standards
|
|
315
|
+
- File naming: kebab-case or established repository convention.
|
|
316
|
+
- Error handling: Use domain-specific errors; zero empty catch blocks.
|
|
317
|
+
- Types: Strict typing; zero unnecessary \`any\` types.
|
|
318
|
+
|
|
319
|
+
## Forbidden Anti-Patterns
|
|
320
|
+
- Zero speculative TODOs or orphan dead code in production pull requests.
|
|
321
|
+
- Never commit private secrets, passwords, or API keys.
|
|
322
|
+
- Do not make unsolicited renovations outside the active Goal Contract scope.
|
|
323
|
+
`;
|
|
324
|
+
fs.writeFileSync(path.join(targetDir, 'conventions.md'), convMd);
|
|
325
|
+
|
|
326
|
+
// 4. verification.md
|
|
327
|
+
const verifyMd = `# Project Verification Commands
|
|
328
|
+
|
|
329
|
+
## Discovered Verification Commands
|
|
330
|
+
- **test_unit**: \`${discovery.scripts.testUnit}\`
|
|
331
|
+
- **typecheck**: \`${discovery.scripts.typecheck}\`
|
|
332
|
+
- **lint**: \`${discovery.scripts.lint}\`
|
|
333
|
+
- **build**: \`${discovery.scripts.build}\`
|
|
334
|
+
${discovery.scripts.e2e ? `- **e2e**: \`${discovery.scripts.e2e}\`` : ''}
|
|
335
|
+
|
|
336
|
+
## Verification Protocol
|
|
337
|
+
- 100% deterministic checks must pass before Devil's Advocate review.
|
|
338
|
+
- Unit tests must cover boundary cases, null safety, and error paths.
|
|
339
|
+
`;
|
|
340
|
+
fs.writeFileSync(path.join(targetDir, 'verification.md'), verifyMd);
|
|
341
|
+
|
|
342
|
+
// 5. adapter.md
|
|
343
|
+
const adapterMd = `# Project Delivery Adapter Configuration
|
|
344
|
+
|
|
345
|
+
## Delivery Pipeline
|
|
346
|
+
- **adapter_type**: "${discovery.adapter.type}" # dot | github | gitlab | standard
|
|
347
|
+
${discovery.adapter.repoSlug ? `- **remote_repository**: "${discovery.adapter.repoSlug}"` : ''}
|
|
348
|
+
- **default_target_branch**: "${discovery.adapter.defaultBranch}"
|
|
349
|
+
- **ci_provider**: "${discovery.adapter.ciProvider}"
|
|
350
|
+
`;
|
|
351
|
+
fs.writeFileSync(path.join(targetDir, 'adapter.md'), adapterMd);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Validate Context Integrity
|
|
356
|
+
*/
|
|
357
|
+
function validateContext(targetDir) {
|
|
358
|
+
if (!fs.existsSync(targetDir)) return { valid: false, reason: 'Directory missing' };
|
|
359
|
+
for (const f of REQUIRED_FILES) {
|
|
360
|
+
const fullPath = path.join(targetDir, f);
|
|
361
|
+
if (!fs.existsSync(fullPath) || fs.statSync(fullPath).size === 0) {
|
|
362
|
+
return { valid: false, reason: `Missing or empty ${f}` };
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return { valid: true };
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Evaluate Drift Against Baseline (Progressive Level 0 & Level 1)
|
|
370
|
+
*/
|
|
371
|
+
function evaluateDrift(rootDir, targetDir) {
|
|
372
|
+
const metadataPath = path.join(targetDir, 'metadata.json');
|
|
373
|
+
const metadataStr = readFileSafe(metadataPath);
|
|
374
|
+
if (!metadataStr) return { status: 'STALE', reason: 'Missing metadata.json baseline' };
|
|
375
|
+
|
|
376
|
+
let metadata;
|
|
377
|
+
try {
|
|
378
|
+
metadata = JSON.parse(metadataStr);
|
|
379
|
+
} catch (e) {
|
|
380
|
+
return { status: 'STALE', reason: 'Corrupt metadata.json' };
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const currentRevision = getGitRevision(rootDir);
|
|
384
|
+
const baselineRevision = metadata.repositoryRevision || 'unknown';
|
|
385
|
+
|
|
386
|
+
// Check manifest checksums (Level 0)
|
|
387
|
+
const currentManifests = {};
|
|
388
|
+
for (const manifest of ['package.json', 'go.mod', 'Cargo.toml', 'pyproject.toml', 'requirements.txt', 'pubspec.yaml']) {
|
|
389
|
+
const p = path.join(rootDir, manifest);
|
|
390
|
+
if (fs.existsSync(p)) {
|
|
391
|
+
currentManifests[manifest] = getFileChecksum(p);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const baselineManifests = metadata.manifestChecksums || {};
|
|
396
|
+
let manifestDrift = false;
|
|
397
|
+
const changedManifests = [];
|
|
398
|
+
|
|
399
|
+
for (const [m, hash] of Object.entries(currentManifests)) {
|
|
400
|
+
if (baselineManifests[m] !== hash) {
|
|
401
|
+
manifestDrift = true;
|
|
402
|
+
changedManifests.push(m);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (manifestDrift) {
|
|
407
|
+
return {
|
|
408
|
+
status: 'STALE',
|
|
409
|
+
reason: `Manifest drift detected in: ${changedManifests.join(', ')}`,
|
|
410
|
+
level: 'LEVEL_2'
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (currentRevision === baselineRevision || currentRevision === 'untracked') {
|
|
415
|
+
return { status: 'CURRENT', reason: 'Git HEAD and manifest checksums match baseline (Level 0)', level: 'LEVEL_0' };
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Inspect touched files if HEAD advanced (Level 1)
|
|
419
|
+
try {
|
|
420
|
+
const diffFiles = execSync(`git diff --name-only ${baselineRevision}..HEAD`, { cwd: rootDir, stdio: ['pipe', 'pipe', 'ignore'] })
|
|
421
|
+
.toString()
|
|
422
|
+
.trim()
|
|
423
|
+
.split('\n')
|
|
424
|
+
.filter(Boolean);
|
|
425
|
+
|
|
426
|
+
const architecturalFiles = diffFiles.filter(
|
|
427
|
+
(f) =>
|
|
428
|
+
f.endsWith('.json') ||
|
|
429
|
+
f.endsWith('.toml') ||
|
|
430
|
+
f.endsWith('.yaml') ||
|
|
431
|
+
f.endsWith('.yml') ||
|
|
432
|
+
f.startsWith('.github/') ||
|
|
433
|
+
f.startsWith('.gitlab-ci')
|
|
434
|
+
);
|
|
435
|
+
|
|
436
|
+
if (architecturalFiles.length === 0) {
|
|
437
|
+
return {
|
|
438
|
+
status: 'CURRENT',
|
|
439
|
+
reason: `HEAD advanced but only non-architectural files modified (${diffFiles.length} files)`,
|
|
440
|
+
level: 'LEVEL_1'
|
|
441
|
+
};
|
|
442
|
+
} else {
|
|
443
|
+
return {
|
|
444
|
+
status: 'POSSIBLE_DRIFT',
|
|
445
|
+
reason: `Architectural files modified: ${architecturalFiles.join(', ')}`,
|
|
446
|
+
level: 'LEVEL_2'
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
} catch (e) {
|
|
450
|
+
return { status: 'CURRENT', reason: 'Unable to compute git diff; manifests match', level: 'LEVEL_0' };
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Command Handlers
|
|
456
|
+
*/
|
|
457
|
+
|
|
458
|
+
// Command: init
|
|
459
|
+
function handleInit() {
|
|
460
|
+
log.info('AI Engineering Loop — Project Context Bootstrap (init)');
|
|
461
|
+
log.dim(`Target directory: ${CWD}`);
|
|
462
|
+
|
|
463
|
+
if (fs.existsSync(CONTEXT_DIR)) {
|
|
464
|
+
const validation = validateContext(CONTEXT_DIR);
|
|
465
|
+
if (validation.valid) {
|
|
466
|
+
log.success('✓ .ai-engineering-loop/ already exists and is valid.');
|
|
467
|
+
const drift = evaluateDrift(CWD, CONTEXT_DIR);
|
|
468
|
+
console.log(`- Baseline Status: ${drift.status} (${drift.reason})`);
|
|
469
|
+
log.dim('Run "npx ai-engineering-loop status" to inspect health, or "refresh" to update.');
|
|
470
|
+
process.exit(0);
|
|
471
|
+
} else {
|
|
472
|
+
log.warn(`! Existing .ai-engineering-loop/ found but incomplete: ${validation.reason}. Repairing...`);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
log.info('Analyzing repository topology and manifests...');
|
|
477
|
+
const discovery = analyzeRepository(CWD);
|
|
478
|
+
|
|
479
|
+
log.dim(`> Profile Bound: ${discovery.profile}`);
|
|
480
|
+
log.dim(`> Languages: ${discovery.languages.join(', ') || 'Unspecified'}`);
|
|
481
|
+
log.dim(`> Package Manager: ${discovery.packageManager}`);
|
|
482
|
+
log.dim(`> Unit Test Command: ${discovery.scripts.testUnit}`);
|
|
483
|
+
|
|
484
|
+
generateContextFiles(CWD, discovery, 'init', 'INITIAL_BOOTSTRAP');
|
|
485
|
+
|
|
486
|
+
const validation = validateContext(CONTEXT_DIR);
|
|
487
|
+
if (validation.valid) {
|
|
488
|
+
log.success('\n✓ Successfully initialized .ai-engineering-loop/ with:');
|
|
489
|
+
REQUIRED_FILES.forEach((f) => console.log(` - .ai-engineering-loop/${f}`));
|
|
490
|
+
log.dim('\nRecommendation: Commit .ai-engineering-loop/ to version control so team agents share context.');
|
|
491
|
+
} else {
|
|
492
|
+
log.error(`\n✗ Initialization validation failed: ${validation.reason}`);
|
|
493
|
+
process.exit(1);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Command: status
|
|
498
|
+
function handleStatus() {
|
|
499
|
+
log.info('AI Engineering Loop — Project Context Status (status)');
|
|
500
|
+
log.dim(`Target directory: ${CWD}`);
|
|
501
|
+
|
|
502
|
+
if (!fs.existsSync(CONTEXT_DIR)) {
|
|
503
|
+
log.warn('Status: NOT INITIALIZED');
|
|
504
|
+
log.dim('Run "npx ai-engineering-loop init" to bootstrap context.');
|
|
505
|
+
process.exit(1);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const validation = validateContext(CONTEXT_DIR);
|
|
509
|
+
if (!validation.valid) {
|
|
510
|
+
log.error(`Status: INCOMPLETE (${validation.reason})`);
|
|
511
|
+
log.dim('Run "npx ai-engineering-loop refresh" or "init" to repair.');
|
|
512
|
+
process.exit(1);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const metadataStr = readFileSafe(path.join(CONTEXT_DIR, 'metadata.json'));
|
|
516
|
+
let metadata = {};
|
|
517
|
+
try { metadata = JSON.parse(metadataStr || '{}'); } catch (e) {}
|
|
518
|
+
|
|
519
|
+
const drift = evaluateDrift(CWD, CONTEXT_DIR);
|
|
520
|
+
|
|
521
|
+
log.success('Status: READY & VALID');
|
|
522
|
+
console.log(`- Project Name: ${path.basename(CWD)}`);
|
|
523
|
+
console.log(`- Project Profile: ${metadata.projectProfile || 'unspecified'}`);
|
|
524
|
+
console.log(`- Context Baseline Git: ${metadata.repositoryRevision ? metadata.repositoryRevision.slice(0, 8) : 'unknown'}`);
|
|
525
|
+
console.log(`- Living Freshness: \x1b[32m${drift.status}\x1b[0m (${drift.reason})`);
|
|
526
|
+
console.log('- Context Files: 6/6 verified (including metadata.json)');
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Command: refresh
|
|
530
|
+
function handleRefresh() {
|
|
531
|
+
log.info('AI Engineering Loop — Context Drift Refresh (refresh)');
|
|
532
|
+
log.dim(`Target directory: ${CWD}`);
|
|
533
|
+
|
|
534
|
+
if (!fs.existsSync(CONTEXT_DIR)) {
|
|
535
|
+
log.warn('Context not found. Initializing fresh context...');
|
|
536
|
+
handleInit();
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const drift = evaluateDrift(CWD, CONTEXT_DIR);
|
|
541
|
+
if (drift.status === 'CURRENT') {
|
|
542
|
+
log.success('✓ Context is already CURRENT. No changes required.');
|
|
543
|
+
log.dim(`Reason: ${drift.reason}`);
|
|
544
|
+
process.exit(0);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
log.info(`Drift detected: ${drift.reason}. Reconciling context...`);
|
|
548
|
+
const discovery = analyzeRepository(CWD);
|
|
549
|
+
|
|
550
|
+
generateContextFiles(CWD, discovery, 'refresh', 'DRIFT_RECONCILIATION');
|
|
551
|
+
|
|
552
|
+
log.success('✓ Context reconciled non-destructively.');
|
|
553
|
+
handleStatus();
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// Command: run
|
|
557
|
+
function handleRun() {
|
|
558
|
+
log.info('AI Engineering Loop — Task Execution Entrypoint (run)');
|
|
559
|
+
|
|
560
|
+
if (!fs.existsSync(CONTEXT_DIR)) {
|
|
561
|
+
log.warn('Project context missing. Auto-initializing before task execution...');
|
|
562
|
+
handleInit();
|
|
563
|
+
} else {
|
|
564
|
+
handleStatus();
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
console.log('\n------------------------------------------------------------');
|
|
568
|
+
log.bold('AI Agent Ready:');
|
|
569
|
+
console.log('1. Formulate Goal Contract (core/goal-contract.md)');
|
|
570
|
+
console.log('2. Execute Root Cause Analysis & Plan');
|
|
571
|
+
console.log('3. Maker Agent implements surgical code and tests');
|
|
572
|
+
console.log('4. Run Deterministic Verification');
|
|
573
|
+
console.log('5. Execute Devil\'s Advocate Adversarial Review');
|
|
574
|
+
console.log('6. Judge Agent evaluates DoD and issues PASS verdict');
|
|
575
|
+
console.log('7. Context Impact Assessment (NONE / TARGETED / MAJOR)');
|
|
576
|
+
console.log('8. Delivery Adapter creates MR/PR');
|
|
577
|
+
console.log('------------------------------------------------------------\n');
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// Help Menu
|
|
581
|
+
function printHelp() {
|
|
582
|
+
console.log(`
|
|
583
|
+
AI Engineering Loop CLI (v${VERSION})
|
|
584
|
+
A reusable, framework-agnostic AI Engineering Operating System.
|
|
585
|
+
|
|
586
|
+
Usage:
|
|
587
|
+
npx ai-engineering-loop <command>
|
|
588
|
+
|
|
589
|
+
Commands:
|
|
590
|
+
init Bootstrap .ai-engineering-loop/ context from repository discovery
|
|
591
|
+
status Check the validity, readiness, and baseline freshness of context
|
|
592
|
+
refresh Reconcile drifted context against repository non-destructively
|
|
593
|
+
run Verify context readiness and instruct AI agent to begin loop
|
|
594
|
+
|
|
595
|
+
Options:
|
|
596
|
+
-h, --help Show this help menu
|
|
597
|
+
-v, --version Show version number
|
|
598
|
+
|
|
599
|
+
Documentation:
|
|
600
|
+
https://github.com/egagofur/ai-engineering-loop
|
|
601
|
+
`);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// CLI Router
|
|
605
|
+
const args = process.argv.slice(2);
|
|
606
|
+
const command = args[0] || 'init';
|
|
607
|
+
|
|
608
|
+
switch (command) {
|
|
609
|
+
case 'init':
|
|
610
|
+
handleInit();
|
|
611
|
+
break;
|
|
612
|
+
case 'status':
|
|
613
|
+
handleStatus();
|
|
614
|
+
break;
|
|
615
|
+
case 'refresh':
|
|
616
|
+
handleRefresh();
|
|
617
|
+
break;
|
|
618
|
+
case 'run':
|
|
619
|
+
handleRun();
|
|
620
|
+
break;
|
|
621
|
+
case '-v':
|
|
622
|
+
case '--version':
|
|
623
|
+
console.log(`ai-engineering-loop v${VERSION}`);
|
|
624
|
+
break;
|
|
625
|
+
case '-h':
|
|
626
|
+
case '--help':
|
|
627
|
+
default:
|
|
628
|
+
if (command && command !== '-h' && command !== '--help') {
|
|
629
|
+
log.error(`Unknown command: ${command}`);
|
|
630
|
+
}
|
|
631
|
+
printHelp();
|
|
632
|
+
break;
|
|
633
|
+
}
|