@tekyzinc/gsd-t 2.71.13 → 2.71.15

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,22 @@
2
2
 
3
3
  All notable changes to GSD-T are documented here. Updated with each release.
4
4
 
5
+ ## [2.71.15] - 2026-04-08
6
+
7
+ ### Changed (design-build command → orchestrator delegate)
8
+ - **`gsd-t-design-build.md` now delegates to the JS orchestrator** — the 388-line prompt-based command is replaced with a thin wrapper that runs `gsd-t design-build`. Both `/user:gsd-t-design-build` and `gsd-t design-build` now end up in the same deterministic pipeline. No more prompt-based gates that get skipped.
9
+
10
+ ## [2.71.14] - 2026-04-08
11
+
12
+ ### Added (design-build orchestrator)
13
+ - **Abstract workflow orchestrator** (`bin/orchestrator.js`) — base engine for deterministic multi-phase pipelines. Handles Claude spawning, review queue management, ironclad JS polling gates, state persistence/resume, server lifecycle, and cleanup. Workflow definitions plug in via a simple interface (phases, prompts, measurement, feedback). Zero external dependencies.
14
+ - **Design-build workflow** (`bin/design-orchestrator.js`) — first workflow implementation: elements → widgets → pages. Discovers contracts from `.gsd-t/contracts/design/`, builds per-tier Claude prompts, Playwright measurement, and review queue items. Plugs into the base orchestrator.
15
+ - **CLI subcommand** — `gsd-t design-build [--resume] [--tier] [--dev-port] [--review-port]` delegates to the orchestrator. Integrated into `bin/gsd-t.js` help and switch statement.
16
+ - **Resume capability** — orchestrator persists state to `orchestrator-state.json`, supports `--resume` to continue from where it left off after interruption.
17
+
18
+ ### Why
19
+ Three separate attempts to enforce review gates via prompt instructions all failed — Claude Code agents optimize for task completion and skip any instruction to pause indefinitely. The orchestrator moves flow control out of prompts entirely into deterministic JavaScript.
20
+
5
21
  ## [2.71.13] - 2026-04-08
6
22
 
7
23
  ### Fixed (design-decompose — successor hint)
