@tekyzinc/gsd-t 3.19.0 → 3.20.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 +45 -0
- package/package.json +1 -1
- package/scripts/gsd-t-auto-route.js +29 -6
- package/scripts/gsd-t-date-guard.js +249 -0
- package/scripts/gsd-t-update-check.js +1 -1
- package/templates/CLAUDE-global.md +43 -11
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,51 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to GSD-T are documented here. Updated with each release.
|
|
4
4
|
|
|
5
|
+
## [3.20.10] - 2026-05-03
|
|
6
|
+
|
|
7
|
+
### Added — Live-clock dated banner + PreToolUse date guard
|
|
8
|
+
|
|
9
|
+
Solves the multi-day-session date-drift problem. Long sessions span days; without a fresh time signal every turn, hand-written timestamps (decision log entries, `continue-here-{ts}.md` filenames, memory entries, banners) silently drift to the session-start date.
|
|
10
|
+
|
|
11
|
+
**Hook — `scripts/gsd-t-auto-route.js` (UserPromptSubmit):**
|
|
12
|
+
- Now emits `[GSD-T NOW] Day: Mon DD, YYYY HH:MM:SS TZ` at the start of every user turn (every project, regardless of GSD-T detection).
|
|
13
|
+
- Existing `[GSD-T AUTO-ROUTE]` behavior preserved (still GSD-T-project-only, plain-text-prompts-only).
|
|
14
|
+
- Exports `liveTimestamp()` for test reuse.
|
|
15
|
+
|
|
16
|
+
**Hook — `scripts/gsd-t-date-guard.js` (PreToolUse on Write|Edit, NEW):**
|
|
17
|
+
- Blocks Write/Edit calls whose content contains timestamps drifting more than ±5 minutes from the live system clock.
|
|
18
|
+
- High-signal patterns validated: decision-log entries (`- YYYY-MM-DD HH:MM:`), `continue-here-YYYY-MM-DDTHHMMSS` filenames, banners (`Day: Mon DD, YYYY HH:MM`), labeled stamps (`Date:`, `Updated:`, `Created:`, etc.).
|
|
19
|
+
- For Edit: timestamps appearing in BOTH `old_string` and `new_string` are pre-existing context — never flagged.
|
|
20
|
+
- Allowlist: machine-written and historical-frozen paths (`.gsd-t/events/`, `.gsd-t/transcripts/`, `.gsd-t/metrics/`, `.gsd-t/.unattended/`, `.gsd-t/headless-*.log`, `.gsd-t/dashboard.log`, `.gsd-t/progress-archive/`, `.gsd-t/milestones/`, `.gsd-t/scan/`, `.git/`, `node_modules/`, `CHANGELOG.md`, `.gsd-t/token-log.md`, `.gsd-t/qa-issues.md`, existing `continue-here-*.md`).
|
|
21
|
+
- Fails open on internal error — broken tool calls are worse than drift.
|
|
22
|
+
- 10/10 smoke tests pass.
|
|
23
|
+
|
|
24
|
+
**Banner change — `scripts/gsd-t-update-check.js`:**
|
|
25
|
+
- CURRENT-state banner no longer ships the changelog URL — pure noise when there's no update to read about. Action-required banners (AUTO-UPDATE, UPDATE) keep the link.
|
|
26
|
+
|
|
27
|
+
**Spec — `~/.claude/CLAUDE.md` + `templates/CLAUDE-global.md` (rewritten §Update Notices + new §Live Clock Rule):**
|
|
28
|
+
- Dated banner mandated as the first line of EVERY response, sourced from `[GSD-T NOW]` only — never `currentDate` (frozen) or SessionStart (frozen).
|
|
29
|
+
- Live Clock Rule: any timestamp written to disk MUST come from live system clock. Date guard mechanically enforces this.
|
|
30
|
+
- Format tightened to `Day: Mon DD, YYYY HH:MM TZ` (HH:MM displayed; seconds emitted but trimmed).
|
|
31
|
+
|
|
32
|
+
### Why
|
|
33
|
+
|
|
34
|
+
Hand-written timestamps were being sourced from `currentDate` in Claude's context — a string injected once at session start. Multi-day sessions (the user reported they happen often) meant decision-log entries, archive filenames, and memory entries were silently dated wrong by N days.
|
|
35
|
+
|
|
36
|
+
Red Team principle applied: directives in CLAUDE.md are not safety properties. The PreToolUse hook is the enforcement.
|
|
37
|
+
|
|
38
|
+
### Settings install
|
|
39
|
+
|
|
40
|
+
Add to `~/.claude/settings.json` (in the `hooks` block, before `PostToolUse`):
|
|
41
|
+
|
|
42
|
+
```json
|
|
43
|
+
"PreToolUse": [
|
|
44
|
+
{ "matcher": "Write|Edit",
|
|
45
|
+
"hooks": [{ "type": "command",
|
|
46
|
+
"command": "node \"$HOME/.claude/scripts/gsd-t-date-guard.js\"" }] }
|
|
47
|
+
]
|
|
48
|
+
```
|
|
49
|
+
|
|
5
50
|
## [3.19.00] - 2026-04-23
|
|
6
51
|
|
|
7
52
|
### Added — M46 Milestone: Unattended Iter-Parallel + Worker Fan-Out Completion
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tekyzinc/gsd-t",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.20.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,26 +1,47 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* GSD-T UserPromptSubmit hook — auto-routes plain text prompts
|
|
3
|
+
* GSD-T UserPromptSubmit hook — emits live timestamp + auto-routes plain text prompts.
|
|
4
4
|
*
|
|
5
5
|
* Receives JSON on stdin: { "prompt": "...", "cwd": "...", "session_id": "..." }
|
|
6
6
|
* Outputs to stdout: injected as system context before Claude processes the prompt.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
* -
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
* Always emits (every turn, every project):
|
|
9
|
+
* - [GSD-T NOW] {Day: Mon DD, YYYY HH:MM:SS TZ} — live system clock for the
|
|
10
|
+
* dated banner at the top of Claude's response. Fresh per turn so multi-day
|
|
11
|
+
* sessions and date-rollovers are reflected accurately.
|
|
12
|
+
*
|
|
13
|
+
* Conditionally emits (GSD-T projects only, plain text prompts only):
|
|
14
|
+
* - [GSD-T AUTO-ROUTE] signal so Claude routes via /gsd
|
|
12
15
|
*/
|
|
13
16
|
|
|
14
17
|
const fs = require("fs");
|
|
15
18
|
const path = require("path");
|
|
16
19
|
|
|
20
|
+
function liveTimestamp(now = new Date()) {
|
|
21
|
+
const day = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][now.getDay()];
|
|
22
|
+
const mon = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
|
|
23
|
+
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"][now.getMonth()];
|
|
24
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
25
|
+
const date = `${day}: ${mon} ${now.getDate()}, ${now.getFullYear()}`;
|
|
26
|
+
const time = `${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`;
|
|
27
|
+
// Pull the local timezone abbreviation (e.g. "PDT", "EST") from toString().
|
|
28
|
+
const tzMatch = now.toString().match(/\(([^)]+)\)$/);
|
|
29
|
+
const tzShort = tzMatch
|
|
30
|
+
? tzMatch[1].split(" ").map((w) => w[0]).join("") // "Pacific Daylight Time" → "PDT"
|
|
31
|
+
: "";
|
|
32
|
+
return `${date} ${time}${tzShort ? " " + tzShort : ""}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
17
35
|
let input = "";
|
|
18
36
|
process.stdin.setEncoding("utf8");
|
|
19
37
|
process.stdin.on("data", (chunk) => { input += chunk; });
|
|
20
38
|
process.stdin.on("end", () => {
|
|
39
|
+
// Always emit live timestamp first — every turn, every project.
|
|
40
|
+
process.stdout.write(`[GSD-T NOW] ${liveTimestamp()}\n`);
|
|
41
|
+
|
|
21
42
|
try {
|
|
22
43
|
const data = JSON.parse(input);
|
|
23
|
-
//
|
|
44
|
+
// Auto-route is GSD-T-project-only.
|
|
24
45
|
const cwd = typeof data.cwd === "string" ? data.cwd : process.cwd();
|
|
25
46
|
if (!fs.existsSync(path.join(cwd, ".gsd-t", "progress.md"))) process.exit(0);
|
|
26
47
|
const prompt = (typeof data.prompt === "string" ? data.prompt : "").trimStart();
|
|
@@ -37,3 +58,5 @@ process.stdin.on("end", () => {
|
|
|
37
58
|
}
|
|
38
59
|
process.exit(0);
|
|
39
60
|
});
|
|
61
|
+
|
|
62
|
+
module.exports = { liveTimestamp };
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* GSD-T PreToolUse hook — blocks Write/Edit calls that contain stale or invented timestamps.
|
|
4
|
+
*
|
|
5
|
+
* Receives JSON on stdin from Claude Code:
|
|
6
|
+
* {
|
|
7
|
+
* "tool_name": "Write" | "Edit",
|
|
8
|
+
* "tool_input": { "file_path": "...", "content"|"new_string"|"old_string": "..." },
|
|
9
|
+
* "cwd": "...",
|
|
10
|
+
* "session_id": "..."
|
|
11
|
+
* }
|
|
12
|
+
*
|
|
13
|
+
* Behavior:
|
|
14
|
+
* - Pulls system clock at hook execution time (independent of Claude's context).
|
|
15
|
+
* - Scans the content being written for date-like patterns (decision-log entries,
|
|
16
|
+
* filename timestamps, ISO date strings, "Day: Mon DD, YYYY" banners).
|
|
17
|
+
* - For Edit: only validates timestamps in `new_string` that are NOT also in `old_string`
|
|
18
|
+
* (so unchanged surrounding context never trips the guard).
|
|
19
|
+
* - For Write: validates timestamps that match high-signal "freshly stamped" patterns;
|
|
20
|
+
* ignores generic ISO matches in prose.
|
|
21
|
+
* - If any flagged timestamp is outside ±DRIFT_MINUTES of system clock, exit 2 with a
|
|
22
|
+
* structured error that surfaces back to Claude as a tool error.
|
|
23
|
+
*
|
|
24
|
+
* Exit codes:
|
|
25
|
+
* 0 — content passes (no suspicious timestamps, or all within window)
|
|
26
|
+
* 2 — block: stale/invented timestamp detected
|
|
27
|
+
*
|
|
28
|
+
* Allowlisted paths bypass entirely (machine-written files, transcripts, git internals).
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
"use strict";
|
|
32
|
+
|
|
33
|
+
const fs = require("fs");
|
|
34
|
+
const path = require("path");
|
|
35
|
+
|
|
36
|
+
const DRIFT_MINUTES = 5;
|
|
37
|
+
const DRIFT_MS = DRIFT_MINUTES * 60 * 1000;
|
|
38
|
+
|
|
39
|
+
// Paths where dates are machine-written or historically frozen — never validate.
|
|
40
|
+
const ALLOWLIST_PATTERNS = [
|
|
41
|
+
/\/\.git\//,
|
|
42
|
+
/\/node_modules\//,
|
|
43
|
+
/\.gsd-t\/events\/[\d-]+\.jsonl$/,
|
|
44
|
+
/\.gsd-t\/transcripts\//,
|
|
45
|
+
/\.gsd-t\/metrics\//,
|
|
46
|
+
/\.gsd-t\/\.unattended\//,
|
|
47
|
+
/\.gsd-t\/headless-\d+\.log$/,
|
|
48
|
+
/\.gsd-t\/dashboard\.log$/,
|
|
49
|
+
/\.gsd-t\/progress-archive\//,
|
|
50
|
+
/\.gsd-t\/milestones\//,
|
|
51
|
+
/\.gsd-t\/scan\//,
|
|
52
|
+
/CHANGELOG\.md$/,
|
|
53
|
+
/\.gsd-t\/token-log\.md$/,
|
|
54
|
+
/\.gsd-t\/qa-issues\.md$/,
|
|
55
|
+
/\.gsd-t\/continue-here-[\d-]+T[\d]+\.md$/, // Existing files; new ones validated by filename rule
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
// High-signal patterns — these are timestamps Claude is producing right now,
|
|
59
|
+
// not historical references inside prose.
|
|
60
|
+
const FRESH_STAMP_PATTERNS = [
|
|
61
|
+
// Decision log entries: "- 2026-05-03 12:35: did the thing"
|
|
62
|
+
{
|
|
63
|
+
name: "decision-log",
|
|
64
|
+
regex: /^- (\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):/gm,
|
|
65
|
+
extract: (m) => new Date(
|
|
66
|
+
Number(m[1]), Number(m[2]) - 1, Number(m[3]),
|
|
67
|
+
Number(m[4]), Number(m[5]), 0
|
|
68
|
+
),
|
|
69
|
+
},
|
|
70
|
+
// continue-here filenames: "continue-here-2026-05-03T123500.md"
|
|
71
|
+
{
|
|
72
|
+
name: "continue-here-filename",
|
|
73
|
+
regex: /continue-here-(\d{4})-(\d{2})-(\d{2})T(\d{2})(\d{2})(\d{2})/g,
|
|
74
|
+
extract: (m) => new Date(
|
|
75
|
+
Number(m[1]), Number(m[2]) - 1, Number(m[3]),
|
|
76
|
+
Number(m[4]), Number(m[5]), Number(m[6])
|
|
77
|
+
),
|
|
78
|
+
},
|
|
79
|
+
// Session-banner format: "Sun: May 3, 2026 12:35 PDT"
|
|
80
|
+
{
|
|
81
|
+
name: "banner",
|
|
82
|
+
regex: /(Sun|Mon|Tue|Wed|Thu|Fri|Sat): (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) (\d{1,2}), (\d{4}) (\d{2}):(\d{2})/g,
|
|
83
|
+
extract: (m) => {
|
|
84
|
+
const monIdx = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
|
|
85
|
+
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"].indexOf(m[2]);
|
|
86
|
+
return new Date(Number(m[4]), monIdx, Number(m[3]), Number(m[5]), Number(m[6]), 0);
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
// Generic ISO date — only validated when surrounded by strong "freshly stamping now"
|
|
92
|
+
// context (e.g., right after labels like "Date:", "Today:", "Stamped:", etc.).
|
|
93
|
+
// Two arms: (a) date+time (validated against full ±DRIFT_MINUTES window),
|
|
94
|
+
// (b) date-only (validated as same-calendar-day-as-now, time-of-day ignored).
|
|
95
|
+
const STAMPED_ISO_PATTERN = {
|
|
96
|
+
name: "stamped-iso",
|
|
97
|
+
regex: /\b(?:Date|Today|Stamped|Updated|Created|Generated|Now|Timestamp|At)\s*[:=]\s*(\d{4})-(\d{2})-(\d{2})(?:[T ](\d{2}):(\d{2}))?/gi,
|
|
98
|
+
extract: (m) => {
|
|
99
|
+
const hasTime = m[4] !== undefined;
|
|
100
|
+
return {
|
|
101
|
+
stamped: new Date(
|
|
102
|
+
Number(m[1]), Number(m[2]) - 1, Number(m[3]),
|
|
103
|
+
hasTime ? Number(m[4]) : 12, // Date-only → noon, neutralizes timezone-edge false positives
|
|
104
|
+
hasTime ? Number(m[5]) : 0,
|
|
105
|
+
0
|
|
106
|
+
),
|
|
107
|
+
dateOnly: !hasTime,
|
|
108
|
+
};
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
function isAllowlisted(filePath) {
|
|
113
|
+
if (!filePath) return false;
|
|
114
|
+
return ALLOWLIST_PATTERNS.some((re) => re.test(filePath));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function findStaleTimestamps(content, now, oldContent) {
|
|
118
|
+
if (!content || typeof content !== "string") return [];
|
|
119
|
+
const findings = [];
|
|
120
|
+
const oldText = typeof oldContent === "string" ? oldContent : "";
|
|
121
|
+
|
|
122
|
+
const allPatterns = [...FRESH_STAMP_PATTERNS, STAMPED_ISO_PATTERN];
|
|
123
|
+
|
|
124
|
+
for (const pattern of allPatterns) {
|
|
125
|
+
const regex = new RegExp(pattern.regex.source, pattern.regex.flags);
|
|
126
|
+
let match;
|
|
127
|
+
while ((match = regex.exec(content)) !== null) {
|
|
128
|
+
const fullMatch = match[0];
|
|
129
|
+
// For Edit: skip matches that also appear in old_string (pre-existing).
|
|
130
|
+
if (oldText && oldText.includes(fullMatch)) continue;
|
|
131
|
+
|
|
132
|
+
let extracted;
|
|
133
|
+
try { extracted = pattern.extract(match); } catch { continue; }
|
|
134
|
+
if (!extracted) continue;
|
|
135
|
+
|
|
136
|
+
// Normalize: extract may return a Date directly, or {stamped, dateOnly}.
|
|
137
|
+
const stamped = extracted instanceof Date ? extracted : extracted.stamped;
|
|
138
|
+
const dateOnly = !!(extracted && extracted.dateOnly);
|
|
139
|
+
if (!stamped || isNaN(stamped.getTime())) continue;
|
|
140
|
+
|
|
141
|
+
// Date-only stamps: validate same-calendar-day-as-now (drift in days, not minutes).
|
|
142
|
+
if (dateOnly) {
|
|
143
|
+
const sameDay = stamped.getFullYear() === now.getFullYear() &&
|
|
144
|
+
stamped.getMonth() === now.getMonth() &&
|
|
145
|
+
stamped.getDate() === now.getDate();
|
|
146
|
+
if (!sameDay) {
|
|
147
|
+
const dayDiff = Math.round((stamped.getTime() - now.getTime()) / 86400000);
|
|
148
|
+
findings.push({
|
|
149
|
+
pattern: pattern.name,
|
|
150
|
+
matched: fullMatch,
|
|
151
|
+
stamped: `${stamped.getFullYear()}-${String(stamped.getMonth()+1).padStart(2,'0')}-${String(stamped.getDate()).padStart(2,'0')}`,
|
|
152
|
+
driftMinutes: Math.abs(dayDiff) * 1440,
|
|
153
|
+
direction: dayDiff > 0 ? "future" : "past",
|
|
154
|
+
note: `date-only stamp; ${Math.abs(dayDiff)} day(s) ${dayDiff > 0 ? "ahead of" : "behind"} today`,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const driftMs = Math.abs(stamped.getTime() - now.getTime());
|
|
161
|
+
if (driftMs > DRIFT_MS) {
|
|
162
|
+
const driftMin = Math.round(driftMs / 60000);
|
|
163
|
+
findings.push({
|
|
164
|
+
pattern: pattern.name,
|
|
165
|
+
matched: fullMatch,
|
|
166
|
+
stamped: stamped.toISOString(),
|
|
167
|
+
driftMinutes: driftMin,
|
|
168
|
+
direction: stamped.getTime() > now.getTime() ? "future" : "past",
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return findings;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function nowBanner(now) {
|
|
177
|
+
const day = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][now.getDay()];
|
|
178
|
+
const mon = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
|
|
179
|
+
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"][now.getMonth()];
|
|
180
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
181
|
+
return `${day}: ${mon} ${now.getDate()}, ${now.getFullYear()} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function formatBlock(findings, now) {
|
|
185
|
+
const lines = [
|
|
186
|
+
"[GSD-T DATE GUARD] Blocked — content contains timestamps that don't match the live system clock.",
|
|
187
|
+
`Live system clock: ${now.toISOString()} (${nowBanner(now)})`,
|
|
188
|
+
`Tolerance: ±${DRIFT_MINUTES} minutes.`,
|
|
189
|
+
"",
|
|
190
|
+
"Stale or invented timestamps detected:",
|
|
191
|
+
];
|
|
192
|
+
for (const f of findings) {
|
|
193
|
+
lines.push(` - [${f.pattern}] "${f.matched}" — stamped ${f.stamped} (${f.driftMinutes} min ${f.direction} of now)`);
|
|
194
|
+
}
|
|
195
|
+
lines.push("");
|
|
196
|
+
lines.push("Fix: re-emit the write using the live system clock from `[GSD-T NOW]` in your context,");
|
|
197
|
+
lines.push("or `node -e \"console.log(new Date().toISOString())\"` if the signal is unavailable.");
|
|
198
|
+
lines.push("Never source timestamps from `currentDate` (frozen at session start) or memory.");
|
|
199
|
+
return lines.join("\n");
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function processInput(rawJson) {
|
|
203
|
+
let payload;
|
|
204
|
+
try { payload = JSON.parse(rawJson); } catch { return { ok: true }; }
|
|
205
|
+
|
|
206
|
+
const tool = payload.tool_name;
|
|
207
|
+
if (tool !== "Write" && tool !== "Edit") return { ok: true };
|
|
208
|
+
|
|
209
|
+
const input = payload.tool_input || {};
|
|
210
|
+
const filePath = input.file_path || "";
|
|
211
|
+
|
|
212
|
+
// Allowlist check — bypass entirely for machine-written / historical-frozen paths.
|
|
213
|
+
if (isAllowlisted(filePath)) return { ok: true };
|
|
214
|
+
|
|
215
|
+
// Pull live system clock — independent of any signal in Claude's context.
|
|
216
|
+
const now = new Date();
|
|
217
|
+
|
|
218
|
+
let content, oldContent;
|
|
219
|
+
if (tool === "Write") {
|
|
220
|
+
content = input.content || "";
|
|
221
|
+
oldContent = ""; // Write replaces — no diff context.
|
|
222
|
+
} else { // Edit
|
|
223
|
+
content = input.new_string || "";
|
|
224
|
+
oldContent = input.old_string || "";
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const findings = findStaleTimestamps(content, now, oldContent);
|
|
228
|
+
if (findings.length === 0) return { ok: true };
|
|
229
|
+
|
|
230
|
+
return { ok: false, message: formatBlock(findings, now) };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
let input = "";
|
|
234
|
+
process.stdin.setEncoding("utf8");
|
|
235
|
+
process.stdin.on("data", (chunk) => { input += chunk; });
|
|
236
|
+
process.stdin.on("end", () => {
|
|
237
|
+
try {
|
|
238
|
+
const result = processInput(input);
|
|
239
|
+
if (result.ok) process.exit(0);
|
|
240
|
+
process.stderr.write(result.message + "\n");
|
|
241
|
+
process.exit(2);
|
|
242
|
+
} catch (e) {
|
|
243
|
+
// Never block on guard error — fail open. Drift is bad; broken tool calls are worse.
|
|
244
|
+
process.stderr.write(`[GSD-T DATE GUARD] internal error (failing open): ${e.message}\n`);
|
|
245
|
+
process.exit(0);
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
module.exports = { findStaleTimestamps, isAllowlisted, processInput, DRIFT_MINUTES };
|
|
@@ -95,7 +95,7 @@ function run() {
|
|
|
95
95
|
if (cached && cached.latest && isNewer(cached.latest, installed)) {
|
|
96
96
|
doAutoUpdate(cached.latest, installed);
|
|
97
97
|
} else {
|
|
98
|
-
console.log(`${dateStamp()}[GSD-T] v${installed} — CURRENT
|
|
98
|
+
console.log(`${dateStamp()}[GSD-T] v${installed} — CURRENT.`);
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
101
|
|
|
@@ -120,27 +120,59 @@ Even in development, the user may have:
|
|
|
120
120
|
|
|
121
121
|
## Update Notices
|
|
122
122
|
|
|
123
|
-
|
|
123
|
+
The hook output at session start is NOT visible to the user — only your response text is. So you MUST emit a dated status banner as the **very first line of every response** (every turn, not just the first), above any routing header or other content.
|
|
124
124
|
|
|
125
|
-
|
|
125
|
+
**Date source — MANDATORY**: use the timestamp from the most recent `[GSD-T NOW]` signal in your context. The UserPromptSubmit hook (`scripts/gsd-t-auto-route.js`) emits `[GSD-T NOW] Day: Mon DD, YYYY HH:MM:SS TZ` at the start of every turn — this is live system clock. Do NOT use:
|
|
126
|
+
- The SessionStart banner (frozen at session start — wrong on day 2 of a long session)
|
|
127
|
+
- The `currentDate` field in your context (frozen at session start — same problem)
|
|
128
|
+
- Your training-cutoff intuition (always wrong)
|
|
129
|
+
|
|
130
|
+
If `[GSD-T NOW]` is absent for any reason, fall back to `currentDate` and flag the gap.
|
|
131
|
+
|
|
132
|
+
**Format** — one line, no changelog noise in steady state:
|
|
133
|
+
|
|
134
|
+
- Steady state (`[GSD-T]` token seen at session start, or no version-check token — default):
|
|
126
135
|
```
|
|
127
|
-
|
|
128
|
-
Changelog: https://github.com/Tekyz-Inc/get-stuff-done-teams/blob/main/CHANGELOG.md
|
|
136
|
+
Day: Mon DD, YYYY HH:MM TZ — GSD-T v{version} — CURRENT
|
|
129
137
|
```
|
|
138
|
+
Example: `Sun: May 3, 2026 12:21 PDT — GSD-T v3.19.00 — CURRENT`
|
|
130
139
|
|
|
131
|
-
-
|
|
140
|
+
- Auto-updated this session (`[GSD-T AUTO-UPDATE]` token seen at session start):
|
|
132
141
|
```
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
Changelog: https://github.com/Tekyz-Inc/get-stuff-done-teams/blob/main/CHANGELOG.md
|
|
142
|
+
Day: Mon DD, YYYY HH:MM TZ — GSD-T v{old} → v{new} ✅ AUTO-UPDATED
|
|
143
|
+
Changelog: https://github.com/Tekyz-Inc/get-stuff-done-teams/blob/main/CHANGELOG.md
|
|
136
144
|
```
|
|
137
|
-
|
|
145
|
+
(The changelog link earns its place here — there's new code to read about.)
|
|
138
146
|
|
|
139
|
-
-
|
|
147
|
+
- Update available, auto-update failed (`[GSD-T UPDATE]` token seen at session start):
|
|
140
148
|
```
|
|
141
|
-
GSD-T v{
|
|
149
|
+
Day: Mon DD, YYYY HH:MM TZ — GSD-T v{installed} → v{latest} ⬆️ UPDATE AVAILABLE (auto-update failed)
|
|
150
|
+
Run: /gsd-t-version-update-all
|
|
142
151
|
Changelog: https://github.com/Tekyz-Inc/get-stuff-done-teams/blob/main/CHANGELOG.md
|
|
143
152
|
```
|
|
153
|
+
Also repeat at the **end** of your first response.
|
|
154
|
+
|
|
155
|
+
(Drop seconds from the displayed banner — keep it to `HH:MM TZ` for readability. The hook emits seconds; you trim.)
|
|
156
|
+
|
|
157
|
+
**Why every response, not just the first**: long sessions span multiple days. A dated header on every turn means the user can scroll back and immediately see when any exchange happened, without inferring from context.
|
|
158
|
+
|
|
159
|
+
**Order**: dated status banner FIRST. Then routing header (if any). Then your response body.
|
|
160
|
+
|
|
161
|
+
## Live Clock Rule (MANDATORY)
|
|
162
|
+
|
|
163
|
+
Whenever you write a date or timestamp to any file — decision log entries in `progress.md`, `continue-here-{ts}.md` filenames, memory entries, banners, "Updated:" / "Date:" frontmatter, archive headings, anything visible — source it from **the live system clock**. Never from `currentDate` (frozen at session start), the SessionStart banner (frozen), or your intuition (unreliable).
|
|
164
|
+
|
|
165
|
+
**How to obtain the live clock**:
|
|
166
|
+
1. Read the most recent `[GSD-T NOW]` signal from your context (UserPromptSubmit hook emits it every turn).
|
|
167
|
+
2. If absent, run `node -e "console.log(new Date().toISOString())"` via Bash before writing.
|
|
168
|
+
|
|
169
|
+
**Enforcement**: a PreToolUse hook (`scripts/gsd-t-date-guard.js`) blocks Write/Edit calls whose content contains timestamps drifting more than ±5 minutes from the live system clock. The guard:
|
|
170
|
+
- Validates decision-log entries (`- YYYY-MM-DD HH:MM:`), filename timestamps (`continue-here-YYYY-MM-DDTHHMMSS`), banners (`Day: Mon DD, YYYY HH:MM`), and labeled stamps (`Date:`, `Updated:`, `Created:`, etc.).
|
|
171
|
+
- For Edit, ignores timestamps that appear in BOTH `old_string` and `new_string` (pre-existing context, not new writes).
|
|
172
|
+
- Allowlists machine-written paths (`.gsd-t/events/`, `.gsd-t/transcripts/`, `.gsd-t/metrics/`, `.git/`, `node_modules/`, archives, log files).
|
|
173
|
+
- Fails open on internal error — broken tool calls would be worse than drift.
|
|
174
|
+
|
|
175
|
+
If the guard blocks your write, do NOT bypass it. Re-read `[GSD-T NOW]`, regenerate the timestamp, retry.
|
|
144
176
|
|
|
145
177
|
## Conversation vs. Work
|
|
146
178
|
|