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.
@@ -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