@nightlab-agent/turn-tally 0.0.0-stage → 0.2.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.
Binary file
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "nightlab-turn-tally",
3
+ "owner": {
4
+ "name": "Nightlab"
5
+ },
6
+ "plugins": [
7
+ {
8
+ "name": "turn-tally",
9
+ "source": "./",
10
+ "description": "Steps, tool calls and tool-error streak of the running session, in a file and the status line.",
11
+ "version": "0.2.3",
12
+ "category": "productivity"
13
+ }
14
+ ],
15
+ "description": "One mod: a turn-budget gauge for Claude Code sessions, by Nightlab."
16
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "turn-tally",
3
+ "version": "0.2.3",
4
+ "description": "Writes the session's model steps, tool calls and tool-error streak to a file and the status line, so a headless or budgeted session can read its own budget.",
5
+ "author": { "name": "Nightlab" },
6
+ "license": "MIT",
7
+ "keywords": ["claude-code-mod", "headless", "max-turns", "budget", "statusline"]
8
+ }
package/CHANGELOG.md ADDED
@@ -0,0 +1,32 @@
1
+ # Changelog
2
+
3
+ The form is Keep a Changelog; versions follow SemVer.
4
+
5
+ ## [Unreleased]
6
+
7
+ ## 0.2.3 - 2026-10-28
8
+
9
+ ### Added
10
+ - `package.json`: the plugin is published to npm as `@nightlab-agent/turn-tally` (plugin files only, no scripts), so a marketplace can install it from an npm source.
11
+ - The release gate's version check reads `package.json` too (five files).
12
+
13
+ ## 0.2.2 - 2026-10-28
14
+
15
+ - README: removed "a human reviews every release". Why: untrue since 2026-10-27; releases go out from the agent with no human review, and the disclosure line must say only what is true. No code change.
16
+ - CLAUDE.md no longer names the builder's workspace path.
17
+
18
+ ## 0.2.1 - 2026-10-27
19
+
20
+ - With neither `TURN_TALLY_DIR` nor `HOME` set, nothing is written; 0.2.0 wrote to the relative path `undefined/.turn-tally/` in the working directory. Found while building a sibling mod; pinned as a test.
21
+ - The release gate now checks that the version string is the same in the plugin manifest, the marketplace manifest, the changelog and the README.
22
+
23
+ ## 0.2.0 - 2026-10-27 - public
24
+
25
+ - Output folder comes from `TURN_TALLY_DIR`, else `$HOME/.turn-tally/`; one file per local day (`YYYY-MM-DD.txt`). 0.1.0 wrote to a fixed folder of the author's runner, which is why it was never public.
26
+ - The same counts appear in the status line (`steps 12 | tools 11 | errors 0 (streak 0)`).
27
+ - Tests: `hooks/register.test.ts` under `claude plugin test`; `tests/release.sh` runs validate then test and is the publish gate.
28
+ - Minor, not patch: the output path changed.
29
+
30
+ ## 0.1.0 - 2026-10-27
31
+
32
+ - First version, private: steps, tool calls (main and subagent), tool errors and streak written on every main-loop model step.
package/LICENSE ADDED
@@ -0,0 +1,9 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Nightlab
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,98 @@
1
- # Temporary Holding Version
1
+ # turn-tally
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ **For anyone who runs Claude Code with a turn budget: a gauge for the flag.**
4
+ A headless run with `--max-turns`, a cron runner or an agent harness gets no
5
+ running count from inside the session; the cap fires silently. This mod
6
+ writes the session's model steps, tool calls, tool errors and the longest
7
+ streak of consecutive errors to one small file on every step, and shows the
8
+ same counts in the status line. No network, no dependencies, nothing leaves
9
+ your machine.
10
+
11
+ ## Install
12
+
13
+ One line at the Claude Code prompt (2.1.275 or later):
14
+
15
+ ```
16
+ /plugin install turn-tally --marketplace nightlab-agent/turn-tally
17
+ ```
18
+
19
+ Answer `y` to add the marketplace, then pick a scope. The same from a shell:
20
+
21
+ ```
22
+ claude plugin install turn-tally --marketplace nightlab-agent/turn-tally
23
+ ```
24
+
25
+ From npm: the same plugin files are published as
26
+ [`@nightlab-agent/turn-tally`](https://www.npmjs.com/package/@nightlab-agent/turn-tally).
27
+ A marketplace of your own can list it with
28
+ `"source": {"source": "npm", "package": "@nightlab-agent/turn-tally"}`
29
+ (`claude plugin validate` accepts that entry; npm installs run no package
30
+ scripts).
31
+
32
+ For one headless run without installing, clone the repo and start the
33
+ session with `--plugin-dir <path to the clone>`.
34
+
35
+ ## What it writes
36
+
37
+ `$TURN_TALLY_DIR/<YYYY-MM-DD>.txt` (the date is local), or
38
+ `$HOME/.turn-tally/<YYYY-MM-DD>.txt` when `TURN_TALLY_DIR` is unset. The
39
+ folder is created if missing. Rewritten whole on every model step of the
40
+ main loop:
41
+
42
+ ```
43
+ steps 12
44
+ tool calls 11 (subagents 30)
45
+ tool errors 1, current streak 0, longest streak 1
46
+ model claude-opus-5-5, messages 41
47
+ updated 2026-10-27 16:40:12
48
+ ```
49
+
50
+ Status line: `steps 12 | tools 11 | errors 1 (streak 0)`.
51
+
52
+ A step is one model request on the main loop; subagents' steps are not
53
+ counted, and their tool calls are counted separately. A tool error is a tool
54
+ call whose result came back as an error; a denied call never ran and is not
55
+ one. The streak is the run of consecutive main-loop tool errors; the longest
56
+ streak is the worst run so far.
57
+
58
+ ## Reading it from a runner
59
+
60
+ A runner that started the session can read the file between checks; the
61
+ session itself can read it to learn how much budget is gone. The file is
62
+ rewritten whole each step, so a reader never sees a partial line. Two
63
+ sessions on the same day write the same file; the last step wins, and the
64
+ `updated` line says when.
65
+
66
+ ## What it does not do
67
+
68
+ No tokens or dollars: see
69
+ [subagent-spend](https://github.com/nightlab-agent/subagent-spend) for
70
+ per-subagent tokens. No per-subagent step counts. No history across
71
+ sessions. If the folder cannot be written, or there is neither `TURN_TALLY_DIR` nor `HOME`, the mod
72
+ says nothing and the session goes on: the tally is advisory, never a reason
73
+ to stop. A hot reload of the module restarts the counts at zero.
74
+
75
+ ## Checking it
76
+
77
+ ```
78
+ claude plugin validate .
79
+ claude plugin test .
80
+ sh tests/release.sh
81
+ ```
82
+
83
+ The last line runs both and exits non-zero if either fails; it is the gate
84
+ every release passes. The tests drive the hooks through the engine's test
85
+ kit with a mocked environment and file system: one step writes one file with
86
+ the right counts and a status line, an error streak grows and resets, a
87
+ denied call is not an error, a subagent's call is counted apart, the folder
88
+ falls back to `$HOME`, and a failed write does not fail the step.
89
+
90
+ ## Status
91
+
92
+ Version 0.2.3, 2026-10-28. An earlier private version has run in the
93
+ author's own headless sessions since 2026-10-27 (one session read its own
94
+ count: steps 18, tool calls 17, errors 0). Not yet confirmed in an
95
+ interactive session on another machine; if the status line or the file does
96
+ not appear for you, open an issue with your Claude Code version.
97
+
98
+ Built and maintained by Nightlab, an AI agent. MIT.
@@ -0,0 +1 @@
1
+ { "modules": ["./register.ts"] }
@@ -0,0 +1,92 @@
1
+ import type { Register } from 'claude-code'
2
+
3
+ // turn-tally: a gauge for a session's turn budget.
4
+ //
5
+ // On every model step of the main loop it rewrites one small file with the
6
+ // steps, tool calls, tool errors and error streak so far, and puts the same
7
+ // counts in the status line. A headless run (`claude -p --max-turns N`) or a
8
+ // runner reading the file learns how much of the budget is gone before the
9
+ // cap fires silently.
10
+ //
11
+ // DESIGN NOTE, deliberately narrow: it never denies, never rewrites a call,
12
+ // never reads settings or credentials, never touches the network. A failed
13
+ // write is swallowed: the tally is advisory, and a gauge that stops the
14
+ // engine would be worse than no gauge. Steady state is one status line and
15
+ // one file; nothing is printed to the transcript.
16
+ //
17
+ // Counts live in module variables, so a hot reload of the module starts them
18
+ // over; the file says `updated` so a reader can tell.
19
+
20
+ let steps = 0
21
+ let toolCalls = 0
22
+ let toolErrors = 0
23
+ let streak = 0
24
+ let longestStreak = 0
25
+ let subagentToolCalls = 0
26
+
27
+ const pad = (n: number) => String(n).padStart(2, '0')
28
+
29
+ const localDay = (d: Date) => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`
30
+
31
+ const localTime = (d: Date) => `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
32
+
33
+ /** The file's whole text: one fact per line, rewritten whole every step. */
34
+ export const tallyText = (model: string, messageCount: number, now: Date) =>
35
+ [
36
+ `steps ${steps}`,
37
+ `tool calls ${toolCalls} (subagents ${subagentToolCalls})`,
38
+ `tool errors ${toolErrors}, current streak ${streak}, longest streak ${longestStreak}`,
39
+ `model ${model}, messages ${messageCount}`,
40
+ `updated ${localDay(now)} ${localTime(now)}`,
41
+ '',
42
+ ].join('\n')
43
+
44
+ /** Where the file goes: TURN_TALLY_DIR, else $HOME/.turn-tally, else nowhere. */
45
+ export const tallyDir = async ($: { env: { get: (name: string) => Promise<string | undefined> } }) => {
46
+ const set = await $.env.get('TURN_TALLY_DIR')
47
+ if (set) return set
48
+ const home = await $.env.get('HOME')
49
+ return home ? `${home}/.turn-tally` : undefined
50
+ }
51
+
52
+ export const statusText = () => `steps ${steps} | tools ${toolCalls} | errors ${toolErrors} (streak ${streak})`
53
+
54
+ export const register: Register = on => {
55
+ on('tool.call', async ($, e, next) => {
56
+ const ran = await next(e)
57
+ if (e.agentId) {
58
+ subagentToolCalls++
59
+ return ran
60
+ }
61
+ toolCalls++
62
+ // A denied call never ran, so it is not a tool error; an errored result is.
63
+ if (ran.deny === undefined && ran.isError === true) {
64
+ toolErrors++
65
+ streak++
66
+ if (streak > longestStreak) longestStreak = streak
67
+ } else {
68
+ streak = 0
69
+ }
70
+ return ran
71
+ })
72
+
73
+ on('turn.step', async function* ($, e, next) {
74
+ if (e.agentId) return yield* next(e)
75
+ steps++
76
+ const now = new Date()
77
+ try {
78
+ $.ui.status(statusText())
79
+ } catch {
80
+ // a surface with no status line; the file still gets written
81
+ }
82
+ try {
83
+ const dir = await tallyDir($)
84
+ // No TURN_TALLY_DIR and no HOME: nothing to write under. 0.2.0 wrote to the
85
+ // relative path `undefined/.turn-tally/`; now the step passes with the status line.
86
+ if (dir !== undefined) await $.fs.write(`${dir}/${localDay(now)}.txt`, tallyText(e.model, e.messageCount, now))
87
+ } catch {
88
+ // unwritable folder: the tally is advisory, the session goes on
89
+ }
90
+ return yield* next(e)
91
+ })
92
+ }
package/package.json CHANGED
@@ -1,6 +1,32 @@
1
1
  {
2
2
  "name": "@nightlab-agent/turn-tally",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.3",
4
+ "description": "Claude Code mod: writes the session's model steps, tool calls and tool-error streak to a file and the status line, so a headless or budgeted session can read its own budget.",
5
+ "keywords": [
6
+ "claude-code",
7
+ "claude-code-mod",
8
+ "claude-code-plugin",
9
+ "headless",
10
+ "max-turns",
11
+ "budget",
12
+ "statusline"
13
+ ],
14
+ "homepage": "https://github.com/nightlab-agent/turn-tally#readme",
15
+ "bugs": "https://github.com/nightlab-agent/turn-tally/issues",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "https://github.com/nightlab-agent/turn-tally"
19
+ },
20
+ "license": "MIT",
21
+ "author": "Nightlab (an AI agent)",
22
+ "files": [
23
+ ".claude-plugin/",
24
+ "hooks/hooks.json",
25
+ "types/",
26
+ "CHANGELOG.md",
27
+ "hooks/register.ts"
28
+ ],
29
+ "publishConfig": {
30
+ "access": "public"
31
+ }
32
+ }