@tekyzinc/gsd-t 5.7.10 → 5.9.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.
@@ -0,0 +1,237 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * gsd-t-fallback-guard.js
4
+ *
5
+ * M106-D2 — PreToolUse hook on Write|Edit. Denies a write that introduces a
6
+ * fallback the user never approved.
7
+ *
8
+ * [RULE] fallback-guard-denies-unapproved
9
+ * [RULE] fallback-guard-halts-never-allows-on-error
10
+ *
11
+ * A fallback added while chasing a bug never appears in any plan — this is the
12
+ * only trigger point that can catch it, because it fires at the moment the code
13
+ * is written.
14
+ *
15
+ * ─── Stdin (Claude Code PreToolUse payload) ─────────────────────────────────
16
+ * { "tool_name": "Write"|"Edit", "cwd": "...",
17
+ * "tool_input": { "file_path": "...", "content"|"new_string": "..." } }
18
+ *
19
+ * ─── Decision contract ──────────────────────────────────────────────────────
20
+ * Deny: {"hookSpecificOutput":{"hookEventName":"PreToolUse",
21
+ * "permissionDecision":"deny","permissionDecisionReason":"..."}}
22
+ * Allow: exit 0, no output.
23
+ *
24
+ * ─── HALT, never allow-on-error ─────────────────────────────────────────────
25
+ * This guard governs the No-Fallback rule, so it must not contain one. If the
26
+ * detector cannot run or its answer cannot be read, the write is DENIED with
27
+ * the reason — never quietly permitted. The single exception is a payload
28
+ * that is not a Write/Edit of source at all: that is "not applicable", not a
29
+ * failure, and it exits 0.
30
+ *
31
+ * Zero dependencies.
32
+ */
33
+
34
+ "use strict";
35
+
36
+ const fs = require("fs");
37
+ const path = require("path");
38
+ const { spawnSync } = require("child_process");
39
+
40
+ const SOURCE_EXT = new Set([".js", ".cjs", ".mjs", ".jsx", ".ts", ".tsx"]);
41
+ const TEST_PATH_RE = /(?:^|[\\/])(?:test|tests|__tests__|spec|e2e|fixtures?)[\\/]|\.(?:test|spec)\.[cm]?[jt]sx?$/i;
42
+
43
+ function deny(reason) {
44
+ process.stdout.write(JSON.stringify({
45
+ hookSpecificOutput: {
46
+ hookEventName: "PreToolUse",
47
+ permissionDecision: "deny",
48
+ permissionDecisionReason: reason,
49
+ },
50
+ }) + "\n");
51
+ process.exit(0);
52
+ }
53
+
54
+ function allow() { process.exit(0); }
55
+
56
+ /**
57
+ * Locate the detector.
58
+ *
59
+ * Every project is supposed to carry its own copy. If it does not, the
60
+ * project's install is broken, and the answer is to REPAIR it — which the
61
+ * SessionStart heal hook does — not to hunt around for a copy somewhere else.
62
+ * Hunting is what let binvoice run for weeks on 20 of 38 tools while every
63
+ * update reported success.
64
+ *
65
+ * Returns the path, or throws with what is wrong. It never returns "not found"
66
+ * as if that were an ordinary answer.
67
+ */
68
+ function findDetector(projectDir) {
69
+ const inProject = path.join(projectDir, "bin", "gsd-t-fallback-detect.cjs");
70
+ if (fs.existsSync(inProject)) return inProject;
71
+
72
+ // Running from inside the package itself (developing GSD-T).
73
+ const inPackage = path.join(__dirname, "..", "bin", "gsd-t-fallback-detect.cjs");
74
+ if (fs.existsSync(inPackage)) return inPackage;
75
+
76
+ throw new Error(
77
+ `This project has no copy of the fallback detector at ${inProject}, which means ` +
78
+ `its GSD-T install is incomplete. Run 'gsd-t install-check' to repair it — do not ` +
79
+ `work around it.`
80
+ );
81
+ }
82
+
83
+ /** Thrown when the settings file exists but cannot be understood. */
84
+ class ConfigUnreadable extends Error {}
85
+
86
+ /**
87
+ * Is the gate switched on for this project?
88
+ *
89
+ * A settings file that cannot be read is NOT assumed to mean "on" — that would
90
+ * be a guess about what the project wanted. It throws, and the caller denies
91
+ * the write and says why. Only an ABSENT file means "on", because absence is
92
+ * unambiguous.
93
+ */
94
+ function isEnabled(projectDir) {
95
+ const p = path.join(projectDir, ".gsd-t", "fallback-gate.json");
96
+ if (!fs.existsSync(p)) return true; // absent = on, by design
97
+ let raw;
98
+ try {
99
+ raw = fs.readFileSync(p, "utf8");
100
+ } catch (e) {
101
+ throw new ConfigUnreadable(`${p} could not be read: ${e.message}`);
102
+ }
103
+ const cfg = JSON.parse(raw); // a parse failure throws, and the caller denies
104
+ return cfg.enabled !== false;
105
+ }
106
+
107
+ function buildReason(findings, filePath) {
108
+ const lines = [
109
+ `This write adds ${findings.length === 1 ? "a fallback" : `${findings.length} fallbacks`} that was never approved.`,
110
+ "",
111
+ ];
112
+ for (const f of findings.slice(0, 5)) {
113
+ lines.push(` ${f.what}`);
114
+ if (f.snippet) lines.push(` ${f.snippet}`);
115
+ lines.push("");
116
+ }
117
+ lines.push(
118
+ "A fallback continues after a failure. It produces wrong data that looks correct,",
119
+ "and it removes the alarm for the bug that caused the failure.",
120
+ "",
121
+ "Do one of these:",
122
+ "",
123
+ " 1. Replace it with a halt — stop, and report what could not be done.",
124
+ " This is almost always the right answer.",
125
+ "",
126
+ " 2. Ask David for approval. Tell him, in plain words:",
127
+ " - what fails",
128
+ " - how often it really fails, with evidence",
129
+ " - why stopping is worse than continuing",
130
+ " - what it does instead (never a guessed value, never a partial result)",
131
+ " If he agrees, add the entry to .gsd-t/fallbacks.json and write again.",
132
+ "",
133
+ ` File: ${filePath}`
134
+ );
135
+ return lines.join("\n");
136
+ }
137
+
138
+ function main() {
139
+ let input = "";
140
+ let done = false;
141
+
142
+ process.stdin.setEncoding("utf8");
143
+ process.stdin.on("data", (c) => { input += c; });
144
+
145
+ const finish = () => {
146
+ if (done) return;
147
+ done = true;
148
+
149
+ let data;
150
+ try {
151
+ data = JSON.parse(input);
152
+ } catch (_) {
153
+ // An unparseable payload is not a write we can inspect — not applicable.
154
+ return allow();
155
+ }
156
+ if (!data || typeof data !== "object") return allow();
157
+
158
+ const tool = data.tool_name;
159
+ if (tool !== "Write" && tool !== "Edit") return allow();
160
+
161
+ const ti = (data.tool_input && typeof data.tool_input === "object") ? data.tool_input : {};
162
+ const filePath = typeof ti.file_path === "string" ? ti.file_path : "";
163
+ if (!filePath) return allow();
164
+
165
+ const ext = path.extname(filePath);
166
+ if (!SOURCE_EXT.has(ext)) return allow(); // not source — not applicable
167
+ if (TEST_PATH_RE.test(filePath.replace(/\\/g, "/"))) return allow(); // tests are exempt
168
+
169
+ const content = typeof ti.content === "string" ? ti.content
170
+ : typeof ti.new_string === "string" ? ti.new_string
171
+ : "";
172
+ if (!content.trim()) return allow();
173
+
174
+ const cwd = (typeof data.cwd === "string" && data.cwd) ? data.cwd : process.cwd();
175
+ let on;
176
+ try {
177
+ on = isEnabled(cwd);
178
+ } catch (e) {
179
+ return deny(
180
+ "The fallback gate's settings could not be read, so it is unclear whether\n" +
181
+ `this project has switched the gate off.\n\n${e.message}\n\n` +
182
+ "Fix the file, or delete it to leave the gate on."
183
+ );
184
+ }
185
+ if (!on) return allow(); // switched off for this project
186
+
187
+ let detector;
188
+ try {
189
+ detector = findDetector(cwd);
190
+ } catch (e) {
191
+ return deny(
192
+ `${e.message}\n\n` +
193
+ "This write cannot be checked, and allowing it unchecked is exactly what this\n" +
194
+ "guard exists to prevent."
195
+ );
196
+ }
197
+
198
+ const run = spawnSync(process.execPath,
199
+ [detector, "--text", content, "--file", filePath, "--project", cwd, "--json"],
200
+ { encoding: "utf8", timeout: 10000, maxBuffer: 8 * 1024 * 1024 });
201
+
202
+ if (run.error || typeof run.stdout !== "string" || !run.stdout.trim()) {
203
+ return deny(
204
+ "The fallback check could not run, so this write cannot be verified.\n" +
205
+ `Reason: ${run.error ? run.error.message : "the detector produced no output"}\n\n` +
206
+ "This guard halts rather than letting an unchecked write through."
207
+ );
208
+ }
209
+
210
+ let result;
211
+ try {
212
+ result = JSON.parse(run.stdout);
213
+ } catch (_) {
214
+ return deny("The fallback check returned something unreadable, so this write cannot be verified.");
215
+ }
216
+
217
+ if (result.exitCode === 64) {
218
+ return deny(
219
+ `The fallback check could not decide: ${result.error || "unknown reason"}\n\n` +
220
+ (result.halt || "Fix the problem above, then write again.")
221
+ );
222
+ }
223
+
224
+ if (result.ok) return allow();
225
+
226
+ return deny(buildReason(result.findings || [], filePath));
227
+ };
228
+
229
+ process.stdin.on("end", finish);
230
+ process.stdin.on("error", finish);
231
+ const wd = setTimeout(finish, 8000);
232
+ if (wd.unref) wd.unref();
233
+ }
234
+
235
+ if (require.main === module) main();
236
+
237
+ module.exports = { buildReason, findDetector, isEnabled };
@@ -0,0 +1,183 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * gsd-t-install-heal.js
4
+ *
5
+ * M108 — SessionStart hook. Checks this project's install, repairs what is
6
+ * missing, and reports anything it could not fix.
7
+ *
8
+ * [RULE] install-heal-repairs-before-work-starts
9
+ * [RULE] install-heal-reports-what-it-could-not-fix
10
+ *
11
+ * This is NOT a fallback. A fallback continues past a failure with a worse
12
+ * answer. This one FIXES the failure — copies the missing tool from the
13
+ * installed package — so the session then runs with everything present. When it
14
+ * cannot fix something it says so loudly rather than letting the session
15
+ * proceed on a broken install.
16
+ *
17
+ * It also surfaces the shared repair log: if the same tool keeps going missing
18
+ * across projects, the installer is what needs fixing, not each project.
19
+ *
20
+ * ─── Stdin (Claude Code SessionStart payload) ───────────────────────────────
21
+ * { "hook_event_name": "SessionStart", "cwd": "...", "session_id": "..." }
22
+ *
23
+ * ─── Output ─────────────────────────────────────────────────────────────────
24
+ * Prints to stdout, which Claude Code shows as session context. Silent when
25
+ * the install is already complete — the common case, and no news is good news.
26
+ *
27
+ * Zero dependencies.
28
+ */
29
+
30
+ "use strict";
31
+
32
+ const fs = require("fs");
33
+ const os = require("os");
34
+ const path = require("path");
35
+ const { spawnSync, execFileSync } = require("child_process");
36
+
37
+ /**
38
+ * Find the install checker. It ships beside this hook, so one place answers it.
39
+ * Throws with what is wrong — never returns nothing as an ordinary answer.
40
+ */
41
+ function findChecker() {
42
+ const beside = path.join(__dirname, "..", "bin", "gsd-t-install-check.cjs");
43
+ if (fs.existsSync(beside)) return beside;
44
+ throw new Error(
45
+ `The install checker is not at ${beside}, where it ships. The GSD-T install ` +
46
+ `itself is incomplete — reinstall it.`
47
+ );
48
+ }
49
+
50
+ function main() {
51
+ let input = "";
52
+ let done = false;
53
+
54
+ process.stdin.setEncoding("utf8");
55
+ process.stdin.on("data", (c) => { input += c; });
56
+
57
+ const finish = () => {
58
+ if (done) return;
59
+ done = true;
60
+
61
+ // Which project is this? The payload says. Guessing with process.cwd()
62
+ // could check — and repair — a different project than the one the session
63
+ // is in, so an unreadable payload stops instead.
64
+ let data;
65
+ try {
66
+ data = JSON.parse(input);
67
+ } catch (e) {
68
+ process.stdout.write(
69
+ `[GSD-T INSTALL] Could not tell which project this session is in, so its ` +
70
+ `install was not checked: ${e.message}\n`
71
+ );
72
+ process.exit(0);
73
+ }
74
+ const cwd = (data && typeof data.cwd === "string" && data.cwd) ? data.cwd : null;
75
+ if (!cwd) {
76
+ process.stdout.write(
77
+ "[GSD-T INSTALL] The session did not say which folder it is in, so this " +
78
+ "project's install was not checked.\n"
79
+ );
80
+ process.exit(0);
81
+ }
82
+
83
+ // Not a GSD-T project — nothing to check.
84
+ if (!fs.existsSync(path.join(cwd, ".gsd-t"))) { process.exit(0); }
85
+
86
+ let checker;
87
+ try {
88
+ checker = findChecker();
89
+ } catch (e) {
90
+ process.stdout.write(`[GSD-T INSTALL] ${e.message}\n`);
91
+ process.exit(0);
92
+ }
93
+
94
+ const run = spawnSync(process.execPath, [checker, "--project", cwd, "--json"], {
95
+ encoding: "utf8", timeout: 60000, maxBuffer: 8 * 1024 * 1024,
96
+ });
97
+
98
+ if (run.error || !run.stdout) {
99
+ process.stdout.write(
100
+ `[GSD-T INSTALL] The install check could not run: ${run.error ? run.error.message : "no output"}\n`
101
+ );
102
+ process.exit(0);
103
+ }
104
+
105
+ let result;
106
+ try {
107
+ result = JSON.parse(run.stdout);
108
+ } catch (_) {
109
+ process.stdout.write("[GSD-T INSTALL] The install check returned something unreadable.\n");
110
+ process.exit(0);
111
+ }
112
+
113
+ const repaired = result.repaired || [];
114
+ const unfixable = result.unrepairable || [];
115
+
116
+ if (repaired.length) {
117
+ process.stdout.write(
118
+ `[GSD-T INSTALL] This project was missing ${repaired.length} tool(s). ` +
119
+ `They have been restored from the installed package, and the session can proceed normally.\n`
120
+ );
121
+ }
122
+
123
+ if (unfixable.length) {
124
+ process.stdout.write(
125
+ `[GSD-T INSTALL] ${unfixable.length} tool(s) could NOT be restored:\n`
126
+ );
127
+ for (const u of unfixable) {
128
+ process.stdout.write(` ${u.tool} — ${u.reason}\n`);
129
+ }
130
+ process.stdout.write(
131
+ "Anything that depends on these will fail. Tell David the install is broken " +
132
+ "before doing work that relies on them.\n"
133
+ );
134
+ }
135
+
136
+ // If one tool keeps going missing across several projects, the installer is
137
+ // the cause, not each project. Say so ONCE — repeating it every session
138
+ // turns a real signal into noise you stop reading.
139
+ if (repaired.length) {
140
+ try {
141
+ const lib = require(checker);
142
+ const { entries } = lib.readLog();
143
+ const suspects = lib.installerSuspects({}, entries).filter((s) => s.projects.length >= 3);
144
+ const seenPath = path.join(os.homedir(), ".claude", "gsd-t-install-suspects-seen.json");
145
+ let alreadyTold = [];
146
+ if (fs.existsSync(seenPath)) {
147
+ alreadyTold = JSON.parse(fs.readFileSync(seenPath, "utf8"));
148
+ }
149
+ const fresh = suspects.filter((s) => !alreadyTold.includes(s.tool));
150
+ if (fresh.length) {
151
+ process.stdout.write(
152
+ "[GSD-T INSTALL] These tools have gone missing across several projects, which " +
153
+ "points at the installer rather than any one project:\n"
154
+ );
155
+ for (const s of fresh.slice(0, 5)) {
156
+ process.stdout.write(` ${s.tool} — ${s.projects.length} projects\n`);
157
+ }
158
+ process.stdout.write("This is worth fixing in GSD-T itself.\n");
159
+ fs.writeFileSync(seenPath, JSON.stringify([...alreadyTold, ...fresh.map((s) => s.tool)]));
160
+ }
161
+ } catch (e) {
162
+ // The repair above already happened and is unaffected. But this is the
163
+ // signal that tells you the INSTALLER is at fault rather than each
164
+ // project — losing it silently is how the underlying cause stays hidden.
165
+ process.stdout.write(
166
+ `[GSD-T INSTALL] Repairs were made, but the cross-project pattern could ` +
167
+ `not be read: ${e.message}\n`
168
+ );
169
+ }
170
+ }
171
+
172
+ process.exit(0);
173
+ };
174
+
175
+ process.stdin.on("end", finish);
176
+ process.stdin.on("error", finish);
177
+ const wd = setTimeout(finish, 20000);
178
+ if (wd.unref) wd.unref();
179
+ }
180
+
181
+ if (require.main === module) main();
182
+
183
+ module.exports = { findChecker };
@@ -449,10 +449,12 @@ See memory pointer: `feedback_auto_research_external_gaps`.
449
449
 
