Skip to content

Commit 35a58d7

Browse files
committed
Added compact for SHELL.md and removed HEARTBEAT OK.
1 parent f1451e1 commit 35a58d7

File tree

16 files changed

+159
-45
lines changed

16 files changed

+159
-45
lines changed

.claude-plugin/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "claude-code-hermit",
3-
"version": "0.1.0",
3+
"version": "0.1.1",
44
"description": "A personal assistant that lives in your project — memory-driven learning, daily rhythm, idle agency, and operational hygiene for Claude Code",
55
"author": {
66
"name": "gtapps"

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
11
# Changelog
22

3+
## [0.1.1] - 2026-03-30
4+
5+
### Fixed
6+
7+
- **Cost tracker now captures real token data** — The Stop hook payload never included token fields; `cost-tracker.js` was silently writing zero-cost entries (or on newer installs, not writing at all). It now reads the last assistant turn's `usage` from `transcript_path` in the hook payload, which is where Claude Code actually exposes token counts.
8+
- **Cache token pricing** — Added separate pricing rates for `cache_creation_input_tokens` (write) and `cache_read_input_tokens` (read) for all models. Previously cache tokens were ignored entirely, understating cost for heavily-cached sessions.
9+
- **Efficient transcript tail-read** — Instead of loading the full transcript file, reads only the last 128KB using a positioned `fs.readSync` call. Avoids unnecessary memory use on long sessions.
10+
11+
### No manual upgrade steps required.
12+
13+
---
14+
315
## [0.1.0] - 2026-03-30
416

517
### Added

agents/session-mgr.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,9 @@ When the main session requests an idle transition (not a full close):
8686
- Clear `## Plan` table (reset to the 3-row placeholder from the template)
8787
- Clear `## Progress Log`, `## Blockers`, `## Findings`, `## Changed` (all task-specific — already preserved in the archived report)
8888
- Preserve `## Monitoring`, `## Cost`, `## Session Summary` (session-scoped, accumulates across tasks)
89+
- **Compact preserved sections if over threshold.** Read `compact` settings from `.claude-code-hermit/config.json`:
90+
- **Monitoring:** Count non-empty, non-comment lines. If count exceeds `monitoring_threshold`: summarize all entries except the most recent `monitoring_keep` into a single `[Earlier]` line — e.g., `[Earlier] 14 alerts, 3 self-evals (03-28 08:00 — 03-30 18:00)`. If an `[Earlier]` line already exists, merge the counts and extend the time range. Keep the most recent `monitoring_keep` entries intact.
91+
- **Session Summary:** Count non-empty, non-comment lines. If count exceeds `summary_threshold`: summarize all entries except the most recent `summary_keep` into a single `[Earlier]` line — e.g., `[Earlier] 15 tasks archived (S-001 — S-015)`. If an `[Earlier]` line already exists, merge counts and extend the range. Keep the most recent `summary_keep` entries intact.
8992
- Append a summary line to `## Session Summary`:
9093
`**S-NNN** (YYYY-MM-DD): [one-line task summary] — [status] ($X.XX)`
9194
5. If cost data is available, preserve the cumulative total in the Cost section
@@ -100,7 +103,8 @@ When the main session requests an idle transition (not a full close):
100103
## Rules
101104
102105
- Session IDs are sequential and never reused
103-
- Always preserve the full content of SHELL.md — never truncate progress logs
106+
- Never truncate progress logs during a task — only clear them on idle transition (after archiving to report)
107+
- Monitoring and Session Summary may be compacted on idle transition per the `compact` config thresholds
104108
- If SHELL.md exists but has Status `completed` or `blocked`, treat it as needing a new session
105109
- If SHELL.md exists with Status `idle`, treat it as ready for a new task (not a new session) — do not create a new SHELL.md
106110
- Keep session reports factual and concise — no filler text

docs/ALWAYS-ON-OPS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ hermit-start -> [in_progress] -> task done -> [idle] -> new task -> [in_progress
8181
| **Reflection runs** | Yes | Yes |
8282
| **Heartbeat** | Keeps running (or starts) | Stopped |
8383
| **Channels** | Keep running (always-on only) | Stopped |
84-
| **SHELL.md** | Reset in-place to `idle` | Replaced with fresh template |
84+
| **SHELL.md** | Reset in-place to `idle`, Monitoring & Summary compacted if over threshold | Replaced with fresh template |
8585
| **Applies to** | Both interactive and always-on | Both interactive and always-on |
8686

8787
Default: idle transition when work finishes. Full shutdown only via explicit `/session-close` or `hermit-stop`.

docs/ARCHITECTURE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ SHELL.md plan, status, S-NNN-REPORT.md,
7373

7474
**Close:** Defaults to idle transition at every task boundary — your hermit says "What's next?" and waits. Reflection fires. Full shutdown only via `/session-close`. See [Always-On Lifecycle](ALWAYS-ON-OPS.md#2-always-on-lifecycle).
7575

76-
**Archive:** SHELL.md -> `S-NNN-REPORT.md`. Fresh template with carry-forward items. Any session can pick up where the last one left off.
76+
**Archive:** SHELL.md -> `S-NNN-REPORT.md`. Fresh template with carry-forward items. Monitoring and Session Summary sections are compacted if over threshold (configurable via `compact` in config.json). Any session can pick up where the last one left off.
7777

7878
---
7979

docs/CONFIG-REFERENCE.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,21 @@ Modify with `/hermit-settings routines`, `/hermit-settings idle`.
7474

7575
---
7676

77+
## `compact`
78+
79+
Controls automatic compaction of SHELL.md sections during idle transitions. When a section exceeds its threshold, older entries are summarized into a single `[Earlier]` line and only the most recent entries are kept.
80+
81+
| Key | Type | Default | Description |
82+
|-----|------|---------|-------------|
83+
| `monitoring_threshold` | integer | `30` | Compact `## Monitoring` when it exceeds this many lines. |
84+
| `monitoring_keep` | integer | `20` | Keep this many recent entries after compacting. |
85+
| `summary_threshold` | integer | `30` | Compact `## Session Summary` when it exceeds this many lines. |
86+
| `summary_keep` | integer | `15` | Keep this many recent entries after compacting. |
87+
88+
Setting `keep` equal to `threshold` effectively disables compaction for that section. Modify with `/hermit-settings compact`.
89+
90+
---
91+
7792
## `docker`
7893

7994
| Key | Type | Default | Description |

docs/OBSIDIAN-SETUP.md

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@ An optional read-mostly companion dashboard. Your hermit works without it.
66

77
1. Open Obsidian -> "Open folder as vault" -> select the repo root
88
2. Add `.obsidian/` to `.gitignore`
9-
3. Install the **Dataview** plugin (Community plugins -> Browse -> "Dataview")
10-
4. Create `dashboard.md` at the repo root (add to `.gitignore`) and paste:
9+
3. **Install Folder Bridge and add .claude and .claude-code-hermit folder:** plugin (Community plugins -> Browse -> "FolderBridge")(https://github.com/tescolopio/Obsidian_FolderBridge)
10+
4. Install the **Dataview** plugin (Community plugins -> Browse -> "Dataview")
11+
5. Create `dashboard.md` at the repo root (add to `.gitignore`) and paste:
1112

1213
````markdown
1314
## Sessions
15+
1416
```dataview
1517
TABLE status, date, duration, cost_usd AS "Cost", tags
1618
FROM ".claude-code-hermit/sessions"
@@ -19,6 +21,7 @@ SORT date DESC
1921
```
2022

2123
## Proposals
24+
2225
```dataview
2326
TABLE status, source, category, session, created
2427
FROM ".claude-code-hermit/proposals"
@@ -27,7 +30,7 @@ SORT created DESC
2730
```
2831
````
2932

30-
5. Pin SHELL.md in the right pane for live updates (right-click tab -> "Pin")
33+
7. Pin SHELL.md in the right pane for live updates (right-click tab -> "Pin")
3134

3235
That's it. You now have a queryable dashboard of all sessions and proposals.
3336

@@ -148,19 +151,20 @@ This is a manual, operator-curated view. The hermit does not generate or modify
148151

149152
## Advanced: Multi-Hermit Setup
150153

151-
If you run multiple hermit instances (e.g., dev hermit + Glam), you can create a cross-hermit observatory:
154+
If you run multiple hermit instances, you can create a cross-hermit observatory:
152155

153156
```
154157
observatory/
155-
dev-hermit/ -> symlink to project-a/.claude-code-hermit/
156-
glam/ -> symlink to project-b/.claude-code-hermit/
157-
dashboard.md -> cross-hermit Dataview queries
158+
dev-hermit/ -> symlink to project-a/.claude-code-hermit/
159+
accountant-hermit/ -> symlink to project-b/.claude-code-hermit/
160+
dashboard.md -> cross-hermit Dataview queries
158161
```
159162

160163
Open the `observatory/` directory as an Obsidian vault. Dataview queries work across symlinked directories:
161164

162165
````markdown
163166
## All Hermits — Recent Sessions
167+
164168
```dataview
165169
TABLE id, status, date, cost_usd AS "Cost"
166170
FROM ""

docs/SKILLS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ This section is only active when `idle_behavior` is set to `"discover"` (default
7070

7171
| Skill | What it does | Auto-triggers |
7272
| ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- |
73-
| `hermit-settings` | View or change project config. Subcommands: `name`, `language`, `timezone`, `escalation`, `sign-off`, `channels`, `remote`, `model`, `budget`, `brief`, `permissions`, `heartbeat`, `routines`, `idle-agency`, `env`, `docker`. | -- |
73+
| `hermit-settings` | View or change project config. Subcommands: `name`, `language`, `timezone`, `escalation`, `sign-off`, `channels`, `remote`, `model`, `budget`, `brief`, `permissions`, `heartbeat`, `routines`, `idle-agency`, `env`, `compact`, `docker`. | -- |
7474
| `init` | One-time project setup. Creates state directory, runs the wizard, scans your project, and writes OPERATOR.md. | -- |
7575
| `upgrade` | Run after updating the plugin. Detects version gaps, refreshes templates, prompts for new settings. | -- |
7676

scripts/cost-tracker.js

Lines changed: 60 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ const path = require('path');
1111

1212
// Per-1M-token pricing (USD)
1313
const PRICING = {
14-
haiku: { input: 0.8, output: 4.0 },
15-
sonnet: { input: 3.0, output: 15.0 },
16-
opus: { input: 15.0, output: 75.0 },
14+
haiku: { input: 0.80, cacheWrite: 1.00, cacheRead: 0.08, output: 4.0 },
15+
sonnet: { input: 3.00, cacheWrite: 3.75, cacheRead: 0.30, output: 15.0 },
16+
opus: { input: 15.0, cacheWrite: 18.75, cacheRead: 1.50, output: 75.0 },
1717
};
1818

1919
const MAX_STDIN = 1024 * 1024; // 1MB safety limit
@@ -30,11 +30,45 @@ function detectModel(modelStr) {
3030
return 'sonnet';
3131
}
3232

33-
function calculateCost(model, inputTokens, outputTokens) {
33+
function calculateCost(model, inputTokens, cacheWriteTokens, cacheReadTokens, outputTokens) {
3434
const pricing = PRICING[model] || PRICING.sonnet;
35-
const inputCost = (inputTokens / 1_000_000) * pricing.input;
36-
const outputCost = (outputTokens / 1_000_000) * pricing.output;
37-
return inputCost + outputCost;
35+
return (inputTokens / 1_000_000) * pricing.input
36+
+ (cacheWriteTokens / 1_000_000) * pricing.cacheWrite
37+
+ (cacheReadTokens / 1_000_000) * pricing.cacheRead
38+
+ (outputTokens / 1_000_000) * pricing.output;
39+
}
40+
41+
function readLastTurnUsage(transcriptPath) {
42+
const TAIL_BYTES = 131072; // 128KB — read from end, avoid loading full transcript
43+
try {
44+
const stat = fs.statSync(transcriptPath);
45+
const readFrom = Math.max(0, stat.size - TAIL_BYTES);
46+
const fd = fs.openSync(transcriptPath, 'r');
47+
const buf = Buffer.alloc(Math.min(TAIL_BYTES, stat.size));
48+
fs.readSync(fd, buf, 0, buf.length, readFrom);
49+
fs.closeSync(fd);
50+
51+
const lines = buf.toString('utf-8').split('\n');
52+
// Drop the first line when mid-file (it's a partial line)
53+
if (readFrom > 0) lines.shift();
54+
55+
for (let i = lines.length - 1; i >= 0; i--) {
56+
try {
57+
const entry = JSON.parse(lines[i]);
58+
if (entry.type === 'assistant' && entry.message?.usage) {
59+
const u = entry.message.usage;
60+
return {
61+
inputTokens: u.input_tokens || 0,
62+
cacheWriteTokens: u.cache_creation_input_tokens || 0,
63+
cacheReadTokens: u.cache_read_input_tokens || 0,
64+
outputTokens: u.output_tokens || 0,
65+
model: entry.message.model || '',
66+
};
67+
}
68+
} catch {}
69+
}
70+
} catch {}
71+
return null;
3872
}
3973

4074
function parseLogEntries() {
@@ -266,18 +300,27 @@ async function main() {
266300

267301
const data = JSON.parse(raw);
268302

269-
// Extract token counts — handle various property names
270-
const inputTokens = data.input_tokens || data.inputTokens || data.usage?.input_tokens || 0;
271-
const outputTokens = data.output_tokens || data.outputTokens || data.usage?.output_tokens || 0;
272-
const model = detectModel(data.model || data.model_name || '');
273-
const sessionId = data.session_id || data.sessionId || 'unknown';
303+
const sessionId = data.session_id || 'unknown';
304+
const transcriptPath = data.transcript_path;
305+
306+
if (!transcriptPath) {
307+
process.exit(0);
308+
}
309+
310+
const turn = readLastTurnUsage(transcriptPath);
311+
if (!turn) {
312+
process.exit(0);
313+
}
314+
315+
const { inputTokens, cacheWriteTokens, cacheReadTokens, outputTokens, model: rawModel } = turn;
316+
const model = detectModel(rawModel);
274317

275-
const totalTokens = inputTokens + outputTokens;
318+
const totalTokens = inputTokens + cacheWriteTokens + cacheReadTokens + outputTokens;
276319
if (totalTokens === 0) {
277320
process.exit(0);
278321
}
279322

280-
const cost = calculateCost(model, inputTokens, outputTokens);
323+
const cost = calculateCost(model, inputTokens, cacheWriteTokens, cacheReadTokens, outputTokens);
281324
const roundedCost = Math.round(cost * 10000) / 10000;
282325

283326
// Log to JSONL
@@ -286,6 +329,8 @@ async function main() {
286329
session_id: sessionId,
287330
model,
288331
input_tokens: inputTokens,
332+
cache_write_tokens: cacheWriteTokens,
333+
cache_read_tokens: cacheReadTokens,
289334
output_tokens: outputTokens,
290335
total_tokens: totalTokens,
291336
estimated_cost_usd: roundedCost,
@@ -314,7 +359,7 @@ async function main() {
314359
writeCostSummary();
315360

316361
// Output brief summary
317-
console.log(`[cost-tracker] ${model}: ${Math.round(totalTokens / 1000)}K tokens, $${cost.toFixed(4)} (cumulative: ${costStr})`);
362+
console.log(`[cost-tracker] ${model}: ${Math.round(totalTokens / 1000)}K tokens (${Math.round(cacheReadTokens / 1000)}K cached), $${cost.toFixed(4)} (cumulative: ${costStr})`);
318363
} catch (err) {
319364
// Non-fatal — never block on cost tracking failure
320365
console.error(`[cost-tracker] Error: ${err.message}`);

skills/heartbeat/SKILL.md

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ Execute one heartbeat tick immediately. Useful for testing the checklist.
4949
10. Determine: does anything need operator attention?
5050

5151
**If nothing actionable:**
52-
- Append to SHELL.md `## Monitoring` section: `[HH:MM] Heartbeat: OK`
52+
- Do NOT append to SHELL.md (the tick is already recorded via `total_ticks` in config.json)
5353
- Read config `heartbeat.show_ok`:
5454
- If `true` AND channels are configured: send "Heartbeat OK" to channel
5555
- If `false` (default): no channel message — silent acknowledgment
@@ -79,21 +79,22 @@ Triggered every N ticks (configured by `heartbeat.self_eval_interval`, default 2
7979

8080
**Steps:**
8181

82-
1. Read recent heartbeat entries from SHELL.md `## Monitoring` section — collect all `[HH:MM] Heartbeat:` lines in the current session
83-
2. For each checklist item in HEARTBEAT.md: check how many recent ticks flagged it vs how many were OK
84-
3. **Stale check detection:** If a specific checklist item was OK for all ticks in the evaluation window, suggest to the operator:
82+
1. Read `heartbeat.total_ticks` and `heartbeat.self_eval_interval` from config.json. The evaluation window covers the last `self_eval_interval` ticks (i.e., ticks `total_ticks - self_eval_interval` through `total_ticks`).
83+
2. Read SHELL.md `## Monitoring` — collect all `[HH:MM] Heartbeat:` alert/warning lines. These are the only entries present (OK ticks are not logged).
84+
3. For each checklist item: count how many alert lines reference that item. Compare against the window size (`self_eval_interval`). If zero alerts across the window, the item has been clean for all ticks in the evaluation period.
85+
4. **Stale check detection:** If a specific checklist item had zero alerts across the evaluation window, suggest to the operator:
8586
> "Heartbeat self-check: the item '[check description]' has been OK for [N] consecutive ticks. Consider removing it to save tokens, or reducing heartbeat frequency."
86-
4. **Checklist weight check:** Count items in HEARTBEAT.md. If count > 10, suggest:
87+
5. **Checklist weight check:** Count items in HEARTBEAT.md. If count > 10, suggest:
8788
> "HEARTBEAT.md has {count} items (recommended: ≤10). Consider moving periodic checks to routines (`/hermit-settings routines`) to reduce per-tick token cost. Items that only need daily/weekly checking are good routine candidates."
88-
5. **Missing check suggestion:** Scan `.claude-code-hermit/proposals/` for recent auto-detected proposals about recurring issues. If a proposal describes a problem that could be caught by a heartbeat check, suggest:
89+
6. **Missing check suggestion:** Scan `.claude-code-hermit/proposals/` for recent auto-detected proposals about recurring issues. If a proposal describes a problem that could be caught by a heartbeat check, suggest:
8990
> "PROP-NNN identified a recurring [issue]. Consider adding a [relevant check] to HEARTBEAT.md."
90-
6. Append self-evaluation findings to SHELL.md `## Monitoring` with timestamp:
91+
7. Append self-evaluation findings to SHELL.md `## Monitoring` with timestamp:
9192
```
9293
[HH:MM] Heartbeat self-eval: [summary of suggestions]
9394
```
94-
7. If channels are configured: send self-eval findings as a notification
95-
8. Do NOT auto-modify HEARTBEAT.md — suggest only. The operator decides what to check. Offer to make the edit if the operator agrees.
96-
9. After the operator acts on a suggestion (adds or removes a check): reset `heartbeat.total_ticks` to 0 in config.json
95+
8. If channels are configured: send self-eval findings as a notification
96+
9. Do NOT auto-modify HEARTBEAT.md — suggest only. The operator decides what to check. Offer to make the edit if the operator agrees.
97+
10. After the operator acts on a suggestion (adds or removes a check): reset `heartbeat.total_ticks` to 0 in config.json
9798

9899
### start
99100

0 commit comments

Comments
 (0)