aiwcli 0.9.7 → 0.10.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.
Files changed (119) hide show
  1. package/bin/run.js +5 -2
  2. package/dist/lib/claude-settings-types.d.ts +2 -0
  3. package/dist/templates/CLAUDE.md +49 -18
  4. package/dist/templates/_shared/.claude/settings.json +4 -0
  5. package/dist/templates/_shared/hooks/__pycache__/__init__.cpython-313.pyc +0 -0
  6. package/dist/templates/_shared/hooks/__pycache__/archive_plan.cpython-313.pyc +0 -0
  7. package/dist/templates/_shared/hooks/__pycache__/context_enforcer.cpython-313.pyc +0 -0
  8. package/dist/templates/_shared/hooks/__pycache__/context_monitor.cpython-313.pyc +0 -0
  9. package/dist/templates/_shared/hooks/__pycache__/file-suggestion.cpython-313.pyc +0 -0
  10. package/dist/templates/_shared/hooks/__pycache__/pre_compact.cpython-313.pyc +0 -0
  11. package/dist/templates/_shared/hooks/__pycache__/session_end.cpython-313.pyc +0 -0
  12. package/dist/templates/_shared/hooks/__pycache__/session_start.cpython-313.pyc +0 -0
  13. package/dist/templates/_shared/hooks/__pycache__/task_create_atomicity.cpython-313.pyc +0 -0
  14. package/dist/templates/_shared/hooks/__pycache__/task_create_capture.cpython-313.pyc +0 -0
  15. package/dist/templates/_shared/hooks/__pycache__/task_update_capture.cpython-313.pyc +0 -0
  16. package/dist/templates/_shared/hooks/__pycache__/user_prompt_submit.cpython-313.pyc +0 -0
  17. package/dist/templates/_shared/hooks/archive_plan.py +87 -178
  18. package/dist/templates/_shared/hooks/context_monitor.py +128 -194
  19. package/dist/templates/_shared/hooks/file-suggestion.py +26 -23
  20. package/dist/templates/_shared/hooks/pre_compact.py +104 -0
  21. package/dist/templates/_shared/hooks/session_end.py +154 -0
  22. package/dist/templates/_shared/hooks/session_start.py +145 -59
  23. package/dist/templates/_shared/hooks/task_create_capture.py +26 -49
  24. package/dist/templates/_shared/hooks/task_update_capture.py +42 -100
  25. package/dist/templates/_shared/hooks/user_prompt_submit.py +63 -77
  26. package/dist/templates/_shared/lib/base/__init__.py +16 -0
  27. package/dist/templates/_shared/lib/base/__pycache__/__init__.cpython-313.pyc +0 -0
  28. package/dist/templates/_shared/lib/base/__pycache__/constants.cpython-313.pyc +0 -0
  29. package/dist/templates/_shared/lib/base/__pycache__/hook_utils.cpython-313.pyc +0 -0
  30. package/dist/templates/_shared/lib/base/__pycache__/inference.cpython-313.pyc +0 -0
  31. package/dist/templates/_shared/lib/base/__pycache__/logger.cpython-313.pyc +0 -0
  32. package/dist/templates/_shared/lib/base/__pycache__/utils.cpython-313.pyc +0 -0
  33. package/dist/templates/_shared/lib/base/constants.py +18 -4
  34. package/dist/templates/_shared/lib/base/hook_utils.py +199 -11
  35. package/dist/templates/_shared/lib/base/inference.py +121 -0
  36. package/dist/templates/_shared/lib/base/logger.py +291 -0
  37. package/dist/templates/_shared/lib/base/utils.py +49 -11
  38. package/dist/templates/_shared/lib/context/__init__.py +72 -80
  39. package/dist/templates/_shared/lib/context/__pycache__/__init__.cpython-313.pyc +0 -0
  40. package/dist/templates/_shared/lib/context/__pycache__/context_formatter.cpython-313.pyc +0 -0
  41. package/dist/templates/_shared/lib/context/__pycache__/context_manager.cpython-313.pyc +0 -0
  42. package/dist/templates/_shared/lib/context/__pycache__/context_selector.cpython-313.pyc +0 -0
  43. package/dist/templates/_shared/lib/context/__pycache__/context_store.cpython-313.pyc +0 -0
  44. package/dist/templates/_shared/lib/context/__pycache__/discovery.cpython-313.pyc +0 -0
  45. package/dist/templates/_shared/lib/context/__pycache__/plan_manager.cpython-313.pyc +0 -0
  46. package/dist/templates/_shared/lib/context/__pycache__/task_tracker.cpython-313.pyc +0 -0
  47. package/dist/templates/_shared/lib/context/context_formatter.py +316 -0
  48. package/dist/templates/_shared/lib/context/context_selector.py +491 -0
  49. package/dist/templates/_shared/lib/context/context_store.py +636 -0
  50. package/dist/templates/_shared/lib/context/plan_manager.py +204 -0
  51. package/dist/templates/_shared/lib/context/task_tracker.py +188 -0
  52. package/dist/templates/_shared/lib/handoff/__pycache__/__init__.cpython-313.pyc +0 -0
  53. package/dist/templates/_shared/lib/handoff/__pycache__/document_generator.cpython-313.pyc +0 -0
  54. package/dist/templates/_shared/lib/handoff/document_generator.py +14 -40
  55. package/dist/templates/_shared/lib/templates/README.md +5 -13
  56. package/dist/templates/_shared/lib/templates/__init__.py +2 -6
  57. package/dist/templates/_shared/lib/templates/__pycache__/__init__.cpython-313.pyc +0 -0
  58. package/dist/templates/_shared/lib/templates/__pycache__/formatters.cpython-313.pyc +0 -0
  59. package/dist/templates/_shared/lib/templates/__pycache__/plan_context.cpython-313.pyc +0 -0
  60. package/dist/templates/_shared/lib/templates/plan_context.py +25 -79
  61. package/dist/templates/_shared/scripts/__pycache__/save_handoff.cpython-313.pyc +0 -0
  62. package/dist/templates/_shared/scripts/__pycache__/status_line.cpython-313.pyc +0 -0
  63. package/dist/templates/_shared/scripts/save_handoff.py +39 -19
  64. package/dist/templates/_shared/scripts/status_line.py +701 -0
  65. package/dist/templates/_shared/workflows/handoff.md +9 -3
  66. package/dist/templates/cc-native/.claude/settings.json +64 -9
  67. package/dist/templates/cc-native/CC-NATIVE-README.md +25 -28
  68. package/dist/templates/cc-native/MIGRATION.md +1 -1
  69. package/dist/templates/cc-native/TEMPLATE-SCHEMA.md +14 -39
  70. package/dist/templates/cc-native/_cc-native/agents/CLAUDE.md +1 -1
  71. package/dist/templates/cc-native/_cc-native/hooks/CLAUDE.md +57 -22
  72. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/add_plan_context.cpython-313.pyc +0 -0
  73. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-plan-review.cpython-313.pyc +0 -0
  74. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/mark_questions_asked.cpython-313.pyc +0 -0
  75. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/plan_accepted.cpython-313.pyc +0 -0
  76. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/plan_questions_early.cpython-313.pyc +0 -0
  77. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/suggest-fresh-perspective.cpython-313.pyc +0 -0
  78. package/dist/templates/cc-native/_cc-native/hooks/add_plan_context.py +57 -57
  79. package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.py +208 -158
  80. package/dist/templates/cc-native/_cc-native/hooks/plan_accepted.py +127 -0
  81. package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.py +81 -0
  82. package/dist/templates/cc-native/_cc-native/hooks/suggest-fresh-perspective.py +26 -25
  83. package/dist/templates/cc-native/_cc-native/lib/CLAUDE.md +35 -10
  84. package/dist/templates/cc-native/_cc-native/lib/__pycache__/__init__.cpython-313.pyc +0 -0
  85. package/dist/templates/cc-native/_cc-native/lib/__pycache__/constants.cpython-313.pyc +0 -0
  86. package/dist/templates/cc-native/_cc-native/lib/__pycache__/debug.cpython-313.pyc +0 -0
  87. package/dist/templates/cc-native/_cc-native/lib/__pycache__/orchestrator.cpython-313.pyc +0 -0
  88. package/dist/templates/cc-native/_cc-native/lib/__pycache__/state.cpython-313.pyc +0 -0
  89. package/dist/templates/cc-native/_cc-native/lib/__pycache__/utils.cpython-313.pyc +0 -0
  90. package/dist/templates/cc-native/_cc-native/lib/debug.py +37 -22
  91. package/dist/templates/cc-native/_cc-native/lib/orchestrator.py +103 -42
  92. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/__init__.cpython-313.pyc +0 -0
  93. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/agent.cpython-313.pyc +0 -0
  94. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/base.cpython-313.pyc +0 -0
  95. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/codex.cpython-313.pyc +0 -0
  96. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/gemini.cpython-313.pyc +0 -0
  97. package/dist/templates/cc-native/_cc-native/lib/reviewers/agent.py +26 -21
  98. package/dist/templates/cc-native/_cc-native/lib/reviewers/codex.py +12 -7
  99. package/dist/templates/cc-native/_cc-native/lib/reviewers/gemini.py +12 -7
  100. package/dist/templates/cc-native/_cc-native/lib/state.py +31 -16
  101. package/dist/templates/cc-native/_cc-native/lib/utils.py +210 -43
  102. package/dist/templates/cc-native/_cc-native/plan-review.config.json +1 -2
  103. package/oclif.manifest.json +1 -1
  104. package/package.json +1 -1
  105. package/dist/templates/_shared/hooks/context_enforcer.py +0 -625
  106. package/dist/templates/_shared/hooks/task_create_atomicity.py +0 -205
  107. package/dist/templates/_shared/lib/context/cache.py +0 -444
  108. package/dist/templates/_shared/lib/context/context_extractor.py +0 -115
  109. package/dist/templates/_shared/lib/context/context_manager.py +0 -1054
  110. package/dist/templates/_shared/lib/context/discovery.py +0 -444
  111. package/dist/templates/_shared/lib/context/event_log.py +0 -308
  112. package/dist/templates/_shared/lib/context/plan_archive.py +0 -101
  113. package/dist/templates/_shared/lib/context/task_sync.py +0 -290
  114. package/dist/templates/_shared/lib/templates/persona_questions.py +0 -113
  115. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/archive_plan.cpython-313.pyc +0 -0
  116. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-agent-review.cpython-313.pyc +0 -0
  117. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/test_permission_request.cpython-313.pyc +0 -0
  118. package/dist/templates/cc-native/_cc-native/lib/async_archive.py +0 -68
  119. package/dist/templates/cc-native/_cc-native/lib/atomic_write.py +0 -98
