@tekyzinc/gsd-t 2.71.21 → 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,19 @@
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
+
5
18
  ## [2.71.21] - 2026-04-08
6
19
 
7
20
  ### Fixed (orchestrator — timeout false-pass, review server health, stale cleanup)
@@ -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}`);
@@ -491,11 +562,17 @@ ${BOLD}Phases:${RESET} ${this.wf.phases.join(" → ")}
491
562
  const { projectDir, resume, startPhase, devPort, reviewPort, skipMeasure } = opts;
492
563
  const phases = this.wf.phases;
493
564
  const maxReviewCycles = this.wf.defaults?.maxReviewCycles || 3;
565
+ this._verbose = opts.verbose || false;
494
566
 
567
+ const pkgVersion = (() => {
568
+ try { return require(path.join(__dirname, "..", "package.json")).version; } catch { return "unknown"; }
569
+ })();
495
570
  heading(`GSD-T ${this.wf.name} Orchestrator`);
571
+ log(` Version: ${BOLD}v${pkgVersion}${RESET}`);
496
572
  log(` Project: ${projectDir}`);
497
573
  log(` Ports: dev=${devPort} review=${reviewPort}`);
498
574
  log(` Phases: ${phases.join(" → ")}`);
575
+ if (this._verbose) log(` ${YELLOW}Verbose mode: ON${RESET}`);
499
576
  log("");
500
577
 
501
578
  // 1. Verify prerequisites
@@ -577,129 +654,256 @@ ${BOLD}Phases:${RESET} ${this.wf.phases.join(" → ")}
577
654
  }
578
655
  }
579
656
 
580
- // 6b. Spawn Claude for this phase
581
- const prompt = this.wf.buildPrompt(phase, items, prevResults, projectDir);
582
- log(`\n${CYAN} ⚙${RESET} Spawning Claude to build ${items.length} ${phase}...`);
583
- 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
+ }
584
675
 
585
- const buildResult = this.spawnClaude(projectDir, prompt, opts.timeout);
586
- if (buildResult.exitCode === 0) {
587
- success(`Claude finished building ${phase} in ${buildResult.duration}s`);
588
- } else {
589
- 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
+ }
590
684
  }
591
685
 
592
- // Log build output for debugging
593
686
  const buildLogDir = path.join(this.getReviewDir(projectDir), "build-logs");
594
687
  ensureDir(buildLogDir);
595
- fs.writeFileSync(
596
- path.join(buildLogDir, `${phase}-build.log`),
597
- `Exit code: ${buildResult.exitCode}\nDuration: ${buildResult.duration}s\nPrompt length: ${prompt.length}\n\n--- OUTPUT ---\n${buildResult.output.slice(0, 20000)}`
598
- );
688
+ const maxAutoReviewCycles = this.wf.defaults?.maxAutoReviewCycles || 4;
689
+ const perItemTimeout = this.wf.defaults?.perItemTimeout || 120_000;
690
+ let builtPaths = [];
691
+ let measurements = {};
599
692
 
600
- // 6c. Collect built paths
601
- const builtPaths = items.map(item =>
602
- item.sourcePath || (this.wf.guessPaths ? this.wf.guessPaths(phase, item) : "")
603
- );
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...`);
604
698
 
