tryassay 0.13.0 → 0.15.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 +145 -1
- package/dist/api/server.js.map +1 -1
- package/dist/cli.js +21 -1
- package/dist/cli.js.map +1 -1
- package/dist/commands/create.d.ts +18 -0
- package/dist/commands/create.js +135 -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 +62 -0
- package/dist/runtime/app-create-orchestrator.js +923 -0
- package/dist/runtime/app-create-orchestrator.js.map +1 -0
- package/dist/runtime/build-verifier.d.ts +32 -0
- package/dist/runtime/build-verifier.js +508 -0
- package/dist/runtime/build-verifier.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 +212 -2
- package/package.json +1 -1
|
@@ -0,0 +1,923 @@
|
|
|
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, readdir, stat } 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
|
+
import { BuildVerifier } from './build-verifier.js';
|
|
16
|
+
// ── Default safety policy ───────────────────────────────────
|
|
17
|
+
const DEFAULT_SAFETY = {
|
|
18
|
+
formalOverridesConsensus: true,
|
|
19
|
+
noSelfVerification: true,
|
|
20
|
+
criticalClaimsRequireFormal: true,
|
|
21
|
+
escalationRules: {
|
|
22
|
+
maxFormalOverridesBeforeHalt: 3,
|
|
23
|
+
maxTrustDemotions: 2,
|
|
24
|
+
maxRejectCyclesPerTask: 3,
|
|
25
|
+
},
|
|
26
|
+
preferModelDiversity: false,
|
|
27
|
+
};
|
|
28
|
+
// ── App Create Orchestrator ─────────────────────────────────
|
|
29
|
+
export class AppCreateOrchestrator extends EventEmitter {
|
|
30
|
+
description;
|
|
31
|
+
options;
|
|
32
|
+
auditTrail = [];
|
|
33
|
+
startTime = 0;
|
|
34
|
+
constructor(description, options) {
|
|
35
|
+
super();
|
|
36
|
+
this.description = description;
|
|
37
|
+
this.options = options;
|
|
38
|
+
}
|
|
39
|
+
/** Run the full app creation pipeline. */
|
|
40
|
+
async run() {
|
|
41
|
+
this.startTime = Date.now();
|
|
42
|
+
const projectPath = join(this.options.outputPath, this.slugify(this.description.name));
|
|
43
|
+
// Phase: initializing
|
|
44
|
+
this.emitPhase({ phase: 'initializing' });
|
|
45
|
+
await mkdir(projectPath, { recursive: true });
|
|
46
|
+
await mkdir(join(projectPath, '.assay'), { recursive: true });
|
|
47
|
+
this.audit('task_status_change', { phase: 'initializing', projectPath });
|
|
48
|
+
// Phase: planning
|
|
49
|
+
this.emitPhase({ phase: 'planning' });
|
|
50
|
+
const plan = await this.runPlanningPhase(projectPath);
|
|
51
|
+
if (!plan) {
|
|
52
|
+
this.emitPhase({ phase: 'failed', reason: 'Planning failed after retries' });
|
|
53
|
+
return this.makeResult('failed', projectPath, null, [], null, null);
|
|
54
|
+
}
|
|
55
|
+
// Phase: verifying_plan
|
|
56
|
+
this.emitPhase({ phase: 'verifying_plan' });
|
|
57
|
+
const planValid = this.verifyPlanStructure(plan);
|
|
58
|
+
if (!planValid) {
|
|
59
|
+
this.emitPhase({ phase: 'failed', reason: 'Plan verification failed — invalid structure' });
|
|
60
|
+
return this.makeResult('failed', projectPath, plan, [], null, null);
|
|
61
|
+
}
|
|
62
|
+
this.audit('verification_completed', {
|
|
63
|
+
phase: 'verifying_plan',
|
|
64
|
+
features: plan.features.length,
|
|
65
|
+
schema: plan.schema.length,
|
|
66
|
+
apiRoutes: plan.apiRoutes.length,
|
|
67
|
+
});
|
|
68
|
+
// Write plan to disk
|
|
69
|
+
await writeFile(join(projectPath, '.assay', 'architecture-plan.json'), JSON.stringify(plan, null, 2), 'utf-8');
|
|
70
|
+
// Phase: scaffolding
|
|
71
|
+
this.emitPhase({ phase: 'scaffolding' });
|
|
72
|
+
const scaffoldResult = await this.runScaffoldingPhase(projectPath, plan);
|
|
73
|
+
if (scaffoldResult.status !== 'completed') {
|
|
74
|
+
this.audit('task_status_change', { phase: 'scaffolding', status: 'failed' });
|
|
75
|
+
// Continue — scaffolding failure is not necessarily fatal
|
|
76
|
+
}
|
|
77
|
+
// Phase: building features
|
|
78
|
+
const featureResults = [];
|
|
79
|
+
const completedFeatures = [];
|
|
80
|
+
const failedFeatures = [];
|
|
81
|
+
if (this.options.sequential) {
|
|
82
|
+
// Sequential mode (legacy): build features one at a time in dependency order
|
|
83
|
+
for (let i = 0; i < plan.dependencyOrder.length; i++) {
|
|
84
|
+
const featureId = plan.dependencyOrder[i];
|
|
85
|
+
const feature = plan.features.find(f => f.id === featureId);
|
|
86
|
+
if (!feature) {
|
|
87
|
+
featureResults.push({ featureId, featureName: featureId, status: 'skipped', filesCreated: [], verificationSummary: { totalClaims: 0, passed: 0, failed: 0 }, durationMs: 0, error: 'Feature not found in plan' });
|
|
88
|
+
failedFeatures.push(featureId);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
const depsFailed = feature.dependsOn.some(dep => failedFeatures.includes(dep));
|
|
92
|
+
if (depsFailed) {
|
|
93
|
+
featureResults.push({ featureId, featureName: feature.name, status: 'skipped', filesCreated: [], verificationSummary: { totalClaims: 0, passed: 0, failed: 0 }, durationMs: 0, error: 'Dependency failed' });
|
|
94
|
+
failedFeatures.push(featureId);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
this.emitPhase({ phase: 'building_feature', featureId, featureIndex: i, totalFeatures: plan.dependencyOrder.length });
|
|
98
|
+
const featureStart = Date.now();
|
|
99
|
+
const result = await this.buildFeature(projectPath, plan, feature, completedFeatures);
|
|
100
|
+
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 };
|
|
101
|
+
featureResults.push(buildResult);
|
|
102
|
+
if (buildResult.status === 'completed') {
|
|
103
|
+
completedFeatures.push(featureId);
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
failedFeatures.push(featureId);
|
|
107
|
+
}
|
|
108
|
+
this.audit('task_status_change', { phase: 'building_feature', featureId, status: buildResult.status, claims: buildResult.verificationSummary });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
// Wave mode (default): build independent features in parallel
|
|
113
|
+
const waves = this.computeWaves(plan);
|
|
114
|
+
for (let waveIdx = 0; waveIdx < waves.length; waveIdx++) {
|
|
115
|
+
const wave = waves[waveIdx];
|
|
116
|
+
// Filter out features whose deps failed
|
|
117
|
+
const buildable = wave.filter(featureId => {
|
|
118
|
+
const feature = plan.features.find(f => f.id === featureId);
|
|
119
|
+
if (!feature) {
|
|
120
|
+
featureResults.push({ featureId, featureName: featureId, status: 'skipped', filesCreated: [], verificationSummary: { totalClaims: 0, passed: 0, failed: 0 }, durationMs: 0, error: 'Feature not found in plan' });
|
|
121
|
+
failedFeatures.push(featureId);
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
const depsFailed = feature.dependsOn.some(dep => failedFeatures.includes(dep));
|
|
125
|
+
if (depsFailed) {
|
|
126
|
+
featureResults.push({ featureId, featureName: feature.name, status: 'skipped', filesCreated: [], verificationSummary: { totalClaims: 0, passed: 0, failed: 0 }, durationMs: 0, error: 'Dependency failed' });
|
|
127
|
+
failedFeatures.push(featureId);
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
return true;
|
|
131
|
+
});
|
|
132
|
+
if (buildable.length === 0)
|
|
133
|
+
continue;
|
|
134
|
+
this.emitPhase({ phase: 'building_wave', waveIndex: waveIdx, totalWaves: waves.length, featureIds: buildable });
|
|
135
|
+
// Build all features in this wave concurrently
|
|
136
|
+
const wavePromises = buildable.map(async (featureId) => {
|
|
137
|
+
const feature = plan.features.find(f => f.id === featureId);
|
|
138
|
+
this.emitPhase({ phase: 'building_feature', featureId, featureIndex: plan.dependencyOrder.indexOf(featureId), totalFeatures: plan.dependencyOrder.length });
|
|
139
|
+
const featureStart = Date.now();
|
|
140
|
+
// Choose direct mode vs full multi-agent loop
|
|
141
|
+
const result = this.shouldUseDirect(feature)
|
|
142
|
+
? await this.buildFeatureDirect(projectPath, plan, feature, completedFeatures)
|
|
143
|
+
: await this.buildFeature(projectPath, plan, feature, completedFeatures);
|
|
144
|
+
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 };
|
|
145
|
+
this.audit('task_status_change', { phase: 'building_feature', featureId, status: buildResult.status, claims: buildResult.verificationSummary, wave: waveIdx, mode: this.shouldUseDirect(feature) ? 'direct' : 'multi-agent' });
|
|
146
|
+
return buildResult;
|
|
147
|
+
});
|
|
148
|
+
const waveResults = await Promise.allSettled(wavePromises);
|
|
149
|
+
for (const settled of waveResults) {
|
|
150
|
+
if (settled.status === 'fulfilled') {
|
|
151
|
+
featureResults.push(settled.value);
|
|
152
|
+
if (settled.value.status === 'completed') {
|
|
153
|
+
completedFeatures.push(settled.value.featureId);
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
failedFeatures.push(settled.value.featureId);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
// Promise rejected — shouldn't happen but handle gracefully
|
|
161
|
+
const featureId = buildable[waveResults.indexOf(settled)];
|
|
162
|
+
featureResults.push({ featureId, featureName: featureId, status: 'failed', filesCreated: [], verificationSummary: { totalClaims: 0, passed: 0, failed: 0 }, durationMs: 0, error: `Build threw: ${settled.reason}` });
|
|
163
|
+
failedFeatures.push(featureId);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
// Post-build: reconcile dependencies and generate env file
|
|
169
|
+
await this.reconcileDependencies(projectPath);
|
|
170
|
+
await this.generateEnvFile(projectPath, plan);
|
|
171
|
+
// Phase: build verification (install, build, start, health check with repair loop)
|
|
172
|
+
let buildVerification = null;
|
|
173
|
+
if (!this.options.skipBuildVerification) {
|
|
174
|
+
const maxAttempts = this.options.maxBuildRepairAttempts ?? 3;
|
|
175
|
+
this.emitPhase({ phase: 'build_verifying', attempt: 1, maxAttempts });
|
|
176
|
+
const buildVerifier = new BuildVerifier(projectPath, {
|
|
177
|
+
maxRepairAttempts: maxAttempts,
|
|
178
|
+
codeModel: this.options.models?.code,
|
|
179
|
+
onReconcileDeps: () => this.reconcileDependencies(projectPath),
|
|
180
|
+
});
|
|
181
|
+
buildVerification = await buildVerifier.verify();
|
|
182
|
+
this.audit('verification_completed', {
|
|
183
|
+
phase: 'build_verifying',
|
|
184
|
+
status: buildVerification.status,
|
|
185
|
+
installStatus: buildVerification.install?.status ?? 'skip',
|
|
186
|
+
buildStatus: buildVerification.build?.status ?? 'skip',
|
|
187
|
+
startStatus: buildVerification.start?.status ?? 'skip',
|
|
188
|
+
healthCheckStatus: buildVerification.healthCheck?.status ?? 'skip',
|
|
189
|
+
repairAttempts: buildVerification.repairAttempts.length,
|
|
190
|
+
finalErrors: buildVerification.finalErrors.length,
|
|
191
|
+
totalDurationMs: buildVerification.totalDurationMs,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
// Phase: cross-verifying
|
|
195
|
+
this.emitPhase({ phase: 'cross_verifying' });
|
|
196
|
+
const crossVerification = await this.runCrossVerification(projectPath, plan);
|
|
197
|
+
this.audit('verification_completed', {
|
|
198
|
+
phase: 'cross_verifying',
|
|
199
|
+
passed: crossVerification.passedCount,
|
|
200
|
+
failed: crossVerification.failedCount,
|
|
201
|
+
});
|
|
202
|
+
// Phase: finalizing
|
|
203
|
+
this.emitPhase({ phase: 'finalizing' });
|
|
204
|
+
const allCompleted = featureResults.every(r => r.status === 'completed');
|
|
205
|
+
const anyCompleted = featureResults.some(r => r.status === 'completed');
|
|
206
|
+
const buildOk = !buildVerification || buildVerification.status === 'pass' || buildVerification.status === 'repaired' || buildVerification.status === 'skipped';
|
|
207
|
+
const finalStatus = allCompleted && buildOk ? 'completed' : anyCompleted ? 'partial' : 'failed';
|
|
208
|
+
this.emitPhase({ phase: 'completed' });
|
|
209
|
+
return this.makeResult(finalStatus, projectPath, plan, featureResults, buildVerification, crossVerification);
|
|
210
|
+
}
|
|
211
|
+
// ── Planning Phase ─────────────────────────────────────────
|
|
212
|
+
async runPlanningPhase(projectPath) {
|
|
213
|
+
const messageBus = new MessageBus();
|
|
214
|
+
const planner = new PlannerAgent(messageBus, { model: this.options.models?.planner });
|
|
215
|
+
const maxRetries = this.options.maxRetries ?? 3;
|
|
216
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
217
|
+
const result = await planner.executeTask({
|
|
218
|
+
taskId: `plan-${randomUUID().slice(0, 8)}`,
|
|
219
|
+
goal: `Design architecture for: ${this.description.description}`,
|
|
220
|
+
constraints: [
|
|
221
|
+
`App name: ${this.description.name}`,
|
|
222
|
+
`Framework: ${this.description.techStack.framework}`,
|
|
223
|
+
`Database: ${this.description.techStack.database}`,
|
|
224
|
+
`Language: ${this.description.techStack.language}`,
|
|
225
|
+
`Features: ${this.description.features.join(', ')}`,
|
|
226
|
+
...(this.description.authModel
|
|
227
|
+
? [`Auth: ${this.description.authModel.methods.join(', ')} via ${this.description.authModel.provider}`]
|
|
228
|
+
: []),
|
|
229
|
+
...(this.description.constraints ?? []),
|
|
230
|
+
],
|
|
231
|
+
dependencies: [],
|
|
232
|
+
contextRefs: [],
|
|
233
|
+
});
|
|
234
|
+
if (result.status === 'completed' && result.artifacts.length > 0) {
|
|
235
|
+
try {
|
|
236
|
+
const plan = JSON.parse(result.artifacts[0].content);
|
|
237
|
+
if (plan.features && plan.features.length > 0) {
|
|
238
|
+
return plan;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
// Parse failed, retry
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
this.audit('task_status_change', {
|
|
246
|
+
phase: 'planning',
|
|
247
|
+
attempt,
|
|
248
|
+
status: 'retry',
|
|
249
|
+
reason: result.summary,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
// ── Plan Verification ──────────────────────────────────────
|
|
255
|
+
verifyPlanStructure(plan) {
|
|
256
|
+
// Check dependency order is acyclic
|
|
257
|
+
if (!this.isAcyclic(plan.features, plan.dependencyOrder)) {
|
|
258
|
+
this.audit('verification_completed', { check: 'acyclic', verdict: 'FAIL' });
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
// Check all features in dependencyOrder exist
|
|
262
|
+
for (const id of plan.dependencyOrder) {
|
|
263
|
+
if (!plan.features.find(f => f.id === id)) {
|
|
264
|
+
this.audit('verification_completed', { check: 'feature_exists', featureId: id, verdict: 'FAIL' });
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
// Check all features are in dependencyOrder
|
|
269
|
+
for (const feature of plan.features) {
|
|
270
|
+
if (!plan.dependencyOrder.includes(feature.id)) {
|
|
271
|
+
this.audit('verification_completed', { check: 'feature_in_order', featureId: feature.id, verdict: 'FAIL' });
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return true;
|
|
276
|
+
}
|
|
277
|
+
isAcyclic(features, order) {
|
|
278
|
+
const visited = new Set();
|
|
279
|
+
const inStack = new Set();
|
|
280
|
+
const depMap = new Map();
|
|
281
|
+
for (const f of features) {
|
|
282
|
+
depMap.set(f.id, f.dependsOn);
|
|
283
|
+
}
|
|
284
|
+
const visit = (id) => {
|
|
285
|
+
if (inStack.has(id))
|
|
286
|
+
return false; // cycle
|
|
287
|
+
if (visited.has(id))
|
|
288
|
+
return true;
|
|
289
|
+
inStack.add(id);
|
|
290
|
+
for (const dep of depMap.get(id) ?? []) {
|
|
291
|
+
if (!visit(dep))
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
inStack.delete(id);
|
|
295
|
+
visited.add(id);
|
|
296
|
+
return true;
|
|
297
|
+
};
|
|
298
|
+
for (const id of order) {
|
|
299
|
+
if (!visit(id))
|
|
300
|
+
return false;
|
|
301
|
+
}
|
|
302
|
+
return true;
|
|
303
|
+
}
|
|
304
|
+
// ── Scaffolding Phase ──────────────────────────────────────
|
|
305
|
+
async runScaffoldingPhase(projectPath, plan) {
|
|
306
|
+
const schemaDesc = plan.schema
|
|
307
|
+
.map(e => `${e.name}: ${e.fields.map(f => `${f.name}:${f.type}`).join(', ')}`)
|
|
308
|
+
.join('\n');
|
|
309
|
+
const authDesc = plan.authPlan
|
|
310
|
+
? `Auth: ${plan.authPlan.provider} with ${plan.authPlan.methods.join(', ')}`
|
|
311
|
+
: 'No auth';
|
|
312
|
+
const dirConvention = this.getDirectoryConvention();
|
|
313
|
+
const scaffoldGoal = `Scaffold base project: package.json, tsconfig.json, directory structure, database schema, and auth setup.
|
|
314
|
+
|
|
315
|
+
Framework: ${this.description.techStack.framework}
|
|
316
|
+
Database: ${this.description.techStack.database}
|
|
317
|
+
Language: ${this.description.techStack.language}
|
|
318
|
+
|
|
319
|
+
${dirConvention}
|
|
320
|
+
|
|
321
|
+
Schema:
|
|
322
|
+
${schemaDesc}
|
|
323
|
+
|
|
324
|
+
${authDesc}
|
|
325
|
+
|
|
326
|
+
Create the project foundation files. Do NOT implement features — only the base project structure and schema.
|
|
327
|
+
Include a .env.local file with placeholder values for all required environment variables so the app can start without crashing.`;
|
|
328
|
+
// Direct mode: single CodeAgent call for scaffolding (bypasses full team)
|
|
329
|
+
const messageBus = new MessageBus();
|
|
330
|
+
const codeAgent = new CodeAgent(messageBus, { model: this.options.models?.code });
|
|
331
|
+
try {
|
|
332
|
+
const result = await codeAgent.executeTask({
|
|
333
|
+
taskId: `scaffold-${randomUUID().slice(0, 8)}`,
|
|
334
|
+
goal: scaffoldGoal,
|
|
335
|
+
constraints: [],
|
|
336
|
+
dependencies: [],
|
|
337
|
+
contextRefs: [],
|
|
338
|
+
});
|
|
339
|
+
if (result.status === 'failed') {
|
|
340
|
+
this.audit('task_status_change', { phase: 'scaffolding', status: 'failed', reason: result.summary });
|
|
341
|
+
return { status: 'failed' };
|
|
342
|
+
}
|
|
343
|
+
// Write artifacts to disk
|
|
344
|
+
for (const artifact of result.artifacts) {
|
|
345
|
+
if (!artifact.path)
|
|
346
|
+
continue;
|
|
347
|
+
const absPath = join(projectPath, artifact.path);
|
|
348
|
+
try {
|
|
349
|
+
await mkdir(dirname(absPath), { recursive: true });
|
|
350
|
+
await writeFile(absPath, artifact.content, 'utf-8');
|
|
351
|
+
}
|
|
352
|
+
catch {
|
|
353
|
+
// Best-effort write
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
this.audit('task_status_change', {
|
|
357
|
+
phase: 'scaffolding',
|
|
358
|
+
status: 'completed',
|
|
359
|
+
filesCreated: result.artifacts.filter(a => a.path).map(a => a.path),
|
|
360
|
+
});
|
|
361
|
+
return { status: 'completed' };
|
|
362
|
+
}
|
|
363
|
+
catch (err) {
|
|
364
|
+
this.audit('task_status_change', { phase: 'scaffolding', status: 'failed', error: err instanceof Error ? err.message : String(err) });
|
|
365
|
+
return { status: 'failed' };
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
// ── Feature Build Phase ────────────────────────────────────
|
|
369
|
+
async buildFeature(projectPath, plan, feature, completedFeatures) {
|
|
370
|
+
const goal = this.buildFeatureGoal(plan, feature, completedFeatures);
|
|
371
|
+
const tier = this.getVerificationTier(feature);
|
|
372
|
+
const agents = ['coordinator', 'code', 'review', 'test'];
|
|
373
|
+
const loop = new MultiAgentLoop(projectPath, {
|
|
374
|
+
goal,
|
|
375
|
+
agents,
|
|
376
|
+
safetyPolicy: DEFAULT_SAFETY,
|
|
377
|
+
maxConcurrentTasks: this.options.maxConcurrentTasks ?? 3,
|
|
378
|
+
stallTimeoutMs: this.options.stallTimeoutMs ?? 120_000,
|
|
379
|
+
maxTotalAttempts: 15,
|
|
380
|
+
models: this.options.models,
|
|
381
|
+
verificationTier: tier,
|
|
382
|
+
useFastVerification: tier === 'full' && !this.options.fullVerification,
|
|
383
|
+
});
|
|
384
|
+
const result = await loop.run();
|
|
385
|
+
this.auditTrail.push(...result.auditTrail);
|
|
386
|
+
// Write artifacts to disk and collect file paths
|
|
387
|
+
const filesCreated = [];
|
|
388
|
+
for (const handoff of result.artifacts) {
|
|
389
|
+
const path = handoff.artifact.path;
|
|
390
|
+
if (!path)
|
|
391
|
+
continue;
|
|
392
|
+
const absPath = join(projectPath, path);
|
|
393
|
+
try {
|
|
394
|
+
await mkdir(dirname(absPath), { recursive: true });
|
|
395
|
+
await writeFile(absPath, handoff.artifact.content, 'utf-8');
|
|
396
|
+
filesCreated.push(path);
|
|
397
|
+
}
|
|
398
|
+
catch {
|
|
399
|
+
// Best-effort write
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
// Aggregate verification stats
|
|
403
|
+
let totalClaims = 0;
|
|
404
|
+
let passed = 0;
|
|
405
|
+
let failed = 0;
|
|
406
|
+
for (const handoff of result.artifacts) {
|
|
407
|
+
totalClaims += handoff.verificationResult.total;
|
|
408
|
+
passed += handoff.verificationResult.passed;
|
|
409
|
+
failed += handoff.verificationResult.failed;
|
|
410
|
+
}
|
|
411
|
+
return {
|
|
412
|
+
status: result.status === 'completed' ? 'completed' : 'failed',
|
|
413
|
+
filesCreated,
|
|
414
|
+
verificationSummary: { totalClaims, passed, failed },
|
|
415
|
+
error: result.status !== 'completed' ? `Feature build ${result.status}` : undefined,
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
// ── Cross-Feature Verification ─────────────────────────────
|
|
419
|
+
async runCrossVerification(projectPath, plan) {
|
|
420
|
+
const checks = [];
|
|
421
|
+
// Check 1: API routes have corresponding files
|
|
422
|
+
for (const route of plan.apiRoutes) {
|
|
423
|
+
const expectedPath = this.routeToFilePath(route.path, this.description.techStack.framework);
|
|
424
|
+
const exists = await this.fileExists(join(projectPath, expectedPath));
|
|
425
|
+
checks.push({
|
|
426
|
+
type: 'api_route_exists',
|
|
427
|
+
description: `${route.method} ${route.path} has handler at ${expectedPath}`,
|
|
428
|
+
verdict: exists ? 'PASS' : 'FAIL',
|
|
429
|
+
evidence: exists ? `File exists: ${expectedPath}` : `Missing: ${expectedPath}`,
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
// Check 2: Pages have corresponding files
|
|
433
|
+
for (const page of plan.pages) {
|
|
434
|
+
const expectedPath = this.pageToFilePath(page.path, page.component, this.description.techStack.framework);
|
|
435
|
+
const exists = await this.fileExists(join(projectPath, expectedPath));
|
|
436
|
+
checks.push({
|
|
437
|
+
type: 'page_references_valid',
|
|
438
|
+
description: `Page ${page.path} has component at ${expectedPath}`,
|
|
439
|
+
verdict: exists ? 'PASS' : 'FAIL',
|
|
440
|
+
evidence: exists ? `File exists: ${expectedPath}` : `Missing: ${expectedPath}`,
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
// Check 3: Schema entities exist in schema/migration files
|
|
444
|
+
for (const entity of plan.schema) {
|
|
445
|
+
const found = await this.searchForSchemaEntity(projectPath, entity.name);
|
|
446
|
+
checks.push({
|
|
447
|
+
type: 'schema_entity_exists',
|
|
448
|
+
description: `Schema entity "${entity.name}" defined in project`,
|
|
449
|
+
verdict: found ? 'PASS' : 'FAIL',
|
|
450
|
+
evidence: found ? `Found in project files` : `Not found in schema/migration files`,
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
// Check 4: Dependency consistency — features that depend on others were built after them
|
|
454
|
+
for (const feature of plan.features) {
|
|
455
|
+
for (const dep of feature.dependsOn) {
|
|
456
|
+
const depIndex = plan.dependencyOrder.indexOf(dep);
|
|
457
|
+
const featureIndex = plan.dependencyOrder.indexOf(feature.id);
|
|
458
|
+
const ordered = depIndex < featureIndex;
|
|
459
|
+
checks.push({
|
|
460
|
+
type: 'dependency_consistency',
|
|
461
|
+
description: `${feature.id} depends on ${dep} — build order correct`,
|
|
462
|
+
verdict: ordered ? 'PASS' : 'FAIL',
|
|
463
|
+
evidence: ordered
|
|
464
|
+
? `${dep} (index ${depIndex}) built before ${feature.id} (index ${featureIndex})`
|
|
465
|
+
: `${dep} (index ${depIndex}) NOT before ${feature.id} (index ${featureIndex})`,
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
const passedCount = checks.filter(c => c.verdict === 'PASS').length;
|
|
470
|
+
const failedCount = checks.filter(c => c.verdict === 'FAIL').length;
|
|
471
|
+
return {
|
|
472
|
+
checks,
|
|
473
|
+
passedCount,
|
|
474
|
+
failedCount,
|
|
475
|
+
verdict: failedCount === 0 ? 'PASS' : passedCount === 0 ? 'FAIL' : 'PARTIAL',
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
// ── Wave Computation ──────────────────────────────────────
|
|
479
|
+
/**
|
|
480
|
+
* Group features into waves based on dependency graph.
|
|
481
|
+
* Wave 0: features with no dependencies
|
|
482
|
+
* Wave N: features whose deps are all in waves < N
|
|
483
|
+
*/
|
|
484
|
+
computeWaves(plan) {
|
|
485
|
+
const featureMap = new Map(plan.features.map(f => [f.id, f]));
|
|
486
|
+
const waves = [];
|
|
487
|
+
const placed = new Set();
|
|
488
|
+
// Iteratively find features whose deps are all placed
|
|
489
|
+
let remaining = [...plan.dependencyOrder];
|
|
490
|
+
while (remaining.length > 0) {
|
|
491
|
+
const wave = [];
|
|
492
|
+
for (const id of remaining) {
|
|
493
|
+
const feature = featureMap.get(id);
|
|
494
|
+
if (!feature)
|
|
495
|
+
continue;
|
|
496
|
+
const allDepsPlaced = feature.dependsOn.every(dep => placed.has(dep));
|
|
497
|
+
if (allDepsPlaced) {
|
|
498
|
+
wave.push(id);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
if (wave.length === 0) {
|
|
502
|
+
// Remaining features have unresolvable deps — put them all in final wave
|
|
503
|
+
waves.push(remaining);
|
|
504
|
+
break;
|
|
505
|
+
}
|
|
506
|
+
waves.push(wave);
|
|
507
|
+
for (const id of wave)
|
|
508
|
+
placed.add(id);
|
|
509
|
+
remaining = remaining.filter(id => !placed.has(id));
|
|
510
|
+
}
|
|
511
|
+
return waves;
|
|
512
|
+
}
|
|
513
|
+
// ── Direct Generation Mode ───────────────────────────────
|
|
514
|
+
/**
|
|
515
|
+
* Build a feature using a single CodeAgent call — bypasses MultiAgentLoop.
|
|
516
|
+
* Used for trivial/small features where full team coordination is overkill.
|
|
517
|
+
*/
|
|
518
|
+
async buildFeatureDirect(projectPath, plan, feature, completedFeatures) {
|
|
519
|
+
const goal = this.buildFeatureGoal(plan, feature, completedFeatures);
|
|
520
|
+
const messageBus = new MessageBus();
|
|
521
|
+
const codeAgent = new CodeAgent(messageBus, { model: this.options.models?.code });
|
|
522
|
+
try {
|
|
523
|
+
const result = await codeAgent.executeTask({
|
|
524
|
+
taskId: `direct-${feature.id}-${randomUUID().slice(0, 8)}`,
|
|
525
|
+
goal,
|
|
526
|
+
constraints: [
|
|
527
|
+
`Framework: ${this.description.techStack.framework}`,
|
|
528
|
+
`Database: ${this.description.techStack.database}`,
|
|
529
|
+
`Language: ${this.description.techStack.language}`,
|
|
530
|
+
],
|
|
531
|
+
dependencies: [],
|
|
532
|
+
contextRefs: [],
|
|
533
|
+
});
|
|
534
|
+
if (result.status === 'failed') {
|
|
535
|
+
return {
|
|
536
|
+
status: 'failed',
|
|
537
|
+
filesCreated: [],
|
|
538
|
+
verificationSummary: { totalClaims: 0, passed: 0, failed: 0 },
|
|
539
|
+
error: result.summary,
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
// Write artifacts to disk
|
|
543
|
+
const filesCreated = [];
|
|
544
|
+
for (const artifact of result.artifacts) {
|
|
545
|
+
if (!artifact.path)
|
|
546
|
+
continue;
|
|
547
|
+
const absPath = join(projectPath, artifact.path);
|
|
548
|
+
try {
|
|
549
|
+
await mkdir(dirname(absPath), { recursive: true });
|
|
550
|
+
await writeFile(absPath, artifact.content, 'utf-8');
|
|
551
|
+
filesCreated.push(artifact.path);
|
|
552
|
+
}
|
|
553
|
+
catch {
|
|
554
|
+
// Best-effort write
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
// In direct mode (single agent), use lightweight (regex-only) verification
|
|
558
|
+
// since there's no cross-agent boundary to verify. This avoids LLM calls
|
|
559
|
+
// per-file while still catching TODOs, empty functions, hardcoded secrets.
|
|
560
|
+
const tier = this.getVerificationTier(feature);
|
|
561
|
+
let totalClaims = 0;
|
|
562
|
+
let passed = 0;
|
|
563
|
+
let failed = 0;
|
|
564
|
+
if (tier !== 'skip' && filesCreated.length > 0) {
|
|
565
|
+
const verifier = new CompositionVerifier(projectPath);
|
|
566
|
+
const sourceIdentity = codeAgent.getIdentity();
|
|
567
|
+
const targetIdentity = sourceIdentity; // Self-review in direct mode
|
|
568
|
+
for (const artifact of result.artifacts) {
|
|
569
|
+
if (!artifact.path)
|
|
570
|
+
continue;
|
|
571
|
+
// Always use lightweight in direct mode — no cross-agent handoff to verify
|
|
572
|
+
const handoff = await verifier.verifyHandoffLightweight(sourceIdentity, targetIdentity, artifact);
|
|
573
|
+
totalClaims += handoff.verificationResult.total;
|
|
574
|
+
passed += handoff.verificationResult.passed;
|
|
575
|
+
failed += handoff.verificationResult.failed;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
return {
|
|
579
|
+
status: 'completed',
|
|
580
|
+
filesCreated,
|
|
581
|
+
verificationSummary: { totalClaims, passed, failed },
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
catch (err) {
|
|
585
|
+
return {
|
|
586
|
+
status: 'failed',
|
|
587
|
+
filesCreated: [],
|
|
588
|
+
verificationSummary: { totalClaims: 0, passed: 0, failed: 0 },
|
|
589
|
+
error: err instanceof Error ? err.message : String(err),
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
/** Whether to use direct generation (single CodeAgent) vs full MultiAgentLoop.
|
|
594
|
+
* For app creation (greenfield), direct mode is used for ALL features by default
|
|
595
|
+
* since there's no existing codebase to review against and the architecture plan
|
|
596
|
+
* already provides full context. Multi-agent mode adds overhead without proportional value. */
|
|
597
|
+
shouldUseDirect(_feature) {
|
|
598
|
+
if (this.options.noDirectMode)
|
|
599
|
+
return false;
|
|
600
|
+
return true; // Default to direct mode for all features in create pipeline
|
|
601
|
+
}
|
|
602
|
+
/** Determine verification tier based on feature characteristics. */
|
|
603
|
+
getVerificationTier(feature) {
|
|
604
|
+
if (this.options.fullVerification)
|
|
605
|
+
return 'full';
|
|
606
|
+
// Auth/security features always get full verification
|
|
607
|
+
const hasAuth = feature.name.toLowerCase().includes('auth')
|
|
608
|
+
|| feature.schemaEntities.some(e => e.toLowerCase().includes('user') || e.toLowerCase().includes('session'));
|
|
609
|
+
if (hasAuth)
|
|
610
|
+
return 'full';
|
|
611
|
+
// Features with API routes get at least lightweight
|
|
612
|
+
if (feature.apiRoutes.length > 0)
|
|
613
|
+
return 'lightweight';
|
|
614
|
+
// UI-only trivial features can be skipped
|
|
615
|
+
if (feature.complexityEstimate === 'trivial')
|
|
616
|
+
return 'skip';
|
|
617
|
+
// Small UI-only features get lightweight
|
|
618
|
+
if (feature.complexityEstimate === 'small' && feature.apiRoutes.length === 0)
|
|
619
|
+
return 'lightweight';
|
|
620
|
+
return 'full';
|
|
621
|
+
}
|
|
622
|
+
// ── Feature Goal Builder ─────────────────────────────────
|
|
623
|
+
/** Build the goal string for a feature (shared between buildFeature and buildFeatureDirect). */
|
|
624
|
+
buildFeatureGoal(plan, feature, completedFeatures) {
|
|
625
|
+
const relevantSchema = plan.schema
|
|
626
|
+
.filter(e => feature.schemaEntities.includes(e.name))
|
|
627
|
+
.map(e => `${e.name}: ${e.fields.map(f => `${f.name}:${f.type}`).join(', ')}`)
|
|
628
|
+
.join('\n');
|
|
629
|
+
const relevantRoutes = plan.apiRoutes
|
|
630
|
+
.filter(r => r.featureId === feature.id)
|
|
631
|
+
.map(r => `${r.method} ${r.path} — ${r.description}`)
|
|
632
|
+
.join('\n');
|
|
633
|
+
const relevantPages = plan.pages
|
|
634
|
+
.filter(p => p.featureId === feature.id)
|
|
635
|
+
.map(p => `${p.path} — ${p.component}: ${p.description ?? ''}`)
|
|
636
|
+
.join('\n');
|
|
637
|
+
const dirConvention = this.getDirectoryConvention();
|
|
638
|
+
return `Implement feature: ${feature.name}
|
|
639
|
+
|
|
640
|
+
Feature description: Build the "${feature.name}" feature for a ${this.description.techStack.framework} application.
|
|
641
|
+
|
|
642
|
+
${dirConvention}
|
|
643
|
+
|
|
644
|
+
Schema entities:
|
|
645
|
+
${relevantSchema || 'None'}
|
|
646
|
+
|
|
647
|
+
API routes to implement:
|
|
648
|
+
${relevantRoutes || 'None'}
|
|
649
|
+
|
|
650
|
+
Pages to implement:
|
|
651
|
+
${relevantPages || 'None'}
|
|
652
|
+
|
|
653
|
+
Tech stack: ${this.description.techStack.framework}, ${this.description.techStack.database}, ${this.description.techStack.language}
|
|
654
|
+
|
|
655
|
+
Previously completed features: ${completedFeatures.join(', ') || 'None'}
|
|
656
|
+
|
|
657
|
+
Implement all API routes, pages, and business logic for this feature. Write complete, working code — no TODOs or stubs.
|
|
658
|
+
IMPORTANT: If you import any npm packages not in the standard Next.js/React stack, include a file "package.additions.json" listing them: { "dependencies": { "package-name": "latest" } }`;
|
|
659
|
+
}
|
|
660
|
+
// ── Post-Build: Dependency Reconciliation ─────────────────
|
|
661
|
+
/**
|
|
662
|
+
* Scan all generated source files for imports and ensure every
|
|
663
|
+
* external package is listed in package.json. Also merges any
|
|
664
|
+
* "package.additions.json" files that feature agents may have created.
|
|
665
|
+
*/
|
|
666
|
+
async reconcileDependencies(projectPath) {
|
|
667
|
+
const pkgPath = join(projectPath, 'package.json');
|
|
668
|
+
let pkg;
|
|
669
|
+
try {
|
|
670
|
+
pkg = JSON.parse(await readFile(pkgPath, 'utf-8'));
|
|
671
|
+
}
|
|
672
|
+
catch {
|
|
673
|
+
return; // No package.json — nothing to reconcile
|
|
674
|
+
}
|
|
675
|
+
const deps = (pkg.dependencies ?? {});
|
|
676
|
+
const devDeps = (pkg.devDependencies ?? {});
|
|
677
|
+
const allExisting = new Set([...Object.keys(deps), ...Object.keys(devDeps)]);
|
|
678
|
+
// Collect imports from all source files
|
|
679
|
+
const sourceFiles = await this.collectFiles(projectPath, ['.ts', '.tsx', '.js', '.jsx']);
|
|
680
|
+
const importedPackages = new Set();
|
|
681
|
+
for (const filePath of sourceFiles) {
|
|
682
|
+
try {
|
|
683
|
+
const content = await readFile(filePath, 'utf-8');
|
|
684
|
+
// Match: import ... from 'package' / import 'package' / require('package')
|
|
685
|
+
const importRegex = /(?:import\s+.*?\s+from\s+['"]|import\s+['"]|require\s*\(\s*['"])([^./][^'"]*)['"]/g;
|
|
686
|
+
let match;
|
|
687
|
+
while ((match = importRegex.exec(content)) !== null) {
|
|
688
|
+
// Extract package name (handle scoped packages like @supabase/ssr)
|
|
689
|
+
const raw = match[1];
|
|
690
|
+
const pkgName = raw.startsWith('@')
|
|
691
|
+
? raw.split('/').slice(0, 2).join('/')
|
|
692
|
+
: raw.split('/')[0];
|
|
693
|
+
importedPackages.add(pkgName);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
catch {
|
|
697
|
+
// Skip unreadable files
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
// Merge any package.additions.json files from feature agents
|
|
701
|
+
const additionsFiles = await this.collectFiles(projectPath, ['.json'], 'package.additions.json');
|
|
702
|
+
for (const addFile of additionsFiles) {
|
|
703
|
+
try {
|
|
704
|
+
const additions = JSON.parse(await readFile(addFile, 'utf-8'));
|
|
705
|
+
if (additions.dependencies && typeof additions.dependencies === 'object') {
|
|
706
|
+
for (const [name, version] of Object.entries(additions.dependencies)) {
|
|
707
|
+
if (!allExisting.has(name)) {
|
|
708
|
+
deps[name] = version;
|
|
709
|
+
allExisting.add(name);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
catch {
|
|
715
|
+
// Skip malformed additions files
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
// Node built-in modules to skip
|
|
719
|
+
const builtins = new Set([
|
|
720
|
+
'node:events', 'node:fs', 'node:path', 'node:crypto', 'node:url', 'node:http', 'node:https',
|
|
721
|
+
'node:stream', 'node:util', 'node:os', 'node:child_process', 'node:buffer', 'node:assert',
|
|
722
|
+
'events', 'fs', 'path', 'crypto', 'url', 'http', 'https', 'stream', 'util', 'os',
|
|
723
|
+
'child_process', 'buffer', 'assert', 'fs/promises', 'react', 'react-dom', 'next',
|
|
724
|
+
]);
|
|
725
|
+
// Add missing packages
|
|
726
|
+
let changed = false;
|
|
727
|
+
for (const pkg of importedPackages) {
|
|
728
|
+
if (builtins.has(pkg))
|
|
729
|
+
continue;
|
|
730
|
+
if (allExisting.has(pkg))
|
|
731
|
+
continue;
|
|
732
|
+
// Skip next/* subpath imports (provided by next)
|
|
733
|
+
if (pkg.startsWith('next/'))
|
|
734
|
+
continue;
|
|
735
|
+
deps[pkg] = 'latest';
|
|
736
|
+
changed = true;
|
|
737
|
+
this.audit('dependency_added', { package: pkg, reason: 'imported but not in package.json' });
|
|
738
|
+
}
|
|
739
|
+
if (changed) {
|
|
740
|
+
const updatedPkg = { ...pkg, dependencies: deps };
|
|
741
|
+
await writeFile(pkgPath, JSON.stringify(updatedPkg, null, 2) + '\n', 'utf-8');
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
// ── Post-Build: Environment File Generation ─────────────
|
|
745
|
+
/**
|
|
746
|
+
* Generate .env.local from the architecture plan's deployConfig.envVars.
|
|
747
|
+
* Only creates the file if it doesn't already exist.
|
|
748
|
+
*/
|
|
749
|
+
async generateEnvFile(projectPath, plan) {
|
|
750
|
+
const envPath = join(projectPath, '.env.local');
|
|
751
|
+
// Don't overwrite if scaffolding already created one
|
|
752
|
+
if (await this.fileExists(envPath))
|
|
753
|
+
return;
|
|
754
|
+
const envVars = plan.deployConfig?.envVars;
|
|
755
|
+
if (!envVars || envVars.length === 0)
|
|
756
|
+
return;
|
|
757
|
+
const lines = [
|
|
758
|
+
'# Generated by Assay Verified App Creator',
|
|
759
|
+
'# Replace placeholder values with real credentials before running.',
|
|
760
|
+
'',
|
|
761
|
+
];
|
|
762
|
+
for (const varName of envVars) {
|
|
763
|
+
lines.push(`${varName}=${this.getEnvPlaceholder(varName)}`);
|
|
764
|
+
}
|
|
765
|
+
await writeFile(envPath, lines.join('\n') + '\n', 'utf-8');
|
|
766
|
+
this.audit('env_file_generated', { path: '.env.local', vars: envVars.length });
|
|
767
|
+
}
|
|
768
|
+
/** Return a sensible placeholder value for known env var patterns. */
|
|
769
|
+
getEnvPlaceholder(varName) {
|
|
770
|
+
const name = varName.toUpperCase();
|
|
771
|
+
if (name.includes('SUPABASE_URL'))
|
|
772
|
+
return 'https://your-project.supabase.co';
|
|
773
|
+
if (name.includes('SUPABASE') && name.includes('KEY'))
|
|
774
|
+
return 'your-supabase-anon-key';
|
|
775
|
+
if (name.includes('DATABASE_URL'))
|
|
776
|
+
return 'postgresql://user:password@localhost:5432/dbname';
|
|
777
|
+
if (name.includes('SECRET') || name.includes('JWT'))
|
|
778
|
+
return 'your-secret-key-change-me';
|
|
779
|
+
if (name.includes('URL'))
|
|
780
|
+
return 'https://localhost:3000';
|
|
781
|
+
if (name.includes('KEY') || name.includes('TOKEN'))
|
|
782
|
+
return 'your-api-key-here';
|
|
783
|
+
return 'placeholder-value';
|
|
784
|
+
}
|
|
785
|
+
/** Recursively collect files with given extensions (or exact filename). */
|
|
786
|
+
async collectFiles(dir, extensions, exactName) {
|
|
787
|
+
const results = [];
|
|
788
|
+
try {
|
|
789
|
+
const entries = await readdir(dir);
|
|
790
|
+
for (const entry of entries) {
|
|
791
|
+
if (entry === 'node_modules' || entry === '.git' || entry === '.next')
|
|
792
|
+
continue;
|
|
793
|
+
const fullPath = join(dir, entry);
|
|
794
|
+
try {
|
|
795
|
+
const s = await stat(fullPath);
|
|
796
|
+
if (s.isDirectory()) {
|
|
797
|
+
results.push(...await this.collectFiles(fullPath, extensions, exactName));
|
|
798
|
+
}
|
|
799
|
+
else if (exactName ? entry === exactName : extensions.some(ext => entry.endsWith(ext))) {
|
|
800
|
+
results.push(fullPath);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
catch {
|
|
804
|
+
// Skip inaccessible entries
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
catch {
|
|
809
|
+
// Skip unreadable directories
|
|
810
|
+
}
|
|
811
|
+
return results;
|
|
812
|
+
}
|
|
813
|
+
// ── Helpers ────────────────────────────────────────────────
|
|
814
|
+
emitPhase(phase) {
|
|
815
|
+
const progress = {
|
|
816
|
+
phase,
|
|
817
|
+
currentFeatureIndex: 'featureIndex' in phase ? phase.featureIndex : 0,
|
|
818
|
+
totalFeatures: 'totalFeatures' in phase ? phase.totalFeatures : 0,
|
|
819
|
+
completedFeatures: [],
|
|
820
|
+
failedFeatures: [],
|
|
821
|
+
elapsedMs: Date.now() - this.startTime,
|
|
822
|
+
};
|
|
823
|
+
this.emit('progress', progress);
|
|
824
|
+
}
|
|
825
|
+
makeResult(status, projectPath, plan, featureResults, buildVerification, crossVerification) {
|
|
826
|
+
return {
|
|
827
|
+
status,
|
|
828
|
+
projectPath,
|
|
829
|
+
plan,
|
|
830
|
+
featureResults,
|
|
831
|
+
buildVerification,
|
|
832
|
+
crossVerification,
|
|
833
|
+
totalDurationMs: Date.now() - this.startTime,
|
|
834
|
+
auditTrail: this.auditTrail,
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
audit(eventType, details) {
|
|
838
|
+
const entry = {
|
|
839
|
+
id: randomUUID(),
|
|
840
|
+
eventType: eventType,
|
|
841
|
+
agentName: 'app-create-orchestrator',
|
|
842
|
+
timestamp: new Date().toISOString(),
|
|
843
|
+
details,
|
|
844
|
+
relatedIds: {},
|
|
845
|
+
};
|
|
846
|
+
this.auditTrail.push(entry);
|
|
847
|
+
this.emit('audit', entry);
|
|
848
|
+
}
|
|
849
|
+
slugify(name) {
|
|
850
|
+
return name
|
|
851
|
+
.toLowerCase()
|
|
852
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
853
|
+
.replace(/^-+|-+$/g, '');
|
|
854
|
+
}
|
|
855
|
+
/** Get framework-specific directory convention instruction for agent prompts. */
|
|
856
|
+
getDirectoryConvention() {
|
|
857
|
+
const fw = this.description.techStack.framework.toLowerCase();
|
|
858
|
+
if (fw.includes('next')) {
|
|
859
|
+
return `DIRECTORY CONVENTION (MANDATORY): Use the Next.js App Router with the "app/" directory at the PROJECT ROOT (NOT "src/app/"). All pages go in "app/", all API routes go in "app/api/". Components go in "components/". Lib/utils go in "lib/". Do NOT create a "src/" directory.`;
|
|
860
|
+
}
|
|
861
|
+
if (fw.includes('svelte')) {
|
|
862
|
+
return `DIRECTORY CONVENTION (MANDATORY): Use "src/routes/" for pages and "src/lib/" for shared code.`;
|
|
863
|
+
}
|
|
864
|
+
return `DIRECTORY CONVENTION: Use "src/" as the source root.`;
|
|
865
|
+
}
|
|
866
|
+
routeToFilePath(routePath, framework) {
|
|
867
|
+
if (framework.toLowerCase().includes('next')) {
|
|
868
|
+
// /api/users -> app/api/users/route.ts
|
|
869
|
+
return `app${routePath}/route.ts`;
|
|
870
|
+
}
|
|
871
|
+
// Express or similar: src/routes/<path>.ts
|
|
872
|
+
const parts = routePath.replace(/^\/api/, '').replace(/^\//, '');
|
|
873
|
+
return `src/routes/${parts || 'index'}.ts`;
|
|
874
|
+
}
|
|
875
|
+
pageToFilePath(pagePath, _component, framework) {
|
|
876
|
+
if (framework.toLowerCase().includes('next')) {
|
|
877
|
+
// / -> app/page.tsx, /dashboard -> app/dashboard/page.tsx
|
|
878
|
+
const normalized = pagePath === '/' ? '' : pagePath;
|
|
879
|
+
return `app${normalized}/page.tsx`;
|
|
880
|
+
}
|
|
881
|
+
if (framework.toLowerCase().includes('svelte')) {
|
|
882
|
+
const normalized = pagePath === '/' ? '' : pagePath;
|
|
883
|
+
return `src/routes${normalized}/+page.svelte`;
|
|
884
|
+
}
|
|
885
|
+
// Generic: src/pages/<path>.tsx
|
|
886
|
+
const normalized = pagePath === '/' ? '/index' : pagePath;
|
|
887
|
+
return `src/pages${normalized}.tsx`;
|
|
888
|
+
}
|
|
889
|
+
async fileExists(path) {
|
|
890
|
+
try {
|
|
891
|
+
await readFile(path);
|
|
892
|
+
return true;
|
|
893
|
+
}
|
|
894
|
+
catch {
|
|
895
|
+
return false;
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
async searchForSchemaEntity(projectPath, entityName) {
|
|
899
|
+
// Check common schema file locations
|
|
900
|
+
const candidates = [
|
|
901
|
+
'schema.prisma',
|
|
902
|
+
'prisma/schema.prisma',
|
|
903
|
+
'drizzle/schema.ts',
|
|
904
|
+
'src/db/schema.ts',
|
|
905
|
+
'supabase/migrations',
|
|
906
|
+
'src/schema.ts',
|
|
907
|
+
'db/schema.ts',
|
|
908
|
+
];
|
|
909
|
+
for (const candidate of candidates) {
|
|
910
|
+
try {
|
|
911
|
+
const content = await readFile(join(projectPath, candidate), 'utf-8');
|
|
912
|
+
if (content.includes(entityName) || content.includes(entityName.toLowerCase())) {
|
|
913
|
+
return true;
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
catch {
|
|
917
|
+
// File doesn't exist, continue
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
return false;
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
//# sourceMappingURL=app-create-orchestrator.js.map
|