iriai-build 0.3.0 → 0.3.1

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 (2) hide show
  1. package/package.json +1 -1
  2. package/v3/orchestrator.js +140 -109
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iriai-build",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Iriai Build tool — AI agent orchestration CLI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -469,67 +469,43 @@ export class Orchestrator {
469
469
  this._notifyOperatorOfDecision(feature, planDecision);
470
470
  }
471
471
  } else if (role === "ux-designer") {
472
- // Compound design step: ux-designer done dispatch ui-designer (no phase-review yet)
473
- await this.adapter.postPipelineMessage(feature.id,
474
- `UX Designer complete. Starting UI Designer...`);
475
- queries.updateFeaturePlanningRole(feature.id, "ui-designer");
476
- this.dispatchPlanningRole(feature.id, "ui-designer");
477
- } else if (role === "ui-designer") {
478
- // Compound design step: ui-designer done → surface as "designer" phase-review
479
- // Both sub-roles are complete; present combined output for user approval
480
- await this._requestPhaseReview(feature, "designer", output);
481
- } else if (role === "architect") {
482
- // Architect validation: ensure structured plan directory before review gate.
483
- // The architect often drifts to writing a monolithic architecture doc
484
- // instead of the structured plan directory (plan.yaml + phases/ + task files).
485
- // Validate required artifacts and redispatch if incomplete.
472
+ // Validate UX designer output before proceeding to UI designer
486
473
  const planDir = path.join(feature.signal_dir, "plans");
487
- const validation = this._validateArchitectOutput(planDir);
488
-
489
- if (validation.pass) {
490
- await this._requestPhaseReview(feature, role, output);
474
+ const validation = this._validatePlanningOutput(role, planDir);
475
+ if (!validation.pass) {
476
+ await this.adapter.postPipelineMessage(feature.id,
477
+ `UX Designer output incomplete (missing: ${validation.missing.join(", ")}). Redispatching...`);
478
+ this._redispatchForMissingArtifacts(feature, role, validation);
491
479
  } else {
492
- // Redispatch architect with focused instructions
493
- console.log(`[orchestrator] Architect output incomplete for ${slug}: missing ${validation.missing.join(", ")}`);
480
+ // Compound design step: ux-designer done → dispatch ui-designer (no phase-review yet)
494
481
  await this.adapter.postPipelineMessage(feature.id,
495
- `Architect investigation complete redispatching for structured plan output (missing: ${validation.missing.join(", ")})`);
496
-
497
- const signalDir = path.join(feature.signal_dir, "planning", "architect");
498
- ensureDir(signalDir);
499
-
500
- // Write focused redispatch instructions as .user-message so the agent picks it up
501
- const redispatchMsg = [
502
- `STRUCTURED PLAN OUTPUT REQUIRED`,
503
- ``,
504
- `Your investigation phase is complete. You wrote a thorough context.md in $PLAN_DIR/.`,
505
- `Now you MUST produce the structured plan directory. Read your CLAUDE.md "Output Format" section carefully.`,
506
- ``,
507
- `WHAT EXISTS (do NOT redo):`,
508
- validation.found.map(f => ` ✓ ${f}`).join("\n"),
509
- ``,
510
- `WHAT IS MISSING (you must create these):`,
511
- validation.missing.map(f => ` ✗ ${f}`).join("\n"),
512
- ``,
513
- `INSTRUCTIONS:`,
514
- `1. Read $PLAN_DIR/context.md for your investigation notes — do NOT re-explore the entire codebase`,
515
- `2. Do targeted source code reads ONLY where you need specific file paths, function signatures, or line numbers for task files`,
516
- `3. Produce plan.yaml with phase DAG, role assignments, and journey refs`,
517
- `4. Create phases/ directory with structured task files (YAML frontmatter + detailed instructions)`,
518
- `5. Create journeys/ directory with test journeys (happy-path, failure, regression)`,
519
- `6. Every task file must cite real file paths and code evidence from the codebase`,
520
- `7. Signal .done + .output when complete`,
521
- ``,
522
- `Your context.md is the foundation. Build the structured plan ON TOP of it.`,
523
- ].join("\n");
524
-
525
- writeSignal(path.join(signalDir, SIGNAL.USER_MESSAGE), redispatchMsg);
526
- queries.updateFeaturePlanningRole(feature.id, "architect");
527
- // Fresh context — investigation session is done, architect needs full budget for structured output
528
- this.dispatchPlanningRole(feature.id, "architect", { continue: false });
482
+ `UX Designer complete. Starting UI Designer...`);
483
+ queries.updateFeaturePlanningRole(feature.id, "ui-designer");
484
+ this.dispatchPlanningRole(feature.id, "ui-designer");
485
+ }
486
+ } else if (role === "ui-designer") {
487
+ // Validate UI designer output before surfacing combined design review
488
+ const planDir = path.join(feature.signal_dir, "plans");
489
+ const validation = this._validatePlanningOutput(role, planDir);
490
+ if (!validation.pass) {
491
+ await this.adapter.postPipelineMessage(feature.id,
492
+ `UI Designer output incomplete (missing: ${validation.missing.join(", ")}). Redispatching...`);
493
+ this._redispatchForMissingArtifacts(feature, role, validation);
494
+ } else {
495
+ // Both sub-roles complete; present combined output for user approval
496
+ await this._requestPhaseReview(feature, "designer", output);
529
497
  }
530
498
  } else {
531
- // Non-final role summary + artifact upload + Block Kit review gate (all sequential)
532
- await this._requestPhaseReview(feature, role, output);
499
+ // All other roles (pm, architect, designer): validate then review gate
500
+ const planDir = path.join(feature.signal_dir, "plans");
501
+ const validation = this._validatePlanningOutput(role, planDir);
502
+ if (!validation.pass) {
503
+ await this.adapter.postPipelineMessage(feature.id,
504
+ `${PLANNING_ROLE_LABELS[role] || role} output incomplete (missing: ${validation.missing.join(", ")}). Redispatching...`);
505
+ this._redispatchForMissingArtifacts(feature, role, validation);
506
+ } else {
507
+ await this._requestPhaseReview(feature, role, output);
508
+ }
533
509
  }
