claude-code-arcane 2.13.0 → 2.15.0

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/CHANGELOG.md CHANGED
@@ -1,3 +1,19 @@
1
+ # [2.15.0](https://github.com/SebastianLuser/claude-code-arcane/compare/v2.14.0...v2.15.0) (2026-10-16)
2
+
3
+
4
+ ### Features
5
+
6
+ * **hooks:** watch the session for loops, scope and unchecked edits ([84d7c3e](https://github.com/SebastianLuser/claude-code-arcane/commit/84d7c3eccc33380f27d7f8901affedb090ee21d5))
7
+ * **installer:** wire the session watch and feed it the real context window ([615d212](https://github.com/SebastianLuser/claude-code-arcane/commit/615d212d96744c180fe9f84f7670382ec58d2772))
8
+
9
+ # [2.14.0](https://github.com/SebastianLuser/claude-code-arcane/compare/v2.13.0...v2.14.0) (2026-10-16)
10
+
11
+
12
+ ### Features
13
+
14
+ * **hooks:** leave finished worktrees and tell a session where it is ([df0531f](https://github.com/SebastianLuser/claude-code-arcane/commit/df0531feef6801649682f8f21dfcf36de006851b))
15
+ * **session:** keep the progress file in .claude/session-state ([0615ce5](https://github.com/SebastianLuser/claude-code-arcane/commit/0615ce5289751044a749a034b1ef99bcbfd7e058))
16
+
1
17
  # [2.13.0](https://github.com/SebastianLuser/claude-code-arcane/compare/v2.12.0...v2.13.0) (2026-10-16)
2
18
 
3
19
 
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Claude Code Arcane
2
2
 
3
- > **437 skills, 115 agents, 16 hooks and 36 rules for Claude Code — selective deploy by profile.**
3
+ > **437 skills, 115 agents, 18 hooks and 36 rules for Claude Code — selective deploy by profile.**
4
4
 
5
5
  A configuration harness installable via `npx`. Pick a **profile** that matches your stack and only the relevant tools get installed into your project's `.claude/` directory.
6
6
 
@@ -81,7 +81,7 @@ Know who to call for what:
81
81
 
82
82
  #### Progress Tracking
83
83
 
84
- Maintain `production/session-state/active.md` with:
84
+ Maintain `.claude/session-state/active.md` with:
85
85
 
86
86
  ```markdown
87
87
  ## VN Production State
@@ -122,7 +122,7 @@ Maintain `production/session-state/active.md` with:
122
122
  | Audio asset gaps | Medium | Medium | Placeholder tracks, generate late |
123
123
 
124
124
  ### File Ownership
125
- - `production/session-state/active.md` — session state
125
+ - `.claude/session-state/active.md` — session state
126
126
  - `production/milestones/` — milestone definitions and status
127
127
  - `design/assets/asset-manifest.md` — asset pipeline tracking (shared with vn-asset-pipeline)
128
128
 
package/dist/cli.js CHANGED
@@ -880,16 +880,19 @@ var FILE_TOOLS_MATCHER = "Write|Edit|MultiEdit|NotebookEdit";
880
880
  var LEGACY_COMMAND = /^bash \.claude\/hooks\/([\w.-]+\.sh)$/;
881
881
  var ARCANE_COMMAND = /^bash "?(?:\$CLAUDE_PROJECT_DIR\/|[^"]*\/)?\.claude\/hooks\/([\w.-]+\.sh)"?$/;
882
882
  var RETIRED_SCRIPTS = /* @__PURE__ */ new Set(["validate-push.sh", "validate-secrets.sh"]);
883
- function hookCommand(target, script) {
883
+ function hookCommand(target, script, args = "") {
884
+ const suffix = args ? ` ${args}` : "";
884
885
  if (path8.resolve(target) === path8.resolve(os2.homedir())) {
885
886
  const absolute = path8.join(target, ".claude", "hooks", script).replace(/\\/g, "/");
886
- return `bash "${absolute}"`;
887
+ return `bash "${absolute}"${suffix}`;
887
888
  }
888
- return `bash "$CLAUDE_PROJECT_DIR/.claude/hooks/${script}"`;
889
+ return `bash "$CLAUDE_PROJECT_DIR/.claude/hooks/${script}"${suffix}`;
889
890
  }
890
- function hook(target, script, timeout) {
891
- return { type: "command", command: hookCommand(target, script), timeout };
891
+ function hook(target, script, timeout, args = "") {
892
+ return { type: "command", command: hookCommand(target, script, args), timeout };
892
893
  }
894
+ var WATCH_POST_TOOL = ["run-py.sh", "session_watch.py post-tool"];
895
+ var WATCH_STOP = ["run-py.sh", "session_watch.py stop"];
893
896
  function defaultHooks(target) {
894
897
  const one = (script, timeout) => [
895
898
  { matcher: "", hooks: [hook(target, script, timeout)] }
@@ -921,12 +924,22 @@ function defaultHooks(target) {
921
924
  {
922
925
  matcher: "Write|Edit",
923
926
  hooks: [hook(target, "validate-assets.sh", 10), hook(target, "validate-skill-change.sh", 5)]
924
- }
927
+ },
928
+ { matcher: "", hooks: [hook(target, WATCH_POST_TOOL[0], 10, WATCH_POST_TOOL[1])] }
925
929
  ],
926
930
  Notification: one("notify.sh", 10),
927
931
  PreCompact: one("pre-compact.sh", 10),
928
932
  PostCompact: one("post-compact.sh", 10),
929
- Stop: one("session-stop.sh", 10),
933
+ Stop: [
934
+ {
935
+ matcher: "",
936
+ hooks: [
937
+ hook(target, "session-stop.sh", 10),
938
+ hook(target, "worktree-stop.sh", 10),
939
+ hook(target, WATCH_STOP[0], 120, WATCH_STOP[1])
940
+ ]
941
+ }
942
+ ],
930
943
  SubagentStart: one("log-agent.sh", 5),
931
944
  SubagentStop: one("log-agent-stop.sh", 5)
932
945
  };
@@ -973,8 +986,21 @@ function healHooks(hooks, target) {
973
986
  pre.push({ matcher: FILE_TOOLS_MATCHER, hooks: [hook(target, "guard.sh", 10)] });
974
987
  changes.push("guard for file edits");
975
988
  }
989
+ ensureHook(hooks, "Stop", hook(target, "worktree-stop.sh", 10), "worktree exit on stop", changes);
990
+ ensureHook(hooks, "Stop", hook(target, WATCH_STOP[0], 120, WATCH_STOP[1]), "format and type check on stop", changes);
991
+ ensureHook(hooks, "PostToolUse", hook(target, WATCH_POST_TOOL[0], 10, WATCH_POST_TOOL[1]), "session watch", changes);
976
992
  return changes;
977
993
  }
994
+ function ensureHook(hooks, event, entry, label, changes) {
995
+ const signature = entry.command.replace(/^bash "[^"]*\/hooks\//, "");
996
+ const groups = hooks[event] ??= [];
997
+ const present = groups.some((g) => g.hooks?.some((h) => h.command?.endsWith(signature)));
998
+ if (present) return;
999
+ const group = groups.find((g) => !g.matcher);
1000
+ if (group) group.hooks = [...group.hooks ?? [], entry];
1001
+ else groups.push({ matcher: "", hooks: [entry] });
1002
+ changes.push(label);
1003
+ }
978
1004
 
979
1005
  // src/installer.ts
980
1006
  var SETTINGS_TEMPLATE = {
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## What is Arcane?
4
4
 
5
- Arcane is a configuration harness for [Claude Code](https://docs.anthropic.com/en/docs/claude-code). It provides a curated library of **437 skills**, **115 agents**, **16 hooks**, and **36 rules** that you selectively deploy into any project. Instead of shipping everything to every project (wasting tokens and degrading performance), you pick a **profile** that matches your stack and only the relevant tools get installed.
5
+ Arcane is a configuration harness for [Claude Code](https://docs.anthropic.com/en/docs/claude-code). It provides a curated library of **437 skills**, **115 agents**, **18 hooks**, and **36 rules** that you selectively deploy into any project. Instead of shipping everything to every project (wasting tokens and degrading performance), you pick a **profile** that matches your stack and only the relevant tools get installed.
6
6
 
7
7
  ## Table of Contents
8
8
 
@@ -239,7 +239,7 @@ Every installation automatically loads `core.yaml` first:
239
239
  |----------|----------|
240
240
  | **Skills (21)** | commit, create-pr, changelog, check, code-review, context-prime, help, start, fix-issue, hotfix, brainstorm, scope-check, reverse-document, skill-improve, skill-test, tech-debt, arcane-status, arcane-list, arcane-add, arcane-remove, arcane-clean |
241
241
  | **Rules (8)** | data-files, prototype-code, test-standards, implementation-workflow, code-comments, verification, agent-delegation, git-worktrees |
242
- | **Hooks (15)** | session-start, detect-division, detect-unreal, detect-gaps, check-update, guard, validate-commit, validate-assets, validate-skill-change, notify, pre-compact, post-compact, session-stop, log-agent, log-agent-stop |
242
+ | **Hooks (17)** | session-start, detect-division, detect-unreal, detect-gaps, check-update, guard, validate-commit, validate-assets, validate-skill-change, notify, pre-compact, post-compact, session-stop, worktree-stop, run-py (session_watch), log-agent, log-agent-stop |
243
243
  | **Agents (1 dir)** | quality |
244
244
  | **Permissions** | 15 allow (git, ls, gh pr/issue/run and read-only gh, curl) + 13 deny (force push, reset --hard, clean -f, sudo, .env, etc.) |
245
245
 
@@ -311,7 +311,7 @@ Para documentos largos (ADRs, PRDs, GDDs, RFCs) escribí sección por sección a
311
311
  - Review con el usuario
312
312
  - Iterar hasta aprobación
313
313
  - Escribir ESA sección al archivo
314
- - Update production/session-state/active.md
314
+ - Update .claude/session-state/active.md
315
315
  - La conversación de esa sección puede compactarse — la decisión ya está en disco
316
316
 
317
317
  3. Si la sesión crashea mid-document:
@@ -16,7 +16,7 @@ Claude Code tiene una ventana de contexto limitada. Sesiones largas llegan al l
16
16
 
17
17
  ### Session State File
18
18
 
19
- Mantené `production/session-state/active.md` como checkpoint vivo. Update después de cada milestone significativo:
19
+ Mantené `.claude/session-state/active.md` como checkpoint vivo. Update después de cada milestone significativo:
20
20
 
21
21
  - Sección de diseño / ADR aprobada y escrita
22
22
  - Decisión arquitectónica tomada
@@ -25,23 +25,6 @@ Mantené `production/session-state/active.md` como checkpoint vivo. Update despu
25
25
 
26
26
  El state file debe contener: tarea actual, checklist de progreso, decisiones clave tomadas, archivos en trabajo, preguntas abiertas.
27
27
 
28
- ### Status Line Block (opcional, para proyectos activos)
29
-
30
- Para proyectos en fase de ejecución, incluir en `active.md` un bloque estructurado que el status line script puede parsear:
31
-
32
- ```markdown
33
- <!-- STATUS -->
34
- Epic: Auth refactor
35
- Feature: JWT rotation
36
- Task: Implementar refresh endpoint
37
- <!-- /STATUS -->
38
- ```
39
-
40
- - Los tres campos (Epic, Feature, Task) son opcionales — incluí solo lo aplicable
41
- - Update cuando cambiás de foco
42
- - El status line lo muestra como breadcrumb: `Auth refactor > JWT rotation > Refresh endpoint`
43
- - Remove o vaciá cuando no hay work focus activo
44
-
45
28
  Después de cualquier disrupción (compaction, crash, `/clear`), leé el state file primero.
46
29
 
47
30
  ---
@@ -65,14 +48,15 @@ Esto mantiene el context window con ~3-5k tokens (solo la sección actual) en ve
65
48
  ## Proactive Compaction
66
49
 
67
50
  - **Compactá proactivamente** a ~60-70% de uso, no reactivamente al límite
51
+ - **Aviso automático:** con el profile `statusline`, `session_watch.py` avisa a Claude al 70% y al 85% de la ventana real (la statusline anota el tamaño que reporta Claude Code en `.claude/session-state/context.json`). Sin statusline no avisa: adivinar la ventana haría saltar el aviso demasiado temprano en sesiones de 1M
68
52
  - **Usá `/clear`** entre tareas no relacionadas, o después de 2+ intentos de corrección fallidos
69
53
  - **Puntos naturales de compaction:** después de escribir una sección al archivo, después de un commit, después de completar una tarea, antes de empezar un tema nuevo
70
54
  - **Focused compaction:** `/compact Focus on [tarea actual] — secciones 1-3 ya escritas al archivo, working on sección 4`
71
55
 
72
56
  ### Compaction Hooks
73
57
 
74
- - `pre-compact.sh`: Guarda summary del estado antes de compaction
75
- - `post-compact.sh`: Carga summary después de compaction
58
+ - `pre-compact.sh` y `post-compact.sh` solo anotan la hora en `.claude/session-state/sessions.log`. No guardan ni cargan estado.
59
+ - Lo que sobrevive a la compactación es el archivo: `session-start.sh` corre también después de cada compactación (SessionStart con matcher vacío) y muestra el inicio de `active.md`. Por eso `active.md` tiene que estar al día **antes** de compactar.
76
60
 
77
61
  ---
78
62
 
@@ -111,7 +95,7 @@ Los subagents no heredan historia de conversación — dales contexto completo e
111
95
 
112
96
  Cuando el contexto se compacta, preservá en el summary:
113
97
 
114
- - Referencia a `production/session-state/active.md` (leer para recover state)
98
+ - Referencia a `.claude/session-state/active.md` (leer para recover state)
115
99
  - Lista de archivos modificados en la sesión y su propósito
116
100
  - Decisiones arquitectónicas hechas y su rationale
117
101
  - Tasks activas del sprint y estado actual
@@ -163,7 +147,7 @@ Cuando el status line muestra >= 70%:
163
147
  ```
164
148
  Tarea: "Implementá auth JWT completo"
165
149
 
166
- 1. Crear production/session-state/active.md con:
150
+ 1. Crear .claude/session-state/active.md con:
167
151
  - Feature: JWT auth
168
152
  - Files to create: src/auth/{tokens.ts, middleware.ts, login.ts}
169
153
  - Tests: tests/auth.test.ts
@@ -71,7 +71,7 @@ Ningún agente hace commits sin instrucción explícita del user. Ni siquiera el
71
71
 
72
72
  ## Regla #9 — Session State
73
73
 
74
- Actualizar `production/session-state/active.md` cuando:
74
+ Actualizar `.claude/session-state/active.md` cuando:
75
75
  - Se completa un section/phase
76
76
  - Se toma una decisión significativa
77
77
  - Se cambia de división/agente
@@ -97,9 +97,9 @@ claude-code-arcane/
97
97
  │ ├── statusline.sh # Solo con el profile statusline
98
98
  │ ├── output-styles/ # Solo si algún profile declara output_styles
99
99
  │ ├── docs/ # Copia de docs/; arcane update la mantiene al día
100
+ │ ├── session-state/ # active.md y logs de sesión; se ignora solo (.gitignore con *)
100
101
  │ └── .arcane-trash/ # Lo que arcane update saca; nunca se borra con rm
101
- ├── .claude.bak/ # Backup de .claude/ antes de instalar
102
- └── production/session-state/ # Estado de sesión que escriben los hooks
102
+ └── .claude.bak/ # Backup de .claude/ antes de instalar
103
103
  ```
104
104
 
105
105
  Fuera del proyecto, `~/.arcane/` guarda el registro de instalaciones (`installations.json`) y el contenido descargado de GitHub (`cache/<version>/`). `ARCANE_HOME` lo desvía.
@@ -125,9 +125,10 @@ Los eventos los define `src/installer.ts` al generar `settings.json`, no los pro
125
125
  | PreToolUse (`Bash`) | `guard.sh` → `validate-commit.sh` |
126
126
  | PreToolUse (`Write\|Edit\|MultiEdit\|NotebookEdit`) | `guard.sh` |
127
127
  | PostToolUse (`Write\|Edit`) | `validate-assets.sh`, `validate-skill-change.sh` |
128
+ | PostToolUse (todas) | `run-py.sh session_watch.py post-tool` (ediciones, loops, alcance, aviso de compactación) |
128
129
  | Notification | `notify.sh` |
129
130
  | PreCompact / PostCompact | `pre-compact.sh` / `post-compact.sh` |
130
- | Stop | `session-stop.sh` |
131
+ | Stop | `session-stop.sh`, `worktree-stop.sh`, `run-py.sh session_watch.py stop` (format + type check de lo editado) |
131
132
  | SubagentStart / SubagentStop | `log-agent.sh` / `log-agent-stop.sh` |
132
133
 
133
134
  Cada comando es `bash "$CLAUDE_PROJECT_DIR/.claude/hooks/<script>"`: dentro de un worktree los hooks corren parados en el worktree, que puede no tener `.claude/hooks/` commiteado, y la variable siempre apunta al proyecto donde arrancó la sesión. `arcane update` migra las instalaciones que tenían la ruta relativa.
@@ -123,8 +123,8 @@ Después de que un gate resuelve, registrar el verdict en el header de status de
123
123
  ```
124
124
 
125
125
  Para phase gates, registrar en:
126
- - Software: `docs/architecture/architecture.md` o `production/session-state/active.md`
127
- - Gamedev: `docs/architecture/architecture.md` o `production/session-state/active.md`
126
+ - Software: `docs/architecture/architecture.md` o `.claude/session-state/active.md`
127
+ - Gamedev: `docs/architecture/architecture.md` o `.claude/session-state/active.md`
128
128
 
129
129
  ---
130
130
 
@@ -22,7 +22,7 @@ main() {
22
22
  DIVISION="engineering (Python)"
23
23
  fi
24
24
 
25
- [[ -d "production/session-state" ]] && echo "Division: $DIVISION" > production/session-state/division.txt
25
+ [[ -d ".claude/session-state" ]] && echo "Division: $DIVISION" > .claude/session-state/division.txt
26
26
  }
27
27
  main 2>/dev/null
28
28
  exit 0
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env bash
2
2
  set +e
3
3
  main() {
4
- [[ -d production/session-state ]] && echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] subagent_stop" >> production/session-state/agents.log
4
+ [[ -d .claude/session-state ]] && echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] subagent_stop" >> .claude/session-state/agents.log
5
5
  }
6
6
  main 2>/dev/null
7
7
  exit 0
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env bash
2
2
  set +e
3
3
  main() {
4
- [[ -d production/session-state ]] && echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] subagent_start" >> production/session-state/agents.log
4
+ [[ -d .claude/session-state ]] && echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] subagent_start" >> .claude/session-state/agents.log
5
5
  }
6
6
  main 2>/dev/null
7
7
  exit 0
package/hooks/notify.sh CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env bash
2
2
  set +e
3
3
  main() {
4
- [[ -d production/session-state ]] && echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] notification" >> production/session-state/notifications.log
4
+ [[ -d .claude/session-state ]] && echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] notification" >> .claude/session-state/notifications.log
5
5
  }
6
6
  main 2>/dev/null
7
7
  exit 0
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env bash
2
2
  set +e
3
3
  main() {
4
- [[ -d production/session-state ]] && echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] post-compact" >> production/session-state/sessions.log
4
+ [[ -d .claude/session-state ]] && echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] post-compact" >> .claude/session-state/sessions.log
5
5
  }