450
450
  ### Architect's Oversight Doctrine (M101 — governed, enforced)
451
451
 
452
- **Contract:** `.gsd-t/contracts/architects-oversight-contract.md` v1.0.0 STABLE
452
+ **Contract:** `.gsd-t/contracts/architects-oversight-contract.md` v1.1.0 STABLE
453
453
 
454
454
  **Never build before the design has passed the architect's interrogation.** GSD-T staffs verifiers (Red Team, QA, code-review, pre-mortem) — all asking "is this correct?" — but no seat asked "is this the *smartest, simplest* design given what we already have?" The result: the wrong thing built correctly, then thoroughly tested, then shipped (the Binvoice completeness-scan waste — a whole-page scan re-deriving a count already stored locally). This doctrine fills the empty architect seat. Sibling to the Unproven-Assumption Doctrine: that one bars unproven *facts*; this one bars unproven *necessity*.
455
455
 
456
+ **§Stage 0 — GROUND BEFORE YOU ASSESS (runs first; contract §0).** A field audit of 16 real architect runs found 21 user corrections, and **13 of them were facts the user already held** — settled rules the architect re-derived and got wrong ("that rule was implemented last week"), and runtime behavior it asserted from a saved page instead of asking ("I believe you're wrong. When scrolling the feed…"). **Asking is cheaper than deriving.** So before the pass: read the code AND the standing rules (CLAUDE.md constraints, `[RULE]` guard maps, contracts, recent Decision Log); label every evidence item **LIVE or SNAPSHOT** (a runtime claim resting on a snapshot is unproven); then **interview the user** — lead by showing your read of *how it works today* as a plain-English flow for confirmation, echo back the rules you're treating as fixed, and ask only what code cannot answer. **Research** (how others solve this class of problem) runs only when you're not confident, and only AFTER the interview so it can't anchor the questions. Loop interview↔research, **max 3 cycles** (1-2 expected); still unsure at the cap → ask the user whether to halt or proceed with the uncertainty flagged. A run succeeded if the build it directed needed few follow-ups — not if the report read well.
457
+
456
458
  **The Six-Stage Pass — run IN ORDER before proposing or building any solution. Each stage can KILL the plan. Every "am I sure?" is answered with EVIDENCE (a grep, a Read, a graph query), never conviction — self-confidence is what produced the waste.**
457
459
 
458
460
  1. **Objective** — What is the core objective? Why is it the core objective? *(Kills: building the wrong thing.)*