@@ -0,0 +1,382 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * GSD-T Design Build Workflow
5
+ *
6
+ * Workflow definition for the design-build pipeline (elements → widgets → pages).
7
+ * Plugs into the base Orchestrator engine for deterministic flow control.
8
+ *
9
+ * Usage:
10
+ * gsd-t design-build [--resume] [--tier elements|widgets|pages] [--dev-port N] [--review-port N]
11
+ *
12
+ * Or directly:
13
+ * node bin/design-orchestrator.js [options]
14
+ */
15
+
16
+ const fs = require("fs");
17
+ const path = require("path");
18
+ const { execFileSync } = require("child_process");
19
+ const { Orchestrator, info, warn, success, error, log, BOLD, RESET, DIM } = require("./orchestrator.js");
20
+
21
+ // ─── Constants ──────────────────────────────────────────────────────────────
22
+
23
+ const PHASES = ["elements", "widgets", "pages"];
24
+ const PHASE_SINGULAR = { elements: "element", widgets: "widget", pages: "page" };
25
+ const CONTRACTS_DIR = ".gsd-t/contracts/design";
26
+
27
+ // ─── Contract Discovery ─────────────────────────────────────────────────────
28
+
29
+ function discoverWork(projectDir) {
30
+ const contractsDir = path.join(projectDir, CONTRACTS_DIR);
31
+ const indexPath = path.join(contractsDir, "INDEX.md");
32
+
33
+ if (!fs.existsSync(indexPath)) {
34
+ error("No design contracts found. Run /user:gsd-t-design-decompose first.");
35
+ process.exit(1);
36
+ }
37
+
38
+ const result = { elements: [], widgets: [], pages: [] };
39
+ const indexContent = fs.readFileSync(indexPath, "utf8");
40
+ const linkRegex = /^\s*-\s+\[([^\]]+)\]\(([^)]+)\)/gm;
41
+ let match;
42
+
43
+ while ((match = linkRegex.exec(indexContent)) !== null) {
44
+ const name = match[1];
45
+ const relPath = match[2];
46
+ const fullPath = path.join(contractsDir, relPath);
47
+
48
+ if (!fs.existsSync(fullPath)) {
49
+ warn(`Contract file missing: ${relPath}`);
50
+ continue;
51
+ }
52
+
53
+ let phase = null;
54
+ for (const p of PHASES) {
55
+ if (relPath.startsWith(`${p}/`)) { phase = p; break; }
56
+ }
57
+ if (!phase) continue;
58
+
59
+ const contract = parseContract(fullPath, name);
60
+ contract.contractPath = relPath;
61
+ contract.fullContractPath = fullPath;
62
+ result[phase].push(contract);
63
+ }
64
+
65
+ return result;
66
+ }
67
+
68
+ function parseContract(filePath, fallbackName) {
69
+ const content = fs.readFileSync(filePath, "utf8");
70
+ const contract = {
71
+ id: fallbackName,
72
+ name: fallbackName,
73
+ sourcePath: null,
74
+ selector: null,
75
+ route: "/",
76
+ };
77
+
78
+ const nameMatch = content.match(/\|\s*(?:element|widget|page)\s*\|\s*(.+?)\s*\|/i);
79
+ if (nameMatch) contract.id = nameMatch[1].trim();
80
+
81
+ const sourceMatch = content.match(/\|\s*source_path\s*\|\s*(.+?)\s*\|/i);
82
+ if (sourceMatch) contract.sourcePath = sourceMatch[1].trim();
83
+
84
+ const selectorMatch = content.match(/\|\s*selector\s*\|\s*(.+?)\s*\|/i);
85
+ if (selectorMatch) contract.selector = selectorMatch[1].trim();
86
+
87
+ const routeMatch = content.match(/\|\s*route\s*\|\s*(.+?)\s*\|/i);
88
+ if (routeMatch) contract.route = routeMatch[1].trim();
89
+
90
+ contract.componentName = contract.id
91
+ .split("-")
92
+ .map(s => s.charAt(0).toUpperCase() + s.slice(1))
93
+ .join("");
94
+
95
+ return contract;
96
+ }
97
+
98
+ // ─── Prompt Building ────────────────────────────────────────────────────────
99
+
100
+ function buildPrompt(phase, items, prevResults, projectDir) {
101
+ const singular = PHASE_SINGULAR[phase];
102
+ const contractList = items.map(c => {
103
+ const sourcePath = c.sourcePath || guessPaths(phase, c);
104
+ return `- ${c.componentName}: read contract at ${c.fullContractPath}, write to ${sourcePath}`;
105
+ }).join("\n");
106
+
107
+ const prevPaths = [];
108
+ for (const [, result] of Object.entries(prevResults)) {
109
+ if (result.builtPaths) prevPaths.push(...result.builtPaths);
110
+ }
111
+
112
+ const importInstructions = prevPaths.length > 0
113
+ ? `\n## Imports from Previous Tier\nImport these already-built components — do NOT rebuild their functionality inline:\n${prevPaths.map(p => `- ${p}`).join("\n")}\n`
114
+ : "";
115
+
116
+ return `You are building ${singular} components for a Vue 3 + TypeScript project.
117
+
118
+ ## Task
119
+ Build ONLY the following ${items.length} ${phase} components from their design contracts.
120
+ Read each contract file for exact visual specs — do NOT approximate values.
121
+
122
+ ## Components to Build
123
+ ${contractList}
124
+
125
+ ${importInstructions}
126
+ ## Rules
127
+ - Read each contract file (the full path is given above) for exact property values
128
+ - Write components to the specified source paths
129
+ - Follow the project's existing code conventions (check existing files in src/)
130
+ - Use the project's existing dependencies (check package.json)
131
+ - When ALL ${items.length} components are complete, STOP. Do not start a dev server, do not ask for review, do not build components from other tiers.
132
+
133
+ ## Important
134
+ This is a FINITE task. Build the ${items.length} ${phase} listed above, then EXIT.`;
135
+ }
136
+
137
+ // ─── Measurement ────────────────────────────────────────────────────────────
138
+
139
+ function hasPlaywright(projectDir) {
140
+ try {
141
+ return fs.existsSync(path.join(projectDir, "node_modules", "@playwright", "test", "package.json"));
142
+ } catch {
143
+ return false;
144
+ }
145
+ }
146
+
147
+ function measure(projectDir, phase, items, ports) {
148
+ if (!hasPlaywright(projectDir)) {
149
+ warn("Playwright not found — skipping automated measurement. Human review only.");
150
+ const results = {};
151
+ for (const c of items) results[c.id] = [];
152
+ return results;
153
+ }
154
+
155
+ info(`Measuring ${items.length} ${phase} with Playwright...`);
156
+
157
+ const measureScript = buildMeasureScript(items, ports.reviewPort);
158
+ const scriptDir = path.join(projectDir, ".gsd-t", "design-review");
159
+ try { fs.mkdirSync(scriptDir, { recursive: true }); } catch { /* exists */ }
160
+ const scriptPath = path.join(scriptDir, "_measure.mjs");
161
+ fs.writeFileSync(scriptPath, measureScript);
162
+
163
+ const results = {};
164
+ try {
165
+ const output = execFileSync("node", [scriptPath], {
166
+ encoding: "utf8",
167
+ timeout: 60_000,
168
+ cwd: projectDir,
169
+ stdio: ["pipe", "pipe", "pipe"],
170
+ });
171
+ try {
172
+ const parsed = JSON.parse(output.trim());
173
+ for (const [id, measurements] of Object.entries(parsed)) {
174
+ results[id] = measurements;
175
+ }
176
+ } catch {
177
+ warn("Could not parse measurement output — proceeding without measurements");
178
+ }
179
+ } catch (e) {
180
+ warn(`Measurement failed: ${(e.message || "").slice(0, 100)}`);
181
+ }
182
+
183
+ try { fs.unlinkSync(scriptPath); } catch { /* ignore */ }
184
+
185
+ let passing = 0, failing = 0;
186
+ for (const measurements of Object.values(results)) {
187
+ for (const m of measurements) {
188
+ if (m.pass) passing++; else failing++;
189
+ }
190
+ }
191
+ if (failing > 0) warn(`Measurements: ${passing} pass, ${failing} fail`);
192
+ else if (passing > 0) success(`Measurements: ${passing} pass, 0 fail`);
193
+
194
+ return results;
195
+ }
196
+
197
+ function buildMeasureScript(items, reviewPort) {
198
+ const selectors = items
199
+ .filter(c => c.selector)
200
+ .map(c => ` "${c.id}": "${c.selector}"`)
201
+ .join(",\n");
202
+
203
+ return `
204
+ import { chromium } from "playwright";
205
+
206
+ const SELECTORS = {
207
+ ${selectors}
208
+ };
209
+
210
+ async function measure() {
211
+ const browser = await chromium.launch({ headless: true });
212
+ const page = await browser.newPage();
213
+ await page.setViewportSize({ width: 1440, height: 900 });
214
+
215
+ try {
216
+ await page.goto("http://localhost:${reviewPort}/", { waitUntil: "networkidle", timeout: 15000 });
217
+ } catch {
218
+ await page.goto("http://localhost:${reviewPort}/", { waitUntil: "load", timeout: 10000 });
219
+ }
220
+
221
+ await page.waitForTimeout(2000);
222
+
223
+ const results = {};
224
+
225
+ for (const [id, selector] of Object.entries(SELECTORS)) {
226
+ try {
227
+ const measurements = await page.evaluate((sel) => {
228
+ const el = document.querySelector(sel);
229
+ if (!el) return [{ property: "element exists", expected: "yes", actual: "no", pass: false, severity: "critical" }];
230
+
231
+ const s = getComputedStyle(el);
232
+ const rect = el.getBoundingClientRect();
233
+
234
+ return [
235
+ { property: "element exists", expected: "yes", actual: "yes", pass: true },
236
+ { property: "display", expected: "visible", actual: s.display === "none" ? "hidden" : "visible", pass: s.display !== "none" },
237
+ { property: "width", expected: ">0", actual: String(Math.round(rect.width)) + "px", pass: rect.width > 0 },
238
+ { property: "height", expected: ">0", actual: String(Math.round(rect.height)) + "px", pass: rect.height > 0 },
239
+ ];
240
+ }, selector);
241
+
242
+ results[id] = measurements;
243
+ } catch {
244
+ results[id] = [{ property: "measurement", expected: "success", actual: "error", pass: false, severity: "critical" }];
245
+ }
246
+ }
247
+
248
+ await browser.close();
249
+ process.stdout.write(JSON.stringify(results));
250
+ }
251
+
252
+ measure().catch(e => {
253
+ process.stderr.write(e.message);
254
+ process.exit(1);
255
+ });
256
+ `;
257
+ }
258
+
259
+ // ─── Queue Items ────────────────────────────────────────────────────────────
260
+
261
+ function buildQueueItem(phase, item, measurements) {
262
+ const singular = PHASE_SINGULAR[phase];
263
+ const sourcePath = item.sourcePath || guessPaths(phase, item);
264
+ return {
265
+ id: `${singular}-${item.id}`,
266
+ name: item.componentName,
267
+ type: singular,
268
+ selector: item.selector || `.${item.id}`,
269
+ sourcePath,
270
+ route: item.route || "/",
271
+ measurements: (measurements && measurements[item.id]) || [],
272
+ };
273
+ }
274
+
275
+ // ─── Path Guessing ──────────────────────────────────────────────────────────
276
+
277
+ function guessPaths(phase, item) {
278
+ const dirMap = {
279
+ elements: "src/components/elements",
280
+ widgets: "src/components/widgets",
281
+ pages: "src/views",
282
+ };
283
+ return `${dirMap[phase]}/${item.componentName}.vue`;
284
+ }
285
+
286
+ // ─── Fix Prompt ─────────────────────────────────────────────────────────────
287
+
288
+ function buildFixPrompt(phase, needsWork) {
289
+ const fixes = needsWork.map(item => {
290
+ const parts = [`Fix ${item.id}:`];
291
+ if (item.changes?.length) {
292
+ for (const c of item.changes) {
293
+ parts.push(` - ${c.property}: change from ${c.oldValue} to ${c.newValue} in ${c.path || "the component file"}`);
294
+ }
295
+ }
296
+ if (item.comment) parts.push(` - Additional: ${item.comment}`);
297
+ return parts.join("\n");
298
+ }).join("\n\n");
299
+
300
+ return `Apply these specific fixes to ${phase} components:\n\n${fixes}\n\nApply the changes and EXIT. Do not rebuild anything else.`;
301
+ }
302
+
303
+ // ─── Summary ────────────────────────────────────────────────────────────────
304
+
305
+ function formatSummary(phase, result) {
306
+ return `${phase}: ${result.builtPaths.length} components built (${result.reviewCycles} review cycle${result.reviewCycles > 1 ? "s" : ""})`;
307
+ }
308
+
309
+ // ─── Usage ──────────────────────────────────────────────────────────────────
310
+
311
+ function showUsage() {
312
+ log(`
313
+ ${BOLD}GSD-T Design Build Orchestrator${RESET}
314
+
315
+ ${BOLD}Usage:${RESET}
316
+ gsd-t design-build [options]
317
+
318
+ ${BOLD}Options:${RESET}
319
+ --resume Resume from last saved state
320
+ --tier <name> Start from specific tier (elements, widgets, pages)
321
+ --project <dir> Project directory (default: cwd)
322
+ --dev-port <N> Dev server port (default: 5173)
323
+ --review-port <N> Review server port (default: 3456)
324
+ --timeout <sec> Claude timeout per tier in seconds (default: 600)
325
+ --skip-measure Skip Playwright measurement (human-review only)
326
+ --help Show this help
327
+
328
+ ${BOLD}Pipeline:${RESET}
329
+ 1. Read contracts from .gsd-t/contracts/design/
330
+ 2. Start dev server + review server
331
+ 3. For each tier (elements → widgets → pages):
332
+ a. Spawn Claude to build components
333
+ b. Measure with Playwright
334
+ c. Queue for human review
335
+ d. Wait for review submission (blocks until human approves)
336
+ e. Process feedback, proceed to next tier
337
+ `);
338
+ }
339
+
340
+ // ─── Workflow Definition ────────────────────────────────────────────────────
341
+
342
+ const designBuildWorkflow = {
343
+ name: "Design Build",
344
+ command: "design-build",
345
+ phases: PHASES,
346
+ reviewDir: ".gsd-t/design-review",
347
+ stateFile: ".gsd-t/design-review/orchestrator-state.json",
348
+ defaults: {
349
+ devPort: 5173,
350
+ reviewPort: 3456,
351
+ timeout: 600_000,
352
+ devServerTimeout: 30_000,
353
+ maxReviewCycles: 3,
354
+ },
355
+ completionMessage: "All done. Run your app to verify: npm run dev",
356
+
357
+ discoverWork,
358
+ buildPrompt,
359
+ measure,
360
+ buildQueueItem,
361
+ buildFixPrompt,
362
+ guessPaths,
363
+ formatSummary,
364
+ showUsage,
365
+
366
+ // Use --tier as alias for --phase (design-build convention)
367
+ parseArgs(argv, parseBase) {
368
+ return parseBase(argv);
369
+ },
370
+ };
371
+
372
+ // ─── Entry Point ────────────────────────────────────────────────────────────
373
+
374
+ function run(args) {
375
+ new Orchestrator(designBuildWorkflow).run(args || []);
376
+ }
377
+
378
+ if (require.main === module) {
379
+ run(process.argv.slice(2));
380
+ }
381
+
382
+ module.exports = { run, workflow: designBuildWorkflow };
package/bin/gsd-t.js CHANGED
@@ -2562,6 +2562,7 @@ function showHelp() {
2562
2562
  log(` ${CYAN}changelog${RESET} Open changelog in the browser`);
2563
2563
  log(` ${CYAN}graph${RESET} Code graph operations (index, status, query)`);
2564
2564
  log(` ${CYAN}headless${RESET} Non-interactive execution via claude -p + fast state queries`);
2565
+ log(` ${CYAN}design-build${RESET} Deterministic design→code pipeline (elements → widgets → pages)`);
2565
2566
  log(` ${CYAN}help${RESET} Show this help\n`);
2566
2567
  log(`${BOLD}Examples:${RESET}`);
2567
2568
  log(` ${DIM}$${RESET} npx @tekyzinc/gsd-t install`);
@@ -2696,6 +2697,11 @@ if (require.main === module) {
2696
2697
  case "headless":
2697
2698
  doHeadless(args.slice(1));
2698
2699
  break;
2700
+ case "design-build": {
2701
+ const orchestrator = require("./design-orchestrator.js");
2702
+ orchestrator.run(args.slice(1));
2703
+ break;
2704
+ }
2699
2705
  case "scan": {
2700
2706
  const exportFlag = args.find(a => a.startsWith('--export='));
2701
2707
  const exportFormat = exportFlag ? exportFlag.split('=')[1] : null;