@tekyzinc/gsd-t 4.0.16 → 4.0.18
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 +33 -0
- package/commands/gsd-t-debug.md +3 -0
- package/commands/gsd-t-execute.md +3 -0
- package/commands/gsd-t-integrate.md +3 -0
- package/commands/gsd-t-scan.md +25 -3
- package/commands/gsd-t-verify.md +3 -0
- package/commands/gsd-t-wave.md +3 -0
- package/commands/gsd.md +1 -1
- package/package.json +1 -1
- package/templates/workflows/gsd-t-scan.workflow.js +267 -578
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,39 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to GSD-T are documented here. Updated with each release.
|
|
4
4
|
|
|
5
|
+
## [4.0.18] - 2026-06-02 (M71 Runtime-Native Scan Workflow — patch)
|
|
6
|
+
|
|
7
|
+
### Fixed — the scan Workflow now actually RUNS in the Workflow sandbox (it never did)
|
|
8
|
+
|
|
9
|
+
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:
|
|
10
|
+
|
|
11
|
+
- **`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.
|
|
12
|
+
- **`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).
|
|
13
|
+
- **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.
|
|
14
|
+
- **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.
|
|
15
|
+
- **`projectDir` pinned**: probe + finder prompts now hard-direct agents to operate only under the target path.
|
|
16
|
+
|
|
17
|
+
- `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.
|
|
18
|
+
|
|
19
|
+
**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.
|
|
20
|
+
|
|
21
|
+
**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).
|
|
22
|
+
|
|
23
|
+
Suite: 1293 pass / 0 fail / 4 skip — zero regressions.
|
|
24
|
+
|
|
25
|
+
## [4.0.17] - 2026-06-02 (M70 Workflow Invocation Guard — patch)
|
|
26
|
+
|
|
27
|
+
### Fixed — workflow commands no longer get hand-driven instead of invoking the Workflow
|
|
28
|
+
|
|
29
|
+
A brand-new session running `/gsd-t-scan` (via the `/gsd` auto-router) read the v4.0.16 command file and STILL hand-drove an 18-slice fan-out — "skip task-list overhead and drive the fan-out directly … proven fallback pattern" — skipping the deterministic synthesis/document/render stages, so it produced only the register (no `.gsd-t/scan/*.md`, no merged living docs, no plain-English doc). Two compounding causes: the command file's prose *described the workflow's internals* and read like a to-do list, and the `/gsd` router's "execute the command's full workflow" was ambiguous between "invoke the tool" and "do the work yourself."
|
|
30
|
+
|
|
31
|
+
- `commands/gsd-t-scan.md`: prepended a strong imperative guard — STOP, your only job is to resolve the path + call the `Workflow` tool; do NOT volume-probe, carve slices, spawn finders, or fall back to a hand-driven run; hand-driving is a FAILURE. Reframed the explanatory prose as "what the Workflow does (background — NOT your to-do list)."
|
|
32
|
+
- `commands/gsd-t-{execute,verify,wave,integrate,debug}.md`: concise version of the same guard near the top.
|
|
33
|
+
- `~/.claude/commands/gsd.md` (router): clarified that "execute the command's full workflow" means follow the command file's instruction to invoke the `Workflow` tool — not improvise the work.
|
|
34
|
+
- `test/m70-workflow-invocation-guard.test.js`: +7 tests asserting every workflow-backed command carries the guard near the top (regression lock).
|
|
35
|
+
|
|
36
|
+
Suite: 1285 pass / 0 fail / 4 skip — zero regressions.
|
|
37
|
+
|
|
5
38
|
## [4.0.16] - 2026-06-02 (M69 Workflow scriptPath Resolution — patch)
|
|
6
39
|
|
|
7
40
|
### Fixed — workflow commands now run from any project, not just the GSD-T source repo
|
package/commands/gsd-t-debug.md
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# GSD-T: Debug — Systematic Debugging with Contract Awareness
|
|
2
2
|
|
|
3
|
+
> **⛔ Invoke the Workflow tool — do not hand-drive.** Your only job is to resolve the workflow path (`gsd-t workflow-path debug`) and call the `Workflow` tool as the steps below instruct. Do NOT reconstruct the workflow stages in your own reasoning, spawn finder/worker subagents yourself, or fall back to a hand-driven run — that skips the deterministic stages and produces an incomplete result. The prose below describes what the Workflow does internally; it is background, not a to-do list for you.
|
|
4
|
+
|
|
5
|
+
|
|
3
6
|
You are the lead agent. Debug a failing test or runtime error by invoking the canonical Workflow script at `templates/workflows/gsd-t-debug.workflow.js`.
|
|
4
7
|
|
|
5
8
|
## What this command does
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# GSD-T: Execute — Run Domain Tasks
|
|
2
2
|
|
|
3
|
+
> **⛔ Invoke the Workflow tool — do not hand-drive.** Your only job is to resolve the workflow path (`gsd-t workflow-path execute`) and call the `Workflow` tool as the steps below instruct. Do NOT reconstruct the workflow stages in your own reasoning, spawn finder/worker subagents yourself, or fall back to a hand-driven run — that skips the deterministic stages and produces an incomplete result. The prose below describes what the Workflow does internally; it is background, not a to-do list for you.
|
|
4
|
+
|
|
5
|
+
|
|
3
6
|
You are the lead agent. Execute the current milestone by invoking the canonical Workflow script at `templates/workflows/gsd-t-execute.workflow.js`.
|
|
4
7
|
|
|
5
8
|
## What this command does
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# GSD-T: Integrate — Wire Domains Together
|
|
2
2
|
|
|
3
|
+
> **⛔ Invoke the Workflow tool — do not hand-drive.** Your only job is to resolve the workflow path (`gsd-t workflow-path integrate`) and call the `Workflow` tool as the steps below instruct. Do NOT reconstruct the workflow stages in your own reasoning, spawn finder/worker subagents yourself, or fall back to a hand-driven run — that skips the deterministic stages and produces an incomplete result. The prose below describes what the Workflow does internally; it is background, not a to-do list for you.
|
|
4
|
+
|
|
5
|
+
|
|
3
6
|
You are the lead agent. Integrate cross-domain work by invoking the canonical Workflow script at `templates/workflows/gsd-t-integrate.workflow.js`.
|
|
4
7
|
|
|
5
8
|
## What this command does
|
package/commands/gsd-t-scan.md
CHANGED
|
@@ -1,8 +1,30 @@
|
|
|
1
1
|
# GSD-T: Scan — Deep Codebase Analysis and Tech Debt Discovery
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
> ## ⛔ STOP — READ THIS FIRST. Your ONLY job is to invoke the Workflow tool.
|
|
4
|
+
>
|
|
5
|
+
> **You MUST NOT perform the scan yourself.** Do NOT run a volume probe by hand, do NOT
|
|
6
|
+
> "carve the codebase into N slices," do NOT spawn deep-finder agents/teammates yourself,
|
|
7
|
+
> do NOT "skip task-list overhead and drive the fan-out directly," and do NOT fall back to
|
|
8
|
+
> any hand-driven or "proven fallback" pattern. The slicing, fan-out, verify, synthesis, and
|
|
9
|
+
> document stages all happen **inside the Workflow script** — not in your reasoning.
|
|
10
|
+
>
|
|
11
|
+
> **The ONLY correct action is exactly two steps (Step 1 + Step 2 below):**
|
|
12
|
+
> 1. Bash: `gsd-t workflow-path scan` → capture the absolute path it prints.
|
|
13
|
+
> 2. Call the **`Workflow`** tool with that `scriptPath` and the `args` shown in Step 2.
|
|
14
|
+
>
|
|
15
|
+
> If you find yourself reading source files, counting files, listing routes, or spawning
|
|
16
|
+
> Agent/Task subagents to find tech debt — **STOP. You are doing it wrong.** That work belongs
|
|
17
|
+
> to the Workflow. Hand-driving the scan is a FAILURE: it skips the deterministic
|
|
18
|
+
> synthesis/document/render stages, so it produces an incomplete result (no
|
|
19
|
+
> `.gsd-t/scan/*.md` dimension files, no merged living docs, no plain-English doc).
|
|
20
|
+
>
|
|
21
|
+
> The prose below explains what the Workflow does internally — it is **background, not a
|
|
22
|
+
> to-do list for you.** Read it as "what will happen when I call the tool," never as "steps I
|
|
23
|
+
> should execute myself."
|
|
24
|
+
|
|
25
|
+
You are the lead agent. Your sole responsibility is to invoke the canonical scan Workflow (resolve its absolute path in Step 1, then call the `Workflow` tool in Step 2). Everything else is the Workflow's job.
|
|
26
|
+
|
|
27
|
+
## What the Workflow does (background — NOT your to-do list)
|
|
6
28
|
|
|
7
29
|
Replaces the legacy 5-teammate prose scan with a single deterministic, **volume-scaled** Workflow:
|
|
8
30
|
|
package/commands/gsd-t-verify.md
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# GSD-T: Verify — Quality Gates
|
|
2
2
|
|
|
3
|
+
> **⛔ Invoke the Workflow tool — do not hand-drive.** Your only job is to resolve the workflow path (`gsd-t workflow-path verify`) and call the `Workflow` tool as the steps below instruct. Do NOT reconstruct the workflow stages in your own reasoning, spawn finder/worker subagents yourself, or fall back to a hand-driven run — that skips the deterministic stages and produces an incomplete result. The prose below describes what the Workflow does internally; it is background, not a to-do list for you.
|
|
4
|
+
|
|
5
|
+
|
|
3
6
|
You are the lead agent. Verify the current milestone by invoking the canonical Workflow script at `templates/workflows/gsd-t-verify.workflow.js`.
|
|
4
7
|
|
|
5
8
|
## What this command does
|
package/commands/gsd-t-wave.md
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# GSD-T: Wave — Full Cycle Orchestration
|
|
2
2
|
|
|
3
|
+
> **⛔ Invoke the Workflow tool — do not hand-drive.** Your only job is to resolve the workflow path (`gsd-t workflow-path wave`) and call the `Workflow` tool as the steps below instruct. Do NOT reconstruct the workflow stages in your own reasoning, spawn finder/worker subagents yourself, or fall back to a hand-driven run — that skips the deterministic stages and produces an incomplete result. The prose below describes what the Workflow does internally; it is background, not a to-do list for you.
|
|
4
|
+
|
|
5
|
+
|
|
3
6
|
You are the lead agent. Run a milestone wave end-to-end by invoking the canonical Workflow script at `templates/workflows/gsd-t-wave.workflow.js`.
|
|
4
7
|
|
|
5
8
|
## What this command does
|
package/commands/gsd.md
CHANGED
|
@@ -196,7 +196,7 @@ Valid command slugs: `quick`, `debug`, `feature`, `execute`, `milestone`, `proje
|
|
|
196
196
|
→ /gsd ──▶ continue /gsd-t-quick
|
|
197
197
|
```
|
|
198
198
|
|
|
199
|
-
This MUST be the very first line of your response. Then immediately
|
|
199
|
+
This MUST be the very first line of your response. Then immediately **read and follow that command file's instructions** (`~/.claude/commands/gsd-t-{slug}.md`), passing `$ARGUMENTS` through. "Execute the command's full workflow" means **do what the command file says** — for workflow-backed commands (scan, execute, verify, wave, integrate, debug, and the phase-runner commands) that means **invoke the `Workflow` tool as the command instructs**. It does NOT mean improvise the work yourself, hand-drive a fan-out, or reconstruct the workflow's stages in your own reasoning. If the command file says "invoke the Workflow tool," you invoke the Workflow tool — full stop.
|
|
200
200
|
|
|
201
201
|
**Do NOT ask "is this the right command?" — just route and go.** The user can interrupt with Esc if it's wrong.
|
|
202
202
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tekyzinc/gsd-t",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.18",
|
|
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,235 +145,142 @@ 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
|
-
stderr: r.stderr && r.stderr.toString(),
|
|
185
|
-
};
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// ───── Script body ────────────────────────────────────────────────────────────
|
|
189
|
-
|
|
190
|
-
phase("Preflight");
|
|
191
|
-
const pre = lib.runPreflight({ projectDir });
|
|
192
|
-
if (!pre.ok) {
|
|
193
|
-
log(`preflight FAIL exitCode=${pre.exitCode} — halting scan`);
|
|
194
|
-
return { status: "failed", reason: "preflight-failed", preflight: pre.envelope };
|
|
195
|
-
}
|
|
196
|
-
log("preflight OK");
|
|
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
|
+
};
|
|
197
187
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
let tdStart = 1;
|
|
209
|
-
const registerPath = path.join(projectDir, ".gsd-t", "techdebt.md");
|
|
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
|
+
};
|
|
210
198
|
|
|
211
|
-
|
|
212
|
-
// Match TD-1, TD-01, TD-001, TD-117 in any "### TD-NNN" or "TD-NNN" form.
|
|
213
|
-
const nums = [];
|
|
214
|
-
const re = /TD-0*(\d+)/g;
|
|
215
|
-
let m;
|
|
216
|
-
while ((m = re.exec(text)) !== null) nums.push(parseInt(m[1], 10));
|
|
217
|
-
return nums.length ? Math.max(...nums) : 0;
|
|
218
|
-
}
|
|
219
|
-
function _archiveDateFrom(text, fallbackDate) {
|
|
220
|
-
// Prefer an explicit YYYY-MM-DD in the header; else use the fallback (mtime-derived).
|
|
221
|
-
const m = text.match(/\b(\d{4}-\d{2}-\d{2})\b/);
|
|
222
|
-
return (m && m[1]) || fallbackDate;
|
|
223
|
-
}
|
|
199
|
+
// ───── Script body — orchestration only, ZERO file I/O here ─────────────────
|
|
224
200
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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`);
|
|
201
|
+
// Preflight: an agent checks branch + whether a prior register exists, via Bash.
|
|
202
|
+
// (No fs in the body — that was the bug.)
|
|
203
|
+
phase("Preflight");
|
|
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 };
|
|
245
218
|
}
|
|
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}`);
|
|
246
221
|
|
|
247
|
-
//
|
|
222
|
+
// Volume probe — an agent measures the codebase (its own Bash) and carves slices.
|
|
248
223
|
phase("Probe");
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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");
|
|
278
|
-
|
|
279
|
-
// Red-team fix: probe runs on SONNET, not haiku. The probe's whole job is to
|
|
280
|
-
// measure volume and resist under-slicing; a haiku probe that under-counts a large
|
|
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
|
-
});
|
|
289
|
-
|
|
290
|
-
const slices = (probe && Array.isArray(probe.slices) && probe.slices) || [];
|
|
291
|
-
if (!slices.length) {
|
|
292
|
-
log("probe returned no slices — halting (cannot scan with an empty slice list)");
|
|
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");
|
|
293
248
|
return { status: "failed", reason: "no-slices", probe };
|
|
294
249
|
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
log(`⚠ COVERAGE NOTE: probe hit the ${maxSlicesHint}-slice cap and merged areas — see probe.notes: ${probe.notes}`);
|
|
303
|
-
} else {
|
|
304
|
-
log(`⚠ COVERAGE RISK: probe returned ${slices.length} slices (≥ cap ${maxSlicesHint}) with NO documented merges. Some areas may be under-covered. Re-run with a higher maxSlicesHint to confirm full coverage.`);
|
|
305
|
-
}
|
|
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.)`);
|
|
306
257
|
}
|
|
258
|
+
log(`probe derived ${rawSlices.length} slice(s); backstop cap=${computedCap}; running ${slices.length} deep-finder(s); totals=${JSON.stringify(probe.totals)}`);
|
|
307
259
|
|
|
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
260
|
const deep = budget && budget.total && budget.total > 300000 ? "MAXIMUM" : "thorough";
|
|
311
261
|
|
|
312
|
-
//
|
|
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.
|
|
262
|
+
// Deep scan — pipeline: per-slice deep finder → single verify (no barrier).
|
|
315
263
|
phase("Deep Scan");
|
|
316
264
|
const sliceResults = await pipeline(
|
|
317
265
|
slices,
|
|
318
|
-
// Stage 1 — deep finder. One agent OWNS one slice and enumerates exhaustively.
|
|
319
266
|
(slice) => agent(
|
|
320
267
|
[
|
|
321
|
-
|
|
322
|
-
`
|
|
268
|
+
`⛔ Scan ONLY files under the absolute project path \`${projectDir}\`. \`cd ${projectDir}\` first; never read outside this tree.`,
|
|
269
|
+
`You are a DEEP tech-debt finder for ONE slice of a scan of \`${projectDir}\`: \`${slice.key}\` (dimension: ${slice.dimension}).`,
|
|
270
|
+
`Owned paths (relative to \`${projectDir}\`): ${JSON.stringify(slice.paths)}.`,
|
|
323
271
|
slice.why ? `Why this slice matters: ${slice.why}` : ``,
|
|
324
272
|
``,
|
|
325
|
-
`MANDATE: ENUMERATE, do NOT sample.
|
|
326
|
-
`
|
|
327
|
-
`
|
|
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.`,
|
|
273
|
+
`MANDATE: ENUMERATE, do NOT sample. Read EVERY file under your owned paths (use Read/Grep). The legacy scan failed because one agent sampled ~5 issues across the whole repo and stopped — you own only this slice, so go to the bottom of it.`,
|
|
274
|
+
`Depth = ${deep}. "thorough" = every file, every non-trivial real defect (high+medium confidence). "MAXIMUM" = also lower-confidence/speculative items worth review.`,
|
|
275
|
+
`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).`,
|
|
276
|
+
`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 if genuinely clean. Return JSON per the schema.`,
|
|
344
277
|
].filter(Boolean).join("\n"),
|
|
345
278
|
{ label: `find:${slice.key}`, phase: "Deep Scan", schema: FINDER_SCHEMA, model: "sonnet" }
|
|
346
279
|
),
|
|
347
|
-
|
|
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) => {
|
|
280
|
+
async (finderResult, originalItem) => {
|
|
354
281
|
const slice = originalItem || {};
|
|
355
282
|
const sliceKey = slice.key || (finderResult && finderResult.slice) || "unknown-slice";
|
|
356
|
-
if (!finderResult || !Array.isArray(finderResult.findings)) {
|
|
357
|
-
return { slice: sliceKey, findings: [] };
|
|
358
|
-
}
|
|
283
|
+
if (!finderResult || !Array.isArray(finderResult.findings)) return { slice: sliceKey, findings: [] };
|
|
359
284
|
if (verifyMode === "none" || finderResult.findings.length === 0) {
|
|
360
285
|
return { slice: sliceKey, findings: finderResult.findings || [] };
|
|
361
286
|
}
|
|
@@ -364,391 +289,155 @@ const sliceResults = await pipeline(
|
|
|
364
289
|
try {
|
|
365
290
|
const v = await agent(
|
|
366
291
|
[
|
|
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
|
-
``,
|
|
292
|
+
`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
293
|
`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.`,
|
|
294
|
+
`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
295
|
].join("\n"),
|
|
377
296
|
{ label: `verify:${sliceKey}`, phase: "Deep Scan", schema: VERIFY_SCHEMA, model: "sonnet" }
|
|
378
297
|
);
|
|
379
298
|
if (!v || v.verdict === "false-positive" || v.confirmed === false) return null;
|
|
380
299
|
return { ...f, severity: v.correctedSeverity || f.severity, _verify: v.verdict };
|
|
381
300
|
} catch (e) {
|
|
382
|
-
// verify errored — keep the finding flagged rather than silently drop it
|
|
383
301
|
return { ...f, _verify: "verify-errored" };
|
|
384
302
|
}
|
|
385
303
|
})
|
|
386
304
|
);
|
|
387
|
-
return { slice: sliceKey, findings: verified.filter(Boolean)
|
|
305
|
+
return { slice: sliceKey, findings: verified.filter(Boolean) };
|
|
388
306
|
}
|
|
389
307
|
);
|
|
390
|
-
|
|
391
|
-
const allFindings = sliceResults
|
|
392
|
-
.filter(Boolean)
|
|
393
|
-
.flatMap((r) => (r.findings || []).map((f) => ({ ...f, slice: r.slice })));
|
|
308
|
+
const allFindings = sliceResults.filter(Boolean).flatMap((r) => (r.findings || []).map((f) => ({ ...f, slice: r.slice })));
|
|
394
309
|
log(`deep scan complete: ${allFindings.length} verified findings across ${sliceResults.filter(Boolean).length} slices`);
|
|
395
310
|
|
|
396
|
-
//
|
|
311
|
+
// Synthesis — an agent does the ARCHIVE + REGISTER WRITE + GIT entirely via its
|
|
312
|
+
// own Bash/Write tools. The orchestrator does NOT touch fs. The agent is given the
|
|
313
|
+
// deterministic tdStart so numbering can't drift, and is told to archive FIRST.
|
|
397
314
|
phase("Synthesis");
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
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",
|
|
453
|
-
});
|
|
454
|
-
|
|
315
|
+
const findingsJson = JSON.stringify(allFindings, null, 2);
|
|
316
|
+
const synthesis = await agent(
|
|
317
|
+
[
|
|
318
|
+
`You are the SYNTHESIS agent for a GSD-T deep scan of \`${projectDir}\`. ${slices.length} slices ran; ${allFindings.length} verified findings came back.`,
|
|
319
|
+
scanNumber ? `This is scan #${scanNumber} — put it in the register header.` : ``,
|
|
320
|
+
``,
|
|
321
|
+
`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.)"}`,
|
|
322
|
+
``,
|
|
323
|
+
`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: 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.`,
|
|
324
|
+
``,
|
|
325
|
+
`STEP 3 — COMMIT via Bash if it's a git repo (\`git add .gsd-t/techdebt.md\` + the archive; commit). Do NOT push.`,
|
|
326
|
+
``,
|
|
327
|
+
`Verified findings:`,
|
|
328
|
+
"```json",
|
|
329
|
+
findingsJson.length > 200000 ? findingsJson.slice(0, 200000) + "\n…(truncated)" : findingsJson,
|
|
330
|
+
"```",
|
|
331
|
+
``,
|
|
332
|
+
`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").`,
|
|
333
|
+
].filter(Boolean).join("\n"),
|
|
334
|
+
{ label: "synthesis", phase: "Synthesis", schema: SYNTHESIS_SCHEMA, model: "opus" }
|
|
335
|
+
);
|
|
455
336
|
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 };
|
|
337
|
+
log("synthesis did not write the register — halting before document phase");
|
|
338
|
+
return { status: "failed", reason: "synthesis-failed", synthesis, findingCount: allFindings.length };
|
|
463
339
|
}
|
|
464
|
-
log(`register written: ${JSON.stringify(synthesis.counts)}`);
|
|
465
|
-
|
|
466
|
-
// Read the synthesized register so the plain-English doc can mirror its EXACT
|
|
467
|
-
// TD-NNN ids / order (red-team HIGH-1: raw findings carry no TD ids, so a doc
|
|
468
|
-
// built only from findings would invent divergent numbering and break the
|
|
469
|
-
// cross-reference to techdebt.md). The register exists here (confirmed above).
|
|
470
|
-
let registerText = "";
|
|
471
|
-
try { registerText = fs.readFileSync(registerPath, "utf8"); } catch (_) {}
|
|
340
|
+
log(`register written: ${JSON.stringify(synthesis.counts)} (${synthesis.tdRange || ""})`);
|
|
472
341
|
|
|
473
|
-
//
|
|
474
|
-
//
|
|
475
|
-
//
|
|
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.
|
|
342
|
+
// Document — per-doc fan-out. Each agent writes its file via Write/Edit (its tools).
|
|
343
|
+
// The orchestrator passes the findings + (for plain-english) tells the agent to
|
|
344
|
+
// READ the just-written register itself, since the orchestrator can't read it.
|
|
480
345
|
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
346
|
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
347
|
const baseCtx = [
|
|
531
348
|
`Project: \`${projectDir}\`. Probe totals: ${JSON.stringify(probe.totals)}.`,
|
|
532
|
-
|
|
533
|
-
`Slices the scan covered (your raw material — each names the paths it owns):`,
|
|
349
|
+
`Slices the scan covered:`,
|
|
534
350
|
sliceSummary,
|
|
535
|
-
|
|
536
|
-
`Verified findings (truncated):`,
|
|
351
|
+
`Verified findings:`,
|
|
537
352
|
"```json",
|
|
538
|
-
findingsJson,
|
|
353
|
+
findingsJson.length > 120000 ? findingsJson.slice(0, 120000) + "\n…(truncated)" : findingsJson,
|
|
539
354
|
"```",
|
|
540
355
|
].join("\n");
|
|
541
356
|
|
|
357
|
+
const mergeNote =
|
|
358
|
+
`If the target file already exists with real content: MERGE using the Edit tool ` +
|
|
359
|
+
`(targeted section edits) — do NOT overwrite with Write (that destroys content you ` +
|
|
360
|
+
`didn't reproduce). If it's placeholder/template-only, you may replace. If absent, create. ` +
|
|
361
|
+
`Replace {Project Name}/{Date} tokens with real values. Derive everything from the slices, ` +
|
|
362
|
+
`findings, and the actual code you read — invent nothing.`;
|
|
363
|
+
|
|
542
364
|
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
|
-
},
|
|
365
|
+
{ id: "scan-architecture", label: "scan:architecture",
|
|
366
|
+
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.` },
|
|
367
|
+
{ id: "scan-security", label: "scan:security",
|
|
368
|
+
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.` },
|
|
369
|
+
{ id: "scan-quality", label: "scan:quality",
|
|
370
|
+
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.` },
|
|
371
|
+
{ id: "scan-business-rules", label: "scan:business-rules",
|
|
372
|
+
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.` },
|
|
373
|
+
{ id: "scan-contract-drift", label: "scan:contract-drift",
|
|
374
|
+
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.` },
|
|
375
|
+
{ id: "docs-architecture", label: "docs/architecture.md", merge: true,
|
|
376
|
+
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.` },
|
|
377
|
+
{ id: "docs-workflows", label: "docs/workflows.md", merge: true,
|
|
378
|
+
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.` },
|
|
379
|
+
{ id: "docs-infrastructure", label: "docs/infrastructure.md", merge: true,
|
|
380
|
+
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.` },
|
|
381
|
+
{ id: "docs-requirements", label: "docs/requirements.md", merge: true,
|
|
382
|
+
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.` },
|
|
383
|
+
{ id: "readme", label: "README.md", merge: true,
|
|
384
|
+
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.` },
|
|
385
|
+
{ id: "techdebt-plain-english", label: ".gsd-t/techdebt_in_plain_english.md", needsRegister: true,
|
|
386
|
+
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
387
|
];
|
|
599
388
|
|
|
600
389
|
const docResults = await parallel(
|
|
601
390
|
docTargets.map((d) => async () => {
|
|
602
391
|
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
392
|
const prompt = [
|
|
628
|
-
`You are the documentation agent for ONE document in a GSD-T deep scan
|
|
629
|
-
``,
|
|
393
|
+
`You are the documentation agent for ONE document in a GSD-T deep scan of \`${projectDir}\`.`,
|
|
630
394
|
baseCtx,
|
|
631
|
-
|
|
395
|
+
d.needsRegister ? `\nThe synthesized register lives at \`${projectDir}/.gsd-t/techdebt.md\` — READ it first for the authoritative TD-NNN ids/order.` : ``,
|
|
632
396
|
``,
|
|
633
397
|
d.prompt,
|
|
634
398
|
``,
|
|
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");
|
|
399
|
+
isLiving ? mergeNote : `Write the file fresh in the format described (use Bash \`mkdir -p\` for parent dirs if needed).`,
|
|
400
|
+
`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.`,
|
|
401
|
+
].filter(Boolean).join("\n");
|
|
643
402
|
try {
|
|
644
|
-
return await agent(prompt, {
|
|
645
|
-
label: d.label,
|
|
646
|
-
phase: "Document",
|
|
647
|
-
schema: DOC_RESULT_SCHEMA,
|
|
648
|
-
model: "sonnet",
|
|
649
|
-
});
|
|
403
|
+
return await agent(prompt, { label: d.label, phase: "Document", schema: DOC_RESULT_SCHEMA, model: "sonnet" });
|
|
650
404
|
} catch (e) {
|
|
651
|
-
return { doc: d.id, status: "failed",
|
|
405
|
+
return { doc: d.id, status: "failed", notes: `agent error: ${e && e.message}` };
|
|
652
406
|
}
|
|
653
407
|
})
|
|
654
408
|
);
|
|
655
|
-
|
|
656
409
|
const docsOk = docResults.filter(Boolean).filter((r) => r.status === "written" || r.status === "merged");
|
|
657
410
|
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
|
-
}
|
|
411
|
+
log(`document phase: ${docsOk.length}/${docTargets.length} written/merged${docsFailed.length ? `; ${docsFailed.length} failed (non-fatal): ${docsFailed.map((d) => d.doc).join(", ")}` : ""}`);
|
|
412
|
+
|
|
413
|
+
// Commit the docs + dimension files + plain-english via a small agent (Bash git).
|
|
414
|
+
const commitAgent = await agent(
|
|
415
|
+
[
|
|
416
|
+
`Commit the GSD-T scan's generated documents in \`${projectDir}\` via Bash git, IF it is a git repo (else report skipped).`,
|
|
417
|
+
`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).`,
|
|
418
|
+
].join("\n"),
|
|
419
|
+
{ label: "commit-docs", phase: "Document", schema: RENDER_SCHEMA, model: "haiku" }
|
|
420
|
+
).catch((e) => ({ status: "failed", notes: String(e && e.message) }));
|
|
421
|
+
log(`docs commit: ${commitAgent && commitAgent.status}`);
|
|
422
|
+
|
|
423
|
+
// NOTE (M71): the HTML render stage was REMOVED. The deterministic bin/scan-report.js
|
|
424
|
+
// renderer resolves its output path relative to the package dir (where the renderer
|
|
425
|
+
// modules live), not projectDir — so it wrote/overwrote `scan-report.html` in the
|
|
426
|
+
// GSD-T package instead of the target project (a data-loss risk: it clobbered the
|
|
427
|
+
// package's own committed report). The authoritative deliverables are the register +
|
|
428
|
+
// 5 dimension files + plain-english + living docs, all correctly written to
|
|
429
|
+
// projectDir. The fragile, wrong-tree HTML report is not worth that risk; dropped.
|
|
740
430
|
|
|
741
431
|
return {
|
|
742
432
|
status: "complete",
|
|
743
433
|
slices: slices.length,
|
|
744
434
|
findings: allFindings.length,
|
|
745
435
|
counts: synthesis.counts,
|
|
746
|
-
|
|
747
|
-
archivePath:
|
|
748
|
-
docs: docResults,
|
|
436
|
+
tdRange: synthesis.tdRange,
|
|
437
|
+
archivePath: synthesis.archivePath || null,
|
|
749
438
|
docsWritten: docsOk.length,
|
|
750
439
|
docsFailed: docsFailed.map((d) => d.doc),
|
|
751
|
-
|
|
752
|
-
|
|
440
|
+
docsCommitted: commitAgent && commitAgent.status,
|
|
441
|
+
htmlReport: null, // render stage removed (M71)
|
|
753
442
|
probeTotals: probe.totals,
|
|
754
443
|
};
|