@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 +22 -0
- package/bin/design-orchestrator.js +98 -15
- package/bin/gsd-t.js +1 -1
- package/bin/orchestrator.js +414 -108
- package/commands/gsd-t-design-build.md +3 -0
- package/package.json +1 -1
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
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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:
|
|
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": {
|
package/bin/orchestrator.js
CHANGED
|
@@ -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",
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
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
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
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
|
-
|
|
596
|
-
|
|
597
|
-
|
|
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
|
-
//
|
|
612
|
-
//
|
|
613
|
-
//
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
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
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
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
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
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(
|
|
671
|
-
|
|
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
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
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
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
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
|
-
|
|
690
|
-
|
|
691
|
-
|
|
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
|
-
|
|
695
|
-
|
|
696
|
-
|
|
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
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
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.
|
|
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",
|