6
6
  main 2>/dev/null
7
7
  exit 0
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env bash
2
2
  set +e
3
3
  main() {
4
- [[ -d production/session-state ]] && echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] pre-compact" >> production/session-state/sessions.log
4
+ [[ -d .claude/session-state ]] && echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] pre-compact" >> .claude/session-state/sessions.log
5
5
  }
6
6
  main 2>/dev/null
7
7
  exit 0
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env bash
2
+ # Runs a Python hook from this folder: run-py.sh <script.py> [args...]
3
+ #
4
+ # Uses the first Python that actually starts (on Windows `python3` can be the
5
+ # Microsoft Store stub, which exits non-zero instead of running). With no working
6
+ # Python the hook does nothing.
7
+ DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
8
+ SCRIPT="$1"
9
+ shift
10
+ [ -f "$DIR/$SCRIPT" ] || exit 0
11
+ INPUT=$(cat)
12
+
13
+ for PY in python3 python py; do
14
+ command -v "$PY" >/dev/null 2>&1 || continue
15
+ if OUT=$(printf '%s' "$INPUT" | "$PY" "$DIR/$SCRIPT" "$@" 2>/dev/null); then
16
+ printf '%s' "$OUT"
17
+ exit 0
18
+ fi
19
+ done
20
+ exit 0
@@ -3,13 +3,49 @@
3
3
  # Graceful: wraps in main(), never blocks session
