@tekyzinc/gsd-t 2.71.21 → 2.73.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,28 @@
2
2
 
3
3
  All notable changes to GSD-T are documented here. Updated with each release.
4
4
 
5
+ ## [2.73.10] - 2026-04-08
6
+
7
+ ### Added (orchestrator — parallel execution)
8
+ - **`--parallel N` flag** — runs N build+review items concurrently via async `spawnClaudeAsync()` and `_runWithConcurrency()`. Default: 1 (sequential). Recommended: 3. Reduces 15-element pipeline from ~30min to ~10min at 3x parallelism.
9
+ - **`--clean` artifact cleanup expanded** — now clears `auto-review/`, `build-logs/`, `queue/`, `feedback/`, `review-complete.json`, `orchestrator-state.json` on fresh start (not just build output).
10
+
11
+ ### Changed
12
+ - **Orchestrator `run()` is now async** — callers (`bin/gsd-t.js`, `bin/design-orchestrator.js`) updated with `.catch()` for proper error handling.
13
+
14
+ ## [2.72.10] - 2026-04-08
15
+
16
+ ### Added (orchestrator — per-item pipeline, stream-json, verbose, clean)
17
+ - **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.
18
+ - **`--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.
19
+ - **`--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.
20
+ - **`--clean` flag** — deletes previous build output files before each phase's build step for fresh builds.
21
+ - **Version display** — orchestrator shows GSD-T version in startup header.
22
+
23
+ ### Changed
24
+ - **Reviewer timeout increased** — 300s → 600s for all-at-once review mode (per-item uses 120s per component).
25
+ - **Design orchestrator** — added `buildSingleItemPrompt` and `buildSingleItemReviewPrompt` for per-item pipeline support. Reviewer prompt restructured: code review first, Playwright spot-check second.
26
+
5
27
  ## [2.71.21] - 2026-04-08
6
28
 
7
29
  ### 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);
@@ -408,6 +485,9 @@ ${BOLD}Options:${RESET}
408
485
  --review-port <N> Review server port (default: 3456)
409
486
  --timeout <sec> Claude timeout per tier in seconds (default: 600)
410
487
  --skip-measure Skip Playwright measurement (human-review only)
488
+ --clean Clear all stale artifacts before starting
489
+ --verbose, -v Show Claude's tool calls and prompts in terminal
490
+ --parallel <N> Run N items concurrently (default: 1 = sequential)
411
491
  --help Show this help
412
492
 
413
493
  ${BOLD}Pipeline:${RESET}
@@ -439,13 +519,16 @@ const designBuildWorkflow = {
439
519
  devServerTimeout: 30_000,
440
520
  maxReviewCycles: 3,
441
521
  maxAutoReviewCycles: 4,
442
- reviewTimeout: 300_000,
522
+ reviewTimeout: 600_000,
523
+ perItemTimeout: 120_000,
443
524
  },
444
525
  completionMessage: "All done. Run your app to verify: npm run dev",
445
526
 
446
527
  discoverWork,
447
528
  buildPrompt,
448
529
  buildReviewPrompt,
530
+ buildSingleItemPrompt,
531
+ buildSingleItemReviewPrompt,
449
532
  buildAutoFixPrompt,
450
533
  measure,
451
534
  buildQueueItem,
