@tekyzinc/gsd-t 4.0.20 → 4.0.22
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 +31 -0
- package/package.json +1 -1
- package/templates/workflows/gsd-t-scan.workflow.js +229 -66
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,37 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to GSD-T are documented here. Updated with each release.
|
|
4
4
|
|
|
5
|
+
## [4.0.22] - 2026-06-02 (M75 Deterministic Chunked Register Write — patch)
|
|
6
|
+
|
|
7
|
+
### Fixed — synthesis no longer stalls writing a large register
|
|
8
|
+
|
|
9
|
+
The Hilo Scan #14 achieved full coverage + 322 verified findings, but the single synthesis agent STALLED writing the register — it managed 9 of 322 items then ran out of turn/output budget. Diagnostics confirmed even a single bounded `Write` truncates a large register at ~165KB (a 466KB register lost half, mid-item). One agent cannot write a multi-hundred-item register, chunked-in-its-own-head or not.
|
|
10
|
+
|
|
11
|
+
- `templates/workflows/gsd-t-scan.workflow.js`: synthesis redesigned to separate JUDGMENT from WRITING:
|
|
12
|
+
- A bounded **dedup agent** (small input: title+severity+location per finding) returns merge groups — it never holds the full register.
|
|
13
|
+
- The **orchestrator** deterministically merges dups, sorts by severity, assigns sequential TD numbers, and formats the register markdown as a string (no fs, no agent — pure string-building).
|
|
14
|
+
- `fmtChunks` splits the register into **≤30KB chunks that never split an item**; a sequence of bounded write-agents creates chunk 0 (Write) then APPENDS each subsequent chunk. Each agent step is small enough to pass intact → can't stall or truncate, at any register size.
|
|
15
|
+
- `test/m75-chunked-register.test.js`: +4 tests (every item once, contiguous numbering, no mid-item splits, header isolation, no over-chunking).
|
|
16
|
+
|
|
17
|
+
Verified by real sandbox diagnostics: a single Write of a 466KB register truncated to 161/322 items (the bug); the chunked write produced **all 322 items intact, no gaps, no duplicates, no truncation** across 12 chunks.
|
|
18
|
+
|
|
19
|
+
This closes the scan fix chain: M71 (runs in sandbox) + M72 (coverage honesty) + M73 (concurrency cap) + M74 (adaptive throttle) + M75 (deterministic chunked write).
|
|
20
|
+
|
|
21
|
+
Suite: 1306 pass / 0 fail / 4 skip — zero regressions.
|
|
22
|
+
|
|
23
|
+
## [4.0.21] - 2026-06-02 (M74 Adaptive Rate-Limit Throttle — patch)
|
|
24
|
+
|
|
25
|
+
### Added — the scan throttle now self-lowers on a rate limit instead of failing
|
|
26
|
+
|
|
27
|
+
M73's fixed 10-permit gate prevents the all-at-once stampede, but couldn't react if a rate limit still occurred. M74 makes the gate ADAPTIVE: on a rate-limit error (`isRateLimit` matches "temporarily limiting requests", 429, overloaded, etc.) `gatedAgent` lowers the global ceiling by 1 (10→9→8…, floor `MIN_CONCURRENT=4`), backs off (2s/4s/6s), and RETRIES the same agent (up to 4 attempts) — so a transient rate limit throttles the run down rather than failing it. After 8 clean completions the ceiling nudges back up toward 10. A non-rate-limit error is not retried (bubbles up normally).
|
|
28
|
+
|
|
29
|
+
- `templates/workflows/gsd-t-scan.workflow.js`: `makeAdaptiveSemaphore` (shrinkable/recoverable ceiling, never yanks in-flight permits), rate-limit-aware `gatedAgent` with backoff+retry, `isRateLimit`, runtime `sleep`.
|
|
30
|
+
- `test/m74-adaptive-throttle.test.js`: +5 tests (rate-limit detection incl. the real server message; floor/recovery bounds; lowered-ceiling stops granting until in-use drops; **all work completes despite 5 injected rate limits, zero failures**).
|
|
31
|
+
|
|
32
|
+
Verified by 3 real sandbox diagnostics: `setTimeout` resolves in the sandbox (backoff is real); the adaptive gate lowered 10→5 under injected rate limits and completed all 12 items with 0 errors.
|
|
33
|
+
|
|
34
|
+
Suite: 1302 pass / 0 fail / 4 skip — zero regressions.
|
|
35
|
+
|
|
5
36
|
## [4.0.20] - 2026-06-02 (M73 Scan Concurrency Throttle — patch)
|
|
6
37
|
|
|
7
38
|
### Fixed — unthrottled fan-out triggered an API rate limit that wiped a whole scan
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tekyzinc/gsd-t",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.22",
|
|
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",
|
|
@@ -152,26 +152,11 @@ const VERIFY_SCHEMA = {
|
|
|
152
152
|
},
|
|
153
153
|
};
|
|
154
154
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
status: { type: "string", enum: ["written", "failed"] },
|
|
161
|
-
registerPath: { type: "string" },
|
|
162
|
-
archivePath: { type: "string" },
|
|
163
|
-
counts: {
|
|
164
|
-
type: "object",
|
|
165
|
-
required: ["critical", "high", "medium", "low"],
|
|
166
|
-
properties: {
|
|
167
|
-
critical: { type: "integer" }, high: { type: "integer" },
|
|
168
|
-
medium: { type: "integer" }, low: { type: "integer" }, total: { type: "integer" },
|
|
169
|
-
},
|
|
170
|
-
},
|
|
171
|
-
tdRange: { type: "string", description: "e.g. 'TD-01..TD-119'" },
|
|
172
|
-
notes: { type: "string" },
|
|
173
|
-
},
|
|
174
|
-
};
|
|
155
|
+
// M75: synthesis no longer writes the register via one agent (the Hilo Scan #14
|
|
156
|
+
// synthesis stalled after 9 of 322 items typing a 466KB file). Instead: a bounded
|
|
157
|
+
// dedup agent (inline DEDUP_SCHEMA, small input) decides merge groups; the
|
|
158
|
+
// orchestrator deterministically sorts/numbers/formats the register STRING (no fs);
|
|
159
|
+
// a bounded write-agent does ONE Write. Each agent step is bounded → can't stall.
|
|
175
160
|
|
|
176
161
|
const DOC_RESULT_SCHEMA = {
|
|
177
162
|
type: "object",
|
|
@@ -294,28 +279,77 @@ const MAX_CONCURRENT = 10;
|
|
|
294
279
|
|
|
295
280
|
// Minimal counting semaphore: acquire() resolves when a permit is free; release()
|
|
296
281
|
// hands the permit to the next waiter (FIFO).
|
|
297
|
-
|
|
298
|
-
|
|
282
|
+
// M74: ADAPTIVE semaphore — the ceiling can shrink (on a rate limit) and recover.
|
|
283
|
+
// `inUse` = permits currently held; a permit is grantable only while inUse < ceiling.
|
|
284
|
+
// Lowering the ceiling doesn't yank in-flight permits; it just stops granting new
|
|
285
|
+
// ones until enough release that inUse drops below the new ceiling.
|
|
286
|
+
const MIN_CONCURRENT = 4; // never throttle below this — keeps the run moving
|
|
287
|
+
function makeAdaptiveSemaphore(initial) {
|
|
288
|
+
let ceiling = initial;
|
|
289
|
+
let inUse = 0;
|
|
299
290
|
const waiters = [];
|
|
291
|
+
function pump() {
|
|
292
|
+
while (inUse < ceiling && waiters.length) { inUse++; waiters.shift()(); }
|
|
293
|
+
}
|
|
300
294
|
return {
|
|
301
295
|
async acquire() {
|
|
302
|
-
if (
|
|
303
|
-
await new Promise((res) => waiters.push(res));
|
|
304
|
-
|
|
296
|
+
if (inUse < ceiling) { inUse++; return; }
|
|
297
|
+
await new Promise((res) => waiters.push(res)); // pump() increments inUse on grant
|
|
298
|
+
},
|
|
299
|
+
release() { inUse--; pump(); },
|
|
300
|
+
lower() { // shrink ceiling by 1 on a rate limit (floor at MIN_CONCURRENT)
|
|
301
|
+
if (ceiling > MIN_CONCURRENT) { ceiling--; return true; }
|
|
302
|
+
return false;
|
|
305
303
|
},
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
else avail++; // no waiter: return permit to the pool
|
|
304
|
+
raise() { // gentle recovery toward the initial ceiling after sustained success
|
|
305
|
+
if (ceiling < initial) { ceiling++; pump(); return true; }
|
|
306
|
+
return false;
|
|
310
307
|
},
|
|
308
|
+
get ceiling() { return ceiling; },
|
|
311
309
|
};
|
|
312
310
|
}
|
|
313
|
-
const gate =
|
|
311
|
+
const gate = makeAdaptiveSemaphore(MAX_CONCURRENT);
|
|
312
|
+
|
|
313
|
+
function isRateLimit(err) {
|
|
314
|
+
const s = String((err && (err.message || err)) || "").toLowerCase();
|
|
315
|
+
return /rate.?limit|temporarily limiting|429|overloaded|too many requests|capacity/.test(s);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// M74: gatedAgent now reacts to rate limits in REAL TIME — on a rate-limit error it
|
|
319
|
+
// lowers the global ceiling (10→9→8…, floor MIN_CONCURRENT), backs off, and retries
|
|
320
|
+
// the SAME agent (up to a few times) instead of letting it fail. After a streak of
|
|
321
|
+
// clean completions it nudges the ceiling back up. This means a transient rate limit
|
|
322
|
+
// throttles the run down automatically rather than failing it (the v4.0.19 wipeout).
|
|
323
|
+
let _cleanStreak = 0;
|
|
314
324
|
async function gatedAgent(prompt, opts) {
|
|
315
325
|
await gate.acquire();
|
|
316
|
-
try {
|
|
317
|
-
|
|
326
|
+
try {
|
|
327
|
+
for (let attempt = 1; attempt <= 4; attempt++) {
|
|
328
|
+
try {
|
|
329
|
+
const r = await agent(prompt, opts);
|
|
330
|
+
// success: nudge the ceiling back up after every 8 clean completions.
|
|
331
|
+
if (++_cleanStreak >= 8) { _cleanStreak = 0; if (gate.raise()) log(`↑ throttle recovered to ${gate.ceiling} concurrent (clean streak)`); }
|
|
332
|
+
return r;
|
|
333
|
+
} catch (e) {
|
|
334
|
+
if (isRateLimit(e) && attempt < 4) {
|
|
335
|
+
_cleanStreak = 0;
|
|
336
|
+
const lowered = gate.lower();
|
|
337
|
+
const backoffMs = 2000 * attempt; // 2s, 4s, 6s
|
|
338
|
+
log(`⚠ rate limit hit — ${lowered ? `throttling down to ${gate.ceiling} concurrent` : `already at floor ${gate.ceiling}`}; backing off ${backoffMs}ms then retry ${attempt + 1}/4 (${(opts && opts.label) || "agent"})`);
|
|
339
|
+
await sleep(backoffMs);
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
throw e; // non-rate-limit error, or out of retries → bubble up
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
} finally {
|
|
346
|
+
gate.release();
|
|
347
|
+
}
|
|
318
348
|
}
|
|
349
|
+
// Backoff sleep. setTimeout is available in the Workflow sandbox (verified by a
|
|
350
|
+
// real sandbox probe, run wf_7e90a974 — NOT assumed; Date.now/Math.random ARE banned
|
|
351
|
+
// but setTimeout is not). Used only for rate-limit backoff between retries.
|
|
352
|
+
function sleep(ms) { return new Promise((res) => setTimeout(res, ms)); }
|
|
319
353
|
|
|
320
354
|
async function runFinder(slice) {
|
|
321
355
|
// up to 2 attempts; a null/invalid (non-array findings) result counts as a drop.
|
|
@@ -388,42 +422,171 @@ if (!coverageComplete) {
|
|
|
388
422
|
}
|
|
389
423
|
log(`deep scan complete: ${allFindings.length} verified findings across ${succeededCount}/${slices.length} slices${coverageComplete ? " (full coverage)" : " (PARTIAL)"}`);
|
|
390
424
|
|
|
391
|
-
// Synthesis
|
|
392
|
-
//
|
|
393
|
-
//
|
|
425
|
+
// Synthesis (M75 redesign). The Hilo Scan #14 failure: one agent asked to dedup +
|
|
426
|
+
// rank + WRITE a 322-item / 466KB register stalled after 9 items (turn/output budget).
|
|
427
|
+
// Fix: split judgment from writing, and do all the heavy lifting DETERMINISTICALLY.
|
|
428
|
+
// (a) The orchestrator already HAS allFindings as data — it sorts by severity and
|
|
429
|
+
// assigns sequential TD numbers itself (no agent needed for that).
|
|
430
|
+
// (b) Dedup is the only real judgment-add; a bounded agent does ONLY dedup over a
|
|
431
|
+
// compact title+location list (small input) and returns merge groups.
|
|
432
|
+
// (c) The orchestrator formats the full markdown as a STRING (no fs, no agent).
|
|
433
|
+
// (d) A dedicated archive-agent renames the prior register (one mv); a dedicated
|
|
434
|
+
// write-agent does ONE Write of the formatted string. Each is bounded → can't
|
|
435
|
+
// stall regardless of register size.
|
|
394
436
|
phase("Synthesis");
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
`
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
437
|
+
|
|
438
|
+
// (b) Dedup pass — small input (title|severity|first-location per finding), so the
|
|
439
|
+
// agent never holds the full register. It returns groups of indices that are the
|
|
440
|
+
// same underlying issue. Best-effort: on failure we proceed with no dedup.
|
|
441
|
+
const dedupList = allFindings.map((f, i) => `${i}: [${f.severity}] ${f.title} @ ${(f.files && f.files[0]) || "?"}`).join("\n");
|
|
442
|
+
let mergeGroups = [];
|
|
443
|
+
if (allFindings.length > 1) {
|
|
444
|
+
const DEDUP_SCHEMA = {
|
|
445
|
+
type: "object", required: ["groups"], additionalProperties: false,
|
|
446
|
+
properties: { groups: { type: "array", items: { type: "array", items: { type: "integer" } } }, notes: { type: "string" } },
|
|
447
|
+
};
|
|
448
|
+
try {
|
|
449
|
+
const d = await agent(
|
|
450
|
+
[
|
|
451
|
+
`You are DEDUPLICATING tech-debt findings from a multi-slice scan. Below are ${allFindings.length} findings as "index: [SEVERITY] title @ location".`,
|
|
452
|
+
`Different slices sometimes surface the SAME underlying issue. Return "groups": arrays of indices that are the SAME root issue (only group true duplicates — same defect, not merely similar). Singletons need not be listed. If nothing is a duplicate, return groups: [].`,
|
|
453
|
+
``,
|
|
454
|
+
dedupList.slice(0, 120000),
|
|
455
|
+
].join("\n"),
|
|
456
|
+
{ label: "synthesis:dedup", phase: "Synthesis", schema: DEDUP_SCHEMA, model: "opus" }
|
|
457
|
+
);
|
|
458
|
+
mergeGroups = (d && Array.isArray(d.groups)) ? d.groups : [];
|
|
459
|
+
} catch (e) {
|
|
460
|
+
log(`dedup pass failed (non-fatal — proceeding with no dedup): ${e && e.message}`);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// (a)+(c) Deterministically merge dups, sort by severity, assign TD numbers, format.
|
|
465
|
+
const SEV_ORDER = { CRITICAL: 0, HIGH: 1, MEDIUM: 2, LOW: 3 };
|
|
466
|
+
const dropped = new Set();
|
|
467
|
+
const merged = [];
|
|
468
|
+
for (const group of mergeGroups) {
|
|
469
|
+
const idxs = group.filter((i) => Number.isInteger(i) && i >= 0 && i < allFindings.length && !dropped.has(i));
|
|
470
|
+
if (idxs.length < 2) continue;
|
|
471
|
+
const keep = { ...allFindings[idxs[0]] };
|
|
472
|
+
keep.files = [...new Set(idxs.flatMap((i) => allFindings[i].files || []))];
|
|
473
|
+
keep.slice = [...new Set(idxs.map((i) => allFindings[i].slice).filter(Boolean))].join(", ");
|
|
474
|
+
idxs.forEach((i) => dropped.add(i));
|
|
475
|
+
merged.push(keep);
|
|
476
|
+
}
|
|
477
|
+
const finalFindings = [
|
|
478
|
+
...merged,
|
|
479
|
+
...allFindings.map((f, i) => (dropped.has(i) ? null : f)).filter(Boolean).filter((f) => !merged.includes(f)),
|
|
480
|
+
];
|
|
481
|
+
finalFindings.sort((a, b) => (SEV_ORDER[a.severity] ?? 9) - (SEV_ORDER[b.severity] ?? 9));
|
|
482
|
+
const counts = { critical: 0, high: 0, medium: 0, low: 0 };
|
|
483
|
+
for (const f of finalFindings) {
|
|
484
|
+
if (f.severity === "CRITICAL") counts.critical++;
|
|
485
|
+
else if (f.severity === "HIGH") counts.high++;
|
|
486
|
+
else if (f.severity === "MEDIUM") counts.medium++;
|
|
487
|
+
else if (f.severity === "LOW") counts.low++;
|
|
425
488
|
}
|
|
426
|
-
|
|
489
|
+
counts.total = finalFindings.length;
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
// M75 chunked formatter: returns an ARRAY of markdown chunks, each ≤ ~30KB, so each
|
|
493
|
+
// can be written through one bounded agent prompt WITHOUT truncation (a single write
|
|
494
|
+
// of a 466KB register truncates at ~165KB — verified). Chunk 0 is the header+summary
|
|
495
|
+
// (Write/create); the rest are appends. Items are grouped so a chunk never splits an
|
|
496
|
+
// item, and the severity-section heading rides with its first item's chunk.
|
|
497
|
+
function fmtChunks(today) {
|
|
498
|
+
const sevHead = { CRITICAL: "🔴 Critical", HIGH: "🟠 High", MEDIUM: "🟡 Medium", LOW: "🟢 Low" };
|
|
499
|
+
const head = [];
|
|
500
|
+
head.push(`# Tech Debt Register — ${projectDir.split("/").pop()}`, "");
|
|
501
|
+
if (scanNumber) head.push(`**Scan #${scanNumber}** — Deep codebase scan (runtime-native, ${coverageComplete ? "full coverage" : "PARTIAL coverage"})`);
|
|
502
|
+
head.push(`**Date:** ${today}`);
|
|
503
|
+
head.push(`**Slices run:** ${slices.length} | **Coverage:** ${coverageComplete ? `FULL — all ${slices.length} slices succeeded` : `PARTIAL — ${succeededCount}/${slices.length} succeeded`}`);
|
|
504
|
+
head.push(`**Verified findings:** ${counts.total}`, "");
|
|
505
|
+
head.push(`> Effort estimates use GSD-T-native units (domain / wave / spawn / token-spend). Never human-hours.`);
|
|
506
|
+
head.push(`> TD numbering continues from the prior register (if any, archived). This scan begins at **TD-${tdStart}**.`, "");
|
|
507
|
+
if (!coverageComplete) head.push(`> ⚠ **PARTIAL COVERAGE — ${failedSlices.length} of ${slices.length} codebase areas were NOT scanned this pass** (failed to return findings): ${failedSlices.join(", ")}. Findings UNDER-COUNT the real debt. Re-run (resume) for full coverage.`, "");
|
|
508
|
+
head.push(`## Summary`, "", `| Severity | Count |`, `|----------|-------|`,
|
|
509
|
+
`| 🔴 CRITICAL | ${counts.critical} |`, `| 🟠 HIGH | ${counts.high} |`,
|
|
510
|
+
`| 🟡 MEDIUM | ${counts.medium} |`, `| 🟢 LOW | ${counts.low} |`,
|
|
511
|
+
`| **Total** | **${counts.total}** |`, "", "---", "");
|
|
512
|
+
|
|
513
|
+
function itemMd(f, td) {
|
|
514
|
+
const L = [`### TD-${td} — ${f.title || "(untitled)"}`,
|
|
515
|
+
`- **Area:** ${f.area || "—"}`, `- **Severity:** ${f.severity}`, `- **Status:** OPEN`,
|
|
516
|
+
`- **Location:** ${(f.files && f.files.length) ? f.files.join(", ") : "—"}`];
|
|
517
|
+
if (f.detail) L.push(`- **Description:** ${f.detail}`);
|
|
518
|
+
if (f.impact) L.push(`- **Impact:** ${f.impact}`);
|
|
519
|
+
if (f.recommendation) L.push(`- **Remediation:** ${f.recommendation}`);
|
|
520
|
+
if (f.slice) L.push(`- **Found in slice:** ${f.slice}`);
|
|
521
|
+
L.push("");
|
|
522
|
+
return L.join("\n");
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const CHUNK_MAX = 30000;
|
|
526
|
+
const chunks = [head.join("\n")];
|
|
527
|
+
let buf = "", n = tdStart, lastSev = null;
|
|
528
|
+
const flush = () => { if (buf) { chunks.push(buf); buf = ""; } };
|
|
529
|
+
for (const f of finalFindings) {
|
|
530
|
+
let piece = "";
|
|
531
|
+
if (f.severity !== lastSev) { piece += `\n## ${sevHead[f.severity] || f.severity} Priority\n\n`; lastSev = f.severity; }
|
|
532
|
+
piece += itemMd(f, n++);
|
|
533
|
+
if (buf.length + piece.length > CHUNK_MAX) flush();
|
|
534
|
+
buf += piece;
|
|
535
|
+
}
|
|
536
|
+
flush();
|
|
537
|
+
return { chunks, lastTd: n - 1 };
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// (d) Archive the prior register (one bounded agent: mv/git mv), then write the new
|
|
541
|
+
// one (one bounded agent: a SINGLE Write of the pre-formatted string).
|
|
542
|
+
const todayAgent = await agent(
|
|
543
|
+
`Run \`date +%F\` via Bash and return ONLY the date string (YYYY-MM-DD), nothing else.`,
|
|
544
|
+
{ label: "synthesis:date", phase: "Synthesis", model: "haiku" }
|
|
545
|
+
).catch(() => null);
|
|
546
|
+
const today = (typeof todayAgent === "string" && /\d{4}-\d{2}-\d{2}/.test(todayAgent)) ? todayAgent.match(/\d{4}-\d{2}-\d{2}/)[0] : "today";
|
|
547
|
+
|
|
548
|
+
let archivePath = "";
|
|
549
|
+
if (pre.priorRegisterExists) {
|
|
550
|
+
const arch = await agent(
|
|
551
|
+
[
|
|
552
|
+
`Archive the existing tech-debt register in \`${projectDir}\` via Bash, then report the archive path.`,
|
|
553
|
+
`Rename \`${projectDir}/.gsd-t/techdebt.md\` to \`${projectDir}/.gsd-t/techdebt_${today}.md\`; if that exists, append _2/_3. Use \`git mv\` if a git repo else \`mv\`. Reply with ONLY the resulting archive filename.`,
|
|
554
|
+
].join("\n"),
|
|
555
|
+
{ label: "synthesis:archive", phase: "Synthesis", model: "haiku" }
|
|
556
|
+
).catch(() => null);
|
|
557
|
+
archivePath = (typeof arch === "string") ? arch.trim().split("\n").pop() : "";
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const { chunks, lastTd } = fmtChunks(today);
|
|
561
|
+
// M75: write the register in BOUNDED CHUNKS (≤30KB each). A single write of a large
|
|
562
|
+
// register truncates at ~165KB (verified) — so chunk 0 creates the file (Write), and
|
|
563
|
+
// each subsequent chunk is APPENDED. Done SEQUENTIALLY so the file builds in order and
|
|
564
|
+
// each agent's prompt+output stays small enough to pass intact. The register path is
|
|
565
|
+
// passed to each chunk; only the chunk content varies.
|
|
566
|
+
const regPath = `${projectDir}/.gsd-t/techdebt.md`;
|
|
567
|
+
let chunkOk = 0;
|
|
568
|
+
for (let ci = 0; ci < chunks.length; ci++) {
|
|
569
|
+
const isFirst = ci === 0;
|
|
570
|
+
const res = await agent(
|
|
571
|
+
[
|
|
572
|
+
isFirst
|
|
573
|
+
? `Create the file \`${regPath}\` (overwrite if it exists) with EXACTLY the content between the markers below, using the Write tool. Verbatim — no edits, no summarizing, no truncation.`
|
|
574
|
+
: `APPEND EXACTLY the content between the markers below to the END of \`${regPath}\` (do not overwrite existing content — append). Verbatim — no edits, no truncation. Use a Bash heredoc append (\`cat >> ${regPath} <<'GSDTEOF'\` … \`GSDTEOF\`) or read-then-Write-concatenation; preserve the file's existing content exactly.`,
|
|
575
|
+
`This is chunk ${ci + 1}/${chunks.length} of a tech-debt register. After writing, reply with ONLY "OK".`,
|
|
576
|
+
``,
|
|
577
|
+
`<<<CHUNK>>>`,
|
|
578
|
+
chunks[ci],
|
|
579
|
+
`<<<END_CHUNK>>>`,
|
|
580
|
+
].join("\n"),
|
|
581
|
+
{ label: `synthesis:write-register ${ci + 1}/${chunks.length}`, phase: "Synthesis", model: "haiku" }
|
|
582
|
+
).catch((e) => ({ _err: String(e && e.message) }));
|
|
583
|
+
if (typeof res === "string" && /ok/i.test(res)) chunkOk++;
|
|
584
|
+
else log(`⚠ register chunk ${ci + 1}/${chunks.length} write uncertain: ${typeof res === "string" ? res.slice(0, 60) : JSON.stringify(res).slice(0, 80)}`);
|
|
585
|
+
}
|
|
586
|
+
log(`register written in ${chunkOk}/${chunks.length} chunks (${counts.total} findings, TD-${tdStart}..TD-${lastTd})`);
|
|
587
|
+
|
|
588
|
+
const synthesis = { status: "written", counts, archivePath, tdRange: `TD-${tdStart}..TD-${lastTd}`, finalFindings };
|
|
589
|
+
log(`register written: ${JSON.stringify(synthesis.counts)} (${synthesis.tdRange})`);
|
|
427
590
|
|
|
428
591
|
// Document — per-doc fan-out. Each agent writes its file via Write/Edit (its tools).
|
|
429
592
|
// The orchestrator passes the findings + (for plain-english) tells the agent to
|