4
4
  set +e
5
5
 
6
- main() {
7
- mkdir -p production/session-state 2>/dev/null
6
+ STATE_DIR=".claude/session-state"
7
+ LEGACY_STATE_DIR="production/session-state"
8
+
9
+ # The folder holds session notes, never project files: a .gitignore with `*` keeps it
10
+ # out of every commit without touching the project's own .gitignore.
11
+ ensure_state_dir() {
12
+ mkdir -p "$STATE_DIR" 2>/dev/null
13
+ [ -f "$STATE_DIR/.gitignore" ] || echo '*' > "$STATE_DIR/.gitignore" 2>/dev/null
14
+ }
15
+
16
+ # active.md used to live in production/session-state/. Move it once, so a task left
17
+ # half done before the update still shows up here.
18
+ migrate_legacy_state() {
19
+ [ -f "$LEGACY_STATE_DIR/active.md" ] || return 0
20
+ [ -f "$STATE_DIR/active.md" ] && return 0
21
+ ensure_state_dir
22
+ mv "$LEGACY_STATE_DIR/active.md" "$STATE_DIR/active.md" 2>/dev/null &&
23
+ echo "Moved $LEGACY_STATE_DIR/active.md to $STATE_DIR/active.md"
24
+ }
8
25
 
9
- SESSION_LOG="production/session-state/sessions.log"
10
- echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] Session started" >> "$SESSION_LOG" 2>/dev/null
26
+ # A session started inside a worktree should know it, and where the main checkout is.
27
+ worktree_notice() {
28
+ local DIRS GIT_DIR COMMON_DIR TOP BRANCH MAIN MAIN_BRANCH
29
+ DIRS=$(git rev-parse --path-format=absolute --git-dir --git-common-dir 2>/dev/null) || return 0
30
+ GIT_DIR=$(echo "$DIRS" | sed -n 1p)
31
+ COMMON_DIR=$(echo "$DIRS" | sed -n 2p)
32
+ [ -z "$COMMON_DIR" ] || [ "$GIT_DIR" = "$COMMON_DIR" ] && return 0
33
+ TOP=$(git rev-parse --show-toplevel 2>/dev/null)
34
+ BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)
35
+ MAIN=$(dirname "$COMMON_DIR")
36
+ MAIN_BRANCH=$(git -C "$MAIN" rev-parse --abbrev-ref HEAD 2>/dev/null)
37
+ echo "Worktree: you are in $TOP on branch $BRANCH."
38
+ echo "Main checkout: $MAIN on branch $MAIN_BRANCH. See the git-worktrees rule before pulling or switching branches."
39
+ echo ""
40
+ }
41
+
42
+ main() {
43
+ migrate_legacy_state
44
+ [ -d "$STATE_DIR" ] && ensure_state_dir
45
+ [ -d "$STATE_DIR" ] && echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] Session started" >> "$STATE_DIR/sessions.log" 2>/dev/null
11
46
 
