@tekyzinc/gsd-t 4.13.11 → 4.14.10

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.
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * gsd-t-graph-intercept.js — M97
3
+ * gsd-t-graph-intercept.js — M97/M99
4
4
  *
5
5
  * A PostToolUse hook on the `Grep` tool. When Claude runs a grep that is actually
6
6
  * a STRUCTURAL question (who-calls / who-imports / a bare symbol), this answers it
@@ -21,8 +21,19 @@
21
21
  * - No graph in this project → pure no-op.
22
22
  * - Original grep hits are RETAINED beneath the graph answer (no silent hiding).
23
23
  *
24
+ * M99 D2:
25
+ * - Presence check uses D1's resolver (resolveStorePath) — never re-derives the path.
26
+ * - Layer-2a logging: one ledger line per decision (replaced + passthrough) via
27
+ * append_ledger_line. Consumer resolved from GSDT_GRAPH_CONSUMER env or
28
+ * payload hook_data.consumer; falls back to 'cli'. Fail-open.
29
+ *
24
30
  * [RULE] graph-intercept-fail-open-never-breaks-grep
25
31
  * [RULE] graph-intercept-structural-only-text-passes-through
32
+ * [RULE] presence-check-repointed
33
+ * [RULE] byte-identical-on-off
34
+ * [RULE] fail-open
35
+ * [RULE] import-resolver-never-hardcode
36
+ * [RULE] consumer-label-from-context-not-setenv
26
37
  */
27
38
 
28
39
  'use strict';
@@ -58,29 +69,93 @@ function resolveQueryCli(cwd) {
58
69
  return null;
59
70
  }
60
71
 
72
+ // M99 D2: load D1's resolver module (resolveStorePath + append_ledger_line).
73
+ // [RULE] import-resolver-never-hardcode / [RULE] presence-check-repointed
74
+ function loadResolver(cwd) {
75
+ try {
76
+ const pkgLocal = path.join(__dirname, '..', 'bin', 'gsd-t-graph-store-resolver.cjs');
77
+ const projLocal = path.join(cwd, 'bin', 'gsd-t-graph-store-resolver.cjs');
78
+ const resolverPath = fs.existsSync(pkgLocal) ? pkgLocal
79
+ : fs.existsSync(projLocal) ? projLocal
80
+ : null;
81
+ if (!resolverPath) return null;
82
+ return require(resolverPath);
83
+ } catch { return null; }
84
+ }
85
+
86
+ // M99 D2: resolve consumer label from env or payload; falls back to 'cli'.
87
+ // [RULE] consumer-label-from-context-not-setenv
88
+ function resolveConsumer(payload) {
89
+ if (process.env.GSDT_GRAPH_CONSUMER) return process.env.GSDT_GRAPH_CONSUMER;
90
+ if (payload && payload.hook_data && typeof payload.hook_data.consumer === 'string') {
91
+ return payload.hook_data.consumer;
92
+ }
93
+ return 'cli';
94
+ }
95
+
96
+ // M99 D2: emit one Layer-2a ledger line. Fail-open — never alters the decision.
97
+ // [RULE] fail-open [RULE] byte-identical-on-off
98
+ function logDecision(appendFn, cwd, consumer, classified, action, patternShape) {
99
+ if (!appendFn) return;
100
+ try {
101
+ appendFn({
102
+ kind: 'grep',
103
+ ts: new Date().toISOString(),
104
+ classified,
105
+ action,
106
+ patternShape: String(patternShape).slice(0, 200),
107
+ consumer,
108
+ }, cwd);
109
+ } catch { /* FAIL-OPEN: sink error never propagates */ }
110
+ }
111
+
61
112
  function main(payload) {
62
113
  // Only act on Grep.
63
114
  if (!payload || payload.tool_name !== 'Grep') passThrough();
64
115
 
65
116
  const cwd = payload.cwd || process.cwd();
66
117
 
67
- // Must be a GSD-T project with a graph present.
118
+ // Must be a GSD-T project.
68
119
  if (!fs.existsSync(path.join(cwd, '.gsd-t'))) passThrough();
69
- if (!fs.existsSync(path.join(cwd, '.gsd-t', 'graph.db'))) passThrough();
120
+
121
+ // M99 D2: repoint presence check at D1's resolver. [RULE] presence-check-repointed
122
+ const resolver = loadResolver(cwd);
123
+ const storePath = resolver ? resolver.resolveStorePath(cwd) : null;
124
+ if (!storePath || !fs.existsSync(storePath)) passThrough();
70
125
 
71
126
  const pattern = payload.tool_input && payload.tool_input.pattern;
72
127
  if (typeof pattern !== 'string' || !pattern) passThrough();
73
128
 
129
+ // Resolve consumer and logging sink BEFORE the classify decision.
130
+ const consumer = resolveConsumer(payload);
131
+ const appendFn = resolver && typeof resolver.append_ledger_line === 'function'
132
+ ? resolver.append_ledger_line : null;
133
+
74
134
  // Classify (the classifier itself fails safe → text).
75
135
  let cls;
76
136
  try {
77
137
  const { classifyGrep } = require(path.join(__dirname, '..', 'bin', 'gsd-t-grep-classifier.cjs'));
78
138
  cls = classifyGrep(pattern);
79
- } catch { passThrough(); }
80
- if (!cls || !cls.structural) passThrough();
139
+ } catch {
140
+ // Classifier unavailable — passthrough without logging (no classified info to log)
141
+ passThrough();
142
+ }
143
+
144
+ if (!cls || !cls.structural) {
145
+ // Text-classified passthrough — log one line per the contract.
146
+ // [RULE] byte-identical-on-off: logging is a side-channel; passthrough is unchanged.
147
+ const classified = (cls && cls.classified) || 'text';
148
+ const patternShape = (cls && cls.patternShape) || pattern.slice(0, 80);
149
+ logDecision(appendFn, cwd, consumer, classified, 'passthrough', patternShape);
150
+ passThrough();
151
+ }
81
152
 