605
- // 6d. Measure
606
- let measurements = {};
607
- if (!skipMeasure && this.wf.measure) {
608
- measurements = this.wf.measure(projectDir, phase, items, { devPort, reviewPort }) || {};
609
- }
699
+ for (let idx = 0; idx < items.length; idx++) {
700
+ const item = items[idx];
701
+ heading(` [${idx + 1}/${items.length}] ${item.componentName}`);
610
702
 
611
- // 6d.5. Automated AI review loop (Term 2 equivalent)
612
- // Spawns an independent reviewer Claude that compares built output against contracts.
613
- // If issues found → spawn fixer Claude → re-measure → re-review until clean.
614
- const maxAutoReviewCycles = this.wf.defaults?.maxAutoReviewCycles || 4;
615
- if (this.wf.buildReviewPrompt) {
616
- // Clear stale auto-review files from previous runs for this phase
617
- const arDir = path.join(this.getReviewDir(projectDir), "auto-review");
618
- if (fs.existsSync(arDir)) {
619
- for (const f of fs.readdirSync(arDir)) {
620
- if (f.startsWith(`${phase}-`)) {
621
- try { fs.unlinkSync(path.join(arDir, f)); } catch { /* ignore */ }
622
- }
623
- }
624
- }
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}` });
625
707
 
626
- let autoReviewCycle = 0;
627
- let autoReviewClean = false;
628
-
629
- while (autoReviewCycle < maxAutoReviewCycles && !autoReviewClean) {
630
- autoReviewCycle++;
631
- heading(`Automated Review — ${phase} (cycle ${autoReviewCycle}/${maxAutoReviewCycles})`);
632
-
633
- // Spawn reviewer Claude — independent, no builder context
634
- const reviewPrompt = this.wf.buildReviewPrompt(phase, items, measurements, projectDir, { devPort, reviewPort });
635
- log(`\n${CYAN} ⚙${RESET} Spawning reviewer Claude for ${phase}...`);
636
- const reviewTimeout = this.wf.defaults?.reviewTimeout || 300_000;
637
- const reviewResult = this.spawnClaude(projectDir, reviewPrompt, reviewTimeout);
638
-
639
- // Parse reviewer output for issues
640
- let issues;
641
-
642
- // Treat reviewer failure as a failed review, not a pass:
643
- // - crash: non-zero exit + very short duration (< 10s)
644
- // - timeout/kill: exit codes 143 (SIGTERM) or 137 (SIGKILL)
645
- // - empty output with non-zero exit: reviewer produced nothing useful
646
- const isCrash = reviewResult.exitCode !== 0 && reviewResult.duration < 10;
647
- const isKilled = [143, 137].includes(reviewResult.exitCode);
648
- const isEmptyFail = reviewResult.exitCode !== 0 && !reviewResult.output.trim();
649
-
650
- if (isCrash || isKilled || isEmptyFail) {
651
- const reason = isCrash ? "crashed" : isKilled ? "killed/timed out" : "failed with no output";
652
- warn(`Reviewer ${reason} (code ${reviewResult.exitCode}, ${reviewResult.duration}s) — treating as review failure, will retry`);
653
- issues = [{ component: "ALL", severity: "critical", description: `Reviewer ${reason} with exit code ${reviewResult.exitCode} — review not performed` }];
708
+ if (buildResult.exitCode === 0) {
709
+ success(` Built (${buildResult.duration}s)`);
654
710
  } else {
655
- issues = this.wf.parseReviewResult
656
- ? this.wf.parseReviewResult(reviewResult.output, phase)
657
- : this._parseDefaultReviewResult(reviewResult.output);
658
-
659
- if (reviewResult.exitCode === 0) {
660
- success(`Reviewer finished in ${reviewResult.duration}s`);
661
- } else {
662
- warn(`Reviewer exited with code ${reviewResult.exitCode} after ${reviewResult.duration}s`);
663
- }
711
+ warn(` Build exited with code ${buildResult.exitCode} (${buildResult.duration}s)`);
664
712
  }
665
713
 
666
- // Write review report
667
- const reportDir = path.join(this.getReviewDir(projectDir), "auto-review");
668
- ensureDir(reportDir);
669
714
  fs.writeFileSync(
670
- path.join(reportDir, `${phase}-cycle-${autoReviewCycle}.json`),
671
- 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)}`
672
717
  );
673
718
 
674
- if (issues.length === 0) {
675
- autoReviewClean = true;
676
- success(`Automated review passed no issues found in ${phase}`);
677
- } else {
678
- warn(`Automated review found ${issues.length} issue(s) in ${phase}`);
679
- for (const issue of issues) {
680
- 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);
681
739
  }
682
740
 
683
- if (autoReviewCycle < maxAutoReviewCycles) {
684
- // Spawn fixer Claude with the issues
685
- const fixPrompt = this.wf.buildAutoFixPrompt
686
- ? this.wf.buildAutoFixPrompt(phase, issues, items, projectDir)
687
- : 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
+ }
688
749
 
689
- log(`\n${CYAN} ⚙${RESET} Spawning fixer Claude for ${issues.length} issue(s)...`);
690
- const fixResult = this.spawnClaude(projectDir, fixPrompt, opts.timeout || 600_000);
691
- if (fixResult.exitCode === 0) success(`Fixer finished in ${fixResult.duration}s`);
692
- 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
+ }
693
765
 
694
- // Re-measure after fixes
695
- if (!skipMeasure && this.wf.measure) {
696
- 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
+ }
697
845
  }
846
+ log(`\n Total review time: ${totalDuration}s for ${items.length} items`);
847
+
848
+ } else {
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}`);
698
883
  } else {
699
- warn(`Max auto-review cycles reached — ${issues.length} issue(s) will go to human review`);
700
- // Attach unresolved issues to measurements for human visibility
701
- const issueFile = path.join(this.getReviewDir(projectDir), "auto-review", `${phase}-unresolved.json`);
702
- fs.writeFileSync(issueFile, JSON.stringify(issues, null, 2));
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
+ }
703
907
  }
704
908
  }
705
909
  }
@@ -728,7 +932,7 @@ ${BOLD}Phases:${RESET} ${this.wf.phases.join(" → ")}
728
932
  ? this.wf.buildFixPrompt(phase, feedback.needsWork)
729
933
  : this._defaultFixPrompt(phase, feedback.needsWork);
730
934
  info(`Spawning Claude to apply ${feedback.needsWork.length} fixes...`);
731
- 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` });
732
936
  if (fixResult.exitCode === 0) success("Fixes applied");
733
937
  else warn(`Fix attempt returned code ${fixResult.exitCode}`);
734
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.21",
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",