12
47
  echo "=== Claude Code Arcane — Session Context ==="
48
+ worktree_notice
13
49
 
14
50
  BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)
15
51
  if [ -n "$BRANCH" ]; then
@@ -55,7 +91,7 @@ main() {
55
91
  fi
56
92
  fi
57
93
 
58
- STATE_FILE="production/session-state/active.md"
94
+ STATE_FILE="$STATE_DIR/active.md"
59
95
  if [ -f "$STATE_FILE" ]; then
60
96
  echo ""
61
97
  echo "=== ACTIVE SESSION STATE DETECTED ==="
@@ -1,7 +1,11 @@
1
1
  #!/usr/bin/env bash
2
2
  set +e
3
3
  main() {
4
- [[ -d production/session-state ]] && echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] Session stopped" >> production/session-state/sessions.log
4
+ [[ -d .claude/session-state ]] || return 0
5
+ # Claude creates the folder the first time it writes active.md; keep it out of
6
+ # commits from that same turn on.
7
+ [[ -f .claude/session-state/.gitignore ]] || echo '*' > .claude/session-state/.gitignore
8
+ echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] Session stopped" >> .claude/session-state/sessions.log
5
9
  }
6
10
  main 2>/dev/null
7
11
  exit 0
@@ -0,0 +1,278 @@
1
+ #!/usr/bin/env python3
2
+ """Session watch: cheap, deterministic signals the model should not have to remember.
3
+
4
+ `session_watch.py post-tool` (PostToolUse, every tool):
5
+ - records each file Claude edits, for the Stop check below;
6
+ - warns when the same tool runs with the same input 5 times in a row (a loop);
7
+ - warns once when a session has edited 15 files (scope drift);
8
+ - warns when the context passes 70% and 85% of the real window, so active.md is
9
+ brought up to date before a compaction. The window size comes from the status
10
+ line (statusline.sh caches what Claude Code reports); without it, no warning,
11
+ since guessing the window would warn far too early on 1M-token sessions.
12
+
13
+ `session_watch.py stop` (Stop): formats the files edited this turn and type-checks
14
+ them with the project's own tools (Prettier or Biome + tsc, gofmt + go vet,
15
+ dotnet format). Only errors in files edited this turn block the stop, once, so
16
+ errors the project already had never trap a session.
17
+
18
+ State lives in .claude/session-state/watch-<session>.json, which ignores itself.
19
+ """
20
+ from __future__ import annotations
21
+
22
+ import hashlib
23
+ import json
24
+ import os
25
+ import re
26
+ import shutil
27
+ import subprocess
28
+ import sys
29
+
30
+ LOOP_LENGTH = 5
31
+ SCOPE_FILES = 15
32
+ CONTEXT_WARNINGS = (70, 85)
33
+ FILE_TOOLS = {"Write", "Edit", "MultiEdit", "NotebookEdit"}
34
+ IGNORED_INPUT_KEYS = {"description", "timeout"}
35
+ CHECK_TIMEOUT = 90
36
+ MAX_REPORT_LINES = 25
37
+
38
+
39
+ # --- state -------------------------------------------------------------------
40
+
41
+ def state_dir(project: str) -> str:
42
+ path = os.path.join(project, ".claude", "session-state")
43
+ os.makedirs(path, exist_ok=True)
44
+ ignore = os.path.join(path, ".gitignore")
45
+ if not os.path.exists(ignore):
46
+ with open(ignore, "w", encoding="utf-8") as f:
47
+ f.write("*\n")
48
+ return path
49
+
50
+
51
+ def load_state(project: str, session: str) -> dict:
52
+ path = os.path.join(state_dir(project), f"watch-{safe(session)}.json")
53
+ try:
54
+ with open(path, encoding="utf-8") as f:
55
+ return json.load(f)
56
+ except (OSError, ValueError):
57
+ return {}
58
+
59
+
60
+ def save_state(project: str, session: str, state: dict) -> None:
61
+ path = os.path.join(state_dir(project), f"watch-{safe(session)}.json")
62
+ with open(path, "w", encoding="utf-8") as f:
63
+ json.dump(state, f)
64
+
65
+
66
+ def safe(session: str) -> str:
67
+ return re.sub(r"[^A-Za-z0-9_-]", "", session or "default")[:64] or "default"
68
+
69
+
70
+ # --- post-tool -----------------------------------------------------------------
71
+
72
+ def post_tool(payload: dict, project: str) -> list[str]:
73
+ """Update the session state and return the warnings to show Claude."""
74
+ session = payload.get("session_id", "")
75
+ state = load_state(project, session)
76
+ warnings: list[str] = []
77
+ tool = payload.get("tool_name", "")
78
+ tool_input = payload.get("tool_input") or {}
79
+
80
+ if tool in FILE_TOOLS:
81
+ path = tool_input.get("file_path") or tool_input.get("notebook_path")
82
+ if path:
83
+ absolute = os.path.abspath(os.path.join(project, path))
84
+ pending = state.setdefault("pending", [])
85
+ if absolute not in pending:
86
+ pending.append(absolute)
87
+ edited = state.setdefault("edited", [])
88
+ if absolute not in edited:
89
+ edited.append(absolute)
90
+ if len(edited) >= SCOPE_FILES and not state.get("scope_warned"):
91
+ state["scope_warned"] = True
92
+ warnings.append(f"This session has edited {len(edited)} files. Check the change is still "
93
+ "within the task's scope before going further.")
94
+
95
+ # The description is Claude's label for the call ("call 2 of 6"), not what runs.
96
+ action = {k: v for k, v in tool_input.items() if k not in IGNORED_INPUT_KEYS}
97
+ digest = hashlib.sha1(json.dumps([tool, action], sort_keys=True).encode()).hexdigest()
98
+ recent = (state.get("recent", []) + [digest])[-LOOP_LENGTH:]
99
+ state["recent"] = recent
100
+ if len(recent) == LOOP_LENGTH and len(set(recent)) == 1 and state.get("loop_warned") != digest:
101
+ state["loop_warned"] = digest
102
+ warnings.append(f"{tool} ran {LOOP_LENGTH} times in a row with the same input. Stop repeating it: "
103
+ "name the assumption that may be wrong, or ask the user one diagnostic question.")
104
+
105
+ used = context_used(project, session)
106
+ if used is not None:
107
+ for level in CONTEXT_WARNINGS:
108
+ if used >= level and state.get("context_warned", 0) < level:
109
+ state["context_warned"] = level
110
+ warnings.append(f"Context is at {used:.0f}% of the window. At the next logical point "
111
+ "(a commit, a finished step), update .claude/session-state/active.md if this "
112
+ "task has one, then suggest the user run /compact.")
113
+ break
114
+
115
+ save_state(project, session, state)
116
+ return warnings
117
+
118
+
119
+ def context_used(project: str, session: str) -> float | None:
120
+ """Percent of the real context window in use, as the status line last saw it."""
121
+ path = os.path.join(project, ".claude", "session-state", "context.json")
122
+ try:
123
+ with open(path, encoding="utf-8") as f:
124
+ data = json.load(f)
125
+ except (OSError, ValueError):
126
+ return None
127
+ if data.get("session_id") != session:
128
+ return None
129
+ try:
130
+ return float(data["used_percentage"])
131
+ except (KeyError, TypeError, ValueError):
132
+ return None
133
+
134
+
135
+ # --- stop ----------------------------------------------------------------------
136
+
137
+ def find_up(start: str, names: tuple[str, ...], stop_at: str) -> str | None:
138
+ """Nearest ancestor of `start` (up to `stop_at`) containing one of `names`."""
139
+ current = os.path.dirname(start)
140
+ stop_at = os.path.abspath(stop_at)
141
+ while True:
142
+ if any(os.path.exists(os.path.join(current, n)) for n in names):
143
+ return current
144
+ if os.path.abspath(current) == stop_at or os.path.dirname(current) == current:
145
+ return None
146
+ current = os.path.dirname(current)
147
+
148
+
149
+ def local_bin(root: str, name: str) -> str | None:
150
+ """A tool installed in the project; never one npx would download."""
151
+ for folder in (root, os.path.dirname(root)):
152
+ base = os.path.join(folder, "node_modules", ".bin", name)
153
+ for candidate in ((base + ".cmd", base) if os.name == "nt" else (base,)):
154
+ if os.path.exists(candidate):
155
+ return candidate
156
+ return None
157
+
158
+
159
+ def plan_checks(files: list[str], project: str) -> list[dict]:
160
+ """Group edited files by project root and pick the format and check commands."""
161
+ plans: dict[tuple, dict] = {}
162
+
163
+ def add(key: tuple, cwd: str, fmt: list | None, check: list | None, file: str) -> None:
164
+ plan = plans.setdefault(key, {"cwd": cwd, "format": fmt, "check": check, "files": []})
165
+ plan["files"].append(file)
166
+
167
+ for file in files:
168
+ if not os.path.exists(file):
169
+ continue
170
+ ext = os.path.splitext(file)[1].lower()
171
+ if ext in (".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".json", ".css", ".scss", ".md"):
172
+ root = find_up(file, ("package.json",), project)
173
+ if not root:
174
+ continue
175
+ fmt = None
176
+ biome = local_bin(root, "biome")
177
+ prettier = local_bin(root, "prettier")
178
+ if biome and any(os.path.exists(os.path.join(root, n)) for n in ("biome.json", "biome.jsonc")):
179
+ fmt = [biome, "format", "--write"]
180
+ elif prettier:
181
+ fmt = [prettier, "--write", "--log-level", "warn"]
182
+ check = None
183
+ if ext in (".ts", ".tsx"):
184
+ tsconfig = find_up(file, ("tsconfig.json",), project)
185
+ tsc = local_bin(root, "tsc")
186
+ if tsconfig and tsc:
187
+ check = [tsc, "--noEmit", "--pretty", "false", "-p", os.path.join(tsconfig, "tsconfig.json")]
188
+ add(("node", root, tuple(check or ())), root, fmt, check, file)
189
+ elif ext == ".go" and shutil.which("gofmt"):
190
+ root = find_up(file, ("go.mod",), project)
191
+ if root:
192
+ rel = "./" + os.path.relpath(os.path.dirname(file), root).replace("\\", "/")
193
+ check = ["go", "vet", rel] if shutil.which("go") else None
194
+ add(("go", root, rel), root, ["gofmt", "-w"], check, file)
195
+ elif ext == ".cs" and shutil.which("dotnet"):
196
+ root = find_up(file, (".editorconfig",), project) or project
197
+ add(("dotnet", root), root, ["dotnet", "format", "whitespace", ".", "--folder", "--include"], None, file)
198
+ return list(plans.values())
199
+
200
+
201
+ def run(command: list[str], cwd: str) -> tuple[int, str]:
202
+ try:
203
+ result = subprocess.run(command, cwd=cwd, capture_output=True, text=True,
204
+ timeout=CHECK_TIMEOUT, encoding="utf-8", errors="replace")
205
+ return result.returncode, (result.stdout or "") + (result.stderr or "")
206
+ except (OSError, subprocess.TimeoutExpired) as error:
207
+ return 0, f"skipped: {error}"
208
+
209
+
210
+ def errors_in(output: str, files: list[str], cwd: str) -> list[str]:
211
+ """Error lines that name one of `files`; errors elsewhere predate this turn."""
212
+ wanted = {os.path.normcase(os.path.abspath(f)) for f in files}
213
+ lines = []
214
+ for line in output.splitlines():
215
+ match = re.match(r"^(.+?)(?:\(\d+,\d+\)|:\d+:\d+)", line.strip())
216
+ if not match:
217
+ continue
218
+ candidate = os.path.normcase(os.path.abspath(os.path.join(cwd, match.group(1).strip())))
219
+ if candidate in wanted:
220
+ lines.append(line.strip())
221
+ return lines
222
+
223
+
224
+ def stop(payload: dict, project: str) -> dict | None:
225
+ session = payload.get("session_id", "")
226
+ state = load_state(project, session)
227
+ files = state.pop("pending", [])
228
+ save_state(project, session, state)
229
+ if not files or payload.get("stop_hook_active"):
230
+ return None
231
+
232
+ problems: list[str] = []
233
+ for plan in plan_checks(files, project):
234
+ if plan["format"]:
235
+ run(plan["format"] + plan["files"], plan["cwd"])
236
+ if plan["check"]:
237
+ code, output = run(plan["check"], plan["cwd"])
238
+ if code != 0:
239
+ problems.extend(errors_in(output, plan["files"], plan["cwd"]))
240
+ if not problems:
241
+ return None
242
+ shown = problems[:MAX_REPORT_LINES]
243
+ more = f"\n... and {len(problems) - len(shown)} more" if len(problems) > len(shown) else ""
244
+ return {
245
+ "decision": "block",
246
+ "reason": "The type check found errors in files you edited this turn. Fix them before finishing:\n"
247
+ + "\n".join(shown) + more,
248
+ }
249
+
250
+
251
+ # --- entry point ---------------------------------------------------------------
252
+
253
+ def main() -> int:
254
+ mode = sys.argv[1] if len(sys.argv) > 1 else ""
255
+ try:
256
+ payload = json.loads(sys.stdin.read() or "{}")
257
+ except ValueError:
258
+ return 0
259
+ project = os.environ.get("CLAUDE_PROJECT_DIR") or payload.get("cwd") or os.getcwd()
260
+ if mode == "post-tool":
261
+ warnings = post_tool(payload, project)
262
+ if warnings:
263
+ print(json.dumps({"hookSpecificOutput": {
264
+ "hookEventName": "PostToolUse",
265
+ "additionalContext": "Arcane session watch: " + " ".join(warnings),
266
+ }}))
267
+ elif mode == "stop":
268
+ decision = stop(payload, project)
269
+ if decision:
270
+ print(json.dumps(decision))
271
+ return 0
272
+
273
+
274
+ if __name__ == "__main__":
275
+ try:
276
+ sys.exit(main())
277
+ except Exception: # a watch bug must never break the session
278
+ sys.exit(0)
@@ -75,6 +75,21 @@ if isinstance(cw, dict) and cw:
75
75
  pct = float(up)
76
76
  except Exception:
77
77
  pct = None
78
+ # Hooks never see the window size; session_watch.py reads it from here to warn
79
+ # before a compaction at a share of the real window instead of a guessed one.
80
+ project_dir = (data.get("workspace") or {}).get("project_dir") or data.get("cwd") or ""
81
+ if pct is not None and project_dir and os.path.isdir(os.path.join(project_dir, ".claude")):
82
+ try:
83
+ state = os.path.join(project_dir, ".claude", "session-state")
84
+ os.makedirs(state, exist_ok=True)
85
+ ignore = os.path.join(state, ".gitignore")
86
+ if not os.path.exists(ignore):
87
+ open(ignore, "w").write("*\n")
88
+ with open(os.path.join(state, "context.json"), "w") as f:
89
+ json.dump({"session_id": data.get("session_id"), "used_percentage": pct,
90
+ "context_window_size": limit}, f)
91
+ except Exception:
92
+ pass
78
93
 
79
94
  # Fallback (older Claude Code without context_window): scan transcript usage.
80
95
  if total is None:
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env bash
2
+ # Hook: worktree-stop.sh — Stop
3
+ #
4
+ # A session that enters a worktree with EnterWorktree tends to stay there after the
5
+ # task is done, and the next pull or branch switch fails inside it. When the turn
6
+ # ends inside such a worktree with everything committed, block the stop once and ask
7
+ # Claude to leave with ExitWorktree (keep), or to say the task continues.
8
+ #
9
+ # Fires once per commit: a new commit means new progress, so it may ask again.
10
+ # Never fires when the session itself started inside the worktree (ExitWorktree
11
+ # could not leave it) or while there are uncommitted changes (work in progress).
12
+ set +e
13
+
14
+ main() {
15
+ local INPUT PROJECT TOP BRANCH HEAD MARKER_DIR MARKER REASON
16
+ INPUT=$(cat 2>/dev/null)
17
+ # Second stop of the same turn: Claude already answered the block.
18
+ echo "$INPUT" | grep -qE '"stop_hook_active"[[:space:]]*:[[:space:]]*true' && return 0
19
+
20
+ PROJECT="${CLAUDE_PROJECT_DIR:-}"
21
+ [ -n "$PROJECT" ] || return 0
22
+ TOP=$(git rev-parse --show-toplevel 2>/dev/null) || return 0
23
+
24
+ # Only worktrees EnterWorktree creates: under the project the session started in.
25
+ case "$TOP" in
26
+ "$PROJECT"/.claude/worktrees/*) ;;
27
+ *) return 0 ;;
28
+ esac
29
+
30
+ [ -z "$(git status --porcelain 2>/dev/null)" ] || return 0
31
+
32
+ BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)
33
+ HEAD=$(git rev-parse HEAD 2>/dev/null)
34
+ MARKER_DIR="$PROJECT/.claude/session-state"
35
+ MARKER="$MARKER_DIR/.worktree-stop"
36
+ [ "$(cat "$MARKER" 2>/dev/null)" = "$TOP $HEAD" ] && return 0
37
+ mkdir -p "$MARKER_DIR" 2>/dev/null
38
+ [ -f "$MARKER_DIR/.gitignore" ] || echo '*' > "$MARKER_DIR/.gitignore"
39
+ echo "$TOP $HEAD" > "$MARKER"
40
+
41
+ REASON="You are still inside the worktree $TOP (branch $BRANCH) and its changes are committed. If the task is finished, push the branch if the task included pushing, then leave with ExitWorktree (action: keep) and tell the user the worktree path and branch. If the task continues, say so in one line and stop."
42
+ REASON=${REASON//\\/\/}
43
+ REASON=${REASON//\"/\'}
44
+ printf '{"decision": "block", "reason": "%s"}\n' "$REASON"
45
+ }
46
+
47
+ main
48
+ exit 0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-arcane",
3
- "version": "2.13.0",
3
+ "version": "2.15.0",
4
4
  "description": "Skills, agents, hooks and rules for Claude Code — installable via npx claude-code-arcane",
5
5
  "type": "module",
6
6
  "bin": {
@@ -57,6 +57,8 @@ hooks:
57
57
  - pre-compact.sh
58
58
  - post-compact.sh
59
59
  - session-stop.sh
60
+ - worktree-stop.sh
61
+ - run-py.sh
60
62
  - log-agent.sh
61
63
  - log-agent-stop.sh
62
64
 
@@ -39,6 +39,30 @@ Use AskUserQuestion exactly like this:
39
39
  - **Goal-driven:** Help the user define a verifiable goal statement and 2-4 checkpoints, create tasks, implement toward each checkpoint. No formal plan.
40
40
  - **Directo:** Proceed immediately without ceremony.
41
41
 
42
+ ## Progress file
43
+
44
+ In **Plan + Goal** and **Goal-driven** mode, keep `.claude/session-state/active.md` up to date. A compaction or a new session loses the conversation, not this file: `session-start.sh` shows its first lines when a session starts and after every compaction.
45
+
46
+ - **Create it** when the goal is set. If `.claude/session-state/` does not exist yet, create it with a `.gitignore` containing `*`, so the file never lands in a commit.
47
+ - **Update it** at every checkpoint, and always before a compaction.
48
+ - **Close it** when the task ends: set `Status: closed` and record the final verification result.
49
+ - Keep it under 30 lines:
50
+
51
+ ```markdown
52
+ # <task>
53
+ Status: in progress | closed
54
+ Goal: <verifiable outcome>
55
+ Criteria: <how we know it is done>
56
+ Plan:
57
+ - [x] <step done>
58
+ - [ ] <step pending>
59
+ Decisions: <decision> — <why>
60
+ Next: <the next concrete step>
61
+ Not verified: <what has not been checked yet>
62
+ ```
63
+
64
+ Directo and Action Plan tasks do not need it.
65
+
42
66
  ## Before writing new code
43
67
 
44
68
  In every mode, search in this order and take the first option that fully works:
@@ -14,7 +14,7 @@ model: haiku
14
14
 
15
15
  - Stage: !`cat production/stage.txt 2>/dev/null | tr -d '[:space:]' || echo 'not set'`
16
16
  - Latest sprint: !`ls -t production/sprints/*.md 2>/dev/null | head -1 || echo 'none'`
17
- - Session state: !`head -5 production/session-state/active.md 2>/dev/null || echo 'none'`
17
+ - Session state: !`head -5 .claude/session-state/active.md 2>/dev/null || echo 'none'`
18
18
 
19
19
  Read-only skill — reports findings, writes no files. Lightweight orientation; for a formal readiness check use `/gate-check`.
20
20
 
@@ -29,7 +29,7 @@ Read `.claude/docs/workflow-catalog.yaml` (authoritative phases/steps). Glob `.c
29
29
 
30
30
  ## Step 3: Read Session Context
31
31
 
32
- Read `production/session-state/active.md` — what was most recently worked on, in-progress tasks, open questions.
32
+ Read `.claude/session-state/active.md` — what was most recently worked on, in-progress tasks, open questions.
33
33
 
34
34
  ## Step 4: Check Step Completion
35
35
 
@@ -12,7 +12,7 @@ Entry point para sesiones nuevas. Detecta contexto, hace preguntas clave, rutea
12
12
 
13
13
  ## Paso 1: Detectar contexto
14
14
 
15
- Escanear en paralelo: Unity (`ProjectSettings/ProjectVersion.txt`), Unreal (`*.uproject`), Web/Node (`package.json`), Go (`go.mod`), Python (`pyproject.toml`/`requirements.txt`), Flutter (`pubspec.yaml`), Rust (`Cargo.toml`), docs (`README.md`, `CLAUDE.md`, `design/`, `docs/`), session state (`production/session-state/active.md`).
15
+ Escanear en paralelo: Unity (`ProjectSettings/ProjectVersion.txt`), Unreal (`*.uproject`), Web/Node (`package.json`), Go (`go.mod`), Python (`pyproject.toml`/`requirements.txt`), Flutter (`pubspec.yaml`), Rust (`Cargo.toml`), docs (`README.md`, `CLAUDE.md`, `design/`, `docs/`), session state (`.claude/session-state/active.md`).
16
16
 
17
17
  ## Paso 2: Determinar etapa
18
18
 
@@ -45,7 +45,7 @@ AskUserQuestion multi-tab:
45
45
 
46
46
  ## Paso 5: Set session state
47
47
 
48
- Crear `production/session-state/active.md`: project name, division, stage, user role, next action, stack detected, existing artifacts, recommended flow (3 commands).
48
+ Crear `.claude/session-state/active.md`: project name, division, stage, user role, next action, stack detected, existing artifacts, recommended flow (3 commands).
49
49
 
50
50
  ## Paso 6: First action
51
51
 
@@ -12,7 +12,7 @@ Closes loop between design and implementation. Verifies acceptance criteria, che
12
12
 
13
13
  ## Phase 1: Find Story
14
14
 
15
- Resolve review mode: `--review` arg → `production/review-mode.txt` → default `lean`. Story source: arg path → `production/session-state/active.md` → most recent sprint file (IN PROGRESS stories) → AskUserQuestion.
15
+ Resolve review mode: `--review` arg → `production/review-mode.txt` → default `lean`. Story source: arg path → `.claude/session-state/active.md` → most recent sprint file (IN PROGRESS stories) → AskUserQuestion.
16
16
 
17
17
  ## Phase 2: Read Story
18
18
 
@@ -55,7 +55,7 @@ BLOCKED → do not proceed, list fixes, offer help.
55
55
 
56
56
  ## Phase 7: Update Story
57
57
 
58
- Ask approval → edit story: `Status: Complete` + `## Completion Notes` (date, criteria, deviations, test evidence, code review status). Advisory deviations → offer to log in `docs/tech-debt-register.md`. Update `production/sprint-status.yaml` if exists. Append to `production/session-state/active.md`.
58
+ Ask approval → edit story: `Status: Complete` + `## Completion Notes` (date, criteria, deviations, test evidence, code review status). Advisory deviations → offer to log in `docs/tech-debt-register.md`. Update `production/sprint-status.yaml` if exists. Append to `.claude/session-state/active.md`.
59
59
 
60
60
  ## Phase 8: Surface Next Story
61
61
 
@@ -38,7 +38,7 @@ If output file exists → read it, show section status table (Complete/Empty/Pla
38
38
 
39
39
  ## Skeleton Creation
40
40
 
41
- Create output file with `[To be designed]` placeholders for all sections. Update `production/session-state/active.md`.
41
+ Create output file with `[To be designed]` placeholders for all sections. Update `.claude/session-state/active.md`.
42
42
 
43
43
  > → Read references/spec-sections.md for [section inventories for UX spec, HUD, pattern library, and key section guidance]
44
44