package/bin/run.js CHANGED
@@ -11,9 +11,12 @@ import {execute} from '@oclif/core'
11
11
  const args = process.argv.slice(2)
12
12
  const firstArg = args[0] ?? ''
13
13
  const hasCommand = args.length > 0 && !firstArg.startsWith('-')
14
- const isHelpOrVersion = firstArg === '--help' || firstArg === '-h' || firstArg === '--version'
14
+ const isHelpOrVersion = firstArg === '--help' || firstArg === '-h' || firstArg === '--version' || firstArg === '-v'
15
+
16
+ // Map -v to --version since oclif doesn't natively understand -v
17
+ const resolvedArgs = firstArg === '-v' ? ['--version', ...args.slice(1)] : args
15
18
 
16
19
  await execute({
17
20
  dir: import.meta.url,
18
- args: hasCommand || isHelpOrVersion ? args : ['launch', ...args],
21
+ args: hasCommand || isHelpOrVersion ? resolvedArgs : ['launch', ...resolvedArgs],
19
22
  })
@@ -6,6 +6,8 @@
6
6
  * Hook command configuration
7
7
  */
8
8
  export interface HookCommand {
9
+ /** If true, runs in background without blocking */
10
+ async?: boolean;
9
11
  /** Command to execute */
10
12
  command: string;
11
13
  /** Optional timeout in seconds */
@@ -17,24 +17,52 @@ Include `_output/{method}/` in template `.gitignore`.
17
17
 
18
18
  ## Directory Structure
19
19
 
20
+ Each template installs into `.aiwcli/` (method files) and `.{ide}/` (IDE integration). The `_shared/` template provides cross-method infrastructure used by all methods.
21
+
20
22
  ```
21
- packages/cli/src/templates/{method}/
22
- ├── _{method}/ # Method-specific shared files
23
- │ ├── templates/*.md.template
24
- │ └── workflows/*.md # Canonical workflow definitions
25
- ├── .{ide}/{ide-folder}/{method}/*.md # IDE command stubs
26
- ├── .gitignore # Output ignore rules
27
- ├── {METHOD}-README.md # User documentation
28
- ├── TEMPLATE-SCHEMA.md # Schema reference
29
- └── MIGRATION.md # Breaking changes
23
+ packages/cli/src/templates/
24
+ ├── _shared/ # Cross-method infrastructure (installed by all methods)
25
+ │ ├── hooks/ # Shared hook scripts (context, tasks, sessions)
26
+ │ └── lib/ # Shared Python libraries
27
+ ├── base/ # Core: atomic_write, constants, inference, utils
28
+ ├── context/ # Context CRUD, selection, formatting, plans, tasks
29
+ ├── handoff/ # Session handoff document generation
30
+ │ └── templates/ # Output formatters, plan context templates
31
+
32
+ ├── cc-native/ # CC-Native method template
33
+ │ ├── _cc-native/ # Method-specific hooks, lib, agents, workflows, scripts
34
+ │ ├── .claude/ # Claude Code: settings.json, commands/, agents/
35
+ │ ├── .windsurf/ # Windsurf: workflows/
36
+ │ └── .gitignore
37
+
38
+ ├── gsd/ # GSD method template
39
+ │ ├── .aiwcli/_gsd/ # Templates, workflows, hooks, config, docs
40
+ │ ├── .claude/ # Claude Code: settings.json, commands/, agents/
41
+ │ ├── .windsurf/ # Windsurf: workflows/
42
+ │ ├── GSD-README.md
43
+ │ ├── TEMPLATE-SCHEMA.md
44
+ │ └── MIGRATION.md
45
+
46
+ ├── bmad/ # BMAD method template
47
+ │ ├── .aiwcli/_bmad/ # Agents, workflows, teams, testarch, config
48
+ │ ├── .claude/ # Claude Code: settings.json, commands/
49
+ │ └── ...
50
+
51
+ ├── planning-with-files/ # Planning-with-Files method template
52
+ │ ├── .claude/ # Claude Code: settings.json, skills/
53
+ │ ├── .windsurf/ # Windsurf: workflows/, scripts/
54
+ │ └── ...
55
+
56
+ └── CLAUDE.md # This file
30
57
  ```
31
58
 
32
59
  ### Tier Details
33
60
 
34
61
  | Tier | Location | Purpose |
35
62
  |------|----------|---------|
36
- | General | `_{method}/` | IDE-agnostic templates and canonical workflows |
37
- | IDE | `.{ide}/{folder}/{method}/` | Lightweight stubs that load canonical workflows |
63
+ | Shared | `_shared/` | Cross-method hooks and libraries (context management, task tracking, sessions) |
64
+ | Method | `_{method}/` or `.aiwcli/_{method}/` | Method-specific templates, workflows, hooks, config |
65
+ | IDE | `.{ide}/` | IDE-specific command stubs, settings, workflow definitions |
38
66
  | Config | `.{ide}/settings.json` | Hooks, model prefs, method settings (merged on install) |
39
67
 
40
68
  ---
@@ -57,7 +85,7 @@ When multiple templates install, settings.json files merge:
57
85
 
58
86
  ## Hooks
59
87
 
60
- **Location:** `.claude/hooks/{method}-{hook-name}.{ext}`
88
+ **Location:** Hooks live in `.aiwcli/_shared/hooks/` (cross-method) and `.aiwcli/_{method}/hooks/` (method-specific). They are configured in `.{ide}/settings.json`, not placed in IDE directories.
61
89
 
62
90
  **Configuration:**
63
91
  ```json
@@ -65,14 +93,14 @@ When multiple templates install, settings.json files merge:
65
93
  "hooks": {
66
94
  "PostToolUse": [{
67
95
  "matcher": "Write",
68
- "hooks": [{ "type": "command", "command": "python .claude/hooks/gsd-plan-review.py", "timeout": 300000 }]
96
+ "hooks": [{ "type": "command", "command": "python .aiwcli/_cc-native/hooks/cc-native-plan-review.py", "timeout": 300000 }]
69
97
  }]
70
98
  }
71
99
  }
72
100
  ```
73
101
 
74
102
  **Requirements:**
75
- - Prefix with method name (e.g., `gsd-plan-review.py`)
103
+ - Prefix method-specific hooks with method name (e.g., `cc-native-plan-review.py`)
76
104
  - Use relative paths from project root
77
105
  - Write outputs to `_output/{method}/`
78
106
  - Specify timeouts
@@ -119,8 +147,8 @@ Load and execute `_{method}/workflows/{name}.md`.
119
147
  | Reference Type | Pattern |
120
148
  |----------------|---------|
121
149
  | Templates | `_{method}/templates/FILE.md.template` |
122
- | Workflows (Claude) | `/gsd:other-workflow` |
123
- | Workflows (Windsurf) | `other-workflow` from GSD workflows |
150
+ | Workflows (Claude) | `/gsd:workflow-name` (maps to `.claude/commands/gsd/workflow-name.md`) |
151
+ | Workflows (Windsurf) | `workflow-name` from method workflows |
124
152
  | Outputs | `_output/{method}/{subdir}/FILE.md` |
125
153
 
126
154
  ---
@@ -143,8 +171,8 @@ Load and execute `_{method}/workflows/{name}.md`.
143
171
 
144
172
  **New Template:**
145
173
  - [ ] Create `_{method}/` with `templates/` and `workflows/`
146
- - [ ] Create `.claude/commands/{method}/` stubs
147
- - [ ] Create `.windsurf/workflows/{method}/` stubs
174
+ - [ ] Create `.claude/commands/{method}/` stubs (Claude Code)
175
+ - [ ] Create `.windsurf/workflows/{method}/` stubs (Windsurf)
148
176
  - [ ] Add `.gitignore` with `_output/{method}/`
149
177
  - [ ] Create `{METHOD}-README.md`, `TEMPLATE-SCHEMA.md`, `MIGRATION.md`
150
178
  - [ ] Configure method-namespaced settings in `.claude/settings.json`
@@ -165,6 +193,7 @@ Load and execute `_{method}/workflows/{name}.md`.
165
193
  - Keep canonical workflows in `_{method}/workflows/`
166
194
  - Use relative paths from project root
167
195
  - Document changes in TEMPLATE-SCHEMA.md
196
+ - Place hooks in `.aiwcli/` directories, wire them in `.{ide}/settings.json`
168
197
 
169
198
  **Avoid:**
170
199
  - Outputs in project root
@@ -172,3 +201,5 @@ Load and execute `_{method}/workflows/{name}.md`.
172
201
  - Hooks without method prefix
173
202
  - Full workflows in IDE command files
174
203
  - Hardcoded paths without method namespace
204
+ - Putting hook scripts directly in IDE directories (`.claude/hooks/`)
205
+ - Creating `_shared/` directories inside method templates (e.g., `cc-native/_shared/`). All shared code lives in `packages/cli/src/templates/_shared/`. Method templates reference shared code via sys.path at runtime, not by copying.
@@ -1,4 +1,8 @@
1
1
  {
2
+ "statusLine": {
3
+ "type": "command",
4
+ "command": "python .aiwcli/_shared/scripts/status_line.py"
5
+ },
2
6
  "fileSuggestion": {
3
7
  "type": "command",
4
8
  "command": "python .aiwcli/_shared/hooks/file-suggestion.py"
@@ -1,21 +1,20 @@
1
1
  #!/usr/bin/env python3
2
- """Plan archival hook for ExitPlanMode PostToolUse event.
2
+ """Plan archival hook for ExitPlanMode PermissionRequest event.
3
3
 
4
- This hook runs when ExitPlanMode completes, extracting the plan path from
5
- the tool result and archiving it to the active context.
4
+ This hook runs when ExitPlanMode is requested (BEFORE user accepts/rejects),
5
+ extracting the plan path from the tool input and archiving it to the
6
+ context's plans/ folder. It does NOT modify state.json plan fields or mode.
6
7
 
7
- Actions:
8
- 1. Detect ExitPlanMode PostToolUse event
9
- 2. Extract plan path from tool result
10
- 3. Check if plan already archived (avoid duplicates)
11
- 4. Determine active context
12
- 5. Archive plan to context's plans folder
13
- 6. Set context in_flight.mode = "pending_implementation"
8
+ Separation of concerns:
9
+ - archive_plan.py (PermissionRequest) -> archives file only, no state.json changes
10
+ - plan_accepted.py (PostToolUse) -> assigns plan fields (hash/signature/path) to state.json
11
+ - session_end.py (SessionEnd) -> transitions active -> has_plan when plan is assigned
12
+ - context_selector.py -> matches plan content, transitions has_plan -> active
14
13
 
15
14
  Usage in .claude/settings.json:
16
15
  {
17
16
  "hooks": {
18
- "PostToolUse": [{
17
+ "PermissionRequest": [{
19
18
  "matcher": "ExitPlanMode",
20
19
  "hooks": [{
21
20
  "type": "command",
@@ -26,7 +25,6 @@ Usage in .claude/settings.json:
26
25
  }
27
26
  }
28
27
  """
29
- import json
30
28
  import re
31
29
  import sys
32
30
  from pathlib import Path
@@ -37,12 +35,11 @@ SCRIPT_DIR = Path(__file__).resolve().parent
37
35
  SHARED_LIB = SCRIPT_DIR.parent / "lib"
38
36
  sys.path.insert(0, str(SHARED_LIB.parent))
39
37
 
40
- from lib.base.hook_utils import load_hook_input
41
- from lib.context.plan_archive import archive_plan_to_context
42
- from lib.context.context_manager import get_all_contexts
43
- from lib.context.context_extractor import extract_context_id_for_session
44
- from lib.base.utils import eprint, project_dir
38
+ from lib.base.hook_utils import load_hook_input, log_debug, log_info, log_warn, log_error
39
+ from lib.base.utils import project_dir
45
40
  from lib.base.constants import get_context_dir
41
+ from lib.context.context_store import get_context_by_session_id
42
+ from lib.context.plan_manager import archive_plan, extract_plan_path_from_result
46
43
 
47
44
  # Import debug cleanup function from cc-native lib
48
45
  _cc_native_lib = SCRIPT_DIR.parent / "_cc-native" / "lib"
@@ -51,210 +48,122 @@ try:
51
48
  from debug import cleanup_debug_folder
52
49
  except ImportError:
53
50
  def cleanup_debug_folder(context_path):
54
- pass # Fallback if debug module not available
51
+ pass
55
52
 
56
53
 
57
- def extract_plan_path_from_result(tool_result: str) -> Optional[str]:
58
- """
59
- Extract plan path from ExitPlanMode tool result.
60
-
61
- Looks for pattern: "Your plan has been saved to: <path>"
62
- """
63
- match = re.search(r'Your plan has been saved to:\s*(.+\.md)', tool_result)
64
- if match:
65
- return match.group(1).strip()
66
- return None
67
-
68
-
69
- def on_plan_archive():
70
- """
71
- Plan archival hook - archives plan when exiting plan mode.
72
-
73
- Called from PostToolUse on ExitPlanMode - extracts plan path from result
74
- and archives to the active context.
75
- """
76
- # Read hook input using shared utility
77
- hook_input = load_hook_input()
78
- if not hook_input:
79
- eprint("[archive_plan] No valid JSON input")
80
- return
81
-
82
- hook_event = hook_input.get("hook_event_name", "unknown")
83
- tool_name = hook_input.get("tool_name", "")
84
- print(f"[archive_plan] Hook triggered: {hook_event}")
85
- print(f"[archive_plan] Tool name: {tool_name}")
86
- print(f"[archive_plan] Hook input keys: {list(hook_input.keys())}")
87
-
88
- # Special handling for ExitPlanMode - don't check permission_mode
89
- is_exit_plan_mode = (hook_event == "PostToolUse" and tool_name == "ExitPlanMode")
90
-
91
- if is_exit_plan_mode:
92
- print("[archive_plan] ExitPlanMode detected, proceeding with archival")
93
- else:
94
- # Check if we're in plan mode for other hooks
95
- permission_mode = hook_input.get("permission_mode", "default")
96
- print(f"[archive_plan] Permission mode: {permission_mode}")
97
-
98
- if permission_mode != "plan":
99
- print("[archive_plan] Not in plan mode, skipping archival")
100
- return
101
-
102
- # Prevent infinite loops from stop_hook_active
103
- if hook_input.get("stop_hook_active", False):
104
- print("[archive_plan] Stop hook already active, skipping to prevent loop")
105
- return
106
-
107
- print(f"[archive_plan] Proceeding with archival via {hook_event}")
108
-
109
- # Get project root from hook input or environment
110
- project_root = project_dir(hook_input)
111
-
112
- # Get plan path from hook input
54
+ def _find_plan_path(hook_input: dict, project_root: Path) -> Optional[str]:
55
+ """Find the plan file path from hook input or standard locations."""
113
56
  tool_input = hook_input.get("tool_input", {})
114
57
  tool_result = hook_input.get("tool_result", "")
58
+ hook_event = hook_input.get("hook_event_name", "")
59
+ tool_name = hook_input.get("tool_name", "")
115
60
 
116
- # Try to find plan path in various locations
117
61
  plan_path = None
118
62
 
119
- # For ExitPlanMode, extract plan path from tool result first
120
- if is_exit_plan_mode and tool_result:
63
+ # For ExitPlanMode, extract from tool result
64
+ if tool_name == "ExitPlanMode" and tool_result:
121
65
  plan_path = extract_plan_path_from_result(tool_result)
122
66
  if plan_path:
123
- print(f"[archive_plan] Extracted plan path from ExitPlanMode result: {plan_path}")
124
-
125
- # Check if plan path is directly provided in tool_input
126
- if not plan_path and "plan_path" in tool_input:
127
- plan_path = tool_input["plan_path"]
128
- elif not plan_path and "planPath" in tool_input:
129
- plan_path = tool_input["planPath"]
67
+ log_info("archive_plan", f"Extracted plan path from result: {plan_path}")
130
68
 
131
- # If not found yet, search standard locations
69
+ # Check tool_input for plan path
132
70
  if not plan_path:
133
- print("[archive_plan] No plan_path found, searching standard locations...")
134
- # Look for plan in common locations
135
- possible_paths = []
71
+ plan_path = tool_input.get("plan_path") or tool_input.get("planPath")
136
72
 
137
- # Check Claude Code plan directory first (~/.claude/plans/)
73
+ # Search standard locations
74
+ if not plan_path:
75
+ log_debug("archive_plan", "No plan_path found, searching standard locations...")
138
76
  claude_plans_dir = Path.home() / ".claude" / "plans"
139
- print(f"[archive_plan] Checking Claude plans dir: {claude_plans_dir}")
140
77
  if claude_plans_dir.exists():
141
- # Find any .md file in Claude plans directory
142
- claude_plans = list(claude_plans_dir.glob("*.md"))
143
- print(f"[archive_plan] Found {len(claude_plans)} .md files in Claude plans dir")
144
- # Sort by modification time, newest first
78
+ claude_plans = sorted(
79
+ claude_plans_dir.glob("*.md"),
80
+ key=lambda p: p.stat().st_mtime,
81
+ reverse=True,
82
+ )
145
83
  if claude_plans:
146
- claude_plans.sort(key=lambda p: p.stat().st_mtime, reverse=True)
147
- possible_paths.extend(claude_plans)
148
- for p in claude_plans[:3]: # Show first 3
149
- print(f"[archive_plan] - {p}")
84
+ plan_path = str(claude_plans[0])
150
85
 
151
- # Existing fallback paths
152
- possible_paths.extend([
86
+ if not plan_path:
87
+ for fallback in [
153
88
  project_root / "_output" / "cc-native" / "plans" / "current-plan.md",
154
89
  project_root / "_output" / "plans" / "current-plan.md",
155
90
  project_root / "plan.md",
156
- ])
157
-
158
- for path in possible_paths:
159
- if path.exists():
160
- plan_path = str(path)
91
+ ]:
92
+ if fallback.exists():
93
+ plan_path = str(fallback)
161
94
  break
162
95
 
163
- if not plan_path:
164
- eprint("[archive_plan] Could not determine plan path")
165
- # Don't block - let ExitPlanMode proceed
166
- print("[archive_plan] Could not find plan file in any of these locations:")
167
- print(f" - ~/.claude/plans/*.md")
168
- print(f" - {project_root}/_output/cc-native/plans/current-plan.md")
169
- print(f" - {project_root}/_output/plans/current-plan.md")
170
- print(f" - {project_root}/plan.md")
171
- print("Plan archival skipped: no plan path found")
96
+ return plan_path
97
+
98
+
99
+ def on_plan_archive():
100
+ """Archive plan on PermissionRequest:ExitPlanMode — file archival only, no state.json changes."""
101
+ hook_input = load_hook_input()
102
+ if not hook_input:
103
+ log_warn("archive_plan", "No valid JSON input")
172
104
  return
173
105
 
174
- print(f"[archive_plan] Found plan at: {plan_path}")
106
+ hook_event = hook_input.get("hook_event_name", "unknown")
107
+ tool_name = hook_input.get("tool_name", "")
108
+
109
+ log_info("archive_plan", f"Hook triggered: {hook_event}, tool: {tool_name}")
110
+
111
+ # Only handle PermissionRequest for ExitPlanMode
112
+ if not (hook_event == "PermissionRequest" and tool_name == "ExitPlanMode"):
113
+ log_debug("archive_plan", "Skipping: not PermissionRequest:ExitPlanMode")
114
+ return
175
115
 
176
- # Resolve plan path relative to project root
116
+ if hook_input.get("stop_hook_active", False):
117
+ log_debug("archive_plan", "Stop hook active, skipping")
118
+ return
119
+
120
+ project_root = project_dir(hook_input)
121
+ plan_path = _find_plan_path(hook_input, project_root)
122
+
123
+ if not plan_path:
124
+ log_warn("archive_plan", "Could not find plan file, skipping archival")
125
+ return
126
+
127
+ # Resolve plan path
177
128
  plan_file = Path(plan_path)
178
129
  if not plan_file.is_absolute():
179
- # Ensure we have a valid project_root
180
- if project_root is None:
181
- project_root = project_dir()
182
130
  plan_file = project_root / plan_path
183
- else:
184
- # On Windows, check if absolute path is on a different drive than project_root
185
- # In that case, use the absolute path as-is
186
- if sys.platform == 'win32':
187
- try:
188
- # Check if drives match (e.g., C: vs D:)
189
- plan_drive = plan_file.drive.upper() if plan_file.drive else None
190
- project_drive = project_root.drive.upper() if hasattr(project_root, 'drive') and project_root.drive else None
191
- if plan_drive and project_drive and plan_drive != project_drive:
192
- # Different drives - use absolute path as-is
193
- pass # plan_file is already set correctly
194
- except Exception:
195
- pass # Fall through to use plan_file as-is
196
-
197
- print(f"[archive_plan] Resolved plan file path: {plan_file}")
131
+
132
+ log_debug("archive_plan", f"Resolved plan file: {plan_file}")
198
133
 
199
134
  if not plan_file.exists():
200
- eprint(f"[archive_plan] Plan file not found: {plan_file}")
201
- print(f"[archive_plan] ERROR: File does not exist at resolved path")
202
- print(f"Plan archival skipped: file not found ({plan_path})")
135
+ log_error("archive_plan", f"Plan file not found: {plan_file}")
203
136
  return
204
137
 
205
- # Find context by session ID using shared extractor
138
+ # Find context by session ID
206
139
  session_id = hook_input.get("session_id", "unknown")
207
- context_id = extract_context_id_for_session(session_id, project_root, "archive_plan")
140
+ state = get_context_by_session_id(session_id, project_root)
208
141
 
209
- if not context_id:
210
- eprint("[archive_plan] Could not determine context for session")
211
- print("Plan archival failed: no context found for this session")
142
+ if not state:
143
+ log_warn("archive_plan", "Could not determine context for session")
212
144
  return
213
145
 
214
- # Check if plan was already archived (avoid duplicates)
215
- contexts = get_all_contexts(status="active", project_root=project_root)
216
- for ctx in contexts:
217
- if ctx.id == context_id:
218
- if ctx.in_flight and ctx.in_flight.mode == "pending_implementation":
219
- print(f"[archive_plan] Plan already archived for context '{context_id}', skipping")
220
- return
221
- break
222
-
223
- # Archive the plan
224
- archived_path, plan_hash = archive_plan_to_context(
225
- str(plan_file),
226
- context_id,
227
- project_root
146
+ context_id = state.id
147
+
148
+ # Archive the plan file (returns path, hash, signature)
149
+ archived_path, plan_hash, plan_signature = archive_plan(
150
+ str(plan_file), context_id, project_root
228
151
  )
229
152
 
230
153
  if archived_path:
231
- # Clean up debug logs before completing archive
154
+ # Clean up debug logs
232
155
  try:
233
156
  context_path = get_context_dir(context_id, project_root)
234
157
  cleanup_debug_folder(context_path)
235
- print(f"[archive_plan] Cleaned up debug logs for context: {context_id}")
236
158
  except Exception as e:
237
- print(f"[archive_plan] Warning: could not clean debug folder: {e}")
238
-
239
- print(f"")
240
- print(f"[archive_plan] SUCCESS!")
241
- print(f"[archive_plan] Plan archived to context: {context_id}")
242
- print(f"[archive_plan] Archived path: {archived_path}")
243
- print(f"[archive_plan] Source path: {plan_file}")
244
- print(f"[archive_plan] Hash: {plan_hash}")
245
- print(f"")
246
- print("After /clear, SessionStart will auto-continue this context for implementation.")
159
+ log_warn("archive_plan", f"could not clean debug folder: {e}")
160
+
161
+ log_info("archive_plan", f"SUCCESS: archived plan for {context_id}")
162
+ log_debug("archive_plan", f"Path: {archived_path}, hash: {plan_hash}")
247
163
  else:
248
- print(f"[archive_plan] FAILED: Could not archive plan for context '{context_id}'")
164
+ log_error("archive_plan", f"Could not archive plan for '{context_id}'")
249
165
 
250
166
 
251
167
  if __name__ == "__main__":
252
- try:
253
- on_plan_archive()
254
- except Exception as e:
255
- # Log errors to stderr
256
- eprint(f"[archive_plan] Error: {e}")
257
- import traceback
258
- eprint(traceback.format_exc())
259
- # Exit cleanly so hook doesn't block
260
- sys.exit(0)
168
+ from lib.base.hook_utils import run_hook
169
+ run_hook(on_plan_archive, "archive_plan")