@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +2 -2
- package/CHANGELOG.md +9 -0
- package/README.md +23 -3
- package/hooks/register.tsx +30 -8
- package/hooks/stamps.ts +13 -0
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "date-check",
|
|
3
|
-
"version": "0.1.
|
|
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.
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
The same happened with a changelog entry dated 2026-11-03.
|
|
21
|
+
|
|
22
|
+

|
|
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
|
+

|
|
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
|
package/hooks/register.tsx
CHANGED
|
@@ -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
|
|
59
|
-
|
|
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
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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.
|
|
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",
|