@tekyzinc/gsd-t 4.0.19 → 4.0.20
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 +13 -0
- package/package.json +1 -1
- package/templates/workflows/gsd-t-scan.workflow.js +73 -34
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to GSD-T are documented here. Updated with each release.
|
|
4
4
|
|
|
5
|
+
## [4.0.20] - 2026-06-02 (M73 Scan Concurrency Throttle — patch)
|
|
6
|
+
|
|
7
|
+
### Fixed — unthrottled fan-out triggered an API rate limit that wiped a whole scan
|
|
8
|
+
|
|
9
|
+
A v4.0.19 Hilo run produced an EMPTY register (0 findings) — not a workflow-logic bug: the scan fanned out 29 finders + their verifiers ALL AT ONCE (~58 concurrent Sonnet agents), hit a server-side API rate limit ("temporarily limiting requests · Rate limited"), and all 58 agents errored out empty. (M72's coverage-honesty correctly flagged it: 0/29, pointed to the prior 133-item register — no false "complete".)
|
|
10
|
+
|
|
11
|
+
- `templates/workflows/gsd-t-scan.workflow.js`: added a single GLOBAL counting semaphore (`makeSemaphore`) of 10 permits. EVERY finder + verify agent acquires a permit before running and releases after (`gatedAgent`); freed permits are handed FIFO to the next waiter. All slices + findings still fan out at once, but total in-flight never exceeds 10 — a shared worker-pool: the instant any agent finishes, the next queued one starts, so throughput stays maximal at a safe ceiling. (Finders + verifiers are Sonnet, fine at 10; the lone Opus synthesis runs after, ungated.)
|
|
12
|
+
- This replaces the initial batched approach (which left worker slots idle while a slice serialized its verifies) with a global queue — better throughput AND simpler.
|
|
13
|
+
|
|
14
|
+
Verified by two real sandbox diagnostics: a 30-agent `mapLimit` probe and a 56-agent (8 finders × 6 verifies, fanned out at once) semaphore probe — both measured peakConcurrency = 10, never exceeded.
|
|
15
|
+
|
|
16
|
+
Suite: 1297 pass / 0 fail / 4 skip — zero regressions.
|
|
17
|
+
|
|
5
18
|
## [4.0.19] - 2026-06-02 (M72 Scan Dropped-Slice Recovery + Coverage Honesty — patch)
|
|
6
19
|
|
|
7
20
|
### Fixed — a scan that under-covers no longer presents partial results as complete
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tekyzinc/gsd-t",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.20",
|
|
4
4
|
"description": "GSD-T: Contract-Driven Development for Claude Code — 54 slash commands with headless-by-default workflow spawning, unattended supervisor relay with event stream, graph-powered code analysis, real-time agent dashboard, 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",
|
|
@@ -281,11 +281,47 @@ function finderPrompt(slice) {
|
|
|
281
281
|
`CRITICAL: you MUST return a JSON object matching the schema (slice + findings array) as your FINAL output — even if findings is empty. Do not end without the structured result.`,
|
|
282
282
|
].filter(Boolean).join("\n");
|
|
283
283
|
}
|
|
284
|
+
// M73: GLOBAL CONCURRENCY GATE (shared-worker-pool model). The v4.0.19 Hilo run
|
|
285
|
+
// fanned out 29 finders + their verifiers ALL AT ONCE → ~58 concurrent Sonnet agents
|
|
286
|
+
// → server-side API rate limit → all errored empty → 0 findings. Rather than batch
|
|
287
|
+
// per-slice (which leaves slots idle while a slice serializes its verifies), use ONE
|
|
288
|
+
// global semaphore of MAX_CONCURRENT permits that EVERY agent call (finder OR verify,
|
|
289
|
+
// from any slice) must acquire. Work fans out naturally; the gate alone enforces the
|
|
290
|
+
// cap. The instant any agent finishes, the next queued one starts → always ≈10 in
|
|
291
|
+
// flight while work remains = max throughput at a safe ceiling. (All gated agents are
|
|
292
|
+
// Sonnet; the lone Opus synthesis runs after, ungated.)
|
|
293
|
+
const MAX_CONCURRENT = 10;
|
|
294
|
+
|
|
295
|
+
// Minimal counting semaphore: acquire() resolves when a permit is free; release()
|
|
296
|
+
// hands the permit to the next waiter (FIFO).
|
|
297
|
+
function makeSemaphore(permits) {
|
|
298
|
+
let avail = permits;
|
|
299
|
+
const waiters = [];
|
|
300
|
+
return {
|
|
301
|
+
async acquire() {
|
|
302
|
+
if (avail > 0) { avail--; return; }
|
|
303
|
+
await new Promise((res) => waiters.push(res));
|
|
304
|
+
// resumed: a release() handed us the permit (avail already decremented there).
|
|
305
|
+
},
|
|
306
|
+
release() {
|
|
307
|
+
const next = waiters.shift();
|
|
308
|
+
if (next) next(); // hand permit directly to the next waiter
|
|
309
|
+
else avail++; // no waiter: return permit to the pool
|
|
310
|
+
},
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
const gate = makeSemaphore(MAX_CONCURRENT);
|
|
314
|
+
async function gatedAgent(prompt, opts) {
|
|
315
|
+
await gate.acquire();
|
|
316
|
+
try { return await agent(prompt, opts); }
|
|
317
|
+
finally { gate.release(); }
|
|
318
|
+
}
|
|
319
|
+
|
|
284
320
|
async function runFinder(slice) {
|
|
285
321
|
// up to 2 attempts; a null/invalid (non-array findings) result counts as a drop.
|
|
286
322
|
for (let attempt = 1; attempt <= 2; attempt++) {
|
|
287
323
|
try {
|
|
288
|
-
const r = await
|
|
324
|
+
const r = await gatedAgent(finderPrompt(slice), {
|
|
289
325
|
label: attempt === 1 ? `find:${slice.key}` : `find:${slice.key} (retry)`,
|
|
290
326
|
phase: "Deep Scan", schema: FINDER_SCHEMA, model: "sonnet",
|
|
291
327
|
});
|
|
@@ -298,40 +334,43 @@ async function runFinder(slice) {
|
|
|
298
334
|
return null; // both attempts failed → dropped slice
|
|
299
335
|
}
|
|
300
336
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
// M72: distinguish a FAILED finder (null after retries) from a genuinely-clean slice.
|
|
308
|
-
if (!finderResult || !Array.isArray(finderResult.findings)) {
|
|
309
|
-
return { slice: sliceKey, findings: [], failed: true };
|
|
310
|
-
}
|
|
311
|
-
if (verifyMode === "none" || finderResult.findings.length === 0) {
|
|
312
|
-
return { slice: sliceKey, findings: finderResult.findings || [], failed: false };
|
|
313
|
-
}
|
|
314
|
-
const verified = await parallel(
|
|
315
|
-
finderResult.findings.map((f) => async () => {
|
|
316
|
-
try {
|
|
317
|
-
const v = await agent(
|
|
318
|
-
[
|
|
319
|
-
`You are a VERIFIER for one tech-debt finding in \`${projectDir}\`. Confirm it against the ACTUAL code (open the referenced files with Read) — do not trust the finder.`,
|
|
320
|
-
`Finding: ${JSON.stringify(f)}`,
|
|
321
|
-
`confirmed=true only if the defect genuinely exists. If misread → verdict="false-positive". If real but wrong severity → set correctedSeverity. If real but underspecified → verdict="needs-detail" (kept). Return JSON per the schema.`,
|
|
322
|
-
].join("\n"),
|
|
323
|
-
{ label: `verify:${sliceKey}`, phase: "Deep Scan", schema: VERIFY_SCHEMA, model: "sonnet" }
|
|
324
|
-
);
|
|
325
|
-
if (!v || v.verdict === "false-positive" || v.confirmed === false) return null;
|
|
326
|
-
return { ...f, severity: v.correctedSeverity || f.severity, _verify: v.verdict };
|
|
327
|
-
} catch (e) {
|
|
328
|
-
return { ...f, _verify: "verify-errored" };
|
|
329
|
-
}
|
|
330
|
-
})
|
|
331
|
-
);
|
|
332
|
-
return { slice: sliceKey, findings: verified.filter(Boolean), failed: false };
|
|
337
|
+
async function scanSlice(slice) {
|
|
338
|
+
const sliceKey = slice.key || "unknown-slice";
|
|
339
|
+
const finderResult = await runFinder(slice);
|
|
340
|
+
// M72: distinguish a FAILED finder (null after retries) from a genuinely-clean slice.
|
|
341
|
+
if (!finderResult || !Array.isArray(finderResult.findings)) {
|
|
342
|
+
return { slice: sliceKey, findings: [], failed: true };
|
|
333
343
|
}
|
|
334
|
-
)
|
|
344
|
+
if (verifyMode === "none" || finderResult.findings.length === 0) {
|
|
345
|
+
return { slice: sliceKey, findings: finderResult.findings || [], failed: false };
|
|
346
|
+
}
|
|
347
|
+
// Fan out ALL verifies for this slice — the global gate (not a per-slice limit)
|
|
348
|
+
// bounds total in-flight, so this is safe AND keeps every worker slot busy.
|
|
349
|
+
const verified = await parallel(
|
|
350
|
+
finderResult.findings.map((f) => async () => {
|
|
351
|
+
try {
|
|
352
|
+
const v = await gatedAgent(
|
|
353
|
+
[
|
|
354
|
+
`You are a VERIFIER for one tech-debt finding in \`${projectDir}\`. Confirm it against the ACTUAL code (open the referenced files with Read) — do not trust the finder.`,
|
|
355
|
+
`Finding: ${JSON.stringify(f)}`,
|
|
356
|
+
`confirmed=true only if the defect genuinely exists. If misread → verdict="false-positive". If real but wrong severity → set correctedSeverity. If real but underspecified → verdict="needs-detail" (kept). Return JSON per the schema.`,
|
|
357
|
+
].join("\n"),
|
|
358
|
+
{ label: `verify:${sliceKey}`, phase: "Deep Scan", schema: VERIFY_SCHEMA, model: "sonnet" }
|
|
359
|
+
);
|
|
360
|
+
if (!v || v.verdict === "false-positive" || v.confirmed === false) return null;
|
|
361
|
+
return { ...f, severity: v.correctedSeverity || f.severity, _verify: v.verdict };
|
|
362
|
+
} catch (e) {
|
|
363
|
+
return { ...f, _verify: "verify-errored" };
|
|
364
|
+
}
|
|
365
|
+
})
|
|
366
|
+
);
|
|
367
|
+
return { slice: sliceKey, findings: verified.filter(Boolean), failed: false };
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Fan out ALL slices at once — every finder + verify acquires the shared gate, so
|
|
371
|
+
// total in-flight never exceeds MAX_CONCURRENT regardless of slice/finding counts.
|
|
372
|
+
log(`deep scan: ${slices.length} slices via a shared ${MAX_CONCURRENT}-permit gate (Sonnet finders+verifiers; max throughput at a safe ceiling)`);
|
|
373
|
+
const sliceResults = await parallel(slices.map((slice) => () => scanSlice(slice)));
|
|
335
374
|
|
|
336
375
|
// M72: coverage accounting — a dropped pipeline result (null) OR a failed:true slice
|
|
337
376
|
// is a COVERAGE GAP. Surface it deterministically; never present partial as complete.
|