@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.
- package/CHANGELOG.md +58 -0
- package/README.md +2 -2
- package/bin/gsd-t-concise-rewrite.cjs +223 -0
- package/bin/gsd-t-fallback-detect.cjs +624 -0
- package/bin/gsd-t-install-check.cjs +342 -0
- package/bin/gsd-t-verify-gate.cjs +2 -0
- package/bin/gsd-t.js +193 -5
- package/commands/gsd-t-architect.md +261 -18
- package/commands/gsd-t-help.md +4 -2
- package/package.json +1 -1
- package/scripts/gsd-t-concise-hook.js +200 -0
- package/scripts/gsd-t-fallback-guard.js +237 -0
- package/scripts/gsd-t-install-heal.js +183 -0
- package/templates/CLAUDE-global.md +3 -1
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* gsd-t-install-check.cjs
|
|
4
|
+
*
|
|
5
|
+
* M108 — Install self-check and repair.
|
|
6
|
+
*
|
|
7
|
+
* [RULE] install-check-repairs-then-continues-never-routes-around
|
|
8
|
+
* [RULE] install-check-records-every-repair
|
|
9
|
+
* [RULE] install-check-halts-when-it-cannot-repair
|
|
10
|
+
*
|
|
11
|
+
* A project's `bin/` is supposed to hold every tool the installer ships. When
|
|
12
|
+
* one is missing, the tools that need it break — and until now nothing noticed.
|
|
13
|
+
* Binvoice ran for weeks with 21 of 38 tools.
|
|
14
|
+
*
|
|
15
|
+
* This is NOT a fallback. A fallback continues PAST a failure with a worse
|
|
16
|
+
* answer. This FIXES the failure — it copies the missing file from the
|
|
17
|
+
* installed package — and then the work continues with everything present. If
|
|
18
|
+
* it cannot fix it, it HALTS and says so; it never lets the work proceed on a
|
|
19
|
+
* broken install.
|
|
20
|
+
*
|
|
21
|
+
* Every repair is written to a shared log so a pattern across projects becomes
|
|
22
|
+
* visible: if the same tool keeps going missing everywhere, the installer
|
|
23
|
+
* itself is what needs fixing.
|
|
24
|
+
*
|
|
25
|
+
* ─── Usage ──────────────────────────────────────────────────────────────────
|
|
26
|
+
* node gsd-t-install-check.cjs --project <dir> # check + repair
|
|
27
|
+
* node gsd-t-install-check.cjs --project <dir> --check # report only
|
|
28
|
+
* node gsd-t-install-check.cjs --report # read the shared log
|
|
29
|
+
*
|
|
30
|
+
* ─── Exit codes ─────────────────────────────────────────────────────────────
|
|
31
|
+
* 0 install is complete (possibly after a repair)
|
|
32
|
+
* 4 something is missing and could not be repaired — HALT
|
|
33
|
+
* 64 bad input
|
|
34
|
+
*
|
|
35
|
+
* Zero dependencies.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
"use strict";
|
|
39
|
+
|
|
40
|
+
const fs = require("fs");
|
|
41
|
+
const os = require("os");
|
|
42
|
+
const path = require("path");
|
|
43
|
+
const { execFileSync } = require("child_process");
|
|
44
|
+
|
|
45
|
+
const EXIT_OK = 0;
|
|
46
|
+
const EXIT_UNREPAIRABLE = 4;
|
|
47
|
+
const EXIT_BAD_INPUT = 64;
|
|
48
|
+
|
|
49
|
+
const LOG_PATH = path.join(os.homedir(), ".claude", "gsd-t-install-repairs.jsonl");
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Where the installed package lives — the source every repair copies from.
|
|
53
|
+
*
|
|
54
|
+
* Two places, asked in order, because they answer DIFFERENT questions: "am I
|
|
55
|
+
* running from inside the package?" and "where does npm keep global installs?".
|
|
56
|
+
* That is not hunting for a file; it is two distinct facts about this machine.
|
|
57
|
+
*
|
|
58
|
+
* Throws with what went wrong. It never returns nothing as if that were an
|
|
59
|
+
* ordinary answer — without the package there is nothing to repair from, and
|
|
60
|
+
* the caller must stop.
|
|
61
|
+
*/
|
|
62
|
+
function findPackageRoot() {
|
|
63
|
+
const problems = [];
|
|
64
|
+
|
|
65
|
+
// 1. Running from inside the package itself.
|
|
66
|
+
const here = path.resolve(__dirname, "..");
|
|
67
|
+
const herePkg = path.join(here, "package.json");
|
|
68
|
+
if (fs.existsSync(herePkg)) {
|
|
69
|
+
let pkg;
|
|
70
|
+
try {
|
|
71
|
+
pkg = JSON.parse(fs.readFileSync(herePkg, "utf8"));
|
|
72
|
+
} catch (e) {
|
|
73
|
+
problems.push(`${herePkg} could not be read: ${e.message}`);
|
|
74
|
+
}
|
|
75
|
+
if (pkg && pkg.name === "@tekyzinc/gsd-t") return here;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// 2. Ask npm where global installs live.
|
|
79
|
+
let root;
|
|
80
|
+
try {
|
|
81
|
+
root = execFileSync("npm", ["root", "-g"], { encoding: "utf8", timeout: 10000 }).trim();
|
|
82
|
+
} catch (e) {
|
|
83
|
+
problems.push(`npm could not be asked where global packages live: ${e.message}`);
|
|
84
|
+
}
|
|
85
|
+
if (root) {
|
|
86
|
+
const p = path.join(root, "@tekyzinc", "gsd-t");
|
|
87
|
+
if (fs.existsSync(path.join(p, "package.json"))) return p;
|
|
88
|
+
problems.push(`GSD-T is not installed at ${p}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
throw new Error(
|
|
92
|
+
"The installed GSD-T package could not be located, so nothing can be checked " +
|
|
93
|
+
"or repaired.\n " + problems.join("\n ")
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** The list of tools every project is supposed to have. Read from the installer. */
|
|
98
|
+
function expectedTools(pkgRoot) {
|
|
99
|
+
const installer = path.join(pkgRoot, "bin", "gsd-t.js");
|
|
100
|
+
const src = fs.readFileSync(installer, "utf8");
|
|
101
|
+
const m = src.match(/const PROJECT_BIN_TOOLS = \[([^]*?)\n\];/);
|
|
102
|
+
if (!m) throw new Error(`could not read the tool list from ${installer}`);
|
|
103
|
+
return [...m[1].matchAll(/"([^"]+)"/g)].map((x) => x[1]);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function packageVersion(pkgRoot) {
|
|
107
|
+
try {
|
|
108
|
+
return JSON.parse(fs.readFileSync(path.join(pkgRoot, "package.json"), "utf8")).version || "unknown";
|
|
109
|
+
} catch (_) {
|
|
110
|
+
return "unknown";
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Compare a project's bin/ against the expected list.
|
|
116
|
+
* Returns { missing, stale, present } — stale means the file exists but its
|
|
117
|
+
* content differs from the package's copy.
|
|
118
|
+
*/
|
|
119
|
+
function inspect(projectDir, pkgRoot, tools) {
|
|
120
|
+
const binDir = path.join(projectDir, "bin");
|
|
121
|
+
const missing = [];
|
|
122
|
+
const stale = [];
|
|
123
|
+
let present = 0;
|
|
124
|
+
|
|
125
|
+
for (const tool of tools) {
|
|
126
|
+
const src = path.join(pkgRoot, "bin", tool);
|
|
127
|
+
const dest = path.join(binDir, tool);
|
|
128
|
+
if (!fs.existsSync(src)) {
|
|
129
|
+
// The package itself is missing a tool it promises. That is an installer
|
|
130
|
+
// defect, not a project one — recorded separately, never skipped quietly.
|
|
131
|
+
missing.push({ tool, reason: "absent from the installed package" });
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (!fs.existsSync(dest)) {
|
|
135
|
+
missing.push({ tool, reason: "absent from the project" });
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
try {
|
|
139
|
+
if (fs.readFileSync(src, "utf8") !== fs.readFileSync(dest, "utf8")) {
|
|
140
|
+
stale.push({ tool, reason: "differs from the installed package" });
|
|
141
|
+
} else {
|
|
142
|
+
present++;
|
|
143
|
+
}
|
|
144
|
+
} catch (e) {
|
|
145
|
+
missing.push({ tool, reason: `could not be compared: ${e.message}` });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return { missing, stale, present };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Copy one tool from the package into the project. Throws if it cannot. */
|
|
152
|
+
function repairOne(projectDir, pkgRoot, tool) {
|
|
153
|
+
const src = path.join(pkgRoot, "bin", tool);
|
|
154
|
+
const dest = path.join(projectDir, "bin", tool);
|
|
155
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
156
|
+
fs.copyFileSync(src, dest);
|
|
157
|
+
try { fs.chmodSync(dest, 0o755); } catch (_) { /* permissions are advisory here */ }
|
|
158
|
+
if (!fs.existsSync(dest)) throw new Error(`copy reported success but ${dest} is not there`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Append a record of what was repaired, so a cross-project pattern is visible. */
|
|
162
|
+
function recordRepair(entry) {
|
|
163
|
+
try {
|
|
164
|
+
fs.mkdirSync(path.dirname(LOG_PATH), { recursive: true });
|
|
165
|
+
fs.appendFileSync(LOG_PATH, JSON.stringify(entry) + "\n");
|
|
166
|
+
return true;
|
|
167
|
+
} catch (e) {
|
|
168
|
+
// The log could not be written. Say so out loud — a repair nobody can see
|
|
169
|
+
// is how this problem stayed invisible for weeks in the first place.
|
|
170
|
+
process.stderr.write(`[gsd-t] could not record the repair in ${LOG_PATH}: ${e.message}\n`);
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Read the shared log and summarise which tools keep going missing. */
|
|
176
|
+
function readLog() {
|
|
177
|
+
if (!fs.existsSync(LOG_PATH)) return { entries: [], byTool: {}, byProject: {} };
|
|
178
|
+
const entries = [];
|
|
179
|
+
for (const line of fs.readFileSync(LOG_PATH, "utf8").split("\n")) {
|
|
180
|
+
if (!line.trim()) continue;
|
|
181
|
+
try { entries.push(JSON.parse(line)); } catch (_) { /* one bad line does not hide the rest */ }
|
|
182
|
+
}
|
|
183
|
+
const byTool = {};
|
|
184
|
+
const byProject = {};
|
|
185
|
+
for (const e of entries) {
|
|
186
|
+
for (const t of e.repaired || []) byTool[t] = (byTool[t] || 0) + 1;
|
|
187
|
+
byProject[e.project] = (byProject[e.project] || 0) + 1;
|
|
188
|
+
}
|
|
189
|
+
return { entries, byTool, byProject };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Which tools have gone missing in MORE THAN ONE project? That points at the
|
|
194
|
+
* installer, not at any single project.
|
|
195
|
+
*/
|
|
196
|
+
function installerSuspects(byTool, entries) {
|
|
197
|
+
const projectsPerTool = {};
|
|
198
|
+
for (const e of entries) {
|
|
199
|
+
for (const t of e.repaired || []) {
|
|
200
|
+
(projectsPerTool[t] = projectsPerTool[t] || new Set()).add(e.project);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return Object.entries(projectsPerTool)
|
|
204
|
+
.filter(([, set]) => set.size > 1)
|
|
205
|
+
.map(([tool, set]) => ({ tool, projects: [...set] }))
|
|
206
|
+
.sort((a, b) => b.projects.length - a.projects.length);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function parseArgs(argv) {
|
|
210
|
+
const args = {};
|
|
211
|
+
for (let i = 2; i < argv.length; i++) {
|
|
212
|
+
const a = argv[i];
|
|
213
|
+
if (a === "--check") args.checkOnly = true;
|
|
214
|
+
else if (a === "--report") args.report = true;
|
|
215
|
+
else if (a === "--json") args.json = true;
|
|
216
|
+
else if (a === "--project") args.project = argv[++i];
|
|
217
|
+
}
|
|
218
|
+
return args;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function main() {
|
|
222
|
+
const args = parseArgs(process.argv);
|
|
223
|
+
|
|
224
|
+
if (args.report) {
|
|
225
|
+
const { entries, byTool, byProject } = readLog();
|
|
226
|
+
const suspects = installerSuspects(byTool, entries);
|
|
227
|
+
const out = { ok: true, exitCode: EXIT_OK, repairs: entries.length, byTool, byProject, installerSuspects: suspects };
|
|
228
|
+
if (args.json) process.stdout.write(JSON.stringify(out, null, 2) + "\n");
|
|
229
|
+
else if (!entries.length) process.stdout.write("No installs have needed repair.\n");
|
|
230
|
+
else {
|
|
231
|
+
process.stdout.write(`${entries.length} repair(s) recorded.\n\n`);
|
|
232
|
+
for (const [p, n] of Object.entries(byProject)) process.stdout.write(` ${p}: ${n}\n`);
|
|
233
|
+
if (suspects.length) {
|
|
234
|
+
process.stdout.write("\nThese tools went missing in more than one project — the installer is the likely cause:\n");
|
|
235
|
+
for (const s of suspects) process.stdout.write(` ${s.tool} (${s.projects.length} projects)\n`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
process.exit(EXIT_OK);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const projectDir = path.resolve(args.project || process.cwd());
|
|
242
|
+
if (!fs.existsSync(path.join(projectDir, ".gsd-t"))) {
|
|
243
|
+
// Not a GSD-T project. Nothing to check — this is "not applicable", which
|
|
244
|
+
// is a different thing from "checked and fine".
|
|
245
|
+
process.exit(EXIT_OK);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
let pkgRoot;
|
|
249
|
+
try {
|
|
250
|
+
pkgRoot = findPackageRoot();
|
|
251
|
+
} catch (e) {
|
|
252
|
+
if (args.json) process.stdout.write(JSON.stringify({ ok: false, exitCode: EXIT_UNREPAIRABLE, halt: e.message }, null, 2) + "\n");
|
|
253
|
+
else process.stderr.write(`[gsd-t] ${e.message}\n`);
|
|
254
|
+
process.exit(EXIT_UNREPAIRABLE);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
let tools;
|
|
258
|
+
try {
|
|
259
|
+
tools = expectedTools(pkgRoot);
|
|
260
|
+
} catch (e) {
|
|
261
|
+
const msg = `Cannot read the list of tools a project should have: ${e.message}`;
|
|
262
|
+
if (args.json) process.stdout.write(JSON.stringify({ ok: false, exitCode: EXIT_UNREPAIRABLE, halt: msg }, null, 2) + "\n");
|
|
263
|
+
else process.stderr.write(`[gsd-t] ${msg}\n`);
|
|
264
|
+
process.exit(EXIT_UNREPAIRABLE);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const found = inspect(projectDir, pkgRoot, tools);
|
|
268
|
+
const needsWork = [...found.missing, ...found.stale];
|
|
269
|
+
|
|
270
|
+
if (!needsWork.length) {
|
|
271
|
+
const out = { ok: true, exitCode: EXIT_OK, expected: tools.length, present: found.present, repaired: [] };
|
|
272
|
+
if (args.json) process.stdout.write(JSON.stringify(out, null, 2) + "\n");
|
|
273
|
+
process.exit(EXIT_OK);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (args.checkOnly) {
|
|
277
|
+
const out = {
|
|
278
|
+
ok: false, exitCode: EXIT_UNREPAIRABLE, expected: tools.length,
|
|
279
|
+
present: found.present, missing: found.missing, stale: found.stale,
|
|
280
|
+
};
|
|
281
|
+
if (args.json) process.stdout.write(JSON.stringify(out, null, 2) + "\n");
|
|
282
|
+
else process.stdout.write(`${needsWork.length} of ${tools.length} tools need attention in ${projectDir}\n`);
|
|
283
|
+
process.exit(EXIT_UNREPAIRABLE);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Repair.
|
|
287
|
+
const repaired = [];
|
|
288
|
+
const unrepairable = [];
|
|
289
|
+
for (const item of needsWork) {
|
|
290
|
+
if (item.reason === "absent from the installed package") {
|
|
291
|
+
unrepairable.push(item);
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
try {
|
|
295
|
+
repairOne(projectDir, pkgRoot, item.tool);
|
|
296
|
+
repaired.push(item.tool);
|
|
297
|
+
} catch (e) {
|
|
298
|
+
unrepairable.push({ tool: item.tool, reason: e.message });
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const projectName = path.basename(projectDir);
|
|
303
|
+
if (repaired.length) {
|
|
304
|
+
recordRepair({
|
|
305
|
+
at: new Date().toISOString(),
|
|
306
|
+
project: projectName,
|
|
307
|
+
projectDir,
|
|
308
|
+
packageVersion: packageVersion(pkgRoot),
|
|
309
|
+
expected: tools.length,
|
|
310
|
+
wasPresent: found.present,
|
|
311
|
+
repaired,
|
|
312
|
+
unrepairable: unrepairable.map((u) => u.tool),
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const out = {
|
|
317
|
+
ok: unrepairable.length === 0,
|
|
318
|
+
exitCode: unrepairable.length === 0 ? EXIT_OK : EXIT_UNREPAIRABLE,
|
|
319
|
+
project: projectName,
|
|
320
|
+
expected: tools.length,
|
|
321
|
+
repaired,
|
|
322
|
+
unrepairable,
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
if (args.json) {
|
|
326
|
+
process.stdout.write(JSON.stringify(out, null, 2) + "\n");
|
|
327
|
+
} else {
|
|
328
|
+
if (repaired.length) {
|
|
329
|
+
process.stdout.write(`[gsd-t] Repaired this project's install: restored ${repaired.length} missing tool(s).\n`);
|
|
330
|
+
}
|
|
331
|
+
if (unrepairable.length) {
|
|
332
|
+
process.stdout.write(`[gsd-t] ${unrepairable.length} tool(s) could NOT be restored:\n`);
|
|
333
|
+
for (const u of unrepairable) process.stdout.write(` ${u.tool} — ${u.reason}\n`);
|
|
334
|
+
process.stdout.write(`Work should not continue on a broken install. Reinstall GSD-T.\n`);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
process.exit(out.exitCode);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (require.main === module) main();
|
|
341
|
+
|
|
342
|
+
module.exports = { inspect, expectedTools, findPackageRoot, readLog, installerSuspects, LOG_PATH };
|
|
@@ -311,6 +311,8 @@ function _detectDefaultTrack2(projectDir, notes) {
|
|
|
311
311
|
|
|
312
312
|
plan.push({ id: 'schema-id', cmd: 'node', args: [path.join(__dirname, 'gsd-t-schema-id-check.cjs'), '--project', projectDir], timeoutMs: 30000 }); // integer-identity PK on every NEW relational table, FAIL-CLOSED (no-op PASS when no new schema files)
|
|
313
313
|
|
|
314
|
+
plan.push({ id: 'fallbacks', cmd: 'node', args: [path.join(__dirname, 'gsd-t-fallback-detect.cjs'), '--scan', '--project', projectDir, '--json'], timeoutMs: 120000 }); // M106: no unapproved continue-after-failure branch, FAIL-CLOSED (pre-existing ones excluded via .gsd-t/fallbacks-baseline.json)
|
|
315
|
+
|
|
314
316
|
// secrets — gitleaks (PATH detection deferred to runtime)
|
|
315
317
|
if (_hasOnPath('gitleaks')) {
|
|
316
318
|
plan.push({
|
package/bin/gsd-t.js
CHANGED
|
@@ -505,6 +505,31 @@ const WORKTREE_HOOK_MARKER = "gsd-t-worktree-guard";
|
|
|
505
505
|
const WORKTREE_HOOK_COMMAND =
|
|
506
506
|
'bash -c \'[ -f "$(npm root -g)/@tekyzinc/gsd-t/scripts/gsd-t-worktree-guard.js" ] && node "$(npm root -g)/@tekyzinc/gsd-t/scripts/gsd-t-worktree-guard.js" || true\'';
|
|
507
507
|
|
|
508
|
+
// ─── M106 Fallback guard (PreToolUse Write|Edit) ────────────────────────────
|
|
509
|
+
// Denies a write that introduces a fallback the user never approved. NO `|| true`
|
|
510
|
+
// here, unlike the worktree guard: if this script is missing, the shell must fail
|
|
511
|
+
// loudly rather than let unchecked writes through. Swallowing its absence is the
|
|
512
|
+
// exact pattern the guard exists to stop.
|
|
513
|
+
const FALLBACK_HOOK_MARKER = "gsd-t-fallback-guard";
|
|
514
|
+
const FALLBACK_HOOK_COMMAND =
|
|
515
|
+
'node "$(npm root -g)/@tekyzinc/gsd-t/scripts/gsd-t-fallback-guard.js"';
|
|
516
|
+
|
|
517
|
+
// ─── M108 Install self-heal (SessionStart) ──────────────────────────────────
|
|
518
|
+
// Checks the project's tools before any work starts, restores what is missing,
|
|
519
|
+
// and reports what it could not fix. Not a fallback — it repairs the failure
|
|
520
|
+
// and then the session runs correctly, rather than routing around it.
|
|
521
|
+
const INSTALL_HEAL_MARKER = "gsd-t-install-heal";
|
|
522
|
+
const INSTALL_HEAL_COMMAND =
|
|
523
|
+
'bash -c \'[ -f "$(npm root -g)/@tekyzinc/gsd-t/scripts/gsd-t-install-heal.js" ] && node "$(npm root -g)/@tekyzinc/gsd-t/scripts/gsd-t-install-heal.js" || true\'';
|
|
524
|
+
|
|
525
|
+
// ─── M107 Concise hook (Stop) ───────────────────────────────────────────────
|
|
526
|
+
// Sends a long reply to a fresh Claude for a shorter rewrite. Fail-open by
|
|
527
|
+
// design (`|| true`): it governs how a reply READS, never whether it is true,
|
|
528
|
+
// so a broken rewriter must never gag a correct answer.
|
|
529
|
+
const CONCISE_HOOK_MARKER = "gsd-t-concise-hook";
|
|
530
|
+
const CONCISE_HOOK_COMMAND =
|
|
531
|
+
'bash -c \'[ -f "$(npm root -g)/@tekyzinc/gsd-t/scripts/gsd-t-concise-hook.js" ] && node "$(npm root -g)/@tekyzinc/gsd-t/scripts/gsd-t-concise-hook.js" || true\'';
|
|
532
|
+
|
|
508
533
|
// Append entries to {projectDir}/.gitignore. Each entry added only if absent.
|
|
509
534
|
// Idempotent. Returns true if any entries were added, false otherwise.
|
|
510
535
|
function ensureGitignoreEntries(projectDir, entries) {
|
|
@@ -963,6 +988,76 @@ function configureWorktreeGuardHook(settingsPath) {
|
|
|
963
988
|
return configureWriteEditHook(settingsPath, WORKTREE_HOOK_MARKER, WORKTREE_HOOK_COMMAND, "worktree guard");
|
|
964
989
|
}
|
|
965
990
|
|
|
991
|
+
// M106 — register the fallback guard on Write|Edit. Same registrar again.
|
|
992
|
+
function configureFallbackGuardHook(settingsPath) {
|
|
993
|
+
return configureWriteEditHook(settingsPath, FALLBACK_HOOK_MARKER, FALLBACK_HOOK_COMMAND, "fallback guard");
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
// M107 — register the concise rewriter on Stop. Not a Write|Edit hook, so it
|
|
997
|
+
// uses the Stop registrar below rather than configureWriteEditHook.
|
|
998
|
+
function configureConciseHook(settingsPath) {
|
|
999
|
+
return configureStopHook(settingsPath, CONCISE_HOOK_MARKER, CONCISE_HOOK_COMMAND, "concise rewrite");
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
// M108 — register the install self-heal on SessionStart, so a broken install is
|
|
1003
|
+
// repaired before any work begins.
|
|
1004
|
+
function configureInstallHealHook(settingsPath) {
|
|
1005
|
+
return configureEventHook(settingsPath, "SessionStart", INSTALL_HEAL_MARKER, INSTALL_HEAL_COMMAND, "install self-heal");
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
// Register a Stop hook by marker. Mirrors configureWriteEditHook exactly, on the
|
|
1009
|
+
// Stop event instead of PreToolUse.
|
|
1010
|
+
function configureStopHook(settingsPath, marker, command, label) {
|
|
1011
|
+
return configureEventHook(settingsPath, "Stop", marker, command, label);
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
// Register a hook on any event by marker. One registrar for every event, so a
|
|
1015
|
+
// new hook needs no new copy of this logic.
|
|
1016
|
+
function configureEventHook(settingsPath, event, marker, command, label) {
|
|
1017
|
+
const targetPath = settingsPath || SETTINGS_JSON;
|
|
1018
|
+
let settings = {};
|
|
1019
|
+
if (fs.existsSync(targetPath)) {
|
|
1020
|
+
try {
|
|
1021
|
+
settings = JSON.parse(fs.readFileSync(targetPath, "utf8"));
|
|
1022
|
+
if (!settings || typeof settings !== "object") settings = {};
|
|
1023
|
+
} catch {
|
|
1024
|
+
warn(`settings.json has invalid JSON — cannot configure ${label} hook`);
|
|
1025
|
+
return { installed: false, action: "noop" };
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
if (!settings.hooks) settings.hooks = {};
|
|
1029
|
+
if (!Array.isArray(settings.hooks[event])) settings.hooks[event] = [];
|
|
1030
|
+
|
|
1031
|
+
let action = "noop";
|
|
1032
|
+
let found = false;
|
|
1033
|
+
for (const entry of settings.hooks[event]) {
|
|
1034
|
+
if (!entry || !Array.isArray(entry.hooks)) continue;
|
|
1035
|
+
for (const h of entry.hooks) {
|
|
1036
|
+
if (h && typeof h.command === "string" && h.command.includes(marker)) {
|
|
1037
|
+
found = true;
|
|
1038
|
+
if (h.command !== command) { h.command = command; action = "refreshed"; }
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
if (!found) {
|
|
1043
|
+
// The install check goes FIRST on SessionStart, so a broken install is
|
|
1044
|
+
// repaired before anything else tries to use the missing tools.
|
|
1045
|
+
const entry = { matcher: "", hooks: [{ type: "command", command }] };
|
|
1046
|
+
if (event === "SessionStart") settings.hooks[event].unshift(entry);
|
|
1047
|
+
else settings.hooks[event].push(entry);
|
|
1048
|
+
action = "added";
|
|
1049
|
+
}
|
|
1050
|
+
if (action === "noop") return { installed: true, action };
|
|
1051
|
+
|
|
1052
|
+
try {
|
|
1053
|
+
fs.writeFileSync(targetPath, JSON.stringify(settings, null, 2));
|
|
1054
|
+
} catch (e) {
|
|
1055
|
+
warn(`Failed to write settings.json: ${e.message}`);
|
|
1056
|
+
return { installed: false, action: "noop" };
|
|
1057
|
+
}
|
|
1058
|
+
return { installed: true, action };
|
|
1059
|
+
}
|
|
1060
|
+
|
|
966
1061
|
function configureWriteEditHook(settingsPath, marker, command, label) {
|
|
967
1062
|
const targetPath = settingsPath || SETTINGS_JSON;
|
|
968
1063
|
let settings = {};
|
|
@@ -1533,10 +1628,16 @@ const UTILITY_SCRIPTS = ["gsd-t-tools.js", "gsd-t-statusline.js", "gsd-t-event-w
|
|
|
1533
1628
|
|
|
1534
1629
|
function installUtilityScripts() {
|
|
1535
1630
|
ensureDir(SCRIPTS_DIR);
|
|
1631
|
+
const notDelivered = [];
|
|
1536
1632
|
for (const script of UTILITY_SCRIPTS) {
|
|
1537
1633
|
const src = path.join(PKG_SCRIPTS, script);
|
|
1538
1634
|
const dest = path.join(SCRIPTS_DIR, script);
|
|
1539
|
-
if (!fs.existsSync(src))
|
|
1635
|
+
if (!fs.existsSync(src)) {
|
|
1636
|
+
// Promised but absent from the package. Reported, never skipped quietly —
|
|
1637
|
+
// the same silence let projects run for weeks on a half install.
|
|
1638
|
+
notDelivered.push(script);
|
|
1639
|
+
continue;
|
|
1640
|
+
}
|
|
1540
1641
|
const srcContent = fs.readFileSync(src, "utf8");
|
|
1541
1642
|
const destContent = fs.existsSync(dest) ? fs.readFileSync(dest, "utf8") : "";
|
|
1542
1643
|
if (normalizeEol(srcContent) !== normalizeEol(destContent)) {
|
|
@@ -1545,6 +1646,11 @@ function installUtilityScripts() {
|
|
|
1545
1646
|
info(`${script} unchanged`);
|
|
1546
1647
|
}
|
|
1547
1648
|
}
|
|
1649
|
+
if (notDelivered.length) {
|
|
1650
|
+
warn(`${notDelivered.length} script(s) are named by the installer but absent from the package:`);
|
|
1651
|
+
for (const s of notDelivered) warn(` ${s}`);
|
|
1652
|
+
warn(` Anything depending on these will fail. This is a packaging defect.`);
|
|
1653
|
+
}
|
|
1548
1654
|
}
|
|
1549
1655
|
|
|
1550
1656
|
// ─── Global Bin Tools (~/.claude/bin/) ───────────────────────────────────────
|
|
@@ -1591,6 +1697,18 @@ const GLOBAL_BIN_TOOLS = [
|
|
|
1591
1697
|
// caller but absent from BOTH lists is dead in every project
|
|
1592
1698
|
// ([[project_global_bin_propagation_gap]], 4 prior occurrences).
|
|
1593
1699
|
"gsd-t-pseudocode-style.cjs",
|
|
1700
|
+
// M106 — Fallback detector. scripts/gsd-t-fallback-guard.js (a PreToolUse hook)
|
|
1701
|
+
// shells out to this by absolute path; if it is missing the guard DENIES the
|
|
1702
|
+
// write rather than allowing it unchecked, so an omission here breaks every
|
|
1703
|
+
// Write/Edit rather than failing silently. Also in PROJECT_BIN_TOOLS below.
|
|
1704
|
+
"gsd-t-fallback-detect.cjs",
|
|
1705
|
+
// M107 — Concise rewriter. scripts/gsd-t-concise-hook.js (a Stop hook) shells
|
|
1706
|
+
// out to this; absent, the hook allows the long reply through (fail-open by
|
|
1707
|
+
// design — it governs readability, never truth).
|
|
1708
|
+
"gsd-t-concise-rewrite.cjs",
|
|
1709
|
+
// M108 — Install self-check, run by the SessionStart hook and by
|
|
1710
|
+
// `gsd-t install-check`.
|
|
1711
|
+
"gsd-t-install-check.cjs",
|
|
1594
1712
|
];
|
|
1595
1713
|
|
|
1596
1714
|
function installGlobalBinTools() {
|
|
@@ -1838,10 +1956,14 @@ const SHARED_TEMPLATES = [
|
|
|
1838
1956
|
function installSharedTemplates() {
|
|
1839
1957
|
ensureDir(CLAUDE_TEMPLATES_DIR);
|
|
1840
1958
|
let installed = 0, skipped = 0;
|
|
1959
|
+
const notDelivered = [];
|
|
1841
1960
|
for (const file of SHARED_TEMPLATES) {
|
|
1842
1961
|
const src = path.join(PKG_TEMPLATES, file);
|
|
1843
1962
|
const dest = path.join(CLAUDE_TEMPLATES_DIR, file);
|
|
1844
|
-
if (!fs.existsSync(src))
|
|
1963
|
+
if (!fs.existsSync(src)) {
|
|
1964
|
+
notDelivered.push(file); // promised but absent — reported, never silent
|
|
1965
|
+
continue;
|
|
1966
|
+
}
|
|
1845
1967
|
if (fs.existsSync(dest) &&
|
|
1846
1968
|
normalizeEol(fs.readFileSync(src, "utf8")) === normalizeEol(fs.readFileSync(dest, "utf8"))) {
|
|
1847
1969
|
skipped++;
|
|
@@ -1852,6 +1974,10 @@ function installSharedTemplates() {
|
|
|
1852
1974
|
}
|
|
1853
1975
|
if (skipped > 0) info(`${skipped} templates unchanged`);
|
|
1854
1976
|
success(`${installed + skipped} shared templates → ~/.claude/templates/`);
|
|
1977
|
+
if (notDelivered.length) {
|
|
1978
|
+
warn(`${notDelivered.length} template(s) are named by the installer but absent from the package:`);
|
|
1979
|
+
for (const f of notDelivered) warn(` ${f}`);
|
|
1980
|
+
}
|
|
1855
1981
|
}
|
|
1856
1982
|
|
|
1857
1983
|
function installCommands(isUpdate) {
|
|
@@ -2035,6 +2161,27 @@ async function doInstall(opts = {}) {
|
|
|
2035
2161
|
else info("Worktree-collision guard already configured");
|
|
2036
2162
|
}
|
|
2037
2163
|
|
|
2164
|
+
const fbHook = configureFallbackGuardHook(SETTINGS_JSON);
|
|
2165
|
+
if (fbHook.installed) {
|
|
2166
|
+
if (fbHook.action === "added") success("Fallback guard added (denies a write that adds an unapproved fallback — M106)");
|
|
2167
|
+
else if (fbHook.action === "refreshed") success("Fallback guard refreshed");
|
|
2168
|
+
else info("Fallback guard already configured");
|
|
2169
|
+
}
|
|
2170
|
+
|
|
2171
|
+
const ccHook = configureConciseHook(SETTINGS_JSON);
|
|
2172
|
+
if (ccHook.installed) {
|
|
2173
|
+
if (ccHook.action === "added") success("Concise-rewrite hook added (shortens a long reply before you read it — M107)");
|
|
2174
|
+
else if (ccHook.action === "refreshed") success("Concise-rewrite hook refreshed");
|
|
2175
|
+
else info("Concise-rewrite hook already configured");
|
|
2176
|
+
}
|
|
2177
|
+
|
|
2178
|
+
const healHook = configureInstallHealHook(SETTINGS_JSON);
|
|
2179
|
+
if (healHook.installed) {
|
|
2180
|
+
if (healHook.action === "added") success("Install self-heal added (restores missing tools before work starts — M108)");
|
|
2181
|
+
else if (healHook.action === "refreshed") success("Install self-heal refreshed");
|
|
2182
|
+
else info("Install self-heal already configured");
|
|
2183
|
+
}
|
|
2184
|
+
|
|
2038
2185
|
heading("Graph Engine (CGC)");
|
|
2039
2186
|
installCgc();
|
|
2040
2187
|
|
|
@@ -2993,6 +3140,16 @@ const PROJECT_BIN_TOOLS = [
|
|
|
2993
3140
|
"gsd-t-model-tier-policy.cjs",
|
|
2994
3141
|
// M86 — Model-profile config + resolver CLI (standard/pro/premium tier-spend switch).
|
|
2995
3142
|
"gsd-t-model-profile.cjs",
|
|
3143
|
+
// M106 — Fallback detector. The PreToolUse guard prefers the project-local
|
|
3144
|
+
// copy; the verify gate dispatches to it by absolute path. Missing here means
|
|
3145
|
+
// every Write/Edit is DENIED (the guard halts rather than allowing unchecked),
|
|
3146
|
+
// so this entry is load-bearing — [[project_global_bin_propagation_gap]].
|
|
3147
|
+
"gsd-t-fallback-detect.cjs",
|
|
3148
|
+
// M107 — Concise rewriter, invoked by the Stop hook.
|
|
3149
|
+
"gsd-t-concise-rewrite.cjs",
|
|
3150
|
+
// M108 — Install self-check. Every project carries its own copy so it can
|
|
3151
|
+
// verify and repair itself even when the global install is what broke.
|
|
3152
|
+
"gsd-t-install-check.cjs",
|
|
2996
3153
|
// M89 — Auto-research gate classifier (classify a guessed claim as internal vs external;
|
|
2997
3154
|
// propagated to each registered project's bin/ so the workflow runCli fallback resolves
|
|
2998
3155
|
// downstream — per [[project_global_bin_propagation_gap]]).
|
|
@@ -3157,10 +3314,20 @@ function copyBinToolsToProject(projectDir, projectName) {
|
|
|
3157
3314
|
}
|
|
3158
3315
|
}
|
|
3159
3316
|
let copied = 0;
|
|
3317
|
+
// Tools this pass could NOT deliver. Every one is reported. A silent skip
|
|
3318
|
+
// here is how binvoice ran for weeks with 20 of 38 tools while every update
|
|
3319
|
+
// reported success — the missing files were never counted, so nothing looked
|
|
3320
|
+
// wrong. ([[project_global_bin_propagation_gap]], now 5 occurrences.)
|
|
3321
|
+
const notDelivered = [];
|
|
3160
3322
|
for (const tool of PROJECT_BIN_TOOLS) {
|
|
3161
3323
|
const src = path.join(PKG_ROOT, "bin", tool);
|
|
3162
3324
|
const dest = path.join(projectBinDir, tool);
|
|
3163
|
-
if (!fs.existsSync(src))
|
|
3325
|
+
if (!fs.existsSync(src)) {
|
|
3326
|
+
// The package promises a tool it does not contain. That is a packaging
|
|
3327
|
+
// defect and it affects EVERY project — it must never pass unremarked.
|
|
3328
|
+
notDelivered.push({ tool, why: "absent from the package being installed" });
|
|
3329
|
+
continue;
|
|
3330
|
+
}
|
|
3164
3331
|
let needsCopy = true;
|
|
3165
3332
|
if (fs.existsSync(dest)) {
|
|
3166
3333
|
try {
|
|
@@ -3168,7 +3335,7 @@ function copyBinToolsToProject(projectDir, projectName) {
|
|
|
3168
3335
|
const destContent = fs.readFileSync(dest, "utf8");
|
|
3169
3336
|
if (srcContent === destContent) needsCopy = false;
|
|
3170
3337
|
} catch {
|
|
3171
|
-
//
|
|
3338
|
+
// could not compare — copy, which is the safe direction
|
|
3172
3339
|
}
|
|
3173
3340
|
}
|
|
3174
3341
|
if (needsCopy) {
|
|
@@ -3177,10 +3344,17 @@ function copyBinToolsToProject(projectDir, projectName) {
|
|
|
3177
3344
|
try { fs.chmodSync(dest, 0o755); } catch {}
|
|
3178
3345
|
copied++;
|
|
3179
3346
|
} catch (e) {
|
|
3180
|
-
|
|
3347
|
+
notDelivered.push({ tool, why: e.message });
|
|
3181
3348
|
}
|
|
3182
3349
|
}
|
|
3183
3350
|
}
|
|
3351
|
+
if (notDelivered.length) {
|
|
3352
|
+
warn(`${projectName} — ${notDelivered.length} of ${PROJECT_BIN_TOOLS.length} tools were NOT delivered:`);
|
|
3353
|
+
for (const n of notDelivered) warn(` ${n.tool} — ${n.why}`);
|
|
3354
|
+
warn(` This project's install is incomplete. Tools that depend on the missing`);
|
|
3355
|
+
warn(` files will fail. Run 'gsd-t install-check --project ${projectDir}' after`);
|
|
3356
|
+
warn(` fixing the package.`);
|
|
3357
|
+
}
|
|
3184
3358
|
let cleaned = 0;
|
|
3185
3359
|
if (!_isGsdTSourcePackage(projectDir)) {
|
|
3186
3360
|
for (const stray of DEPRECATED_BIN_STRAYS) {
|
|
@@ -5369,6 +5543,20 @@ if (require.main === module) {
|
|
|
5369
5543
|
});
|
|
5370
5544
|
process.exit(res.status == null ? 1 : res.status);
|
|
5371
5545
|
}
|
|
5546
|
+
case "install-check": {
|
|
5547
|
+
// M108 — check this project's tools against the installed package, and
|
|
5548
|
+
// restore anything missing. `--check` reports without changing anything;
|
|
5549
|
+
// `--report` reads the shared log of every repair across all projects.
|
|
5550
|
+
const { spawnSync } = require("child_process");
|
|
5551
|
+
const js = path.join(__dirname, "gsd-t-install-check.cjs");
|
|
5552
|
+
if (!require("node:fs").existsSync(js)) {
|
|
5553
|
+
error(`gsd-t-install-check.cjs not found at ${js} — reinstall GSD-T`);
|
|
5554
|
+
process.exit(1);
|
|
5555
|
+
}
|
|
5556
|
+
const res = spawnSync(process.execPath, [js, ...args.slice(1)], { stdio: "inherit" });
|
|
5557
|
+
process.exit(res.status == null ? 1 : res.status);
|
|
5558
|
+
}
|
|
5559
|
+
|
|
5372
5560
|
case "pseudocode-style": {
|
|
5373
5561
|
// Contract v1.2.0 §1.1 — `gsd-t pseudocode-style (--doc <f> | --dir <d>)` thin
|
|
5374
5562
|
// dispatcher to the PseudoCode flow-line STYLE gate. Sibling of `guard-map`:
|