@tekyzinc/gsd-t 5.10.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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # GSD-T: Contract-Driven Development for Claude Code
2
2
 
3
- **v5.10.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();
package/bin/gsd-t.js CHANGED
@@ -3166,6 +3166,8 @@ const PROJECT_BIN_TOOLS = [
3166
3166
  // M90 D2 — Loop ledger (non-convergence detection + halt directive; §3).
3167
3167
  // Propagated so project-local runCli helpers (gsd-t-debug.workflow.js) can invoke it.
3168
3168
  "gsd-t-loop-ledger.cjs",
3169
+ // M110 — CLAUDE.md changelog tracking. Scaffolds and appends to the changelog file.
3170
+ "gsd-t-claude-md-changelog.cjs",
3169
3171
  // Backlog #40 — deterministic archive+sweep of a completed milestone's domain dirs
3170
3172
  // (complete-milestone Step 7). Propagated so complete-milestone can invoke it project-local.
3171
3173
  "gsd-t-archive-domains.cjs",
@@ -367,6 +367,19 @@ The `bin/gsd-t.js init` flow calls `runLoggingScaffoldStep(projectDir)` (from `b
367
367
 
368
368
  See `.gsd-t/contracts/logging-scaffold-seam-contract.md` for the full seam envelope shape consumed by d2 (trace), d4 (audit), and d5 (migrate-logging).
369
369
 
370
+ ## Step 11.6: CLAUDE.md Changelog Scaffold (M110)
371
+
372
+ Scaffold the CLAUDE.md changelog file if it doesn't exist:
373
+
374
+ ```bash
375
+ node bin/gsd-t-claude-md-changelog.cjs scaffold .
376
+ ```
377
+
378
+ This creates `CLAUDE.md updates.md` — an append-only log of CLAUDE.md changes. The file tracks:
379
+ - Rewrites (via `/gsd-t-setup`)
380
+ - Updates (user-directed edits)
381
+ - External changes (modifications outside GSD-T sessions)
382
+
370
383
  ## Step 12: Test Verification
371
384
 
372
385
  After initialization:
@@ -299,6 +299,16 @@ Wait for user confirmation before writing.
299
299
  2. Verify it's valid markdown (no broken tables, unclosed code blocks)
300
300
  3. If `.gsd-t/progress.md` exists, log the setup in the Decision Log
301
301
 
302
+ ## Step 7.5: Update CLAUDE.md Changelog (M110)
303
+
304
+ After writing the project CLAUDE.md, update the changelog:
305
+
306
+ ```bash
307
+ node bin/gsd-t-claude-md-changelog.cjs append . rewritten "Generated via /gsd-t-setup"
308
+ ```
309
+
310
+ If the changelog file doesn't exist, the command scaffolds it first.
311
+
302
312
  ## Document Ripple
303
313
 
304
314
  ### Always update:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tekyzinc/gsd-t",
3
- "version": "5.10.10",
3
+ "version": "5.10.11",
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",
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * gsd-t-changelog-check.js
4
+ *
5
+ * SessionStart hook: checks if CLAUDE.md was modified outside GSD-T since the
6
+ * last tracked changelog entry. If so, appends an "External update detected" entry.
7
+ *
8
+ * Runs once at session start, not on every prompt.
9
+ * Exit codes:
10
+ * 0 — success (up to date, or external update logged)
11
+ * 1 — not a GSD-T project (no .gsd-t/), or no CLI found — silent skip, not an error
12
+ * 2 — changelog CLI failed unexpectedly
13
+ */
14
+
15
+ const { execSync } = require('child_process');
16
+ const path = require('path');
17
+ const fs = require('fs');
18
+
19
+ function findCli() {
20
+ const globalRoot = execSync('npm root -g', { encoding: 'utf8' }).trim();
21
+ const globalCli = path.join(globalRoot, '@tekyzinc', 'gsd-t', 'bin', 'gsd-t-claude-md-changelog.cjs');
22
+ if (fs.existsSync(globalCli)) return globalCli;
23
+
24
+ const localCli = path.join(process.cwd(), 'bin', 'gsd-t-claude-md-changelog.cjs');
25
+ if (fs.existsSync(localCli)) return localCli;
26
+
27
+ return null;
28
+ }
29
+
30
+ function main() {
31
+ const cwd = process.cwd();
32
+
33
+ // Only run in GSD-T projects — silent exit if not
34
+ if (!fs.existsSync(path.join(cwd, '.gsd-t'))) {
35
+ process.exit(0);
36
+ }
37
+
38
+ const cli = findCli();
39
+ if (!cli) {
40
+ // CLI not installed — silent exit (GSD-T may not be fully set up yet)
41
+ process.exit(0);
42
+ }
43
+
44
+ let checkResult;
45
+ try {
46
+ checkResult = execSync(`node "${cli}" check-external "${cwd}"`, { encoding: 'utf8' });
47
+ // Exit code 0 = up to date
48
+ process.exit(0);
49
+ } catch (e) {
50
+ if (e.status === 1) {
51
+ // External update detected — append entry
52
+ execSync(`node "${cli}" append "${cwd}" external`, { encoding: 'utf8' });
53
+ process.stdout.write('CLAUDE.md changelog: external update detected and logged.\n');
54
+ process.exit(0);
55
+ }
56
+ if (e.status === 2) {
57
+ // Corrupt changelog — report but don't block session
58
+ process.stderr.write('CLAUDE.md changelog is corrupt. Run: node bin/gsd-t-claude-md-changelog.cjs get-last-entry-time .\n');
59
+ process.exit(0);
60
+ }
61
+ // Unexpected error — report it
62
+ process.stderr.write('CLAUDE.md changelog check failed: ' + (e.message || 'unknown error') + '\n');
63
+ process.exit(2);
64
+ }
65
+ }
66
+
67
+ main();
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * gsd-t-changelog-post-edit.js
4
+ *
5
+ * PostToolUse hook for Write|Edit: after a successful write to CLAUDE.md,
6
+ * reminds Claude to update the changelog.
7
+ *
8
+ * Receives JSON on stdin with tool_input containing file_path.
9
+ * Outputs reminder text to stdout if the target was CLAUDE.md.
10
+ *
11
+ * Does NOT auto-append — Claude should consciously decide what changed and
12
+ * call the append command with appropriate bullets.
13
+ */
14
+
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+
18
+ let input = '';
19
+ process.stdin.setEncoding('utf8');
20
+ process.stdin.on('data', (chunk) => { input += chunk; });
21
+ process.stdin.on('end', () => {
22
+ let data;
23
+ try {
24
+ data = JSON.parse(input);
25
+ } catch {
26
+ process.exit(0);
27
+ }
28
+
29
+ // Get the file path from tool_input
30
+ const toolInput = data.tool_input;
31
+ if (!toolInput) {
32
+ process.exit(0);
33
+ }
34
+
35
+ let filePath;
36
+ if (typeof toolInput === 'string') {
37
+ try {
38
+ const parsed = JSON.parse(toolInput);
39
+ filePath = parsed.file_path;
40
+ } catch {
41
+ process.exit(0);
42
+ }
43
+ } else if (typeof toolInput === 'object') {
44
+ filePath = toolInput.file_path;
45
+ }
46
+
47
+ if (!filePath) {
48
+ process.exit(0);
49
+ }
50
+
51
+ // Check if it's CLAUDE.md (root or .claude/)
52
+ const basename = path.basename(filePath);
53
+ if (basename !== 'CLAUDE.md') {
54
+ process.exit(0);
55
+ }
56
+
57
+ // Check if it's NOT the global ~/.claude/CLAUDE.md (only track project CLAUDE.md)
58
+ const homeClaudeMd = path.join(process.env.HOME || '', '.claude', 'CLAUDE.md');
59
+ if (path.resolve(filePath) === path.resolve(homeClaudeMd)) {
60
+ process.exit(0);
61
+ }
62
+
63
+ // It's a project CLAUDE.md — remind to update changelog
64
+ const projectDir = data.cwd || process.cwd();
65
+ const changelogPath = path.join(projectDir, 'CLAUDE.md updates.md');
66
+
67
+ process.stdout.write(
68
+ `[M110] You just modified CLAUDE.md. Update the changelog:\n` +
69
+ ` node bin/gsd-t-claude-md-changelog.cjs append "${projectDir}" updated "" '["<describe what changed>"]'\n`
70
+ );
71
+
72
+ process.exit(0);
73
+ });