@tekyzinc/gsd-t 2.71.20 → 2.72.10

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/CHANGELOG.md CHANGED
@@ -2,6 +2,27 @@
2
2
 
3
3
  All notable changes to GSD-T are documented here. Updated with each release.
4
4
 
5
+ ## [2.72.10] - 2026-04-08
6
+
7
+ ### Added (orchestrator — per-item pipeline, stream-json, verbose, clean)
8
+ - **Per-item build+review pipeline** — when workflow provides `buildSingleItemPrompt` + `buildSingleItemReviewPrompt`, each component is built and reviewed individually (1 contract + 1 source per Claude spawn) instead of all-at-once. Fixes reviewer timeout caused by 30+ files in a single context. Each item gets up to 4 auto-review fix cycles independently.
9
+ - **`--output-format stream-json`** — Claude spawns now use streaming JSON output. On timeout, partial output is captured and parsed instead of returning empty string. Enables diagnosing what the reviewer was doing before being killed.
10
+ - **`--verbose` / `-v` flag** — streams Claude's stderr to terminal for real-time tool call visibility, saves prompts to `build-logs/` for post-mortem, logs completion stats after each spawn.
11
+ - **`--clean` flag** — deletes previous build output files before each phase's build step for fresh builds.
12
+ - **Version display** — orchestrator shows GSD-T version in startup header.
13
+
14
+ ### Changed
15
+ - **Reviewer timeout increased** — 300s → 600s for all-at-once review mode (per-item uses 120s per component).
16
+ - **Design orchestrator** — added `buildSingleItemPrompt` and `buildSingleItemReviewPrompt` for per-item pipeline support. Reviewer prompt restructured: code review first, Playwright spot-check second.
17
+
18
+ ## [2.71.21] - 2026-04-08
19
+
20
+ ### Fixed (orchestrator — timeout false-pass, review server health, stale cleanup)
21
+ - **Reviewer timeout/kill no longer treated as "pass"** — exit codes 143 (SIGTERM) and 137 (SIGKILL) are now always detected as failures regardless of duration. Previously, a reviewer that timed out at 300s was parsed as "0 issues = pass" because crash detection only checked duration < 10s.
22
+ - **Empty output with non-zero exit also caught** — any reviewer that exits non-zero with no output is treated as a failure, not a clean pass.
23
+ - **Review server health check during human review gate** — every ~30s the polling loop verifies port 3456 is alive. If the review server dies, it auto-restarts. Previously, a dead review server left the orchestrator stuck forever.
24
+ - **Stale auto-review cleanup** — old auto-review files from previous runs are cleared at the start of each phase's review cycle, preventing misleading results from prior orchestrator runs.
25
+
5
26
  ## [2.71.20] - 2026-04-08
6
27
 
7
28
  ### Fixed (orchestrator — reviewer crash false-pass + Ctrl+C + build logging)
@@ -134,6 +134,39 @@ ${importInstructions}
134
134
  This is a FINITE task. Build the ${items.length} ${phase} listed above, then EXIT.`;
135
135
  }
136
136
 
137
+ function buildSingleItemPrompt(phase, item, prevResults, projectDir) {
138
+ const singular = PHASE_SINGULAR[phase];
139
+ const sourcePath = item.sourcePath || guessPaths(phase, item);
140
+
141
+ const prevPaths = [];
142
+ for (const [, result] of Object.entries(prevResults)) {
143
+ if (result.builtPaths) prevPaths.push(...result.builtPaths);
144
+ }
145
+
146
+ const importInstructions = prevPaths.length > 0
147
+ ? `\n## Imports from Previous Tier\nImport these already-built components — do NOT rebuild their functionality inline:\n${prevPaths.map(p => `- ${p}`).join("\n")}\n`
148
+ : "";
149
+
150
+ return `You are building ONE ${singular} component for a Vue 3 + TypeScript project.
151
+
152
+ ## Task
153
+ Build this component from its design contract. Read the contract for exact visual specs — do NOT approximate values.
154
+
155
+ ## Component
156
+ - **${item.componentName}**
157
+ - Contract: ${item.fullContractPath}
158
+ - Write to: ${sourcePath}
159
+ ${importInstructions}
160
+ ## Rules
161
+ - Read the contract file for exact property values
162
+ - Write the component to the specified source path
163
+ - Follow the project's existing code conventions (check existing files in src/)
164
+ - Use the project's existing dependencies (check package.json)
165
+ - When the component is complete, STOP. Do not start a dev server or build other components.
166
+
167
+ This is a FINITE task. Build this ONE ${singular}, then EXIT.`;
168
+ }
169
+
137
170
  // ─── Measurement ────────────────────────────────────────────────────────────
138
171
 
