Skip to content

Commit 36295ee

Browse files
committed
fix: resolve conflict markers
1 parent c0a2923 commit 36295ee

3 files changed

Lines changed: 97 additions & 1 deletion

File tree

.almanac/pages/capture-flow.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
title: Capture Flow
3-
summary: `almanac capture` resolves transcript inputs and runs the Absorb operation, while `capture sweep` adds scheduled quiet-transcript discovery for Claude and Codex.
3+
summary: "`almanac capture` resolves transcript inputs and runs the Absorb operation, while `capture sweep` adds scheduled quiet-transcript discovery for Claude and Codex."
44
topics: [agents, flows]
55
files:
66
- src/commands/operations.ts
@@ -116,6 +116,8 @@ Continuation capture keeps passing the original transcript path into capture, an
116116

117117
One open operational consequence from the same session is that "pass the original transcript path" is still compatible with a smarter first step inside Absorb. Future prompt or tooling work can keep the current command surface while instructing the agent to parse JSONL structurally before reading deeply, and can optionally add a cheap preflight size estimator or cap for unusually large first-run backlogs. Neither behavior is part of the current implementation.
118118

119+
Discovery first tries to match transcripts by directory name hash, which is the fast path with no transcript-content IO. If no matches are found, it falls back to content scanning: each transcript is opened, and `readHead(path, 4096)` checks whether the first 4 KB contains `"cwd":"<repoRoot>"`. One known performance issue remains in that fallback: `readHead` currently calls `readFile()` to load the entire file into memory before slicing. On a Claude projects directory with many large session files this causes hundreds of MB of unnecessary IO at `almanac capture` startup. The fix is to use `fs.open().read()` or a bounded stream limited to 4,096 bytes.
120+
119121
## Sweep dry-run semantics
120122

121123
`almanac capture sweep --dry-run` is a verification path, not a background-start variant. It should discover the same candidates and compute the same cursor ranges as a real sweep, but it must not enqueue capture jobs or create/update [[capture-ledger]] state.