@@ -462,12 +545,12 @@ const designBuildWorkflow = {
462
545
 
463
546
  // ─── Entry Point ────────────────────────────────────────────────────────────
464
547
 
465
- function run(args) {
466
- new Orchestrator(designBuildWorkflow).run(args || []);
548
+ async function run(args) {
549
+ await new Orchestrator(designBuildWorkflow).run(args || []);
467
550
  }
468
551
 
469
552
  if (require.main === module) {
470
- run(process.argv.slice(2));
553
+ run(process.argv.slice(2)).catch(e => { console.error(e); process.exit(1); });
471
554
  }
472
555
 
473
556
  module.exports = { run, workflow: designBuildWorkflow };
package/bin/gsd-t.js CHANGED
@@ -2699,7 +2699,7 @@ if (require.main === module) {
2699
2699
  break;
2700
2700
  case "design-build": {
2701
2701
  const orchestrator = require("./design-orchestrator.js");
2702
- orchestrator.run(args.slice(1));
2702
+ orchestrator.run(args.slice(1)).catch(e => { console.error(e); process.exit(1); });
2703
2703
  break;
2704
2704
  }
2705
2705
  case "scan": {
@@ -25,7 +25,7 @@
25
25
 
26
26
  const fs = require("fs");
27
27
  const path = require("path");
28
- const { execFileSync, spawn: cpSpawn } = require("child_process");
28
+ const { execFileSync, execFile, spawn: cpSpawn } = require("child_process");
29
29
 
30
30
  // ─── ANSI Colors ────────────────────────────────────────────────────────────
31
31
 
@@ -134,6 +134,9 @@ 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;
139
+ case "--parallel": opts.parallel = parseInt(argv[++i], 10) || 3; break;
137
140
  case "--help":
138
141
  case "-h":
139
142
  if (this.wf.showUsage) this.wf.showUsage();
@@ -160,6 +163,9 @@ ${BOLD}Options:${RESET}
160
163
  --review-port <N> Review server port (default: ${this.wf.defaults?.reviewPort || 3456})
161
164
  --timeout <sec> Claude timeout per phase in seconds (default: 600)
162
165
  --skip-measure Skip automated measurement (human-review only)
166
+ --clean Clear all artifacts from previous runs + delete build output
167
+ --parallel <N> Run N items concurrently (default: 1, recommended: 3)
168
+ --verbose, -v Show Claude's tool calls and prompts in terminal
163
169
  --help Show this help
164
170
 
165
171
  ${BOLD}Phases:${RESET} ${this.wf.phases.join(" → ")}
@@ -185,31 +191,163 @@ ${BOLD}Phases:${RESET} ${this.wf.phases.join(" → ")}
185
191
  const start = Date.now();
186
192
  let output = "";
187
193
  let exitCode = 0;
194
+ const verbose = this._verbose;
188
195
 
189
196
  // Build args: -p for print mode, --dangerously-skip-permissions so spawned
190
197
  // Claude can write files without interactive permission prompts
191
- const args = ["-p", "--dangerously-skip-permissions", prompt];
198
+ const args = ["-p", "--dangerously-skip-permissions", "--output-format", "stream-json"];
199
+ if (verbose) args.push("--verbose");
200
+ args.push(prompt);
201
+
202
+ // Log prompt to file for debugging
203
+ if (verbose) {
204
+ const logDir = path.join(this.getReviewDir(projectDir), "build-logs");
205
+ ensureDir(logDir);
206
+ const label = opts.label || "claude";
207
+ fs.writeFileSync(
208
+ path.join(logDir, `${label}-prompt.txt`),
209
+ `--- Prompt (${new Date().toISOString()}) ---\nTimeout: ${(timeout || 600_000) / 1000}s\nCWD: ${projectDir}\n\n${prompt}`
210
+ );
211
+ }
192
212
 
193
213
  try {
194
- output = execFileSync("claude", args, {
214
+ const raw = execFileSync("claude", args, {
195
215
  encoding: "utf8",
196
216
  timeout: timeout || this.wf.defaults?.timeout || 600_000,
197
217
  stdio: ["pipe", "pipe", "pipe"],
198
218
  cwd: projectDir,
199
219
  maxBuffer: 10 * 1024 * 1024,
200
220
  });
221
+ // Parse stream-json: each line is a JSON event, extract assistant text
222
+ output = this._parseStreamJson(raw, verbose);
201
223
  } catch (e) {
202
- output = (e.stdout || "") + (e.stderr || "");
224
+ // On timeout/error, still parse any partial stream-json output we got
225
+ const rawOut = (e.stdout || "") + (e.stderr || "");
226
+ output = this._parseStreamJson(rawOut, verbose);
203
227
  exitCode = e.status || 1;
204
228
  if (e.killed) warn(`Claude timed out after ${(timeout || 600_000) / 1000}s`);
205
229
  }
206
230
 
207
231
  const duration = Math.round((Date.now() - start) / 1000);
232
+
233
+ if (verbose) {
234
+ dim(`Claude finished: exit=${exitCode}, duration=${duration}s, output=${output.length} chars`);
235
+ }
236
+
208
237
  return { output, exitCode, duration };
209
238
  }
210
239
 
211
240
  // ─── Server Management ───────────────────────────────────────────────
212
241
 
242
+ /**
243
+ * Async Claude spawn — returns a Promise. Used for parallel execution.
244
+ */
245
+ spawnClaudeAsync(projectDir, prompt, timeout, opts = {}) {
246
+ const start = Date.now();
247
+ const verbose = this._verbose;
248
+
249
+ const args = ["-p", "--dangerously-skip-permissions", "--output-format", "stream-json"];
250
+ if (verbose) args.push("--verbose");
251
+ args.push(prompt);
252
+
253
+ if (verbose && opts.label) {
254
+ const logDir = path.join(this.getReviewDir(projectDir), "build-logs");
255
+ ensureDir(logDir);
256
+ fs.writeFileSync(
257
+ path.join(logDir, `${opts.label}-prompt.txt`),
258
+ `--- Prompt (${new Date().toISOString()}) ---\nTimeout: ${(timeout || 120_000) / 1000}s\nCWD: ${projectDir}\n\n${prompt}`
259
+ );
260
+ }
261
+
262
+ return new Promise((resolve) => {
263
+ const child = execFile("claude", args, {
264
+ encoding: "utf8",
265
+ timeout: timeout || 120_000,
266
+ cwd: projectDir,
267
+ maxBuffer: 10 * 1024 * 1024,
268
+ }, (err, stdout, stderr) => {
269
+ const raw = err ? ((err.stdout || "") + (err.stderr || "")) : (stdout || "");
270
+ const output = this._parseStreamJson(raw, false);
271
+ const exitCode = err ? (err.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER" ? 1 : (err.killed ? 143 : (err.code || 1))) : 0;
272
+ const duration = Math.round((Date.now() - start) / 1000);
273
+
274
+ if (verbose) {
275
+ dim(` ${opts.label || "claude"}: exit=${exitCode}, ${duration}s, ${output.length} chars`);
276
+ }
277
+
278
+ resolve({ output, exitCode, duration });
279
+ });
280
+ });
281
+ }
282
+
283
+ /**
284
+ * Run tasks with concurrency limit. Returns results in same order as tasks.
285
+ * @param {Array<Function>} taskFns — array of () => Promise<result>
286
+ * @param {number} concurrency — max concurrent tasks
287
+ */
288
+ async _runWithConcurrency(taskFns, concurrency) {
289
+ const results = new Array(taskFns.length).fill(null);
290
+ let nextIdx = 0;
291
+
292
+ async function runNext() {
293
+ while (nextIdx < taskFns.length) {
294
+ const idx = nextIdx++;
295
+ results[idx] = await taskFns[idx]();
296
+ }
297
+ }
298
+
299
+ const workers = [];
300
+ for (let i = 0; i < Math.min(concurrency, taskFns.length); i++) {
301
+ workers.push(runNext());
302
+ }
303
+ await Promise.all(workers);
304
+ return results;
305
+ }
306
+
307
+ _parseStreamJson(raw, verbose) {
308
+ // stream-json format: one JSON object per line
309
+ // We want assistant text content and tool use visibility
310
+ const textParts = [];
311
+ const toolCalls = [];
312
+
313
+ for (const line of raw.split("\n")) {
314
+ if (!line.trim()) continue;
315
+ try {
316
+ const event = JSON.parse(line);
317
+ // Assistant text messages
318
+ if (event.type === "assistant" && event.message?.content) {
319
+ for (const block of event.message.content) {
320
+ if (block.type === "text") textParts.push(block.text);
321
+ }
322
+ }
323
+ // Content block deltas (partial streaming)
324
+ if (event.type === "content_block_delta" && event.delta?.text) {
325
+ textParts.push(event.delta.text);
326
+ }
327
+ // Result message
328
+ if (event.type === "result" && event.result) {
329
+ // result contains the final response text
330
+ if (typeof event.result === "string") textParts.push(event.result);
331
+ }
332
+ // Tool use tracking for verbose
333
+ if (verbose && event.type === "assistant" && event.message?.content) {
334
+ for (const block of event.message.content) {
335
+ if (block.type === "tool_use") {
336
+ toolCalls.push(block.name);
337
+ dim(` → ${block.name}${block.input?.command ? ": " + String(block.input.command).slice(0, 80) : ""}`);
338
+ }
339
+ }
340
+ }
341
+ } catch { /* skip non-JSON lines */ }
342
+ }
343
+
344
+ if (verbose && toolCalls.length > 0) {
345
+ dim(` Tool calls: ${toolCalls.length} (${[...new Set(toolCalls)].join(", ")})`);
346
+ }
347
+
348
+ return textParts.join("");
349
+ }
350
+
213
351
  startDevServer(projectDir, port) {
214
352
  if (isPortInUse(port)) {
215
353
  success(`Dev server already running on port ${port}`);
@@ -483,7 +621,7 @@ ${BOLD}Phases:${RESET} ${this.wf.phases.join(" → ")}
483
621
 
484
622
  // ─── Main Pipeline ──────────────────────────────────────────────────
485
623
 
486
- run(argv) {
624
+ async run(argv) {
487
625
  const opts = this.wf.parseArgs
488
626
  ? this.wf.parseArgs(argv, this.parseBaseArgs.bind(this))
489
627
  : this.parseBaseArgs(argv || []);
@@ -491,11 +629,17 @@ ${BOLD}Phases:${RESET} ${this.wf.phases.join(" → ")}
491
629
  const { projectDir, resume, startPhase, devPort, reviewPort, skipMeasure } = opts;
492
630
  const phases = this.wf.phases;
493
631
  const maxReviewCycles = this.wf.defaults?.maxReviewCycles || 3;
632
+ this._verbose = opts.verbose || false;
494
633
 
634
+ const pkgVersion = (() => {
635
+ try { return require(path.join(__dirname, "..", "package.json")).version; } catch { return "unknown"; }
636
+ })();
495
637
  heading(`GSD-T ${this.wf.name} Orchestrator`);
638
+ log(` Version: ${BOLD}v${pkgVersion}${RESET}`);
496
639
  log(` Project: ${projectDir}`);
497
640
  log(` Ports: dev=${devPort} review=${reviewPort}`);
498
641
  log(` Phases: ${phases.join(" → ")}`);
642
+ if (this._verbose) log(` ${YELLOW}Verbose mode: ON${RESET}`);
499
643
  log("");
500
644
 
501
645
  // 1. Verify prerequisites
@@ -520,6 +664,29 @@ ${BOLD}Phases:${RESET} ${this.wf.phases.join(" → ")}
520
664
  }
521
665
  } else {
522
666
  state = this._createState();
667
+
668
+ // Clean all orchestrator artifacts on fresh start (not --resume)
669
+ if (opts.clean) {
670
+ const reviewDir = this.getReviewDir(projectDir);
671
+ const dirsToClean = ["auto-review", "build-logs", "queue", "feedback"];
672
+ let cleaned = 0;
673
+ for (const dir of dirsToClean) {
674
+ const full = path.join(reviewDir, dir);
675
+ if (fs.existsSync(full)) {
676
+ for (const f of fs.readdirSync(full)) {
677
+ try { fs.unlinkSync(path.join(full, f)); cleaned++; } catch { /* ignore */ }
678
+ }
679
+ }
680
+ }
681
+ // Remove signal and state files
682
+ for (const f of ["review-complete.json", "orchestrator-state.json", "shutdown.json"]) {
683
+ const full = path.join(reviewDir, f);
684
+ if (fs.existsSync(full)) {
685
+ try { fs.unlinkSync(full); cleaned++; } catch { /* ignore */ }
686
+ }
687
+ }
688
+ if (cleaned > 0) info(`--clean: removed ${cleaned} stale artifact(s)`);
689
+ }
523
690
  }
524
691
 
525
692
  // 4. Start servers
@@ -577,129 +744,268 @@ ${BOLD}Phases:${RESET} ${this.wf.phases.join(" → ")}
577
744
  }
578
745
  }
579
746
 
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`);
747
+ // Clean previous build output if --clean
748
+ if (opts.clean && this.wf.guessPaths) {
749
+ const cleaned = [];
750
+ for (const item of items) {
751
+ const guessed = this.wf.guessPaths(phase, item);
752
+ const paths = Array.isArray(guessed) ? guessed : [guessed];
753
+ for (const p of paths) {
754
+ const full = path.join(projectDir, p);
755
+ if (fs.existsSync(full)) {
756
+ fs.unlinkSync(full);
757
+ cleaned.push(p);
758
+ }
759
+ }
760
+ }
761
+ if (cleaned.length > 0) {
762
+ info(`--clean: removed ${cleaned.length} existing file(s) for ${phase}`);
763
+ }
764
+ }
584
765
 
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`);
766
+ // Clear stale auto-review files from previous runs
767
+ const arDir = path.join(this.getReviewDir(projectDir), "auto-review");
768
+ if (fs.existsSync(arDir)) {
769
+ for (const f of fs.readdirSync(arDir)) {
770
+ if (f.startsWith(`${phase}-`)) {
771
+ try { fs.unlinkSync(path.join(arDir, f)); } catch { /* ignore */ }
772
+ }
773
+ }
590
774
  }
591
775
 
592
- // Log build output for debugging
593
776
  const buildLogDir = path.join(this.getReviewDir(projectDir), "build-logs");
594
777
  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
- );
599
-
600
- // 6c. Collect built paths
601
- const builtPaths = items.map(item =>
602
- item.sourcePath || (this.wf.guessPaths ? this.wf.guessPaths(phase, item) : "")
603
- );
604
-
605
- // 6d. Measure
778
+ const maxAutoReviewCycles = this.wf.defaults?.maxAutoReviewCycles || 4;
779
+ const perItemTimeout = this.wf.defaults?.perItemTimeout || 120_000;
780
+ let builtPaths = [];
606
781
  let measurements = {};
607
- if (!skipMeasure && this.wf.measure) {
608
- measurements = this.wf.measure(projectDir, phase, items, { devPort, reviewPort }) || {};
609
- }
610
782
 
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
- }
783
+ // ── Per-item pipeline: build ONE → review ONE fix if needed ──
784
+ // Each item is independent: 1 contract + 1 source = tiny context.
785
+ // With --parallel N, runs N items concurrently.
786
+ if (this.wf.buildSingleItemPrompt && this.wf.buildSingleItemReviewPrompt) {
787
+ const concurrency = opts.parallel || 1;
788
+ if (concurrency > 1) {
789
+ log(`\n${CYAN} ⚙${RESET} Building and reviewing ${items.length} ${phase} (${concurrency} parallel)...`);
790
+ } else {
791
+ log(`\n${CYAN} ⚙${RESET} Building and reviewing ${items.length} ${phase} one at a time...`);
624
792
  }
625
793
 
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` }];
654
- } else {
655
- issues = this.wf.parseReviewResult
656
- ? this.wf.parseReviewResult(reviewResult.output, phase)
657
- : this._parseDefaultReviewResult(reviewResult.output);
794
+ // Each task: build → review → fix loop for one item
795
+ const processItem = async (item, idx) => {
796
+ const label = `[${idx + 1}/${items.length}] ${item.componentName}`;
797
+ log(`\n ${BOLD}${label}${RESET}`);
658
798
 
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
- }
799
+ // Build
800
+ const buildPrompt = this.wf.buildSingleItemPrompt(phase, item, prevResults, projectDir);
801
+ dim(` Building...`);
802
+ const buildResult = await this.spawnClaudeAsync(projectDir, buildPrompt, perItemTimeout, { label: `${phase}-build-${item.id}` });
803
+
804
+ if (buildResult.exitCode === 0) {
805
+ success(` ${item.componentName}: built (${buildResult.duration}s)`);
806
+ } else {
807
+ warn(` ${item.componentName}: build exit ${buildResult.exitCode} (${buildResult.duration}s)`);
664
808
  }
665
809
 
666
- // Write review report
667
- const reportDir = path.join(this.getReviewDir(projectDir), "auto-review");
668
- ensureDir(reportDir);
669
810
  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)
811
+ path.join(buildLogDir, `${phase}-build-${item.id}.log`),
812
+ `Exit code: ${buildResult.exitCode}\nDuration: ${buildResult.duration}s\n\n--- OUTPUT ---\n${buildResult.output.slice(0, 5000)}`
672
813
  );
673
814
 
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"}]`);
815
+ // Review cycles
816
+ let itemClean = false;
817
+ for (let cycle = 1; cycle <= maxAutoReviewCycles && !itemClean; cycle++) {
818
+ dim(` ${item.componentName}: review c${cycle}...`);
819
+ const reviewPrompt = this.wf.buildSingleItemReviewPrompt(phase, item, {}, projectDir, { devPort, reviewPort });
820
+ const reviewResult = await this.spawnClaudeAsync(projectDir, reviewPrompt, perItemTimeout, { label: `${phase}-review-${item.id}-c${cycle}` });
821
+
822
+ const isCrash = reviewResult.exitCode !== 0 && reviewResult.duration < 10;
823
+ const isKilled = [143, 137].includes(reviewResult.exitCode);
824
+ const isEmptyFail = reviewResult.exitCode !== 0 && !reviewResult.output.trim();
825
+
826
+ let itemIssues = [];
827
+ if (isCrash || isKilled || isEmptyFail) {
828
+ const reason = isCrash ? "crashed" : isKilled ? "killed/timed out" : "failed with no output";
829
+ warn(` ${item.componentName}: reviewer ${reason} (${reviewResult.duration}s)`);
830
+ itemIssues = [{ component: item.componentName, severity: "critical", description: `Reviewer ${reason}` }];
831
+ } else {
832
+ itemIssues = this.wf.parseReviewResult
833
+ ? this.wf.parseReviewResult(reviewResult.output, phase)
834
+ : this._parseDefaultReviewResult(reviewResult.output);
835
+ }
836
+
837
+ if (itemIssues.length === 0) {
838
+ itemClean = true;
839
+ success(` ${item.componentName}: clean (${reviewResult.duration}s)`);
840
+ } else {
841
+ warn(` ${item.componentName}: ${itemIssues.length} issue(s)`);
842
+ for (const issue of itemIssues) {
843
+ dim(` ${issue.description || "issue"} [${issue.severity || "medium"}]`);
844
+ }
845
+
846
+ if (cycle < maxAutoReviewCycles) {
847
+ const fixPrompt = this.wf.buildAutoFixPrompt
848
+ ? this.wf.buildAutoFixPrompt(phase, itemIssues, [item], projectDir)
849
+ : this._defaultAutoFixPrompt(phase, itemIssues);
850
+ dim(` ${item.componentName}: fixing...`);
851
+ const fixResult = await this.spawnClaudeAsync(projectDir, fixPrompt, perItemTimeout, { label: `${phase}-fix-${item.id}-c${cycle}` });
852
+ if (fixResult.exitCode === 0) success(` ${item.componentName}: fixed (${fixResult.duration}s)`);
853
+ else warn(` ${item.componentName}: fix exit ${fixResult.exitCode}`);
854
+ }
681
855
  }
856
+ }
857
+
858
+ return item.sourcePath || (this.wf.guessPaths ? this.wf.guessPaths(phase, item) : "");
859
+ };
860
+
861
+ // Run with concurrency
862
+ const taskFns = items.map((item, idx) => () => processItem(item, idx));
863
+ const startTime = Date.now();
864
+ builtPaths = await this._runWithConcurrency(taskFns, concurrency);
865
+ const totalSec = Math.round((Date.now() - startTime) / 1000);
866
+ success(`All ${items.length} ${phase} processed in ${totalSec}s (${concurrency}x parallel)`);
867
+
868
+ // Measure ALL at once (one Playwright run after all items built)
869
+ if (!skipMeasure && this.wf.measure) {
870
+ heading("Measuring all built components");
871
+ measurements = this.wf.measure(projectDir, phase, items, { devPort, reviewPort }) || {};
872
+ }
873
+
874
+ } else {
875
+ // ── Legacy pipeline: build ALL → measure ALL → review loop ──
876
+ const prompt = this.wf.buildPrompt(phase, items, prevResults, projectDir);
877
+ log(`\n${CYAN} ⚙${RESET} Spawning Claude to build ${items.length} ${phase}...`);
878
+ dim(`Timeout: ${(opts.timeout || 600_000) / 1000}s`);
879
+
880
+ const buildResult = this.spawnClaude(projectDir, prompt, opts.timeout, { label: `${phase}-build` });
881
+ if (buildResult.exitCode === 0) {
882
+ success(`Claude finished building ${phase} in ${buildResult.duration}s`);
883
+ } else {
884
+ warn(`Claude exited with code ${buildResult.exitCode} after ${buildResult.duration}s`);
885
+ }
682
886
 
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);
887
+ fs.writeFileSync(
888
+ path.join(buildLogDir, `${phase}-build.log`),
889
+ `Exit code: ${buildResult.exitCode}\nDuration: ${buildResult.duration}s\nPrompt length: ${prompt.length}\n\n--- OUTPUT ---\n${buildResult.output.slice(0, 20000)}`
890
+ );
688
891
 
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}`);
892
+ builtPaths = items.map(item =>
893
+ item.sourcePath || (this.wf.guessPaths ? this.wf.guessPaths(phase, item) : "")
894
+ );
693
895
 
694
- // Re-measure after fixes
695
- if (!skipMeasure && this.wf.measure) {
696
- measurements = this.wf.measure(projectDir, phase, items, { devPort, reviewPort }) || {};
896
+ // Measure
897
+ if (!skipMeasure && this.wf.measure) {
898
+ measurements = this.wf.measure(projectDir, phase, items, { devPort, reviewPort }) || {};
899
+ }
900
+
901
+ // Auto-review loop (legacy all-at-once)
902
+ if (this.wf.buildReviewPrompt || this.wf.buildSingleItemReviewPrompt) {
903
+ let autoReviewCycle = 0;
904
+ let autoReviewClean = false;
905
+
906
+ while (autoReviewCycle < maxAutoReviewCycles && !autoReviewClean) {
907
+ autoReviewCycle++;
908
+ heading(`Automated Review — ${phase} (cycle ${autoReviewCycle}/${maxAutoReviewCycles})`);
909
+
910
+ const reviewTimeout = this.wf.defaults?.reviewTimeout || 300_000;
911
+ let issues = [];
912
+
913
+ if (this.wf.buildSingleItemReviewPrompt) {
914
+ // Per-item review even in legacy build mode
915
+ log(`\n${CYAN} ⚙${RESET} Reviewing ${items.length} ${phase} one at a time...`);
916
+ let totalDuration = 0;
917
+
918
+ for (let idx = 0; idx < items.length; idx++) {
919
+ const item = items[idx];
920
+ const itemMeasurements = { [item.id]: measurements[item.id] || [] };
921
+ const reviewPrompt = this.wf.buildSingleItemReviewPrompt(phase, item, itemMeasurements, projectDir, { devPort, reviewPort });
922
+
923
+ dim(` [${idx + 1}/${items.length}] ${item.componentName}...`);
924
+ const reviewResult = this.spawnClaude(projectDir, reviewPrompt, Math.min(reviewTimeout, perItemTimeout), { label: `${phase}-review-c${autoReviewCycle}-${item.id}` });
925
+ totalDuration += reviewResult.duration;
926
+
927
+ const isCrash = reviewResult.exitCode !== 0 && reviewResult.duration < 10;
928
+ const isKilled = [143, 137].includes(reviewResult.exitCode);
929
+ const isEmptyFail = reviewResult.exitCode !== 0 && !reviewResult.output.trim();
930
+
931
+ if (isCrash || isKilled || isEmptyFail) {
932
+ const reason = isCrash ? "crashed" : isKilled ? "killed/timed out" : "failed with no output";
933
+ warn(` ${item.componentName}: reviewer ${reason} (${reviewResult.duration}s)`);
934
+ issues.push({ component: item.componentName, severity: "critical", description: `Reviewer ${reason} — review not performed` });
935
+ } else {
936
+ const itemIssues = this.wf.parseReviewResult
937
+ ? this.wf.parseReviewResult(reviewResult.output, phase)
938
+ : this._parseDefaultReviewResult(reviewResult.output);
939
+
940
+ if (itemIssues.length > 0) {
941
+ warn(` ${item.componentName}: ${itemIssues.length} issue(s) (${reviewResult.duration}s)`);
942
+ issues.push(...itemIssues);
943
+ } else {
944
+ success(` ${item.componentName}: clean (${reviewResult.duration}s)`);
945
+ }
946
+ }
697
947
  }
948
+ log(`\n Total review time: ${totalDuration}s for ${items.length} items`);
949
+
698
950
  } 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));
951
+ // All-at-once review
952
+ const reviewPrompt = this.wf.buildReviewPrompt(phase, items, measurements, projectDir, { devPort, reviewPort });
953
+ log(`\n${CYAN} ⚙${RESET} Spawning reviewer Claude for all ${phase}...`);
954
+ const reviewTimeout2 = this.wf.defaults?.reviewTimeout || 300_000;
955
+ const reviewResult = this.spawnClaude(projectDir, reviewPrompt, reviewTimeout2, { label: `${phase}-review-cycle${autoReviewCycle}` });
956
+
957
+ const isCrash = reviewResult.exitCode !== 0 && reviewResult.duration < 10;
958
+ const isKilled = [143, 137].includes(reviewResult.exitCode);
959
+ const isEmptyFail = reviewResult.exitCode !== 0 && !reviewResult.output.trim();
960
+
961
+ if (isCrash || isKilled || isEmptyFail) {
962
+ const reason = isCrash ? "crashed" : isKilled ? "killed/timed out" : "failed with no output";
963
+ warn(`Reviewer ${reason} (code ${reviewResult.exitCode}, ${reviewResult.duration}s)`);
964
+ issues = [{ component: "ALL", severity: "critical", description: `Reviewer ${reason} with exit code ${reviewResult.exitCode}` }];
965
+ } else {
966
+ issues = this.wf.parseReviewResult
967
+ ? this.wf.parseReviewResult(reviewResult.output, phase)
968
+ : this._parseDefaultReviewResult(reviewResult.output);
969
+ if (reviewResult.exitCode === 0) success(`Reviewer finished in ${reviewResult.duration}s`);
970
+ else warn(`Reviewer exited with code ${reviewResult.exitCode} after ${reviewResult.duration}s`);
971
+ }
972
+ }
973
+
974
+ // Write review report
975
+ const reportDir = path.join(this.getReviewDir(projectDir), "auto-review");
976
+ ensureDir(reportDir);
977
+ fs.writeFileSync(
978
+ path.join(reportDir, `${phase}-cycle-${autoReviewCycle}.json`),
979
+ JSON.stringify({ cycle: autoReviewCycle, issues, itemCount: items.length }, null, 2)
980
+ );
981
+
982
+ if (issues.length === 0) {
983
+ autoReviewClean = true;
984
+ success(`Automated review passed — no issues found in ${phase}`);
985
+ } else {
986
+ warn(`Automated review found ${issues.length} issue(s) in ${phase}`);
987
+ for (const issue of issues) {
988
+ dim(`${issue.component || "?"}: ${issue.description || issue.reason || "issue"} [${issue.severity || "medium"}]`);
989
+ }
990
+
991
+ if (autoReviewCycle < maxAutoReviewCycles) {
992
+ const fixPrompt = this.wf.buildAutoFixPrompt
993
+ ? this.wf.buildAutoFixPrompt(phase, issues, items, projectDir)
994
+ : this._defaultAutoFixPrompt(phase, issues);
995
+
996
+ log(`\n${CYAN} ⚙${RESET} Spawning fixer Claude for ${issues.length} issue(s)...`);
997
+ const fixResult = this.spawnClaude(projectDir, fixPrompt, opts.timeout || 600_000, { label: `${phase}-fix-cycle${autoReviewCycle}` });
998
+ if (fixResult.exitCode === 0) success(`Fixer finished in ${fixResult.duration}s`);
999
+ else warn(`Fixer exited with code ${fixResult.exitCode}`);
1000
+
1001
+ if (!skipMeasure && this.wf.measure) {
1002
+ measurements = this.wf.measure(projectDir, phase, items, { devPort, reviewPort }) || {};
1003
+ }
1004
+ } else {
1005
+ warn(`Max auto-review cycles reached — ${issues.length} issue(s) will go to human review`);
1006
+ const issueFile = path.join(this.getReviewDir(projectDir), "auto-review", `${phase}-unresolved.json`);
1007
+ fs.writeFileSync(issueFile, JSON.stringify(issues, null, 2));
1008
+ }
703
1009
  }
704
1010
  }
705
1011
  }
@@ -728,7 +1034,7 @@ ${BOLD}Phases:${RESET} ${this.wf.phases.join(" → ")}
728
1034
  ? this.wf.buildFixPrompt(phase, feedback.needsWork)
729
1035
  : this._defaultFixPrompt(phase, feedback.needsWork);
730
1036
  info(`Spawning Claude to apply ${feedback.needsWork.length} fixes...`);
731
- const fixResult = this.spawnClaude(projectDir, fixPrompt, opts.timeout || 600_000);
1037
+ const fixResult = this.spawnClaude(projectDir, fixPrompt, opts.timeout || 600_000, { label: `${phase}-human-fix` });
732
1038
  if (fixResult.exitCode === 0) success("Fixes applied");
733
1039
  else warn(`Fix attempt returned code ${fixResult.exitCode}`);
734
1040
  } else {
@@ -33,6 +33,9 @@ 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` | Clear all stale artifacts + delete build output before each phase |
37
+ | `--parallel <N>` | Run N items concurrently (default: 1, recommended: 3) |
38
+ | `--verbose`, `-v` | Show Claude's tool calls and prompts in terminal |
36
39
 
37
40
  ## Prerequisites
38
41
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tekyzinc/gsd-t",
3
- "version": "2.71.21",
3
+ "version": "2.73.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",