82
153
  const cliPath = resolveQueryCli(cwd);
83
- if (!cliPath) passThrough();
154
+ if (!cliPath) {
155
+ // Structural but no CLI — log passthrough, pass through.
156
+ logDecision(appendFn, cwd, consumer, 'structural', 'passthrough', cls.patternShape || pattern.slice(0, 80));
157
+ passThrough();
158
+ }
84
159
 
85
160
  // Query the graph. who-calls for symbols/calls; who-imports for imports.
86
161
  // For a bare symbol we ALSO try who-imports so the model sees both usages.
@@ -118,8 +193,14 @@ function main(payload) {
118
193
  }
119
194
  }
120
195
 
121
- // No graph answer → pass the grep through (don't replace with nothing).
122
- if (!sections.length) passThrough();
196
+ // No graph answer → log passthrough (structural query but empty result), pass through.
197
+ if (!sections.length) {
198
+ logDecision(appendFn, cwd, consumer, 'structural', 'passthrough', cls.patternShape || pattern.slice(0, 80));
199
+ passThrough();
200
+ }
201
+
202
+ // Graph answered — log replaced, then emit.
203
+ logDecision(appendFn, cwd, consumer, 'structural', 'replaced', cls.patternShape || pattern.slice(0, 80));
123
204
 
124
205
  // Build the replacement: graph answer first (labeled), original grep beneath.
125
206
  const original = payload.tool_response || payload.tool_output || '';
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * gsd-t-read-intercept.js — M98
3
+ * gsd-t-read-intercept.js — M98/M99
4
4
  *
5
5
  * A PostToolUse hook on the `Read` tool. When Claude reads an INDEXED code file
6
6
  * with an offset+limit that lands inside exactly one known function's line range,
@@ -21,10 +21,22 @@
21
21
  * updatedToolOutput: "<original file output> + graph note" } }
22
22
  * ...or nothing (exit 0) to pass the Read through unchanged.
23
23
  *
24
+ * M99 D2:
25
+ * - Presence check and Database open use D1's resolver (resolveStorePath) — never re-derive path.
26
+ * - Layer-2b logging: one line per augment/passthrough decision via append_ledger_line.
27
+ * Consumer resolved from GSDT_GRAPH_CONSUMER env or payload hook_data.consumer or 'cli'.
28
+ * Fail-open: a throwing sink never alters the decision or the output.
29
+ *
24
30
  * INVARIANTS:
25
- * [RULE] read-intercept-fail-open — any error / missing graph / non-code → pass through
26
- * [RULE] read-intercept-augment-never-shrink — only APPEND; never replace the file body
27
- * [RULE] read-intercept-structural-only — augment ONLY when offset+limit ∈ one funcId range
31
+ * [RULE] read-intercept-fail-open — any error / missing graph / non-code → pass through
32
+ * [RULE] read-intercept-augment-never-shrink — only APPEND; never replace the file body
33
+ * [RULE] read-intercept-structural-only — augment ONLY when offset+limit ∈ one funcId range
34
+ * [RULE] presence-check-repointed — M99: presence + DB open via resolver, never literal
35
+ * [RULE] byte-identical-on-off — logging is a side-channel; decision unchanged
36
+ * [RULE] fail-open — sink error never propagates
37
+ * [RULE] import-resolver-never-hardcode — import resolver, never re-derive path
38
+ * [RULE] augment-never-shrink-kept — M98 augment-never-shrink rule KEPT
39
+ * [RULE] consumer-label-from-context-not-setenv
28
40
  * - NEVER calls Read/Grep (loop guard). Reads the graph DB directly (read-only).
29
41
  * - No graph in this project → pure no-op.
30
42
  */
@@ -63,15 +75,63 @@ function emitAugment(original, note) {
63
75
  process.exit(0);
64
76
  }
65
77
 
