@tekyzinc/gsd-t 4.13.12 → 4.15.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