@tekyzinc/gsd-t 5.9.10 → 5.10.11

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.11** - 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,360 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * gsd-t-claude-md-changelog.cjs
4
+ *
5
+ * M110: CLAUDE.md changelog tracking.
6
+ * Maintains an append-only changelog for CLAUDE.md mutations.
7
+ *
8
+ * Commands:
9
+ * scaffold <projectDir> Create changelog file if missing
10
+ * append <projectDir> <type> [desc] Append an entry (rewritten|updated|external)
11
+ * check-external <projectDir> Detect external updates (mtime > last entry)
12
+ * get-last-entry-time <projectDir> Return ISO timestamp of last entry (for hooks)
13
+ *
14
+ * Entry format:
15
+ * **[Rewritten|Updated|External update detected] {date} {time} | {model} | GSD-T v{gsdtVersion} | project v{projectVersion}**
16
+ * - bullet 1 (for Updated only)
17
+ * - bullet 2
18
+ *
19
+ * Exit codes:
20
+ * 0 — success / up-to-date
21
+ * 1 — external update detected (check-external only)
22
+ * 2 — corrupt changelog (HALT)
23
+ * 64 — bad input
24
+ */
25
+
26
+ const fs = require('fs');
27
+ const path = require('path');
28
+
29
+ const CHANGELOG_FILENAME = 'CLAUDE.md updates.md';
30
+
31
+ // ─────────────────────────────────────────────────────────────────────────────
32
+ // Helpers
33
+ // ─────────────────────────────────────────────────────────────────────────────
34
+
35
+ function getChangelogPath(projectDir) {
36
+ return path.join(projectDir, CHANGELOG_FILENAME);
37
+ }
38
+
39
+ function getClaudeMdPath(projectDir) {
40
+ const rootPath = path.join(projectDir, 'CLAUDE.md');
41
+ const dotClaudePath = path.join(projectDir, '.claude', 'CLAUDE.md');
42
+ if (fs.existsSync(rootPath)) return rootPath;
43
+ if (fs.existsSync(dotClaudePath)) return dotClaudePath;
44
+ return rootPath;
45
+ }
46
+
47
+ function halt(message, exitCode) {
48
+ console.error(JSON.stringify({ error: message }));
49
+ process.exit(exitCode);
50
+ }
51
+
52
+ function getGsdtVersion() {
53
+ const pkgPath = path.join(__dirname, '..', 'package.json');
54
+ if (!fs.existsSync(pkgPath)) {
55
+ halt('GSD-T package.json not found: ' + pkgPath, 64);
56
+ }
57
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
58
+ if (!pkg.version) {
59
+ halt('GSD-T package.json missing version field', 64);
60
+ }
61
+ return pkg.version;
62
+ }
63
+
64
+ function getProjectVersion(projectDir) {
65
+ const pkgPath = path.join(projectDir, 'package.json');
66
+ if (fs.existsSync(pkgPath)) {
67
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
68
+ if (pkg.version) return pkg.version;
69
+ }
70
+ const pyprojectPath = path.join(projectDir, 'pyproject.toml');
71
+ if (fs.existsSync(pyprojectPath)) {
72
+ const content = fs.readFileSync(pyprojectPath, 'utf8');
73
+ const match = content.match(/version\s*=\s*["']([^"']+)["']/);
74
+ if (match) return match[1];
75
+ }
76
+ return 'unversioned';
77
+ }
78
+
79
+ function getClaudeModel() {
80
+ const model = process.env.CLAUDE_MODEL || process.env.MODEL;
81
+ if (!model) return 'Claude';
82
+ if (model.includes('opus-5') || model.includes('opus5')) return 'Claude-Opus-5';
83
+ if (model.includes('opus-4.5') || model.includes('opus4.5')) return 'Claude-Opus-4.5';
84
+ if (model.includes('opus')) return 'Claude-Opus';
85
+ if (model.includes('sonnet-5') || model.includes('sonnet5')) return 'Claude-Sonnet-5';
86
+ if (model.includes('sonnet')) return 'Claude-Sonnet';
87
+ if (model.includes('haiku')) return 'Claude-Haiku';
88
+ return model;
89
+ }
90
+
91
+ function formatDate(date) {
92
+ const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
93
+ 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
94
+ const month = months[date.getMonth()];
95
+ const day = date.getDate();
96
+ const year = date.getFullYear();
97
+ let hours = date.getHours();
98
+ const ampm = hours >= 12 ? 'pm' : 'am';
99
+ hours = hours % 12 || 12;
100
+ const minutes = date.getMinutes().toString().padStart(2, '0');
101
+ return `${month} ${day}, ${year} ${hours}:${minutes}${ampm}`;
102
+ }
103
+
104
+ /**
105
+ * Parse the timestamp of the last entry in the changelog.
106
+ *
107
+ * @returns {Date | null} Date of last entry, or null if no file/entries
108
+ * @throws Calls halt() and exits if changelog is corrupt
109
+ */
110
+ function parseLastEntryTime(changelogPath) {
111
+ if (!fs.existsSync(changelogPath)) {
112
+ return null;
113
+ }
114
+
115
+ const content = fs.readFileSync(changelogPath, 'utf8');
116
+
117
+ const hasHeader = content.includes('# CLAUDE.md Change Log');
118
+ const hasEntries = content.includes('**Rewritten') ||
119
+ content.includes('**Updated') ||
120
+ content.includes('**External update detected');
121
+
122
+ if (hasHeader && !hasEntries) {
123
+ return null;
124
+ }
125
+
126
+ // Match entries with optional ~ before time (e.g., "~3:00pm" or "3:00pm")
127
+ const entryPattern = /\*\*(?:Rewritten|Updated|External update detected)\s+(\w+\s+\d+,\s+\d{4}\s+~?\d{1,2}:\d{2}(?:am|pm))/gi;
128
+
129
+ let lastMatch = null;
130
+ let match;
131
+ while ((match = entryPattern.exec(content)) !== null) {
132
+ lastMatch = match[1];
133
+ }
134
+
135
+ if (!lastMatch) {
136
+ halt('Changelog file is corrupt — has content but no parseable entries: ' + changelogPath, 2);
137
+ }
138
+
139
+ // Remove optional ~ and parse the date
140
+ const parsed = new Date(lastMatch.replace(/~/, '').replace(/(\d{1,2}:\d{2})(am|pm)/i, (_, time, ampm) => {
141
+ const [h, m] = time.split(':');
142
+ let hour = parseInt(h, 10);
143
+ if (ampm.toLowerCase() === 'pm' && hour !== 12) hour += 12;
144
+ if (ampm.toLowerCase() === 'am' && hour === 12) hour = 0;
145
+ return `${hour.toString().padStart(2, '0')}:${m}`;
146
+ }));
147
+
148
+ if (isNaN(parsed.getTime())) {
149
+ halt('Changelog file has unparseable date: ' + changelogPath, 2);
150
+ }
151
+
152
+ return parsed;
153
+ }
154
+
155
+ // ─────────────────────────────────────────────────────────────────────────────
156
+ // Commands
157
+ // ─────────────────────────────────────────────────────────────────────────────
158
+
159
+ function scaffold(projectDir) {
160
+ const changelogPath = getChangelogPath(projectDir);
161
+
162
+ if (fs.existsSync(changelogPath)) {
163
+ console.log(JSON.stringify({ status: 'exists', path: changelogPath }));
164
+ process.exit(0);
165
+ }
166
+
167
+ const content = `# CLAUDE.md Change Log
168
+
169
+ Append-only record of CLAUDE.md rewrites and updates.
170
+
171
+ ---
172
+
173
+ `;
174
+
175
+ fs.writeFileSync(changelogPath, content, 'utf8');
176
+ console.log(JSON.stringify({ status: 'created', path: changelogPath }));
177
+ process.exit(0);
178
+ }
179
+
180
+ function append(projectDir, type, description, bullets) {
181
+ const changelogPath = getChangelogPath(projectDir);
182
+
183
+ if (!fs.existsSync(changelogPath)) {
184
+ const content = `# CLAUDE.md Change Log
185
+
186
+ Append-only record of CLAUDE.md rewrites and updates.
187
+
188
+ ---
189
+
190
+ `;
191
+ fs.writeFileSync(changelogPath, content, 'utf8');
192
+ }
193
+
194
+ const now = new Date();
195
+ const dateStr = formatDate(now);
196
+ const model = getClaudeModel();
197
+ const gsdtVersion = getGsdtVersion();
198
+ const projectVersion = getProjectVersion(projectDir);
199
+
200
+ let typeLabel;
201
+ switch (type.toLowerCase()) {
202
+ case 'rewritten':
203
+ case 'rewrite':
204
+ typeLabel = 'Rewritten';
205
+ break;
206
+ case 'updated':
207
+ case 'update':
208
+ typeLabel = 'Updated';
209
+ break;
210
+ case 'external':
211
+ typeLabel = 'External update detected';
212
+ break;
213
+ default:
214
+ halt('Unknown type: ' + type + '. Use: rewritten, updated, external', 64);
215
+ }
216
+
217
+ let entry = `\n**${typeLabel} ${dateStr} | ${model} | GSD-T v${gsdtVersion} | project v${projectVersion}**\n`;
218
+
219
+ if (description && typeLabel === 'Rewritten') {
220
+ entry += `\n${description}\n`;
221
+ }
222
+
223
+ if (bullets && bullets.length > 0 && typeLabel === 'Updated') {
224
+ entry += '\n';
225
+ for (const bullet of bullets) {
226
+ entry += `- ${bullet}\n`;
227
+ }
228
+ }
229
+
230
+ fs.appendFileSync(changelogPath, entry, 'utf8');
231
+ console.log(JSON.stringify({
232
+ status: 'appended',
233
+ type: typeLabel,
234
+ path: changelogPath,
235
+ timestamp: now.toISOString()
236
+ }));
237
+ process.exit(0);
238
+ }
239
+
240
+ /**
241
+ * check-external: Detects if CLAUDE.md was modified outside GSD-T.
242
+ *
243
+ * Exit codes:
244
+ * 0 — up to date, no action needed
245
+ * 1 — external update detected, caller should append entry
246
+ * 2 — changelog is corrupt (HALT)
247
+ */
248
+ function checkExternal(projectDir) {
249
+ const changelogPath = getChangelogPath(projectDir);
250
+ const claudeMdPath = getClaudeMdPath(projectDir);
251
+
252
+ if (!fs.existsSync(claudeMdPath)) {
253
+ console.log(JSON.stringify({ status: 'no-claude-md', path: claudeMdPath }));
254
+ process.exit(0);
255
+ }
256
+
257
+ const claudeMdStat = fs.statSync(claudeMdPath);
258
+ const claudeMdMtime = claudeMdStat.mtime;
259
+
260
+ const lastEntryTime = parseLastEntryTime(changelogPath);
261
+
262
+ if (lastEntryTime === null) {
263
+ console.log(JSON.stringify({
264
+ status: 'external-detected',
265
+ reason: 'no-prior-entries',
266
+ claudeMdMtime: claudeMdMtime.toISOString()
267
+ }));
268
+ process.exit(1);
269
+ }
270
+
271
+ const bufferMs = 60 * 1000;
272
+
273
+ if (claudeMdMtime.getTime() > lastEntryTime.getTime() + bufferMs) {
274
+ console.log(JSON.stringify({
275
+ status: 'external-detected',
276
+ reason: 'mtime-newer',
277
+ claudeMdMtime: claudeMdMtime.toISOString(),
278
+ lastEntryTime: lastEntryTime.toISOString()
279
+ }));
280
+ process.exit(1);
281
+ }
282
+
283
+ console.log(JSON.stringify({
284
+ status: 'up-to-date',
285
+ claudeMdMtime: claudeMdMtime.toISOString(),
286
+ lastEntryTime: lastEntryTime.toISOString()
287
+ }));
288
+ process.exit(0);
289
+ }
290
+
291
+ function getLastEntryTime(projectDir) {
292
+ const changelogPath = getChangelogPath(projectDir);
293
+ const result = parseLastEntryTime(changelogPath);
294
+
295
+ if (result === null) {
296
+ console.log(JSON.stringify({ status: 'none' }));
297
+ process.exit(0);
298
+ }
299
+
300
+ console.log(JSON.stringify({
301
+ status: 'found',
302
+ timestamp: result.toISOString()
303
+ }));
304
+ process.exit(0);
305
+ }
306
+
307
+ // ─────────────────────────────────────────────────────────────────────────────
308
+ // CLI
309
+ // ─────────────────────────────────────────────────────────────────────────────
310
+
311
+ function main() {
312
+ const args = process.argv.slice(2);
313
+ const command = args[0];
314
+
315
+ if (!command) {
316
+ console.error('Usage: gsd-t-claude-md-changelog <command> [args]');
317
+ console.error('Commands: scaffold, append, check-external, get-last-entry-time');
318
+ process.exit(64);
319
+ }
320
+
321
+ switch (command) {
322
+ case 'scaffold': {
323
+ const projectDir = args[1] || process.cwd();
324
+ scaffold(projectDir);
325
+ break;
326
+ }
327
+ case 'append': {
328
+ const projectDir = args[1] || process.cwd();
329
+ const type = args[2];
330
+ const description = args[3];
331
+ let bullets = [];
332
+ if (args[4]) {
333
+ const parsed = JSON.parse(args[4]);
334
+ if (!Array.isArray(parsed)) {
335
+ halt('Bullets must be a JSON array', 64);
336
+ }
337
+ bullets = parsed;
338
+ }
339
+ if (!type) {
340
+ halt('Usage: gsd-t-claude-md-changelog append <projectDir> <type> [description] [bulletsJson]', 64);
341
+ }
342
+ append(projectDir, type, description, bullets);
343
+ break;
344
+ }
345
+ case 'check-external': {
346
+ const projectDir = args[1] || process.cwd();
347
+ checkExternal(projectDir);
348
+ break;
349
+ }
350
+ case 'get-last-entry-time': {
351
+ const projectDir = args[1] || process.cwd();
352
+ getLastEntryTime(projectDir);
353
+ break;
354
+ }
355
+ default:
356
+ halt('Unknown command: ' + command, 64);
357
+ }
358
+ }
359
+
360
+ main();