@tekyzinc/gsd-t 5.9.10 → 5.10.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 CHANGED
@@ -2,6 +2,34 @@
2
2
 
3
3
  All notable changes to GSD-T are documented here. Updated with each release.
4
4
 
5
+ ## [5.10.10] - 2026-08-07
6
+
7
+ ### Added — a project's CLAUDE.md is written from what actually happened in it
8
+
9
+ The template was GSD-T's own CLAUDE.md. It byte-copied into another project with that project's name substituted into GSD-T's own prose — 127 lines describing an npm CLI installer the project was not. Every new project was getting a file about the wrong software.
10
+
11
+ **`/gsd-t-setup` now reads the project's history first.** A rule that keeps getting broken is the rule that most needs writing down, and nobody recalls those on request — they have to be found. Three sources: git, the decision log, and what you typed in past sessions.
12
+
13
+ Session history is large — one project holds 315 MB across 57 sessions. Keeping only what you typed leaves 645 KB; dropping pasted logs leaves 92 complaint-shaped turns. **735 milliseconds, no subagents.**
14
+
15
+ The search terms were corrected against real transcripts. Every obvious guess scored zero — "I never asked you to", "that's the third time". People don't accuse; they restate the requirement. "still not", "you keep", "why did you" are what actually appear.
16
+
17
+ **Then it shows you the rules the project already states, and you tick the ones that can never be broken.** Six sources, ranked by how many agree — repetition is the evidence. Each rule shows where it came from, so you're confirming something you already said rather than recalling it. Capped at 12 on screen; the rest are written to a file, never dropped.
18
+
19
+ On one project the top four are its real inviolable rules with their ids. Three other projects each surface their own true rule first.
20
+
21
+ **The template is replaced** with a real mold: 45 lines, every section omittable with a stated reason, and one rule written into the mold itself — nothing that carries a number which changes on its own. A version, a line count, or "currently in progress" is wrong within a week and belongs in `progress.md`.
22
+
23
+ - `bin/gsd-t-project-history.cjs`: the funnel, three sources, each reporting whether it was there
24
+ - `bin/gsd-t-rule-mine.cjs`: six sources, deduplicated by what the rule claims
25
+ - `templates/CLAUDE-project.md`: replaced
26
+ - `commands/gsd-t-setup.md`: reads history, shows a tick-list, uses the mold
27
+ - `test/m109-project-claude-md.test.js`: 15 tests
28
+
29
+ **Not fixed, and said plainly:** a project CLAUDE.md is still written once and never updated, so a fresh file starts going stale immediately. The fix — giving project files the same marker-block treatment the global file has — is a separate milestone.
30
+
31
+ Run `/gsd-t-setup` inside a project to rewrite its file. It shows you the result and waits for a yes before writing.
32
+
5
33
  ## [5.9.10] - 2026-08-07
6
34
 
7
35
  ### Added — projects repair their own install, and fallbacks need approval by name
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # GSD-T: Contract-Driven Development for Claude Code
2
2
 
3
- **v5.9.10** - A methodology for reliable, parallelizable development using Claude Code with optional Agent Teams support.
3
+ **v5.10.10** - A methodology for reliable, parallelizable development using Claude Code with optional Agent Teams support.
4
4
 
5
5
  **Eliminates context rot** — task-level fresh dispatch (one subagent per task, ~10-20% context each) means compaction never triggers.
6
6
  **Compaction-proof debug loops** — `gsd-t headless --debug-loop` runs test-fix-retest cycles as separate `claude -p` sessions. A JSONL debug ledger persists all hypothesis/fix/learning history across fresh sessions. Anti-repetition preamble injection prevents retrying failed hypotheses. Escalation tiers (sonnet → opus → human) and a hard iteration ceiling enforced externally.