78
+ // M99 D2: load D1's resolver module.
79
+ // [RULE] import-resolver-never-hardcode / [RULE] presence-check-repointed
80
+ function loadResolver(cwd) {
81
+ try {
82
+ const pkgLocal = path.join(__dirname, '..', 'bin', 'gsd-t-graph-store-resolver.cjs');
83
+ const projLocal = path.join(cwd, 'bin', 'gsd-t-graph-store-resolver.cjs');
84
+ const resolverPath = fs.existsSync(pkgLocal) ? pkgLocal
85
+ : fs.existsSync(projLocal) ? projLocal
86
+ : null;
87
+ if (!resolverPath) return null;
88
+ return require(resolverPath);
89
+ } catch { return null; }
90
+ }
91
+
92
+ // M99 D2: resolve consumer label from env or payload.
93
+ // [RULE] consumer-label-from-context-not-setenv
94
+ function resolveConsumer(payload) {
95
+ if (process.env.GSDT_GRAPH_CONSUMER) return process.env.GSDT_GRAPH_CONSUMER;
96
+ if (payload && payload.hook_data && typeof payload.hook_data.consumer === 'string') {
97
+ return payload.hook_data.consumer;
98
+ }
99
+ return 'cli';
100
+ }
101
+
102
+ // M99 D2: emit one Layer-2b ledger line. Fail-open — never alters the decision.
103
+ // [RULE] fail-open [RULE] byte-identical-on-off
104
+ function logDecision(appendFn, cwd, consumer, action, filePath) {
105
+ if (!appendFn) return;
106
+ try {
107
+ appendFn({
108
+ kind: 'read',
109
+ ts: new Date().toISOString(),
110
+ action,
111
+ file: String(filePath).slice(0, 500),
112
+ consumer,
113
+ }, cwd);
114
+ } catch { /* FAIL-OPEN: sink error never propagates */ }
115
+ }
116
+
66
117
  function main(payload) {
67
118
  // Only act on Read.
68
119
  if (!payload || payload.tool_name !== 'Read') passThrough();
69
120
 
70
121
  const cwd = payload.cwd || process.cwd();
71
122
 
72
- // Must be a GSD-T project with a graph present.
123
+ // Must be a GSD-T project.
73
124
  if (!fs.existsSync(path.join(cwd, '.gsd-t'))) passThrough();
74
- if (!fs.existsSync(path.join(cwd, '.gsd-t', 'graph.db'))) passThrough();
125
+
126
+ // M99 D2: resolve consumer and logging sink early (used in ALL decision branches).
127
+ const consumer = resolveConsumer(payload);
128
+ const resolver = loadResolver(cwd);
129
+ const appendFn = resolver && typeof resolver.append_ledger_line === 'function'
130
+ ? resolver.append_ledger_line : null;
131
+
132
+ // M99 D2: repoint presence check at D1's resolver. [RULE] presence-check-repointed
133
+ const storePath = resolver ? resolver.resolveStorePath(cwd) : null;
134
+ if (!storePath || !fs.existsSync(storePath)) passThrough();
75
135
 
76
136
  const input = payload.tool_input || {};
77
137
  const filePath = input.file_path;
@@ -84,7 +144,11 @@ function main(payload) {
84
144
  // full-file read has no structural target → pass through (no silent shrinking).
85
145
  const offset = Number(input.offset);
86
146
  const limit = Number(input.limit);
87
- if (!Number.isFinite(offset) || !Number.isFinite(limit) || limit <= 0) passThrough();
147
+ if (!Number.isFinite(offset) || !Number.isFinite(limit) || limit <= 0) {
148
+ // Log passthrough for reads with a code file but no structural signal.
149
+ logDecision(appendFn, cwd, consumer, 'passthrough', filePath);
150
+ passThrough();
151
+ }
88
152
  const readStart = offset; // 1-based first line read (Read's offset is 1-based)
89
153
  const readEnd = offset + limit - 1;
90
154
 
@@ -93,19 +157,17 @@ function main(payload) {
93
157
  if (path.isAbsolute(filePath)) rel = path.relative(cwd, filePath);
94
158
  rel = rel.split(path.sep).join('/');
95
159
 
96
- // Find the function whose [start,end] range the read window lands inside, by
97
- // enumerating this file's funcIds straight from the graph DB (read-only). Cheaper
98
- // and more direct than spawning the query CLL — and it's the same store the CLI reads.
160
+ // Find the function whose [start,end] range the read window lands inside.
99
161
  let match = null;
100
162
  try {
101
- // Resolve the store loader from the global package (where this hook ships) first,
102
- // falling back to the project's own copy a synthetic project may have neither,
103
- // in which case we fail-open (pass through).
163
+ // M99 D2: open the DB via the resolver-provided store path.
164
+ // [RULE] presence-check-repointed storePath comes from D1's resolver, not a literal.
104
165
  let requireStore;
105
166
  try { requireStore = require(path.join(__dirname, '..', 'bin', 'gsd-t-require-store.cjs')); }
106
167
  catch { requireStore = require(path.join(cwd, 'bin', 'gsd-t-require-store.cjs')); }
107
168
  const Database = requireStore.requireBetterSqlite();
108
- const db = new Database(path.join(cwd, '.gsd-t', 'graph.db'), { readonly: true });
169
+ // Use the resolver-provided storePath (M99 D2 repoint).
170
+ const db = new Database(storePath, { readonly: true });
109
171
  try {
110
172
  const hasEnd = db.prepare('PRAGMA table_info(nodes)').all().some((c) => c.name === 'end_line');
111
173
  if (hasEnd) {
@@ -117,9 +179,7 @@ function main(payload) {
117
179
  if (!m) continue;
118
180
  const start = parseInt(m[1], 10);
119
181
  const end = r.end_line;
120
- // The read window starts inside this function's [start,end] body.
121
182
  if (readStart >= start && readStart <= end) {
122
- // Prefer the innermost (largest start) enclosing function.
123
183
  if (!match || start > match.start) match = { funcId: r.func_id, name: r.name, start, end };
124
184
  }
125
185
  }
@@ -127,9 +187,15 @@ function main(payload) {
127
187
  } finally {
128
188
  try { db.close(); } catch { /* best-effort */ }
129
189
  }
130
- } catch (_e) { passThrough(); }
190
+ } catch (_e) {
191
+ logDecision(appendFn, cwd, consumer, 'passthrough', rel);
192
+ passThrough();
193
+ }
131
194
 
132
- if (!match) passThrough();
195
+ if (!match) {
196
+ logDecision(appendFn, cwd, consumer, 'passthrough', rel);
197
+ passThrough();
198
+ }
133
199
 
134
200
  // Build the augment note (appended beneath the original file output, kept intact).
135
201
  const original = payload.tool_response || payload.tool_output || '';
@@ -139,6 +205,9 @@ function main(payload) {
139
205
  `source + its imports, class header, and callers (≈10× fewer tokens), run:\n` +
140
206
  ` gsd-t graph body '${match.funcId}'`;
141
207
 
208
+ // Log augment decision BEFORE emitting (fail-open: if logging throws, decision unchanged).
209
+ logDecision(appendFn, cwd, consumer, 'augment', rel);
210
+
142
211
  emitAugment(typeof original === 'string' ? original : JSON.stringify(original), note);
143
212
  }
144
213
 
@@ -90,6 +90,23 @@ const projectDir = _args.projectDir || ".";
90
90
  const symptom = _args.symptom || null;
91
91
  const milestone = _args.milestone || null; // tags loop-ledger halts so they scope to this milestone
92
92
 
93
+ // M99 D2: persist a kind:'wiring' ledger line. M81 sandbox: all I/O through agent() Bash.
94
+ // Uses the `gsd-t graph wiring-log --auto` CLI shim (avoids embedding require() in strings).
95
+ // [RULE] wiring-mode-three-states / [RULE] consumer-label-from-context-not-setenv
96
+ async function persistWiringMode(phaseName) {
97
+ const consumer = "debug";
98
+ await agent(
99
+ [
100
+ `Persist one graph-wiring-mode ledger line for the debug workflow.`,
101
+ `Run: \`gsd-t graph wiring-log --consumer ${consumer} --auto --project '${projectDir}'\``,
102
+ `(--auto detects WIRED if the graph store exists, else fallback-announced)`,
103
+ `If the command is not found, exit 0 (ledger write is optional).`,
104
+ `Return ONLY: {"ok": true} or {"ok": false, "reason": "<short reason>"}.`,
105
+ ].join("\n"),
106
+ { label: "debug:wiring-ledger", phase: phaseName, model: "haiku", schema: { type: "object", required: ["ok"], properties: { ok: { type: "boolean" }, reason: { type: "string" } } } }
107
+ ).catch(() => null);
108
+ }
109
+
93
110
  const DEBUG_CYCLE_SCHEMA = {
94
111
  type: "object",
95
112
  required: ["resolved", "rootCause", "filesEdited"],
@@ -359,6 +376,8 @@ phase("Preflight");
359
376
  const pre = await runPreflight(projectDir);
360
377
  if (!pre.ok) return { status: "failed", reason: "preflight-failed", preflight: pre.envelope };
361
378
  const brief = await generateBrief(projectDir, { kind: "execute", id: "debug-brief" });
379
+ // M99 D2: persist graphWiringMode for the debug consumer. [RULE] wiring-mode-three-states
380
+ await persistWiringMode("Preflight");
362
381
 
363
382
  // M94-D11 §READER: query graph ONCE before cycles (the structural slice is the same
364
383
  // for all cycles — same symptom). Injected into each cycle's prompt.
@@ -52,6 +52,23 @@ const projectDir = _args.projectDir || ".";
52
52
  const milestone = _args.milestone || null;
53
53
  const domains = _args.domains || [];
54
54
 
55
+ // M99 D2: persist a kind:'wiring' ledger line for this workflow.
56
+ // Uses the `gsd-t graph wiring-log --auto` CLI shim (avoids embedding require() in strings).
57
+ // [RULE] wiring-mode-three-states / [RULE] consumer-label-from-context-not-setenv
58
+ async function persistWiringMode(phaseName) {
59
+ const consumer = "integrate";
60
+ await agent(
61
+ [
62
+ `Persist one graph-wiring-mode ledger line for the integrate workflow.`,
63
+ `Run: \`gsd-t graph wiring-log --consumer ${consumer} --auto --project '${projectDir}'\``,
64
+ `(--auto detects WIRED if the graph store exists, else fallback-announced)`,
65
+ `If the command is not found, exit 0 (ledger write is optional).`,
66
+ `Return ONLY: {"ok": true, "mode": "<mode>"} or {"ok": false, "reason": "<short reason>"}.`,
67
+ ].join("\n"),
68
+ { label: "integrate:wiring-ledger", phase: phaseName, model: "haiku", schema: { type: "object", required: ["ok"], properties: { ok: { type: "boolean" }, mode: { type: "string" }, reason: { type: "string" } } } }
69
+ ).catch(() => null); // fail-open
70
+ }
71
+
55
72
  const INTEGRATE_SCHEMA = {
56
73
  type: "object",
57
74
  required: ["status", "crossDomainEdits"],
@@ -71,6 +88,8 @@ phase("Preflight");
71
88
  const pre = await runPreflight(projectDir);
72
89
  if (!pre.ok) return { status: "failed", reason: "preflight-failed", preflight: pre.envelope };
73
90
  const brief = await generateBrief(projectDir, { kind: "execute", milestone, id: `integrate-${(milestone || "m").toLowerCase()}` });
91
+ // M99 D2: persist graphWiringMode for the integrate consumer. [RULE] wiring-mode-three-states
92
+ await persistWiringMode("Preflight");
74
93
 
75
94
  // M94-D10-T6: Graph Structural Slice — who-imports + blast-radius (ADDITIVE, announced-degradation)
76
95
  // [RULE] integrate-uses-graph-for-wiring-verification
@@ -733,6 +733,23 @@ const milestone = _args.milestone || null;
733
733
  const userInput = _args.userInput || "";
734
734
  const phaseName = _args.phase;
735
735
 
736
+ // M99 D2: persist a kind:'wiring' ledger line for this workflow.
737
+ // Uses the `gsd-t graph wiring-log --auto` CLI shim (avoids embedding require() in strings).
738
+ // [RULE] wiring-mode-three-states / [RULE] consumer-label-from-context-not-setenv
739
+ async function persistWiringMode(phaseNameArg) {
740
+ const consumer = "phase";
741
+ await agent(
742
+ [
743
+ `Persist one graph-wiring-mode ledger line for the phase workflow.`,
744
+ `Run: \`gsd-t graph wiring-log --consumer ${consumer} --auto --project '${projectDir}'\``,
745
+ `(--auto detects WIRED if the graph store exists, else fallback-announced)`,
746
+ `If the command is not found, exit 0 (ledger write is optional).`,
747
+ `Return ONLY: {"ok": true, "mode": "<mode>"} or {"ok": false, "reason": "<short reason>"}.`,
748
+ ].join("\n"),
749
+ { label: "phase:wiring-ledger", phase: phaseNameArg, model: "haiku", schema: { type: "object", required: ["ok"], properties: { ok: { type: "boolean" }, mode: { type: "string" }, reason: { type: "string" } } } }
750
+ ).catch(() => null); // fail-open
751
+ }
752
+
736
753
  // M84: competition is AUTOMATIC. By default the workflow PROBES the solution space
737
754
  // (after brief) and self-decides whether to run a 3-producer + judge competition —
738
755
  // no flag needed. Optional manual OVERRIDES: `competition: N` (2-5) forces N
@@ -765,6 +782,8 @@ phase("Preflight");
765
782
  const pre = await runPreflight(projectDir);
766
783
  if (!pre.ok) return { status: "failed", reason: "preflight-failed", preflight: pre.envelope };
767
784
  const brief = await generateBrief(projectDir, { kind: phaseName, milestone, id: `${phaseName}-${(milestone || "m").toLowerCase()}` });
785
+ // M99 D2: persist graphWiringMode for the phase consumer. [RULE] wiring-mode-three-states
786
+ await persistWiringMode("Preflight");
768
787
 
769
788
  // ── M84: resolve competition AUTOMATICALLY (after brief, before producing) ──
770
789
  // Default: probe the solution space and self-decide. Overrides pin it.
@@ -88,6 +88,23 @@ const projectDir = _args.projectDir || ".";
88
88
  const task = _args.task || null;
89
89
  const model = _args.model || "sonnet";
90
90
 
91
+ // M99 D2: persist a kind:'wiring' ledger line. M81 sandbox: all I/O through agent() Bash.
92
+ // Uses the `gsd-t graph wiring-log --auto` CLI shim (avoids embedding require() in strings).
93
+ // [RULE] wiring-mode-three-states / [RULE] consumer-label-from-context-not-setenv
94
+ async function persistWiringMode(phaseName) {
95
+ const consumer = "quick";
96
+ await agent(
97
+ [
98
+ `Persist one graph-wiring-mode ledger line for the quick workflow.`,
99
+ `Run: \`gsd-t graph wiring-log --consumer ${consumer} --auto --project '${projectDir}'\``,
100
+ `(--auto detects WIRED if the graph store exists, else fallback-announced)`,
101
+ `If the command is not found, exit 0 (ledger write is optional).`,
102
+ `Return ONLY: {"ok": true} or {"ok": false, "reason": "<short reason>"}.`,
103
+ ].join("\n"),
104
+ { label: "quick:wiring-ledger", phase: phaseName, model: "haiku", schema: { type: "object", required: ["ok"], properties: { ok: { type: "boolean" }, reason: { type: "string" } } } }
105
+ ).catch(() => null);
106
+ }
107
+
91
108
  const QUICK_SCHEMA = {
92
109
  type: "object",
93
110
  required: ["status", "filesEdited"],
@@ -228,6 +245,8 @@ phase("Preflight");
228
245
  const pre = await runPreflight(projectDir);
229
246
  if (!pre.ok) return { status: "failed", reason: "preflight-failed", preflight: pre.envelope };
230
247
  const brief = await generateBrief(projectDir, { kind: "execute", id: "quick-brief" });
248
+ // M99 D2: persist graphWiringMode for the quick consumer. [RULE] wiring-mode-three-states
249
+ await persistWiringMode("Preflight");
231
250
 
232
251
  // M94-D11 §READER: query graph for structural impact before the Execute agent reasons
233
252
  // [RULE] quick-writer-pattern
@@ -238,29 +238,70 @@ const GRAPH_SLICE_SCHEMA = {
238
238
  let structuralSlice = null; // { deadCode, dangling, clusters, coverage, tier } | null
239
239
  let graphWiringMode = "pending"; // "wired" | "fallback-announced" | "disabled"
240
240
 
241
- // M94-D6: runCli — inline async helper that delegates CLI calls to an agent() Bash.
242
- // M81 invariant: NO require/fs/child_process in the orchestrator body.
243
- // This is the ONLY way to invoke the D5 query CLI from the sandbox.
244
- // [RULE] no-graph-baseline-proven-graph-free — this helper is called ONLY when graphMode==="wired".
241
+ // M94-D6: runCli — inline async helper that delegates the D5 graph-query CLI to an
242
+ // agent() Bash. M81 invariant: NO require/fs/child_process in the orchestrator body
243
+ // the agent runs the command; we read back a SCHEMA-VALIDATED envelope.
244
+ //
245
+ // v4.13.12 HARDENING (root cause of the NiceNote 2026-06-29 silent grep-fallback):
246
+ // the old version told a HAIKU agent to "return ONLY the raw JSON line" and then
247
+ // JSON.parse()'d its free-text reply. Haiku wrapped the JSON in a ```json fence →
248
+ // JSON.parse threw → caught → graph-unavailable → grep-mode, even though the graph
249
+ // was live (status returned ok, 156 files, compiler-accurate). Three fixes, mirroring
250
+ // the proven gsd-t-verify.workflow.js runCli:
251
+ // 1. SCHEMA-validated agent output (StructuredOutput) — the model returns structured
252
+ // JSON via the tool layer, never fenced text; no brittle JSON.parse of prose.
253
+ // 2. project-local bin → GLOBAL `gsd-t` fallback (a project without a local bin copy
254
+ // no longer fails the probe — it was hardcoded to `node bin/...cjs`).
255
+ // 3. stderr captured (dropped `2>/dev/null`) and surfaced in `reason` so the fallback
256
+ // log can say WHY (parse-fail vs not-found vs CLI-error), never a bare "unavailable".
257
+ // [RULE] no-graph-baseline-proven-graph-free — called ONLY when graphMode==="wired".
258
+ // [RULE] graph-probe-schema-validated-never-fence-parsed
259
+ const _GRAPH_CLI_ENVELOPE_SCHEMA = {
260
+ type: "object",
261
+ required: ["ok"],
262
+ additionalProperties: true,
263
+ properties: {
264
+ ok: { type: "boolean", description: "true iff the CLI printed a JSON envelope with ok:true" },
265
+ reason: { type: "string", description: "on failure: graph-unavailable / cli-not-found / cli-error / non-json-output, plus any stderr" },
266
+ via: { type: "string", description: "local | global | error" },
267
+ results: { type: "array", items: {}, description: "the verb's results array (dead-code/dangling/cluster/etc.), [] if none" },
268
+ tier: { type: "string", description: "compiler-accurate | tree-sitter-floor | ... when present" },
269
+ coverage: { description: "coverage envelope when the verb returns one" },
270
+ },
271
+ };
245
272
  async function runCli(verb, target, label) {
246
- const targetArg = target ? ` ${JSON.stringify(target)}` : "";
247
- const cmd = `node bin/gsd-t-graph-query-cli.cjs ${verb}${targetArg} 2>/dev/null || echo '{"ok":false,"reason":"graph-unavailable"}'`;
248
- const result = await agent(
273
+ const targetArg = target ? ` '${String(target).replace(/'/g, "'\\''")}'` : "";
274
+ const prompt = [
275
+ `Run the GSD-T graph-query CLI for the project at \`${projectDir}\` and report its result. Steps:`,
276
+ `1. If \`${projectDir}/bin/gsd-t-graph-query-cli.cjs\` exists, run: \`node ${projectDir}/bin/gsd-t-graph-query-cli.cjs ${verb}${targetArg}\` (set via="local"). Otherwise run: \`gsd-t graph ${verb}${targetArg}\` (set via="global"). Use cwd \`${projectDir}\`. Do NOT redirect stderr — capture it.`,
277
+ `2. The command prints ONE JSON envelope to stdout. Parse it. Set ok = (the parsed envelope's "ok" field === true). Copy its "results", "tier", and "coverage" fields through if present.`,
278
+ `3. If the command exits non-zero, prints no JSON, or stdout is not valid JSON: set ok=false and put a short reason in "reason" — "cli-not-found" if the file/binary was missing, else "cli-error", else "non-json-output" — and append the first ~200 chars of stderr.`,
279
+ `Do NOT do any other work. ONLY run this one command and report the structured result.`,
280
+ ].join("\n");
281
+ const r = await agent(prompt, {
282
+ label: `graph:${label || verb}`,
283
+ phase: "Graph-Wiring",
284
+ model: "haiku",
285
+ schema: _GRAPH_CLI_ENVELOPE_SCHEMA,
286
+ }).catch((e) => ({ ok: false, reason: `agent-error: ${e && e.message}`, via: "error" }));
287
+ return r || { ok: false, reason: "graph-unavailable", via: "error" };
288
+ }
289
+
290
+ // M99 D2: persist a kind:'wiring' ledger line for this workflow.
291
+ // M81 sandbox: all I/O through agent() Bash; no require/fs in the sandbox.
292
+ // Uses the `gsd-t graph wiring-log` CLI shim (avoids embedding require() in strings).
293
+ // [RULE] wiring-mode-three-states / [RULE] consumer-label-from-context-not-setenv
294
+ async function persistWiringMode(mode) {
295
+ const consumer = "scan";
296
+ await agent(
249
297
  [
250
- `Run the following command in \`${projectDir}\` via Bash and return ONLY the raw JSON line it prints (no commentary):`,
251
- `\`\`\`bash`,
252
- `cd ${JSON.stringify(projectDir)} && ${cmd}`,
253
- `\`\`\``,
254
- `If the command fails or prints no JSON, return: {"ok":false,"reason":"graph-unavailable"}`,
255
- `Return ONLY the JSON, nothing else.`,
298
+ `Persist one graph-wiring-mode ledger line for the scan workflow.`,
299
+ `Run: \`gsd-t graph wiring-log --consumer ${consumer} --mode ${mode} --project '${projectDir}'\``,
300
+ `If the command is not found, exit 0 (ledger write is optional).`,
301
+ `Return ONLY: {"ok": true} or {"ok": false, "reason": "<short reason>"}.`,
256
302
  ].join("\n"),
257
- { label: `graph:${label || verb}`, phase: "Graph-Wiring", model: "haiku" }
258
- ).catch(() => null);
259
- try {
260
- return (typeof result === "string") ? JSON.parse(result.trim()) : (result || { ok: false, reason: "graph-unavailable" });
261
- } catch (_) {
262
- return { ok: false, reason: "graph-unavailable" };
263
- }
303
+ { label: "scan:wiring-ledger", phase: "Graph-Wiring", model: "haiku", schema: { type: "object", required: ["ok"], properties: { ok: { type: "boolean" }, reason: { type: "string" } } } }
304
+ ).catch(() => null); // fail-open
264
305
  }
265
306
 
266
307
  // Preflight: an agent checks branch + whether a prior register exists, via Bash.
@@ -331,6 +372,7 @@ if (graphMode === "disabled") {
331
372
  // ZERO graph calls in this path — [RULE] no-graph-baseline-proven-graph-free.
332
373
  graphWiringMode = "disabled";
333
374
  log("graph-wiring: DISABLED (no-graph baseline mode — graph-query call-count == 0; AC-4 baseline)");
375
+ await persistWiringMode("disabled"); // M99 D2: persist wiring mode [RULE] wiring-mode-three-states
334
376
  } else {
335
377
  // graphMode === "wired": build index if absent, then query structural slice.
336
378
  // Step 1: check if index exists and is queryable.
@@ -339,7 +381,8 @@ if (graphMode === "disabled") {
339
381
  // Graph unavailable — announce fallback, continue with intact grep-mode scan.
340
382
  // [RULE] parser-fail-disables-loud-never-silent (from graph-query-cli-contract)
341
383
  graphWiringMode = "fallback-announced";
342
- log(`⚠ GRAPH-FALLBACK (ANNOUNCED): graph index not available (${(statusResult && statusResult.reason) || "graph-unavailable"}) — scan continues in full grep-mode (today's architecture, intact). Structural findings from LLM reconstruction only. Build the index (gsd-t graph build) to enable graph-wired accuracy.`);
384
+ log(`⚠ GRAPH-FALLBACK (ANNOUNCED): graph status probe returned not-ok [reason=${(statusResult && statusResult.reason) || "graph-unavailable"}, via=${(statusResult && statusResult.via) || "?"}] — scan continues in full grep-mode (today's architecture, intact). Structural findings from LLM reconstruction only. Build the index (gsd-t graph build) to enable graph-wired accuracy.`);
385
+ await persistWiringMode("fallback-announced"); // M99 D2 [RULE] wiring-mode-three-states
343
386
  } else {
344
387
  // Step 2: query the structural slice (dead-code + dangling + clusters).
345
388
  // These are the findings the deep-finders currently reconstruct by reading files (error-prone);
@@ -358,6 +401,7 @@ if (graphMode === "disabled") {
358
401
  // All three verbs returned graph-unavailable — announce fallback.
359
402
  graphWiringMode = "fallback-announced";
360
403
  log(`⚠ GRAPH-FALLBACK (ANNOUNCED): all structural-slice queries returned graph-unavailable — scan continues in full grep-mode. Dead-code / dangling / cluster findings from LLM reconstruction only.`);
404
+ await persistWiringMode("fallback-announced"); // M99 D2 [RULE] wiring-mode-three-states
361
405
  } else {
362
406
  // Structural slice assembled — will be injected into scanSlice finder agents.
363
407
  structuralSlice = {
@@ -372,8 +416,15 @@ if (graphMode === "disabled") {
372
416
  },
373
417
  tier: (deadCodeResult && deadCodeResult.tier) || (danglingResult && danglingResult.tier) || "unknown",
374
418
  };
375
- graphWiringMode = "wired";
419
+ // M99 Red Team fix: emit "WIRED" (uppercase) to MATCH the --auto router
420
+ // producers (bin/gsd-t.js:3915) AND the rollup's comparison
421
+ // (gsd-t-graph-metrics-rollup.cjs:333 `=== "WIRED"`). Lowercase "wired" here
422
+ // fell through all three rollup branches → a successfully-wired scan reported
423
+ // WIRED:0, defeating success criterion 13 (the metric M99 exists to surface).
424
+ // [RULE] wiring-mode-casing-matches-rollup
425
+ graphWiringMode = "WIRED";
376
426
  log(`graph-wiring: WIRED — structural slice ready (dead-code: ${structuralSlice.deadCode.length} candidates, dangling: ${structuralSlice.dangling.length} edges, clusters: ${structuralSlice.clusters.length} groups, tier: ${structuralSlice.tier}). Slice will be INJECTED ADDITIVELY into scanSlice deep-finders. [RULE] scan-injects-structural-slice`);
427
+ await persistWiringMode("WIRED"); // M99 D2 [RULE] wiring-mode-three-states / wiring-mode-casing-matches-rollup
377
428
  }
378
429
  }
379
430
  }
@@ -698,6 +749,11 @@ function fmtChunks(today) {
698
749
  head.push(`**Date:** ${today}`);
699
750
  head.push(`**Slices run:** ${slices.length} | **Coverage:** ${coverageComplete ? `FULL - all ${slices.length} slices succeeded` : `PARTIAL - ${succeededCount}/${slices.length} succeeded`}`);
700
751
  head.push(`**Verified findings:** ${counts.total}`, "");
752
+ // M99 D2 T4: stamp graphWiringMode into the header (north-star: invisible fallback leaves a trace).
753
+ // A `fallback-announced` mode co-occurring with a same-window outcome:'hit' is the machine-visible
754
+ // NiceNote scan-#12 contradiction. [RULE] wiring-mode-three-states
755
+ const _wiringEmoji = { "wired": "✅", "fallback-announced": "⚠️", "disabled": "🚫", "pending": "⏳" };
756
+ head.push(`**Graph wiring:** ${_wiringEmoji[graphWiringMode] || "?"} \`${graphWiringMode}\` (WIRED = graph answered structural queries; fallback-announced = graph unavailable, fell back to grep; disabled = no-graph baseline)`, "");
701
757
  head.push(`> Effort estimates use GSD-T-native units (domain / wave / spawn / token-spend). Never human-hours.`);
702
758
  head.push(`> TD numbering continues from the prior register (if any, archived). This scan begins at **TD-${tdStart}**.`, "");
703
759
  if (!coverageComplete) head.push(`> ⚠️ **PARTIAL COVERAGE - ${failedSlices.length} of ${slices.length} codebase areas were NOT scanned this pass** (failed to return findings): ${ascii(failedSlices.join(", "))}. Findings UNDER-COUNT the real debt. Re-run (resume) for full coverage.`, "");
@@ -230,6 +230,23 @@ const VERDICT_SCHEMA = {
230
230
  },
231
231
  };
232
232
 
233
+ // M99 D2: persist a kind:'wiring' ledger line. M81 sandbox: all I/O through agent() Bash.
234
+ // Uses the `gsd-t graph wiring-log --auto` CLI shim (avoids embedding require() in strings).
235
+ // [RULE] wiring-mode-three-states / [RULE] consumer-label-from-context-not-setenv
236
+ async function persistWiringMode(phaseName) {
237
+ const consumer = "verify";
238
+ await agent(
239
+ [
240
+ `Persist one graph-wiring-mode ledger line for the verify workflow.`,
241
+ `Run: \`gsd-t graph wiring-log --consumer ${consumer} --auto --project '${projectDir}'\``,
242
+ `(--auto detects WIRED if the graph store exists, else fallback-announced)`,
243
+ `If the command is not found, exit 0 (ledger write is optional).`,
244
+ `Return ONLY: {"ok": true} or {"ok": false, "reason": "<short reason>"}.`,
245
+ ].join("\n"),
246
+ { label: "verify:wiring-ledger", phase: phaseName, model: "haiku", schema: { type: "object", required: ["ok"], properties: { ok: { type: "boolean" }, reason: { type: "string" } } } }
247
+ ).catch(() => null);
248
+ }
249
+
233
250
  // ───── Script body ─────
234
251
 
235
252
  if (!milestone) {
@@ -244,6 +261,8 @@ if (!pre.ok) {
244
261
  return { status: "failed", reason: "preflight-failed", preflight: pre.envelope };
245
262
  }
246
263
  const brief = await generateBrief(projectDir, { kind: "verify", milestone, id: `verify-${(milestone || "m").toLowerCase()}` });
264
+ // M99 D2: persist graphWiringMode for the verify consumer. [RULE] wiring-mode-three-states
265
+ await persistWiringMode("Preflight");
247
266
 
248
267
  phase("Verify-Gate");
249
268
  const vg = await runVerifyGate(projectDir);