@nightlab-agent/date-check 0.1.2 → 0.1.3

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.
@@ -8,7 +8,7 @@
8
8
  "name": "date-check",
9
9
  "source": "./",
10
10
  "description": "Checks the dates and times Claude Code writes into files against the real clock before the write lands.",
11
- "version": "0.1.2",
11
+ "version": "0.1.3",
12
12
  "category": "productivity"
13
13
  }
14
14
  ],
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "date-check",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Checks the dates and times Claude Code writes into changelogs, logs, release notes and handoffs against the real clock before the write lands: a stamp in the future is refused with the real time, a stale one gets a note.",
5
5
  "homepage": "https://github.com/nightlab-agent/date-check#readme",
6
6
  "repository": "https://github.com/nightlab-agent/date-check",
@@ -31,7 +31,7 @@
31
31
  "staleDays": {
32
32
  "type": "number",
33
33
  "title": "Days behind before a note",
34
- "description": "A written date more than this many days before today gets a note.",
34
+ "description": "A written date more than this many days before today gets a note; a new file named for such a day is refused once.",
35
35
  "default": 1
36
36
  },
37
37
  "staleNotes": {
package/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.3 (2026-11-01)
4
+
5
+ - A new Markdown or text file whose name carries a date more than a day before today
6
+ (`briefs/daily-brief-2025-06-12.md` created on 2026-11-01) is refused once, with the real
7
+ date. If that date is meant (a record of that day), Claude writes the same file again and it
8
+ goes through. A name with today's date, yesterday's or a later day's (a plan) is not judged,
9
+ nor a file that already exists, nor a date in a folder's name. The days allowed follow the
10
+ "days behind" setting. Before, only the stamps inside a file were checked, never its name.
11
+
3
12
  ## 0.1.2 (2026-11-01)
4
13
 
5
14
  - These zone names after a time are now read: UTC, GMT, EST, EDT, CST, CDT, MST, MDT, PST, PDT,
package/README.md CHANGED
@@ -17,9 +17,16 @@ Seen in a Claude Code 2.1.312 terminal (120 columns) on 2026-10-31 at 16:52.
17
17
  Asked to add a log line starting with 23:40, Claude's first write was refused
18
18
  with the real time. Because the request had named 23:40, Claude wrote the same
19
19
  line again, it went through, and Claude told the person it was later than now.
20
- The same happened with a changelog entry dated 2026-11-03. A stamp Claude
21
- guesses on its own, and the note on an old stamp, have not yet been seen on a
22
- screen.
20
+ The same happened with a changelog entry dated 2026-11-03.
21
+
22
+ ![Claude Code wrote a log line stamped 00:40 at 02:11; date-check's note on the old stamp gave Claude the real time, and Claude used 02:11 for a line it had held back](https://raw.githubusercontent.com/nightlab-agent/date-check/4901a65a423dbfc9d61aad0b444deb5a445e774d/docs/preview-note.png)
23
+
24
+ Seen on 2026-11-01 at 02:11 EST, same terminal. Asked to start a log line with
25
+ the current time and not to run commands, Claude did not guess: it asked for
26
+ the time. Then asked to add `- 00:40 backup started`, it wrote that line; the
27
+ note on the old stamp gave it the real time, and it wrote the line it had held
28
+ back as `- 02:11 release went out`. A stamp Claude guesses on its own has not
29
+ yet been seen on a screen.
23
30
 
24
31
  - A stamp more than 2 minutes in the future is refused, and the refusal tells
25
32
  Claude the real date and time. If the future date is meant (a deadline, a
@@ -27,6 +34,11 @@ screen.
27
34
  - A time more than 30 minutes old today, or a date more than a day old, goes
28
35
  through with a note to Claude: fine for a record of the past, a fix for one
29
36
  meant as now.
37
+ - A new Markdown or text file named for a day more than a day before today
38
+ (`briefs/daily-brief-2025-06-12.md`, created on 2026-11-01) is refused once
39
+ with the real date. If the date is meant, Claude writes the same file again
40
+ and it goes through. Today, yesterday and later days (a plan) are not judged,
41
+ nor files that already exist, nor dates in folder names.
30
42
  - What counts as a stamp: a date in a Markdown heading (`## 0.2.0 (2026-11-03)`),
31
43
  a labelled field that starts its line (`Date:`, `Updated:`, `Released:`,
32
44
  `date:` in frontmatter), or a date or date and time at the start of a line. A
@@ -64,6 +76,14 @@ the night New York's clocks went back. Asked to add the line
64
76
  its offset, and Claude asked whether the deploy had finished at 01:58 EDT, an
65
77
  hour earlier.
66
78
 
79
+ ![Claude Code's write of briefs/daily-brief-2025-06-12.md refused by date-check with the real date; Claude wrote it again because the name was given, then said the date was likely wrong and offered today's name](https://raw.githubusercontent.com/nightlab-agent/date-check/e008e44b11e50b3115766b19c489e199b7f94ed1/docs/preview-name.png)
80
+
81
+ Seen in the same terminal on 2026-11-01 at 02:23 EST. Asked to create today's
82
+ brief as `briefs/daily-brief-2025-06-12.md`, Claude's first write was refused
83
+ with the real date. Because the request had named the file, Claude wrote it
84
+ again, it went through, and Claude told the person the date was more than a
85
+ year old and offered to rename it to `daily-brief-2026-11-01.md`.
86
+
67
87
  ## What it writes
68
88
 
69
89
  Nothing. It reads the file a Write or an Edit is about to change, to judge only the new
@@ -1,5 +1,5 @@
1
1
  import type { EngineInterface, Register } from 'claude-code'
2
- import { dateOfFile, dayOf, judge, LIMITS, newStamps } from './stamps'
2
+ import { dateOfFile, dayOf, judge, judgeName, LIMITS, newStamps } from './stamps'
3
3
  import type { Limits, Verdict } from './stamps'
4
4
 
5
5
  // date-check, the hooks module. Before a Write, an Edit, or a shell command that appends to
@@ -9,6 +9,8 @@ import type { Limits, Verdict } from './stamps'
9
9
  // the same line written to the same file a second time goes through (a deadline or a plan
10
10
  // can be in the future on purpose), with a note;
11
11
  // - a stamp older than the limits goes through with a note the agent reads after the result.
12
+ // - a new file whose name carries a day more than staleDays past (logs/2025-06-12.md written
13
+ // on 2026-11-01) is refused once with the real date, the same way; a later day is a plan.
12
14
  // Lines the file already had are never judged: only what this call adds. Only Markdown and
13
15
  // text files (isText): a date in code is data, not a stamp.
14
16
 
@@ -52,11 +54,12 @@ export const shellWrite = (command: string): { file: string; text: string } | un
52
54
  }
53
55
 
54
56
  /** What a call changes in which file: the file's text before and after it. */
55
- async function change($: EngineInterface, x: Call): Promise<{ file: string; before: string; after: string }> {
57
+ async function change($: EngineInterface, x: Call): Promise<{ file: string; before: string; after: string; created?: boolean }> {
56
58
  const none = { file: '', before: '', after: '' }
57
59
  if (x.tool === 'Write' && typeof x.content === 'string' && x.file_path) {
58
- const before = (await $.fs.exists(x.file_path)) ? await $.fs.read(x.file_path) : ''
59
- return { file: x.file_path, before, after: x.content }
60
+ const exists = await $.fs.exists(x.file_path)
61
+ const before = exists ? await $.fs.read(x.file_path) : ''
62
+ return { file: x.file_path, before, after: x.content, created: !exists }
60
63
  }
61
64
  if (x.tool === 'Edit' && typeof x.new_string === 'string' && typeof x.old_string === 'string' && x.file_path) {
62
65
  const before = (await $.fs.exists(x.file_path)) ? await $.fs.read(x.file_path) : ''
@@ -69,7 +72,11 @@ async function change($: EngineInterface, x: Call): Promise<{ file: string; befo
69
72
  }
70
73
  if (x.tool === 'Bash' && typeof x.command === 'string') {
71
74
  const w = shellWrite(x.command)
72
- return w ? { file: w.file, before: '', after: w.text } : none
75
+ if (!w) return none
76
+ // Whether the shell's file is new is only known when its path means one place: not under
77
+ // `~` (the engine does not expand it) and not after a `cd` in the same command.
78
+ const sure = !w.file.startsWith('~') && !/(?:^|[;&|\n]\s*)(?:cd|pushd)\s/.test(x.command)
79
+ return { file: w.file, before: '', after: w.text, created: sure && !(await $.fs.exists(w.file)) }
73
80
  }
74
81
  return none
75
82
  }
@@ -81,6 +88,11 @@ export const refusal = (vs: Verdict[]) =>
81
88
  `It is ${vs[0]!.real} now. Write the real date and time, or leave the stamp out. ` +
82
89
  `If a future date is meant (a deadline, a planned release), write the same text again and it goes through.`
83
90
 
91
+ export const nameRefusal = (file: string, n: { date: string; real: string }) =>
92
+ `date-check: the new file's name says ${n.date} (${file.split(/[/\\]/).pop()}), a day that has passed. ` +
93
+ `It is ${n.real} now. If the file is for today, name it for today. ` +
94
+ `If that date is meant (a record of that day), write the same file again and it goes through.`
95
+
84
96
  export const staleNote = (vs: Verdict[]) =>
85
97
  `date-check: ${vs.map(quote).join('; ')} ${vs.length === 1 ? 'is' : 'are'} older than now (${vs[0]!.real}). ` +
86
98
  `If ${vs.length === 1 ? 'it records' : 'they record'} something past, leave ${vs.length === 1 ? 'it' : 'them'}; if ${vs.length === 1 ? 'it was' : 'they were'} meant as now, correct ${vs.length === 1 ? 'it' : 'them'}.`
@@ -103,11 +115,13 @@ export const register: Register = (on, options: Options = {}) => {
103
115
  if (x.tool !== 'Write' && x.tool !== 'Edit' && x.tool !== 'Bash') return next(e)
104
116
  const began = ++order
105
117
  let verdicts: Verdict[] = []
118
+ let name: ReturnType<typeof judgeName>
106
119
  let file = ''
107
120
  try {
108
121
  const now = await $.clock.now()
109
122
  const w = await change($, x)
110
123
  file = w.file
124
+ if (file && isText(file) && w.created) name = judgeName(file, now, limits)
111
125
  if (file && isText(file) && w.after) {
112
126
  // Bare times in a file named for a later day are a plan for that day, not stamps.
113
127
  const day = dateOfFile(file)
@@ -116,17 +130,25 @@ export const register: Register = (on, options: Options = {}) => {
116
130
  } catch {
117
131
  // A failure of this mod never stops the call.
118
132
  verdicts = []
133
+ name = undefined
119
134
  }
120
135
  const key = (v: Verdict) => `${file}\n${v.stamp.line}`
121
136
  const future = verdicts.filter(v => v.kind === 'future')
122
137
  const fresh = future.filter(v => !((meant.get(key(v)) ?? Infinity) < began))
123
- if (fresh.length) {
124
- for (const v of fresh) meant.set(key(v), ++order)
125
- return { deny: refusal(fresh) }
138
+ // By the file's name alone, so a retry spelled another way (relative, absolute) is the same file.
139
+ const nameKey = `(name)\n${file.split(/[/\\]/).pop()}`
140
+ const freshName = name && !((meant.get(nameKey) ?? Infinity) < began) ? name : undefined
141
+ if (fresh.length || freshName) {
142
+ // The first refusal's number is kept: an older call refused late must not move it past a retry.
143
+ const mark = (k: string) => { const n = ++order; meant.set(k, Math.min(meant.get(k) ?? Infinity, n)) }
144
+ for (const v of fresh) mark(key(v))
145
+ if (freshName) mark(nameKey)
146
+ return { deny: [...(freshName ? [nameRefusal(file, freshName)] : []), ...(fresh.length ? [refusal(fresh)] : [])].join('\n') }
126
147
  }
127
148
  const done = await next(e)
128
149
  if (done.deny !== undefined || done.isError) return done
129
150
  const said = [
151
+ ...(name ? [`date-check: kept the date ${name.date} in the new file's name as written the second time.`] : []),
130
152
  ...(future.length ? [`date-check: kept ${future.map(v => `"${v.stamp.text}"`).join(', ')} as written the second time (a date in the future on purpose).`] : []),
131
153
  ...(notes && verdicts.some(v => v.kind === 'stale') ? [staleNote(verdicts.filter(v => v.kind === 'stale'))] : []),
132
154
  ]
package/hooks/stamps.ts CHANGED
@@ -224,3 +224,16 @@ export const judge = (stamps: Stamp[], now: number, limits: Limits = LIMITS, zon
224
224
  }
225
225
  return out
226
226
  }
227
+
228
+ /**
229
+ * The date in a new file's name, when it is more than `staleDays` before today: the agent
230
+ * named the file for the day it guessed, often one near its training. A later day is a plan
231
+ * (an agenda for 2026-11-05), not judged. `undefined` when the name carries no date or the
232
+ * date can be today.
233
+ */
234
+ export const judgeName = (path: string, now: number, limits: Limits = LIMITS): { date: string; real: string } | undefined => {
235
+ const date = dateOfFile(path)
236
+ if (!date) return undefined
237
+ const v = judge([{ text: date, line: path, where: 'field', date }], now, limits)[0]
238
+ return v?.kind === 'stale' ? { date, real: v.real } : undefined
239
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nightlab-agent/date-check",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Claude Code mod: checks the dates and times Claude Code writes into changelogs, logs, release notes and handoffs against the real clock before the write lands; a stamp in the future is refused with the real time.",
5
5
  "keywords": [
6
6
  "claude-code",