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.
Files changed (32) hide show
  1. package/dist/api/server.d.ts +4 -0
  2. package/dist/api/server.js +145 -1
  3. package/dist/api/server.js.map +1 -1
  4. package/dist/cli.js +21 -1
  5. package/dist/cli.js.map +1 -1
  6. package/dist/commands/create.d.ts +18 -0
  7. package/dist/commands/create.js +135 -0
  8. package/dist/commands/create.js.map +1 -0
  9. package/dist/runtime/agents/code-agent.d.ts +1 -1
  10. package/dist/runtime/agents/code-agent.js +17 -7
  11. package/dist/runtime/agents/code-agent.js.map +1 -1
  12. package/dist/runtime/agents/coordinator-agent.js +2 -1
  13. package/dist/runtime/agents/coordinator-agent.js.map +1 -1
  14. package/dist/runtime/agents/planner-agent.d.ts +18 -0
  15. package/dist/runtime/agents/planner-agent.js +201 -0
  16. package/dist/runtime/agents/planner-agent.js.map +1 -0
  17. package/dist/runtime/app-create-orchestrator.d.ts +62 -0
  18. package/dist/runtime/app-create-orchestrator.js +923 -0
  19. package/dist/runtime/app-create-orchestrator.js.map +1 -0
  20. package/dist/runtime/build-verifier.d.ts +32 -0
  21. package/dist/runtime/build-verifier.js +508 -0
  22. package/dist/runtime/build-verifier.js.map +1 -0
  23. package/dist/runtime/composition-verifier.d.ts +46 -0
  24. package/dist/runtime/composition-verifier.js +278 -1
  25. package/dist/runtime/composition-verifier.js.map +1 -1
  26. package/dist/runtime/multi-agent-loop.d.ts +2 -0
  27. package/dist/runtime/multi-agent-loop.js +65 -1
  28. package/dist/runtime/multi-agent-loop.js.map +1 -1
  29. package/dist/runtime/specialized-agent.js +1 -1
  30. package/dist/runtime/specialized-agent.js.map +1 -1
  31. package/dist/runtime/types.d.ts +212 -2
  32. 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