@tekyzinc/gsd-t 5.6.10 → 5.6.11
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 +15 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/templates/workflows/gsd-t-debug.workflow.js +28 -2
- package/templates/workflows/gsd-t-execute.workflow.js +28 -2
- package/templates/workflows/gsd-t-integrate.workflow.js +28 -2
- package/templates/workflows/gsd-t-phase.workflow.js +77 -12
- package/templates/workflows/gsd-t-quick.workflow.js +28 -2
- package/templates/workflows/gsd-t-verify.workflow.js +28 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to GSD-T are documented here. Updated with each release.
|
|
4
4
|
|
|
5
|
+
## [5.6.11] - 2026-08-02
|
|
6
|
+
|
|
7
|
+
### Fixed — four bugs that reported a healthy code graph as broken (M104)
|
|
8
|
+
|
|
9
|
+
A partition and plan phase halted four separate times with `graph BROKEN` while the graph was fine — exit 0, `ok:true`, compiler-accurate. Four distinct bugs, all producing the identical message, each sitting downstream of the last, so every fix only revealed the next.
|
|
10
|
+
|
|
11
|
+
- `templates/workflows/gsd-t-phase.workflow.js`:
|
|
12
|
+
- **The verb map pointed five phases at graph queries that require a target.** `plan`, `impact`, `feature`, `populate` and `promote-debt` mapped to `who-imports` / `blast-radius`, which return `{"ok":false,"reason":"missing-target"}` when called without one. A comment above the map asserted these "return the full graph slice" without a target — that was never true, and nobody checked it against the CLI. **These five phases never once completed their graph query in any project since the map shipped.** All now use `cluster`, which answers target-free.
|
|
13
|
+
- **The verb travelled in the wrong argument.** `runCli` builds the local-bin command from `argv` alone, so with the verb in the subcommand string and `argv` empty, any project carrying its own `bin/gsd-t-graph-query-cli.cjs` ran it with no verb at all → `{"ok":false,"reason":"no-verb"}`. This failed only in projects with a local copy, which is why it stayed invisible elsewhere.
|
|
14
|
+
- `gsd-t-phase`, `-integrate`, `-debug`, `-execute`, `-quick`, `-verify`: the CLI-result envelope was typed as `{}` — a schema accepting anything. A helper agent that returned the raw JSON *text*, or omitted the field and left the JSON in `stdout`, validated as cleanly as a correct parse; callers then read `ok` on a string or on nothing, got `undefined`, and failed closed on a healthy command. The field is now typed `object|null`, and `_coerceCliResult` normalizes both shapes at the single point every caller passes through. A genuinely failing command still fails; non-JSON output is left alone.
|
|
15
|
+
- `test/m104-phase-graph-verb-map.test.js`: 24 tests. Every mapped verb must answer target-free; the false comment cannot return; the verb must travel in `argv`; all six workflows must type their envelope and actually *call* the normalizer (defined-but-unwired fails).
|
|
16
|
+
- `test/m94-d10-reader-wiring.test.js`: rewritten. It previously hardcoded the broken pairs (`plan → who-imports`, `impact → blast-radius`) and asserted they were present — passing on every run while those phases were dead. Restating a buggy value in a test is what let the bug live; it now asserts coverage and leaves verb correctness to the M104 tests, which check the CLI's real behaviour.
|
|
17
|
+
|
|
18
|
+
Five other workflows carried the same permissive schema and could have failed identically — the phase workflow simply makes the largest graph query and hit it first. Suite 3107/0/13-skip.
|
|
19
|
+
|
|
5
20
|
## [5.6.10] - 2026-08-02
|
|
6
21
|
|
|
7
22
|
### Changed — the Environment Registry now fires (M103)
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# GSD-T: Contract-Driven Development for Claude Code
|
|
2
2
|
|
|
3
|
-
**v5.6.
|
|
3
|
+
**v5.6.11** - A methodology for reliable, parallelizable development using Claude Code with optional Agent Teams support.
|
|
4
4
|
|
|
5
5
|
**Eliminates context rot** — task-level fresh dispatch (one subagent per task, ~10-20% context each) means compaction never triggers.
|
|
6
6
|
**Compaction-proof debug loops** — `gsd-t headless --debug-loop` runs test-fix-retest cycles as separate `claude -p` sessions. A JSONL debug ledger persists all hypothesis/fix/learning history across fresh sessions. Anti-repetition preamble injection prevents retrying failed hypotheses. Escalation tiers (sonnet → opus → human) and a hard iteration ceiling enforced externally.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tekyzinc/gsd-t",
|
|
3
|
-
"version": "5.6.
|
|
3
|
+
"version": "5.6.11",
|
|
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",
|
|
@@ -61,8 +61,34 @@ const _args = (typeof args === "string") ? (() => { try { return JSON.parse(args
|
|
|
61
61
|
const overrides = (_args.overrides && typeof _args.overrides === "object") ? _args.overrides : {};
|
|
62
62
|
const _CLI_ENVELOPE_SCHEMA = {
|
|
63
63
|
type: "object", required: ["ok", "exitCode"], additionalProperties: true,
|
|
64
|
-
properties: { ok: { type: "boolean" }, exitCode: { type: "integer" }, envelope: {}, stdout: { type: "string" }, stderr: { type: "string" }, via: { type: "string" } },
|
|
64
|
+
properties: { ok: { type: "boolean" }, exitCode: { type: "integer" }, envelope: { type: ["object", "null"], description: "stdout PARSED as JSON — an object, never the raw text" }, stdout: { type: "string" }, stderr: { type: "string" }, via: { type: "string" } },
|
|
65
65
|
};
|
|
66
|
+
// ─── Normalize a helper-agent CLI result (M104) ──────────────────────────────
|
|
67
|
+
//
|
|
68
|
+
// runCli asks a haiku helper to run a command and hand back the parsed JSON in
|
|
69
|
+
// `envelope`. A model doing that job is not perfectly reliable, and two broken
|
|
70
|
+
// shapes both read `env.ok === undefined` — which every caller treats as failure,
|
|
71
|
+
// so a HEALTHY CLI reports as broken:
|
|
72
|
+
// (a) `envelope` holds the raw JSON TEXT rather than the parsed object
|
|
73
|
+
// (b) `envelope` is absent and the JSON only reached `stdout`
|
|
74
|
+
// Observed live 2026-08-02: a partition/plan halted four times on "graph BROKEN"
|
|
75
|
+
// while the graph was fine (exit 0, ok:true).
|
|
76
|
+
//
|
|
77
|
+
// NOT a fallback: it continues past no failure and substitutes no guess — it reads
|
|
78
|
+
// the result the CLI actually returned. A failing CLI still returns ok:false and
|
|
79
|
+
// still fails; non-JSON stdout is left alone.
|
|
80
|
+
function _coerceCliResult(x) {
|
|
81
|
+
if (!x) return x;
|
|
82
|
+
if (typeof x.envelope === "string") {
|
|
83
|
+
try { x.envelope = JSON.parse(x.envelope); } catch (_) { /* genuinely not JSON */ }
|
|
84
|
+
}
|
|
85
|
+
if ((x.envelope === undefined || x.envelope === null) &&
|
|
86
|
+
typeof x.stdout === "string" && x.stdout.trim().startsWith("{")) {
|
|
87
|
+
try { x.envelope = JSON.parse(x.stdout); } catch (_) { /* not JSON */ }
|
|
88
|
+
}
|
|
89
|
+
return x;
|
|
90
|
+
}
|
|
91
|
+
|
|
66
92
|
async function runCli(projectDir, subcmd, argv, localBin, label, parseJson = true, phaseName) {
|
|
67
93
|
const argStr = (argv || []).map((a) => `'${String(a).replace(/'/g, "'\\''")}'`).join(" ");
|
|
68
94
|
const prompt = [
|
|
@@ -75,7 +101,7 @@ async function runCli(projectDir, subcmd, argv, localBin, label, parseJson = tru
|
|
|
75
101
|
const opts = { label, schema: _CLI_ENVELOPE_SCHEMA, model: "haiku" };
|
|
76
102
|
if (phaseName) opts.phase = phaseName;
|
|
77
103
|
const r = await agent(prompt, opts).catch((e) => ({ ok: false, exitCode: -1, envelope: null, stderr: String(e && e.message), via: "error" }));
|
|
78
|
-
return r || { ok: false, exitCode: -1, envelope: null, via: "error" };
|
|
104
|
+
return _coerceCliResult(r) || { ok: false, exitCode: -1, envelope: null, via: "error" };
|
|
79
105
|
}
|
|
80
106
|
async function runPreflight(projectDir, label = "preflight", phaseName) { return runCli(projectDir, "preflight", ["--json"], "cli-preflight.cjs", label, true, phaseName); }
|
|
81
107
|
async function generateBrief(projectDir, { kind = "execute", milestone, domain, id, label = "brief", phaseName } = {}) {
|
|
@@ -190,8 +190,34 @@ const INTEGRATE_RESULT_SCHEMA = {
|
|
|
190
190
|
const _args = (typeof args === "string") ? (() => { try { return JSON.parse(args); } catch { return {}; } })() : (args || {});
|
|
191
191
|
const _CLI_ENVELOPE_SCHEMA = {
|
|
192
192
|
type: "object", required: ["ok", "exitCode"], additionalProperties: true,
|
|
193
|
-
properties: { ok: { type: "boolean" }, exitCode: { type: "integer" }, envelope: {}, stdout: { type: "string" }, stderr: { type: "string" }, via: { type: "string" } },
|
|
193
|
+
properties: { ok: { type: "boolean" }, exitCode: { type: "integer" }, envelope: { type: ["object", "null"], description: "stdout PARSED as JSON — an object, never the raw text" }, stdout: { type: "string" }, stderr: { type: "string" }, via: { type: "string" } },
|
|
194
194
|
};
|
|
195
|
+
// ─── Normalize a helper-agent CLI result (M104) ──────────────────────────────
|
|
196
|
+
//
|
|
197
|
+
// runCli asks a haiku helper to run a command and hand back the parsed JSON in
|
|
198
|
+
// `envelope`. A model doing that job is not perfectly reliable, and two broken
|
|
199
|
+
// shapes both read `env.ok === undefined` — which every caller treats as failure,
|
|
200
|
+
// so a HEALTHY CLI reports as broken:
|
|
201
|
+
// (a) `envelope` holds the raw JSON TEXT rather than the parsed object
|
|
202
|
+
// (b) `envelope` is absent and the JSON only reached `stdout`
|
|
203
|
+
// Observed live 2026-08-02: a partition/plan halted four times on "graph BROKEN"
|
|
204
|
+
// while the graph was fine (exit 0, ok:true).
|
|
205
|
+
//
|
|
206
|
+
// NOT a fallback: it continues past no failure and substitutes no guess — it reads
|
|
207
|
+
// the result the CLI actually returned. A failing CLI still returns ok:false and
|
|
208
|
+
// still fails; non-JSON stdout is left alone.
|
|
209
|
+
function _coerceCliResult(x) {
|
|
210
|
+
if (!x) return x;
|
|
211
|
+
if (typeof x.envelope === "string") {
|
|
212
|
+
try { x.envelope = JSON.parse(x.envelope); } catch (_) { /* genuinely not JSON */ }
|
|
213
|
+
}
|
|
214
|
+
if ((x.envelope === undefined || x.envelope === null) &&
|
|
215
|
+
typeof x.stdout === "string" && x.stdout.trim().startsWith("{")) {
|
|
216
|
+
try { x.envelope = JSON.parse(x.stdout); } catch (_) { /* not JSON */ }
|
|
217
|
+
}
|
|
218
|
+
return x;
|
|
219
|
+
}
|
|
220
|
+
|
|
195
221
|
async function runCli(projectDir, subcmd, argv, localBin, label, parseJson = true, phaseName) {
|
|
196
222
|
const argStr = (argv || []).map((a) => `'${String(a).replace(/'/g, "'\\''")}'`).join(" ");
|
|
197
223
|
const prompt = [
|
|
@@ -204,7 +230,7 @@ async function runCli(projectDir, subcmd, argv, localBin, label, parseJson = tru
|
|
|
204
230
|
const opts = { label, schema: _CLI_ENVELOPE_SCHEMA, model: "haiku" };
|
|
205
231
|
if (phaseName) opts.phase = phaseName;
|
|
206
232
|
const r = await agent(prompt, opts).catch((e) => ({ ok: false, exitCode: -1, envelope: null, stderr: String(e && e.message), via: "error" }));
|
|
207
|
-
return r || { ok: false, exitCode: -1, envelope: null, via: "error" };
|
|
233
|
+
return _coerceCliResult(r) || { ok: false, exitCode: -1, envelope: null, via: "error" };
|
|
208
234
|
}
|
|
209
235
|
async function runPreflight(projectDir, label = "preflight", phaseName) { return runCli(projectDir, "preflight", ["--json"], "cli-preflight.cjs", label, true, phaseName); }
|
|
210
236
|
async function runVerifyGate(projectDir, label = "verify-gate", phaseName) { return runCli(projectDir, "verify-gate", ["--json"], "gsd-t-verify-gate.cjs", label, true, phaseName); }
|
|
@@ -22,8 +22,34 @@ export const meta = {
|
|
|
22
22
|
const _args = (typeof args === "string") ? (() => { try { return JSON.parse(args); } catch { return {}; } })() : (args || {});
|
|
23
23
|
const _CLI_ENVELOPE_SCHEMA = {
|
|
24
24
|
type: "object", required: ["ok", "exitCode"], additionalProperties: true,
|
|
25
|
-
properties: { ok: { type: "boolean" }, exitCode: { type: "integer" }, envelope: {}, stdout: { type: "string" }, stderr: { type: "string" }, via: { type: "string" } },
|
|
25
|
+
properties: { ok: { type: "boolean" }, exitCode: { type: "integer" }, envelope: { type: ["object", "null"], description: "stdout PARSED as JSON — an object, never the raw text" }, stdout: { type: "string" }, stderr: { type: "string" }, via: { type: "string" } },
|
|
26
26
|
};
|
|
27
|
+
// ─── Normalize a helper-agent CLI result (M104) ──────────────────────────────
|
|
28
|
+
//
|
|
29
|
+
// runCli asks a haiku helper to run a command and hand back the parsed JSON in
|
|
30
|
+
// `envelope`. A model doing that job is not perfectly reliable, and two broken
|
|
31
|
+
// shapes both read `env.ok === undefined` — which every caller treats as failure,
|
|
32
|
+
// so a HEALTHY CLI reports as broken:
|
|
33
|
+
// (a) `envelope` holds the raw JSON TEXT rather than the parsed object
|
|
34
|
+
// (b) `envelope` is absent and the JSON only reached `stdout`
|
|
35
|
+
// Observed live 2026-08-02: a partition/plan halted four times on "graph BROKEN"
|
|
36
|
+
// while the graph was fine (exit 0, ok:true).
|
|
37
|
+
//
|
|
38
|
+
// NOT a fallback: it continues past no failure and substitutes no guess — it reads
|
|
39
|
+
// the result the CLI actually returned. A failing CLI still returns ok:false and
|
|
40
|
+
// still fails; non-JSON stdout is left alone.
|
|
41
|
+
function _coerceCliResult(x) {
|
|
42
|
+
if (!x) return x;
|
|
43
|
+
if (typeof x.envelope === "string") {
|
|
44
|
+
try { x.envelope = JSON.parse(x.envelope); } catch (_) { /* genuinely not JSON */ }
|
|
45
|
+
}
|
|
46
|
+
if ((x.envelope === undefined || x.envelope === null) &&
|
|
47
|
+
typeof x.stdout === "string" && x.stdout.trim().startsWith("{")) {
|
|
48
|
+
try { x.envelope = JSON.parse(x.stdout); } catch (_) { /* not JSON */ }
|
|
49
|
+
}
|
|
50
|
+
return x;
|
|
51
|
+
}
|
|
52
|
+
|
|
27
53
|
async function runCli(projectDir, subcmd, argv, localBin, label, parseJson = true, phaseName) {
|
|
28
54
|
const argStr = (argv || []).map((a) => `'${String(a).replace(/'/g, "'\\''")}'`).join(" ");
|
|
29
55
|
const prompt = [
|
|
@@ -36,7 +62,7 @@ async function runCli(projectDir, subcmd, argv, localBin, label, parseJson = tru
|
|
|
36
62
|
const opts = { label, schema: _CLI_ENVELOPE_SCHEMA, model: "haiku" };
|
|
37
63
|
if (phaseName) opts.phase = phaseName;
|
|
38
64
|
const r = await agent(prompt, opts).catch((e) => ({ ok: false, exitCode: -1, envelope: null, stderr: String(e && e.message), via: "error" }));
|
|
39
|
-
return r || { ok: false, exitCode: -1, envelope: null, via: "error" };
|
|
65
|
+
return _coerceCliResult(r) || { ok: false, exitCode: -1, envelope: null, via: "error" };
|
|
40
66
|
}
|
|
41
67
|
async function runPreflight(projectDir, label = "preflight", phaseName) { return runCli(projectDir, "preflight", ["--json"], "cli-preflight.cjs", label, true, phaseName); }
|
|
42
68
|
// Broken-Graph-Halts (EXEMPT carve-out): integrate is additive/announced — it does not
|
|
@@ -85,12 +85,57 @@ const _args = (typeof args === "string") ? (() => { try { return JSON.parse(args
|
|
|
85
85
|
// are tier ALIASES. The sandbox runtime accepts BOTH forms in model: — proven live for
|
|
86
86
|
// the tier alias resolves to claude-opus-5 (Fable removed 2026-07-24).
|
|
87
87
|
const overrides = (_args.overrides && typeof _args.overrides === "object") ? _args.overrides : {};
|
|
88
|
+
// `envelope` is typed as an OBJECT (or null), not "any".
|
|
89
|
+
//
|
|
90
|
+
// It used to be `envelope: {}` — a schema that accepts everything. A helper that
|
|
91
|
+
// returned the raw JSON *text*, or omitted the field entirely, passed validation
|
|
92
|
+
// just as cleanly as a correct parse; every caller then read `env.ok` on a string
|
|
93
|
+
// or on undefined, got undefined, and failed closed on a HEALTHY CLI. Three
|
|
94
|
+
// separate live halts traced back to this one permissive field.
|
|
95
|
+
//
|
|
96
|
+
// Typing it means the model is told the shape and a wrong shape is visible rather
|
|
97
|
+
// than silently equivalent. `_coerceCliResult` still normalizes the two shapes
|
|
98
|
+
// seen in the wild — the type is the specification, the coercion is the repair.
|
|
88
99
|
const _CLI_ENVELOPE_SCHEMA = {
|
|
89
100
|
type: "object", required: ["ok", "exitCode"], additionalProperties: true,
|
|
90
|
-
properties: {
|
|
101
|
+
properties: {
|
|
102
|
+
ok: { type: "boolean" },
|
|
103
|
+
exitCode: { type: "integer" },
|
|
104
|
+
envelope: { type: ["object", "null"], description: "stdout PARSED as JSON — an object, never the raw text" },
|
|
105
|
+
stdout: { type: "string" },
|
|
106
|
+
stderr: { type: "string" },
|
|
107
|
+
via: { type: "string" },
|
|
108
|
+
},
|
|
91
109
|
};
|
|
92
110
|
// Single-quote a value for safe shell interpolation (Red Team MED-5).
|
|
93
111
|
function _shq(s) { return `'${String(s).replace(/'/g, "'\\''")}'`; }
|
|
112
|
+
// ─── Normalize a helper-agent CLI result ─────────────────────────────────────
|
|
113
|
+
//
|
|
114
|
+
// `runCli` asks a haiku helper to run a command and hand back the parsed JSON in
|
|
115
|
+
// `envelope`. A language model doing that job is not perfectly reliable, and the
|
|
116
|
+
// schema types `envelope` as "any" — so THREE different shapes all validate and
|
|
117
|
+
// only one is usable. Both broken shapes read `env.ok === undefined`, which every
|
|
118
|
+
// caller treats as failure, so a HEALTHY CLI reports as broken:
|
|
119
|
+
// (a) `envelope` holds the raw JSON TEXT rather than the parsed object
|
|
120
|
+
// (b) `envelope` is absent and the JSON only reached `stdout`
|
|
121
|
+
// Observed in the wild 2026-08-02: a binvoice partition halted three times on
|
|
122
|
+
// "graph BROKEN" while the graph was fine (exit 0, ok:true).
|
|
123
|
+
//
|
|
124
|
+
// This normalizes both shapes at the ONE place every caller passes through. It is
|
|
125
|
+
// NOT a fallback: it does not continue past a failure or substitute a guess — it
|
|
126
|
+
// reads the result the CLI actually returned. A genuinely failing CLI still
|
|
127
|
+
// returns `ok:false` and still fails; a non-JSON stdout is left alone.
|
|
128
|
+
function _coerceCliResult(x) {
|
|
129
|
+
if (!x) return x;
|
|
130
|
+
if (typeof x.envelope === "string") {
|
|
131
|
+
try { x.envelope = JSON.parse(x.envelope); } catch (_) { /* genuinely not JSON — leave it */ }
|
|
132
|
+
}
|
|
133
|
+
if ((x.envelope === undefined || x.envelope === null) &&
|
|
134
|
+
typeof x.stdout === "string" && x.stdout.trim().startsWith("{")) {
|
|
135
|
+
try { x.envelope = JSON.parse(x.stdout); } catch (_) { /* not JSON — leave envelope absent */ }
|
|
136
|
+
}
|
|
137
|
+
return x;
|
|
138
|
+
}
|
|
94
139
|
async function runCli(projectDir, subcmd, argv, localBin, label, parseJson = true, phaseNameOpt) {
|
|
95
140
|
const argStr = (argv || []).map((a) => `'${String(a).replace(/'/g, "'\\''")}'`).join(" ");
|
|
96
141
|
const prompt = [
|
|
@@ -108,14 +153,14 @@ async function runCli(projectDir, subcmd, argv, localBin, label, parseJson = tru
|
|
|
108
153
|
// while a transient helper miss is recovered. Only retry when JSON was expected (parseJson)
|
|
109
154
|
// and the parsed result is absent; never retry on a clean exit that simply returned no JSON.
|
|
110
155
|
const runOnce = () => agent(prompt, opts).catch((e) => ({ ok: false, exitCode: -1, envelope: null, stderr: String(e && e.message), via: "error" }));
|
|
111
|
-
let r = await runOnce();
|
|
156
|
+
let r = _coerceCliResult(await runOnce());
|
|
112
157
|
// Retry once when JSON was expected but no parsed result came back — covers both the
|
|
113
158
|
// throw path (via="error") and a malformed return the loose schema let through (ok=false
|
|
114
159
|
// with the result absent). A real CLI failure that returned valid JSON (envelope present,
|
|
115
160
|
// ok=false) is NOT retried — that is a true result, not a transient miss.
|
|
116
161
|
const missingResult = (x) => !x || (parseJson && (x.envelope === undefined || x.envelope === null) && x.ok !== true);
|
|
117
162
|
if (missingResult(r)) {
|
|
118
|
-
r = await runOnce();
|
|
163
|
+
r = _coerceCliResult(await runOnce());
|
|
119
164
|
}
|
|
120
165
|
return r || { ok: false, exitCode: -1, envelope: null, via: "error" };
|
|
121
166
|
}
|
|
@@ -175,15 +220,28 @@ async function generateBrief(projectDir, { kind = "execute", milestone, domain,
|
|
|
175
220
|
// promote-debt → blast-radius (scope a debt item's reach)
|
|
176
221
|
// prd → cluster (structure-aware decomposition)
|
|
177
222
|
// milestone/discuss/design-decompose/doc-ripple → no structural verb (no-op)
|
|
223
|
+
// A PHASE-level query takes NO target — it wants the whole structural picture, not
|
|
224
|
+
// one file's neighbourhood. Only `cluster` and `dead-code` answer target-free;
|
|
225
|
+
// `who-imports` / `blast-radius` / `who-calls` / `body` REQUIRE a target and return
|
|
226
|
+
// {"ok":false,"reason":"missing-target"} without one.
|
|
227
|
+
//
|
|
228
|
+
// The original map used who-imports/blast-radius for five phases, under a comment
|
|
229
|
+
// asserting they "return the full graph slice" without a target. That was never
|
|
230
|
+
// true — verified against the live CLI. So plan / impact / feature / populate /
|
|
231
|
+
// promote-debt failed their graph query on EVERY run in EVERY project since the map
|
|
232
|
+
// shipped, each one halting with "graph BROKEN" on a perfectly healthy graph.
|
|
233
|
+
// Fixed 2026-08-02 (binvoice S2-M14 plan halted on reason=missing-target).
|
|
234
|
+
//
|
|
235
|
+
// A mapped verb MUST answer target-free. Enforced by test/m104-phase-graph-verb-map.test.js.
|
|
178
236
|
const PHASE_GRAPH_VERB_MAP = {
|
|
179
|
-
impact: "
|
|
180
|
-
plan: "
|
|
237
|
+
impact: "cluster",
|
|
238
|
+
plan: "cluster",
|
|
181
239
|
partition: "cluster",
|
|
182
|
-
feature: "
|
|
240
|
+
feature: "cluster",
|
|
183
241
|
"gap-analysis": "dead-code",
|
|
184
242
|
project: "cluster",
|
|
185
|
-
populate: "
|
|
186
|
-
"promote-debt": "
|
|
243
|
+
populate: "cluster",
|
|
244
|
+
"promote-debt": "cluster",
|
|
187
245
|
prd: "cluster",
|
|
188
246
|
};
|
|
189
247
|
|
|
@@ -203,11 +261,18 @@ async function queryStructuralSlice(projectDir, phaseName, phaseNameOpt, _rebuil
|
|
|
203
261
|
// Phase has no mapped structural verb (milestone/discuss/design-decompose/doc-ripple) — no-op.
|
|
204
262
|
return { ok: true, verb: null, slice: null, graphUnavailable: false, graphBroken: false, loudMessage: null };
|
|
205
263
|
}
|
|
206
|
-
// The graph query
|
|
207
|
-
//
|
|
208
|
-
//
|
|
264
|
+
// The graph query runs bin/gsd-t-graph-query-cli.cjs (local) or `gsd-t graph …` (global).
|
|
265
|
+
//
|
|
266
|
+
// The verb MUST travel in `argv`, not in the subcmd string: runCli builds the LOCAL-bin
|
|
267
|
+
// command from `argv` ALONE (the subcmd text is only used for the global `gsd-t …` form).
|
|
268
|
+
// With the verb in subcmd and argv empty, any project carrying a local graph CLI ran it
|
|
269
|
+
// with NO verb at all → {"ok":false,"reason":"no-verb"} → fail-closed to BROKEN. That is
|
|
270
|
+
// why this failed in a project with a local bin/ copy and worked everywhere else.
|
|
271
|
+
// Fixed 2026-08-02 (binvoice S2-M14 partition halted twice on this).
|
|
272
|
+
//
|
|
273
|
+
// No target is passed — every mapped verb answers target-free (see PHASE_GRAPH_VERB_MAP).
|
|
209
274
|
const r = await runCli(
|
|
210
|
-
projectDir,
|
|
275
|
+
projectDir, "graph", [verb], "gsd-t-graph-query-cli.cjs",
|
|
211
276
|
`graph:${verb}`, true, phaseNameOpt
|
|
212
277
|
);
|
|
213
278
|
const env = r.envelope || {};
|
|
@@ -44,11 +44,37 @@ const _CLI_ENVELOPE_SCHEMA = {
|
|
|
44
44
|
type: "object", required: ["ok", "exitCode"], additionalProperties: true,
|
|
45
45
|
properties: {
|
|
46
46
|
ok: { type: "boolean" }, exitCode: { type: "integer" },
|
|
47
|
-
envelope: {}, stdout: { type: "string" }, stderr: { type: "string" }, via: { type: "string" },
|
|
47
|
+
envelope: { type: ["object", "null"], description: "stdout PARSED as JSON — an object, never the raw text" }, stdout: { type: "string" }, stderr: { type: "string" }, via: { type: "string" },
|
|
48
48
|
},
|
|
49
49
|
};
|
|
50
50
|
// Run a `gsd-t <subcmd>` CLI (or project-local bin/<localBin>) via an agent's Bash and
|
|
51
51
|
// return { ok, exitCode, envelope, stderr, via }. parseJson=true parses stdout as the envelope.
|
|
52
|
+
// ─── Normalize a helper-agent CLI result (M104) ──────────────────────────────
|
|
53
|
+
//
|
|
54
|
+
// runCli asks a haiku helper to run a command and hand back the parsed JSON in
|
|
55
|
+
// `envelope`. A model doing that job is not perfectly reliable, and two broken
|
|
56
|
+
// shapes both read `env.ok === undefined` — which every caller treats as failure,
|
|
57
|
+
// so a HEALTHY CLI reports as broken:
|
|
58
|
+
// (a) `envelope` holds the raw JSON TEXT rather than the parsed object
|
|
59
|
+
// (b) `envelope` is absent and the JSON only reached `stdout`
|
|
60
|
+
// Observed live 2026-08-02: a partition/plan halted four times on "graph BROKEN"
|
|
61
|
+
// while the graph was fine (exit 0, ok:true).
|
|
62
|
+
//
|
|
63
|
+
// NOT a fallback: it continues past no failure and substitutes no guess — it reads
|
|
64
|
+
// the result the CLI actually returned. A failing CLI still returns ok:false and
|
|
65
|
+
// still fails; non-JSON stdout is left alone.
|
|
66
|
+
function _coerceCliResult(x) {
|
|
67
|
+
if (!x) return x;
|
|
68
|
+
if (typeof x.envelope === "string") {
|
|
69
|
+
try { x.envelope = JSON.parse(x.envelope); } catch (_) { /* genuinely not JSON */ }
|
|
70
|
+
}
|
|
71
|
+
if ((x.envelope === undefined || x.envelope === null) &&
|
|
72
|
+
typeof x.stdout === "string" && x.stdout.trim().startsWith("{")) {
|
|
73
|
+
try { x.envelope = JSON.parse(x.stdout); } catch (_) { /* not JSON */ }
|
|
74
|
+
}
|
|
75
|
+
return x;
|
|
76
|
+
}
|
|
77
|
+
|
|
52
78
|
async function runCli(projectDir, subcmd, argv, localBin, label, parseJson = true, phaseName) {
|
|
53
79
|
const argStr = (argv || []).map((a) => `'${String(a).replace(/'/g, "'\\''")}'`).join(" ");
|
|
54
80
|
const prompt = [
|
|
@@ -66,7 +92,7 @@ async function runCli(projectDir, subcmd, argv, localBin, label, parseJson = tru
|
|
|
66
92
|
if (phaseName) opts.phase = phaseName; // opts.phase MUST be a string, never the phase() fn
|
|
67
93
|
const r = await agent(prompt, opts)
|
|
68
94
|
.catch((e) => ({ ok: false, exitCode: -1, envelope: null, stderr: String(e && e.message), via: "error" }));
|
|
69
|
-
return r || { ok: false, exitCode: -1, envelope: null, via: "error" };
|
|
95
|
+
return _coerceCliResult(r) || { ok: false, exitCode: -1, envelope: null, via: "error" };
|
|
70
96
|
}
|
|
71
97
|
async function runPreflight(projectDir, label = "preflight", phaseName) {
|
|
72
98
|
return runCli(projectDir, "preflight", ["--json"], "cli-preflight.cjs", label, true, phaseName);
|
|
@@ -60,8 +60,34 @@ const _args = (typeof args === "string") ? (() => { try { return JSON.parse(args
|
|
|
60
60
|
const overrides = (_args.overrides && typeof _args.overrides === "object") ? _args.overrides : {};
|
|
61
61
|
const _CLI_ENVELOPE_SCHEMA = {
|
|
62
62
|
type: "object", required: ["ok", "exitCode"], additionalProperties: true,
|
|
63
|
-
properties: { ok: { type: "boolean" }, exitCode: { type: "integer" }, envelope: {}, stdout: { type: "string" }, stderr: { type: "string" }, via: { type: "string" } },
|
|
63
|
+
properties: { ok: { type: "boolean" }, exitCode: { type: "integer" }, envelope: { type: ["object", "null"], description: "stdout PARSED as JSON — an object, never the raw text" }, stdout: { type: "string" }, stderr: { type: "string" }, via: { type: "string" } },
|
|
64
64
|
};
|
|
65
|
+
// ─── Normalize a helper-agent CLI result (M104) ──────────────────────────────
|
|
66
|
+
//
|
|
67
|
+
// runCli asks a haiku helper to run a command and hand back the parsed JSON in
|
|
68
|
+
// `envelope`. A model doing that job is not perfectly reliable, and two broken
|
|
69
|
+
// shapes both read `env.ok === undefined` — which every caller treats as failure,
|
|
70
|
+
// so a HEALTHY CLI reports as broken:
|
|
71
|
+
// (a) `envelope` holds the raw JSON TEXT rather than the parsed object
|
|
72
|
+
// (b) `envelope` is absent and the JSON only reached `stdout`
|
|
73
|
+
// Observed live 2026-08-02: a partition/plan halted four times on "graph BROKEN"
|
|
74
|
+
// while the graph was fine (exit 0, ok:true).
|
|
75
|
+
//
|
|
76
|
+
// NOT a fallback: it continues past no failure and substitutes no guess — it reads
|
|
77
|
+
// the result the CLI actually returned. A failing CLI still returns ok:false and
|
|
78
|
+
// still fails; non-JSON stdout is left alone.
|
|
79
|
+
function _coerceCliResult(x) {
|
|
80
|
+
if (!x) return x;
|
|
81
|
+
if (typeof x.envelope === "string") {
|
|
82
|
+
try { x.envelope = JSON.parse(x.envelope); } catch (_) { /* genuinely not JSON */ }
|
|
83
|
+
}
|
|
84
|
+
if ((x.envelope === undefined || x.envelope === null) &&
|
|
85
|
+
typeof x.stdout === "string" && x.stdout.trim().startsWith("{")) {
|
|
86
|
+
try { x.envelope = JSON.parse(x.stdout); } catch (_) { /* not JSON */ }
|
|
87
|
+
}
|
|
88
|
+
return x;
|
|
89
|
+
}
|
|
90
|
+
|
|
65
91
|
async function runCli(projectDir, subcmd, argv, localBin, label, parseJson = true, phaseName) {
|
|
66
92
|
const argStr = (argv || []).map((a) => `'${String(a).replace(/'/g, "'\\''")}'`).join(" ");
|
|
67
93
|
const prompt = [
|
|
@@ -74,7 +100,7 @@ async function runCli(projectDir, subcmd, argv, localBin, label, parseJson = tru
|
|
|
74
100
|
const opts = { label, schema: _CLI_ENVELOPE_SCHEMA, model: "haiku" };
|
|
75
101
|
if (phaseName) opts.phase = phaseName;
|
|
76
102
|
const r = await agent(prompt, opts).catch((e) => ({ ok: false, exitCode: -1, envelope: null, stderr: String(e && e.message), via: "error" }));
|
|
77
|
-
return r || { ok: false, exitCode: -1, envelope: null, via: "error" };
|
|
103
|
+
return _coerceCliResult(r) || { ok: false, exitCode: -1, envelope: null, via: "error" };
|
|
78
104
|
}
|
|
79
105
|
async function runPreflight(projectDir, label = "preflight", phaseName) { return runCli(projectDir, "preflight", ["--json"], "cli-preflight.cjs", label, true, phaseName); }
|
|
80
106
|
// Broken-Graph-Halts (EXEMPT carve-out): verify's graph slice is additive/announced — it
|