@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 +28 -0
- package/README.md +1 -1
- package/bin/gsd-t-project-history.cjs +374 -0
- package/bin/gsd-t-rule-mine.cjs +467 -0
- package/bin/gsd-t.js +6 -0
- package/commands/gsd-t-setup.md +98 -74
- package/package.json +1 -1
- package/templates/CLAUDE-project.md +50 -107
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.
|
|
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]]).
|
package/commands/gsd-t-setup.md
CHANGED
|
@@ -72,109 +72,133 @@ CLAUDE.md Analysis:
|
|
|
72
72
|
|
|
73
73
|
Note: "No existing CLAUDE.md — will generate from scratch."
|
|
74
74
|
|
|
75
|
-
## Step
|
|
75
|
+
## Step 3.5: Read what actually happened here
|
|
76
76
|
|
|
77
|
-
|
|
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
|
-
|
|
81
|
+
```bash
|
|
82
|
+
node bin/gsd-t-project-history.cjs --project . --json
|
|
83
|
+
```
|
|
80
84
|
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
```
|
|
99
|
-
|
|
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
|
-
##
|
|
102
|
-
**Expected branch**: {branch}
|
|
116
|
+
## Step 4: Show the rules found, and let the user tick them
|
|
103
117
|
|
|
104
|
-
|
|
105
|
-
|
|
118
|
+
Do **not** ask the user to recall rules. Mine every rule the project already
|
|
119
|
+
states, and let them confirm:
|
|
106
120
|
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
| {component} | {path} |
|
|
136
|
+
Tick numbers (e.g. 1,3,4) or 'all':
|
|
137
|
+
```
|
|
115
138
|
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
120
|
-
-
|
|
121
|
-
|
|
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
|
-
|
|
126
|
-
**Level {N} — {Name}** ({description}) <!-- default: Level 3 — Full Auto -->
|
|
147
|
+
**Determine these by reading, never by asking:**
|
|
127
148
|
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
132
|
-
|
|
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
|
-
|
|
135
|
-
{project-specific overrides, or omit section}
|
|
159
|
+
## Step 5: Generate CLAUDE.md
|
|
136
160
|
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
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
|
-
|
|
144
|
-
|
|
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
|
-
|
|
147
|
-
|
|
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
|
-
|
|
150
|
-
| Variable | Purpose | Default |
|
|
151
|
-
|----------|---------|---------|
|
|
152
|
-
| {VAR} | {purpose} | {default} |
|
|
179
|
+
### What must never appear
|
|
153
180
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
160
|
-
{Project-specific rules only — don't repeat global rules}
|
|
189
|
+
### The line that matters most
|
|
161
190
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
169
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
#
|
|
1
|
+
# {PROJECT_NAME}
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
{ONE_LINE_WHAT_THIS_IS}
|
|
4
4
|
|
|
5
|
-
|
|
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
|
-
|
|
8
|
+
## Rules that can never be broken
|
|
8
9
|
|
|
9
|
-
|
|
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
|
-
|
|
15
|
+
Leave this section out entirely if none were found, and say why.
|
|
16
|
+
-->
|
|
17
|
+
{RULES}
|
|
12
18
|
|
|
13
|
-
|
|
19
|
+
## Where the repo misleads you
|
|
14
20
|
|
|
15
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
38
|
+
<!-- ONLY genuine differences. An empty table means there are none — leave it out. -->
|
|
40
39
|
|
|
41
|
-
|
|
40
|
+
| Global default | Here | Why |
|
|
41
|
+
|---|---|---|
|
|
42
|
+
{OVERRIDES}
|
|
42
43
|
|
|
43
|
-
|
|
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
|
-
|
|
46
|
+
{STACK}
|
|
49
47
|
|
|
50
|
-
|
|
48
|
+
| | |
|
|
49
|
+
|---|---|
|
|
50
|
+
| Build | `{BUILD_CMD}` |
|
|
51
|
+
| Test | `{TEST_CMD}` |
|
|
52
|
+
| Branch | `{BRANCH}` |
|
|
51
53
|
|
|
52
|
-
|
|
54
|
+
## Where things are written down
|
|
53
55
|
|
|
54
|
-
|
|
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
|
-
|
|
62
|
+
<!--
|
|
63
|
+
Written {GENERATED_DATE} from this project's own history.
|
|
57
64
|
|
|
58
|
-
|
|
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
|
-
|
|
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
|
+
-->
|