.almanac/pages/sessionend-hook.md

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
---
2+
title: SessionEnd Hook
3+
summary: "`almanac hook install` wires the shared capture script into Claude, Codex, and Cursor with agent-specific hook shapes and timing behavior."
4+
topics: [agents, flows, cli]
5+
files:
6+
- src/commands/hook.ts
7+
- hooks/almanac-capture.sh
8+
- src/cli/register-wiki-lifecycle-commands.ts
9+
- test/hook.test.ts
10+
sources:
11+
- /Users/kushagrachitkara/.codex/sessions/2026/05/11/rollout-2026-05-11T14-32-08-019e18f4-5e73-7790-ba49-73cc02544a58.jsonl
12+
verified: 2026-05-11
13+
---
14+
15+
# SessionEnd Hook
16+
17+
`almanac hook install` wires one shared `hooks/almanac-capture.sh` script into each supported agent app, but the event name and config file differ by app. Claude uses `SessionEnd` in `~/.claude/settings.json`, Codex uses `Stop` in `~/.codex/hooks.json`, and Cursor uses `sessionEnd` in `~/.cursor/hooks.json`.
18+
19+
The shared script backgrounds `almanac capture` after the session transcript is available. This makes hook installation part of the capture pipeline, not a Claude-only feature.
20+
21+
A 2026-05-11 capture session verified the main Codex-specific invariant behind this mapping: current Codex builds expose `Stop` as the usable lifecycle hook surface, not a working `SessionEnd` event. For Almanac, "Codex hook support" therefore means "debounced quiet-session capture after `Stop`," not "capture exactly once when a thread is permanently closed."
22+
23+
## Agent-specific install targets
24+
25+
Claude uses a wrapped `SessionEnd` hook entry in `~/.claude/settings.json`.
26+
27+
Codex uses a wrapped `Stop` hook entry in `~/.codex/hooks.json`. `almanac hook install --source codex` also ensures `codex_hooks = true` in `~/.codex/config.toml`, because Codex will not emit hook callbacks unless that feature flag is enabled.
28+
29+
Cursor uses a flat `sessionEnd` hook entry in `~/.cursor/hooks.json`.
30+
31+
## Settings.json shape
32+
33+
Claude Code validates `settings.json` against a strict schema. Each event array entry (e.g. `SessionEnd`) is a `{ matcher, hooks: [...] }` container; actual command objects live inside the nested `hooks` array. `matcher` is always `""` for `SessionEnd`, which matches every session.
34+
35+
```json
36+
{
37+
"hooks": {
38+
"SessionEnd": [
39+
{
40+
"matcher": "",
41+
"hooks": [
42+
{ "type": "command", "command": "/path/to/almanac-capture.sh" }
43+
]
44+
}
45+
]
46+
}
47+
}
48+
```
49+
50+
Versions v0.1.0–v0.1.4 of codealmanac wrote command objects directly at the event-array level; that shape is rejected by newer Claude Code. `almanac hook install` migrates any legacy entry it recognizes (by `command` ending in `almanac-capture.sh`) to the wrapped form on install.
51+
52+
## Script path
53+
54+
The hook script is `hooks/almanac-capture.sh`. On install, `almanac hook install` copies it to `~/.claude/hooks/almanac-capture.sh`, and each agent config points at that stable path. This survives npm version bumps, npx cache evictions, and nvm version switches, so the installed hook never depends on an ephemeral package-manager path.
55+
56+
## Install/uninstall
57+
58+
`almanac hook install` — adds the configured agent hook entry or entries; idempotent (re-running is safe).
59+
`almanac hook uninstall` — removes Almanac's entry; leaves all other entries in `hooks` untouched.
60+
`almanac hook status` — reports whether the hook is installed, without modifying anything.
61+
62+
`runHookInstall()` already supports `--source <claude|codex|cursor|all>`, but the CLI surface is still asymmetric: `hook uninstall` and `hook status` do not take `--source` today and still default to Claude's `~/.claude/settings.json` path. Future hook UX changes should preserve or intentionally remove that asymmetry rather than assuming all three commands are symmetric now.
63+
64+
## Backgrounding
65+
66+
The hook runs `almanac capture`, which now starts a V1 background job by default. The hook should return quickly after the parent writes the queued run record under [[process-manager-runs]]. Because capture runs headlessly with no terminal attached, provider auth must already be available; Claude can use the saved credential store (`~/.claude/credentials/`) or `ANTHROPIC_API_KEY`.
67+
68+
Codex needs special handling here: `Stop` is turn-scoped, not session-scoped. The shared shell script debounces Codex `Stop` events with a `.almanac/runs/.capture-<session>.debounce` marker and only runs capture after a quiet period. Without that debounce, a long interactive Codex session could trigger capture repeatedly between turns.
69+
70+
## What capture auto-resolves
71+
72+
When triggered by the hook with no explicit transcript path, `capture` uses Claude transcript discovery for the current repo. V1 currently supports Claude latest-session and filtered discovery; Codex/Cursor session discovery is future work. See [[capture-flow]] for the current resolver contract.
73+
74+
## Failure behavior
75+
76+
Capture failure during a hook-triggered run produces no visible output in the ended agent session. The postmortem path is `almanac jobs`, `almanac jobs show <run-id>`, and `almanac jobs logs <run-id>` for the current wiki. `almanac hook status` only confirms the hook is wired.
77+
78+
Hook installs mutate user-level config outside the repo. That means git history answers whether the hook contract was committed, but not whether a local machine is currently wired correctly. Testing a local build can temporarily leave `~/.codex/hooks.json` pointing at the wrong event name even when repo code and tests still expect Codex `Stop`.
79+
80+
## Repo state versus machine state
81+
82+
When debugging "was the hook change committed or pushed?", inspect repo-owned hook files and user-owned config separately.
83+
84+
Repo state lives in `src/commands/hook.ts`, `hooks/almanac-capture.sh`, `test/hook.test.ts`, and this page. `git diff`, `git diff HEAD`, and `git diff origin/<branch>` over those files answer whether the hook contract changed locally, in the current commit, or on the remote branch.
85+
86+
Machine state lives in agent config under the home directory, especially `~/.codex/hooks.json`, `~/.codex/config.toml`, `~/.claude/settings.json`, and `~/.cursor/hooks.json`. Those files are outside the repo, so a bad local `hook_event_name` or stale installed script path can survive even when git shows no hook-related repo changes.

src/commands/doctor-checks/types.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,16 @@ export interface DoctorOptions {
2020
providerStatuses?: ProviderStatus[];
2121
/** Override auto-capture launchd plist path. */
2222
automationPlistPath?: string;
23+
/** Override `~/.claude/settings.json` path. */
24+
settingsPath?: string;
25+
/** Override `~/.almanac/` directory. */
26+
almanacDir?: string;
2327
/** Override `~/.claude/` directory. */
2428
claudeDir?: string;
29+
/** Override `~/.codex/` directory. */
30+
codexDir?: string;
31+
/** Override the bundled hooks directory lookup. */
32+
hookScriptPath?: string;
2533
/** Override the `codealmanac` install path detector. */
2634
installPath?: string;
2735
/** Override the reported codealmanac version. */

0 commit comments

Comments
 (0)