534
510
 
535
511
  } finally {
@@ -539,69 +515,124 @@ export class Orchestrator {
539
515
  }
540
516
 
541
517
  /**
542
- * Validate that the architect produced the required structured plan directory.
543
- * Auto-normalizes architecture.mdcontext.md if needed.
544
- * Returns { pass: boolean, found: string[], missing: string[] }
518
+ * Artifact requirements per planning role.
519
+ * Each entry: { label, check(planDir) boolean }
545
520
  */
546
- _validateArchitectOutput(planDir) {
547
- const found = [];
548
- const missing = [];
521
+ static ROLE_ARTIFACTS = {
522
+ pm: [
523
+ { label: "PRD document (*-prd.md)", check: (d) => !!findArtifact("prd", d) },
524
+ ],
525
+ "ux-designer": [
526
+ { label: "design-decisions.md", check: (d) => !!findArtifact("design-decisions", d) },
527
+ ],
528
+ "ui-designer": [
529
+ { label: "design-decisions.md (with Visual Design Language)", check: (d) => {
530
+ const content = findArtifact("design-decisions", d);
531
+ if (!content) return false;
532
+ try {
533
+ const text = fs.readFileSync(content, "utf-8");
534
+ return text.includes("Visual Design Language") || text.includes("Color Palette");
535
+ } catch { return false; }
536
+ }},
537
+ { label: "mockup.html", check: (d) => fs.existsSync(path.join(d, "mockup.html")) },
538
+ ],
539
+ designer: [
540
+ { label: "design-decisions.md", check: (d) => !!findArtifact("design-decisions", d) },
541
+ { label: "mockup.html", check: (d) => fs.existsSync(path.join(d, "mockup.html")) },
542
+ ],
543
+ architect: [
544
+ { label: "context.md (investigation notes)", check: (d) => !!findArtifact("context", d) || !!findArtifact("architecture", d) },
545
+ { label: "plan.yaml (phase DAG)", check: (d) => fs.existsSync(path.join(d, "plan.yaml")) },
546
+ { label: "phases/ (task files)", check: (d) => {
547
+ try {
548
+ return fs.readdirSync(path.join(d, "phases"), { withFileTypes: true }).some(e => e.isDirectory());
549
+ } catch { return false; }
550
+ }},
551
+ { label: "journeys/ (test journeys)", check: (d) => {
552
+ try {
553
+ return fs.readdirSync(path.join(d, "journeys")).some(e => e.endsWith(".md"));
554
+ } catch { return false; }
555
+ }},
556
+ ],
557
+ };
549
558
 
550
- // Normalize: rename architecture.md → context.md if architect used wrong name
551
- const archFile = path.join(planDir, "architecture.md");
552
- const contextFile = path.join(planDir, "context.md");
553
- if (fs.existsSync(archFile) && !fs.existsSync(contextFile)) {
554
- try {
555
- fs.renameSync(archFile, contextFile);
556
- console.log(`[orchestrator] Renamed architecture.md context.md in ${planDir}`);
557
- } catch { /* ok, proceed with whatever exists */ }
559
+ /**
560
+ * Validate that a planning role produced its required artifacts.
561
+ * Runs auto-normalization (e.g. architecture.md → context.md) before checking.
562
+ * Returns { pass, found, missing }
563
+ */
564
+ _validatePlanningOutput(role, planDir) {
565
+ // Auto-normalize known filename drift
566
+ if (role === "architect") {
567
+ const archFile = path.join(planDir, "architecture.md");
568
+ const contextFile = path.join(planDir, "context.md");
569
+ if (fs.existsSync(archFile) && !fs.existsSync(contextFile)) {
570
+ try {
571
+ fs.renameSync(archFile, contextFile);
572
+ console.log(`[orchestrator] Renamed architecture.md → context.md in ${planDir}`);
573
+ } catch { /* ok */ }
574
+ }
558
575
  }
559
576
 
560
- // Check context.md (investigation notes)
561
- if (findArtifact("context", planDir) || findArtifact("architecture", planDir)) {
562
- found.push("context.md (investigation notes)");
563
- } else {
564
- missing.push("context.md (investigation notes)");
565
- }
577
+ const artifacts = Orchestrator.ROLE_ARTIFACTS[role];
578
+ if (!artifacts) return { pass: true, found: [], missing: [] };
566
579
 
567
- // Check plan.yaml (phase DAG metadata)
568
- if (fs.existsSync(path.join(planDir, "plan.yaml"))) {
569
- found.push("plan.yaml (phase DAG)");
570
- } else {
571
- missing.push("plan.yaml (phase DAG + metadata)");
580
+ const found = [];
581
+ const missing = [];
582
+ for (const art of artifacts) {
583
+ if (art.check(planDir)) {
584
+ found.push(art.label);
585
+ } else {
586
+ missing.push(art.label);
587
+ }
572
588
  }
573
589
 
574
- // Check phases/ directory with at least one phase subdirectory
575
- const phasesDir = path.join(planDir, "phases");
576
- let hasPhases = false;
577
- try {
578
- const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
579
- hasPhases = entries.some(e => e.isDirectory());
580
- } catch { /* doesn't exist */ }
581
- if (hasPhases) {
582
- found.push("phases/ (task files)");
583
- } else {
584
- missing.push("phases/ directory with task files");
585
- }
590
+ return { pass: missing.length === 0, found, missing };
591
+ }
586
592
 
587
- // Check journeys/ directory
588
- const journeysDir = path.join(planDir, "journeys");
589
- let hasJourneys = false;
590
- try {
591
- const entries = fs.readdirSync(journeysDir);
592
- hasJourneys = entries.some(e => e.endsWith(".md"));
593
- } catch { /* doesn't exist */ }
594
- if (hasJourneys) {
595
- found.push("journeys/ (test journeys)");
596
- } else {
597
- missing.push("journeys/ (test journeys)");
593
+ /**
594
+ * Redispatch a planning role that produced incomplete output.
595
+ * Writes focused instructions to .user-message and re-spawns with fresh context.
596
+ */
597
+ _redispatchForMissingArtifacts(feature, role, validation) {
598
+ const slug = feature.slug;
599
+ console.log(`[orchestrator] ${role} output incomplete for ${slug}: missing ${validation.missing.join(", ")}`);
600
+
601
+ const signalDir = path.join(feature.signal_dir, "planning", role);
602
+ ensureDir(signalDir);
603
+
604
+ const roleName = PLANNING_ROLE_LABELS[role] || role;
605
+ const lines = [
606
+ `INCOMPLETE OUTPUT — REDISPATCH`,
607
+ ``,
608
+ `You signaled .done but required artifacts are missing.`,
609
+ `Read your CLAUDE.md output format section and produce the missing items.`,
610
+ ``,
611
+ `WHAT EXISTS (do NOT redo):`,
612
+ ...validation.found.map(f => ` ✓ ${f}`),
613
+ ``,
614
+ `WHAT IS MISSING (you MUST create these):`,
615
+ ...validation.missing.map(f => ` ✗ ${f}`),
616
+ ``,
617
+ `Write the missing artifacts to $PLAN_DIR/, then signal .done + .output again.`,
618
+ ];
619
+
620
+ // Role-specific guidance
621
+ if (role === "architect") {
622
+ lines.push(
623
+ ``,
624
+ `ARCHITECT-SPECIFIC:`,
625
+ `1. Read $PLAN_DIR/context.md for your investigation notes — do NOT re-explore the entire codebase`,
626
+ `2. Do targeted source code reads ONLY where needed for specific file paths or function signatures`,
627
+ `3. Every task file must cite real file paths and code evidence from the codebase`,
628
+ );
598
629
  }
599
630
 
600
- return {
601
- pass: missing.length === 0,
602
- found,
603
- missing,
604
- };
631
+ writeSignal(path.join(signalDir, SIGNAL.USER_MESSAGE), lines.join("\n"));
632
+ queries.updateFeaturePlanningRole(feature.id, role);
633
+
634
+ // Fresh context for the structured output pass
635
+ this.dispatchPlanningRole(feature.id, role, { continue: false });
605
636
  }
606
637
 
607
638
  /**