tryassay 0.13.0 → 0.14.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/dist/api/server.d.ts +4 -0
- package/dist/api/server.js +138 -1
- package/dist/api/server.js.map +1 -1
- package/dist/cli.js +19 -1
- package/dist/cli.js.map +1 -1
- package/dist/commands/create.d.ts +16 -0
- package/dist/commands/create.js +103 -0
- package/dist/commands/create.js.map +1 -0
- package/dist/runtime/agents/code-agent.d.ts +1 -1
- package/dist/runtime/agents/code-agent.js +17 -7
- package/dist/runtime/agents/code-agent.js.map +1 -1
- package/dist/runtime/agents/coordinator-agent.js +2 -1
- package/dist/runtime/agents/coordinator-agent.js.map +1 -1
- package/dist/runtime/agents/planner-agent.d.ts +18 -0
- package/dist/runtime/agents/planner-agent.js +201 -0
- package/dist/runtime/agents/planner-agent.js.map +1 -0
- package/dist/runtime/app-create-orchestrator.d.ts +45 -0
- package/dist/runtime/app-create-orchestrator.js +722 -0
- package/dist/runtime/app-create-orchestrator.js.map +1 -0
- package/dist/runtime/composition-verifier.d.ts +46 -0
- package/dist/runtime/composition-verifier.js +278 -1
- package/dist/runtime/composition-verifier.js.map +1 -1
- package/dist/runtime/multi-agent-loop.d.ts +2 -0
- package/dist/runtime/multi-agent-loop.js +65 -1
- package/dist/runtime/multi-agent-loop.js.map +1 -1
- package/dist/runtime/specialized-agent.js +1 -1
- package/dist/runtime/specialized-agent.js.map +1 -1
- package/dist/runtime/types.d.ts +166 -2
- package/package.json +1 -1
|
@@ -0,0 +1,722 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// Assay Verified App Creator — Meta-Orchestrator
|
|
3
|
+
// Two-phase flow: PlannerAgent designs architecture,
|
|
4
|
+
// then MultiAgentLoop builds each feature with verification.
|
|
5
|
+
// ============================================================
|
|
6
|
+
import { EventEmitter } from 'node:events';
|
|
7
|
+
import { mkdir, writeFile, readFile } from 'node:fs/promises';
|
|
8
|
+
import { join, dirname } from 'node:path';
|
|
9
|
+
import { randomUUID } from 'node:crypto';
|
|
10
|
+
import { PlannerAgent } from './agents/planner-agent.js';
|
|
11
|
+
import { CodeAgent } from './agents/code-agent.js';
|
|
12
|
+
import { MessageBus } from './message-bus.js';
|
|
13
|
+
import { CompositionVerifier } from './composition-verifier.js';
|
|
14
|
+
import { MultiAgentLoop } from './multi-agent-loop.js';
|
|
15
|
+
// ── Default safety policy ───────────────────────────────────
|
|
16
|
+
const DEFAULT_SAFETY = {
|
|
17
|
+
formalOverridesConsensus: true,
|
|
18
|
+
noSelfVerification: true,
|
|
19
|
+
criticalClaimsRequireFormal: true,
|
|
20
|
+
escalationRules: {
|
|
21
|
+
maxFormalOverridesBeforeHalt: 3,
|
|
22
|
+
maxTrustDemotions: 2,
|
|
23
|
+
maxRejectCyclesPerTask: 3,
|
|
24
|
+
},
|
|
25
|
+
preferModelDiversity: false,
|
|
26
|
+
};
|
|
27
|
+
// ── App Create Orchestrator ─────────────────────────────────
|
|
28
|
+
export class AppCreateOrchestrator extends EventEmitter {
|
|
29
|
+
description;
|
|
30
|
+
options;
|
|
31
|
+
auditTrail = [];
|
|
32
|
+
startTime = 0;
|
|
33
|
+
constructor(description, options) {
|
|
34
|
+
super();
|
|
35
|
+
this.description = description;
|
|
36
|
+
this.options = options;
|
|
37
|
+
}
|
|
38
|
+
/** Run the full app creation pipeline. */
|
|
39
|
+
async run() {
|
|
40
|
+
this.startTime = Date.now();
|
|
41
|
+
const projectPath = join(this.options.outputPath, this.slugify(this.description.name));
|
|
42
|
+
// Phase: initializing
|
|
43
|
+
this.emitPhase({ phase: 'initializing' });
|
|
44
|
+
await mkdir(projectPath, { recursive: true });
|
|
45
|
+
await mkdir(join(projectPath, '.assay'), { recursive: true });
|
|
46
|
+
this.audit('task_status_change', { phase: 'initializing', projectPath });
|
|
47
|
+
// Phase: planning
|
|
48
|
+
this.emitPhase({ phase: 'planning' });
|
|
49
|
+
const plan = await this.runPlanningPhase(projectPath);
|
|
50
|
+
if (!plan) {
|
|
51
|
+
this.emitPhase({ phase: 'failed', reason: 'Planning failed after retries' });
|
|
52
|
+
return this.makeResult('failed', projectPath, null, [], null);
|
|
53
|
+
}
|
|
54
|
+
// Phase: verifying_plan
|
|
55
|
+
this.emitPhase({ phase: 'verifying_plan' });
|
|
56
|
+
const planValid = this.verifyPlanStructure(plan);
|
|
57
|
+
if (!planValid) {
|
|
58
|
+
this.emitPhase({ phase: 'failed', reason: 'Plan verification failed — invalid structure' });
|
|
59
|
+
return this.makeResult('failed', projectPath, plan, [], null);
|
|
60
|
+
}
|
|
61
|
+
this.audit('verification_completed', {
|
|
62
|
+
phase: 'verifying_plan',
|
|
63
|
+
features: plan.features.length,
|
|
64
|
+
schema: plan.schema.length,
|
|
65
|
+
apiRoutes: plan.apiRoutes.length,
|
|
66
|
+
});
|
|
67
|
+
// Write plan to disk
|
|
68
|
+
await writeFile(join(projectPath, '.assay', 'architecture-plan.json'), JSON.stringify(plan, null, 2), 'utf-8');
|
|
69
|
+
// Phase: scaffolding
|
|
70
|
+
this.emitPhase({ phase: 'scaffolding' });
|
|
71
|
+
const scaffoldResult = await this.runScaffoldingPhase(projectPath, plan);
|
|
72
|
+
if (scaffoldResult.status !== 'completed') {
|
|
73
|
+
this.audit('task_status_change', { phase: 'scaffolding', status: 'failed' });
|
|
74
|
+
// Continue — scaffolding failure is not necessarily fatal
|
|
75
|
+
}
|
|
76
|
+
// Phase: building features
|
|
77
|
+
const featureResults = [];
|
|
78
|
+
const completedFeatures = [];
|
|
79
|
+
const failedFeatures = [];
|
|
80
|
+
if (this.options.sequential) {
|
|
81
|
+
// Sequential mode (legacy): build features one at a time in dependency order
|
|
82
|
+
for (let i = 0; i < plan.dependencyOrder.length; i++) {
|
|
83
|
+
const featureId = plan.dependencyOrder[i];
|
|
84
|
+
const feature = plan.features.find(f => f.id === featureId);
|
|
85
|
+
if (!feature) {
|
|
86
|
+
featureResults.push({ featureId, featureName: featureId, status: 'skipped', filesCreated: [], verificationSummary: { totalClaims: 0, passed: 0, failed: 0 }, durationMs: 0, error: 'Feature not found in plan' });
|
|
87
|
+
failedFeatures.push(featureId);
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
const depsFailed = feature.dependsOn.some(dep => failedFeatures.includes(dep));
|
|
91
|
+
if (depsFailed) {
|
|
92
|
+
featureResults.push({ featureId, featureName: feature.name, status: 'skipped', filesCreated: [], verificationSummary: { totalClaims: 0, passed: 0, failed: 0 }, durationMs: 0, error: 'Dependency failed' });
|
|
93
|
+
failedFeatures.push(featureId);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
this.emitPhase({ phase: 'building_feature', featureId, featureIndex: i, totalFeatures: plan.dependencyOrder.length });
|
|
97
|
+
const featureStart = Date.now();
|
|
98
|
+
const result = await this.buildFeature(projectPath, plan, feature, completedFeatures);
|
|
99
|
+
const buildResult = { featureId, featureName: feature.name, status: result.status === 'completed' ? 'completed' : 'failed', filesCreated: result.filesCreated, verificationSummary: result.verificationSummary, durationMs: Date.now() - featureStart, error: result.error };
|
|
100
|
+
featureResults.push(buildResult);
|
|
101
|
+
if (buildResult.status === 'completed') {
|
|
102
|
+
completedFeatures.push(featureId);
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
failedFeatures.push(featureId);
|
|
106
|
+
}
|
|
107
|
+
this.audit('task_status_change', { phase: 'building_feature', featureId, status: buildResult.status, claims: buildResult.verificationSummary });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
// Wave mode (default): build independent features in parallel
|
|
112
|
+
const waves = this.computeWaves(plan);
|
|
113
|
+
for (let waveIdx = 0; waveIdx < waves.length; waveIdx++) {
|
|
114
|
+
const wave = waves[waveIdx];
|
|
115
|
+
// Filter out features whose deps failed
|
|
116
|
+
const buildable = wave.filter(featureId => {
|
|
117
|
+
const feature = plan.features.find(f => f.id === featureId);
|
|
118
|
+
if (!feature) {
|
|
119
|
+
featureResults.push({ featureId, featureName: featureId, status: 'skipped', filesCreated: [], verificationSummary: { totalClaims: 0, passed: 0, failed: 0 }, durationMs: 0, error: 'Feature not found in plan' });
|
|
120
|
+
failedFeatures.push(featureId);
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
const depsFailed = feature.dependsOn.some(dep => failedFeatures.includes(dep));
|
|
124
|
+
if (depsFailed) {
|
|
125
|
+
featureResults.push({ featureId, featureName: feature.name, status: 'skipped', filesCreated: [], verificationSummary: { totalClaims: 0, passed: 0, failed: 0 }, durationMs: 0, error: 'Dependency failed' });
|
|
126
|
+
failedFeatures.push(featureId);
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
return true;
|
|
130
|
+
});
|
|
131
|
+
if (buildable.length === 0)
|
|
132
|
+
continue;
|
|
133
|
+
this.emitPhase({ phase: 'building_wave', waveIndex: waveIdx, totalWaves: waves.length, featureIds: buildable });
|
|
134
|
+
// Build all features in this wave concurrently
|
|
135
|
+
const wavePromises = buildable.map(async (featureId) => {
|
|
136
|
+
const feature = plan.features.find(f => f.id === featureId);
|
|
137
|
+
this.emitPhase({ phase: 'building_feature', featureId, featureIndex: plan.dependencyOrder.indexOf(featureId), totalFeatures: plan.dependencyOrder.length });
|
|
138
|
+
const featureStart = Date.now();
|
|
139
|
+
// Choose direct mode vs full multi-agent loop
|
|
140
|
+
const result = this.shouldUseDirect(feature)
|
|
141
|
+
? await this.buildFeatureDirect(projectPath, plan, feature, completedFeatures)
|
|
142
|
+
: await this.buildFeature(projectPath, plan, feature, completedFeatures);
|
|
143
|
+
const buildResult = { featureId, featureName: feature.name, status: result.status === 'completed' ? 'completed' : 'failed', filesCreated: result.filesCreated, verificationSummary: result.verificationSummary, durationMs: Date.now() - featureStart, error: result.error };
|
|
144
|
+
this.audit('task_status_change', { phase: 'building_feature', featureId, status: buildResult.status, claims: buildResult.verificationSummary, wave: waveIdx, mode: this.shouldUseDirect(feature) ? 'direct' : 'multi-agent' });
|
|
145
|
+
return buildResult;
|
|
146
|
+
});
|
|
147
|
+
const waveResults = await Promise.allSettled(wavePromises);
|
|
148
|
+
for (const settled of waveResults) {
|
|
149
|
+
if (settled.status === 'fulfilled') {
|
|
150
|
+
featureResults.push(settled.value);
|
|
151
|
+
if (settled.value.status === 'completed') {
|
|
152
|
+
completedFeatures.push(settled.value.featureId);
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
failedFeatures.push(settled.value.featureId);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
// Promise rejected — shouldn't happen but handle gracefully
|
|
160
|
+
const featureId = buildable[waveResults.indexOf(settled)];
|
|
161
|
+
featureResults.push({ featureId, featureName: featureId, status: 'failed', filesCreated: [], verificationSummary: { totalClaims: 0, passed: 0, failed: 0 }, durationMs: 0, error: `Build threw: ${settled.reason}` });
|
|
162
|
+
failedFeatures.push(featureId);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
// Phase: cross-verifying
|
|
168
|
+
this.emitPhase({ phase: 'cross_verifying' });
|
|
169
|
+
const crossVerification = await this.runCrossVerification(projectPath, plan);
|
|
170
|
+
this.audit('verification_completed', {
|
|
171
|
+
phase: 'cross_verifying',
|
|
172
|
+
passed: crossVerification.passedCount,
|
|
173
|
+
failed: crossVerification.failedCount,
|
|
174
|
+
});
|
|
175
|
+
// Phase: finalizing
|
|
176
|
+
this.emitPhase({ phase: 'finalizing' });
|
|
177
|
+
const allCompleted = featureResults.every(r => r.status === 'completed');
|
|
178
|
+
const anyCompleted = featureResults.some(r => r.status === 'completed');
|
|
179
|
+
const finalStatus = allCompleted ? 'completed' : anyCompleted ? 'partial' : 'failed';
|
|
180
|
+
this.emitPhase({ phase: 'completed' });
|
|
181
|
+
return this.makeResult(finalStatus, projectPath, plan, featureResults, crossVerification);
|
|
182
|
+
}
|
|
183
|
+
// ── Planning Phase ─────────────────────────────────────────
|
|
184
|
+
async runPlanningPhase(projectPath) {
|
|
185
|
+
const messageBus = new MessageBus();
|
|
186
|
+
const planner = new PlannerAgent(messageBus, { model: this.options.models?.planner });
|
|
187
|
+
const maxRetries = this.options.maxRetries ?? 3;
|
|
188
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
189
|
+
const result = await planner.executeTask({
|
|
190
|
+
taskId: `plan-${randomUUID().slice(0, 8)}`,
|
|
191
|
+
goal: `Design architecture for: ${this.description.description}`,
|
|
192
|
+
constraints: [
|
|
193
|
+
`App name: ${this.description.name}`,
|
|
194
|
+
`Framework: ${this.description.techStack.framework}`,
|
|
195
|
+
`Database: ${this.description.techStack.database}`,
|
|
196
|
+
`Language: ${this.description.techStack.language}`,
|
|
197
|
+
`Features: ${this.description.features.join(', ')}`,
|
|
198
|
+
...(this.description.authModel
|
|
199
|
+
? [`Auth: ${this.description.authModel.methods.join(', ')} via ${this.description.authModel.provider}`]
|
|
200
|
+
: []),
|
|
201
|
+
...(this.description.constraints ?? []),
|
|
202
|
+
],
|
|
203
|
+
dependencies: [],
|
|
204
|
+
contextRefs: [],
|
|
205
|
+
});
|
|
206
|
+
if (result.status === 'completed' && result.artifacts.length > 0) {
|
|
207
|
+
try {
|
|
208
|
+
const plan = JSON.parse(result.artifacts[0].content);
|
|
209
|
+
if (plan.features && plan.features.length > 0) {
|
|
210
|
+
return plan;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
// Parse failed, retry
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
this.audit('task_status_change', {
|
|
218
|
+
phase: 'planning',
|
|
219
|
+
attempt,
|
|
220
|
+
status: 'retry',
|
|
221
|
+
reason: result.summary,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
// ── Plan Verification ──────────────────────────────────────
|
|
227
|
+
verifyPlanStructure(plan) {
|
|
228
|
+
// Check dependency order is acyclic
|
|
229
|
+
if (!this.isAcyclic(plan.features, plan.dependencyOrder)) {
|
|
230
|
+
this.audit('verification_completed', { check: 'acyclic', verdict: 'FAIL' });
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
// Check all features in dependencyOrder exist
|
|
234
|
+
for (const id of plan.dependencyOrder) {
|
|
235
|
+
if (!plan.features.find(f => f.id === id)) {
|
|
236
|
+
this.audit('verification_completed', { check: 'feature_exists', featureId: id, verdict: 'FAIL' });
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
// Check all features are in dependencyOrder
|
|
241
|
+
for (const feature of plan.features) {
|
|
242
|
+
if (!plan.dependencyOrder.includes(feature.id)) {
|
|
243
|
+
this.audit('verification_completed', { check: 'feature_in_order', featureId: feature.id, verdict: 'FAIL' });
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return true;
|
|
248
|
+
}
|
|
249
|
+
isAcyclic(features, order) {
|
|
250
|
+
const visited = new Set();
|
|
251
|
+
const inStack = new Set();
|
|
252
|
+
const depMap = new Map();
|
|
253
|
+
for (const f of features) {
|
|
254
|
+
depMap.set(f.id, f.dependsOn);
|
|
255
|
+
}
|
|
256
|
+
const visit = (id) => {
|
|
257
|
+
if (inStack.has(id))
|
|
258
|
+
return false; // cycle
|
|
259
|
+
if (visited.has(id))
|
|
260
|
+
return true;
|
|
261
|
+
inStack.add(id);
|
|
262
|
+
for (const dep of depMap.get(id) ?? []) {
|
|
263
|
+
if (!visit(dep))
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
inStack.delete(id);
|
|
267
|
+
visited.add(id);
|
|
268
|
+
return true;
|
|
269
|
+
};
|
|
270
|
+
for (const id of order) {
|
|
271
|
+
if (!visit(id))
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
return true;
|
|
275
|
+
}
|
|
276
|
+
// ── Scaffolding Phase ──────────────────────────────────────
|
|
277
|
+
async runScaffoldingPhase(projectPath, plan) {
|
|
278
|
+
const schemaDesc = plan.schema
|
|
279
|
+
.map(e => `${e.name}: ${e.fields.map(f => `${f.name}:${f.type}`).join(', ')}`)
|
|
280
|
+
.join('\n');
|
|
281
|
+
const authDesc = plan.authPlan
|
|
282
|
+
? `Auth: ${plan.authPlan.provider} with ${plan.authPlan.methods.join(', ')}`
|
|
283
|
+
: 'No auth';
|
|
284
|
+
const scaffoldGoal = `Scaffold base project: package.json, tsconfig.json, directory structure, database schema, and auth setup.
|
|
285
|
+
|
|
286
|
+
Framework: ${this.description.techStack.framework}
|
|
287
|
+
Database: ${this.description.techStack.database}
|
|
288
|
+
Language: ${this.description.techStack.language}
|
|
289
|
+
|
|
290
|
+
Schema:
|
|
291
|
+
${schemaDesc}
|
|
292
|
+
|
|
293
|
+
${authDesc}
|
|
294
|
+
|
|
295
|
+
Create the project foundation files. Do NOT implement features — only the base project structure and schema.`;
|
|
296
|
+
// Direct mode: single CodeAgent call for scaffolding (bypasses full team)
|
|
297
|
+
const messageBus = new MessageBus();
|
|
298
|
+
const codeAgent = new CodeAgent(messageBus, { model: this.options.models?.code });
|
|
299
|
+
try {
|
|
300
|
+
const result = await codeAgent.executeTask({
|
|
301
|
+
taskId: `scaffold-${randomUUID().slice(0, 8)}`,
|
|
302
|
+
goal: scaffoldGoal,
|
|
303
|
+
constraints: [],
|
|
304
|
+
dependencies: [],
|
|
305
|
+
contextRefs: [],
|
|
306
|
+
});
|
|
307
|
+
if (result.status === 'failed') {
|
|
308
|
+
this.audit('task_status_change', { phase: 'scaffolding', status: 'failed', reason: result.summary });
|
|
309
|
+
return { status: 'failed' };
|
|
310
|
+
}
|
|
311
|
+
// Write artifacts to disk
|
|
312
|
+
for (const artifact of result.artifacts) {
|
|
313
|
+
if (!artifact.path)
|
|
314
|
+
continue;
|
|
315
|
+
const absPath = join(projectPath, artifact.path);
|
|
316
|
+
try {
|
|
317
|
+
await mkdir(dirname(absPath), { recursive: true });
|
|
318
|
+
await writeFile(absPath, artifact.content, 'utf-8');
|
|
319
|
+
}
|
|
320
|
+
catch {
|
|
321
|
+
// Best-effort write
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
this.audit('task_status_change', {
|
|
325
|
+
phase: 'scaffolding',
|
|
326
|
+
status: 'completed',
|
|
327
|
+
filesCreated: result.artifacts.filter(a => a.path).map(a => a.path),
|
|
328
|
+
});
|
|
329
|
+
return { status: 'completed' };
|
|
330
|
+
}
|
|
331
|
+
catch (err) {
|
|
332
|
+
this.audit('task_status_change', { phase: 'scaffolding', status: 'failed', error: err instanceof Error ? err.message : String(err) });
|
|
333
|
+
return { status: 'failed' };
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
// ── Feature Build Phase ────────────────────────────────────
|
|
337
|
+
async buildFeature(projectPath, plan, feature, completedFeatures) {
|
|
338
|
+
const goal = this.buildFeatureGoal(plan, feature, completedFeatures);
|
|
339
|
+
const tier = this.getVerificationTier(feature);
|
|
340
|
+
const agents = ['coordinator', 'code', 'review', 'test'];
|
|
341
|
+
const loop = new MultiAgentLoop(projectPath, {
|
|
342
|
+
goal,
|
|
343
|
+
agents,
|
|
344
|
+
safetyPolicy: DEFAULT_SAFETY,
|
|
345
|
+
maxConcurrentTasks: this.options.maxConcurrentTasks ?? 3,
|
|
346
|
+
stallTimeoutMs: this.options.stallTimeoutMs ?? 120_000,
|
|
347
|
+
maxTotalAttempts: 15,
|
|
348
|
+
models: this.options.models,
|
|
349
|
+
verificationTier: tier,
|
|
350
|
+
useFastVerification: tier === 'full' && !this.options.fullVerification,
|
|
351
|
+
});
|
|
352
|
+
const result = await loop.run();
|
|
353
|
+
this.auditTrail.push(...result.auditTrail);
|
|
354
|
+
// Write artifacts to disk and collect file paths
|
|
355
|
+
const filesCreated = [];
|
|
356
|
+
for (const handoff of result.artifacts) {
|
|
357
|
+
const path = handoff.artifact.path;
|
|
358
|
+
if (!path)
|
|
359
|
+
continue;
|
|
360
|
+
const absPath = join(projectPath, path);
|
|
361
|
+
try {
|
|
362
|
+
await mkdir(dirname(absPath), { recursive: true });
|
|
363
|
+
await writeFile(absPath, handoff.artifact.content, 'utf-8');
|
|
364
|
+
filesCreated.push(path);
|
|
365
|
+
}
|
|
366
|
+
catch {
|
|
367
|
+
// Best-effort write
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
// Aggregate verification stats
|
|
371
|
+
let totalClaims = 0;
|
|
372
|
+
let passed = 0;
|
|
373
|
+
let failed = 0;
|
|
374
|
+
for (const handoff of result.artifacts) {
|
|
375
|
+
totalClaims += handoff.verificationResult.total;
|
|
376
|
+
passed += handoff.verificationResult.passed;
|
|
377
|
+
failed += handoff.verificationResult.failed;
|
|
378
|
+
}
|
|
379
|
+
return {
|
|
380
|
+
status: result.status === 'completed' ? 'completed' : 'failed',
|
|
381
|
+
filesCreated,
|
|
382
|
+
verificationSummary: { totalClaims, passed, failed },
|
|
383
|
+
error: result.status !== 'completed' ? `Feature build ${result.status}` : undefined,
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
// ── Cross-Feature Verification ─────────────────────────────
|
|
387
|
+
async runCrossVerification(projectPath, plan) {
|
|
388
|
+
const checks = [];
|
|
389
|
+
// Check 1: API routes have corresponding files
|
|
390
|
+
for (const route of plan.apiRoutes) {
|
|
391
|
+
const expectedPath = this.routeToFilePath(route.path, this.description.techStack.framework);
|
|
392
|
+
const exists = await this.fileExists(join(projectPath, expectedPath));
|
|
393
|
+
checks.push({
|
|
394
|
+
type: 'api_route_exists',
|
|
395
|
+
description: `${route.method} ${route.path} has handler at ${expectedPath}`,
|
|
396
|
+
verdict: exists ? 'PASS' : 'FAIL',
|
|
397
|
+
evidence: exists ? `File exists: ${expectedPath}` : `Missing: ${expectedPath}`,
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
// Check 2: Pages have corresponding files
|
|
401
|
+
for (const page of plan.pages) {
|
|
402
|
+
const expectedPath = this.pageToFilePath(page.path, page.component, this.description.techStack.framework);
|
|
403
|
+
const exists = await this.fileExists(join(projectPath, expectedPath));
|
|
404
|
+
checks.push({
|
|
405
|
+
type: 'page_references_valid',
|
|
406
|
+
description: `Page ${page.path} has component at ${expectedPath}`,
|
|
407
|
+
verdict: exists ? 'PASS' : 'FAIL',
|
|
408
|
+
evidence: exists ? `File exists: ${expectedPath}` : `Missing: ${expectedPath}`,
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
// Check 3: Schema entities exist in schema/migration files
|
|
412
|
+
for (const entity of plan.schema) {
|
|
413
|
+
const found = await this.searchForSchemaEntity(projectPath, entity.name);
|
|
414
|
+
checks.push({
|
|
415
|
+
type: 'schema_entity_exists',
|
|
416
|
+
description: `Schema entity "${entity.name}" defined in project`,
|
|
417
|
+
verdict: found ? 'PASS' : 'FAIL',
|
|
418
|
+
evidence: found ? `Found in project files` : `Not found in schema/migration files`,
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
// Check 4: Dependency consistency — features that depend on others were built after them
|
|
422
|
+
for (const feature of plan.features) {
|
|
423
|
+
for (const dep of feature.dependsOn) {
|
|
424
|
+
const depIndex = plan.dependencyOrder.indexOf(dep);
|
|
425
|
+
const featureIndex = plan.dependencyOrder.indexOf(feature.id);
|
|
426
|
+
const ordered = depIndex < featureIndex;
|
|
427
|
+
checks.push({
|
|
428
|
+
type: 'dependency_consistency',
|
|
429
|
+
description: `${feature.id} depends on ${dep} — build order correct`,
|
|
430
|
+
verdict: ordered ? 'PASS' : 'FAIL',
|
|
431
|
+
evidence: ordered
|
|
432
|
+
? `${dep} (index ${depIndex}) built before ${feature.id} (index ${featureIndex})`
|
|
433
|
+
: `${dep} (index ${depIndex}) NOT before ${feature.id} (index ${featureIndex})`,
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
const passedCount = checks.filter(c => c.verdict === 'PASS').length;
|
|
438
|
+
const failedCount = checks.filter(c => c.verdict === 'FAIL').length;
|
|
439
|
+
return {
|
|
440
|
+
checks,
|
|
441
|
+
passedCount,
|
|
442
|
+
failedCount,
|
|
443
|
+
verdict: failedCount === 0 ? 'PASS' : passedCount === 0 ? 'FAIL' : 'PARTIAL',
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
// ── Wave Computation ──────────────────────────────────────
|
|
447
|
+
/**
|
|
448
|
+
* Group features into waves based on dependency graph.
|
|
449
|
+
* Wave 0: features with no dependencies
|
|
450
|
+
* Wave N: features whose deps are all in waves < N
|
|
451
|
+
*/
|
|
452
|
+
computeWaves(plan) {
|
|
453
|
+
const featureMap = new Map(plan.features.map(f => [f.id, f]));
|
|
454
|
+
const waves = [];
|
|
455
|
+
const placed = new Set();
|
|
456
|
+
// Iteratively find features whose deps are all placed
|
|
457
|
+
let remaining = [...plan.dependencyOrder];
|
|
458
|
+
while (remaining.length > 0) {
|
|
459
|
+
const wave = [];
|
|
460
|
+
for (const id of remaining) {
|
|
461
|
+
const feature = featureMap.get(id);
|
|
462
|
+
if (!feature)
|
|
463
|
+
continue;
|
|
464
|
+
const allDepsPlaced = feature.dependsOn.every(dep => placed.has(dep));
|
|
465
|
+
if (allDepsPlaced) {
|
|
466
|
+
wave.push(id);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
if (wave.length === 0) {
|
|
470
|
+
// Remaining features have unresolvable deps — put them all in final wave
|
|
471
|
+
waves.push(remaining);
|
|
472
|
+
break;
|
|
473
|
+
}
|
|
474
|
+
waves.push(wave);
|
|
475
|
+
for (const id of wave)
|
|
476
|
+
placed.add(id);
|
|
477
|
+
remaining = remaining.filter(id => !placed.has(id));
|
|
478
|
+
}
|
|
479
|
+
return waves;
|
|
480
|
+
}
|
|
481
|
+
// ── Direct Generation Mode ───────────────────────────────
|
|
482
|
+
/**
|
|
483
|
+
* Build a feature using a single CodeAgent call — bypasses MultiAgentLoop.
|
|
484
|
+
* Used for trivial/small features where full team coordination is overkill.
|
|
485
|
+
*/
|
|
486
|
+
async buildFeatureDirect(projectPath, plan, feature, completedFeatures) {
|
|
487
|
+
const goal = this.buildFeatureGoal(plan, feature, completedFeatures);
|
|
488
|
+
const messageBus = new MessageBus();
|
|
489
|
+
const codeAgent = new CodeAgent(messageBus, { model: this.options.models?.code });
|
|
490
|
+
try {
|
|
491
|
+
const result = await codeAgent.executeTask({
|
|
492
|
+
taskId: `direct-${feature.id}-${randomUUID().slice(0, 8)}`,
|
|
493
|
+
goal,
|
|
494
|
+
constraints: [
|
|
495
|
+
`Framework: ${this.description.techStack.framework}`,
|
|
496
|
+
`Database: ${this.description.techStack.database}`,
|
|
497
|
+
`Language: ${this.description.techStack.language}`,
|
|
498
|
+
],
|
|
499
|
+
dependencies: [],
|
|
500
|
+
contextRefs: [],
|
|
501
|
+
});
|
|
502
|
+
if (result.status === 'failed') {
|
|
503
|
+
return {
|
|
504
|
+
status: 'failed',
|
|
505
|
+
filesCreated: [],
|
|
506
|
+
verificationSummary: { totalClaims: 0, passed: 0, failed: 0 },
|
|
507
|
+
error: result.summary,
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
// Write artifacts to disk
|
|
511
|
+
const filesCreated = [];
|
|
512
|
+
for (const artifact of result.artifacts) {
|
|
513
|
+
if (!artifact.path)
|
|
514
|
+
continue;
|
|
515
|
+
const absPath = join(projectPath, artifact.path);
|
|
516
|
+
try {
|
|
517
|
+
await mkdir(dirname(absPath), { recursive: true });
|
|
518
|
+
await writeFile(absPath, artifact.content, 'utf-8');
|
|
519
|
+
filesCreated.push(artifact.path);
|
|
520
|
+
}
|
|
521
|
+
catch {
|
|
522
|
+
// Best-effort write
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
// In direct mode (single agent), use lightweight (regex-only) verification
|
|
526
|
+
// since there's no cross-agent boundary to verify. This avoids LLM calls
|
|
527
|
+
// per-file while still catching TODOs, empty functions, hardcoded secrets.
|
|
528
|
+
const tier = this.getVerificationTier(feature);
|
|
529
|
+
let totalClaims = 0;
|
|
530
|
+
let passed = 0;
|
|
531
|
+
let failed = 0;
|
|
532
|
+
if (tier !== 'skip' && filesCreated.length > 0) {
|
|
533
|
+
const verifier = new CompositionVerifier(projectPath);
|
|
534
|
+
const sourceIdentity = codeAgent.getIdentity();
|
|
535
|
+
const targetIdentity = sourceIdentity; // Self-review in direct mode
|
|
536
|
+
for (const artifact of result.artifacts) {
|
|
537
|
+
if (!artifact.path)
|
|
538
|
+
continue;
|
|
539
|
+
// Always use lightweight in direct mode — no cross-agent handoff to verify
|
|
540
|
+
const handoff = await verifier.verifyHandoffLightweight(sourceIdentity, targetIdentity, artifact);
|
|
541
|
+
totalClaims += handoff.verificationResult.total;
|
|
542
|
+
passed += handoff.verificationResult.passed;
|
|
543
|
+
failed += handoff.verificationResult.failed;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
return {
|
|
547
|
+
status: 'completed',
|
|
548
|
+
filesCreated,
|
|
549
|
+
verificationSummary: { totalClaims, passed, failed },
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
catch (err) {
|
|
553
|
+
return {
|
|
554
|
+
status: 'failed',
|
|
555
|
+
filesCreated: [],
|
|
556
|
+
verificationSummary: { totalClaims: 0, passed: 0, failed: 0 },
|
|
557
|
+
error: err instanceof Error ? err.message : String(err),
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
/** Whether to use direct generation (single CodeAgent) vs full MultiAgentLoop.
|
|
562
|
+
* For app creation (greenfield), direct mode is used for ALL features by default
|
|
563
|
+
* since there's no existing codebase to review against and the architecture plan
|
|
564
|
+
* already provides full context. Multi-agent mode adds overhead without proportional value. */
|
|
565
|
+
shouldUseDirect(_feature) {
|
|
566
|
+
if (this.options.noDirectMode)
|
|
567
|
+
return false;
|
|
568
|
+
return true; // Default to direct mode for all features in create pipeline
|
|
569
|
+
}
|
|
570
|
+
/** Determine verification tier based on feature characteristics. */
|
|
571
|
+
getVerificationTier(feature) {
|
|
572
|
+
if (this.options.fullVerification)
|
|
573
|
+
return 'full';
|
|
574
|
+
// Auth/security features always get full verification
|
|
575
|
+
const hasAuth = feature.name.toLowerCase().includes('auth')
|
|
576
|
+
|| feature.schemaEntities.some(e => e.toLowerCase().includes('user') || e.toLowerCase().includes('session'));
|
|
577
|
+
if (hasAuth)
|
|
578
|
+
return 'full';
|
|
579
|
+
// Features with API routes get at least lightweight
|
|
580
|
+
if (feature.apiRoutes.length > 0)
|
|
581
|
+
return 'lightweight';
|
|
582
|
+
// UI-only trivial features can be skipped
|
|
583
|
+
if (feature.complexityEstimate === 'trivial')
|
|
584
|
+
return 'skip';
|
|
585
|
+
// Small UI-only features get lightweight
|
|
586
|
+
if (feature.complexityEstimate === 'small' && feature.apiRoutes.length === 0)
|
|
587
|
+
return 'lightweight';
|
|
588
|
+
return 'full';
|
|
589
|
+
}
|
|
590
|
+
// ── Feature Goal Builder ─────────────────────────────────
|
|
591
|
+
/** Build the goal string for a feature (shared between buildFeature and buildFeatureDirect). */
|
|
592
|
+
buildFeatureGoal(plan, feature, completedFeatures) {
|
|
593
|
+
const relevantSchema = plan.schema
|
|
594
|
+
.filter(e => feature.schemaEntities.includes(e.name))
|
|
595
|
+
.map(e => `${e.name}: ${e.fields.map(f => `${f.name}:${f.type}`).join(', ')}`)
|
|
596
|
+
.join('\n');
|
|
597
|
+
const relevantRoutes = plan.apiRoutes
|
|
598
|
+
.filter(r => r.featureId === feature.id)
|
|
599
|
+
.map(r => `${r.method} ${r.path} — ${r.description}`)
|
|
600
|
+
.join('\n');
|
|
601
|
+
const relevantPages = plan.pages
|
|
602
|
+
.filter(p => p.featureId === feature.id)
|
|
603
|
+
.map(p => `${p.path} — ${p.component}: ${p.description ?? ''}`)
|
|
604
|
+
.join('\n');
|
|
605
|
+
return `Implement feature: ${feature.name}
|
|
606
|
+
|
|
607
|
+
Feature description: Build the "${feature.name}" feature for a ${this.description.techStack.framework} application.
|
|
608
|
+
|
|
609
|
+
Schema entities:
|
|
610
|
+
${relevantSchema || 'None'}
|
|
611
|
+
|
|
612
|
+
API routes to implement:
|
|
613
|
+
${relevantRoutes || 'None'}
|
|
614
|
+
|
|
615
|
+
Pages to implement:
|
|
616
|
+
${relevantPages || 'None'}
|
|
617
|
+
|
|
618
|
+
Tech stack: ${this.description.techStack.framework}, ${this.description.techStack.database}, ${this.description.techStack.language}
|
|
619
|
+
|
|
620
|
+
Previously completed features: ${completedFeatures.join(', ') || 'None'}
|
|
621
|
+
|
|
622
|
+
Implement all API routes, pages, and business logic for this feature. Write complete, working code — no TODOs or stubs.`;
|
|
623
|
+
}
|
|
624
|
+
// ── Helpers ────────────────────────────────────────────────
|
|
625
|
+
emitPhase(phase) {
|
|
626
|
+
const progress = {
|
|
627
|
+
phase,
|
|
628
|
+
currentFeatureIndex: 'featureIndex' in phase ? phase.featureIndex : 0,
|
|
629
|
+
totalFeatures: 'totalFeatures' in phase ? phase.totalFeatures : 0,
|
|
630
|
+
completedFeatures: [],
|
|
631
|
+
failedFeatures: [],
|
|
632
|
+
elapsedMs: Date.now() - this.startTime,
|
|
633
|
+
};
|
|
634
|
+
this.emit('progress', progress);
|
|
635
|
+
}
|
|
636
|
+
makeResult(status, projectPath, plan, featureResults, crossVerification) {
|
|
637
|
+
return {
|
|
638
|
+
status,
|
|
639
|
+
projectPath,
|
|
640
|
+
plan,
|
|
641
|
+
featureResults,
|
|
642
|
+
crossVerification,
|
|
643
|
+
totalDurationMs: Date.now() - this.startTime,
|
|
644
|
+
auditTrail: this.auditTrail,
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
audit(eventType, details) {
|
|
648
|
+
const entry = {
|
|
649
|
+
id: randomUUID(),
|
|
650
|
+
eventType: eventType,
|
|
651
|
+
agentName: 'app-create-orchestrator',
|
|
652
|
+
timestamp: new Date().toISOString(),
|
|
653
|
+
details,
|
|
654
|
+
relatedIds: {},
|
|
655
|
+
};
|
|
656
|
+
this.auditTrail.push(entry);
|
|
657
|
+
this.emit('audit', entry);
|
|
658
|
+
}
|
|
659
|
+
slugify(name) {
|
|
660
|
+
return name
|
|
661
|
+
.toLowerCase()
|
|
662
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
663
|
+
.replace(/^-+|-+$/g, '');
|
|
664
|
+
}
|
|
665
|
+
routeToFilePath(routePath, framework) {
|
|
666
|
+
if (framework === 'next.js') {
|
|
667
|
+
// /api/users -> src/app/api/users/route.ts
|
|
668
|
+
return `src/app${routePath}/route.ts`;
|
|
669
|
+
}
|
|
670
|
+
// Express or similar: src/routes/<path>.ts
|
|
671
|
+
const parts = routePath.replace(/^\/api/, '').replace(/^\//, '');
|
|
672
|
+
return `src/routes/${parts || 'index'}.ts`;
|
|
673
|
+
}
|
|
674
|
+
pageToFilePath(pagePath, _component, framework) {
|
|
675
|
+
if (framework === 'next.js') {
|
|
676
|
+
// / -> src/app/page.tsx, /dashboard -> src/app/dashboard/page.tsx
|
|
677
|
+
const normalized = pagePath === '/' ? '' : pagePath;
|
|
678
|
+
return `src/app${normalized}/page.tsx`;
|
|
679
|
+
}
|
|
680
|
+
if (framework === 'sveltekit') {
|
|
681
|
+
const normalized = pagePath === '/' ? '' : pagePath;
|
|
682
|
+
return `src/routes${normalized}/+page.svelte`;
|
|
683
|
+
}
|
|
684
|
+
// Generic: src/pages/<path>.tsx
|
|
685
|
+
const normalized = pagePath === '/' ? '/index' : pagePath;
|
|
686
|
+
return `src/pages${normalized}.tsx`;
|
|
687
|
+
}
|
|
688
|
+
async fileExists(path) {
|
|
689
|
+
try {
|
|
690
|
+
await readFile(path);
|
|
691
|
+
return true;
|
|
692
|
+
}
|
|
693
|
+
catch {
|
|
694
|
+
return false;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
async searchForSchemaEntity(projectPath, entityName) {
|
|
698
|
+
// Check common schema file locations
|
|
699
|
+
const candidates = [
|
|
700
|
+
'schema.prisma',
|
|
701
|
+
'prisma/schema.prisma',
|
|
702
|
+
'drizzle/schema.ts',
|
|
703
|
+
'src/db/schema.ts',
|
|
704
|
+
'supabase/migrations',
|
|
705
|
+
'src/schema.ts',
|
|
706
|
+
'db/schema.ts',
|
|
707
|
+
];
|
|
708
|
+
for (const candidate of candidates) {
|
|
709
|
+
try {
|
|
710
|
+
const content = await readFile(join(projectPath, candidate), 'utf-8');
|
|
711
|
+
if (content.includes(entityName) || content.includes(entityName.toLowerCase())) {
|
|
712
|
+
return true;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
catch {
|
|
716
|
+
// File doesn't exist, continue
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
return false;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
//# sourceMappingURL=app-create-orchestrator.js.map
|