aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.ai/workflows/startup.org4
-rw-r--r--Makefile17
-rw-r--r--claude-templates/.ai/workflows/startup.org4
-rw-r--r--scripts/tests/install-hooks-link.bats56
-rw-r--r--todo.org13
5 files changed, 86 insertions, 8 deletions
diff --git a/.ai/workflows/startup.org b/.ai/workflows/startup.org
index e630460..2fe6827 100644
--- a/.ai/workflows/startup.org
+++ b/.ai/workflows/startup.org
@@ -48,7 +48,7 @@ Behavior:
A skill, rule, or bin script added to rulesets and pushed reaches each machine's *files* on the next pull, but not its =~/.claude= *symlink* — =make install= only links what isn't already linked, and =git pull= doesn't run it. So a newly-added skill stays silently uninstalled until someone re-runs =make install= by hand. The flush skill sat in that gap from 2026-06-02 until a manual install on 2026-06-05. Running =make install= here, right after the rulesets pull, closes it: "add a skill, commit, push" becomes enough for it to reach every machine on the next session.
-=make install= is idempotent — it skips every already-linked target, links only what's new, WARNs on a non-symlink collision, and only ever writes symlinks under =~/.claude= and =~/.local/bin=, so it's safe and reversible. It covers skills, rules, claude config, and bin scripts in one pass, so the same step also picks up a newly-added rule or script, not just a skill.
+=make install= is idempotent — it skips every already-linked target, links only what's new, WARNs on a non-symlink collision, and only ever writes symlinks under =~/.claude= and =~/.local/bin=, so it's safe and reversible. It covers skills, rules, claude config, default hooks, and bin scripts in one pass, so the same step also picks up a newly-added rule, hook, or script, not just a skill. (Hooks joined the set 2026-06-11 after =session-clear-resume.sh= sat in exactly this gap: its settings.json entry traveled via the tracked symlink on 2026-06-02, but the hook symlink itself only landed where someone ran =make install-hooks= by hand, so the hook errored silently on every =/clear= elsewhere.)
#+begin_src bash
if [ -d "$HOME/code/rulesets" ]; then
@@ -184,7 +184,7 @@ This phase touches the user and runs sequentially:
- Briefly note significant template updates noticed during sync (new workflows, protocol changes).
- *Task-review nudge.* If the Phase A staleness count (step 11) is greater than zero, surface one line: "=<N>= top-level tasks unreviewed for >7 days — say 'let's do a task review' to run a cycle." If zero, say nothing.
- *Language-bundle sync.* If the Phase A step-12 call (=sync-language-bundle.sh=) printed anything, surface it. =fixed= lines are informational — the drift was already repaired (note that =.claude/= is now dirty if the project commits it). A =drift= line on =settings.json= is surface-only and needs the printed =make install-<lang> PROJECT=.= to reconcile; flag it so the user can decide. If the call was silent, say nothing.
- - *Newly-installed symlinks.* If the Phase A.0 =make install= step printed any =link= / =relink= / =WARN= line, surface it. A =link= line means a skill, rule, or script added to rulesets is now linked into =~/.claude= for the first time on this machine. For a newly-linked *skill*, check the agent's available-skills list: if the harness already registered it mid-session, note it's available and move on; if it's absent, stop and tell Craig to restart the agent so it loads (whether a mid-session reload works is harness-version-dependent). A =WARN ... not a symlink= line is a real collision at the target path — surface it; it needs a human. If the step printed only "nothing new to link", say nothing.
+ - *Newly-installed symlinks.* If the Phase A.0 =make install= step printed any =link= / =relink= / =WARN= line, surface it. A =link= line means a skill, rule, hook, or script added to rulesets is now linked into =~/.claude= for the first time on this machine. For a newly-linked *skill*, check the agent's available-skills list: if the harness already registered it mid-session, note it's available and move on; if it's absent, stop and tell Craig to restart the agent so it loads (whether a mid-session reload works is harness-version-dependent). For a newly-linked *hook*, note that the harness reads hooks at session start — it fires from the next session (or after Craig opens =/hooks= once); its settings.json wiring travels with the tracked file, so the link is usually the only missing piece. A =WARN ... not a symlink= line is a real collision at the target path — surface it; it needs a human. If the step printed only "nothing new to link", say nothing.
- *Template-sync churn (safety net).* Check whether Phase A's rsync left uncommitted churn in the synced =.ai/= paths — accumulated from a prior session that crashed before wrap-up, or freshly added this session when rulesets advanced. Without surfacing, it builds up silently until it blocks Phase A.0's auto-ff (git won't ff a dirty tree). Skip in the rulesets repo itself (there =.ai/= is a committed mirror, kept honest by the pre-commit hook). The check is sequential here, after the rsync has finished — not a Phase A step, to keep that batch race-free.
#+begin_src bash
diff --git a/Makefile b/Makefile
index 33c582f..5ff155d 100644
--- a/Makefile
+++ b/Makefile
@@ -170,8 +170,8 @@ bootstrap: ## First-time machine setup: install + install-hooks + install-mcp
##@ Global install (symlinks into ~/.claude/)
-install: ## Symlink skills and rules into ~/.claude/
- @mkdir -p $(SKILLS_DIR) $(RULES_DIR)
+install: ## Symlink skills, rules, config, hooks, and bin scripts into place
+ @mkdir -p $(SKILLS_DIR) $(RULES_DIR) $(HOOKS_DIR)
@echo "Skills:"
@for skill in $(SKILLS); do \
if [ -L "$(SKILLS_DIR)/$$skill" ]; then \
@@ -220,6 +220,19 @@ install: ## Symlink skills and rules into ~/.claude/
fi \
fi
@echo ""
+ @echo "Hooks (default):"
+ @for hook in $(DEFAULT_HOOKS); do \
+ name=$$(basename $$hook); \
+ if [ -L "$(HOOKS_DIR)/$$name" ]; then \
+ echo " skip $$name (already linked)"; \
+ elif [ -e "$(HOOKS_DIR)/$$name" ]; then \
+ echo " WARN $$name exists and is not a symlink — skipping"; \
+ else \
+ ln -s "$(CURDIR)/$$hook" "$(HOOKS_DIR)/$$name"; \
+ echo " link $$name → $(HOOKS_DIR)/$$name"; \
+ fi \
+ done
+ @echo ""
@echo "bin scripts:"
@mkdir -p "$(LOCAL_BIN)"
@for src in $(CURDIR)/claude-templates/bin/*; do \
diff --git a/claude-templates/.ai/workflows/startup.org b/claude-templates/.ai/workflows/startup.org
index e630460..2fe6827 100644
--- a/claude-templates/.ai/workflows/startup.org
+++ b/claude-templates/.ai/workflows/startup.org
@@ -48,7 +48,7 @@ Behavior:
A skill, rule, or bin script added to rulesets and pushed reaches each machine's *files* on the next pull, but not its =~/.claude= *symlink* — =make install= only links what isn't already linked, and =git pull= doesn't run it. So a newly-added skill stays silently uninstalled until someone re-runs =make install= by hand. The flush skill sat in that gap from 2026-06-02 until a manual install on 2026-06-05. Running =make install= here, right after the rulesets pull, closes it: "add a skill, commit, push" becomes enough for it to reach every machine on the next session.
-=make install= is idempotent — it skips every already-linked target, links only what's new, WARNs on a non-symlink collision, and only ever writes symlinks under =~/.claude= and =~/.local/bin=, so it's safe and reversible. It covers skills, rules, claude config, and bin scripts in one pass, so the same step also picks up a newly-added rule or script, not just a skill.
+=make install= is idempotent — it skips every already-linked target, links only what's new, WARNs on a non-symlink collision, and only ever writes symlinks under =~/.claude= and =~/.local/bin=, so it's safe and reversible. It covers skills, rules, claude config, default hooks, and bin scripts in one pass, so the same step also picks up a newly-added rule, hook, or script, not just a skill. (Hooks joined the set 2026-06-11 after =session-clear-resume.sh= sat in exactly this gap: its settings.json entry traveled via the tracked symlink on 2026-06-02, but the hook symlink itself only landed where someone ran =make install-hooks= by hand, so the hook errored silently on every =/clear= elsewhere.)
#+begin_src bash
if [ -d "$HOME/code/rulesets" ]; then
@@ -184,7 +184,7 @@ This phase touches the user and runs sequentially:
- Briefly note significant template updates noticed during sync (new workflows, protocol changes).
- *Task-review nudge.* If the Phase A staleness count (step 11) is greater than zero, surface one line: "=<N>= top-level tasks unreviewed for >7 days — say 'let's do a task review' to run a cycle." If zero, say nothing.
- *Language-bundle sync.* If the Phase A step-12 call (=sync-language-bundle.sh=) printed anything, surface it. =fixed= lines are informational — the drift was already repaired (note that =.claude/= is now dirty if the project commits it). A =drift= line on =settings.json= is surface-only and needs the printed =make install-<lang> PROJECT=.= to reconcile; flag it so the user can decide. If the call was silent, say nothing.
- - *Newly-installed symlinks.* If the Phase A.0 =make install= step printed any =link= / =relink= / =WARN= line, surface it. A =link= line means a skill, rule, or script added to rulesets is now linked into =~/.claude= for the first time on this machine. For a newly-linked *skill*, check the agent's available-skills list: if the harness already registered it mid-session, note it's available and move on; if it's absent, stop and tell Craig to restart the agent so it loads (whether a mid-session reload works is harness-version-dependent). A =WARN ... not a symlink= line is a real collision at the target path — surface it; it needs a human. If the step printed only "nothing new to link", say nothing.
+ - *Newly-installed symlinks.* If the Phase A.0 =make install= step printed any =link= / =relink= / =WARN= line, surface it. A =link= line means a skill, rule, hook, or script added to rulesets is now linked into =~/.claude= for the first time on this machine. For a newly-linked *skill*, check the agent's available-skills list: if the harness already registered it mid-session, note it's available and move on; if it's absent, stop and tell Craig to restart the agent so it loads (whether a mid-session reload works is harness-version-dependent). For a newly-linked *hook*, note that the harness reads hooks at session start — it fires from the next session (or after Craig opens =/hooks= once); its settings.json wiring travels with the tracked file, so the link is usually the only missing piece. A =WARN ... not a symlink= line is a real collision at the target path — surface it; it needs a human. If the step printed only "nothing new to link", say nothing.
- *Template-sync churn (safety net).* Check whether Phase A's rsync left uncommitted churn in the synced =.ai/= paths — accumulated from a prior session that crashed before wrap-up, or freshly added this session when rulesets advanced. Without surfacing, it builds up silently until it blocks Phase A.0's auto-ff (git won't ff a dirty tree). Skip in the rulesets repo itself (there =.ai/= is a committed mirror, kept honest by the pre-commit hook). The check is sequential here, after the rsync has finished — not a Phase A step, to keep that batch race-free.
#+begin_src bash
diff --git a/scripts/tests/install-hooks-link.bats b/scripts/tests/install-hooks-link.bats
new file mode 100644
index 0000000..80ac8dd
--- /dev/null
+++ b/scripts/tests/install-hooks-link.bats
@@ -0,0 +1,56 @@
+#!/usr/bin/env bats
+# make install must link default hooks into HOOKS_DIR. The gap this guards:
+# install-hooks was a separate target nobody re-ran, so a hook added to the
+# repo (session-clear-resume.sh, 2026-06-02) reached settings.json on every
+# machine via the tracked symlink but its ~/.claude/hooks/ link never landed,
+# and the hook errored silently on every /clear until a manual install.
+
+setup() {
+ REPO_ROOT="$(cd "$(dirname "$BATS_TEST_FILENAME")/../.." && pwd)"
+ TMPHOME="$(mktemp -d)"
+}
+
+teardown() {
+ rm -rf "$TMPHOME"
+}
+
+run_install() {
+ make -C "$REPO_ROOT" install \
+ SKILLS_DIR="$TMPHOME/skills" \
+ RULES_DIR="$TMPHOME/rules" \
+ HOOKS_DIR="$TMPHOME/hooks" \
+ CLAUDE_DIR="$TMPHOME/claude" \
+ LOCAL_BIN="$TMPHOME/bin"
+}
+
+@test "install links default hooks into HOOKS_DIR" {
+ run run_install
+ [ "$status" -eq 0 ]
+ [ -L "$TMPHOME/hooks/session-clear-resume.sh" ]
+ [ -L "$TMPHOME/hooks/precompact-priorities.sh" ]
+}
+
+@test "install does not link opt-in hooks" {
+ run run_install
+ [ "$status" -eq 0 ]
+ [ ! -e "$TMPHOME/hooks/destructive-bash-confirm.py" ]
+}
+
+@test "install is idempotent on hooks (second run skips, link survives)" {
+ run run_install
+ [ "$status" -eq 0 ]
+ run run_install
+ [ "$status" -eq 0 ]
+ [[ "$output" == *"skip session-clear-resume.sh (already linked)"* ]]
+ [ -L "$TMPHOME/hooks/session-clear-resume.sh" ]
+}
+
+@test "install warns on a non-symlink collision and leaves the file alone" {
+ mkdir -p "$TMPHOME/hooks"
+ echo "real file" > "$TMPHOME/hooks/session-clear-resume.sh"
+ run run_install
+ [ "$status" -eq 0 ]
+ [[ "$output" == *"WARN session-clear-resume.sh exists and is not a symlink"* ]]
+ [ ! -L "$TMPHOME/hooks/session-clear-resume.sh" ]
+ grep -q "real file" "$TMPHOME/hooks/session-clear-resume.sh"
+}
diff --git a/todo.org b/todo.org
index 30fd7e0..bae9d09 100644
--- a/todo.org
+++ b/todo.org
@@ -42,14 +42,23 @@ The org-table standard keeps project-doc tables <=120 cols with multi-line wrapp
Out of a work-project handoff 2026-06-09.
-** TODO [#C] SessionStart-on-clear hook for auto-resume :feature:
+** DONE [#C] SessionStart-on-clear hook for auto-resume :feature:
+CLOSED: [2026-06-11 Thu]
:PROPERTIES:
-:LAST_REVIEWED: 2026-06-10
+:LAST_REVIEWED: 2026-06-11
:END:
Add a SessionStart hook (matcher: clear) in settings.json that auto-injects "read .ai/session-context.org and resume if present, else run startup.org". Today /flush prompts the user to /clear and the next session relies on the model re-reading session-context; the hook makes resume automatic on /clear. Keep full startup.org for genuine fresh starts (new day, other machine, been away). Likely lands as claude-templates workflow notes plus the hook in settings.json.
The checkpoint+resume halves already shipped as /flush. This is the remaining automation piece. Out of a work-project handoff 2026-06-09 (process tooling, belongs in rulesets not the work project).
+Resolution 2026-06-11: the hook itself had already shipped 2026-06-02 (hooks/session-clear-resume.sh + the SessionStart clear entry in the tracked settings.json — this task duplicated it). What was actually broken: make install didn't cover hooks, so the symlink never reached machines that hadn't run make install-hooks by hand, and the hook errored silently on every /clear. Fixed by folding default-hook linking into make install (startup's Phase A.0 now propagates hooks machine-wide), with bats coverage in scripts/tests/install-hooks-link.bats. Both hook branches verified on ratio; the live /clear fire is a one-keystroke manual test.
+*** TODO Manual testing and validation :test:
+**** /clear mid-session resumes from the anchor
+What we're verifying: the SessionStart(clear) hook fires and the fresh context resumes instead of cold-starting.
+- In any project session with a live .ai/session-context.org (this rulesets session qualifies), type /clear
+- Send any short message (the injected context loads but the model waits for your next keystroke)
+Expected: the reply starts with "flushed." on its own line, restates the Active Goal and immediate Next Step, and does NOT run the startup workflow.
+
** DOING [#C] Check that memories are sync'd across machines via git :spec:
:PROPERTIES:
:LAST_REVIEWED: 2026-06-10