@tekyzinc/gsd-t 2.72.10 → 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 +9 -0
- package/bin/design-orchestrator.js +6 -3
- package/bin/gsd-t.js +1 -1
- package/bin/orchestrator.js +129 -27
- package/commands/gsd-t-design-build.md +2 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
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
|
+
|
|
5
14
|
## [2.72.10] - 2026-04-08
|
|
6
15
|
|
|
7
16
|
### Added (orchestrator — per-item pipeline, stream-json, verbose, clean)
|
|
@@ -485,6 +485,9 @@ ${BOLD}Options:${RESET}
|
|
|
485
485
|
--review-port <N> Review server port (default: 3456)
|
|
486
486
|
--timeout <sec> Claude timeout per tier in seconds (default: 600)
|
|
487
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)
|
|
488
491
|
--help Show this help
|
|
489
492
|
|
|
490
493
|
${BOLD}Pipeline:${RESET}
|
|
@@ -542,12 +545,12 @@ const designBuildWorkflow = {
|
|
|
542
545
|
|
|
543
546
|
// ─── Entry Point ────────────────────────────────────────────────────────────
|
|
544
547
|
|
|
545
|
-
function run(args) {
|
|
546
|
-
new Orchestrator(designBuildWorkflow).run(args || []);
|
|
548
|
+
async function run(args) {
|
|
549
|
+
await new Orchestrator(designBuildWorkflow).run(args || []);
|
|
547
550
|
}
|
|
548
551
|
|
|
549
552
|
if (require.main === module) {
|
|
550
|
-
run(process.argv.slice(2));
|
|
553
|
+
run(process.argv.slice(2)).catch(e => { console.error(e); process.exit(1); });
|
|
551
554
|
}
|
|
552
555
|
|
|
553
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
|
|
|
@@ -136,6 +136,7 @@ class Orchestrator {
|
|
|
136
136
|
case "--skip-measure": opts.skipMeasure = true; break;
|
|
137
137
|
case "--clean": opts.clean = true; break;
|
|
138
138
|
case "--verbose": case "-v": opts.verbose = true; break;
|
|
139
|
+
case "--parallel": opts.parallel = parseInt(argv[++i], 10) || 3; break;
|
|
139
140
|
case "--help":
|
|
140
141
|
case "-h":
|
|
141
142
|
if (this.wf.showUsage) this.wf.showUsage();
|
|
@@ -162,7 +163,8 @@ ${BOLD}Options:${RESET}
|
|
|
162
163
|
--review-port <N> Review server port (default: ${this.wf.defaults?.reviewPort || 3456})
|
|
163
164
|
--timeout <sec> Claude timeout per phase in seconds (default: 600)
|
|
164
165
|
--skip-measure Skip automated measurement (human-review only)
|
|
165
|
-
--clean
|
|
166
|
+
--clean Clear all artifacts from previous runs + delete build output
|
|
167
|
+
--parallel <N> Run N items concurrently (default: 1, recommended: 3)
|
|
166
168
|
--verbose, -v Show Claude's tool calls and prompts in terminal
|
|
167
169
|
--help Show this help
|
|
168
170
|
|
|
@@ -237,6 +239,71 @@ ${BOLD}Phases:${RESET} ${this.wf.phases.join(" → ")}
|
|
|
237
239
|
|
|
238
240
|
// ─── Server Management ───────────────────────────────────────────────
|
|
239
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
|
+
|
|
240
307
|
_parseStreamJson(raw, verbose) {
|
|
241
308
|
// stream-json format: one JSON object per line
|
|
242
309
|
// We want assistant text content and tool use visibility
|
|
@@ -554,7 +621,7 @@ ${BOLD}Phases:${RESET} ${this.wf.phases.join(" → ")}
|
|
|
554
621
|
|
|
555
622
|
// ─── Main Pipeline ──────────────────────────────────────────────────
|
|
556
623
|
|
|
557
|
-
run(argv) {
|
|
624
|
+
async run(argv) {
|
|
558
625
|
const opts = this.wf.parseArgs
|
|
559
626
|
? this.wf.parseArgs(argv, this.parseBaseArgs.bind(this))
|
|
560
627
|
: this.parseBaseArgs(argv || []);
|
|
@@ -597,6 +664,29 @@ ${BOLD}Phases:${RESET} ${this.wf.phases.join(" → ")}
|
|
|
597
664
|
}
|
|
598
665
|
} else {
|
|
599
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
|
+
}
|
|
600
690
|
}
|
|
601
691
|
|
|
602
692
|
// 4. Start servers
|
|
@@ -691,24 +781,30 @@ ${BOLD}Phases:${RESET} ${this.wf.phases.join(" → ")}
|
|
|
691
781
|
let measurements = {};
|
|
692
782
|
|
|
693
783
|
// ── Per-item pipeline: build ONE → review ONE → fix if needed ──
|
|
694
|
-
//
|
|
695
|
-
//
|
|
784
|
+
// Each item is independent: 1 contract + 1 source = tiny context.
|
|
785
|
+
// With --parallel N, runs N items concurrently.
|
|
696
786
|
if (this.wf.buildSingleItemPrompt && this.wf.buildSingleItemReviewPrompt) {
|
|
697
|
-
|
|
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...`);
|
|
792
|
+
}
|
|
698
793
|
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
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}`);
|
|
702
798
|
|
|
703
|
-
// Build
|
|
799
|
+
// Build
|
|
704
800
|
const buildPrompt = this.wf.buildSingleItemPrompt(phase, item, prevResults, projectDir);
|
|
705
801
|
dim(` Building...`);
|
|
706
|
-
const buildResult = this.
|
|
802
|
+
const buildResult = await this.spawnClaudeAsync(projectDir, buildPrompt, perItemTimeout, { label: `${phase}-build-${item.id}` });
|
|
707
803
|
|
|
708
804
|
if (buildResult.exitCode === 0) {
|
|
709
|
-
success(`
|
|
805
|
+
success(` ${item.componentName}: built (${buildResult.duration}s)`);
|
|
710
806
|
} else {
|
|
711
|
-
warn(`
|
|
807
|
+
warn(` ${item.componentName}: build exit ${buildResult.exitCode} (${buildResult.duration}s)`);
|
|
712
808
|
}
|
|
713
809
|
|
|
714
810
|
fs.writeFileSync(
|
|
@@ -716,12 +812,12 @@ ${BOLD}Phases:${RESET} ${this.wf.phases.join(" → ")}
|
|
|
716
812
|
`Exit code: ${buildResult.exitCode}\nDuration: ${buildResult.duration}s\n\n--- OUTPUT ---\n${buildResult.output.slice(0, 5000)}`
|
|
717
813
|
);
|
|
718
814
|
|
|
719
|
-
// Review
|
|
815
|
+
// Review cycles
|
|
720
816
|
let itemClean = false;
|
|
721
817
|
for (let cycle = 1; cycle <= maxAutoReviewCycles && !itemClean; cycle++) {
|
|
722
|
-
dim(`
|
|
818
|
+
dim(` ${item.componentName}: review c${cycle}...`);
|
|
723
819
|
const reviewPrompt = this.wf.buildSingleItemReviewPrompt(phase, item, {}, projectDir, { devPort, reviewPort });
|
|
724
|
-
const reviewResult = this.
|
|
820
|
+
const reviewResult = await this.spawnClaudeAsync(projectDir, reviewPrompt, perItemTimeout, { label: `${phase}-review-${item.id}-c${cycle}` });
|
|
725
821
|
|
|
726
822
|
const isCrash = reviewResult.exitCode !== 0 && reviewResult.duration < 10;
|
|
727
823
|
const isKilled = [143, 137].includes(reviewResult.exitCode);
|
|
@@ -730,7 +826,7 @@ ${BOLD}Phases:${RESET} ${this.wf.phases.join(" → ")}
|
|
|
730
826
|
let itemIssues = [];
|
|
731
827
|
if (isCrash || isKilled || isEmptyFail) {
|
|
732
828
|
const reason = isCrash ? "crashed" : isKilled ? "killed/timed out" : "failed with no output";
|
|
733
|
-
warn(`
|
|
829
|
+
warn(` ${item.componentName}: reviewer ${reason} (${reviewResult.duration}s)`);
|
|
734
830
|
itemIssues = [{ component: item.componentName, severity: "critical", description: `Reviewer ${reason}` }];
|
|
735
831
|
} else {
|
|
736
832
|
itemIssues = this.wf.parseReviewResult
|
|
@@ -740,28 +836,34 @@ ${BOLD}Phases:${RESET} ${this.wf.phases.join(" → ")}
|
|
|
740
836
|
|
|
741
837
|
if (itemIssues.length === 0) {
|
|
742
838
|
itemClean = true;
|
|
743
|
-
success(`
|
|
839
|
+
success(` ${item.componentName}: clean (${reviewResult.duration}s)`);
|
|
744
840
|
} else {
|
|
745
|
-
warn(` ${itemIssues.length} issue(s)
|
|
841
|
+
warn(` ${item.componentName}: ${itemIssues.length} issue(s)`);
|
|
746
842
|
for (const issue of itemIssues) {
|
|
747
|
-
dim(` ${issue.description ||
|
|
843
|
+
dim(` ${issue.description || "issue"} [${issue.severity || "medium"}]`);
|
|
748
844
|
}
|
|
749
845
|
|
|
750
846
|
if (cycle < maxAutoReviewCycles) {
|
|
751
|
-
// Fix this one item
|
|
752
847
|
const fixPrompt = this.wf.buildAutoFixPrompt
|
|
753
848
|
? this.wf.buildAutoFixPrompt(phase, itemIssues, [item], projectDir)
|
|
754
849
|
: this._defaultAutoFixPrompt(phase, itemIssues);
|
|
755
|
-
dim(`
|
|
756
|
-
const fixResult = this.
|
|
757
|
-
if (fixResult.exitCode === 0) success(`
|
|
758
|
-
else warn(`
|
|
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}`);
|
|
759
854
|
}
|
|
760
855
|
}
|
|
761
856
|
}
|
|
762
857
|
|
|
763
|
-
|
|
764
|
-
}
|
|
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)`);
|
|
765
867
|
|
|
766
868
|
// Measure ALL at once (one Playwright run after all items built)
|
|
767
869
|
if (!skipMeasure && this.wf.measure) {
|
|
@@ -33,7 +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` |
|
|
36
|
+
| `--clean` | Clear all stale artifacts + delete build output before each phase |
|
|
37
|
+
| `--parallel <N>` | Run N items concurrently (default: 1, recommended: 3) |
|
|
37
38
|
| `--verbose`, `-v` | Show Claude's tool calls and prompts in terminal |
|
|
38
39
|
|
|
39
40
|
## Prerequisites
|
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",
|