@tekyzinc/gsd-t 2.71.20 → 2.72.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/bin/design-orchestrator.js +92 -12
- package/bin/orchestrator.js +329 -89
- package/commands/gsd-t-design-build.md +2 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to GSD-T are documented here. Updated with each release.
|
|
4
4
|
|
|
5
|
+
## [2.72.10] - 2026-04-08
|
|
6
|
+
|
|
7
|
+
### Added (orchestrator — per-item pipeline, stream-json, verbose, clean)
|
|
8
|
+
- **Per-item build+review pipeline** — when workflow provides `buildSingleItemPrompt` + `buildSingleItemReviewPrompt`, each component is built and reviewed individually (1 contract + 1 source per Claude spawn) instead of all-at-once. Fixes reviewer timeout caused by 30+ files in a single context. Each item gets up to 4 auto-review fix cycles independently.
|
|
9
|
+
- **`--output-format stream-json`** — Claude spawns now use streaming JSON output. On timeout, partial output is captured and parsed instead of returning empty string. Enables diagnosing what the reviewer was doing before being killed.
|
|
10
|
+
- **`--verbose` / `-v` flag** — streams Claude's stderr to terminal for real-time tool call visibility, saves prompts to `build-logs/` for post-mortem, logs completion stats after each spawn.
|
|
11
|
+
- **`--clean` flag** — deletes previous build output files before each phase's build step for fresh builds.
|
|
12
|
+
- **Version display** — orchestrator shows GSD-T version in startup header.
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
- **Reviewer timeout increased** — 300s → 600s for all-at-once review mode (per-item uses 120s per component).
|
|
16
|
+
- **Design orchestrator** — added `buildSingleItemPrompt` and `buildSingleItemReviewPrompt` for per-item pipeline support. Reviewer prompt restructured: code review first, Playwright spot-check second.
|
|
17
|
+
|
|
18
|
+
## [2.71.21] - 2026-04-08
|
|
19
|
+
|
|
20
|
+
### Fixed (orchestrator — timeout false-pass, review server health, stale cleanup)
|
|
21
|
+
- **Reviewer timeout/kill no longer treated as "pass"** — exit codes 143 (SIGTERM) and 137 (SIGKILL) are now always detected as failures regardless of duration. Previously, a reviewer that timed out at 300s was parsed as "0 issues = pass" because crash detection only checked duration < 10s.
|
|
22
|
+
- **Empty output with non-zero exit also caught** — any reviewer that exits non-zero with no output is treated as a failure, not a clean pass.
|
|
23
|
+
- **Review server health check during human review gate** — every ~30s the polling loop verifies port 3456 is alive. If the review server dies, it auto-restarts. Previously, a dead review server left the orchestrator stuck forever.
|
|
24
|
+
- **Stale auto-review cleanup** — old auto-review files from previous runs are cleared at the start of each phase's review cycle, preventing misleading results from prior orchestrator runs.
|
|
25
|
+
|
|
5
26
|
## [2.71.20] - 2026-04-08
|
|
6
27
|
|
|
7
28
|
### Fixed (orchestrator — reviewer crash false-pass + Ctrl+C + build logging)
|
|
@@ -134,6 +134,39 @@ ${importInstructions}
|
|
|
134
134
|
This is a FINITE task. Build the ${items.length} ${phase} listed above, then EXIT.`;
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
function buildSingleItemPrompt(phase, item, prevResults, projectDir) {
|
|
138
|
+
const singular = PHASE_SINGULAR[phase];
|
|
139
|
+
const sourcePath = item.sourcePath || guessPaths(phase, item);
|
|
140
|
+
|
|
141
|
+
const prevPaths = [];
|
|
142
|
+
for (const [, result] of Object.entries(prevResults)) {
|
|
143
|
+
if (result.builtPaths) prevPaths.push(...result.builtPaths);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const importInstructions = prevPaths.length > 0
|
|
147
|
+
? `\n## Imports from Previous Tier\nImport these already-built components — do NOT rebuild their functionality inline:\n${prevPaths.map(p => `- ${p}`).join("\n")}\n`
|
|
148
|
+
: "";
|
|
149
|
+
|
|
150
|
+
return `You are building ONE ${singular} component for a Vue 3 + TypeScript project.
|
|
151
|
+
|
|
152
|
+
## Task
|
|
153
|
+
Build this component from its design contract. Read the contract for exact visual specs — do NOT approximate values.
|
|
154
|
+
|
|
155
|
+
## Component
|
|
156
|
+
- **${item.componentName}**
|
|
157
|
+
- Contract: ${item.fullContractPath}
|
|
158
|
+
- Write to: ${sourcePath}
|
|
159
|
+
${importInstructions}
|
|
160
|
+
## Rules
|
|
161
|
+
- Read the contract file for exact property values
|
|
162
|
+
- Write the component to the specified source path
|
|
163
|
+
- Follow the project's existing code conventions (check existing files in src/)
|
|
164
|
+
- Use the project's existing dependencies (check package.json)
|
|
165
|
+
- When the component is complete, STOP. Do not start a dev server or build other components.
|
|
166
|
+
|
|
167
|
+
This is a FINITE task. Build this ONE ${singular}, then EXIT.`;
|
|
168
|
+
}
|
|
169
|
+
|
|
137
170
|
// ─── Measurement ────────────────────────────────────────────────────────────
|
|
138
171
|
|
|
139
172
|
function hasPlaywright(projectDir) {
|
|
@@ -332,16 +365,16 @@ ${componentList}
|
|
|
332
365
|
${measurementContext}
|
|
333
366
|
## Review Process
|
|
334
367
|
|
|
335
|
-
|
|
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);
|
|
@@ -439,13 +516,16 @@ const designBuildWorkflow = {
|
|
|
439
516
|
devServerTimeout: 30_000,
|
|
440
517
|
maxReviewCycles: 3,
|
|
441
518
|
maxAutoReviewCycles: 4,
|
|
442
|
-
reviewTimeout:
|
|
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,
|
package/bin/orchestrator.js
CHANGED
|
@@ -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",
|
|
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
|
-
|
|
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
|
-
|
|
222
|
+
// On timeout/error, still parse any partial stream-json output we got
|
|
223
|
+
const rawOut = (e.stdout || "") + (e.stderr || "");
|
|
224
|
+
output = this._parseStreamJson(rawOut, verbose);
|
|
203
225
|
exitCode = e.status || 1;
|
|
204
226
|
if (e.killed) warn(`Claude timed out after ${(timeout || 600_000) / 1000}s`);
|
|
205
227
|
}
|
|
206
228
|
|
|
207
229
|
const duration = Math.round((Date.now() - start) / 1000);
|
|
230
|
+
|
|
231
|
+
if (verbose) {
|
|
232
|
+
dim(`Claude finished: exit=${exitCode}, duration=${duration}s, output=${output.length} chars`);
|
|
233
|
+
}
|
|
234
|
+
|
|
208
235
|
return { output, exitCode, duration };
|
|
209
236
|
}
|
|
210
237
|
|
|
211
238
|
// ─── Server Management ───────────────────────────────────────────────
|
|
212
239
|
|
|
240
|
+
_parseStreamJson(raw, verbose) {
|
|
241
|
+
// stream-json format: one JSON object per line
|
|
242
|
+
// We want assistant text content and tool use visibility
|
|
243
|
+
const textParts = [];
|
|
244
|
+
const toolCalls = [];
|
|
245
|
+
|
|
246
|
+
for (const line of raw.split("\n")) {
|
|
247
|
+
if (!line.trim()) continue;
|
|
248
|
+
try {
|
|
249
|
+
const event = JSON.parse(line);
|
|
250
|
+
// Assistant text messages
|
|
251
|
+
if (event.type === "assistant" && event.message?.content) {
|
|
252
|
+
for (const block of event.message.content) {
|
|
253
|
+
if (block.type === "text") textParts.push(block.text);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
// Content block deltas (partial streaming)
|
|
257
|
+
if (event.type === "content_block_delta" && event.delta?.text) {
|
|
258
|
+
textParts.push(event.delta.text);
|
|
259
|
+
}
|
|
260
|
+
// Result message
|
|
261
|
+
if (event.type === "result" && event.result) {
|
|
262
|
+
// result contains the final response text
|
|
263
|
+
if (typeof event.result === "string") textParts.push(event.result);
|
|
264
|
+
}
|
|
265
|
+
// Tool use tracking for verbose
|
|
266
|
+
if (verbose && event.type === "assistant" && event.message?.content) {
|
|
267
|
+
for (const block of event.message.content) {
|
|
268
|
+
if (block.type === "tool_use") {
|
|
269
|
+
toolCalls.push(block.name);
|
|
270
|
+
dim(` → ${block.name}${block.input?.command ? ": " + String(block.input.command).slice(0, 80) : ""}`);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
} catch { /* skip non-JSON lines */ }
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (verbose && toolCalls.length > 0) {
|
|
278
|
+
dim(` Tool calls: ${toolCalls.length} (${[...new Set(toolCalls)].join(", ")})`);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return textParts.join("");
|
|
282
|
+
}
|
|
283
|
+
|
|
213
284
|
startDevServer(projectDir, port) {
|
|
214
285
|
if (isPortInUse(port)) {
|
|
215
286
|
success(`Dev server already running on port ${port}`);
|
|
@@ -368,6 +439,7 @@ ${BOLD}Phases:${RESET} ${this.wf.phases.join(" → ")}
|
|
|
368
439
|
openBrowser(`http://localhost:${reviewPort}/review`);
|
|
369
440
|
|
|
370
441
|
// IRONCLAD GATE — JavaScript polling loop
|
|
442
|
+
let healthCheckCounter = 0;
|
|
371
443
|
while (true) {
|
|
372
444
|
if (fs.existsSync(signalPath)) {
|
|
373
445
|
try {
|
|
@@ -376,6 +448,22 @@ ${BOLD}Phases:${RESET} ${this.wf.phases.join(" → ")}
|
|
|
376
448
|
return data;
|
|
377
449
|
} catch { /* malformed — wait for rewrite */ }
|
|
378
450
|
}
|
|
451
|
+
|
|
452
|
+
// Every 10 polls (~30s), verify review server is still alive
|
|
453
|
+
healthCheckCounter++;
|
|
454
|
+
if (healthCheckCounter % 10 === 0) {
|
|
455
|
+
if (!isPortInUse(reviewPort)) {
|
|
456
|
+
warn("Review server is down! Restarting...");
|
|
457
|
+
const devPort = this._activeDevPort || reviewPort - 1283; // fallback heuristic
|
|
458
|
+
const info = this.startReviewServer(projectDir, devPort, reviewPort);
|
|
459
|
+
if (info.pid || info.alreadyRunning) {
|
|
460
|
+
success("Review server restarted");
|
|
461
|
+
} else {
|
|
462
|
+
error("Failed to restart review server — please restart the orchestrator");
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
379
467
|
syncSleep(3000);
|
|
380
468
|
}
|
|
381
469
|
}
|
|
@@ -474,11 +562,17 @@ ${BOLD}Phases:${RESET} ${this.wf.phases.join(" → ")}
|
|
|
474
562
|
const { projectDir, resume, startPhase, devPort, reviewPort, skipMeasure } = opts;
|
|
475
563
|
const phases = this.wf.phases;
|
|
476
564
|
const maxReviewCycles = this.wf.defaults?.maxReviewCycles || 3;
|
|
565
|
+
this._verbose = opts.verbose || false;
|
|
477
566
|
|
|
567
|
+
const pkgVersion = (() => {
|
|
568
|
+
try { return require(path.join(__dirname, "..", "package.json")).version; } catch { return "unknown"; }
|
|
569
|
+
})();
|
|
478
570
|
heading(`GSD-T ${this.wf.name} Orchestrator`);
|
|
571
|
+
log(` Version: ${BOLD}v${pkgVersion}${RESET}`);
|
|
479
572
|
log(` Project: ${projectDir}`);
|
|
480
573
|
log(` Ports: dev=${devPort} review=${reviewPort}`);
|
|
481
574
|
log(` Phases: ${phases.join(" → ")}`);
|
|
575
|
+
if (this._verbose) log(` ${YELLOW}Verbose mode: ON${RESET}`);
|
|
482
576
|
log("");
|
|
483
577
|
|
|
484
578
|
// 1. Verify prerequisites
|
|
@@ -514,6 +608,7 @@ ${BOLD}Phases:${RESET} ${this.wf.phases.join(" → ")}
|
|
|
514
608
|
const devInfo = this.startDevServer(projectDir, devPort);
|
|
515
609
|
const reviewInfo = this.startReviewServer(projectDir, devPort, reviewPort);
|
|
516
610
|
this.pids = [devInfo.pid, reviewInfo.pid].filter(Boolean);
|
|
611
|
+
this._activeDevPort = devPort;
|
|
517
612
|
}
|
|
518
613
|
|
|
519
614
|
// Register cleanup on exit
|
|
@@ -559,111 +654,256 @@ ${BOLD}Phases:${RESET} ${this.wf.phases.join(" → ")}
|
|
|
559
654
|
}
|
|
560
655
|
}
|
|
561
656
|
|
|
562
|
-
//
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
657
|
+
// Clean previous build output if --clean
|
|
658
|
+
if (opts.clean && this.wf.guessPaths) {
|
|
659
|
+
const cleaned = [];
|
|
660
|
+
for (const item of items) {
|
|
661
|
+
const guessed = this.wf.guessPaths(phase, item);
|
|
662
|
+
const paths = Array.isArray(guessed) ? guessed : [guessed];
|
|
663
|
+
for (const p of paths) {
|
|
664
|
+
const full = path.join(projectDir, p);
|
|
665
|
+
if (fs.existsSync(full)) {
|
|
666
|
+
fs.unlinkSync(full);
|
|
667
|
+
cleaned.push(p);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
if (cleaned.length > 0) {
|
|
672
|
+
info(`--clean: removed ${cleaned.length} existing file(s) for ${phase}`);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
566
675
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
676
|
+
// Clear stale auto-review files from previous runs
|
|
677
|
+
const arDir = path.join(this.getReviewDir(projectDir), "auto-review");
|
|
678
|
+
if (fs.existsSync(arDir)) {
|
|
679
|
+
for (const f of fs.readdirSync(arDir)) {
|
|
680
|
+
if (f.startsWith(`${phase}-`)) {
|
|
681
|
+
try { fs.unlinkSync(path.join(arDir, f)); } catch { /* ignore */ }
|
|
682
|
+
}
|
|
683
|
+
}
|
|
572
684
|
}
|
|
573
685
|
|
|
574
|
-
// Log build output for debugging
|
|
575
686
|
const buildLogDir = path.join(this.getReviewDir(projectDir), "build-logs");
|
|
576
687
|
ensureDir(buildLogDir);
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
688
|
+
const maxAutoReviewCycles = this.wf.defaults?.maxAutoReviewCycles || 4;
|
|
689
|
+
const perItemTimeout = this.wf.defaults?.perItemTimeout || 120_000;
|
|
690
|
+
let builtPaths = [];
|
|
691
|
+
let measurements = {};
|
|
581
692
|
|
|
582
|
-
//
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
)
|
|
693
|
+
// ── Per-item pipeline: build ONE → review ONE → fix if needed ──
|
|
694
|
+
// Preferred when workflow provides single-item prompts.
|
|
695
|
+
// Each Claude spawn reads 1 contract + 1 source = tiny context, fast completion.
|
|
696
|
+
if (this.wf.buildSingleItemPrompt && this.wf.buildSingleItemReviewPrompt) {
|
|
697
|
+
log(`\n${CYAN} ⚙${RESET} Building and reviewing ${items.length} ${phase} one at a time...`);
|
|
586
698
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
measurements = this.wf.measure(projectDir, phase, items, { devPort, reviewPort }) || {};
|
|
591
|
-
}
|
|
699
|
+
for (let idx = 0; idx < items.length; idx++) {
|
|
700
|
+
const item = items[idx];
|
|
701
|
+
heading(` [${idx + 1}/${items.length}] ${item.componentName}`);
|
|
592
702
|
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
if (this.wf.buildReviewPrompt) {
|
|
598
|
-
let autoReviewCycle = 0;
|
|
599
|
-
let autoReviewClean = false;
|
|
600
|
-
|
|
601
|
-
while (autoReviewCycle < maxAutoReviewCycles && !autoReviewClean) {
|
|
602
|
-
autoReviewCycle++;
|
|
603
|
-
heading(`Automated Review — ${phase} (cycle ${autoReviewCycle}/${maxAutoReviewCycles})`);
|
|
604
|
-
|
|
605
|
-
// Spawn reviewer Claude — independent, no builder context
|
|
606
|
-
const reviewPrompt = this.wf.buildReviewPrompt(phase, items, measurements, projectDir, { devPort, reviewPort });
|
|
607
|
-
log(`\n${CYAN} ⚙${RESET} Spawning reviewer Claude for ${phase}...`);
|
|
608
|
-
const reviewTimeout = this.wf.defaults?.reviewTimeout || 300_000;
|
|
609
|
-
const reviewResult = this.spawnClaude(projectDir, reviewPrompt, reviewTimeout);
|
|
610
|
-
|
|
611
|
-
// Parse reviewer output for issues
|
|
612
|
-
let issues;
|
|
613
|
-
|
|
614
|
-
// Treat reviewer crash (non-zero exit, very short duration) as a failed review, not a pass
|
|
615
|
-
if (reviewResult.exitCode !== 0 && reviewResult.duration < 10) {
|
|
616
|
-
warn(`Reviewer crashed (code ${reviewResult.exitCode}, ${reviewResult.duration}s) — treating as review failure, will retry`);
|
|
617
|
-
issues = [{ component: "ALL", severity: "critical", description: `Reviewer crashed with exit code ${reviewResult.exitCode} — review not performed` }];
|
|
618
|
-
} else {
|
|
619
|
-
issues = this.wf.parseReviewResult
|
|
620
|
-
? this.wf.parseReviewResult(reviewResult.output, phase)
|
|
621
|
-
: this._parseDefaultReviewResult(reviewResult.output);
|
|
703
|
+
// Build one item
|
|
704
|
+
const buildPrompt = this.wf.buildSingleItemPrompt(phase, item, prevResults, projectDir);
|
|
705
|
+
dim(` Building...`);
|
|
706
|
+
const buildResult = this.spawnClaude(projectDir, buildPrompt, perItemTimeout, { label: `${phase}-build-${item.id}` });
|
|
622
707
|
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
}
|
|
708
|
+
if (buildResult.exitCode === 0) {
|
|
709
|
+
success(` Built (${buildResult.duration}s)`);
|
|
710
|
+
} else {
|
|
711
|
+
warn(` Build exited with code ${buildResult.exitCode} (${buildResult.duration}s)`);
|
|
628
712
|
}
|
|
629
713
|
|
|
630
|
-
// Write review report
|
|
631
|
-
const reportDir = path.join(this.getReviewDir(projectDir), "auto-review");
|
|
632
|
-
ensureDir(reportDir);
|
|
633
714
|
fs.writeFileSync(
|
|
634
|
-
path.join(
|
|
635
|
-
|
|
715
|
+
path.join(buildLogDir, `${phase}-build-${item.id}.log`),
|
|
716
|
+
`Exit code: ${buildResult.exitCode}\nDuration: ${buildResult.duration}s\n\n--- OUTPUT ---\n${buildResult.output.slice(0, 5000)}`
|
|
636
717
|
);
|
|
637
718
|
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
719
|
+
// Review one item (up to maxAutoReviewCycles)
|
|
720
|
+
let itemClean = false;
|
|
721
|
+
for (let cycle = 1; cycle <= maxAutoReviewCycles && !itemClean; cycle++) {
|
|
722
|
+
dim(` Review cycle ${cycle}/${maxAutoReviewCycles}...`);
|
|
723
|
+
const reviewPrompt = this.wf.buildSingleItemReviewPrompt(phase, item, {}, projectDir, { devPort, reviewPort });
|
|
724
|
+
const reviewResult = this.spawnClaude(projectDir, reviewPrompt, perItemTimeout, { label: `${phase}-review-${item.id}-c${cycle}` });
|
|
725
|
+
|
|
726
|
+
const isCrash = reviewResult.exitCode !== 0 && reviewResult.duration < 10;
|
|
727
|
+
const isKilled = [143, 137].includes(reviewResult.exitCode);
|
|
728
|
+
const isEmptyFail = reviewResult.exitCode !== 0 && !reviewResult.output.trim();
|
|
729
|
+
|
|
730
|
+
let itemIssues = [];
|
|
731
|
+
if (isCrash || isKilled || isEmptyFail) {
|
|
732
|
+
const reason = isCrash ? "crashed" : isKilled ? "killed/timed out" : "failed with no output";
|
|
733
|
+
warn(` Reviewer ${reason} (${reviewResult.duration}s)`);
|
|
734
|
+
itemIssues = [{ component: item.componentName, severity: "critical", description: `Reviewer ${reason}` }];
|
|
735
|
+
} else {
|
|
736
|
+
itemIssues = this.wf.parseReviewResult
|
|
737
|
+
? this.wf.parseReviewResult(reviewResult.output, phase)
|
|
738
|
+
: this._parseDefaultReviewResult(reviewResult.output);
|
|
645
739
|
}
|
|
646
740
|
|
|
647
|
-
if (
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
741
|
+
if (itemIssues.length === 0) {
|
|
742
|
+
itemClean = true;
|
|
743
|
+
success(` Clean (${reviewResult.duration}s)`);
|
|
744
|
+
} else {
|
|
745
|
+
warn(` ${itemIssues.length} issue(s) found`);
|
|
746
|
+
for (const issue of itemIssues) {
|
|
747
|
+
dim(` ${issue.description || issue.reason || "issue"} [${issue.severity || "medium"}]`);
|
|
748
|
+
}
|
|
652
749
|
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
750
|
+
if (cycle < maxAutoReviewCycles) {
|
|
751
|
+
// Fix this one item
|
|
752
|
+
const fixPrompt = this.wf.buildAutoFixPrompt
|
|
753
|
+
? this.wf.buildAutoFixPrompt(phase, itemIssues, [item], projectDir)
|
|
754
|
+
: this._defaultAutoFixPrompt(phase, itemIssues);
|
|
755
|
+
dim(` Fixing...`);
|
|
756
|
+
const fixResult = this.spawnClaude(projectDir, fixPrompt, perItemTimeout, { label: `${phase}-fix-${item.id}-c${cycle}` });
|
|
757
|
+
if (fixResult.exitCode === 0) success(` Fixed (${fixResult.duration}s)`);
|
|
758
|
+
else warn(` Fix exited with code ${fixResult.exitCode}`);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
builtPaths.push(item.sourcePath || (this.wf.guessPaths ? this.wf.guessPaths(phase, item) : ""));
|
|
764
|
+
}
|
|
657
765
|
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
766
|
+
// Measure ALL at once (one Playwright run after all items built)
|
|
767
|
+
if (!skipMeasure && this.wf.measure) {
|
|
768
|
+
heading("Measuring all built components");
|
|
769
|
+
measurements = this.wf.measure(projectDir, phase, items, { devPort, reviewPort }) || {};
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
} else {
|
|
773
|
+
// ── Legacy pipeline: build ALL → measure ALL → review loop ──
|
|
774
|
+
const prompt = this.wf.buildPrompt(phase, items, prevResults, projectDir);
|
|
775
|
+
log(`\n${CYAN} ⚙${RESET} Spawning Claude to build ${items.length} ${phase}...`);
|
|
776
|
+
dim(`Timeout: ${(opts.timeout || 600_000) / 1000}s`);
|
|
777
|
+
|
|
778
|
+
const buildResult = this.spawnClaude(projectDir, prompt, opts.timeout, { label: `${phase}-build` });
|
|
779
|
+
if (buildResult.exitCode === 0) {
|
|
780
|
+
success(`Claude finished building ${phase} in ${buildResult.duration}s`);
|
|
781
|
+
} else {
|
|
782
|
+
warn(`Claude exited with code ${buildResult.exitCode} after ${buildResult.duration}s`);
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
fs.writeFileSync(
|
|
786
|
+
path.join(buildLogDir, `${phase}-build.log`),
|
|
787
|
+
`Exit code: ${buildResult.exitCode}\nDuration: ${buildResult.duration}s\nPrompt length: ${prompt.length}\n\n--- OUTPUT ---\n${buildResult.output.slice(0, 20000)}`
|
|
788
|
+
);
|
|
789
|
+
|
|
790
|
+
builtPaths = items.map(item =>
|
|
791
|
+
item.sourcePath || (this.wf.guessPaths ? this.wf.guessPaths(phase, item) : "")
|
|
792
|
+
);
|
|
793
|
+
|
|
794
|
+
// Measure
|
|
795
|
+
if (!skipMeasure && this.wf.measure) {
|
|
796
|
+
measurements = this.wf.measure(projectDir, phase, items, { devPort, reviewPort }) || {};
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// Auto-review loop (legacy all-at-once)
|
|
800
|
+
if (this.wf.buildReviewPrompt || this.wf.buildSingleItemReviewPrompt) {
|
|
801
|
+
let autoReviewCycle = 0;
|
|
802
|
+
let autoReviewClean = false;
|
|
803
|
+
|
|
804
|
+
while (autoReviewCycle < maxAutoReviewCycles && !autoReviewClean) {
|
|
805
|
+
autoReviewCycle++;
|
|
806
|
+
heading(`Automated Review — ${phase} (cycle ${autoReviewCycle}/${maxAutoReviewCycles})`);
|
|
807
|
+
|
|
808
|
+
const reviewTimeout = this.wf.defaults?.reviewTimeout || 300_000;
|
|
809
|
+
let issues = [];
|
|
810
|
+
|
|
811
|
+
if (this.wf.buildSingleItemReviewPrompt) {
|
|
812
|
+
// Per-item review even in legacy build mode
|
|
813
|
+
log(`\n${CYAN} ⚙${RESET} Reviewing ${items.length} ${phase} one at a time...`);
|
|
814
|
+
let totalDuration = 0;
|
|
815
|
+
|
|
816
|
+
for (let idx = 0; idx < items.length; idx++) {
|
|
817
|
+
const item = items[idx];
|
|
818
|
+
const itemMeasurements = { [item.id]: measurements[item.id] || [] };
|
|
819
|
+
const reviewPrompt = this.wf.buildSingleItemReviewPrompt(phase, item, itemMeasurements, projectDir, { devPort, reviewPort });
|
|
820
|
+
|
|
821
|
+
dim(` [${idx + 1}/${items.length}] ${item.componentName}...`);
|
|
822
|
+
const reviewResult = this.spawnClaude(projectDir, reviewPrompt, Math.min(reviewTimeout, perItemTimeout), { label: `${phase}-review-c${autoReviewCycle}-${item.id}` });
|
|
823
|
+
totalDuration += reviewResult.duration;
|
|
824
|
+
|
|
825
|
+
const isCrash = reviewResult.exitCode !== 0 && reviewResult.duration < 10;
|
|
826
|
+
const isKilled = [143, 137].includes(reviewResult.exitCode);
|
|
827
|
+
const isEmptyFail = reviewResult.exitCode !== 0 && !reviewResult.output.trim();
|
|
828
|
+
|
|
829
|
+
if (isCrash || isKilled || isEmptyFail) {
|
|
830
|
+
const reason = isCrash ? "crashed" : isKilled ? "killed/timed out" : "failed with no output";
|
|
831
|
+
warn(` ${item.componentName}: reviewer ${reason} (${reviewResult.duration}s)`);
|
|
832
|
+
issues.push({ component: item.componentName, severity: "critical", description: `Reviewer ${reason} — review not performed` });
|
|
833
|
+
} else {
|
|
834
|
+
const itemIssues = this.wf.parseReviewResult
|
|
835
|
+
? this.wf.parseReviewResult(reviewResult.output, phase)
|
|
836
|
+
: this._parseDefaultReviewResult(reviewResult.output);
|
|
837
|
+
|
|
838
|
+
if (itemIssues.length > 0) {
|
|
839
|
+
warn(` ${item.componentName}: ${itemIssues.length} issue(s) (${reviewResult.duration}s)`);
|
|
840
|
+
issues.push(...itemIssues);
|
|
841
|
+
} else {
|
|
842
|
+
success(` ${item.componentName}: clean (${reviewResult.duration}s)`);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
661
845
|
}
|
|
846
|
+
log(`\n Total review time: ${totalDuration}s for ${items.length} items`);
|
|
847
|
+
|
|
662
848
|
} else {
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
849
|
+
// All-at-once review
|
|
850
|
+
const reviewPrompt = this.wf.buildReviewPrompt(phase, items, measurements, projectDir, { devPort, reviewPort });
|
|
851
|
+
log(`\n${CYAN} ⚙${RESET} Spawning reviewer Claude for all ${phase}...`);
|
|
852
|
+
const reviewTimeout2 = this.wf.defaults?.reviewTimeout || 300_000;
|
|
853
|
+
const reviewResult = this.spawnClaude(projectDir, reviewPrompt, reviewTimeout2, { label: `${phase}-review-cycle${autoReviewCycle}` });
|
|
854
|
+
|
|
855
|
+
const isCrash = reviewResult.exitCode !== 0 && reviewResult.duration < 10;
|
|
856
|
+
const isKilled = [143, 137].includes(reviewResult.exitCode);
|
|
857
|
+
const isEmptyFail = reviewResult.exitCode !== 0 && !reviewResult.output.trim();
|
|
858
|
+
|
|
859
|
+
if (isCrash || isKilled || isEmptyFail) {
|
|
860
|
+
const reason = isCrash ? "crashed" : isKilled ? "killed/timed out" : "failed with no output";
|
|
861
|
+
warn(`Reviewer ${reason} (code ${reviewResult.exitCode}, ${reviewResult.duration}s)`);
|
|
862
|
+
issues = [{ component: "ALL", severity: "critical", description: `Reviewer ${reason} with exit code ${reviewResult.exitCode}` }];
|
|
863
|
+
} else {
|
|
864
|
+
issues = this.wf.parseReviewResult
|
|
865
|
+
? this.wf.parseReviewResult(reviewResult.output, phase)
|
|
866
|
+
: this._parseDefaultReviewResult(reviewResult.output);
|
|
867
|
+
if (reviewResult.exitCode === 0) success(`Reviewer finished in ${reviewResult.duration}s`);
|
|
868
|
+
else warn(`Reviewer exited with code ${reviewResult.exitCode} after ${reviewResult.duration}s`);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// Write review report
|
|
873
|
+
const reportDir = path.join(this.getReviewDir(projectDir), "auto-review");
|
|
874
|
+
ensureDir(reportDir);
|
|
875
|
+
fs.writeFileSync(
|
|
876
|
+
path.join(reportDir, `${phase}-cycle-${autoReviewCycle}.json`),
|
|
877
|
+
JSON.stringify({ cycle: autoReviewCycle, issues, itemCount: items.length }, null, 2)
|
|
878
|
+
);
|
|
879
|
+
|
|
880
|
+
if (issues.length === 0) {
|
|
881
|
+
autoReviewClean = true;
|
|
882
|
+
success(`Automated review passed — no issues found in ${phase}`);
|
|
883
|
+
} else {
|
|
884
|
+
warn(`Automated review found ${issues.length} issue(s) in ${phase}`);
|
|
885
|
+
for (const issue of issues) {
|
|
886
|
+
dim(`${issue.component || "?"}: ${issue.description || issue.reason || "issue"} [${issue.severity || "medium"}]`);
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
if (autoReviewCycle < maxAutoReviewCycles) {
|
|
890
|
+
const fixPrompt = this.wf.buildAutoFixPrompt
|
|
891
|
+
? this.wf.buildAutoFixPrompt(phase, issues, items, projectDir)
|
|
892
|
+
: this._defaultAutoFixPrompt(phase, issues);
|
|
893
|
+
|
|
894
|
+
log(`\n${CYAN} ⚙${RESET} Spawning fixer Claude for ${issues.length} issue(s)...`);
|
|
895
|
+
const fixResult = this.spawnClaude(projectDir, fixPrompt, opts.timeout || 600_000, { label: `${phase}-fix-cycle${autoReviewCycle}` });
|
|
896
|
+
if (fixResult.exitCode === 0) success(`Fixer finished in ${fixResult.duration}s`);
|
|
897
|
+
else warn(`Fixer exited with code ${fixResult.exitCode}`);
|
|
898
|
+
|
|
899
|
+
if (!skipMeasure && this.wf.measure) {
|
|
900
|
+
measurements = this.wf.measure(projectDir, phase, items, { devPort, reviewPort }) || {};
|
|
901
|
+
}
|
|
902
|
+
} else {
|
|
903
|
+
warn(`Max auto-review cycles reached — ${issues.length} issue(s) will go to human review`);
|
|
904
|
+
const issueFile = path.join(this.getReviewDir(projectDir), "auto-review", `${phase}-unresolved.json`);
|
|
905
|
+
fs.writeFileSync(issueFile, JSON.stringify(issues, null, 2));
|
|
906
|
+
}
|
|
667
907
|
}
|
|
668
908
|
}
|
|
669
909
|
}
|
|
@@ -692,7 +932,7 @@ ${BOLD}Phases:${RESET} ${this.wf.phases.join(" → ")}
|
|
|
692
932
|
? this.wf.buildFixPrompt(phase, feedback.needsWork)
|
|
693
933
|
: this._defaultFixPrompt(phase, feedback.needsWork);
|
|
694
934
|
info(`Spawning Claude to apply ${feedback.needsWork.length} fixes...`);
|
|
695
|
-
const fixResult = this.spawnClaude(projectDir, fixPrompt, opts.timeout || 600_000);
|
|
935
|
+
const fixResult = this.spawnClaude(projectDir, fixPrompt, opts.timeout || 600_000, { label: `${phase}-human-fix` });
|
|
696
936
|
if (fixResult.exitCode === 0) success("Fixes applied");
|
|
697
937
|
else warn(`Fix attempt returned code ${fixResult.exitCode}`);
|
|
698
938
|
} else {
|
|
@@ -33,6 +33,8 @@ Pass any of these as `$ARGUMENTS`:
|
|
|
33
33
|
| `--review-port <N>` | Review server port (default: 3456) |
|
|
34
34
|
| `--timeout <sec>` | Claude timeout per tier in seconds (default: 600) |
|
|
35
35
|
| `--skip-measure` | Skip Playwright measurement (human-review only) |
|
|
36
|
+
| `--clean` | Delete previous build output before building each phase |
|
|
37
|
+
| `--verbose`, `-v` | Show Claude's tool calls and prompts in terminal |
|
|
36
38
|
|
|
37
39
|
## Prerequisites
|
|
38
40
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tekyzinc/gsd-t",
|
|
3
|
-
"version": "2.
|
|
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",
|