@tekyzinc/gsd-t 4.0.17 → 4.0.19
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 +37 -0
- package/package.json +1 -1
- package/templates/workflows/gsd-t-scan.workflow.js +325 -585
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,43 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to GSD-T are documented here. Updated with each release.
|
|
4
4
|
|
|
5
|
+
## [4.0.19] - 2026-06-02 (M72 Scan Dropped-Slice Recovery + Coverage Honesty — patch)
|
|
6
|
+
|
|
7
|
+
### Fixed — a scan that under-covers no longer presents partial results as complete
|
|
8
|
+
|
|
9
|
+
The first real Hilo run (v4.0.18) surfaced 133 findings but 7 of 19 deep-finder slices had silently FAILED — they returned no schema-valid output (the runtime nudged them twice then dropped them), and the workflow treated a dropped slice identically to a genuinely-clean one (`findings: []`). The register confidently presented ~⅔ coverage as complete. For a quality tool, silent partial coverage is worse than a shallow scan.
|
|
10
|
+
|
|
11
|
+
- `templates/workflows/gsd-t-scan.workflow.js`:
|
|
12
|
+
- **Retry**: each finder is now retried once on a null/invalid result (`runFinder`).
|
|
13
|
+
- **Detect, don't conflate**: a slice that still fails is flagged `failed:true` — never merged with an empty-but-successful slice. Coverage accounting computes `failedSlices`, `slicesSucceeded`, `coverageComplete` deterministically; dropped slices' (absent) findings are excluded from the count.
|
|
14
|
+
- **Surface loudly**: synthesis is REQUIRED (not relying on the agent's notice) to put a "⚠ PARTIAL COVERAGE — N of M areas not scanned" banner at the top of the register and list the un-scanned slices. The workflow return status downgrades to `"complete-partial-coverage"` (not `"complete"`) and includes `slicesFailed[]`.
|
|
15
|
+
- **Synthesis robustness**: instructed to write the register INCREMENTALLY (header+summary, then append each severity section) so a multi-hundred-item register can't stall on one giant Write; synthesis-input truncation raised 200KB→500KB.
|
|
16
|
+
- `test/m72-coverage-accounting.test.js`: +4 tests (dropped slice excluded + flagged; clean slice not a gap; null pipeline result = failed; full coverage = complete). Coverage logic also verified by a real sandbox diagnostic run.
|
|
17
|
+
|
|
18
|
+
Effect: a scan with any failed slice is clearly marked incomplete; resuming the run re-scans only the failed slices (cached successes are reused).
|
|
19
|
+
|
|
20
|
+
Suite: 1297 pass / 0 fail / 4 skip — zero regressions.
|
|
21
|
+
|
|
22
|
+
## [4.0.18] - 2026-06-02 (M71 Runtime-Native Scan Workflow — patch)
|
|
23
|
+
|
|
24
|
+
### Fixed — the scan Workflow now actually RUNS in the Workflow sandbox (it never did)
|
|
25
|
+
|
|
26
|
+
The M61→M67 "native Workflow migration" was verified only with `node --check` (syntax) and never executed in the real Anthropic Workflow runtime. It never worked: every invocation crashed and the agent silently fell back to a hand-driven scan (the cause of every shallow/incomplete scan in this saga). Root causes, all found via real-sandbox runs + a 1-agent diagnostic:
|
|
27
|
+
|
|
28
|
+
- **`require`/`fs`/`spawnSync` banned**: the sandbox exposes only `agent/parallel/pipeline/log/phase/budget/args`. `require("./_lib.js")` etc. threw `ReferenceError: require is not defined`. **Fix:** re-architected `gsd-t-scan.workflow.js` to be runtime-native — the orchestrator does ZERO file I/O; all reads/writes/archive/git happen INSIDE subagents (which have Bash/Read/Write tools). `bin/_lib`, preflight-as-spawnSync, and the bin/scan-*.js render shell-outs are gone.
|
|
29
|
+
- **`args` arrives as a JSON STRING, not an object**: `args.projectDir` was always `undefined` → defaulted to `"."` → agents scanned the package CWD instead of the target. **Fix:** `JSON.parse(args)` normalization at the top (verified by diagnostic run wf_6934cc1a).
|
|
30
|
+
- **Runaway fan-out**: the probe over-sliced (a 5-file repo → ~20 slices/44 agents; the GSD-T repo → 191) — prose alone never bounded it. **Fix:** slices redefined as cohesive sub-domains/responsibilities (not per-file/per-module), AND a volume-derived backstop cap (`computeSliceCap`) deterministically truncates the fan-out (tiny→3, mid→10, Hilo-scale→~27, huge→50 ceiling). The probe decides the count by structure WITHIN the cap.
|
|
31
|
+
- **HTML render stage removed**: `bin/scan-report.js` resolved its output to the package dir and overwrote the package's own report (data-loss). The register + 5 dimension files + plain-english + living docs are the authoritative deliverables; the fragile report was dropped.
|
|
32
|
+
- **`projectDir` pinned**: probe + finder prompts now hard-direct agents to operate only under the target path.
|
|
33
|
+
|
|
34
|
+
- `test/m71-workflow-runtime-native-lint.test.js`: mechanical lint failing any `*.workflow.js` that uses require/fs/child_process/spawnSync. `test/m71-slice-cap-algorithm.test.js`: locks the cap calibration.
|
|
35
|
+
|
|
36
|
+
**Acceptance**: verified by an actual sandbox run (wf_da75f310) — `status: complete`, 3 slices (cap held), 22 findings (all planted issues caught), 11 docs + 5 dimension files + plain-english all written to the correct target, committed in-target, GSD-T repo unpolluted. `node --check` is no longer accepted as sufficient — runtime-native workflows must be RUN to completion in the sandbox before shipping.
|
|
37
|
+
|
|
38
|
+
**Note**: only `gsd-t-scan` is migrated to runtime-native. The other 7 workflows (`execute/verify/wave/integrate/debug/phase/quick`) still use `require`/`fs` and will crash identically in the sandbox — they need the same treatment (follow-up).
|
|
39
|
+
|
|
40
|
+
Suite: 1293 pass / 0 fail / 4 skip — zero regressions.
|
|
41
|
+
|
|
5
42
|
## [4.0.17] - 2026-06-02 (M70 Workflow Invocation Guard — patch)
|
|
6
43
|
|
|
7
44
|
### Fixed — workflow commands no longer get hand-driven instead of invoking the Workflow
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tekyzinc/gsd-t",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.19",
|
|
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",
|
|
@@ -1,77 +1,98 @@
|
|
|
1
1
|
// templates/workflows/gsd-t-scan.workflow.js
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
3
|
+
// RUNTIME: Anthropic native Workflow tool ONLY. The sandbox exposes EXACTLY these
|
|
4
|
+
// globals: agent(), parallel(), pipeline(), log(), phase(), budget, args.
|
|
5
|
+
// It does NOT provide require / module / fs / path / child_process / process.
|
|
6
|
+
// Using any of those throws `ReferenceError: require is not defined` at runtime —
|
|
7
|
+
// the bug (M71) that made every GSD-T workflow silently fail and fall back to a
|
|
8
|
+
// hand-driven scan. `node --check` validates syntax only and CANNOT catch this;
|
|
9
|
+
// `test/m71-workflow-runtime-native-lint.test.js` is the mechanical guard, and an
|
|
10
|
+
// actual sandbox run is the acceptance gate.
|
|
6
11
|
//
|
|
7
|
-
//
|
|
12
|
+
// THE ARCHITECTURE (M71): the orchestrator body does NO file I/O. It only sequences
|
|
13
|
+
// phases and threads data (as strings) between agents. ALL reads, writes, archive,
|
|
14
|
+
// git, and counting happen INSIDE subagents — they have Bash/Read/Write/Grep tools.
|
|
15
|
+
// `projectDir` is passed into prompts so each agent operates on the right tree.
|
|
8
16
|
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
// volume scaling — a 5-file repo and a 1,809-file repo both got 5 agents. A single
|
|
13
|
-
// `quality` agent asked to cover dead-code+dup+complexity+errors+perf+test-gaps
|
|
14
|
-
// across the whole codebase samples ~5 issues and stops. That produced a cursory
|
|
15
|
-
// 16-item register on a codebase whose deep scan surfaced 117 findings.
|
|
16
|
-
// It also referenced a retired `autoSpawnHeadless()` + `headless-default-contract
|
|
17
|
-
// v2.0.0` that no longer exist post-M61/M65.
|
|
18
|
-
//
|
|
19
|
-
// THE FIX: fan out by codebase VOLUME, not by a fixed dimension count.
|
|
20
|
-
// preflight → volume-probe (derive per-AREA slice list) →
|
|
21
|
-
// pipeline( slice → deep-finder "enumerate, do not sample" → single verify ) →
|
|
22
|
-
// archive prior register → synthesis (dedup/merge/re-rank, continue TD numbering) →
|
|
23
|
-
// deterministic bin/scan-*.js stages (schema / diagrams / HTML report).
|
|
24
|
-
//
|
|
25
|
-
// The number of finders scales with the slice list the probe derives, and slice
|
|
26
|
-
// DEPTH scales with budget.total when a turn target is set. KEEPS the brains:
|
|
27
|
-
// preflight + the deterministic bin/scan-*.js renderers.
|
|
17
|
+
// preflight(agent) → volume-probe(agent) → pipeline(deep-finder → single verify)
|
|
18
|
+
// → synthesis(agent: archive + write register + git) → document(parallel per-doc
|
|
19
|
+
// agents: living docs + 5 dimension files + plain-english) → render(agent: HTML).
|
|
28
20
|
//
|
|
29
21
|
// args shape:
|
|
30
|
-
// {
|
|
31
|
-
//
|
|
32
|
-
// scanNumber: 12, // optional — for the register header
|
|
33
|
-
// maxSlicesHint: 40, // optional — soft cap on derived slices
|
|
34
|
-
// verify: "single", // optional — "single" (default) | "none"
|
|
35
|
-
// }
|
|
22
|
+
// { projectDir: ".", scanNumber?: 13, verify?: "single"|"none" }
|
|
23
|
+
// (no slice cap — the probe's cohesive-sub-domain decomposition is the slice set.)
|
|
36
24
|
|
|
37
25
|
export const meta = {
|
|
38
26
|
name: "gsd-t-scan",
|
|
39
27
|
description:
|
|
40
|
-
"GSD-T scan
|
|
28
|
+
"GSD-T scan (runtime-native): preflight → volume-probe → pipeline(deep-finder per slice → single verify) → synthesis(archive+register) → document(living docs + 5 dimension files + plain-english) → render. Orchestrator does NO fs/require; all I/O is inside subagents. Fans out by codebase volume.",
|
|
41
29
|
phases: [
|
|
42
|
-
{ title: "Preflight",
|
|
43
|
-
{ title: "Probe",
|
|
44
|
-
{ title: "Deep Scan",
|
|
45
|
-
{ title: "Synthesis",
|
|
46
|
-
{ title: "Document",
|
|
47
|
-
{ title: "Render",
|
|
30
|
+
{ title: "Preflight", detail: "branch + prior-register check (agent via Bash)" },
|
|
31
|
+
{ title: "Probe", detail: "volume probe → per-area slice list", model: "sonnet" },
|
|
32
|
+
{ title: "Deep Scan", detail: "pipeline: per-slice deep finder → single verify" },
|
|
33
|
+
{ title: "Synthesis", detail: "archive prior + write fresh register + git", model: "opus" },
|
|
34
|
+
{ title: "Document", detail: "living docs + 5 dimension files + plain-english (per-doc fan-out)" },
|
|
35
|
+
{ title: "Render", detail: "HTML scan report via gsd-t bin renderers (agent via Bash)" },
|
|
48
36
|
],
|
|
49
37
|
};
|
|
50
38
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
39
|
+
// CRITICAL (M71): the Workflow runtime passes `args` as a JSON STRING, not a parsed
|
|
40
|
+
// object — verified by diagnostic run wf_6934cc1a-537 (typeof args === "string").
|
|
41
|
+
// Reading `args.projectDir` directly yields undefined → projectDir defaults to "."
|
|
42
|
+
// → agents scan the GSD-T package CWD instead of the target, and maxSlicesHint is
|
|
43
|
+
// undefined → the slice cap no-ops → runaway 150+ finder fan-out. Normalize FIRST.
|
|
44
|
+
const _args = (typeof args === "string")
|
|
45
|
+
? (() => { try { return JSON.parse(args); } catch (_) { return {}; } })()
|
|
46
|
+
: (args || {});
|
|
47
|
+
const projectDir = _args.projectDir || ".";
|
|
48
|
+
const scanNumber = _args.scanNumber || null;
|
|
49
|
+
const verifyMode = _args.verify || "single"; // "single" | "none"
|
|
50
|
+
const maxSlicesOverride = _args.maxSlicesHint || null; // optional power-user ceiling override
|
|
51
|
+
|
|
52
|
+
// VOLUME-DERIVED CAP — a RUNAWAY BACKSTOP, not the target count. The probe decides
|
|
53
|
+
// the actual slice count by cohesive sub-domain WITHIN this cap; the cap only fires
|
|
54
|
+
// when the probe over-slices. EVIDENCE it's necessary: with no cap, the probe sliced
|
|
55
|
+
// a 5-FILE repo into ~20 slices → 44 agents (run wf_9c993376-097), and the GSD-T repo
|
|
56
|
+
// into 191 — the slice-definition prose alone does NOT keep it bounded (same lesson as
|
|
57
|
+
// M70: prose the agent can ignore isn't enforcement). So the count is structure-driven
|
|
58
|
+
// but HARD-capped. Calibrated as a CEILING (the probe normally lands under it):
|
|
59
|
+
// tiny(5 files)→3, mid(300)→10, Hilo(1809f/~1M LOC/150 routes/361 tables)→~27,
|
|
60
|
+
// huge(10k files)→50. _args.maxSlicesHint optionally overrides. Tune here.
|
|
61
|
+
function computeSliceCap(t) {
|
|
62
|
+
const files = Number(t && (t.files || t.total_files || t.source_files)) || 0;
|
|
63
|
+
const loc = Number(t && (t.loc || t.lines_of_code || t.totalLoc)) || 0;
|
|
64
|
+
const routes = Number(t && (t.routes || t.route_modules)) || 0;
|
|
65
|
+
const tables = Number(t && (t.tables || t.orm_tables)) || 0;
|
|
66
|
+
const components = Number(t && t.components) || 0;
|
|
67
|
+
const domains = Number(t && (t.featureDomains || t.feature_domains)) || 0;
|
|
68
|
+
const fileSignal = Math.sqrt(files) * 0.42;
|
|
69
|
+
const structSignal = domains + Math.round(routes / 70) + Math.round(tables / 200) + Math.round(components / 250);
|
|
70
|
+
const locSignal = Math.sqrt(loc) / 800;
|
|
71
|
+
const raw = fileSignal * 0.7 + structSignal * 0.8 + locSignal;
|
|
72
|
+
return Math.max(3, Math.min(50, Math.round(raw)));
|
|
73
|
+
}
|
|
55
74
|
|
|
56
|
-
|
|
57
|
-
const scanNumber = (args && args.scanNumber) || null;
|
|
58
|
-
const maxSlicesHint = (args && args.maxSlicesHint) || 40;
|
|
59
|
-
const verifyMode = (args && args.verify) || "single"; // "single" | "none"
|
|
75
|
+
// ───── Schemas (pure data — no runtime dependency) ──────────────────────────
|
|
60
76
|
|
|
61
|
-
|
|
77
|
+
const PREFLIGHT_SCHEMA = {
|
|
78
|
+
type: "object",
|
|
79
|
+
required: ["ok", "branch", "priorRegisterExists"],
|
|
80
|
+
additionalProperties: false,
|
|
81
|
+
properties: {
|
|
82
|
+
ok: { type: "boolean" },
|
|
83
|
+
branch: { type: "string" },
|
|
84
|
+
priorRegisterExists: { type: "boolean" },
|
|
85
|
+
priorMaxTd: { type: "integer", description: "highest TD-NNN in the prior register, 0 if none" },
|
|
86
|
+
notes: { type: "string" },
|
|
87
|
+
},
|
|
88
|
+
};
|
|
62
89
|
|
|
63
|
-
// Probe output: a list of slices to fan out over. Each slice is one narrow area
|
|
64
|
-
// of the codebase a single deep-finder agent can exhaustively own.
|
|
65
90
|
const PROBE_SCHEMA = {
|
|
66
91
|
type: "object",
|
|
67
92
|
required: ["totals", "slices"],
|
|
68
93
|
additionalProperties: false,
|
|
69
94
|
properties: {
|
|
70
|
-
totals: {
|
|
71
|
-
type: "object",
|
|
72
|
-
additionalProperties: true,
|
|
73
|
-
description: "Headline counts: files, routes, tables, components, testFiles, topLevelDirs.",
|
|
74
|
-
},
|
|
95
|
+
totals: { type: "object", additionalProperties: true },
|
|
75
96
|
slices: {
|
|
76
97
|
type: "array",
|
|
77
98
|
minItems: 1,
|
|
@@ -80,13 +101,10 @@ const PROBE_SCHEMA = {
|
|
|
80
101
|
required: ["key", "paths", "dimension"],
|
|
81
102
|
additionalProperties: false,
|
|
82
103
|
properties: {
|
|
83
|
-
key: { type: "string"
|
|
84
|
-
paths: { type: "array", items: { type: "string" }
|
|
85
|
-
dimension: {
|
|
86
|
-
|
|
87
|
-
enum: ["architecture", "business-rules", "security", "quality", "contracts", "feature-domain", "data-layer", "api-surface", "testing"],
|
|
88
|
-
},
|
|
89
|
-
why: { type: "string", description: "what makes this slice worth a dedicated deep finder" },
|
|
104
|
+
key: { type: "string" },
|
|
105
|
+
paths: { type: "array", items: { type: "string" } },
|
|
106
|
+
dimension: { type: "string", enum: ["architecture", "business-rules", "security", "quality", "contracts", "feature-domain", "data-layer", "api-surface", "testing"] },
|
|
107
|
+
why: { type: "string" },
|
|
90
108
|
},
|
|
91
109
|
},
|
|
92
110
|
},
|
|
@@ -109,7 +127,7 @@ const FINDER_SCHEMA = {
|
|
|
109
127
|
properties: {
|
|
110
128
|
title: { type: "string" },
|
|
111
129
|
severity: { type: "string", enum: ["CRITICAL", "HIGH", "MEDIUM", "LOW"] },
|
|
112
|
-
area: { type: "string"
|
|
130
|
+
area: { type: "string" },
|
|
113
131
|
files: { type: "array", items: { type: "string" } },
|
|
114
132
|
detail: { type: "string" },
|
|
115
133
|
impact: { type: "string" },
|
|
@@ -127,628 +145,350 @@ const VERIFY_SCHEMA = {
|
|
|
127
145
|
required: ["confirmed", "verdict"],
|
|
128
146
|
additionalProperties: false,
|
|
129
147
|
properties: {
|
|
130
|
-
confirmed:
|
|
131
|
-
verdict:
|
|
132
|
-
note:
|
|
148
|
+
confirmed: { type: "boolean" },
|
|
149
|
+
verdict: { type: "string", enum: ["confirmed", "false-positive", "needs-detail"] },
|
|
150
|
+
note: { type: "string" },
|
|
133
151
|
correctedSeverity: { type: "string", enum: ["CRITICAL", "HIGH", "MEDIUM", "LOW"] },
|
|
134
152
|
},
|
|
135
153
|
};
|
|
136
154
|
|
|
137
|
-
const DOC_RESULT_SCHEMA = {
|
|
138
|
-
type: "object",
|
|
139
|
-
required: ["doc", "status", "path"],
|
|
140
|
-
additionalProperties: false,
|
|
141
|
-
properties: {
|
|
142
|
-
doc: { type: "string", description: "logical doc id, e.g. 'docs/architecture.md'" },
|
|
143
|
-
status: { type: "string", enum: ["written", "merged", "skipped", "failed"] },
|
|
144
|
-
path: { type: "string" },
|
|
145
|
-
bytes: { type: "integer" },
|
|
146
|
-
notes: { type: "string" },
|
|
147
|
-
},
|
|
148
|
-
};
|
|
149
|
-
|
|
150
155
|
const SYNTHESIS_SCHEMA = {
|
|
151
156
|
type: "object",
|
|
152
|
-
required: ["status", "
|
|
157
|
+
required: ["status", "counts"],
|
|
153
158
|
additionalProperties: false,
|
|
154
159
|
properties: {
|
|
155
160
|
status: { type: "string", enum: ["written", "failed"] },
|
|
156
161
|
registerPath: { type: "string" },
|
|
162
|
+
archivePath: { type: "string" },
|
|
157
163
|
counts: {
|
|
158
164
|
type: "object",
|
|
159
165
|
required: ["critical", "high", "medium", "low"],
|
|
160
166
|
properties: {
|
|
161
|
-
critical: { type: "integer" },
|
|
162
|
-
|
|
163
|
-
medium: { type: "integer" },
|
|
164
|
-
low: { type: "integer" },
|
|
165
|
-
total: { type: "integer" },
|
|
167
|
+
critical: { type: "integer" }, high: { type: "integer" },
|
|
168
|
+
medium: { type: "integer" }, low: { type: "integer" }, total: { type: "integer" },
|
|
166
169
|
},
|
|
167
170
|
},
|
|
168
|
-
|
|
169
|
-
notes:
|
|
171
|
+
tdRange: { type: "string", description: "e.g. 'TD-01..TD-119'" },
|
|
172
|
+
notes: { type: "string" },
|
|
170
173
|
},
|
|
171
174
|
};
|
|
172
175
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
176
|
+
const DOC_RESULT_SCHEMA = {
|
|
177
|
+
type: "object",
|
|
178
|
+
required: ["doc", "status"],
|
|
179
|
+
additionalProperties: false,
|
|
180
|
+
properties: {
|
|
181
|
+
doc: { type: "string" },
|
|
182
|
+
status: { type: "string", enum: ["written", "merged", "skipped", "failed"] },
|
|
183
|
+
path: { type: "string" },
|
|
184
|
+
notes: { type: "string" },
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const RENDER_SCHEMA = {
|
|
189
|
+
type: "object",
|
|
190
|
+
required: ["status"],
|
|
191
|
+
additionalProperties: false,
|
|
192
|
+
properties: {
|
|
193
|
+
status: { type: "string", enum: ["rendered", "skipped", "failed"] },
|
|
194
|
+
outputPath: { type: "string" },
|
|
195
|
+
notes: { type: "string" },
|
|
196
|
+
},
|
|
197
|
+
};
|
|
187
198
|
|
|
188
|
-
// ───── Script body
|
|
199
|
+
// ───── Script body — orchestration only, ZERO file I/O here ─────────────────
|
|
189
200
|
|
|
201
|
+
// Preflight: an agent checks branch + whether a prior register exists, via Bash.
|
|
202
|
+
// (No fs in the body — that was the bug.)
|
|
190
203
|
phase("Preflight");
|
|
191
|
-
const pre =
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
204
|
+
const pre = await agent(
|
|
205
|
+
[
|
|
206
|
+
`You are the preflight check for a GSD-T deep scan of the project at \`${projectDir}\`.`,
|
|
207
|
+
`Using Bash/Read tools, determine:`,
|
|
208
|
+
`1. The current git branch (\`git -C ${projectDir} rev-parse --abbrev-ref HEAD\`; if not a git repo, report branch "(no-git)").`,
|
|
209
|
+
`2. Whether \`${projectDir}/.gsd-t/techdebt.md\` exists (priorRegisterExists).`,
|
|
210
|
+
`3. If it exists, the HIGHEST TD-NNN number in it (grep \`### TD-\`, parse the max integer; priorMaxTd). If absent, priorMaxTd=0.`,
|
|
211
|
+
`Set ok=true unless something makes scanning impossible (e.g. projectDir does not exist). Return JSON per the schema.`,
|
|
212
|
+
].join("\n"),
|
|
213
|
+
{ label: "preflight", phase: "Preflight", schema: PREFLIGHT_SCHEMA, model: "haiku" }
|
|
214
|
+
);
|
|
215
|
+
if (!pre || !pre.ok) {
|
|
216
|
+
log(`preflight failed — halting. notes: ${pre && pre.notes}`);
|
|
217
|
+
return { status: "failed", reason: "preflight-failed", preflight: pre };
|
|
195
218
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
// Load prior register (for dedup + TD-numbering continuation).
|
|
199
|
-
// Red-team fix (CRITICAL-1/CRITICAL-2/MEDIUM-1): the archive + TD-numbering must be
|
|
200
|
-
// DETERMINISTIC JS, not LLM prose — an agent told to "archive then rewrite" can
|
|
201
|
-
// clobber the register without archiving, and an agent told to "continue numbering"
|
|
202
|
-
// with no data hallucinates a start number → colliding TD IDs. So we do the rename
|
|
203
|
-
// here (collision-safe), parse the max TD number, and PASS the prior content into
|
|
204
|
-
// synthesis. This mirrors verify.workflow.js, where every destructive gate is
|
|
205
|
-
// deterministic code, not an agent instruction.
|
|
206
|
-
let priorRegister = null;
|
|
207
|
-
let priorArchivePath = null;
|
|
208
|
-
let tdStart = 1;
|
|
209
|
-
const registerPath = path.join(projectDir, ".gsd-t", "techdebt.md");
|
|
219
|
+
const tdStart = (pre.priorRegisterExists ? (pre.priorMaxTd || 0) : 0) + 1;
|
|
220
|
+
log(`preflight ok — branch=${pre.branch}, priorRegister=${pre.priorRegisterExists}, TD numbering starts at TD-${tdStart}`);
|
|
210
221
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
222
|
+
// Volume probe — an agent measures the codebase (its own Bash) and carves slices.
|
|
223
|
+
phase("Probe");
|
|
224
|
+
const probe = await agent(
|
|
225
|
+
[
|
|
226
|
+
`⛔ TARGET DIRECTORY IS FIXED: you MUST scan ONLY the project at the absolute path \`${projectDir}\`. Before any measurement, \`cd ${projectDir}\` (or pass that exact path to every Read/Grep/Bash). Do NOT scan your current working directory, the GSD-T package, or any other tree — every file you measure and every slice path you emit MUST be under \`${projectDir}\`. If \`${projectDir}\` does not exist or is empty, return a single slice noting that.`,
|
|
227
|
+
``,
|
|
228
|
+
`You are the VOLUME PROBE for a GSD-T deep scan. Measure the codebase (Bash/Grep/Read), then decompose it into SLICES.`,
|
|
229
|
+
``,
|
|
230
|
+
`WHAT A SLICE IS: one COHESIVE SUB-DOMAIN or RESPONSIBILITY — a logical section of the code, like a domain but SMALLER. Finer than a domain, coarser than a single file. Examples: "invoice generation", "Stripe webhook handling", "refund/void logic", "tenant-scoping enforcement", "the work-order state machine", "scheduling conflict detection", "the auth/session layer". Each slice is something a developer would name as a distinct concern, and small enough that ONE agent can read EVERY file in it and reason about it as a coherent whole.`,
|
|
231
|
+
``,
|
|
232
|
+
`HOW TO DECOMPOSE (structure leads, not a number):`,
|
|
233
|
+
`1. Find the logical seams — walk the actual structure (dirs, modules, services, feature areas, route groups, schema groupings) and identify cohesive units of responsibility. Think the way you'd list a system's sub-domains.`,
|
|
234
|
+
`2. Size-check each unit — if a logical section is too big for one agent to read exhaustively (e.g. a giant component tree, a monolithic schema with hundreds of tables, a sprawling service), SUBDIVIDE it into smaller cohesive sub-sections until each fits one agent.`,
|
|
235
|
+
`3. The slice COUNT falls out of this — you produce as many slices as the code has cohesive, agent-sized sections. Do NOT target a number; do NOT split by raw file boundaries; do NOT make one slice per file/per module. A small repo naturally yields few slices; a large multi-domain app yields more.`,
|
|
236
|
+
``,
|
|
237
|
+
`Each slice: a \`key\` (kebab name of the responsibility, e.g. "invoice-generation"), concrete \`paths\` it owns (under \`${projectDir}\`), a \`dimension\`, and \`why\` (what makes it one cohesive concern).`,
|
|
238
|
+
``,
|
|
239
|
+
`Decompose HONESTLY by cohesive responsibility: not so coarse that an agent can't read its whole slice, not so fine that you emit one slice per file. A well-decomposed system has a finite, sensible number of real responsibilities — find them. (A volume-derived backstop cap is enforced after you return ONLY to catch over-slicing; a clean sub-domain decomposition lands under it. Report accurate \`totals\` — they set the backstop. If your count is truncated, you sliced too finely.)`,
|
|
240
|
+
``,
|
|
241
|
+
`Measure with real tooling and report in \`totals\`: files, loc, routes, tables, components, featureDomains (distinct business/feature areas). Read \`${projectDir}/package.json\` for the stack. Return JSON per the schema: totals + slices.`,
|
|
242
|
+
].join("\n"),
|
|
243
|
+
{ label: "volume-probe", phase: "Probe", schema: PROBE_SCHEMA, model: "sonnet" }
|
|
244
|
+
);
|
|
245
|
+
const rawSlices = (probe && Array.isArray(probe.slices) && probe.slices) || [];
|
|
246
|
+
if (!rawSlices.length) {
|
|
247
|
+
log("probe returned no slices — halting");
|
|
248
|
+
return { status: "failed", reason: "no-slices", probe };
|
|
223
249
|
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
const fallbackDate = mtime.toISOString().slice(0, 10);
|
|
232
|
-
const archiveDate = _archiveDateFrom(priorRegister, fallbackDate);
|
|
233
|
-
const scanDir = path.join(projectDir, ".gsd-t");
|
|
234
|
-
let candidate = path.join(scanDir, `techdebt_${archiveDate}.md`);
|
|
235
|
-
let counter = 2;
|
|
236
|
-
while (fs.existsSync(candidate)) {
|
|
237
|
-
candidate = path.join(scanDir, `techdebt_${archiveDate}_${counter}.md`);
|
|
238
|
-
counter += 1;
|
|
239
|
-
}
|
|
240
|
-
fs.renameSync(registerPath, candidate);
|
|
241
|
-
priorArchivePath = candidate;
|
|
242
|
-
log(`prior register archived → ${path.basename(candidate)} (${priorRegister.length} bytes); TD numbering continues at TD-${tdStart}`);
|
|
243
|
-
} else {
|
|
244
|
-
log(`no prior register — TD numbering starts at TD-01`);
|
|
250
|
+
// Volume-derived cap as a runaway backstop (the probe over-slices without it).
|
|
251
|
+
const computedCap = computeSliceCap(probe.totals || {});
|
|
252
|
+
const sliceCap = maxSlicesOverride || computedCap;
|
|
253
|
+
let slices = rawSlices;
|
|
254
|
+
if (rawSlices.length > sliceCap) {
|
|
255
|
+
slices = rawSlices.slice(0, sliceCap);
|
|
256
|
+
log(`⚠ SLICE CAP ENFORCED: probe returned ${rawSlices.length} cohesive slices; volume-derived backstop=${computedCap}${maxSlicesOverride ? ` (override ${maxSlicesOverride})` : ""}. Truncated to ${sliceCap} to bound the agent fan-out. Dropped ${rawSlices.length - sliceCap}: ${rawSlices.slice(sliceCap).map((s) => s.key).join(", ")}. (Probe over-sliced — it should group by cohesive sub-domain, not per file/module.)`);
|
|
245
257
|
}
|
|
258
|
+
log(`probe derived ${rawSlices.length} slice(s); backstop cap=${computedCap}; running ${slices.length} deep-finder(s); totals=${JSON.stringify(probe.totals)}`);
|
|
246
259
|
|
|
247
|
-
|
|
248
|
-
phase("Probe");
|
|
249
|
-
const probePrompt = [
|
|
250
|
-
`You are the VOLUME PROBE for a GSD-T deep codebase scan of \`${projectDir}\`.`,
|
|
251
|
-
``,
|
|
252
|
-
`Your job: measure the codebase's volume, then carve it into NARROW SLICES — one`,
|
|
253
|
-
`slice per area that a single deep-finder agent can EXHAUSTIVELY own. The number`,
|
|
254
|
-
`of slices MUST scale with volume: a tiny repo yields 1-3 slices; a large repo`,
|
|
255
|
-
`(thousands of files, hundreds of routes/tables, many feature domains) yields`,
|
|
256
|
-
`15-40. Do NOT default to a fixed 5 — that under-scaling is exactly the bug M66 fixes.`,
|
|
257
|
-
``,
|
|
258
|
-
`How to slice (combine these axes — prefer FEATURE-DOMAIN slicing for large apps):`,
|
|
259
|
-
`- By feature domain: each major business area (e.g. billing, scheduling, dispatch,`,
|
|
260
|
-
` work-orders, LMS, maintenance, integrations) is its own slice, owned end-to-end.`,
|
|
261
|
-
`- By layer where a layer is huge: routes/API surface, data/schema layer, the`,
|
|
262
|
-
` largest component trees, async jobs/queues.`,
|
|
263
|
-
`- By cross-cutting concern: tenant-isolation, secrets/config, auth, rate-limiting.`,
|
|
264
|
-
`Each slice names the concrete \`paths\` (dirs/globs) it owns and a \`dimension\`.`,
|
|
265
|
-
``,
|
|
266
|
-
`Use real tooling to measure: count files by extension, count route modules,`,
|
|
267
|
-
`count ORM table definitions (e.g. pgTable/Entity/model), count components, list`,
|
|
268
|
-
`top-level source dirs and their subdirs. Read package.json for the stack. If a`,
|
|
269
|
-
`single dir (e.g. src/lib/) has many independent subdirs, each substantial subdir`,
|
|
270
|
-
`is a candidate slice.`,
|
|
271
|
-
``,
|
|
272
|
-
`Soft cap: aim for ≤ ${maxSlicesHint} slices. If volume genuinely exceeds that,`,
|
|
273
|
-
`merge the smallest related areas but state in notes what you merged so depth loss`,
|
|
274
|
-
`is visible (no silent truncation).`,
|
|
275
|
-
``,
|
|
276
|
-
`Return JSON per the schema: totals (headline counts) + slices (the fan-out list).`,
|
|
277
|
-
].join("\n");
|
|
260
|
+
const deep = budget && budget.total && budget.total > 300000 ? "MAXIMUM" : "thorough";
|
|
278
261
|
|
|
279
|
-
//
|
|
280
|
-
|
|
281
|
-
// repo (truncated tooling output → guesses) would re-introduce the exact under-scaling
|
|
282
|
-
// M66 fixes. Sonnet is the right tier for this measurement+judgment task.
|
|
283
|
-
const probe = await agent(probePrompt, {
|
|
284
|
-
label: "volume-probe",
|
|
285
|
-
phase: "Probe",
|
|
286
|
-
schema: PROBE_SCHEMA,
|
|
287
|
-
model: "sonnet",
|
|
288
|
-
});
|
|
262
|
+
// Deep scan — pipeline: per-slice deep finder → single verify (no barrier).
|
|
263
|
+
phase("Deep Scan");
|
|
289
264
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
265
|
+
// M72: a finder that returns no schema-valid output (runtime nudged it twice then
|
|
266
|
+
// dropped it) must NOT be silently treated as a clean slice — that presents PARTIAL
|
|
267
|
+
// coverage as complete (7/19 slices dropped this way on the Hilo run). So: (1) the
|
|
268
|
+
// finder call is RETRIED once on a null/invalid result; (2) a slice that still fails
|
|
269
|
+
// is flagged `failed:true` and tracked — never conflated with a genuinely-empty slice.
|
|
270
|
+
function finderPrompt(slice) {
|
|
271
|
+
return [
|
|
272
|
+
`⛔ Scan ONLY files under the absolute project path \`${projectDir}\`. \`cd ${projectDir}\` first; never read outside this tree.`,
|
|
273
|
+
`You are a DEEP tech-debt finder for ONE slice of a scan of \`${projectDir}\`: \`${slice.key}\` (dimension: ${slice.dimension}).`,
|
|
274
|
+
`Owned paths (relative to \`${projectDir}\`): ${JSON.stringify(slice.paths)}.`,
|
|
275
|
+
slice.why ? `Why this slice matters: ${slice.why}` : ``,
|
|
276
|
+
``,
|
|
277
|
+
`MANDATE: ENUMERATE, do NOT sample. Read EVERY file under your owned paths (use Read/Grep). You own only this slice, so go to the bottom of it.`,
|
|
278
|
+
`Depth = ${deep}. "thorough" = every file, every non-trivial real defect (high+medium confidence). "MAXIMUM" = also lower-confidence/speculative items worth review.`,
|
|
279
|
+
`Surface: bugs, security holes, missing validation, broken invariants, race conditions, dead/duplicated code, N+1s, untested critical paths, contract drift, domain-specific correctness (money math, state-machine gaps, timezone bugs, idempotency holes).`,
|
|
280
|
+
`For each finding: title, severity (CRITICAL/HIGH/MEDIUM/LOW), human area label, concrete file:line refs, detail, impact, remediation, honest confidence. If a substantial slice yields only 1-2 findings, re-check before concluding it's clean. Empty findings array ONLY if genuinely clean.`,
|
|
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
|
+
].filter(Boolean).join("\n");
|
|
294
283
|
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
284
|
+
async function runFinder(slice) {
|
|
285
|
+
// up to 2 attempts; a null/invalid (non-array findings) result counts as a drop.
|
|
286
|
+
for (let attempt = 1; attempt <= 2; attempt++) {
|
|
287
|
+
try {
|
|
288
|
+
const r = await agent(finderPrompt(slice), {
|
|
289
|
+
label: attempt === 1 ? `find:${slice.key}` : `find:${slice.key} (retry)`,
|
|
290
|
+
phase: "Deep Scan", schema: FINDER_SCHEMA, model: "sonnet",
|
|
291
|
+
});
|
|
292
|
+
if (r && Array.isArray(r.findings)) return r; // valid (incl. empty)
|
|
293
|
+
log(`⚠ finder slice "${slice.key}" attempt ${attempt} returned no valid output${attempt < 2 ? " — retrying" : ""}`);
|
|
294
|
+
} catch (e) {
|
|
295
|
+
log(`⚠ finder slice "${slice.key}" attempt ${attempt} threw: ${e && e.message}${attempt < 2 ? " — retrying" : ""}`);
|
|
296
|
+
}
|
|
305
297
|
}
|
|
298
|
+
return null; // both attempts failed → dropped slice
|
|
306
299
|
}
|
|
307
300
|
|
|
308
|
-
// budget-aware depth hint: with a larger turn target, finders are told to dig deeper.
|
|
309
|
-
// "thorough" and "MAXIMUM" are defined inline in the finder prompt so the hint is actionable.
|
|
310
|
-
const deep = budget && budget.total && budget.total > 300000 ? "MAXIMUM" : "thorough";
|
|
311
|
-
|
|
312
|
-
// ─── Deep scan — pipeline: per-slice deep finder → single verify (no barrier) ───
|
|
313
|
-
// pipeline() runs each slice through both stages independently: slice A can be in
|
|
314
|
-
// verify while slice B is still finding. Wall-clock = slowest single chain.
|
|
315
|
-
phase("Deep Scan");
|
|
316
301
|
const sliceResults = await pipeline(
|
|
317
302
|
slices,
|
|
318
|
-
|
|
319
|
-
(
|
|
320
|
-
[
|
|
321
|
-
`You are a DEEP tech-debt finder for ONE slice of a GSD-T scan: \`${slice.key}\`.`,
|
|
322
|
-
`Dimension: ${slice.dimension}. Owned paths: ${JSON.stringify(slice.paths)}.`,
|
|
323
|
-
slice.why ? `Why this slice matters: ${slice.why}` : ``,
|
|
324
|
-
``,
|
|
325
|
-
`MANDATE: ENUMERATE, do NOT sample. Walk EVERY file under your owned paths.`,
|
|
326
|
-
`The legacy scan failed because one agent sampled the top ~5 issues across the`,
|
|
327
|
-
`whole repo and stopped. You own only this slice, so go to the bottom of it.`,
|
|
328
|
-
``,
|
|
329
|
-
`Depth = ${deep}. "thorough" = walk every file in your paths and report every`,
|
|
330
|
-
`non-trivial real defect, high and medium confidence. "MAXIMUM" = additionally`,
|
|
331
|
-
`include lower-confidence and speculative issues worth a human's review.`,
|
|
332
|
-
`Surface every real defect: bugs, security holes, missing validation, broken`,
|
|
333
|
-
`invariants, race conditions, dead/duplicated code, N+1s, untested critical`,
|
|
334
|
-
`paths, contract drift, and domain-specific correctness flaws (e.g. money math,`,
|
|
335
|
-
`state-machine gaps, timezone bugs, idempotency holes).`,
|
|
336
|
-
``,
|
|
337
|
-
`For each finding give: title, severity (CRITICAL/HIGH/MEDIUM/LOW), a human area`,
|
|
338
|
-
`label, concrete file:line refs, the detail, the impact, and a remediation.`,
|
|
339
|
-
`Set confidence honestly. If this slice is a substantial area (many files), a`,
|
|
340
|
-
`result of only 1-2 findings is suspicious — re-check before concluding it is`,
|
|
341
|
-
`clean. If the slice is genuinely clean, return an empty findings array.`,
|
|
342
|
-
``,
|
|
343
|
-
`Return JSON per the schema.`,
|
|
344
|
-
].filter(Boolean).join("\n"),
|
|
345
|
-
{ label: `find:${slice.key}`, phase: "Deep Scan", schema: FINDER_SCHEMA, model: "sonnet" }
|
|
346
|
-
),
|
|
347
|
-
// Stage 2 — single verify pass (per user decision: single, not 3-vote).
|
|
348
|
-
// Confirms each finding against the ACTUAL code; drops false positives.
|
|
349
|
-
// Red-team fix (HIGH-3): pipeline() stage-2 contract is (prevResult, originalItem, index).
|
|
350
|
-
// Defend against an unexpected runtime signature so a missing `slice` arg can never
|
|
351
|
-
// throw `slice.key` and silently drop a whole slice's findings (which would resolve
|
|
352
|
-
// to null at the pipeline level). Recover the key from the finder result if needed.
|
|
353
|
-
async (finderResult, originalItem, _index) => {
|
|
303
|
+
(slice) => runFinder(slice),
|
|
304
|
+
async (finderResult, originalItem) => {
|
|
354
305
|
const slice = originalItem || {};
|
|
355
306
|
const sliceKey = slice.key || (finderResult && finderResult.slice) || "unknown-slice";
|
|
307
|
+
// M72: distinguish a FAILED finder (null after retries) from a genuinely-clean slice.
|
|
356
308
|
if (!finderResult || !Array.isArray(finderResult.findings)) {
|
|
357
|
-
return { slice: sliceKey, findings: [] };
|
|
309
|
+
return { slice: sliceKey, findings: [], failed: true };
|
|
358
310
|
}
|
|
359
311
|
if (verifyMode === "none" || finderResult.findings.length === 0) {
|
|
360
|
-
return { slice: sliceKey, findings: finderResult.findings || [] };
|
|
312
|
+
return { slice: sliceKey, findings: finderResult.findings || [], failed: false };
|
|
361
313
|
}
|
|
362
314
|
const verified = await parallel(
|
|
363
315
|
finderResult.findings.map((f) => async () => {
|
|
364
316
|
try {
|
|
365
317
|
const v = await agent(
|
|
366
318
|
[
|
|
367
|
-
`You are a VERIFIER for one tech-debt finding
|
|
368
|
-
`Open the referenced files and check the claim is real and correctly characterized.`,
|
|
369
|
-
``,
|
|
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.`,
|
|
370
320
|
`Finding: ${JSON.stringify(f)}`,
|
|
371
|
-
|
|
372
|
-
`Set confirmed=true only if the defect genuinely exists. If the finder`,
|
|
373
|
-
`misread the code, return verdict="false-positive". If real but the`,
|
|
374
|
-
`severity is wrong, set correctedSeverity. If real but underspecified,`,
|
|
375
|
-
`verdict="needs-detail" (still kept). Return JSON per the schema.`,
|
|
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.`,
|
|
376
322
|
].join("\n"),
|
|
377
323
|
{ label: `verify:${sliceKey}`, phase: "Deep Scan", schema: VERIFY_SCHEMA, model: "sonnet" }
|
|
378
324
|
);
|
|
379
325
|
if (!v || v.verdict === "false-positive" || v.confirmed === false) return null;
|
|
380
326
|
return { ...f, severity: v.correctedSeverity || f.severity, _verify: v.verdict };
|
|
381
327
|
} catch (e) {
|
|
382
|
-
// verify errored — keep the finding flagged rather than silently drop it
|
|
383
328
|
return { ...f, _verify: "verify-errored" };
|
|
384
329
|
}
|
|
385
330
|
})
|
|
386
331
|
);
|
|
387
|
-
return { slice: sliceKey, findings: verified.filter(Boolean),
|
|
332
|
+
return { slice: sliceKey, findings: verified.filter(Boolean), failed: false };
|
|
388
333
|
}
|
|
389
334
|
);
|
|
390
335
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
// Red-team fix (CRITICAL-1/CRITICAL-2): the archive already happened in JS above.
|
|
399
|
-
// Synthesis no longer touches the archive — it gets the prior register CONTENT and the
|
|
400
|
-
// deterministically-computed starting TD number, so dedup + numbering can't hallucinate.
|
|
401
|
-
// Red-team fix (HIGH-1): synthesis ALSO writes the .gsd-t/scan/*.md dimension files the
|
|
402
|
-
// deterministic renderer (bin/scan-data-collector.js) reads, so the HTML report reflects
|
|
403
|
-
// real data instead of silently rendering a hollow shell. scanNumber (formerly dead) is
|
|
404
|
-
// threaded into the register header here.
|
|
405
|
-
const synthesisPrompt = [
|
|
406
|
-
`You are the SYNTHESIS agent for a GSD-T deep scan of \`${projectDir}\`.`,
|
|
407
|
-
`${slices.length} slices ran; ${allFindings.length} verified findings came back.`,
|
|
408
|
-
scanNumber ? `This is scan #${scanNumber} — put it in the register header.` : ``,
|
|
409
|
-
``,
|
|
410
|
-
priorRegister
|
|
411
|
-
? [
|
|
412
|
-
`The prior register was ALREADY archived (in code) to \`${priorArchivePath ? path.basename(priorArchivePath) : "an archive"}\`.`,
|
|
413
|
-
`Start TD numbering at TD-${tdStart} (computed deterministically from the highest`,
|
|
414
|
-
`prior TD number — do NOT renumber existing items or restart at 1).`,
|
|
415
|
-
`DEDUPLICATE against the prior register below: do not re-add a finding already`,
|
|
416
|
-
`represented there; cross-reference the prior TD id instead.`,
|
|
417
|
-
``,
|
|
418
|
-
`Prior register content (for dedup + numbering reference):`,
|
|
419
|
-
"```markdown",
|
|
420
|
-
priorRegister.slice(0, 20000),
|
|
421
|
-
"```",
|
|
422
|
-
].join("\n")
|
|
423
|
-
: `No prior register — start TD numbering at TD-${String(tdStart).padStart(2, "0")}.`,
|
|
424
|
-
``,
|
|
425
|
-
`Verified findings:`,
|
|
426
|
-
"```json",
|
|
427
|
-
JSON.stringify(allFindings, null, 2),
|
|
428
|
-
"```",
|
|
429
|
-
``,
|
|
430
|
-
`Write a FRESH \`.gsd-t/techdebt.md\` (use the Write tool). Structure per the GSD-T`,
|
|
431
|
-
`register format: a Summary table (CRITICAL/HIGH/MEDIUM/LOW counts), then sections`,
|
|
432
|
-
`Critical → High → Medium → Low, each finding as \`### TD-NNN — {title}\` with`,
|
|
433
|
-
`Area / Severity / Status: OPEN / Location (file:line) / Description / Impact /`,
|
|
434
|
-
`Remediation / Milestone candidate fields. Re-rank globally by true severity, not`,
|
|
435
|
-
`by slice order. De-duplicate findings that multiple slices surfaced (e.g. a`,
|
|
436
|
-
`cross-cutting tenant-scoping gap) into one item that lists all locations.`,
|
|
437
|
-
``,
|
|
438
|
-
`Write ONLY the register here. The .gsd-t/scan/*.md analysis files and the living`,
|
|
439
|
-
`docs are produced by the Document phase that runs next (M67) — do not write them.`,
|
|
440
|
-
``,
|
|
441
|
-
`Do NOT express effort in human-hours/days/sprints — GSD-T units only (domain/`,
|
|
442
|
-
`wave/spawn/token-spend) per the effort-estimates rule. Then commit the new`,
|
|
443
|
-
`register via git (you are on a feature branch; do not push).`,
|
|
444
|
-
`Return JSON per the schema with the final counts and the archivePath`,
|
|
445
|
-
`\`${priorArchivePath || ""}\`.`,
|
|
446
|
-
].filter(Boolean).join("\n");
|
|
447
|
-
|
|
448
|
-
const synthesis = await agent(synthesisPrompt, {
|
|
449
|
-
label: "synthesis",
|
|
450
|
-
phase: "Synthesis",
|
|
451
|
-
schema: SYNTHESIS_SCHEMA,
|
|
452
|
-
model: "opus",
|
|
336
|
+
// M72: coverage accounting — a dropped pipeline result (null) OR a failed:true slice
|
|
337
|
+
// is a COVERAGE GAP. Surface it deterministically; never present partial as complete.
|
|
338
|
+
const resultsByIndex = sliceResults; // pipeline preserves order
|
|
339
|
+
const failedSlices = [];
|
|
340
|
+
slices.forEach((s, i) => {
|
|
341
|
+
const r = resultsByIndex[i];
|
|
342
|
+
if (!r || r.failed) failedSlices.push(s.key);
|
|
453
343
|
});
|
|
344
|
+
const succeededCount = slices.length - failedSlices.length;
|
|
345
|
+
const coverageComplete = failedSlices.length === 0;
|
|
346
|
+
const allFindings = sliceResults.filter(Boolean).filter((r) => !r.failed).flatMap((r) => (r.findings || []).map((f) => ({ ...f, slice: r.slice })));
|
|
347
|
+
if (!coverageComplete) {
|
|
348
|
+
log(`⚠ PARTIAL COVERAGE — ${failedSlices.length}/${slices.length} slices failed after retry and produced NO findings: ${failedSlices.join(", ")}. The register will be flagged INCOMPLETE. Resume the run to re-scan only the failed slices.`);
|
|
349
|
+
}
|
|
350
|
+
log(`deep scan complete: ${allFindings.length} verified findings across ${succeededCount}/${slices.length} slices${coverageComplete ? " (full coverage)" : " (PARTIAL)"}`);
|
|
454
351
|
|
|
352
|
+
// Synthesis — an agent does the ARCHIVE + REGISTER WRITE + GIT entirely via its
|
|
353
|
+
// own Bash/Write tools. The orchestrator does NOT touch fs. The agent is given the
|
|
354
|
+
// deterministic tdStart so numbering can't drift, and is told to archive FIRST.
|
|
355
|
+
phase("Synthesis");
|
|
356
|
+
const findingsJson = JSON.stringify(allFindings, null, 2);
|
|
357
|
+
const synthesis = await agent(
|
|
358
|
+
[
|
|
359
|
+
`You are the SYNTHESIS agent for a GSD-T deep scan of \`${projectDir}\`. ${slices.length} slices ran; ${allFindings.length} verified findings came back.`,
|
|
360
|
+
scanNumber ? `This is scan #${scanNumber} — put it in the register header.` : ``,
|
|
361
|
+
``,
|
|
362
|
+
`STEP 1 — ARCHIVE (do this FIRST, deterministically, via Bash): if \`${projectDir}/.gsd-t/techdebt.md\` exists, rename it to \`${projectDir}/.gsd-t/techdebt_YYYY-MM-DD.md\` using its header date (fallback: today's date from \`date +%F\`); on same-day collision append \`_2\`, \`_3\`. Use \`git mv\` if it's a git repo, else \`mv\`. Capture the archive path. ${pre.priorRegisterExists ? "(A prior register EXISTS — you MUST archive it.)" : "(No prior register — skip archiving.)"}`,
|
|
363
|
+
``,
|
|
364
|
+
// M72: MANDATORY coverage banner — enforced here, not left to the agent's notice.
|
|
365
|
+
coverageComplete
|
|
366
|
+
? `COVERAGE: all ${slices.length} slices succeeded — full coverage. Note this in the header.`
|
|
367
|
+
: `⚠ COVERAGE IS INCOMPLETE — ${failedSlices.length} of ${slices.length} slices FAILED to return findings and were NOT scanned: ${failedSlices.join(", ")}. You MUST put a prominent "> ⚠ PARTIAL COVERAGE — N of M codebase areas were not scanned this pass (listed below); findings UNDER-COUNT the real debt. Re-run for full coverage." banner at the TOP of the register, AND list the un-scanned slice names. Do NOT present this as a complete picture.`,
|
|
368
|
+
``,
|
|
369
|
+
`STEP 2 — WRITE the fresh \`${projectDir}/.gsd-t/techdebt.md\` (Write tool). Start TD numbering at TD-${tdStart} (computed deterministically — do NOT renumber or restart). Structure: the coverage banner (above), a Summary table (CRITICAL/HIGH/MEDIUM/LOW counts), then sections Critical→High→Medium→Low, each finding as \`### TD-NNN — {title}\` with Area / Severity / Status: OPEN / Location (file:line) / Description / Impact / Remediation / Milestone candidate. Re-rank globally by true severity; de-duplicate findings multiple slices surfaced into one item listing all locations. GSD-T effort units only (domain/wave/spawn/token) — never human-hours.`,
|
|
370
|
+
`If the findings set is large, WRITE INCREMENTALLY — create the file with the header+summary first (Write), then APPEND each severity section with Edit. Do NOT attempt to emit the entire multi-hundred-item register in a single Write call (it can stall); build it up section by section so progress is durable.`,
|
|
371
|
+
``,
|
|
372
|
+
`STEP 3 — COMMIT via Bash if it's a git repo (\`git add .gsd-t/techdebt.md\` + the archive; commit). Do NOT push.`,
|
|
373
|
+
``,
|
|
374
|
+
`Verified findings (${allFindings.length} total):`,
|
|
375
|
+
"```json",
|
|
376
|
+
findingsJson.length > 500000 ? findingsJson.slice(0, 500000) + "\n…(TRUNCATED — too many findings to inline; the above is the first portion. Note in the register that some lower-severity items may be omitted due to volume.)" : findingsJson,
|
|
377
|
+
"```",
|
|
378
|
+
``,
|
|
379
|
+
`Return JSON per the schema: status "written" once the register file is on disk, the counts, the archivePath (or ""), and tdRange (e.g. "TD-${tdStart}..TD-NNN").`,
|
|
380
|
+
].filter(Boolean).join("\n"),
|
|
381
|
+
{ label: "synthesis", phase: "Synthesis", schema: SYNTHESIS_SCHEMA, model: "opus" }
|
|
382
|
+
);
|
|
455
383
|
if (!synthesis || synthesis.status !== "written") {
|
|
456
|
-
log("synthesis did not write the register — halting before
|
|
457
|
-
return { status: "failed", reason: "synthesis-failed", synthesis, findingCount: allFindings.length
|
|
458
|
-
}
|
|
459
|
-
// Deterministic confirmation the register actually landed (don't trust the agent's status alone).
|
|
460
|
-
if (!fs.existsSync(registerPath)) {
|
|
461
|
-
log("synthesis reported written but .gsd-t/techdebt.md is absent — halting");
|
|
462
|
-
return { status: "failed", reason: "register-missing", synthesis, archivePath: priorArchivePath };
|
|
384
|
+
log("synthesis did not write the register — halting before document phase");
|
|
385
|
+
return { status: "failed", reason: "synthesis-failed", synthesis, findingCount: allFindings.length };
|
|
463
386
|
}
|
|
464
|
-
log(`register written: ${JSON.stringify(synthesis.counts)}`);
|
|
387
|
+
log(`register written: ${JSON.stringify(synthesis.counts)} (${synthesis.tdRange || ""})`);
|
|
465
388
|
|
|
466
|
-
//
|
|
467
|
-
//
|
|
468
|
-
//
|
|
469
|
-
// cross-reference to techdebt.md). The register exists here (confirmed above).
|
|
470
|
-
let registerText = "";
|
|
471
|
-
try { registerText = fs.readFileSync(registerPath, "utf8"); } catch (_) {}
|
|
472
|
-
|
|
473
|
-
// ─── Document — deep living-doc + dimension-file cross-population (M67) ───
|
|
474
|
-
// M66 made the register deep but left doc cross-population as a non-deterministic
|
|
475
|
-
// "lead agent follow-on" — effectively dropped. M67 fans out one agent PER DOCUMENT,
|
|
476
|
-
// each drawing on the SAME slices + verified findings the finders produced, so the
|
|
477
|
-
// docs are as thorough as the register. Runs BEFORE Render so the HTML report reads
|
|
478
|
-
// the deep .gsd-t/scan/architecture.md (with the parseable file/LOC line) instead of
|
|
479
|
-
// a stub. Per-doc failures are non-fatal (the register is authoritative) but logged.
|
|
389
|
+
// Document — per-doc fan-out. Each agent writes its file via Write/Edit (its tools).
|
|
390
|
+
// The orchestrator passes the findings + (for plain-english) tells the agent to
|
|
391
|
+
// READ the just-written register itself, since the orchestrator can't read it.
|
|
480
392
|
phase("Document");
|
|
481
|
-
|
|
482
|
-
// Red-team fix (HIGH-1): the living-doc agents "merge not overwrite" via prose, and a
|
|
483
|
-
// dirty working tree does NOT halt the scan (working-tree-state is a warn, not error).
|
|
484
|
-
// So a doc agent could clobber a user's UNCOMMITTED edits to docs/ or README.md —
|
|
485
|
-
// unrecoverable via git. Deterministic backstop: snapshot every existing living doc to
|
|
486
|
-
// .gsd-t/scan/.doc-backup/ BEFORE the fan-out, mirroring the deterministic archive the
|
|
487
|
-
// register gets. Recovery is then a plain file copy, regardless of git state.
|
|
488
|
-
const LIVING_DOCS = [
|
|
489
|
-
"docs/architecture.md",
|
|
490
|
-
"docs/workflows.md",
|
|
491
|
-
"docs/infrastructure.md",
|
|
492
|
-
"docs/requirements.md",
|
|
493
|
-
"README.md",
|
|
494
|
-
];
|
|
495
|
-
const docBackupDir = path.join(projectDir, ".gsd-t", "scan", ".doc-backup");
|
|
496
|
-
const backedUp = [];
|
|
497
|
-
fs.mkdirSync(docBackupDir, { recursive: true });
|
|
498
|
-
for (const rel of LIVING_DOCS) {
|
|
499
|
-
const src = path.join(projectDir, rel);
|
|
500
|
-
if (fs.existsSync(src)) {
|
|
501
|
-
const dest = path.join(docBackupDir, rel.replace(/[\/]/g, "__"));
|
|
502
|
-
fs.copyFileSync(src, dest);
|
|
503
|
-
backedUp.push(rel);
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
if (backedUp.length) {
|
|
507
|
-
log(`backed up ${backedUp.length} existing living doc(s) to .gsd-t/scan/.doc-backup/ before the document phase (recover by copying back if a merge clobbers content): ${backedUp.join(", ")}`);
|
|
508
|
-
}
|
|
509
|
-
|
|
510
393
|
const sliceSummary = slices.map((s) => `- ${s.key} (${s.dimension}): ${JSON.stringify(s.paths)}`).join("\n");
|
|
511
|
-
const findingsByArea = {};
|
|
512
|
-
for (const f of allFindings) {
|
|
513
|
-
const k = (f.area || "general").toLowerCase();
|
|
514
|
-
(findingsByArea[k] = findingsByArea[k] || []).push(f);
|
|
515
|
-
}
|
|
516
|
-
const findingsJson = JSON.stringify(allFindings, null, 2).slice(0, 40000);
|
|
517
|
-
|
|
518
|
-
// Each entry: { id, label, prompt }. mergeNote is appended to every living-doc prompt.
|
|
519
|
-
const mergeNote =
|
|
520
|
-
`If the file already exists with real content: MERGE, and do it with the Edit tool ` +
|
|
521
|
-
`(targeted section edits/appends) — do NOT call Write on a pre-existing file, because ` +
|
|
522
|
-
`Write is a full overwrite that would destroy the user's structure and any content you ` +
|
|
523
|
-
`did not reproduce. Preserve the user's structure and custom content; update/add sections. ` +
|
|
524
|
-
`If the file is only placeholder/template tokens, you may replace it. If absent, create it ` +
|
|
525
|
-
`with Write. Replace {Project Name}/{Date} tokens with real values (date from the system ` +
|
|
526
|
-
`clock). Do NOT invent facts — derive everything from the slices, findings, and the actual ` +
|
|
527
|
-
`code you read. (A pre-edit backup exists at .gsd-t/scan/.doc-backup/ as a safety net, but ` +
|
|
528
|
-
`do not rely on it — edit carefully.)`;
|
|
529
|
-
|
|
530
394
|
const baseCtx = [
|
|
531
395
|
`Project: \`${projectDir}\`. Probe totals: ${JSON.stringify(probe.totals)}.`,
|
|
532
|
-
|
|
533
|
-
`Slices the scan covered (your raw material — each names the paths it owns):`,
|
|
396
|
+
`Slices the scan covered:`,
|
|
534
397
|
sliceSummary,
|
|
535
|
-
|
|
536
|
-
`Verified findings (truncated):`,
|
|
398
|
+
`Verified findings:`,
|
|
537
399
|
"```json",
|
|
538
|
-
findingsJson,
|
|
400
|
+
findingsJson.length > 120000 ? findingsJson.slice(0, 120000) + "\n…(truncated)" : findingsJson,
|
|
539
401
|
"```",
|
|
540
402
|
].join("\n");
|
|
541
403
|
|
|
404
|
+
const mergeNote =
|
|
405
|
+
`If the target file already exists with real content: MERGE using the Edit tool ` +
|
|
406
|
+
`(targeted section edits) — do NOT overwrite with Write (that destroys content you ` +
|
|
407
|
+
`didn't reproduce). If it's placeholder/template-only, you may replace. If absent, create. ` +
|
|
408
|
+
`Replace {Project Name}/{Date} tokens with real values. Derive everything from the slices, ` +
|
|
409
|
+
`findings, and the actual code you read — invent nothing.`;
|
|
410
|
+
|
|
542
411
|
const docTargets = [
|
|
543
|
-
{
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
{
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
{
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
{
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
{
|
|
564
|
-
|
|
565
|
-
prompt: `Write \`.gsd-t/scan/contract-drift.md\` — compare \`.gsd-t/contracts/\` (if it exists) to the actual implementation: API endpoints vs api-contract, schema vs schema-contract, undocumented endpoints/tables/components, drift. If no contracts dir exists, write a short note saying so and list the de-facto interfaces worth documenting.`,
|
|
566
|
-
},
|
|
567
|
-
{
|
|
568
|
-
id: "docs-architecture", label: "docs/architecture.md", merge: true,
|
|
569
|
-
prompt: `Update or create \`docs/architecture.md\`: system overview (stack, structure, patterns); component descriptions with locations + dependencies (one section per feature-domain slice); data flow (request → handler → service → data layer → response); data models from schema/ORM; API structure from routes; external integrations; design decisions found in code/configs. Go deep — this should be a real architecture reference, not a stub.`,
|
|
570
|
-
},
|
|
571
|
-
{
|
|
572
|
-
id: "docs-workflows", label: "docs/workflows.md", merge: true,
|
|
573
|
-
prompt: `Update or create \`docs/workflows.md\`: trace USER JOURNEYS per feature-domain slice (each major business area gets its own end-to-end journey from entry point through handlers to data); technical workflows from cron jobs / queue workers / scheduled tasks; API workflows for multi-step operations; integration workflows for external syncing; state machines and approval flows discovered in code. One journey per feature-domain slice minimum.`,
|
|
574
|
-
},
|
|
575
|
-
{
|
|
576
|
-
id: "docs-infrastructure", label: "docs/infrastructure.md", merge: true,
|
|
577
|
-
prompt: `Update or create \`docs/infrastructure.md\` (the most commonly-lost knowledge): Quick Reference commands (package.json scripts, Makefile, CI/CD); local dev setup (README, docker-compose, .env.example); database commands (migrations, seeds, ORM, backups); cloud provisioning (Terraform/CFN/Pulumi/deploy scripts); credentials/secrets (NAMES ONLY from .env.example, never values); deployment (CI/CD, Dockerfiles, platform configs); logging/monitoring.`,
|
|
578
|
-
},
|
|
579
|
-
{
|
|
580
|
-
id: "docs-requirements", label: "docs/requirements.md", merge: true,
|
|
581
|
-
prompt: `Update or create \`docs/requirements.md\`: functional requirements discovered from routes/handlers/UI components; technical requirements from configs/package.json/runtime; non-functional requirements from performance configs, rate limits, caching. Derive from what the code actually does.`,
|
|
582
|
-
},
|
|
583
|
-
{
|
|
584
|
-
id: "readme", label: "README.md", merge: true,
|
|
585
|
-
prompt: `Update or create \`README.md\`: project name + description; tech stack + versions discovered; getting-started/setup (from infrastructure findings); brief architecture overview; link to \`docs/\` for detail. If it exists, MERGE — update tech-stack + setup sections but preserve the user's existing structure and custom content.`,
|
|
586
|
-
},
|
|
587
|
-
{
|
|
588
|
-
id: "techdebt-plain-english", label: ".gsd-t/techdebt_in_plain_english.md",
|
|
589
|
-
needsRegister: true, // red-team HIGH-1: must receive the synthesized register (TD-NNN ids live there, not in findings)
|
|
590
|
-
prompt: `Write \`.gsd-t/techdebt_in_plain_english.md\` — a NON-TECHNICAL companion to the tech-debt register, written for a smart reader who is NOT an engineer (e.g. a founder, PM, or stakeholder). Cover EVERY item in the register (one entry per TD-NNN, in the same severity order), using the EXACT TD-NNN ids from the register provided below. For each item:\n` +
|
|
591
|
-
`- **Heading**: \`### TD-NNN — <the plain-English name of the problem>\` (keep the TD-NNN id so it cross-references the technical register, but rename the title into everyday language — no jargon).\n` +
|
|
592
|
-
`- **What it is** (1-2 sentences): explain the problem with ZERO technical terms. If a technical word is unavoidable, define it in parentheses the way you'd explain it to a friend.\n` +
|
|
593
|
-
`- **Why it matters** (1-2 sentences): the business/user consequence — what could go wrong, who is affected, what it costs.\n` +
|
|
594
|
-
`- **Real-world analogy**: a concrete everyday comparison that makes the risk intuitive (e.g. "This is like leaving the spare house key under the doormat — convenient, but anyone who thinks to look can get in." for a hardcoded secret). The analogy must genuinely map to THIS specific item, not be generic.\n` +
|
|
595
|
-
`- **Severity in plain terms**: translate CRITICAL/HIGH/MEDIUM/LOW into urgency a non-engineer feels ("fix before launch" / "schedule soon" / "clean up eventually").\n` +
|
|
596
|
-
`Open with a 2-3 sentence plain-English summary of the overall health of the codebase and the headline counts. Derive everything from the verified findings — do not invent items or analogies that don't fit. This file is the layman's lens on the same findings as \`.gsd-t/techdebt.md\`.`,
|
|
597
|
-
},
|
|
412
|
+
{ id: "scan-architecture", label: "scan:architecture",
|
|
413
|
+
prompt: `Write \`${projectDir}/.gsd-t/scan/architecture.md\` (use Bash mkdir -p for .gsd-t/scan first if needed) — architecture dimension: stack, structure, patterns, a Components/Domains list (one per feature-domain slice), data flow. For the HTML renderer, give the file+LOC GRAND TOTAL as a markdown TABLE ROW exactly: \`| Grand Total | <N> files | <LOC> |\` from the probe totals. Per-dir \`<n> files (~<loc> LOC)\` lines in a Structure section are fine; do NOT write a second bare grand-total line.` },
|
|
414
|
+
{ id: "scan-security", label: "scan:security",
|
|
415
|
+
prompt: `Write \`${projectDir}/.gsd-t/scan/security.md\` — security findings as sections \`### SEC-H<n>: <title>\` (HIGH) / \`### SEC-M<n>: <title>\` (MEDIUM), each with \`- **Details**: …\` and \`- **Fix**: …\` bullets.` },
|
|
416
|
+
{ id: "scan-quality", label: "scan:quality",
|
|
417
|
+
prompt: `Write \`${projectDir}/.gsd-t/scan/quality.md\` — quality/dead-code/dup/test-gap findings as \`### DC-<n>: <title>\` / \`### TCG-<n>: <title>\` / \`### TD-<n>: <title>\`, each with a \`\\\`file:line\\\`\` location line and \`- **Impact**: …\` / \`- **Suggestion**: …\` bullets.` },
|
|
418
|
+
{ id: "scan-business-rules", label: "scan:business-rules",
|
|
419
|
+
prompt: `Write \`${projectDir}/.gsd-t/scan/business-rules.md\` — embedded business logic across the slices: validation, authorization, workflow/state-machine, calculation (pricing/scoring/quotas), integration (retry/fallback/timeout) rules. Each: where implemented (file:line) + assessment. Add an "Undocumented Rules" section.` },
|
|
420
|
+
{ id: "scan-contract-drift", label: "scan:contract-drift",
|
|
421
|
+
prompt: `Write \`${projectDir}/.gsd-t/scan/contract-drift.md\` — compare \`${projectDir}/.gsd-t/contracts/\` (if present) to the implementation: endpoints vs api-contract, schema vs schema-contract, undocumented endpoints/tables/components, drift. If no contracts dir, say so + list de-facto interfaces worth documenting.` },
|
|
422
|
+
{ id: "docs-architecture", label: "docs/architecture.md", merge: true,
|
|
423
|
+
prompt: `Update or create \`${projectDir}/docs/architecture.md\`: system overview; component descriptions w/ locations+dependencies (one section per feature-domain slice); data flow (request→handler→service→data→response); data models; API structure; integrations; design decisions. Go deep.` },
|
|
424
|
+
{ id: "docs-workflows", label: "docs/workflows.md", merge: true,
|
|
425
|
+
prompt: `Update or create \`${projectDir}/docs/workflows.md\`: a USER JOURNEY per feature-domain slice (entry→handlers→data); technical workflows (cron/queues/scheduled); API workflows for multi-step ops; integration workflows; state machines/approval flows. ≥1 journey per feature-domain slice.` },
|
|
426
|
+
{ id: "docs-infrastructure", label: "docs/infrastructure.md", merge: true,
|
|
427
|
+
prompt: `Update or create \`${projectDir}/docs/infrastructure.md\`: Quick Reference commands (package.json scripts/Makefile/CI); local dev setup; DB commands (migrations/seeds/ORM/backups); cloud provisioning; credentials/secrets (NAMES ONLY, never values); deployment; logging/monitoring.` },
|
|
428
|
+
{ id: "docs-requirements", label: "docs/requirements.md", merge: true,
|
|
429
|
+
prompt: `Update or create \`${projectDir}/docs/requirements.md\`: functional requirements from routes/handlers/UI; technical from configs/package.json/runtime; non-functional from perf configs/rate limits/caching.` },
|
|
430
|
+
{ id: "readme", label: "README.md", merge: true,
|
|
431
|
+
prompt: `Update or create \`${projectDir}/README.md\`: project name+description; tech stack+versions; getting-started/setup; brief architecture overview; link to docs/. If it exists, MERGE — preserve the user's structure/custom content.` },
|
|
432
|
+
{ id: "techdebt-plain-english", label: ".gsd-t/techdebt_in_plain_english.md", needsRegister: true,
|
|
433
|
+
prompt: `Write \`${projectDir}/.gsd-t/techdebt_in_plain_english.md\` — a NON-TECHNICAL companion to the register for a non-engineer (founder/PM/stakeholder). FIRST read \`${projectDir}/.gsd-t/techdebt.md\` (Read tool) to get the EXACT TD-NNN ids/order — it was just written and IS the source of truth (the findings JSON has no TD ids). Cover EVERY item, one entry each: \`### TD-NNN — <plain-English name>\`; **What it is** (no jargon; define any unavoidable term in parentheses); **Why it matters** (business/user consequence); **Real-world analogy** (a concrete everyday comparison that genuinely maps to THIS item); **Severity in plain terms** (CRITICAL/HIGH/MEDIUM/LOW → "fix before launch"/"schedule soon"/"clean up eventually"). Open with a 2-3 sentence plain-English health summary + headline counts.` },
|
|
598
434
|
];
|
|
599
435
|
|
|
600
436
|
const docResults = await parallel(
|
|
601
437
|
docTargets.map((d) => async () => {
|
|
602
438
|
const isLiving = !!d.merge;
|
|
603
|
-
// red-team HIGH-1: targets that mirror the register (plain-english) get the
|
|
604
|
-
// synthesized register text — the authoritative source of TD-NNN ids/order —
|
|
605
|
-
// not just the raw findings.
|
|
606
|
-
// red-team MEDIUM: do NOT silently truncate the register — a large register
|
|
607
|
-
// (e.g. 119 items ≈ 180KB) sliced to a small cap would drop the entire
|
|
608
|
-
// MEDIUM/LOW tail while the prompt says "cover EVERY item" (silent-cap
|
|
609
|
-
// anti-pattern the workflow forbids elsewhere). Cap generously, and if we DO
|
|
610
|
-
// hit it, tell the agent explicitly + log it so completeness loss is visible.
|
|
611
|
-
const REGISTER_CAP = 400000;
|
|
612
|
-
let registerBlock = "";
|
|
613
|
-
if (d.needsRegister) {
|
|
614
|
-
const truncated = registerText.length > REGISTER_CAP;
|
|
615
|
-
if (truncated) {
|
|
616
|
-
log(`⚠ register (${registerText.length} bytes) exceeds the ${REGISTER_CAP}-byte plain-english injection cap — the tail will be missing from techdebt_in_plain_english.md. Consider splitting the register.`);
|
|
617
|
-
}
|
|
618
|
-
registerBlock = [
|
|
619
|
-
"",
|
|
620
|
-
"Synthesized register (.gsd-t/techdebt.md) — use these EXACT TD-NNN ids/order:",
|
|
621
|
-
truncated ? "⚠ NOTE: the register was truncated to fit; cover every item you CAN see and end with a line noting that items beyond the last shown TD-NNN were omitted due to size." : "",
|
|
622
|
-
"```markdown",
|
|
623
|
-
registerText.slice(0, REGISTER_CAP),
|
|
624
|
-
"```",
|
|
625
|
-
].join("\n");
|
|
626
|
-
}
|
|
627
439
|
const prompt = [
|
|
628
|
-
`You are the documentation agent for ONE document in a GSD-T deep scan
|
|
629
|
-
``,
|
|
440
|
+
`You are the documentation agent for ONE document in a GSD-T deep scan of \`${projectDir}\`.`,
|
|
630
441
|
baseCtx,
|
|
631
|
-
|
|
442
|
+
d.needsRegister ? `\nThe synthesized register lives at \`${projectDir}/.gsd-t/techdebt.md\` — READ it first for the authoritative TD-NNN ids/order.` : ``,
|
|
632
443
|
``,
|
|
633
444
|
d.prompt,
|
|
634
445
|
``,
|
|
635
|
-
isLiving ? mergeNote : `Write the file fresh
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
`do not summarize only from the findings. Use the Write/Edit tools to write the`,
|
|
639
|
-
`file, then return JSON per the schema (status: "written" new, "merged" if you`,
|
|
640
|
-
`merged into existing content, "skipped" if genuinely nothing to write, "failed"`,
|
|
641
|
-
`on error). Do NOT commit — the workflow handles git.`,
|
|
642
|
-
].join("\n");
|
|
446
|
+
isLiving ? mergeNote : `Write the file fresh in the format described (use Bash \`mkdir -p\` for parent dirs if needed).`,
|
|
447
|
+
`Read the actual code under the relevant slice paths for specifics — don't summarize only from findings. Use Write/Edit to write the file, then return JSON per the schema (status "written"/"merged"/"skipped"/"failed"). Do NOT commit — the workflow handles git at the end.`,
|
|
448
|
+
].filter(Boolean).join("\n");
|
|
643
449
|
try {
|
|
644
|
-
return await agent(prompt, {
|
|
645
|
-
label: d.label,
|
|
646
|
-
phase: "Document",
|
|
647
|
-
schema: DOC_RESULT_SCHEMA,
|
|
648
|
-
model: "sonnet",
|
|
649
|
-
});
|
|
450
|
+
return await agent(prompt, { label: d.label, phase: "Document", schema: DOC_RESULT_SCHEMA, model: "sonnet" });
|
|
650
451
|
} catch (e) {
|
|
651
|
-
return { doc: d.id, status: "failed",
|
|
452
|
+
return { doc: d.id, status: "failed", notes: `agent error: ${e && e.message}` };
|
|
652
453
|
}
|
|
653
454
|
})
|
|
654
455
|
);
|
|
655
|
-
|
|
656
456
|
const docsOk = docResults.filter(Boolean).filter((r) => r.status === "written" || r.status === "merged");
|
|
657
457
|
const docsFailed = docResults.filter(Boolean).filter((r) => r.status === "failed");
|
|
658
|
-
log(`document phase: ${docsOk.length}/${docTargets.length}
|
|
659
|
-
|
|
660
|
-
//
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
//
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
];
|
|
678
|
-
const missingBin = requiredBins.find((b) => !fs.existsSync(path.join(projectDir, b)));
|
|
679
|
-
let render;
|
|
680
|
-
if (missingBin) {
|
|
681
|
-
render = { ok: false, exitCode: 127, stderr: `missing renderer ${missingBin}` };
|
|
682
|
-
log(`render skipped — ${missingBin} not found (register is the primary artifact; report is optional)`);
|
|
683
|
-
} else {
|
|
684
|
-
const renderExpr = `
|
|
685
|
-
const {collectScanData}=require('./bin/scan-data-collector.js');
|
|
686
|
-
const {extractSchema}=require('./bin/scan-schema.js');
|
|
687
|
-
const {generateDiagrams}=require('./bin/scan-diagrams.js');
|
|
688
|
-
const {generateReport}=require('./bin/scan-report.js');
|
|
689
|
-
const root=process.argv[1];
|
|
690
|
-
const analysisData=collectScanData(root);
|
|
691
|
-
const schemaData=extractSchema(root);
|
|
692
|
-
const diagrams=generateDiagrams(analysisData, schemaData, {projectRoot:root});
|
|
693
|
-
const r=generateReport(analysisData, schemaData, diagrams, {projectRoot:root});
|
|
694
|
-
if (r.outputPath) console.log(JSON.stringify({outputPath:r.outputPath, diagramsRendered:r.diagramsRendered, filesScanned:analysisData.filesScanned||0, findings:(analysisData.findings||[]).length}));
|
|
695
|
-
else console.error('report-failed:', r.error);
|
|
696
|
-
`;
|
|
697
|
-
render = _runNode("bin/scan-report.js", renderExpr, [projectDir]);
|
|
698
|
-
}
|
|
699
|
-
let reportInfo = null;
|
|
700
|
-
let reportHollow = null;
|
|
701
|
-
if (render.ok) {
|
|
702
|
-
try { reportInfo = JSON.parse((render.stdout || "").trim()); } catch (_) {}
|
|
703
|
-
// A real scan ALWAYS has files. filesScanned===0 alone is a hollow signal —
|
|
704
|
-
// do NOT require findings===0 too (that &&-guard was defeated by any non-empty
|
|
705
|
-
// scan, where the security/quality findings parse but the file count does not).
|
|
706
|
-
reportHollow = !!(reportInfo && reportInfo.filesScanned === 0);
|
|
707
|
-
if (reportHollow) {
|
|
708
|
-
log(`⚠ HTML report rendered but reads HOLLOW (filesScanned=0) — the architecture.md file/LOC line is missing or in an unparsed format. Report at ${reportInfo && reportInfo.outputPath}; the techdebt.md register is the authoritative artifact.`);
|
|
709
|
-
} else {
|
|
710
|
-
log(`HTML report: ${render.stdout && render.stdout.trim()}`);
|
|
711
|
-
}
|
|
712
|
-
} else {
|
|
713
|
-
// Non-fatal — the register is the primary artifact; the report is a nicety.
|
|
714
|
-
log(`render stage non-fatal failure (exitCode=${render.exitCode}): ${render.stderr || render.stdout}`);
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
// Commit the docs + dimension files + HTML report deterministically (doc agents were
|
|
718
|
-
// told NOT to commit, to avoid interleaved concurrent git operations). Best-effort:
|
|
719
|
-
// a commit failure is non-fatal (artifacts are on disk).
|
|
720
|
-
// Red-team fix (LOW): drop `-f` — none of these targets are gitignored, and `-f` would
|
|
721
|
-
// force-add gitignored junk under the pathspecs (e.g. docs/.DS_Store). The .doc-backup
|
|
722
|
-
// dir lives under .gsd-t/scan/ which IS committed; exclude it explicitly so backups
|
|
723
|
-
// aren't versioned.
|
|
724
|
-
const commit = spawnSync(
|
|
725
|
-
"git",
|
|
726
|
-
["add", "-A", ".gsd-t/scan", ".gsd-t/techdebt_in_plain_english.md", "docs", "README.md", ":!.gsd-t/scan/.doc-backup"],
|
|
727
|
-
{ cwd: projectDir, stdio: "pipe" }
|
|
728
|
-
);
|
|
729
|
-
if (commit.status === 0) {
|
|
730
|
-
const ci = spawnSync(
|
|
731
|
-
"git",
|
|
732
|
-
["commit", "-q", "-m", `scan: deep document cross-population (${docsOk.length} docs) + HTML report`],
|
|
733
|
-
{ cwd: projectDir, stdio: "pipe" }
|
|
734
|
-
);
|
|
735
|
-
if (ci.status === 0) log("docs + report committed");
|
|
736
|
-
else log(`docs staged but commit skipped (likely nothing to commit or hook): ${ci.stderr && ci.stderr.toString().slice(0, 200)}`);
|
|
737
|
-
} else {
|
|
738
|
-
log(`doc git add non-fatal failure: ${commit.stderr && commit.stderr.toString().slice(0, 200)}`);
|
|
739
|
-
}
|
|
458
|
+
log(`document phase: ${docsOk.length}/${docTargets.length} written/merged${docsFailed.length ? `; ${docsFailed.length} failed (non-fatal): ${docsFailed.map((d) => d.doc).join(", ")}` : ""}`);
|
|
459
|
+
|
|
460
|
+
// Commit the docs + dimension files + plain-english via a small agent (Bash git).
|
|
461
|
+
const commitAgent = await agent(
|
|
462
|
+
[
|
|
463
|
+
`Commit the GSD-T scan's generated documents in \`${projectDir}\` via Bash git, IF it is a git repo (else report skipped).`,
|
|
464
|
+
`Stage: \`.gsd-t/scan\`, \`.gsd-t/techdebt_in_plain_english.md\`, \`docs\`, \`README.md\` (do NOT stage \`.gsd-t/scan/.doc-backup\` if present). Commit message: "scan: deep document cross-population (${docsOk.length} docs) + dimension files". Do NOT push. Return JSON per the schema (status "rendered" if committed, "skipped" if not a git repo / nothing to commit, "failed" on error; outputPath optional).`,
|
|
465
|
+
].join("\n"),
|
|
466
|
+
{ label: "commit-docs", phase: "Document", schema: RENDER_SCHEMA, model: "haiku" }
|
|
467
|
+
).catch((e) => ({ status: "failed", notes: String(e && e.message) }));
|
|
468
|
+
log(`docs commit: ${commitAgent && commitAgent.status}`);
|
|
469
|
+
|
|
470
|
+
// NOTE (M71): the HTML render stage was REMOVED. The deterministic bin/scan-report.js
|
|
471
|
+
// renderer resolves its output path relative to the package dir (where the renderer
|
|
472
|
+
// modules live), not projectDir — so it wrote/overwrote `scan-report.html` in the
|
|
473
|
+
// GSD-T package instead of the target project (a data-loss risk: it clobbered the
|
|
474
|
+
// package's own committed report). The authoritative deliverables are the register +
|
|
475
|
+
// 5 dimension files + plain-english + living docs, all correctly written to
|
|
476
|
+
// projectDir. The fragile, wrong-tree HTML report is not worth that risk; dropped.
|
|
740
477
|
|
|
741
478
|
return {
|
|
742
|
-
|
|
743
|
-
|
|
479
|
+
// M72: status reflects coverage — a partial scan is NOT "complete".
|
|
480
|
+
status: coverageComplete ? "complete" : "complete-partial-coverage",
|
|
481
|
+
coverageComplete,
|
|
482
|
+
slicesTotal: slices.length,
|
|
483
|
+
slicesSucceeded: succeededCount,
|
|
484
|
+
slicesFailed: failedSlices, // names of un-scanned areas (empty if full coverage)
|
|
744
485
|
findings: allFindings.length,
|
|
745
486
|
counts: synthesis.counts,
|
|
746
|
-
|
|
747
|
-
archivePath:
|
|
748
|
-
docs: docResults,
|
|
487
|
+
tdRange: synthesis.tdRange,
|
|
488
|
+
archivePath: synthesis.archivePath || null,
|
|
749
489
|
docsWritten: docsOk.length,
|
|
750
490
|
docsFailed: docsFailed.map((d) => d.doc),
|
|
751
|
-
|
|
752
|
-
|
|
491
|
+
docsCommitted: commitAgent && commitAgent.status,
|
|
492
|
+
htmlReport: null, // render stage removed (M71)
|
|
753
493
|
probeTotals: probe.totals,
|
|
754
494
|
};
|