@tekyzinc/gsd-t 3.16.12 → 3.18.12
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 +67 -0
- package/README.md +13 -3
- package/bin/gsd-t-depgraph-validate.cjs +140 -0
- package/bin/gsd-t-economics.cjs +287 -0
- package/bin/gsd-t-file-disjointness.cjs +227 -0
- package/bin/gsd-t-in-session-usage.cjs +213 -0
- package/bin/gsd-t-orchestrator-config.cjs +100 -3
- package/bin/gsd-t-orchestrator.js +2 -1
- package/bin/gsd-t-parallel.cjs +382 -0
- package/bin/gsd-t-report-tokens.cjs +549 -0
- package/bin/gsd-t-task-graph.cjs +366 -0
- package/bin/gsd-t-token-capture.cjs +29 -14
- package/bin/gsd-t-token-dashboard.cjs +35 -0
- package/bin/gsd-t-tool-attribution.cjs +377 -0
- package/bin/gsd-t-tool-cost.cjs +195 -0
- package/bin/gsd-t-unattended-platform.cjs +7 -1
- package/bin/gsd-t-unattended.cjs +2 -0
- package/bin/gsd-t.js +155 -5
- package/bin/headless-auto-spawn.cjs +69 -49
- package/bin/headless-auto-spawn.js +18 -24
- package/bin/runway-estimator.cjs +212 -0
- package/bin/spawn-plan-derive.cjs +163 -0
- package/bin/spawn-plan-status-updater.cjs +292 -0
- package/bin/spawn-plan-writer.cjs +204 -0
- package/commands/gsd-t-debug.md +26 -7
- package/commands/gsd-t-execute.md +36 -28
- package/commands/gsd-t-help.md +11 -0
- package/commands/gsd-t-integrate.md +27 -7
- package/commands/gsd-t-quick.md +30 -13
- package/commands/gsd-t-scan.md +5 -5
- package/commands/gsd-t-unattended-watch.md +4 -3
- package/commands/gsd-t-unattended.md +9 -3
- package/commands/gsd-t-verify.md +5 -5
- package/commands/gsd-t-wave.md +21 -8
- package/commands/gsd.md +45 -3
- package/docs/GSD-T-README.md +43 -5
- package/docs/architecture.md +423 -3
- package/docs/requirements.md +203 -0
- package/package.json +1 -1
- package/scripts/gsd-t-calibration-hook.js +256 -0
- package/scripts/gsd-t-compact-detector.js +223 -0
- package/scripts/gsd-t-compaction-scanner.js +305 -0
- package/scripts/gsd-t-dashboard-autostart.cjs +172 -0
- package/scripts/gsd-t-dashboard-server.js +179 -0
- package/scripts/gsd-t-dashboard.html +3 -3
- package/scripts/gsd-t-heartbeat.js +50 -2
- package/scripts/gsd-t-post-commit-spawn-plan.sh +86 -0
- package/scripts/gsd-t-transcript.html +546 -43
- package/scripts/hooks/gsd-t-in-session-usage-hook.js +84 -0
- package/scripts/spawn-plan-fmt-tokens.cjs +80 -0
- package/templates/CLAUDE-global.md +8 -3
- package/templates/CLAUDE-project.md +17 -14
- package/templates/hooks/post-commit-spawn-plan.sh +85 -0
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* gsd-t-parallel — M44 D2
|
|
5
|
+
*
|
|
6
|
+
* `gsd-t parallel` subcommand. Wraps the M40 orchestrator with task-level
|
|
7
|
+
* (not just domain-level) parallelism and mode-aware gating math.
|
|
8
|
+
*
|
|
9
|
+
* Consumes:
|
|
10
|
+
* - D1 task graph (bin/gsd-t-task-graph.cjs)
|
|
11
|
+
* - D4 depgraph validation (bin/gsd-t-depgraph-validate.cjs)
|
|
12
|
+
* - D5 file-disjointness (bin/gsd-t-file-disjointness.cjs)
|
|
13
|
+
* - D6 economics estimator (bin/gsd-t-economics.cjs)
|
|
14
|
+
* - token-budget (bin/token-budget.cjs) — in-session ctxPct
|
|
15
|
+
* - mode-aware gating math (bin/gsd-t-orchestrator-config.cjs)
|
|
16
|
+
*
|
|
17
|
+
* Does NOT replace `bin/gsd-t-orchestrator.js`. `runParallel` prepares the
|
|
18
|
+
* validated ready-task set and plan; the existing orchestrator machinery
|
|
19
|
+
* owns the actual worker spawn. In T2 this module emits the plan;
|
|
20
|
+
* the downstream orchestrator consumes it.
|
|
21
|
+
*
|
|
22
|
+
* Contract: `.gsd-t/contracts/wave-join-contract.md` v1.1.0 (§Mode-Aware Gating Math).
|
|
23
|
+
*
|
|
24
|
+
* Hard rules (from constraints.md):
|
|
25
|
+
* - Zero external runtime deps (Node built-ins only)
|
|
26
|
+
* - Never throws pause/resume prompts under any condition
|
|
27
|
+
* - All three invariants (disjointness, auto-merge, economics) apply to both modes
|
|
28
|
+
* - `--dry-run` MUST be supported; prints plan without spawning
|
|
29
|
+
* - `--mode` auto-detect fallback: `GSD_T_UNATTENDED=1` → `unattended`, else `in-session`
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
const fs = require("node:fs");
|
|
33
|
+
const path = require("node:path");
|
|
34
|
+
|
|
35
|
+
const { buildTaskGraph, getReadyTasks } = require(path.join(__dirname, "gsd-t-task-graph.cjs"));
|
|
36
|
+
const { validateDepGraph } = require(path.join(__dirname, "gsd-t-depgraph-validate.cjs"));
|
|
37
|
+
const { proveDisjointness } = require(path.join(__dirname, "gsd-t-file-disjointness.cjs"));
|
|
38
|
+
const { estimateTaskFootprint } = require(path.join(__dirname, "gsd-t-economics.cjs"));
|
|
39
|
+
const {
|
|
40
|
+
computeInSessionHeadroom,
|
|
41
|
+
computeUnattendedGate,
|
|
42
|
+
} = require(path.join(__dirname, "gsd-t-orchestrator-config.cjs"));
|
|
43
|
+
|
|
44
|
+
// token-budget is optional at require-time so unit tests can stub via dependency injection.
|
|
45
|
+
let _tokenBudget = null;
|
|
46
|
+
function loadTokenBudget() {
|
|
47
|
+
if (_tokenBudget) return _tokenBudget;
|
|
48
|
+
try {
|
|
49
|
+
_tokenBudget = require(path.join(__dirname, "token-budget.cjs"));
|
|
50
|
+
} catch {
|
|
51
|
+
_tokenBudget = { getSessionStatus: () => ({ pct: 0 }) };
|
|
52
|
+
}
|
|
53
|
+
return _tokenBudget;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ─── event stream writer ──────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
function appendEvent(projectDir, event) {
|
|
59
|
+
try {
|
|
60
|
+
const eventsDir = path.join(projectDir, ".gsd-t", "events");
|
|
61
|
+
fs.mkdirSync(eventsDir, { recursive: true });
|
|
62
|
+
const day = (event.ts || new Date().toISOString()).slice(0, 10);
|
|
63
|
+
const file = path.join(eventsDir, `${day}.jsonl`);
|
|
64
|
+
fs.appendFileSync(file, JSON.stringify(event) + "\n");
|
|
65
|
+
} catch {
|
|
66
|
+
// Best-effort; event-log failures must not break control flow.
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ─── mode detection ───────────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
function detectMode(opts, env) {
|
|
73
|
+
if (opts && typeof opts.mode === "string" && opts.mode) return opts.mode;
|
|
74
|
+
const e = env || process.env;
|
|
75
|
+
if (e.GSD_T_UNATTENDED === "1") return "unattended";
|
|
76
|
+
return "in-session";
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ─── CLI arg parsing ──────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
function parseArgv(argv) {
|
|
82
|
+
const out = { help: false, dryRun: false, mode: null, milestone: null, domain: null };
|
|
83
|
+
for (let i = 0; i < argv.length; i++) {
|
|
84
|
+
const a = argv[i];
|
|
85
|
+
if (a === "--help" || a === "-h") out.help = true;
|
|
86
|
+
else if (a === "--dry-run") out.dryRun = true;
|
|
87
|
+
else if (a === "--mode") out.mode = argv[++i] || null;
|
|
88
|
+
else if (a.startsWith("--mode=")) out.mode = a.slice("--mode=".length);
|
|
89
|
+
else if (a === "--milestone") out.milestone = argv[++i] || null;
|
|
90
|
+
else if (a.startsWith("--milestone=")) out.milestone = a.slice("--milestone=".length);
|
|
91
|
+
else if (a === "--domain") out.domain = argv[++i] || null;
|
|
92
|
+
else if (a.startsWith("--domain=")) out.domain = a.slice("--domain=".length);
|
|
93
|
+
}
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const HELP_TEXT = `Usage: gsd-t parallel [options]
|
|
98
|
+
|
|
99
|
+
Dispatch M44 task-level parallelism through the M40 orchestrator with
|
|
100
|
+
mode-aware gating math. Extends — does not replace — the orchestrator.
|
|
101
|
+
|
|
102
|
+
Options:
|
|
103
|
+
--mode <in-session|unattended> Explicit mode. Auto-detects from
|
|
104
|
+
GSD_T_UNATTENDED=1 env when omitted;
|
|
105
|
+
defaults to in-session otherwise.
|
|
106
|
+
--milestone <Mxx> Limit planning to a single milestone.
|
|
107
|
+
--domain <name> Limit planning to a single domain.
|
|
108
|
+
--dry-run Print the proposed worker plan table
|
|
109
|
+
and exit without spawning any workers.
|
|
110
|
+
--help, -h Show this message and exit 0.
|
|
111
|
+
|
|
112
|
+
Gates applied before any fan-out (in order):
|
|
113
|
+
1. D4 depgraph validation — any task with unmet deps is vetoed.
|
|
114
|
+
2. D5 file-disjointness prover — overlap → sequential fallback.
|
|
115
|
+
3. D6 economics estimator — per-task CW% footprint.
|
|
116
|
+
|
|
117
|
+
Modes:
|
|
118
|
+
in-session Never throws pause/resume prompts. Before fan-out,
|
|
119
|
+
computes ctxPct + N × summarySize ≤ 85. If not, reduces
|
|
120
|
+
N until it fits; final floor is N=1 (sequential).
|
|
121
|
+
unattended Per-worker CW headroom is the binding gate. Tasks whose
|
|
122
|
+
estimated CW% > 60 emit a task_split signal.
|
|
123
|
+
|
|
124
|
+
Contract: .gsd-t/contracts/wave-join-contract.md v1.1.0
|
|
125
|
+
`;
|
|
126
|
+
|
|
127
|
+
// ─── runParallel — the exported entrypoint ────────────────────────────────
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* runParallel({projectDir, mode, milestone, domain, dryRun}) → plan object
|
|
131
|
+
*
|
|
132
|
+
* Applies D4 dep-graph + D5 disjointness + D6 economics gates, then the
|
|
133
|
+
* mode-aware headroom/split gate, and returns the resolved worker plan.
|
|
134
|
+
*
|
|
135
|
+
* Does not spawn. The caller (M40 orchestrator) owns actual worker launch.
|
|
136
|
+
*/
|
|
137
|
+
function runParallel(opts) {
|
|
138
|
+
const projectDir = (opts && opts.projectDir) || process.cwd();
|
|
139
|
+
const mode = detectMode(opts, opts && opts.env);
|
|
140
|
+
const milestone = (opts && opts.milestone) || null;
|
|
141
|
+
const domain = (opts && opts.domain) || null;
|
|
142
|
+
const dryRun = !!(opts && opts.dryRun);
|
|
143
|
+
const summarySize = Number.isFinite(opts && opts.summarySize)
|
|
144
|
+
? opts.summarySize
|
|
145
|
+
: require(path.join(__dirname, "gsd-t-orchestrator-config.cjs")).DEFAULT_SUMMARY_SIZE_PCT;
|
|
146
|
+
|
|
147
|
+
const graph = buildTaskGraph({ projectDir });
|
|
148
|
+
let candidates = getReadyTasks(graph);
|
|
149
|
+
|
|
150
|
+
// Optional filtering by milestone / domain.
|
|
151
|
+
if (domain) candidates = candidates.filter((t) => t.domain === domain);
|
|
152
|
+
if (milestone) {
|
|
153
|
+
// Milestone id prefixes task ids in this codebase (M44-D2-T1 → M44).
|
|
154
|
+
const prefix = String(milestone).toUpperCase();
|
|
155
|
+
candidates = candidates.filter((t) => String(t.id).toUpperCase().startsWith(prefix + "-"));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ── D4 depgraph gate ──
|
|
159
|
+
const depResult = validateDepGraph({ graph: { ...graph, ready: candidates.map((t) => t.id) }, projectDir });
|
|
160
|
+
const depReady = depResult.ready;
|
|
161
|
+
const depVetoed = depResult.vetoed || [];
|
|
162
|
+
for (const v of depVetoed) {
|
|
163
|
+
appendEvent(projectDir, {
|
|
164
|
+
type: "gate_veto",
|
|
165
|
+
task_id: v.task && v.task.id,
|
|
166
|
+
gate: "depgraph",
|
|
167
|
+
reason: `unmet_deps:${(v.unmet_deps || []).join(",")}`,
|
|
168
|
+
ts: new Date().toISOString(),
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ── D5 disjointness gate ──
|
|
173
|
+
const disj = proveDisjointness({ tasks: depReady, projectDir });
|
|
174
|
+
const disjointTaskIds = new Set();
|
|
175
|
+
for (const group of disj.parallel || []) {
|
|
176
|
+
for (const t of group) disjointTaskIds.add(t.id);
|
|
177
|
+
}
|
|
178
|
+
// Sequential groups + unprovable are NOT candidates for parallel fan-out
|
|
179
|
+
// but still allowed as single-worker (sequential) — surfaced as gate_veto
|
|
180
|
+
// events so "why wasn't this parallelized?" is observable.
|
|
181
|
+
const sequentialFallback = new Set();
|
|
182
|
+
for (const group of disj.sequential || []) {
|
|
183
|
+
for (const t of group) {
|
|
184
|
+
sequentialFallback.add(t.id);
|
|
185
|
+
appendEvent(projectDir, {
|
|
186
|
+
type: "gate_veto",
|
|
187
|
+
task_id: t.id,
|
|
188
|
+
gate: "disjointness",
|
|
189
|
+
reason: "write-target-overlap-or-unprovable",
|
|
190
|
+
ts: new Date().toISOString(),
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ── D6 economics gate (per-task estimate) ──
|
|
196
|
+
const perTask = new Map();
|
|
197
|
+
for (const t of depReady) {
|
|
198
|
+
let est;
|
|
199
|
+
try {
|
|
200
|
+
est = estimateTaskFootprint({ taskNode: t, mode, projectDir });
|
|
201
|
+
} catch {
|
|
202
|
+
est = { estimatedCwPct: 0, parallelOk: true, split: false, workerCount: 1, confidence: "low" };
|
|
203
|
+
}
|
|
204
|
+
perTask.set(t.id, est);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ── Mode-aware gating math ──
|
|
208
|
+
let finalParallelTasks = [];
|
|
209
|
+
let reducedCount = null;
|
|
210
|
+
let ctxPctObserved = null;
|
|
211
|
+
if (mode === "unattended") {
|
|
212
|
+
// Each parallel-candidate task gets an unattended gate check.
|
|
213
|
+
for (const t of depReady) {
|
|
214
|
+
const est = perTask.get(t.id);
|
|
215
|
+
if (!disjointTaskIds.has(t.id)) continue; // already sequential
|
|
216
|
+
const gate = computeUnattendedGate({ estimatedCwPct: est.estimatedCwPct, threshold: 60 });
|
|
217
|
+
if (gate.split) {
|
|
218
|
+
appendEvent(projectDir, {
|
|
219
|
+
type: "task_split",
|
|
220
|
+
task_id: t.id,
|
|
221
|
+
estimatedCwPct: est.estimatedCwPct,
|
|
222
|
+
ts: new Date().toISOString(),
|
|
223
|
+
});
|
|
224
|
+
// Actual slicing is the caller's responsibility — the task stays in
|
|
225
|
+
// the parallel set; the orchestrator (or caller) treats it as
|
|
226
|
+
// "needs split". Per D2-T2 acceptance: emitting the event and
|
|
227
|
+
// returning the plan is sufficient.
|
|
228
|
+
}
|
|
229
|
+
finalParallelTasks.push(t);
|
|
230
|
+
}
|
|
231
|
+
} else {
|
|
232
|
+
// in-session path
|
|
233
|
+
const tb = loadTokenBudget();
|
|
234
|
+
let status;
|
|
235
|
+
try { status = tb.getSessionStatus(projectDir); } catch { status = { pct: 0 }; }
|
|
236
|
+
const ctxPct = Number.isFinite(status && status.pct) ? status.pct : 0;
|
|
237
|
+
ctxPctObserved = ctxPct;
|
|
238
|
+
const parallelCandidates = depReady.filter((t) => disjointTaskIds.has(t.id));
|
|
239
|
+
const requested = parallelCandidates.length;
|
|
240
|
+
const headroom = computeInSessionHeadroom({ ctxPct, workerCount: requested, summarySize });
|
|
241
|
+
reducedCount = headroom.reducedCount;
|
|
242
|
+
if (reducedCount < requested) {
|
|
243
|
+
appendEvent(projectDir, {
|
|
244
|
+
type: "parallelism_reduced",
|
|
245
|
+
original_count: requested,
|
|
246
|
+
reduced_count: reducedCount,
|
|
247
|
+
reason: "in_session_headroom",
|
|
248
|
+
ts: new Date().toISOString(),
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
finalParallelTasks = parallelCandidates.slice(0, reducedCount);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Build the plan table rows (all ready tasks, labeled by decision).
|
|
255
|
+
const plan = depReady.map((t) => {
|
|
256
|
+
const est = perTask.get(t.id) || {};
|
|
257
|
+
const disjointOk = disjointTaskIds.has(t.id);
|
|
258
|
+
const isFinalParallel = finalParallelTasks.some((x) => x.id === t.id);
|
|
259
|
+
let decision;
|
|
260
|
+
if (isFinalParallel) {
|
|
261
|
+
decision = mode === "unattended" && est.split ? "parallel-split" : "parallel";
|
|
262
|
+
} else if (sequentialFallback.has(t.id)) {
|
|
263
|
+
decision = "sequential";
|
|
264
|
+
} else {
|
|
265
|
+
decision = "sequential";
|
|
266
|
+
}
|
|
267
|
+
return {
|
|
268
|
+
task_id: t.id,
|
|
269
|
+
domain: t.domain,
|
|
270
|
+
estimatedCwPct: Number.isFinite(est.estimatedCwPct) ? est.estimatedCwPct : null,
|
|
271
|
+
disjoint: disjointOk,
|
|
272
|
+
depsOk: true,
|
|
273
|
+
decision,
|
|
274
|
+
};
|
|
275
|
+
});
|
|
276
|
+
// Also show dep-vetoed tasks so the dry-run table is complete.
|
|
277
|
+
for (const v of depVetoed) {
|
|
278
|
+
if (!v.task) continue;
|
|
279
|
+
plan.push({
|
|
280
|
+
task_id: v.task.id,
|
|
281
|
+
domain: v.task.domain,
|
|
282
|
+
estimatedCwPct: null,
|
|
283
|
+
disjoint: null,
|
|
284
|
+
depsOk: false,
|
|
285
|
+
decision: "veto-deps",
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return {
|
|
290
|
+
mode,
|
|
291
|
+
milestone,
|
|
292
|
+
domain,
|
|
293
|
+
dryRun,
|
|
294
|
+
projectDir,
|
|
295
|
+
plan,
|
|
296
|
+
workerCount: finalParallelTasks.length || (plan.length ? 1 : 0),
|
|
297
|
+
parallelTasks: finalParallelTasks.map((t) => t.id),
|
|
298
|
+
reducedCount,
|
|
299
|
+
ctxPct: ctxPctObserved,
|
|
300
|
+
warnings: graph.warnings || [],
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// ─── CLI entry ────────────────────────────────────────────────────────────
|
|
305
|
+
|
|
306
|
+
// ─── dry-run table formatter ──────────────────────────────────────────────
|
|
307
|
+
|
|
308
|
+
const PLAN_HEADER = ["task_id", "domain", "estimated CW%", "disjoint?", "deps ok?", "decision"];
|
|
309
|
+
|
|
310
|
+
function formatPlanTable(plan) {
|
|
311
|
+
const rows = [PLAN_HEADER.slice()];
|
|
312
|
+
for (const r of plan) {
|
|
313
|
+
rows.push([
|
|
314
|
+
String(r.task_id),
|
|
315
|
+
String(r.domain || "-"),
|
|
316
|
+
r.estimatedCwPct == null ? "-" : String(Math.round(r.estimatedCwPct)),
|
|
317
|
+
r.disjoint == null ? "-" : (r.disjoint ? "yes" : "no"),
|
|
318
|
+
r.depsOk ? "yes" : "no",
|
|
319
|
+
String(r.decision),
|
|
320
|
+
]);
|
|
321
|
+
}
|
|
322
|
+
const widths = PLAN_HEADER.map((_, col) =>
|
|
323
|
+
rows.reduce((w, row) => Math.max(w, String(row[col]).length), 0),
|
|
324
|
+
);
|
|
325
|
+
const sep = widths.map((w) => "-".repeat(w)).join(" ");
|
|
326
|
+
const lines = [];
|
|
327
|
+
for (let i = 0; i < rows.length; i++) {
|
|
328
|
+
const row = rows[i].map((cell, col) => String(cell).padEnd(widths[col])).join(" ");
|
|
329
|
+
lines.push(row);
|
|
330
|
+
if (i === 0) lines.push(sep);
|
|
331
|
+
}
|
|
332
|
+
return lines.join("\n") + "\n";
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function runCli(argv, env) {
|
|
336
|
+
const args = parseArgv(argv || []);
|
|
337
|
+
if (args.help) {
|
|
338
|
+
process.stdout.write(HELP_TEXT);
|
|
339
|
+
return 0;
|
|
340
|
+
}
|
|
341
|
+
const mode = args.mode || detectMode({}, env);
|
|
342
|
+
const result = runParallel({
|
|
343
|
+
projectDir: process.cwd(),
|
|
344
|
+
mode,
|
|
345
|
+
milestone: args.milestone,
|
|
346
|
+
domain: args.domain,
|
|
347
|
+
dryRun: args.dryRun,
|
|
348
|
+
env,
|
|
349
|
+
});
|
|
350
|
+
if (args.dryRun) {
|
|
351
|
+
process.stdout.write(formatPlanTable(result.plan));
|
|
352
|
+
process.stdout.write(
|
|
353
|
+
`\nTotal workers: ${result.workerCount} Mode: ${result.mode}` +
|
|
354
|
+
(result.reducedCount != null && result.reducedCount !== result.parallelTasks.length + (result.parallelTasks.length === 0 ? 0 : 0)
|
|
355
|
+
? ` reducedCount: ${result.reducedCount}`
|
|
356
|
+
: "") +
|
|
357
|
+
"\n",
|
|
358
|
+
);
|
|
359
|
+
return 0;
|
|
360
|
+
}
|
|
361
|
+
process.stdout.write(
|
|
362
|
+
`gsd-t parallel — mode=${result.mode} workers=${result.workerCount}\n`,
|
|
363
|
+
);
|
|
364
|
+
return 0;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
module.exports = {
|
|
368
|
+
runParallel,
|
|
369
|
+
runCli,
|
|
370
|
+
formatPlanTable,
|
|
371
|
+
PLAN_HEADER,
|
|
372
|
+
// Exposed for tests:
|
|
373
|
+
_parseArgv: parseArgv,
|
|
374
|
+
_detectMode: detectMode,
|
|
375
|
+
_appendEvent: appendEvent,
|
|
376
|
+
_HELP_TEXT: HELP_TEXT,
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
if (require.main === module) {
|
|
380
|
+
const code = runCli(process.argv.slice(2), process.env);
|
|
381
|
+
process.exit(code);
|
|
382
|
+
}
|