@@ -0,0 +1,374 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * gsd-t-project-history.cjs
4
+ *
5
+ * M109-D1 — Reads what actually happened in a project, so its CLAUDE.md can be
6
+ * written from evidence rather than from a template.
7
+ *
8
+ * [RULE] history-distinguishes-absent-from-unreadable
9
+ * [RULE] history-names-every-session-it-could-not-read
10
+ * [RULE] history-reads-what-the-user-typed-not-what-was-pasted
11
+ *
12
+ * Three sources, cheapest first: git, the decision log, and what the user typed
13
+ * in past sessions. The transcripts are the richest and the largest — binvoice
14
+ * holds 315 MB across 27 sessions — so they are funnelled, not read whole:
15
+ *
16
+ * 315 MB raw sessions
17
+ * 2.9 MB keep only the user's own turns
18
+ * 645 KB drop pasted logs (35 giant lines held 78% of the bytes)
19
+ * 7.4 KB keep only complaint-shaped lines
20
+ *
21
+ * Two seconds, no subagents, and the result fits in context.
22
+ *
23
+ * ─── Absent is not the same as unreadable ───────────────────────────────────
24
+ * A source that ISN'T THERE is an ordinary fact — a project may have no git
25
+ * history. A source that IS there but cannot be read is a FAILURE, and saying
26
+ * "not available" about it would produce a thinner CLAUDE.md that looks
27
+ * complete. Those two cases are reported differently, and the second one halts.
28
+ *
29
+ * The one sanctioned exception (David's call, 2026-08-07): among many session
30
+ * files, one that cannot be read is skipped and NAMED rather than halting —
31
+ * a single truncated in-progress session should not block the rewrite. Every
32
+ * skipped session is listed in the output and belongs in the generated file.
33
+ *
34
+ * ─── Usage ──────────────────────────────────────────────────────────────────
35
+ * node gsd-t-project-history.cjs --project <dir> [--json] [--sessions N]
36
+ *
37
+ * ─── Exit codes ─────────────────────────────────────────────────────────────
38
+ * 0 read something
39
+ * 4 no history at all — the caller must halt, never write a thin file
40
+ * 64 a source exists but could not be read, or bad input
41
+ *
42
+ * Zero dependencies.
43
+ */
44
+
45
+ "use strict";
46
+
47
+ const fs = require("fs");
48
+ const os = require("os");
49
+ const path = require("path");
50
+ const { execFileSync } = require("child_process");
51
+
52
+ const EXIT_OK = 0;
53
+ const EXIT_NO_HISTORY = 4;
54
+ const EXIT_UNREADABLE = 64;
55
+
56
+ // A pasted log is not something the user typed. 35 such lines held 78% of
57
+ // binvoice's post-filter bytes.
58
+ const PASTE_THRESHOLD = 2000;
59
+
60
+ /** Thrown when a source exists but cannot be read. Never treated as absent. */
61
+ class Unreadable extends Error {}
62
+
63
+ /**
64
+ * What a complaint actually looks like — measured against real transcripts,
65
+ * not assumed. The obvious guesses all scored zero:
66
+ * "I never asked you to" 0 hits
67
+ * "that's the third time" 0 hits
68
+ * "you were supposed to" 0 hits
69
+ * The user does not accuse. He restates the requirement.
70
+ */
71
+ const COMPLAINT = new RegExp([
72
+ "still (not|isn'?t|doesn'?t|don'?t|broken|failing|wrong|happening)",
73
+ "you keep",
74
+ "why (did|didn'?t|do|does|is|are) (you|it|this)",
75
+ "keeps? (happening|coming up|breaking|failing)",
76
+ "hard rule",
77
+ "(third|3rd|second|2nd) (time|attempt|try)",
78
+ "same (result|issue|problem|error|thing)",
79
+ "revert(ed|ing)?\\b",
80
+ "that'?s (wrong|not right|incorrect)",
81
+ "without asking",
82
+ "never (do|again|clear|change|touch|delete|assume)",
83
+ "don'?t (just|ever|keep|do that)",
84
+ "i (didn'?t|never) (ask|say|want)",
85
+ "stop (doing|adding|using)",
86
+ "you (broke|removed|deleted|changed) ",
87
+ "not what i (asked|wanted|said)",
88
+ ].join("|"), "i");
89
+
90
+ // Turns that are machinery, not the user speaking.
91
+ const NOT_TYPED = [
92
+ /^<command-name>/,
93
+ /^<local-command/,
94
+ /^\[Request interrupted/,
95
+ /^<system-reminder>/,
96
+ /^Caveat: The messages below/,
97
+ /hook (success|feedback)/i,
98
+ ];
99
+
100
+ /** Where Claude Code keeps a project's sessions. */
101
+ function transcriptDir(projectDir) {
102
+ const encoded = path.resolve(projectDir).replace(/\//g, "-");
103
+ return path.join(os.homedir(), ".claude", "projects", encoded);
104
+ }
105
+
106
+ /** The text of one turn, whatever shape the record uses. */
107
+ function turnText(record) {
108
+ const c = record && record.message && record.message.content;
109
+ if (typeof c === "string") return c;
110
+ if (!Array.isArray(c)) return "";
111
+ return c.filter((b) => b && b.type === "text").map((b) => b.text || "").join("\n");
112
+ }
113
+
114
+ /**
115
+ * The funnel. Returns every complaint-shaped thing the user typed, newest
116
+ * session first. A session that cannot be read is skipped and NAMED.
117
+ */
118
+ function readTranscripts(projectDir, maxSessions) {
119
+ const dir = transcriptDir(projectDir);
120
+ if (!fs.existsSync(dir)) {
121
+ return { present: false, why: `no session history at ${dir}` };
122
+ }
123
+
124
+ let names;
125
+ try {
126
+ names = fs.readdirSync(dir).filter((f) => f.endsWith(".jsonl"));
127
+ } catch (e) {
128
+ throw new Unreadable(`${dir} exists but could not be listed: ${e.message}`);
129
+ }
130
+ if (!names.length) return { present: false, why: `no sessions recorded in ${dir}` };
131
+
132
+ let files = names
133
+ .map((f) => {
134
+ const full = path.join(dir, f);
135
+ return { full, mtime: fs.statSync(full).mtimeMs };
136
+ })
137
+ .sort((a, b) => b.mtime - a.mtime)
138
+ .map((x) => x.full);
139
+
140
+ if (maxSessions > 0 && files.length > maxSessions) files = files.slice(0, maxSessions);
141
+
142
+ const complaints = [];
143
+ const skipped = [];
144
+ const stats = { sessions: files.length, rawBytes: 0, userTurns: 0, afterPaste: 0, matched: 0 };
145
+
146
+ for (const file of files) {
147
+ let raw;
148
+ try {
149
+ stats.rawBytes += fs.statSync(file).size;
150
+ raw = fs.readFileSync(file, "utf8");
151
+ } catch (e) {
152
+ // Sanctioned: one bad session does not block the rewrite, but it is named
153
+ // here and reported to the user. Never silent.
154
+ skipped.push({ session: path.basename(file, ".jsonl"), why: e.message });
155
+ continue;
156
+ }
157
+
158
+ const session = path.basename(file, ".jsonl");
159
+ for (const line of raw.split("\n")) {
160
+ if (!line) continue;
161
+ let rec;
162
+ try { rec = JSON.parse(line); } catch (_) { continue; } // a half-written last line
163
+ if (!rec || rec.type !== "user" || rec.isSidechain || rec.isMeta) continue;
164
+
165
+ const text = turnText(rec).trim();
166
+ if (!text) continue;
167
+ stats.userTurns++;
168
+
169
+ if (text.length > PASTE_THRESHOLD) continue; // pasted, not typed
170
+ if (NOT_TYPED.some((re) => re.test(text))) continue;
171
+ stats.afterPaste++;
172
+
173
+ if (!COMPLAINT.test(text)) continue;
174
+ stats.matched++;
175
+
176
+ complaints.push({
177
+ session,
178
+ when: rec.timestamp || null,
179
+ branch: rec.gitBranch || null,
180
+ text: text.length > 600 ? text.slice(0, 600) + "…" : text,
181
+ });
182
+ }
183
+ }
184
+
185
+ return { present: true, complaints, skipped, stats };
186
+ }
187
+
188
+ /** Decision-log entries — already curated, much smaller. */
189
+ function readDecisionLog(projectDir) {
190
+ const p = path.join(projectDir, ".gsd-t", "progress.md");
191
+ if (!fs.existsSync(p)) return { present: false, why: "no .gsd-t/progress.md" };
192
+
193
+ let text;
194
+ try {
195
+ text = fs.readFileSync(p, "utf8");
196
+ } catch (e) {
197
+ throw new Unreadable(`progress.md exists but could not be read: ${e.message}`);
198
+ }
199
+
200
+ const start = text.indexOf("## Decision Log");
201
+ if (start === -1) return { present: false, why: "progress.md has no Decision Log section" };
202
+
203
+ // A date stamp carries a time and a zone — "2026-08-05 21:15 PDT: [debug] …" —
204
+ // so the colon that ends it is the one AFTER the zone, not the first one on
205
+ // the line. Matching the first colon finds nothing.
206
+ const entries = [];
207
+ for (const line of text.slice(start).split("\n")) {
208
+ const m = line.match(/^-\s+(\d{4}-\d{2}-\d{2}(?:\s+[\d:]+)?(?:\s+[A-Z]{2,5})?)\s*:\s*(.+)$/);
209
+ if (!m) continue;
210
+ entries.push({ when: m[1], text: m[2].length > 400 ? m[2].slice(0, 400) + "…" : m[2] });
211
+ }
212
+ return { present: true, entries };
213
+ }
214
+
215
+ /**
216
+ * Files fixed the same way repeatedly. A file that keeps being fixed is a rule
217
+ * nobody wrote down — and this works when there are no transcripts at all.
218
+ */
219
+ function readGit(projectDir) {
220
+ const git = (args) => execFileSync("git", ["-C", projectDir, ...args], {
221
+ encoding: "utf8", timeout: 20000, maxBuffer: 32 * 1024 * 1024,
222
+ });
223
+
224
+ if (!fs.existsSync(path.join(projectDir, ".git"))) {
225
+ return { present: false, why: "not a git repository" };
226
+ }
227
+
228
+ let commits, branch;
229
+ try {
230
+ commits = parseInt(git(["rev-list", "--count", "HEAD"]).trim(), 10);
231
+ branch = git(["branch", "--show-current"]).trim();
232
+ } catch (e) {
233
+ throw new Unreadable(`.git exists but git could not read it: ${e.message}`);
234
+ }
235
+
236
+ // A real revert says so at the start of its subject. Matching "broke" or a
237
+ // bare "revert" anywhere pulled in 20 ordinary commits on binvoice — every
238
+ // one a false positive, which buries the handful that are real.
239
+ let reverts;
240
+ try {
241
+ reverts = git(["log", "--oneline", "-i", "--grep=^revert", "--grep=^chore: revert", "--grep=reverts commit", "-n", "40"])
242
+ .split("\n").filter(Boolean).slice(0, 20);
243
+ } catch (e) {
244
+ throw new Unreadable(`git log failed while looking for reverts: ${e.message}`);
245
+ }
246
+
247
+ let hotFiles;
248
+ try {
249
+ const counts = {};
250
+ const out = git(["log", "-i", "--name-only", "--pretty=format:", "--grep=fix", "-n", "300"]);
251
+ for (const f of out.split("\n")) {
252
+ const name = f.trim();
253
+ if (!name) continue;
254
+ counts[name] = (counts[name] || 0) + 1;
255
+ }
256
+ hotFiles = Object.entries(counts)
257
+ .filter(([, n]) => n >= 3)
258
+ .sort((a, b) => b[1] - a[1])
259
+ .slice(0, 12)
260
+ .map(([file, fixes]) => ({ file, fixes }));
261
+ } catch (e) {
262
+ throw new Unreadable(`git log failed while looking for repeatedly-fixed files: ${e.message}`);
263
+ }
264
+
265
+ return { present: true, commits, branch, reverts, hotFiles };
266
+ }
267
+
268
+ function parseArgs(argv) {
269
+ const args = { project: process.cwd(), sessions: 0 };
270
+ for (let i = 2; i < argv.length; i++) {
271
+ const a = argv[i];
272
+ if (a === "--json") args.json = true;
273
+ else if (a === "--project") args.project = argv[++i];
274
+ else if (a === "--sessions") args.sessions = parseInt(argv[++i], 10) || 0;
275
+ }
276
+ return args;
277
+ }
278
+
279
+ function halt(message, asJson) {
280
+ if (asJson) {
281
+ process.stdout.write(JSON.stringify({ ok: false, exitCode: EXIT_UNREADABLE, halt: message }, null, 2) + "\n");
282
+ } else {
283
+ process.stderr.write(`[gsd-t] ${message}\n`);
284
+ }
285
+ process.exit(EXIT_UNREADABLE);
286
+ }
287
+
288
+ function main() {
289
+ const args = parseArgs(process.argv);
290
+ const projectDir = path.resolve(args.project);
291
+ const asJson = args.json || !process.stdout.isTTY;
292
+
293
+ if (!fs.existsSync(projectDir)) halt(`No such directory: ${projectDir}`, asJson);
294
+
295
+ const started = Date.now();
296
+
297
+ // A source that exists but cannot be read HALTS. Reporting it as "not
298
+ // available" would produce a thinner CLAUDE.md that looks complete.
299
+ let git, log, sessions;
300
+ try {
301
+ git = readGit(projectDir);
302
+ log = readDecisionLog(projectDir);
303
+ sessions = readTranscripts(projectDir, args.sessions);
304
+ } catch (e) {
305
+ if (e instanceof Unreadable) {
306
+ halt(
307
+ `${e.message}\n\nThis source exists, so its contents belong in the rewrite. ` +
308
+ `Continuing would produce a CLAUDE.md that looks complete but isn't.`,
309
+ asJson
310
+ );
311
+ }
312
+ throw e;
313
+ }
314
+
315
+ // Every source says whether it was there, so a thin result can never be
316
+ // mistaken for "this project has no history."
317
+ const sources = {
318
+ git: git.present ? `${git.commits} commits` : `none — ${git.why}`,
319
+ decisionLog: log.present ? `${log.entries.length} entries` : `none — ${log.why}`,
320
+ sessions: sessions.present
321
+ ? `${sessions.stats.sessions} sessions, ${sessions.stats.matched} things worth reading`
322
+ : `none — ${sessions.why}`,
323
+ };
324
+
325
+ const anything = git.present || log.present || sessions.present;
326
+
327
+ const result = {
328
+ ok: anything,
329
+ exitCode: anything ? EXIT_OK : EXIT_NO_HISTORY,
330
+ project: path.basename(projectDir),
331
+ sources,
332
+ tookMs: Date.now() - started,
333
+ git: git.present ? git : null,
334
+ decisionLog: log.present ? log.entries.slice(0, 40) : null,
335
+ complaints: sessions.present ? sessions.complaints : null,
336
+ skippedSessions: sessions.present ? sessions.skipped : [],
337
+ funnel: sessions.present ? sessions.stats : null,
338
+ };
339
+
340
+ if (!anything) {
341
+ result.halt = "No history at all. Do not write a thinner CLAUDE.md and call it normal — ask the user how to proceed.";
342
+ }
343
+
344
+ if (asJson) {
345
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
346
+ } else {
347
+ process.stdout.write(`History for ${result.project}\n\n`);
348
+ for (const [k, v] of Object.entries(sources)) process.stdout.write(` ${k.padEnd(13)} ${v}\n`);
349
+ if (result.skippedSessions.length) {
350
+ process.stdout.write(`\n ${result.skippedSessions.length} session(s) could NOT be read:\n`);
351
+ for (const s of result.skippedSessions) process.stdout.write(` ${s.session} — ${s.why}\n`);
352
+ }
353
+ if (sessions.present) {
354
+ const s = sessions.stats;
355
+ process.stdout.write(`\n ${(s.rawBytes / 1e6).toFixed(0)} MB raw → ${s.userTurns} typed → ${s.afterPaste} after dropping pastes → ${s.matched} worth reading\n`);
356
+ }
357
+ if (result.complaints && result.complaints.length) {
358
+ process.stdout.write(`\nWhat kept going wrong:\n\n`);
359
+ for (const c of result.complaints.slice(0, 15)) {
360
+ process.stdout.write(` ${(c.when || "").slice(0, 10)} ${c.text.replace(/\s+/g, " ").slice(0, 110)}\n`);
361
+ }
362
+ }
363
+ if (git.present && git.hotFiles.length) {
364
+ process.stdout.write(`\nFiles fixed repeatedly:\n`);
365
+ for (const h of git.hotFiles.slice(0, 8)) process.stdout.write(` ${h.fixes}× ${h.file}\n`);
366
+ }
367
+ process.stdout.write(`\n(${result.tookMs} ms)\n`);
368
+ }
369
+ process.exit(result.exitCode);
370
+ }
371
+
372
+ if (require.main === module) main();
373
+
374
+ module.exports = { readTranscripts, readDecisionLog, readGit, transcriptDir, COMPLAINT, turnText, Unreadable };
@@ -0,0 +1,467 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * gsd-t-rule-mine.cjs
4
+ *
5
+ * M109-D2 — Finds every rule already stated in a project, so David can tick the
6
+ * ones that can never be broken instead of trying to recall them.
7
+ *
8
+ * [RULE] rule-mine-never-invents-a-rule
9
+ * [RULE] rule-mine-shows-where-each-came-from
10
+ * [RULE] rule-mine-keeps-what-it-cannot-rank
11
+ *
12
+ * Six sources, best evidence first. A rule found in three places ranks top —
13
+ * repetition IS the evidence.
14
+ *
15
+ * Every rule carries where it came from. That column is what makes the list
16
+ * fast to read: David is confirming something he already said, not recalling it.
17
+ *
18
+ * ─── Usage ──────────────────────────────────────────────────────────────────
19
+ * node gsd-t-rule-mine.cjs --project <dir> [--json] [--top N]
20
+ *
21
+ * ─── Exit codes ─────────────────────────────────────────────────────────────
22
+ * 0 found rules
23
+ * 4 found none — the caller must ask, never write a rules section anyway
24
+ * 64 a source exists but could not be read
25
+ *
26
+ * Zero dependencies.
27
+ */
28
+
29
+ "use strict";
30
+
31
+ const fs = require("fs");
32
+ const path = require("path");
33
+ const { execFileSync } = require("child_process");
34
+
35
+ const EXIT_OK = 0;
36
+ const EXIT_NONE = 4;
37
+ const EXIT_UNREADABLE = 64;
38
+
39
+ const DEFAULT_TOP = 12; // more than this on screen has already failed
40
+
41
+ /** Thrown when a source exists but cannot be read. Never treated as absent. */
42
+ class Unreadable extends Error {}
43
+
44
+ /**
45
+ * Lines that state a rule. Deliberately broad — a rule wrongly included costs
46
+ * one glance, a rule missed costs a broken build.
47
+ */
48
+ const RULE_SHAPE = new RegExp([
49
+ "\\bnever\\b",
50
+ "\\balways\\b",
51
+ "\\bmust (not |never )?\\b",
52
+ "\\bdo not\\b|\\bdon'?t\\b",
53
+ "\\bforbidden\\b|\\bbanned\\b|\\bprohibited\\b",
54
+ "\\brequired\\b|\\bmandatory\\b",
55
+ "\\bonly ever\\b|\\bunder no circumstances\\b",
56
+ "\\bread-?only\\b",
57
+ "\\bno-?fallback\\b",
58
+ ].join("|"), "i");
59
+
60
+ // Prose that mentions a rule without being one.
61
+ const NOT_A_RULE = [
62
+ /^#{1,6}\s/, // a heading
63
+ /^\|.*\|.*\|/, // a table row
64
+ /^\s*```/,
65
+ /^\s*[-*]\s*\[[ x]\]/i, // a checklist item
66
+ /\bfor example\b|\be\.g\.\b/i,
67
+ /^\s*<!--/,
68
+ ];
69
+
70
+ function clean(line) {
71
+ return line
72
+ .replace(/^\s*[-*+]\s+/, "")
73
+ .replace(/^\s*\d+\.\s+/, "")
74
+ .replace(/\*\*/g, "")
75
+ .replace(/`/g, "")
76
+ .replace(/\[([^\]]+)\]\([^)]*\)/g, "$1")
77
+ .trim();
78
+ }
79
+
80
+ /**
81
+ * The claim a rule makes, stripped of wording, so the same rule stated three
82
+ * different ways collapses to one entry.
83
+ */
84
+ function claimKey(text) {
85
+ return text
86
+ .toLowerCase()
87
+ .replace(/[^a-z0-9\s]/g, " ")
88
+ .split(/\s+/)
89
+ .filter((w) => w.length > 3 && !STOP.has(w))
90
+ .sort()
91
+ .slice(0, 8)
92
+ .join(" ");
93
+ }
94
+
95
+ const STOP = new Set([
96
+ "never", "always", "must", "should", "would", "could", "does", "this", "that",
97
+ "with", "from", "into", "when", "what", "which", "have", "been", "will",
98
+ "they", "them", "then", "than", "your", "yours", "make", "made", "only",
99
+ "ever", "under", "unless", "because", "there", "their", "these", "those",
100
+ ]);
101
+
102
+ /**
103
+ * A fragment is a piece of a sentence torn out of a paragraph — "domain never
104
+ * opens orders.ts.", "empty/flagged), never javascript:/data:.". It reads as a
105
+ * rule to a pattern but tells a human nothing they can act on, and it buries
106
+ * the real rules. A rule must start like a sentence and stand on its own.
107
+ */
108
+ function isFragment(t) {
109
+ if (/^[a-z]/.test(t)) return true; // starts mid-sentence
110
+ if (/^[>)\]}]/.test(t)) return true; // a quote or a stray bracket
111
+ if (/^(and|but|or|so|then|which|that|where|when)\b/i.test(t)) return true;
112
+ if (/[([{]/.test(t) && !/[)\]}]/.test(t)) return true; // an unclosed bracket
113
+ if (/:$/.test(t)) return true; // a label, not a rule
114
+ const words = t.split(/\s+/).length;
115
+ return words < 4;
116
+ }
117
+
118
+ function push(out, text, source, detail) {
119
+ const t = clean(text);
120
+ if (t.length < 15 || t.length > 300) return;
121
+ if (!RULE_SHAPE.test(t)) return;
122
+ if (NOT_A_RULE.some((re) => re.test(text))) return;
123
+ if (isFragment(t)) return;
124
+ out.push({ text: t, source, detail: detail || "" });
125
+ }
126
+
127
+ /** Read a file, or throw. An unreadable source is never reported as absent. */
128
+ function readOr(p, label) {
129
+ try {
130
+ return fs.readFileSync(p, "utf8");
131
+ } catch (e) {
132
+ throw new Unreadable(`${label} exists but could not be read: ${e.message}`);
133
+ }
134
+ }
135
+
136
+ /** 1. Contracts — already written as rules, with their reasons. */
137
+ function fromContracts(projectDir, found) {
138
+ const dir = path.join(projectDir, ".gsd-t", "contracts");
139
+ if (!fs.existsSync(dir)) return { present: false };
140
+ let names;
141
+ try {
142
+ names = fs.readdirSync(dir).filter((f) => f.endsWith(".md"));
143
+ } catch (e) {
144
+ throw new Unreadable(`${dir} exists but could not be listed: ${e.message}`);
145
+ }
146
+ let n = 0;
147
+ for (const name of names) {
148
+ const text = readOr(path.join(dir, name), name);
149
+ for (const line of text.split("\n")) {
150
+ const before = found.length;
151
+ push(found, line, "contract", name.replace(/\.md$/, ""));
152
+ if (found.length > before) n++;
153
+ }
154
+ }
155
+ return { present: true, count: n, files: names.length };
156
+ }
157
+
158
+ /**
159
+ * A rule announced by its own heading — "## The Inviolable Rule (HC-005) —
160
+ * NEVER scan the whole page". These are the rules that matter most, and reading
161
+ * line-by-line misses them entirely: the heading states the rule and the
162
+ * paragraph beneath explains it.
163
+ */
164
+ const RULE_HEADING = /^#{2,4}\s+(.*(?:inviolable|hard rule|hc-\d+|never|must not|forbidden|non-goal|do not).*)$/i;
165
+
166
+ function fromHeadings(text, found, source, detail) {
167
+ let n = 0;
168
+ const lines = text.split("\n");
169
+ for (let i = 0; i < lines.length; i++) {
170
+ const m = lines[i].match(RULE_HEADING);
171
+ if (!m) continue;
172
+ // The heading is the rule. Strip the section marker and any id in brackets,
173
+ // then keep the part that states the requirement.
174
+ let t = clean(m[1]).replace(/^The Inviolable Rule\s*/i, "").replace(/^\(([^)]+)\)\s*[—-]?\s*/, "");
175
+ const id = (m[1].match(/HC-\d+/i) || [])[0] || "";
176
+ if (t.length < 8) {
177
+ // The heading is only an id — the rule is the first real line beneath it.
178
+ for (let j = i + 1; j < Math.min(i + 6, lines.length); j++) {
179
+ const c = clean(lines[j]);
180
+ if (c.length > 20 && !/^#{1,6}\s/.test(lines[j])) { t = c; break; }
181
+ }
182
+ }
183
+ if (t.length < 15) continue;
184
+ found.push({
185
+ text: t.length > 200 ? t.slice(0, 200) + "…" : t,
186
+ source,
187
+ detail: id || detail || "",
188
+ heading: true,
189
+ });
190
+ n++;
191
+ }
192
+ return n;
193
+ }
194
+
195
+ /** 2. The existing CLAUDE.md, minus anything restating a global rule. */
196
+ function fromClaudeMd(projectDir, found, globalText) {
197
+ const p = path.join(projectDir, "CLAUDE.md");
198
+ if (!fs.existsSync(p)) return { present: false };
199
+ const text = readOr(p, "CLAUDE.md");
200
+
201
+ // Headed rules first — these are the ones that matter.
202
+ let n = fromHeadings(text, found, "existing CLAUDE.md", "");
203
+
204
+ let restated = 0;
205
+ for (const line of text.split("\n")) {
206
+ const t = clean(line);
207
+ if (globalText && t.length > 40 && globalText.includes(t)) { restated++; continue; }
208
+ const before = found.length;
209
+ push(found, line, "existing CLAUDE.md", "");
210
+ if (found.length > before) n++;
211
+ }
212
+ return { present: true, count: n, restatedFromGlobal: restated };
213
+ }
214
+
215
+ /** 3. [RULE] markers in the pseudocode docs — machine-findable by design. */
216
+ function fromPseudocode(projectDir, found) {
217
+ const dir = path.join(projectDir, ".gsd-t", "pseudocode");
218
+ if (!fs.existsSync(dir)) return { present: false };
219
+ let names;
220
+ try {
221
+ names = fs.readdirSync(dir).filter((f) => f.endsWith(".md"));
222
+ } catch (e) {
223
+ throw new Unreadable(`${dir} exists but could not be listed: ${e.message}`);
224
+ }
225
+ let n = 0;
226
+ for (const name of names) {
227
+ const text = readOr(path.join(dir, name), name);
228
+ for (const line of text.split("\n")) {
229
+ if (!/\[RULE\]/.test(line)) continue;
230
+ const t = clean(line.replace(/\[RULE\]\s*/, "").replace(/-/g, " "));
231
+ if (t.length < 10) continue;
232
+ found.push({ text: t, source: "pseudocode", detail: name.replace(/^PseudoCode-|\.md$/g, "") });
233
+ n++;
234
+ }
235
+ }
236
+ return { present: true, count: n };
237
+ }
238
+
239
+ /** 4. A project-local hook is a rule somebody already enforced in code. */
240
+ function fromHooks(projectDir, found) {
241
+ const p = path.join(projectDir, ".claude", "settings.json");
242
+ if (!fs.existsSync(p)) return { present: false };
243
+ const raw = readOr(p, ".claude/settings.json");
244
+ let settings;
245
+ try {
246
+ settings = JSON.parse(raw);
247
+ } catch (e) {
248
+ throw new Unreadable(`.claude/settings.json exists but is not valid JSON: ${e.message}`);
249
+ }
250
+ let n = 0;
251
+ for (const [event, arr] of Object.entries((settings && settings.hooks) || {})) {
252
+ for (const m of arr || []) {
253
+ for (const h of (m.hooks || [])) {
254
+ const cmd = h.command || "";
255
+ const script = (cmd.match(/([\w-]+)\.(?:js|cjs|sh)/) || [])[1];
256
+ if (!script) continue;
257
+ // A project-local guard names the rule it enforces; only its own count.
258
+ if (!cmd.includes(projectDir) && !cmd.includes("./")) continue;
259
+ found.push({
260
+ text: `Enforced by a hook on ${event}: ${script.replace(/-/g, " ")}`,
261
+ source: "project hook",
262
+ detail: script,
263
+ });
264
+ n++;
265
+ }
266
+ }
267
+ }
268
+ return { present: true, count: n };
269
+ }
270
+
271
+ /** 5. Decision-log entries recording a directive from the user. */
272
+ function fromDecisionLog(projectDir, found) {
273
+ const p = path.join(projectDir, ".gsd-t", "progress.md");
274
+ if (!fs.existsSync(p)) return { present: false };
275
+ const text = readOr(p, "progress.md");
276
+ const start = text.indexOf("## Decision Log");
277
+ if (start === -1) return { present: false };
278
+ let n = 0;
279
+ for (const line of text.slice(start).split("\n")) {
280
+ if (!/^-\s+\d{4}-\d{2}-\d{2}/.test(line)) continue;
281
+ if (!/USER DIRECTIVE|hard rule|David'?s (call|ruling|decision)|never again/i.test(line)) continue;
282
+ // Take the sentence that states the rule, not the whole entry.
283
+ const sentences = line.split(/(?<=[.!?])\s+/);
284
+ for (const s of sentences) {
285
+ const before = found.length;
286
+ push(found, s, "decision log", (line.match(/^-\s+(\d{4}-\d{2}-\d{2})/) || [])[1] || "");
287
+ if (found.length > before) { n++; break; }
288
+ }
289
+ }
290
+ return { present: true, count: n };
291
+ }
292
+
293
+ /** 6. Files fixed the same way repeatedly — a rule nobody wrote down. */
294
+ function fromGit(projectDir, found) {
295
+ if (!fs.existsSync(path.join(projectDir, ".git"))) return { present: false };
296
+ let out;
297
+ try {
298
+ out = execFileSync("git", ["-C", projectDir, "log", "-i", "--name-only",
299
+ "--pretty=format:", "--grep=fix", "-n", "300"],
300
+ { encoding: "utf8", timeout: 20000, maxBuffer: 32 * 1024 * 1024 });
301
+ } catch (e) {
302
+ throw new Unreadable(`.git exists but git log failed: ${e.message}`);
303
+ }
304
+ const counts = {};
305
+ for (const f of out.split("\n")) {
306
+ const name = f.trim();
307
+ if (!name) continue;
308
+ counts[name] = (counts[name] || 0) + 1;
309
+ }
310
+ const hot = Object.entries(counts).filter(([, n]) => n >= 5)
311
+ .sort((a, b) => b[1] - a[1]).slice(0, 6);
312
+ for (const [file, fixes] of hot) {
313
+ found.push({
314
+ text: `Changes to ${file} are riskier than they look — fixed ${fixes} times`,
315
+ source: "git",
316
+ detail: `${fixes} fixes`,
317
+ });
318
+ }
319
+ return { present: true, count: hot.length };
320
+ }
321
+
322
+ /**
323
+ * Collapse the same rule stated in several places into one entry, keeping the
324
+ * clearest wording and merging where it came from. Rank by how many DIFFERENT
325
+ * sources agree — repetition is the evidence.
326
+ */
327
+ function merge(found) {
328
+ const byClaim = new Map();
329
+ for (const r of found) {
330
+ const key = claimKey(r.text);
331
+ if (!key) continue;
332
+ if (!byClaim.has(key)) {
333
+ byClaim.set(key, { text: r.text, sources: new Map() });
334
+ }
335
+ const entry = byClaim.get(key);
336
+ // A rule that had its own heading is the authoritative wording — it beats a
337
+ // sentence found mid-paragraph, whatever the lengths.
338
+ if (r.heading && !entry.heading) { entry.text = r.text; entry.heading = true; }
339
+ else if (!entry.heading && r.text.length < entry.text.length && r.text.length > 25) entry.text = r.text;
340
+
341
+ if (!entry.sources.has(r.source)) entry.sources.set(r.source, []);
342
+ const list = entry.sources.get(r.source);
343
+ if (r.detail && !list.includes(r.detail)) list.push(r.detail);
344
+ }
345
+
346
+ return [...byClaim.values()]
347
+ .map((e) => ({
348
+ text: e.text,
349
+ seenIn: [...e.sources.entries()].map(([s, d]) => (d.length ? `${s} (${d.slice(0, 2).join(", ")})` : s)),
350
+ sourceCount: e.sources.size,
351
+ heading: !!e.heading,
352
+ // A rule stated as a whole sentence beats a fragment. "Consumers
353
+ // (read-only):" is a table header, not a rule anybody can act on.
354
+ wellFormed: /\s(never|always|must|do not|don't|only|no)\s/i.test(e.text) && !/:$/.test(e.text),
355
+ }))
356
+ .sort((a, b) =>
357
+ (Number(b.heading) - Number(a.heading)) ||
358
+ (Number(b.wellFormed) - Number(a.wellFormed)) ||
359
+ (b.sourceCount - a.sourceCount) ||
360
+ (a.text.length - b.text.length));
361
+ }
362
+
363
+ function parseArgs(argv) {
364
+ const args = { project: process.cwd(), top: DEFAULT_TOP };
365
+ for (let i = 2; i < argv.length; i++) {
366
+ const a = argv[i];
367
+ if (a === "--json") args.json = true;
368
+ else if (a === "--project") args.project = argv[++i];
369
+ else if (a === "--top") args.top = parseInt(argv[++i], 10) || DEFAULT_TOP;
370
+ }
371
+ return args;
372
+ }
373
+
374
+ function main() {
375
+ const args = parseArgs(process.argv);
376
+ const projectDir = path.resolve(args.project);
377
+ const asJson = args.json || !process.stdout.isTTY;
378
+
379
+ let globalText = "";
380
+ const globalPath = path.join(process.env.HOME || "", ".claude", "CLAUDE.md");
381
+ if (fs.existsSync(globalPath)) {
382
+ try { globalText = fs.readFileSync(globalPath, "utf8"); } catch (e) {
383
+ // The global file decides which project rules are mere restatements.
384
+ // Without it every restated rule would be listed as project-specific.
385
+ process.stdout.write(JSON.stringify({
386
+ ok: false, exitCode: EXIT_UNREADABLE,
387
+ halt: `The global CLAUDE.md exists at ${globalPath} but could not be read: ${e.message}. ` +
388
+ `Without it, rules copied from the global file would be listed as if this project invented them.`,
389
+ }, null, 2) + "\n");
390
+ process.exit(EXIT_UNREADABLE);
391
+ }
392
+ }
393
+
394
+ const found = [];
395
+ let sources;
396
+ try {
397
+ sources = {
398
+ contracts: fromContracts(projectDir, found),
399
+ claudeMd: fromClaudeMd(projectDir, found, globalText),
400
+ pseudocode: fromPseudocode(projectDir, found),
401
+ hooks: fromHooks(projectDir, found),
402
+ decisionLog: fromDecisionLog(projectDir, found),
403
+ git: fromGit(projectDir, found),
404
+ };
405
+ } catch (e) {
406
+ if (e instanceof Unreadable) {
407
+ const msg = `${e.message}\n\nThis source exists, so the rules in it belong in the list. ` +
408
+ `Continuing would offer a list that looks complete but isn't.`;
409
+ if (asJson) process.stdout.write(JSON.stringify({ ok: false, exitCode: EXIT_UNREADABLE, halt: msg }, null, 2) + "\n");
410
+ else process.stderr.write(`[gsd-t] ${msg}\n`);
411
+ process.exit(EXIT_UNREADABLE);
412
+ }
413
+ throw e;
414
+ }
415
+
416
+ const rules = merge(found);
417
+ const shown = rules.slice(0, args.top);
418
+ const rest = rules.slice(args.top);
419
+
420
+ const result = {
421
+ ok: rules.length > 0,
422
+ exitCode: rules.length > 0 ? EXIT_OK : EXIT_NONE,
423
+ project: path.basename(projectDir),
424
+ sources: Object.fromEntries(Object.entries(sources).map(([k, v]) =>
425
+ [k, v.present ? `${v.count} found` : "none"])),
426
+ total: rules.length,
427
+ shown: shown.map((r, i) => ({ n: i + 1, ...r })),
428
+ remainder: rest.length,
429
+ remainderRules: rest,
430
+ };
431
+
432
+ if (!rules.length) {
433
+ result.halt = "No rules found anywhere in this project. Do not write a rules section — ask the user.";
434
+ }
435
+
436
+ if (asJson) {
437
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
438
+ } else if (!rules.length) {
439
+ process.stdout.write(`No rules found in ${result.project}.\n`);
440
+ } else {
441
+ process.stdout.write(`\n${shown.length} candidate rules for ${result.project}. `);
442
+ process.stdout.write(`Tick the ones that must NEVER be broken.\n\n`);
443
+ process.stdout.write(` RULE${" ".repeat(62)}SEEN IN\n`);
444
+ shown.forEach((r, i) => {
445
+ const words = r.text.split(" ");
446
+ const lines = [];
447
+ let cur = "";
448
+ for (const w of words) {
449
+ if ((cur + " " + w).trim().length > 60) { lines.push(cur.trim()); cur = w; }
450
+ else cur += " " + w;
451
+ }
452
+ if (cur.trim()) lines.push(cur.trim());
453
+ const seen = r.seenIn.join(" + ");
454
+ process.stdout.write(`${String(i + 1).padStart(2)} [ ] ${lines[0].padEnd(62)}${seen}\n`);
455
+ for (const l of lines.slice(1)) process.stdout.write(` ${l}\n`);
456
+ });
457
+ if (rest.length) {
458
+ process.stdout.write(`\n (${rest.length} more written to .gsd-t/setup-rules-full.md — none dropped)\n`);
459
+ }
460
+ process.stdout.write(`\nTick numbers (e.g. 1,3,4) or 'all':\n`);
461
+ }
462
+ process.exit(result.exitCode);
463
+ }
464
+
465
+ if (require.main === module) main();
466
+
467
+ module.exports = { merge, claimKey, RULE_SHAPE, clean, Unreadable };
package/bin/gsd-t.js CHANGED
@@ -1709,6 +1709,9 @@ const GLOBAL_BIN_TOOLS = [
1709
1709
  // M108 — Install self-check, run by the SessionStart hook and by
1710
1710
  // `gsd-t install-check`.
1711
1711
  "gsd-t-install-check.cjs",
1712
+ // M109 — project-history reader + rule miner, used by /gsd-t-setup to write a
1713
+ // project CLAUDE.md from what actually happened rather than from a template.
1714
+ "gsd-t-project-history.cjs", "gsd-t-rule-mine.cjs",
1712
1715
  ];
1713
1716
 
1714
1717
  function installGlobalBinTools() {
@@ -3150,6 +3153,9 @@ const PROJECT_BIN_TOOLS = [
3150
3153
  // M108 — Install self-check. Every project carries its own copy so it can
3151
3154
  // verify and repair itself even when the global install is what broke.
3152
3155
  "gsd-t-install-check.cjs",
3156
+ // M109 — project-history reader + rule miner, so /gsd-t-setup can write a
3157
+ // project CLAUDE.md from what actually happened rather than from a template.
3158
+ "gsd-t-project-history.cjs", "gsd-t-rule-mine.cjs",
3153
3159
  // M89 — Auto-research gate classifier (classify a guessed claim as internal vs external;
3154
3160
  // propagated to each registered project's bin/ so the workflow runCli fallback resolves
3155
3161
  // downstream — per [[project_global_bin_propagation_gap]]).
@@ -72,109 +72,133 @@ CLAUDE.md Analysis:
72
72
 
73
73
  Note: "No existing CLAUDE.md — will generate from scratch."
74
74
 
75
- ## Step 4: Ask Targeted Questions
75
+ ## Step 3.5: Read what actually happened here
76
76
 
77
- Only ask what could NOT be auto-detected. Skip questions where the answer is already clear from scanning.
77
+ Before writing anything, read the project's own history. A rule that keeps
78
+ getting broken is the rule that most needs writing down, and nobody remembers
79
+ those on request — they have to be found.
78
80
 
79
- **Potential questions** (ask only if not auto-detected):
81
+ ```bash
82
+ node bin/gsd-t-project-history.cjs --project . --json
83
+ ```
80
84
 
81
- 1. **Branch Guard**: "Which branch should commits target?" (skip if only `main` or `master` exists)
82
- 2. **Autonomy Level**: "What autonomy level? Level 1 (Supervised), Level 2 (Standard), Level 3 (Full Auto — default)" (skip if existing CLAUDE.md already declares it)
83
- 3. **Workflow Preferences**: "Any overrides to the global defaults? (Research Policy, Phase Flow)" (skip if user has no overrides)
84
- 4. **Deployed URLs**: "Production, staging, and local URLs?" (skip if found in .env or existing docs)
85
- 5. **Project-specific rules**: "Any 'never do' rules specific to this project?" (skip if existing CLAUDE.md already has them)
85
+ Three sources, cheapest first. Each reports whether it was there; a missing one
86
+ is **named**, never skipped in silence:
86
87
 
87
- **Do NOT ask about**:
88
- - Tech stack (auto-detected)
89
- - Naming conventions (auto-detected)
90
- - Testing framework (auto-detected)
91
- - File structure (auto-detected)
92
- - Anything already covered by the global CLAUDE.md
88
+ | Source | What it gives |
89
+ |---|---|
90
+ | Git | Files fixed the same way repeatedly, and real reverts |
91
+ | Decision log | Directives already recorded, with dates |
92
+ | Past sessions | What the user typed when something kept going wrong |
93
93
 
94
- ## Step 5: Generate CLAUDE.md
94
+ Session history is large — one project holds 315 MB — so the tool funnels it:
95
+ keep only the user's own turns, drop pasted logs, keep only complaint-shaped
96
+ lines. Under a second, and the result fits in context.
95
97
 
96
- Build the file using this structure. Include only sections that have real content omit empty sections entirely.
98
+ **If it exits 4, there is no history at all.** Do not carry on and write a
99
+ thinner file. Stop and ask:
97
100
 
98
- ```markdown
99
- # {Project Name}
101
+ ```
102
+ Sources found: git 67 commits | decision log 1 line | sessions 0 | contracts 0
103
+
104
+ I can mine 0 rules from this project's history.
105
+
106
+ a) Write stack and commands only — the rules section left out, with the
107
+ reason written into the file. Not a normal CLAUDE.md.
108
+ b) You dictate the rules now.
109
+ c) Stop.
110
+ ```
111
+
112
+ Whichever they pick, the gap goes **into the generated file** — "Rules: none
113
+ derivable, no project history" — so thinness stays visible rather than reading
114
+ as "this project has no rules."
100
115
 
101
- ## Branch Guard
102
- **Expected branch**: {branch}
116
+ ## Step 4: Show the rules found, and let the user tick them
103
117
 
104
- ## Project Overview
105
- {Brief description what problem does this solve and for whom?}
118
+ Do **not** ask the user to recall rules. Mine every rule the project already
119
+ states, and let them confirm:
106
120
 
107
- ### Architecture
108
- {High-level architecture summary if the project has one — e.g., "Three-tier WebSocket bridge" with a diagram. Keep it concise. Details belong in docs/architecture.md}
121
+ ```bash
122
+ node bin/gsd-t-rule-mine.cjs --project . --top 12
123
+ ```
124
+
125
+ Six sources, ranked by how many agree — a rule found in three places ranks top,
126
+ because repetition is the evidence. The output is a tick-list:
127
+
128
+ ```
129
+ 12 candidate rules for binvoice. Tick the ones that must NEVER be broken.
109
130
 
110
- ## Where Things Live
131
+ RULE SEEN IN
132
+ 1 [ ] NEVER contact the buyer directly CLAUDE.md (HC-003)
133
+ 2 [ ] TEXT-FIRST capture (enforced by a pre-write gate) CLAUDE.md (HC-004)
134
+ 3 [ ] NEVER scan the whole page CLAUDE.md (HC-005)
111
135
 
112
- | Need to find... | Look here |
113
- |-----------------|-----------|
114
- | {component} | {path} |
136
+ Tick numbers (e.g. 1,3,4) or 'all':
137
+ ```
115
138
 
116
- ## Key Technologies
117
- {Bulleted list of language, framework, database, testing, etc.}
139
+ The right-hand column is what makes this fast — the user is confirming
140
+ something they already said, not recalling it. **Cap the screen at 12.**
141
+ Anything below goes to `.gsd-t/setup-rules-full.md`; nothing is dropped.
118
142
 
119
- ## Documentation
120
- - Requirements: docs/requirements.md
121
- - Architecture: docs/architecture.md
122
- - Workflows: docs/workflows.md
123
- - Infrastructure: docs/infrastructure.md
143
+ **Never invent a rule.** Every one comes from somewhere in the project, and the
144
+ tick-list shows where. A rule the user did not tick is not inviolable — it may
145
+ still belong elsewhere in the file, but not in that section.
124
146
 
125
- ## Autonomy Level
126
- **Level {N} — {Name}** ({description}) <!-- default: Level 3 — Full Auto -->
147
+ **Determine these by reading, never by asking:**
127
148
 
128
- ## Workflow Preferences
129
- <!-- Override global defaults. Delete what you don't need to override. -->
149
+ | | How |
150
+ |---|---|
151
+ | Which branch | `git branch --show-current` |
152
+ | Where the repo misleads | Two version fields disagreeing, a legacy path that still looks live |
153
+ | Which files are dangerous | Fixed 5+ times in the git history |
154
+ | Stack, build, test | The manifest |
130
155
 
131
- ### Research Policy
132
- {project-specific overrides, or omit section}
156
+ **Ask only if genuinely unresolvable:** the autonomy level when no existing
157
+ CLAUDE.md declares one, and deployed URLs when nothing records them.
133
158
 
134
- ### Phase Flow
135
- {project-specific overrides, or omit section}
159
+ ## Step 5: Generate CLAUDE.md
136
160
 
137
- ## Testing
138
- {Framework, file organization, naming, running instructions}
161
+ Use the mold at `templates/CLAUDE-project.md`. It is a real mold — do NOT copy
162
+ any existing project's file, and never GSD-T's own.
139
163
 
140
- ## Code Patterns to Follow
141
- {Project-specific conventions that differ from or extend global defaults}
164
+ **Every section may be left out, with the reason stated. None may be padded.**
142
165
 
143
- ### Naming Conventions
144
- {If different from global defaults}
166
+ | Section | Include when | Fill from |
167
+ |---|---|---|
168
+ | Title + one-line what-this-is | Always | The manifest, the README |
169
+ | **Rules that can never be broken** | The user ticked at least one | Step 4's tick-list, verbatim |
170
+ | Where the repo misleads you | Reading the code gives a confident wrong answer | Determined, never asked |
171
+ | Files riskier than they look | Some file was fixed 5+ times | Git history |
172
+ | Where this differs from global | A genuine difference exists | Compared against `~/.claude/CLAUDE.md` |
173
+ | Stack and commands | Always | The manifest |
174
+ | Where things are written down | Always | Fixed pointers |
145
175
 
146
- ## Running the App
147
- {Dev server, build commands, first-time setup}
176
+ **Target 40–70 lines.** If it is longer, something in it is either restating a
177
+ global rule or repeating what the repo already says.
148
178
 
149
- ## Environment Variables
150
- | Variable | Purpose | Default |
151
- |----------|---------|---------|
152
- | {VAR} | {purpose} | {default} |
179
+ ### What must never appear
153
180
 
154
- ## Deployed URLs
155
- - **Production**: {url}
156
- - **Staging**: {url}
157
- - **Local**: http://localhost:{port}
181
+ | Never | Why |
182
+ |---|---|
183
+ | Anything the global file already says | Three projects each paste the entire Destructive Action Guard verbatim — 15 wasted lines apiece |
184
+ | Anything a hook or gate enforces | The machine does not read this file |
185
+ | A version number, a line count, "currently in progress" | Wrong within a week. It belongs in `progress.md` |
186
+ | A file listing, a dependency list, a command count | The repo answers it, and answers it currently |
187
+ | A rule nobody stated | Every rule comes from the tick-list, and the tick-list shows its source |
158
188
 
159
- ## Don't Do These Things
160
- {Project-specific rules only — don't repeat global rules}
189
+ ### The line that matters most
161
190
 
162
- ## GSD-T Workflow
163
- This project uses contract-driven development.
164
- - State: .gsd-t/progress.md
165
- - Contracts: .gsd-t/contracts/
166
- - Domains: .gsd-t/domains/
191
+ A rule states **what must never happen and what breaks if it does**. If it is
192
+ enforced by a hook or a gate, say so — the reader should know a machine is
193
+ watching:
167
194
 
168
- ## Current Status
169
- See `.gsd-t/progress.md` for current milestone/phase state.
195
+ ```
196
+ - **Never touch facebook.com** no permissions, no requests, no DOM writes.
197
+ A single write turns a read-only observer into an actor on someone else's
198
+ account. Enforced by a pre-write gate.
170
199
  ```
171
200
 
172
- ### Section Rules:
173
- - **NEVER duplicate** global CLAUDE.md content (Destructive Action Guard, Pre-Commit Gate, Prime Directives, etc.)
174
- - **ALWAYS include**: Branch Guard, GSD-T Workflow, Current Status
175
- - **Include if relevant**: Where Things Live, Testing, Code Patterns, Environment Variables
176
- - **Omit if empty**: Deployed URLs (if not deployed), Architecture (if trivial), Workflow Preferences (if no overrides)
177
-
201
+ Not: *"Be careful with Facebook interactions."*
178
202
  ## Step 5.5: Quality North Star Configuration
179
203
 
180
204
  After generating the CLAUDE.md content (Step 5) and before presenting it to the user, offer a Quality North Star section if one is not already present.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tekyzinc/gsd-t",
3
- "version": "5.9.10",
3
+ "version": "5.10.10",
4
4
  "description": "GSD-T: Contract-Driven Development for Claude Code — 54 slash commands with headless-by-default workflow spawning, unattended supervisor relay with event stream, graph-powered code analysis, real-time agent dashboard, task telemetry, doc-ripple enforcement, backlog management, impact analysis, test sync, milestone archival, and PRD generation",
5
5
  "author": "Tekyz, Inc.",
6
6
  "license": "MIT",
@@ -1,127 +1,70 @@
1
- # GSD-T Framework (@tekyzinc/gsd-t)
1
+ # {PROJECT_NAME}
2
2
 
3
- Prime Directives, core guards, and workflow rules live in `~/.claude/CLAUDE.md`. This file covers what's specific to this repo.
3
+ {ONE_LINE_WHAT_THIS_IS}
4
4
 
5
- ## Overview
5
+ > Everything general lives in `~/.claude/CLAUDE.md` and applies here. This file
6
+ > holds only what is true of this project and nothing else.
6
7
 
7
- Contract-driven development methodology for Claude Code. npm package providing slash commands, a CLI installer, templates, and stack rules for reliable parallelizable AI-assisted development.
8
+ ## Rules that can never be broken
8
9
 
9
- ## Autonomy Level
10
+ <!--
11
+ Ticked by David from what the project already says. Nothing invented, nothing
12
+ copied from the global file. If a rule is enforced by a hook or a gate, say so
13
+ — the reader should know a machine is watching.
10
14
 
11
- > Overrides global: pins the default from `~/.claude/CLAUDE.md` § Autonomy Levels.
15
+ Leave this section out entirely if none were found, and say why.
16
+ -->
17
+ {RULES}
12
18
 
13
- **Level 3 Full Auto**. Only pause for blockers, destructive actions, or project completion.
19
+ ## Where the repo misleads you
14
20
 
15
- ## Tech Stack
21
+ <!--
22
+ Only where reading the code gives a confident WRONG answer — two version
23
+ fields disagreeing, a legacy path that still looks live, a config that is not
24
+ the one in use. Leave out if none.
25
+ -->
26
+ {MISLEADS}
16
27
 
17
- - **Language**: JavaScript (Node.js >= 16), zero external runtime deps for the installer
18
- - **Distribution**: npm package `@tekyzinc/gsd-t`
19
- - **CLI**: `bin/gsd-t.js` (install, update, init, status, uninstall, doctor, graph, headless, …)
20
- - **Testing**: `npm test` (Node built-in test runner) + manual CLI testing
28
+ ## Files that are riskier than they look
21
29
 
22
- ## Project Structure
30
+ <!--
31
+ Determined by reading, never asked. A file fixed the same way five times is a
32
+ rule nobody wrote down. Say what breaks, not just which file. Leave out if none.
33
+ -->
34
+ {DANGER_MAP}
23
35
 
24
- ```
25
- bin/ — CLI entry (gsd-t.js) + orchestrators (orchestrator.js, design-orchestrator.js)
26
- + support modules (gsd-t-context-brief.cjs, cli-preflight.cjs, gsd-t-verify-gate.cjs, gsd-t-model-tier-policy.cjs, …)
27
- commands/ — slash commands for Claude Code (GSD-T workflow + utility)
28
- templates/ — document + prompt + stack templates
29
- CLAUDE-{global,project}.md, requirements.md, architecture.md, workflows.md,
30
- infrastructure.md, progress.md, backlog.md, backlog-settings.md, design-contract.md
31
- prompts/ — validation subagent protocols (qa, red-team, design-verify)
32
- stacks/ — Stack Rules Engine templates (injected at spawn time)
33
- scripts/ — runtime scripts (design review, context meter hook, event writer)
34
- examples/ — example project structure + settings
35
- docs/methodology.md — GSD → GSD-T evolution and concepts
36
- package.json, README.md, GSD-T-README.md, CHANGELOG.md
37
- ```
36
+ ## Where this differs from the global rules
38
37
 
39
- Exact command list: `ls commands/`. Exact stack rule set: `ls templates/stacks/`. Don't hand-maintain counts in docs.
38
+ <!-- ONLY genuine differences. An empty table means there are none leave it out. -->
40
39
 
41
- ## Meta-Project Notes
40
+ | Global default | Here | Why |
41
+ |---|---|---|
42
+ {OVERRIDES}
42
43
 
43
- - The "source" is the `.md` files in `commands/` + `templates/` and the JS in `bin/` + `scripts/`. There is no `src/`.
44
- - Changes to command files change the methodology itself — treat them as code; verify by running the workflow.
45
- - The `.gsd-t/` state dir coexists with the commands that *define* `.gsd-t/` — intentional.
46
- - `bin/gsd-t.js` is the primary testable surface; command files are validated by use.
44
+ ## Stack and commands
47
45
 
48
- ## Conventions
46
+ {STACK}
49
47
 
50
- **CLI** ANSI colors via escape codes, zero external deps, sync file APIs, version tracked in `package.json` and `~/.claude/.gsd-t-version`.
48
+ | | |
49
+ |---|---|
50
+ | Build | `{BUILD_CMD}` |
51
+ | Test | `{TEST_CMD}` |
52
+ | Branch | `{BRANCH}` |
51
53
 
52
- **Command files** pure markdown, no frontmatter, accept `$ARGUMENTS`, step-numbered, thin Workflow invokers (`Workflow({scriptPath, args})`). Include a Document Ripple section listing files the underlying Workflow expects domain workers to update. Validation protocol bodies stay in `templates/prompts/*-subagent.md`; Workflow scripts load them via `_lib.loadProtocol(name)`. Don't inline the protocol.
54
+ ## Where things are written down
53
55
 
54
- **Templates** `{Project Name}`, `{Date}`, `{description}` replacement tokens; tables for structured data.
56
+ | | |
57
+ |---|---|
58
+ | State and decisions | `.gsd-t/progress.md` |
59
+ | Interfaces between parts | `.gsd-t/contracts/` |
60
+ | How to reach environments | `docs/infrastructure.md` |
55
61
 
56
- **Directory structure** — `.gsd-t/contracts/` (domain interfaces), `.gsd-t/domains/{name}/` (scope/tasks/constraints), `.gsd-t/milestones/` (archives), `.gsd-t/scan/` (analysis outputs).
62
+ <!--
63
+ Written {GENERATED_DATE} from this project's own history.
57
64
 
58
- **Publishing** after `npm publish`, ALWAYS run `/gsd-t-version-update-all` to propagate to registered projects.
65
+ Nothing above should carry a number that changes on its own no version, no
66
+ line count, no "currently in progress". Those belong in progress.md, and a
67
+ file that repeats them is wrong within a week.
59
68
 
60
- ## GSD-T Workflows (M61 — v4.0.10+)
61
-
62
- Phase orchestration lives in `templates/workflows/`. Each command file (`commands/gsd-t-*.md`) is a thin invoker that calls `Workflow({scriptPath, args})`. Canonical scripts:
63
-
64
- - `gsd-t-execute.workflow.js` — preflight → brief → file-disjointness → parallel(domain workers) → integrate → verify-gate
65
- - `gsd-t-verify.workflow.js` — orthogonal triad with M57 CI-parity + M58 test-data purge as FAIL-blocking gates
66
- - `gsd-t-wave.workflow.js`, `-integrate`, `-debug`, `-quick`, `-phase` (generic upper-stage runner)
67
-
68
- Shared helpers: `templates/workflows/_lib.js`. Each helper prefers project-local `bin/<tool>.cjs` and falls back to global `gsd-t` PATH binary.
69
-
70
- The brains stay in `bin/`: `gsd-t-file-disjointness.cjs`, `gsd-t-task-graph.cjs`, `gsd-t-context-brief.cjs`, `cli-preflight.cjs`, `gsd-t-verify-gate.cjs`, `gsd-t-verify-gate-judge.cjs`, `gsd-t-build-coverage.cjs`, `gsd-t-ci-parity.cjs`, `gsd-t-test-data-ledger.cjs`, `journey-coverage.cjs`. Workflows invoke them via `lib.*` helpers.
71
-
72
- ## Validation Protocols (KEPT — methodology layer)
73
-
74
- Three validation protocol bodies stay at `templates/prompts/`:
75
- - `qa-subagent.md` — test mechanics + shallow-test detection + contract compliance
76
- - `red-team-subagent.md` — adversarial / security / boundaries; verdict `FAIL` / `GRUDGING-PASS`
77
- - `design-verify-subagent.md` — visual MATCH/DEVIATION against the design contract
78
-
79
- These are invoked as Workflow `agent()` stages with schema-validated output. The methodology body is unchanged; only the invocation context (Workflow stage vs. Task subagent) updated. Per `.gsd-t/contracts/orthogonal-validation-contract.md` v1.0.0 STABLE.
80
-
81
-
82
- # Destructive Action Guard (MANDATORY)
83
-
84
- **NEVER perform destructive or structural changes without explicit user approval.** This applies at ALL autonomy levels.
85
-
86
- Before any of these actions, STOP and ask the user:
87
- - DROP TABLE, DROP COLUMN, DROP INDEX, TRUNCATE, DELETE without WHERE
88
- - Renaming or removing database tables or columns
89
- - Schema migrations that lose data or break existing queries
90
- - Replacing an existing architecture pattern (e.g., normalized → denormalized)
91
- - Removing or replacing existing files/modules that contain working functionality
92
- - Changing ORM models in ways that conflict with the existing database schema
93
- - Removing API endpoints or changing response shapes that existing clients depend on
94
- - Any change that would require other parts of the system to be rewritten
95
-
96
- **Rule: "Adapt new code to existing structures, not the other way around."**
97
-
98
- ## Pre-Commit Gate (project-specific additions)
99
-
100
- The global gate applies first (see `~/.claude/CLAUDE.md`). Additionally for this repo:
101
-
102
- - **Command file interface/behavior changed** → update `GSD-T-README.md` + `README.md` commands table + `templates/CLAUDE-global.md` + `commands/gsd-t-help.md`.
103
- - **Command added/removed** → update all 4 files above, bump `package.json`, update any command-counting logic in `bin/gsd-t.js`.
104
- - **New command invokes a Workflow** → verify `scriptPath` resolves to `templates/workflows/<name>.workflow.js` and `args` shape matches the script's `meta.phases`.
105
- - **CLI installer changed** → smoke test `install`, `update`, `status`, `doctor`, `init`, `uninstall`.
106
- - **Template changed** → verify `gsd-t-init` still produces correct output.
107
- - **Wave flow changed (phases added/removed/reordered)** → update `gsd-t-wave.md`, `GSD-T-README.md` wave diagram, `README.md` workflow section.
108
- - **Contract or domain boundary changed** → update `.gsd-t/contracts/` and owning `scope.md`.
109
-
110
- ## Don't
111
-
112
- - NEVER add external npm runtime dependencies to the installer — zero-dep invariant.
113
- - NEVER rename a command without updating all 4 reference files above.
114
- - NEVER modify wave phase sequence without updating wave, README, GSD-T-README in the same commit.
115
- - NEVER let installer's command count diverge from `commands/` directory reality.
116
- - NEVER inline validation-subagent protocol bodies into Workflow scripts — `_lib.loadProtocol("qa"|"red-team"|"design-verify")` reads the methodology body from `templates/prompts/`.
117
-
118
- ## Recovery After Interruption
119
-
120
- 1. Read `.gsd-t/progress.md`
121
- 2. Read `README.md` for what the package delivers
122
- 3. Check `commands/` and `package.json` for current state
123
- 4. Continue from current task; don't restart the phase
124
-
125
- ## Current Status
126
-
127
- See `.gsd-t/progress.md`.
69
+ Regenerate with /gsd-t-setup.
70
+ -->