@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 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": {
@@ -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 Delete previous build output before building each phase
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
- // Preferred when workflow provides single-item prompts.
695
- // Each Claude spawn reads 1 contract + 1 source = tiny context, fast completion.
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
- log(`\n${CYAN} ⚙${RESET} Building and reviewing ${items.length} ${phase} one at a time...`);
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
- for (let idx = 0; idx < items.length; idx++) {
700
- const item = items[idx];
701
- heading(` [${idx + 1}/${items.length}] ${item.componentName}`);
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 one item
799
+ // Build
704
800
  const buildPrompt = this.wf.buildSingleItemPrompt(phase, item, prevResults, projectDir);
705
801
  dim(` Building...`);
706
- const buildResult = this.spawnClaude(projectDir, buildPrompt, perItemTimeout, { label: `${phase}-build-${item.id}` });
802
+ const buildResult = await this.spawnClaudeAsync(projectDir, buildPrompt, perItemTimeout, { label: `${phase}-build-${item.id}` });
707
803
 
708
804
  if (buildResult.exitCode === 0) {
709
- success(` Built (${buildResult.duration}s)`);
805
+ success(` ${item.componentName}: built (${buildResult.duration}s)`);
710
806
  } else {
711
- warn(` Build exited with code ${buildResult.exitCode} (${buildResult.duration}s)`);
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 one item (up to maxAutoReviewCycles)
815
+ // Review cycles
720
816
  let itemClean = false;
721
817
  for (let cycle = 1; cycle <= maxAutoReviewCycles && !itemClean; cycle++) {
722
- dim(` Review cycle ${cycle}/${maxAutoReviewCycles}...`);
818
+ dim(` ${item.componentName}: review c${cycle}...`);
723
819
  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}` });
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(` Reviewer ${reason} (${reviewResult.duration}s)`);
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(` Clean (${reviewResult.duration}s)`);
839
+ success(` ${item.componentName}: clean (${reviewResult.duration}s)`);
744
840
  } else {
745
- warn(` ${itemIssues.length} issue(s) found`);
841
+ warn(` ${item.componentName}: ${itemIssues.length} issue(s)`);
746
842
  for (const issue of itemIssues) {
747
- dim(` ${issue.description || issue.reason || "issue"} [${issue.severity || "medium"}]`);
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(` 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}`);
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
- builtPaths.push(item.sourcePath || (this.wf.guessPaths ? this.wf.guessPaths(phase, item) : ""));
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` | Delete previous build output before building each phase |
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.72.10",
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",