139
172
  function hasPlaywright(projectDir) {
@@ -332,16 +365,16 @@ ${componentList}
332
365
  ${measurementContext}
333
366
  ## Review Process
334
367
 
335
- For EACH component:
336
- 1. Read the design contract file (path given above) — note every specified property value
337
- 2. Read the source file — check that specified values are implemented correctly
338
- 3. Use Playwright to render the component at http://localhost:${ports.reviewPort}/ and measure:
339
- - Does the component render and have correct dimensions?
340
- - Do colors, fonts, spacing, border-radius match the contract?
341
- - For charts: correct chart type, orientation, axis labels, legend position, data format?
342
- - For layouts: correct grid columns, gap, padding, child count and arrangement?
343
- - For interactive elements: correct states, hover effects, click behavior?
344
- 4. Compare contract values against actual rendered values be SPECIFIC (exact px, hex, counts)
368
+ **Step 1 — Code review (do this FIRST for ALL components):**
369
+ For each component, read the design contract file and the source file. Check that every contract-specified value (colors, sizes, spacing, border-radius, font, layout, chart type, etc.) is correctly implemented in the code. This is your primary review most issues are catchable from code alone.
370
+
371
+ **Step 2 Playwright spot-check (do this AFTER code review):**
372
+ Use Playwright to render components at http://localhost:${ports.reviewPort}/ and verify:
373
+ - Components render without errors and have correct dimensions
374
+ - Chart types, orientations, and data structures are correct
375
+ - Interactive elements respond correctly (hover, click, states)
376
+
377
+ Focus Playwright on components where code review raised concerns or where visual behavior can't be verified from code alone (e.g., SVG rendering, computed layouts). You do NOT need to re-measure every CSS property — the orchestrator already ran Playwright measurements above.
345
378
 
346
379
  ## Output Format
347
380
 
@@ -359,7 +392,8 @@ If ALL components match their contracts, output:
359
392
  []
360
393
  [/REVIEW_ISSUES]
361
394
 
362
- ## Rules
395
+ ## CRITICAL — Output Rules
396
+ - Output MUST contain the [REVIEW_ISSUES] markers — the orchestrator parses your result from these markers. Without them, your review is lost.
363
397
  - You write ZERO code. You ONLY review.
364
398
  - Be HARSH. Your value is in catching what the builder missed.
365
399
  - NEVER say "looks close" or "appears to match" — give SPECIFIC values.
@@ -367,6 +401,49 @@ If ALL components match their contracts, output:
367
401
  - Severity guide: critical = wrong component type, missing element, broken render. high = wrong dimensions, colors, layout. medium = spacing/padding off. low = minor visual difference.`;
368
402
  }
369
403
 
404
+ function buildSingleItemReviewPrompt(phase, item, measurements, projectDir, ports) {
405
+ const sourcePath = item.sourcePath || guessPaths(phase, item);
406
+
407
+ // Include measurement failures for this item
408
+ const m = measurements[item.id] || [];
409
+ const failures = m.filter(x => !x.pass);
410
+ const measurementContext = failures.length > 0
411
+ ? `\n## Measurement Failures\nPlaywright detected: ${failures.map(f => `${f.property}: expected ${f.expected}, got ${f.actual}`).join("; ")}\n`
412
+ : "";
413
+
414
+ return `You are an INDEPENDENT design reviewer. Review this ONE component against its design contract.
415
+
416
+ ## Component
417
+ - **${item.componentName}**
418
+ - Contract: ${item.fullContractPath}
419
+ - Source: ${sourcePath}
420
+ - Selector: \`${item.selector || "." + item.id}\`
421
+ ${measurementContext}
422
+ ## Review Process
423
+
424
+ 1. Read the design contract file — note every specified property value
425
+ 2. Read the source file — check that every contract-specified value is implemented correctly
426
+ 3. If needed, use Playwright to render at http://localhost:${ports.reviewPort}/ and verify visual behavior
427
+
428
+ ## Output Format
429
+
430
+ [REVIEW_ISSUES]
431
+ [
432
+ {"component": "${item.componentName}", "severity": "high", "description": "Contract specifies X, code has Y"}
433
+ ]
434
+ [/REVIEW_ISSUES]
435
+
436
+ If the component matches its contract, output:
437
+ [REVIEW_ISSUES]
438
+ []
439
+ [/REVIEW_ISSUES]
440
+
441
+ ## Rules
442
+ - You write ZERO code. You ONLY review.
443
+ - Be HARSH — specific values only, no "looks close."
444
+ - Output MUST contain [REVIEW_ISSUES] markers.`;
445
+ }
446
+
370
447
  function buildAutoFixPrompt(phase, issues, items, projectDir) {
371
448
  const issueList = issues.map((issue, i) => {
372
449
  const item = items.find(c => c.componentName === issue.component);
@@ -439,13 +516,16 @@ const designBuildWorkflow = {
439
516
  devServerTimeout: 30_000,
440
517
  maxReviewCycles: 3,
441
518
  maxAutoReviewCycles: 4,
442
- reviewTimeout: 300_000,
519
+ reviewTimeout: 600_000,
520
+ perItemTimeout: 120_000,
443
521
  },
444
522
  completionMessage: "All done. Run your app to verify: npm run dev",
445
523
 
446
524
  discoverWork,
447
525
  buildPrompt,
448
526
  buildReviewPrompt,
527
+ buildSingleItemPrompt,
528
+ buildSingleItemReviewPrompt,
449
529
  buildAutoFixPrompt,
450
530
  measure,
451
531
  buildQueueItem,
@@ -134,6 +134,8 @@ class Orchestrator {
134
134
  case "--review-port": opts.reviewPort = parseInt(argv[++i], 10); break;
135
135
  case "--timeout": opts.timeout = parseInt(argv[++i], 10) * 1000; break;
136
136
  case "--skip-measure": opts.skipMeasure = true; break;
137
+ case "--clean": opts.clean = true; break;
138
+ case "--verbose": case "-v": opts.verbose = true; break;
137
139
  case "--help":
138
140
  case "-h":
139
141
  if (this.wf.showUsage) this.wf.showUsage();
@@ -160,6 +162,8 @@ ${BOLD}Options:${RESET}
160
162
  --review-port <N> Review server port (default: ${this.wf.defaults?.reviewPort || 3456})
161
163
  --timeout <sec> Claude timeout per phase in seconds (default: 600)
162
164
  --skip-measure Skip automated measurement (human-review only)
165
+ --clean Delete previous build output before building each phase
166
+ --verbose, -v Show Claude's tool calls and prompts in terminal
163
167
  --help Show this help
164
168
 
165
169
  ${BOLD}Phases:${RESET} ${this.wf.phases.join(" → ")}
@@ -185,31 +189,98 @@ ${BOLD}Phases:${RESET} ${this.wf.phases.join(" → ")}
185
189
  const start = Date.now();
186
190
  let output = "";
187
191
  let exitCode = 0;
192
+ const verbose = this._verbose;
188
193
 
189
194
  // Build args: -p for print mode, --dangerously-skip-permissions so spawned
190
195
  // Claude can write files without interactive permission prompts
191
- const args = ["-p", "--dangerously-skip-permissions", prompt];
196
+ const args = ["-p", "--dangerously-skip-permissions", "--output-format", "stream-json"];
197
+ if (verbose) args.push("--verbose");
198
+ args.push(prompt);
199
+
200
+ // Log prompt to file for debugging
201
+ if (verbose) {
202
+ const logDir = path.join(this.getReviewDir(projectDir), "build-logs");
203
+ ensureDir(logDir);
204
+ const label = opts.label || "claude";
205
+ fs.writeFileSync(
206
+ path.join(logDir, `${label}-prompt.txt`),
207
+ `--- Prompt (${new Date().toISOString()}) ---\nTimeout: ${(timeout || 600_000) / 1000}s\nCWD: ${projectDir}\n\n${prompt}`
208
+ );
209
+ }
192
210
 
193
211
  try {
194
- output = execFileSync("claude", args, {
212
+ const raw = execFileSync("claude", args, {
195
213
  encoding: "utf8",
196
214
  timeout: timeout || this.wf.defaults?.timeout || 600_000,
197
215
  stdio: ["pipe", "pipe", "pipe"],
198
216
  cwd: projectDir,
199
217
  maxBuffer: 10 * 1024 * 1024,
200
218
  });
219
+ // Parse stream-json: each line is a JSON event, extract assistant text
220
+ output = this._parseStreamJson(raw, verbose);
201
221
  } catch (e) {
202
- output = (e.stdout || "") + (e.stderr || "");
222
+ // On timeout/error, still parse any partial stream-json output we got
223
+ const rawOut = (e.stdout || "") + (e.stderr || "");
224
+ output = this._parseStreamJson(rawOut, verbose);
203
225
  exitCode = e.status || 1;
204
226
  if (e.killed) warn(`Claude timed out after ${(timeout || 600_000) / 1000}s`);
205
227
  }
206
228
 
207
229
  const duration = Math.round((Date.now() - start) / 1000);
230
+
231
+ if (verbose) {
232
+ dim(`Claude finished: exit=${exitCode}, duration=${duration}s, output=${output.length} chars`);
233
+ }
234
+
208
235
  return { output, exitCode, duration };
209
236
  }
210
237
 
211
238
  // ─── Server Management ───────────────────────────────────────────────
212
239
 
240
+ _parseStreamJson(raw, verbose) {
241
+ // stream-json format: one JSON object per line
242
+ // We want assistant text content and tool use visibility
243
+ const textParts = [];
244
+ const toolCalls = [];
245
+
246
+ for (const line of raw.split("\n")) {
247
+ if (!line.trim()) continue;
248
+ try {
249
+ const event = JSON.parse(line);
250
+ // Assistant text messages
251
+ if (event.type === "assistant" && event.message?.content) {
252
+ for (const block of event.message.content) {
253
+ if (block.type === "text") textParts.push(block.text);
254
+ }
255
+ }
256
+ // Content block deltas (partial streaming)
257
+ if (event.type === "content_block_delta" && event.delta?.text) {
258
+ textParts.push(event.delta.text);
259
+ }
260
+ // Result message
261
+ if (event.type === "result" && event.result) {
262
+ // result contains the final response text
263
+ if (typeof event.result === "string") textParts.push(event.result);
264
+ }
265
+ // Tool use tracking for verbose
266
+ if (verbose && event.type === "assistant" && event.message?.content) {
267
+ for (const block of event.message.content) {
268
+ if (block.type === "tool_use") {
269
+ toolCalls.push(block.name);
270
+ dim(` → ${block.name}${block.input?.command ? ": " + String(block.input.command).slice(0, 80) : ""}`);
271
+ }
272
+ }
273
+ }
274
+ } catch { /* skip non-JSON lines */ }
275
+ }
276
+
277
+ if (verbose && toolCalls.length > 0) {
278
+ dim(` Tool calls: ${toolCalls.length} (${[...new Set(toolCalls)].join(", ")})`);
279
+ }
280
+
281
+ return textParts.join("");
282
+ }
283
+
213
284
  startDevServer(projectDir, port) {
214
285
  if (isPortInUse(port)) {
215
286
  success(`Dev server already running on port ${port}`);
@@ -368,6 +439,7 @@ ${BOLD}Phases:${RESET} ${this.wf.phases.join(" → ")}
368
439
  openBrowser(`http://localhost:${reviewPort}/review`);
369
440
 
370
441
  // IRONCLAD GATE — JavaScript polling loop
442
+ let healthCheckCounter = 0;
371
443
  while (true) {
372
444
  if (fs.existsSync(signalPath)) {
373
445
  try {
@@ -376,6 +448,22 @@ ${BOLD}Phases:${RESET} ${this.wf.phases.join(" → ")}
376
448
  return data;
377
449
  } catch { /* malformed — wait for rewrite */ }
378
450
  }
451
+
452
+ // Every 10 polls (~30s), verify review server is still alive
453
+ healthCheckCounter++;
454
+ if (healthCheckCounter % 10 === 0) {
455
+ if (!isPortInUse(reviewPort)) {
456
+ warn("Review server is down! Restarting...");
457
+ const devPort = this._activeDevPort || reviewPort - 1283; // fallback heuristic
458
+ const info = this.startReviewServer(projectDir, devPort, reviewPort);
459
+ if (info.pid || info.alreadyRunning) {
460
+ success("Review server restarted");
461
+ } else {
462
+ error("Failed to restart review server — please restart the orchestrator");
463
+ }
464
+ }
465
+ }
466
+
379
467
  syncSleep(3000);
380
468
  }
381
469
  }
@@ -474,11 +562,17 @@ ${BOLD}Phases:${RESET} ${this.wf.phases.join(" → ")}
474
562
  const { projectDir, resume, startPhase, devPort, reviewPort, skipMeasure } = opts;
475
563
  const phases = this.wf.phases;
476
564
  const maxReviewCycles = this.wf.defaults?.maxReviewCycles || 3;
565
+ this._verbose = opts.verbose || false;
477
566
 
567
+ const pkgVersion = (() => {
568
+ try { return require(path.join(__dirname, "..", "package.json")).version; } catch { return "unknown"; }
569
+ })();
478
570
  heading(`GSD-T ${this.wf.name} Orchestrator`);
571
+ log(` Version: ${BOLD}v${pkgVersion}${RESET}`);
479
572
  log(` Project: ${projectDir}`);
480
573
  log(` Ports: dev=${devPort} review=${reviewPort}`);
481
574
  log(` Phases: ${phases.join(" → ")}`);
575
+ if (this._verbose) log(` ${YELLOW}Verbose mode: ON${RESET}`);
482
576
  log("");
483
577
 
484
578
  // 1. Verify prerequisites
@@ -514,6 +608,7 @@ ${BOLD}Phases:${RESET} ${this.wf.phases.join(" → ")}
514
608
  const devInfo = this.startDevServer(projectDir, devPort);
515
609
  const reviewInfo = this.startReviewServer(projectDir, devPort, reviewPort);
516
610
  this.pids = [devInfo.pid, reviewInfo.pid].filter(Boolean);
611
+ this._activeDevPort = devPort;
517
612
  }
518
613
 
519
614
  // Register cleanup on exit
@@ -559,111 +654,256 @@ ${BOLD}Phases:${RESET} ${this.wf.phases.join(" → ")}
559
654
  }
560
655
  }
561
656
 
562
- // 6b. Spawn Claude for this phase
563
- const prompt = this.wf.buildPrompt(phase, items, prevResults, projectDir);
564
- log(`\n${CYAN} ⚙${RESET} Spawning Claude to build ${items.length} ${phase}...`);
565
- dim(`Timeout: ${(opts.timeout || 600_000) / 1000}s`);
657
+ // Clean previous build output if --clean
658
+ if (opts.clean && this.wf.guessPaths) {
659
+ const cleaned = [];
660
+ for (const item of items) {
661
+ const guessed = this.wf.guessPaths(phase, item);
662
+ const paths = Array.isArray(guessed) ? guessed : [guessed];
663
+ for (const p of paths) {
664
+ const full = path.join(projectDir, p);
665
+ if (fs.existsSync(full)) {
666
+ fs.unlinkSync(full);
667
+ cleaned.push(p);
668
+ }
669
+ }
670
+ }
671
+ if (cleaned.length > 0) {
672
+ info(`--clean: removed ${cleaned.length} existing file(s) for ${phase}`);
673
+ }
674
+ }
566
675
 
567
- const buildResult = this.spawnClaude(projectDir, prompt, opts.timeout);
568
- if (buildResult.exitCode === 0) {
569
- success(`Claude finished building ${phase} in ${buildResult.duration}s`);
570
- } else {
571
- warn(`Claude exited with code ${buildResult.exitCode} after ${buildResult.duration}s`);
676
+ // Clear stale auto-review files from previous runs
677
+ const arDir = path.join(this.getReviewDir(projectDir), "auto-review");
678
+ if (fs.existsSync(arDir)) {
679
+ for (const f of fs.readdirSync(arDir)) {
680
+ if (f.startsWith(`${phase}-`)) {
681
+ try { fs.unlinkSync(path.join(arDir, f)); } catch { /* ignore */ }
682
+ }
683
+ }
572
684
  }
573
685
 
574
- // Log build output for debugging
575
686
  const buildLogDir = path.join(this.getReviewDir(projectDir), "build-logs");
576
687
  ensureDir(buildLogDir);
577
- fs.writeFileSync(
578
- path.join(buildLogDir, `${phase}-build.log`),
579
- `Exit code: ${buildResult.exitCode}\nDuration: ${buildResult.duration}s\nPrompt length: ${prompt.length}\n\n--- OUTPUT ---\n${buildResult.output.slice(0, 20000)}`
580
- );
688
+ const maxAutoReviewCycles = this.wf.defaults?.maxAutoReviewCycles || 4;
689
+ const perItemTimeout = this.wf.defaults?.perItemTimeout || 120_000;
690
+ let builtPaths = [];
691
+ let measurements = {};
581
692
 
582
- // 6c. Collect built paths
583
- const builtPaths = items.map(item =>
584
- item.sourcePath || (this.wf.guessPaths ? this.wf.guessPaths(phase, item) : "")
585
- );
693
+ // ── Per-item pipeline: build ONE → review ONE → fix if needed ──
694
+ // Preferred when workflow provides single-item prompts.
695
+ // Each Claude spawn reads 1 contract + 1 source = tiny context, fast completion.
696
+ if (this.wf.buildSingleItemPrompt && this.wf.buildSingleItemReviewPrompt) {
697
+ log(`\n${CYAN} ⚙${RESET} Building and reviewing ${items.length} ${phase} one at a time...`);
586
698
 
587
- // 6d. Measure
588
- let measurements = {};
589
- if (!skipMeasure && this.wf.measure) {
590
- measurements = this.wf.measure(projectDir, phase, items, { devPort, reviewPort }) || {};
591
- }
699
+ for (let idx = 0; idx < items.length; idx++) {
700
+ const item = items[idx];
701
+ heading(` [${idx + 1}/${items.length}] ${item.componentName}`);
592
702
 
593
- // 6d.5. Automated AI review loop (Term 2 equivalent)
594
- // Spawns an independent reviewer Claude that compares built output against contracts.
595
- // If issues found → spawn fixer Claude → re-measure → re-review until clean.
596
- const maxAutoReviewCycles = this.wf.defaults?.maxAutoReviewCycles || 4;
597
- if (this.wf.buildReviewPrompt) {
598
- let autoReviewCycle = 0;
599
- let autoReviewClean = false;
600
-
601
- while (autoReviewCycle < maxAutoReviewCycles && !autoReviewClean) {
602
- autoReviewCycle++;
603
- heading(`Automated Review — ${phase} (cycle ${autoReviewCycle}/${maxAutoReviewCycles})`);
604
-
605
- // Spawn reviewer Claude — independent, no builder context
606
- const reviewPrompt = this.wf.buildReviewPrompt(phase, items, measurements, projectDir, { devPort, reviewPort });
607
- log(`\n${CYAN} ⚙${RESET} Spawning reviewer Claude for ${phase}...`);
608
- const reviewTimeout = this.wf.defaults?.reviewTimeout || 300_000;
609
- const reviewResult = this.spawnClaude(projectDir, reviewPrompt, reviewTimeout);
610
-
611
- // Parse reviewer output for issues
612
- let issues;
613
-
614
- // Treat reviewer crash (non-zero exit, very short duration) as a failed review, not a pass
615
- if (reviewResult.exitCode !== 0 && reviewResult.duration < 10) {
616
- warn(`Reviewer crashed (code ${reviewResult.exitCode}, ${reviewResult.duration}s) — treating as review failure, will retry`);
617
- issues = [{ component: "ALL", severity: "critical", description: `Reviewer crashed with exit code ${reviewResult.exitCode} — review not performed` }];
618
- } else {
619
- issues = this.wf.parseReviewResult
620
- ? this.wf.parseReviewResult(reviewResult.output, phase)
621
- : this._parseDefaultReviewResult(reviewResult.output);
703
+ // Build one item
704
+ const buildPrompt = this.wf.buildSingleItemPrompt(phase, item, prevResults, projectDir);
705
+ dim(` Building...`);
706
+ const buildResult = this.spawnClaude(projectDir, buildPrompt, perItemTimeout, { label: `${phase}-build-${item.id}` });
622
707
 
623
- if (reviewResult.exitCode === 0) {
624
- success(`Reviewer finished in ${reviewResult.duration}s`);
625
- } else {
626
- warn(`Reviewer exited with code ${reviewResult.exitCode} after ${reviewResult.duration}s`);
627
- }
708
+ if (buildResult.exitCode === 0) {
709
+ success(` Built (${buildResult.duration}s)`);
710
+ } else {
711
+ warn(` Build exited with code ${buildResult.exitCode} (${buildResult.duration}s)`);
628
712
  }
629
713
 
630
- // Write review report
631
- const reportDir = path.join(this.getReviewDir(projectDir), "auto-review");
632
- ensureDir(reportDir);
633
714
  fs.writeFileSync(
634
- path.join(reportDir, `${phase}-cycle-${autoReviewCycle}.json`),
635
- JSON.stringify({ cycle: autoReviewCycle, issues, exitCode: reviewResult.exitCode, duration: reviewResult.duration, output: reviewResult.output.slice(0, 5000) }, null, 2)
715
+ path.join(buildLogDir, `${phase}-build-${item.id}.log`),
716
+ `Exit code: ${buildResult.exitCode}\nDuration: ${buildResult.duration}s\n\n--- OUTPUT ---\n${buildResult.output.slice(0, 5000)}`
636
717
  );
637
718
 
638
- if (issues.length === 0) {
639
- autoReviewClean = true;
640
- success(`Automated review passed no issues found in ${phase}`);
641
- } else {
642
- warn(`Automated review found ${issues.length} issue(s) in ${phase}`);
643
- for (const issue of issues) {
644
- dim(`${issue.component || "?"}: ${issue.description || issue.reason || "issue"} [${issue.severity || "medium"}]`);
719
+ // Review one item (up to maxAutoReviewCycles)
720
+ let itemClean = false;
721
+ for (let cycle = 1; cycle <= maxAutoReviewCycles && !itemClean; cycle++) {
722
+ dim(` Review cycle ${cycle}/${maxAutoReviewCycles}...`);
723
+ const reviewPrompt = this.wf.buildSingleItemReviewPrompt(phase, item, {}, projectDir, { devPort, reviewPort });
724
+ const reviewResult = this.spawnClaude(projectDir, reviewPrompt, perItemTimeout, { label: `${phase}-review-${item.id}-c${cycle}` });
725
+
726
+ const isCrash = reviewResult.exitCode !== 0 && reviewResult.duration < 10;
727
+ const isKilled = [143, 137].includes(reviewResult.exitCode);
728
+ const isEmptyFail = reviewResult.exitCode !== 0 && !reviewResult.output.trim();
729
+
730
+ let itemIssues = [];
731
+ if (isCrash || isKilled || isEmptyFail) {
732
+ const reason = isCrash ? "crashed" : isKilled ? "killed/timed out" : "failed with no output";
733
+ warn(` Reviewer ${reason} (${reviewResult.duration}s)`);
734
+ itemIssues = [{ component: item.componentName, severity: "critical", description: `Reviewer ${reason}` }];
735
+ } else {
736
+ itemIssues = this.wf.parseReviewResult
737
+ ? this.wf.parseReviewResult(reviewResult.output, phase)
738
+ : this._parseDefaultReviewResult(reviewResult.output);
645
739
  }
646
740
 
647
- if (autoReviewCycle < maxAutoReviewCycles) {
648
- // Spawn fixer Claude with the issues
649
- const fixPrompt = this.wf.buildAutoFixPrompt
650
- ? this.wf.buildAutoFixPrompt(phase, issues, items, projectDir)
651
- : this._defaultAutoFixPrompt(phase, issues);
741
+ if (itemIssues.length === 0) {
742
+ itemClean = true;
743
+ success(` Clean (${reviewResult.duration}s)`);
744
+ } else {
745
+ warn(` ${itemIssues.length} issue(s) found`);
746
+ for (const issue of itemIssues) {
747
+ dim(` ${issue.description || issue.reason || "issue"} [${issue.severity || "medium"}]`);
748
+ }
652
749
 
653
- log(`\n${CYAN} ⚙${RESET} Spawning fixer Claude for ${issues.length} issue(s)...`);
654
- const fixResult = this.spawnClaude(projectDir, fixPrompt, opts.timeout || 600_000);
655
- if (fixResult.exitCode === 0) success(`Fixer finished in ${fixResult.duration}s`);
656
- else warn(`Fixer exited with code ${fixResult.exitCode}`);
750
+ if (cycle < maxAutoReviewCycles) {
751
+ // Fix this one item
752
+ const fixPrompt = this.wf.buildAutoFixPrompt
753
+ ? this.wf.buildAutoFixPrompt(phase, itemIssues, [item], projectDir)
754
+ : this._defaultAutoFixPrompt(phase, itemIssues);
755
+ dim(` Fixing...`);
756
+ const fixResult = this.spawnClaude(projectDir, fixPrompt, perItemTimeout, { label: `${phase}-fix-${item.id}-c${cycle}` });
757
+ if (fixResult.exitCode === 0) success(` Fixed (${fixResult.duration}s)`);
758
+ else warn(` Fix exited with code ${fixResult.exitCode}`);
759
+ }
760
+ }
761
+ }
762
+
763
+ builtPaths.push(item.sourcePath || (this.wf.guessPaths ? this.wf.guessPaths(phase, item) : ""));
764
+ }
657
765
 
658
- // Re-measure after fixes
659
- if (!skipMeasure && this.wf.measure) {
660
- measurements = this.wf.measure(projectDir, phase, items, { devPort, reviewPort }) || {};
766
+ // Measure ALL at once (one Playwright run after all items built)
767
+ if (!skipMeasure && this.wf.measure) {
768
+ heading("Measuring all built components");
769
+ measurements = this.wf.measure(projectDir, phase, items, { devPort, reviewPort }) || {};
770
+ }
771
+
772
+ } else {
773
+ // ── Legacy pipeline: build ALL → measure ALL → review loop ──
774
+ const prompt = this.wf.buildPrompt(phase, items, prevResults, projectDir);
775
+ log(`\n${CYAN} ⚙${RESET} Spawning Claude to build ${items.length} ${phase}...`);
776
+ dim(`Timeout: ${(opts.timeout || 600_000) / 1000}s`);
777
+
778
+ const buildResult = this.spawnClaude(projectDir, prompt, opts.timeout, { label: `${phase}-build` });
779
+ if (buildResult.exitCode === 0) {
780
+ success(`Claude finished building ${phase} in ${buildResult.duration}s`);
781
+ } else {
782
+ warn(`Claude exited with code ${buildResult.exitCode} after ${buildResult.duration}s`);
783
+ }
784
+
785
+ fs.writeFileSync(
786
+ path.join(buildLogDir, `${phase}-build.log`),
787
+ `Exit code: ${buildResult.exitCode}\nDuration: ${buildResult.duration}s\nPrompt length: ${prompt.length}\n\n--- OUTPUT ---\n${buildResult.output.slice(0, 20000)}`
788
+ );
789
+
790
+ builtPaths = items.map(item =>
791
+ item.sourcePath || (this.wf.guessPaths ? this.wf.guessPaths(phase, item) : "")
792
+ );
793
+
794
+ // Measure
795
+ if (!skipMeasure && this.wf.measure) {
796
+ measurements = this.wf.measure(projectDir, phase, items, { devPort, reviewPort }) || {};
797
+ }
798
+
799
+ // Auto-review loop (legacy all-at-once)
800
+ if (this.wf.buildReviewPrompt || this.wf.buildSingleItemReviewPrompt) {
801
+ let autoReviewCycle = 0;
802
+ let autoReviewClean = false;
803
+
804
+ while (autoReviewCycle < maxAutoReviewCycles && !autoReviewClean) {
805
+ autoReviewCycle++;
806
+ heading(`Automated Review — ${phase} (cycle ${autoReviewCycle}/${maxAutoReviewCycles})`);
807
+
808
+ const reviewTimeout = this.wf.defaults?.reviewTimeout || 300_000;
809
+ let issues = [];
810
+
811
+ if (this.wf.buildSingleItemReviewPrompt) {
812
+ // Per-item review even in legacy build mode
813
+ log(`\n${CYAN} ⚙${RESET} Reviewing ${items.length} ${phase} one at a time...`);
814
+ let totalDuration = 0;
815
+
816
+ for (let idx = 0; idx < items.length; idx++) {
817
+ const item = items[idx];
818
+ const itemMeasurements = { [item.id]: measurements[item.id] || [] };
819
+ const reviewPrompt = this.wf.buildSingleItemReviewPrompt(phase, item, itemMeasurements, projectDir, { devPort, reviewPort });
820
+
821
+ dim(` [${idx + 1}/${items.length}] ${item.componentName}...`);
822
+ const reviewResult = this.spawnClaude(projectDir, reviewPrompt, Math.min(reviewTimeout, perItemTimeout), { label: `${phase}-review-c${autoReviewCycle}-${item.id}` });
823
+ totalDuration += reviewResult.duration;
824
+
825
+ const isCrash = reviewResult.exitCode !== 0 && reviewResult.duration < 10;
826
+ const isKilled = [143, 137].includes(reviewResult.exitCode);
827
+ const isEmptyFail = reviewResult.exitCode !== 0 && !reviewResult.output.trim();
828
+
829
+ if (isCrash || isKilled || isEmptyFail) {
830
+ const reason = isCrash ? "crashed" : isKilled ? "killed/timed out" : "failed with no output";
831
+ warn(` ${item.componentName}: reviewer ${reason} (${reviewResult.duration}s)`);
832
+ issues.push({ component: item.componentName, severity: "critical", description: `Reviewer ${reason} — review not performed` });
833
+ } else {
834
+ const itemIssues = this.wf.parseReviewResult
835
+ ? this.wf.parseReviewResult(reviewResult.output, phase)
836
+ : this._parseDefaultReviewResult(reviewResult.output);
837
+
838
+ if (itemIssues.length > 0) {
839
+ warn(` ${item.componentName}: ${itemIssues.length} issue(s) (${reviewResult.duration}s)`);
840
+ issues.push(...itemIssues);
841
+ } else {
842
+ success(` ${item.componentName}: clean (${reviewResult.duration}s)`);
843
+ }
844
+ }
661
845
  }
846
+ log(`\n Total review time: ${totalDuration}s for ${items.length} items`);
847
+
662
848
  } else {
663
- warn(`Max auto-review cycles reached — ${issues.length} issue(s) will go to human review`);
664
- // Attach unresolved issues to measurements for human visibility
665
- const issueFile = path.join(this.getReviewDir(projectDir), "auto-review", `${phase}-unresolved.json`);
666
- fs.writeFileSync(issueFile, JSON.stringify(issues, null, 2));
849
+ // All-at-once review
850
+ const reviewPrompt = this.wf.buildReviewPrompt(phase, items, measurements, projectDir, { devPort, reviewPort });
851
+ log(`\n${CYAN} ⚙${RESET} Spawning reviewer Claude for all ${phase}...`);
852
+ const reviewTimeout2 = this.wf.defaults?.reviewTimeout || 300_000;
853
+ const reviewResult = this.spawnClaude(projectDir, reviewPrompt, reviewTimeout2, { label: `${phase}-review-cycle${autoReviewCycle}` });
854
+
855
+ const isCrash = reviewResult.exitCode !== 0 && reviewResult.duration < 10;
856
+ const isKilled = [143, 137].includes(reviewResult.exitCode);
857
+ const isEmptyFail = reviewResult.exitCode !== 0 && !reviewResult.output.trim();
858
+
859
+ if (isCrash || isKilled || isEmptyFail) {
860
+ const reason = isCrash ? "crashed" : isKilled ? "killed/timed out" : "failed with no output";
861
+ warn(`Reviewer ${reason} (code ${reviewResult.exitCode}, ${reviewResult.duration}s)`);
862
+ issues = [{ component: "ALL", severity: "critical", description: `Reviewer ${reason} with exit code ${reviewResult.exitCode}` }];
863
+ } else {
864
+ issues = this.wf.parseReviewResult
865
+ ? this.wf.parseReviewResult(reviewResult.output, phase)
866
+ : this._parseDefaultReviewResult(reviewResult.output);
867
+ if (reviewResult.exitCode === 0) success(`Reviewer finished in ${reviewResult.duration}s`);
868
+ else warn(`Reviewer exited with code ${reviewResult.exitCode} after ${reviewResult.duration}s`);
869
+ }
870
+ }
871
+
872
+ // Write review report
873
+ const reportDir = path.join(this.getReviewDir(projectDir), "auto-review");
874
+ ensureDir(reportDir);
875
+ fs.writeFileSync(
876
+ path.join(reportDir, `${phase}-cycle-${autoReviewCycle}.json`),
877
+ JSON.stringify({ cycle: autoReviewCycle, issues, itemCount: items.length }, null, 2)
878
+ );
879
+
880
+ if (issues.length === 0) {
881
+ autoReviewClean = true;
882
+ success(`Automated review passed — no issues found in ${phase}`);
883
+ } else {
884
+ warn(`Automated review found ${issues.length} issue(s) in ${phase}`);
885
+ for (const issue of issues) {
886
+ dim(`${issue.component || "?"}: ${issue.description || issue.reason || "issue"} [${issue.severity || "medium"}]`);
887
+ }
888
+
889
+ if (autoReviewCycle < maxAutoReviewCycles) {
890
+ const fixPrompt = this.wf.buildAutoFixPrompt
891
+ ? this.wf.buildAutoFixPrompt(phase, issues, items, projectDir)
892
+ : this._defaultAutoFixPrompt(phase, issues);
893
+
894
+ log(`\n${CYAN} ⚙${RESET} Spawning fixer Claude for ${issues.length} issue(s)...`);
895
+ const fixResult = this.spawnClaude(projectDir, fixPrompt, opts.timeout || 600_000, { label: `${phase}-fix-cycle${autoReviewCycle}` });
896
+ if (fixResult.exitCode === 0) success(`Fixer finished in ${fixResult.duration}s`);
897
+ else warn(`Fixer exited with code ${fixResult.exitCode}`);
898
+
899
+ if (!skipMeasure && this.wf.measure) {
900
+ measurements = this.wf.measure(projectDir, phase, items, { devPort, reviewPort }) || {};
901
+ }
902
+ } else {
903
+ warn(`Max auto-review cycles reached — ${issues.length} issue(s) will go to human review`);
904
+ const issueFile = path.join(this.getReviewDir(projectDir), "auto-review", `${phase}-unresolved.json`);
905
+ fs.writeFileSync(issueFile, JSON.stringify(issues, null, 2));
906
+ }
667
907
  }
668
908
  }
669
909
  }
@@ -692,7 +932,7 @@ ${BOLD}Phases:${RESET} ${this.wf.phases.join(" → ")}
692
932
  ? this.wf.buildFixPrompt(phase, feedback.needsWork)
693
933
  : this._defaultFixPrompt(phase, feedback.needsWork);
694
934
  info(`Spawning Claude to apply ${feedback.needsWork.length} fixes...`);
695
- const fixResult = this.spawnClaude(projectDir, fixPrompt, opts.timeout || 600_000);
935
+ const fixResult = this.spawnClaude(projectDir, fixPrompt, opts.timeout || 600_000, { label: `${phase}-human-fix` });
696
936
  if (fixResult.exitCode === 0) success("Fixes applied");
697
937
  else warn(`Fix attempt returned code ${fixResult.exitCode}`);
698
938
  } else {
@@ -33,6 +33,8 @@ Pass any of these as `$ARGUMENTS`:
33
33
  | `--review-port <N>` | Review server port (default: 3456) |
34
34
  | `--timeout <sec>` | Claude timeout per tier in seconds (default: 600) |
35
35
  | `--skip-measure` | Skip Playwright measurement (human-review only) |
36
+ | `--clean` | Delete previous build output before building each phase |
37
+ | `--verbose`, `-v` | Show Claude's tool calls and prompts in terminal |
36
38
 
37
39
  ## Prerequisites
38
40
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tekyzinc/gsd-t",
3
- "version": "2.71.20",
3
+ "version": "2.72.10",
4
4
  "description": "GSD-T: Contract-Driven Development for Claude Code — 56 slash commands with headless CI/CD mode, graph-powered code analysis, real-time agent dashboard, execution intelligence, task telemetry, doc-ripple enforcement, backlog management, impact analysis, test sync, milestone archival, and PRD generation",
5
5
  "author": "Tekyz, Inc.",
6
6
  "license": "MIT",