aiwcli 0.10.3 → 0.11.1

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 (191) hide show
  1. package/bin/run.js +1 -1
  2. package/dist/commands/clear.js +28 -131
  3. package/dist/commands/init/index.js +3 -3
  4. package/dist/lib/gitignore-manager.d.ts +32 -0
  5. package/dist/lib/gitignore-manager.js +141 -2
  6. package/dist/templates/CLAUDE.md +8 -8
  7. package/dist/templates/_shared/.claude/commands/handoff-resume.md +64 -0
  8. package/dist/templates/_shared/.claude/commands/handoff.md +16 -10
  9. package/dist/templates/_shared/.claude/settings.json +7 -7
  10. package/dist/templates/_shared/hooks-ts/_utils/git-state.ts +2 -0
  11. package/dist/templates/_shared/hooks-ts/archive_plan.ts +159 -0
  12. package/dist/templates/_shared/hooks-ts/context_monitor.ts +147 -0
  13. package/dist/templates/_shared/hooks-ts/file-suggestion.ts +130 -0
  14. package/dist/templates/_shared/hooks-ts/pre_compact.ts +49 -0
  15. package/dist/templates/_shared/hooks-ts/session_end.ts +107 -0
  16. package/dist/templates/_shared/hooks-ts/session_start.ts +144 -0
  17. package/dist/templates/_shared/hooks-ts/task_create_capture.ts +48 -0
  18. package/dist/templates/_shared/hooks-ts/task_update_capture.ts +74 -0
  19. package/dist/templates/_shared/hooks-ts/user_prompt_submit.ts +83 -0
  20. package/dist/templates/_shared/lib-ts/CLAUDE.md +318 -0
  21. package/dist/templates/_shared/lib-ts/base/atomic-write.ts +12 -12
  22. package/dist/templates/_shared/lib-ts/base/constants.ts +22 -15
  23. package/dist/templates/_shared/lib-ts/base/git-state.ts +1 -1
  24. package/dist/templates/_shared/lib-ts/base/hook-utils.ts +129 -50
  25. package/dist/templates/_shared/lib-ts/base/inference.ts +28 -21
  26. package/dist/templates/_shared/lib-ts/base/logger.ts +15 -2
  27. package/dist/templates/_shared/lib-ts/base/state-io.ts +9 -7
  28. package/dist/templates/_shared/lib-ts/base/stop-words.ts +131 -131
  29. package/dist/templates/_shared/lib-ts/base/subprocess-utils.ts +142 -0
  30. package/dist/templates/_shared/lib-ts/base/utils.ts +69 -69
  31. package/dist/templates/_shared/lib-ts/context/context-formatter.ts +30 -24
  32. package/dist/templates/_shared/lib-ts/context/context-selector.ts +50 -32
  33. package/dist/templates/_shared/lib-ts/context/context-store.ts +76 -48
  34. package/dist/templates/_shared/lib-ts/context/plan-manager.ts +43 -23
  35. package/dist/templates/_shared/lib-ts/context/task-tracker.ts +10 -6
  36. package/dist/templates/_shared/lib-ts/handoff/document-generator.ts +11 -10
  37. package/dist/templates/_shared/lib-ts/handoff/handoff-reader.ts +158 -0
  38. package/dist/templates/_shared/lib-ts/templates/formatters.ts +6 -4
  39. package/dist/templates/_shared/lib-ts/types.ts +68 -55
  40. package/dist/templates/_shared/scripts/resolve_context.ts +24 -0
  41. package/dist/templates/_shared/scripts/resume_handoff.ts +345 -0
  42. package/dist/templates/_shared/scripts/save_handoff.ts +3 -3
  43. package/dist/templates/_shared/scripts/status_line.ts +687 -0
  44. package/dist/templates/cc-native/.claude/settings.json +175 -185
  45. package/dist/templates/cc-native/TEMPLATE-SCHEMA.md +15 -17
  46. package/dist/templates/cc-native/_cc-native/agents/CLAUDE.md +0 -2
  47. package/dist/templates/cc-native/_cc-native/hooks/CLAUDE.md +109 -135
  48. package/dist/templates/cc-native/_cc-native/hooks/add_plan_context.ts +119 -0
  49. package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.ts +1027 -0
  50. package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.ts +61 -0
  51. package/dist/templates/cc-native/_cc-native/lib-ts/aggregate-agents.ts +156 -0
  52. package/dist/templates/cc-native/_cc-native/lib-ts/artifacts.ts +792 -0
  53. package/dist/templates/cc-native/_cc-native/lib-ts/cc-native-state.ts +199 -0
  54. package/dist/templates/cc-native/_cc-native/lib-ts/cli-output-parser.ts +144 -0
  55. package/dist/templates/cc-native/_cc-native/lib-ts/config.ts +57 -0
  56. package/dist/templates/cc-native/_cc-native/lib-ts/constants.ts +83 -0
  57. package/dist/templates/cc-native/_cc-native/lib-ts/corroboration.ts +115 -0
  58. package/dist/templates/cc-native/_cc-native/lib-ts/debug.ts +80 -0
  59. package/dist/templates/cc-native/_cc-native/lib-ts/index.ts +120 -0
  60. package/dist/templates/cc-native/_cc-native/lib-ts/json-parser.ts +168 -0
  61. package/dist/templates/cc-native/_cc-native/lib-ts/nul +3 -0
  62. package/dist/templates/cc-native/_cc-native/lib-ts/orchestrator.ts +250 -0
  63. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/agent.ts +275 -0
  64. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/codex.ts +130 -0
  65. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/gemini.ts +107 -0
  66. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/index.ts +10 -0
  67. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/types.ts +23 -0
  68. package/dist/templates/cc-native/_cc-native/lib-ts/state.ts +240 -0
  69. package/dist/templates/cc-native/_cc-native/lib-ts/tsconfig.json +18 -0
  70. package/dist/templates/cc-native/_cc-native/lib-ts/types.ts +385 -0
  71. package/dist/templates/cc-native/_cc-native/lib-ts/verdict.ts +72 -0
  72. package/dist/templates/cc-native/_cc-native/plan-review.config.json +14 -1
  73. package/oclif.manifest.json +1 -1
  74. package/package.json +2 -2
  75. package/dist/templates/_shared/hooks/__init__.py +0 -16
  76. package/dist/templates/_shared/hooks/__pycache__/__init__.cpython-313.pyc +0 -0
  77. package/dist/templates/_shared/hooks/__pycache__/archive_plan.cpython-313.pyc +0 -0
  78. package/dist/templates/_shared/hooks/__pycache__/context_enforcer.cpython-313.pyc +0 -0
  79. package/dist/templates/_shared/hooks/__pycache__/context_monitor.cpython-313.pyc +0 -0
  80. package/dist/templates/_shared/hooks/__pycache__/file-suggestion.cpython-313.pyc +0 -0
  81. package/dist/templates/_shared/hooks/__pycache__/pre_compact.cpython-313.pyc +0 -0
  82. package/dist/templates/_shared/hooks/__pycache__/session_end.cpython-313.pyc +0 -0
  83. package/dist/templates/_shared/hooks/__pycache__/session_start.cpython-313.pyc +0 -0
  84. package/dist/templates/_shared/hooks/__pycache__/task_create_atomicity.cpython-313.pyc +0 -0
  85. package/dist/templates/_shared/hooks/__pycache__/task_create_capture.cpython-313.pyc +0 -0
  86. package/dist/templates/_shared/hooks/__pycache__/task_update_capture.cpython-313.pyc +0 -0
  87. package/dist/templates/_shared/hooks/__pycache__/user_prompt_submit.cpython-313.pyc +0 -0
  88. package/dist/templates/_shared/hooks/archive_plan.py +0 -177
  89. package/dist/templates/_shared/hooks/context_monitor.py +0 -270
  90. package/dist/templates/_shared/hooks/file-suggestion.py +0 -215
  91. package/dist/templates/_shared/hooks/pre_compact.py +0 -104
  92. package/dist/templates/_shared/hooks/session_end.py +0 -173
  93. package/dist/templates/_shared/hooks/session_start.py +0 -206
  94. package/dist/templates/_shared/hooks/task_create_capture.py +0 -108
  95. package/dist/templates/_shared/hooks/task_update_capture.py +0 -145
  96. package/dist/templates/_shared/hooks/user_prompt_submit.py +0 -139
  97. package/dist/templates/_shared/lib/__init__.py +0 -1
  98. package/dist/templates/_shared/lib/__pycache__/__init__.cpython-313.pyc +0 -0
  99. package/dist/templates/_shared/lib/base/__init__.py +0 -65
  100. package/dist/templates/_shared/lib/base/__pycache__/__init__.cpython-313.pyc +0 -0
  101. package/dist/templates/_shared/lib/base/__pycache__/atomic_write.cpython-313.pyc +0 -0
  102. package/dist/templates/_shared/lib/base/__pycache__/constants.cpython-313.pyc +0 -0
  103. package/dist/templates/_shared/lib/base/__pycache__/hook_utils.cpython-313.pyc +0 -0
  104. package/dist/templates/_shared/lib/base/__pycache__/inference.cpython-313.pyc +0 -0
  105. package/dist/templates/_shared/lib/base/__pycache__/logger.cpython-313.pyc +0 -0
  106. package/dist/templates/_shared/lib/base/__pycache__/stop_words.cpython-313.pyc +0 -0
  107. package/dist/templates/_shared/lib/base/__pycache__/subprocess_utils.cpython-313.pyc +0 -0
  108. package/dist/templates/_shared/lib/base/__pycache__/utils.cpython-313.pyc +0 -0
  109. package/dist/templates/_shared/lib/base/atomic_write.py +0 -180
  110. package/dist/templates/_shared/lib/base/constants.py +0 -358
  111. package/dist/templates/_shared/lib/base/hook_utils.py +0 -339
  112. package/dist/templates/_shared/lib/base/inference.py +0 -307
  113. package/dist/templates/_shared/lib/base/logger.py +0 -305
  114. package/dist/templates/_shared/lib/base/stop_words.py +0 -221
  115. package/dist/templates/_shared/lib/base/subprocess_utils.py +0 -46
  116. package/dist/templates/_shared/lib/base/utils.py +0 -263
  117. package/dist/templates/_shared/lib/context/__init__.py +0 -102
  118. package/dist/templates/_shared/lib/context/__pycache__/__init__.cpython-313.pyc +0 -0
  119. package/dist/templates/_shared/lib/context/__pycache__/cache.cpython-313.pyc +0 -0
  120. package/dist/templates/_shared/lib/context/__pycache__/context_extractor.cpython-313.pyc +0 -0
  121. package/dist/templates/_shared/lib/context/__pycache__/context_formatter.cpython-313.pyc +0 -0
  122. package/dist/templates/_shared/lib/context/__pycache__/context_manager.cpython-313.pyc +0 -0
  123. package/dist/templates/_shared/lib/context/__pycache__/context_selector.cpython-313.pyc +0 -0
  124. package/dist/templates/_shared/lib/context/__pycache__/context_store.cpython-313.pyc +0 -0
  125. package/dist/templates/_shared/lib/context/__pycache__/discovery.cpython-313.pyc +0 -0
  126. package/dist/templates/_shared/lib/context/__pycache__/event_log.cpython-313.pyc +0 -0
  127. package/dist/templates/_shared/lib/context/__pycache__/plan_archive.cpython-313.pyc +0 -0
  128. package/dist/templates/_shared/lib/context/__pycache__/plan_manager.cpython-313.pyc +0 -0
  129. package/dist/templates/_shared/lib/context/__pycache__/task_sync.cpython-313.pyc +0 -0
  130. package/dist/templates/_shared/lib/context/__pycache__/task_tracker.cpython-313.pyc +0 -0
  131. package/dist/templates/_shared/lib/context/context_formatter.py +0 -317
  132. package/dist/templates/_shared/lib/context/context_selector.py +0 -508
  133. package/dist/templates/_shared/lib/context/context_store.py +0 -653
  134. package/dist/templates/_shared/lib/context/plan_manager.py +0 -303
  135. package/dist/templates/_shared/lib/context/task_tracker.py +0 -188
  136. package/dist/templates/_shared/lib/handoff/__init__.py +0 -22
  137. package/dist/templates/_shared/lib/handoff/__pycache__/__init__.cpython-313.pyc +0 -0
  138. package/dist/templates/_shared/lib/handoff/__pycache__/document_generator.cpython-313.pyc +0 -0
  139. package/dist/templates/_shared/lib/handoff/document_generator.py +0 -278
  140. package/dist/templates/_shared/lib/templates/README.md +0 -206
  141. package/dist/templates/_shared/lib/templates/__init__.py +0 -36
  142. package/dist/templates/_shared/lib/templates/__pycache__/__init__.cpython-313.pyc +0 -0
  143. package/dist/templates/_shared/lib/templates/__pycache__/formatters.cpython-313.pyc +0 -0
  144. package/dist/templates/_shared/lib/templates/__pycache__/persona_questions.cpython-313.pyc +0 -0
  145. package/dist/templates/_shared/lib/templates/__pycache__/plan_context.cpython-313.pyc +0 -0
  146. package/dist/templates/_shared/lib/templates/formatters.py +0 -146
  147. package/dist/templates/_shared/lib/templates/plan_context.py +0 -73
  148. package/dist/templates/_shared/scripts/__pycache__/save_handoff.cpython-313.pyc +0 -0
  149. package/dist/templates/_shared/scripts/__pycache__/status_line.cpython-313.pyc +0 -0
  150. package/dist/templates/_shared/scripts/save_handoff.py +0 -357
  151. package/dist/templates/_shared/scripts/status_line.py +0 -716
  152. package/dist/templates/cc-native/.claude/commands/cc-native/fresh-perspective.md +0 -8
  153. package/dist/templates/cc-native/.windsurf/workflows/cc-native/fresh-perspective.md +0 -8
  154. package/dist/templates/cc-native/MIGRATION.md +0 -86
  155. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/add_plan_context.cpython-313.pyc +0 -0
  156. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-plan-review.cpython-313.pyc +0 -0
  157. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/mark_questions_asked.cpython-313.pyc +0 -0
  158. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/plan_accepted.cpython-313.pyc +0 -0
  159. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/plan_questions_early.cpython-313.pyc +0 -0
  160. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/suggest-fresh-perspective.cpython-313.pyc +0 -0
  161. package/dist/templates/cc-native/_cc-native/hooks/add_plan_context.py +0 -130
  162. package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.py +0 -954
  163. package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.py +0 -81
  164. package/dist/templates/cc-native/_cc-native/hooks/suggest-fresh-perspective.py +0 -340
  165. package/dist/templates/cc-native/_cc-native/lib/CLAUDE.md +0 -265
  166. package/dist/templates/cc-native/_cc-native/lib/__init__.py +0 -53
  167. package/dist/templates/cc-native/_cc-native/lib/__pycache__/__init__.cpython-313.pyc +0 -0
  168. package/dist/templates/cc-native/_cc-native/lib/__pycache__/atomic_write.cpython-313.pyc +0 -0
  169. package/dist/templates/cc-native/_cc-native/lib/__pycache__/constants.cpython-313.pyc +0 -0
  170. package/dist/templates/cc-native/_cc-native/lib/__pycache__/debug.cpython-313.pyc +0 -0
  171. package/dist/templates/cc-native/_cc-native/lib/__pycache__/orchestrator.cpython-313.pyc +0 -0
  172. package/dist/templates/cc-native/_cc-native/lib/__pycache__/state.cpython-313.pyc +0 -0
  173. package/dist/templates/cc-native/_cc-native/lib/__pycache__/utils.cpython-313.pyc +0 -0
  174. package/dist/templates/cc-native/_cc-native/lib/constants.py +0 -45
  175. package/dist/templates/cc-native/_cc-native/lib/debug.py +0 -139
  176. package/dist/templates/cc-native/_cc-native/lib/orchestrator.py +0 -362
  177. package/dist/templates/cc-native/_cc-native/lib/reviewers/__init__.py +0 -28
  178. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/__init__.cpython-313.pyc +0 -0
  179. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/agent.cpython-313.pyc +0 -0
  180. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/base.cpython-313.pyc +0 -0
  181. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/codex.cpython-313.pyc +0 -0
  182. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/gemini.cpython-313.pyc +0 -0
  183. package/dist/templates/cc-native/_cc-native/lib/reviewers/agent.py +0 -215
  184. package/dist/templates/cc-native/_cc-native/lib/reviewers/base.py +0 -88
  185. package/dist/templates/cc-native/_cc-native/lib/reviewers/codex.py +0 -124
  186. package/dist/templates/cc-native/_cc-native/lib/reviewers/gemini.py +0 -108
  187. package/dist/templates/cc-native/_cc-native/lib/state.py +0 -268
  188. package/dist/templates/cc-native/_cc-native/lib/utils.py +0 -1071
  189. package/dist/templates/cc-native/_cc-native/scripts/__pycache__/aggregate_agents.cpython-313.pyc +0 -0
  190. package/dist/templates/cc-native/_cc-native/scripts/aggregate_agents.py +0 -168
  191. package/dist/templates/cc-native/_cc-native/workflows/fresh-perspective.md +0 -134
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Pure verdict aggregation logic.
3
+ * See cc-native-plan-review-spec.md §4.2
4
+ */
5
+
6
+ import type { ReviewDecisionResult, Verdict } from "./types.js";
7
+
8
+ /**
9
+ * Return the worst verdict from a list.
10
+ * Order: pass < warn < fail. skip→pass, error→warn.
11
+ */
12
+ export function worstVerdict(verdicts: Verdict[]): Verdict {
13
+ const order: Record<Verdict, number> = {
14
+ pass: 0,
15
+ warn: 1,
16
+ fail: 2,
17
+ skip: 0,
18
+ error: 1,
19
+ };
20
+
21
+ let worst: Verdict = "pass";
22
+ for (const v of verdicts) {
23
+ if ((order[v] ?? 1) > (order[worst] ?? 0)) {
24
+ worst = v;
25
+ }
26
+ }
27
+
28
+ // Normalize error → warn
29
+ if (worst === "error") return "warn";
30
+ return worst;
31
+ }
32
+
33
+ /**
34
+ * Verdict aggregation: fail veto triggers a block.
35
+ *
36
+ * Priority order:
37
+ * 1. Fail Veto: Any fail → deny (ISO 61508 zero-tolerance)
38
+ * 2. Acceptable: warns are informational only
39
+ *
40
+ * Error exclusion: Detectors that produce no signal (error/skip) are excluded
41
+ * from the denominator.
42
+ *
43
+ * @param allVerdicts - List of verdict strings from all reviewers
44
+ * @returns ReviewDecisionResult with should_deny, reason, and score
45
+ */
46
+ export function computeReviewDecision(
47
+ allVerdicts: Verdict[],
48
+ ): ReviewDecisionResult {
49
+ // Exclude non-signal verdicts
50
+ const signalVerdicts = allVerdicts.filter(
51
+ (v) => v === "pass" || v === "warn" || v === "fail",
52
+ );
53
+
54
+ if (signalVerdicts.length === 0) {
55
+ return { should_deny: false, reason: "no_signal", score: 0.0 };
56
+ }
57
+
58
+ // Fail blocks unconditionally
59
+ const failCount = signalVerdicts.filter((v) => v === "fail").length;
60
+ if (failCount > 0) {
61
+ return { should_deny: true, reason: "fail_veto", score: 1.0 };
62
+ }
63
+
64
+ // Warn also blocks — reviewers flagged concerns worth addressing
65
+ const warnCount = signalVerdicts.filter((v) => v === "warn").length;
66
+ const warnRatio = warnCount / signalVerdicts.length;
67
+ if (warnCount > 0) {
68
+ return { should_deny: true, reason: "warn_block", score: warnRatio };
69
+ }
70
+
71
+ return { should_deny: false, reason: "acceptable", score: 0.0 };
72
+ }
@@ -1,10 +1,22 @@
1
1
  {
2
+ "models": {
3
+ "providers": {
4
+ "claude": {
5
+ "enabled": true,
6
+ "models": ["sonnet"]
7
+ },
8
+ "codex": {
9
+ "enabled": true,
10
+ "models": ["gpt-5.1-codex-mini"]
11
+ }
12
+ }
13
+ },
2
14
  "planReview": {
3
15
  "enabled": true,
4
16
  "reviewers": {
5
17
  "codex": {
6
18
  "enabled": true,
7
- "model": "o4-mini",
19
+ "model": "gpt-5.1-codex-mini",
8
20
  "timeout": 120
9
21
  },
10
22
  "gemini": {
@@ -23,6 +35,7 @@
23
35
  "enabled": true,
24
36
  "timeout": 180,
25
37
  "warnThreshold": 0.01,
38
+ "maxIssuesPerAgent": 3,
26
39
  "orchestrator": {
27
40
  "enabled": true,
28
41
  "model": "opus",
@@ -416,5 +416,5 @@
416
416
  ]
417
417
  }
418
418
  },
419
- "version": "0.10.3"
419
+ "version": "0.11.1"
420
420
  }
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "aiwcli",
3
3
  "description": "AI Workflow CLI - Command-line interface for AI-powered workflows",
4
- "version": "0.10.3",
4
+ "version": "0.11.1",
5
5
  "author": "jofu-tofu",
6
6
  "bin": {
7
- "aiw": "./bin/run.js"
7
+ "aiw": "bin/run.js"
8
8
  },
9
9
  "bugs": {
10
10
  "url": "https://github.com/jofu-tofu/AI-Workflow-CLI/issues"
@@ -1,16 +0,0 @@
1
- """Shared hooks for AIW CLI templates.
2
-
3
- These hooks integrate with Claude Code's hook system to provide:
4
- - UserPromptSubmit: Context enforcement (ensures work happens in tracked context)
5
- - PostToolUse: Context monitoring and handoff warnings
6
- - ExitPlanMode: Plan archival to context
7
-
8
- Hooks read JSON input from stdin and output instructions to stdout.
9
-
10
- Available hooks:
11
- - user_prompt_submit.py: Runs on user prompt, enforces context tracking
12
- - context_monitor.py: Runs on PostToolUse, monitors context and warns when low
13
- - archive_plan.py: Runs on ExitPlanMode, archives plan to context
14
- - task_create_capture.py: Runs on TaskCreate, captures task events
15
- - task_update_capture.py: Runs on TaskUpdate, captures task updates
16
- """
@@ -1,177 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Plan archival hook for ExitPlanMode PermissionRequest event.
3
-
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.
7
-
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
13
-
14
- Usage in .claude/settings.json:
15
- {
16
- "hooks": {
17
- "PermissionRequest": [{
18
- "matcher": "ExitPlanMode",
19
- "hooks": [{
20
- "type": "command",
21
- "command": "python .aiwcli/_shared/hooks/archive_plan.py",
22
- "timeout": 5000
23
- }]
24
- }]
25
- }
26
- }
27
- """
28
- import re
29
- import sys
30
- from pathlib import Path
31
- from typing import Optional
32
-
33
- # Add parent directories to path for imports
34
- SCRIPT_DIR = Path(__file__).resolve().parent
35
- SHARED_LIB = SCRIPT_DIR.parent / "lib"
36
- sys.path.insert(0, str(SHARED_LIB.parent))
37
-
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
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, find_plan_path_in_transcript
43
-
44
- # Import debug cleanup function from cc-native lib
45
- _cc_native_lib = SCRIPT_DIR.parent / "_cc-native" / "lib"
46
- sys.path.insert(0, str(_cc_native_lib))
47
- try:
48
- from debug import cleanup_debug_folder
49
- except ImportError:
50
- def cleanup_debug_folder(context_path):
51
- pass
52
-
53
-
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."""
56
- tool_input = hook_input.get("tool_input", {})
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", "")
60
-
61
- plan_path = None
62
-
63
- # For ExitPlanMode, extract from tool result
64
- if tool_name == "ExitPlanMode" and tool_result:
65
- plan_path = extract_plan_path_from_result(tool_result)
66
- if plan_path:
67
- log_info("archive_plan", f"Extracted plan path from result: {plan_path}")
68
-
69
- # Check tool_input for plan path
70
- if not plan_path:
71
- plan_path = tool_input.get("plan_path") or tool_input.get("planPath")
72
-
73
- # Parse transcript for most recent Write to .claude/plans/
74
- if not plan_path:
75
- transcript_path = hook_input.get("transcript_path")
76
- if transcript_path:
77
- plan_path = find_plan_path_in_transcript(transcript_path)
78
- if plan_path:
79
- log_info("archive_plan", f"Found plan path via transcript: {plan_path}")
80
-
81
- # Search standard locations (mtime-based fallback)
82
- if not plan_path:
83
- log_debug("archive_plan", "No plan_path found, searching standard locations...")
84
- claude_plans_dir = Path.home() / ".claude" / "plans"
85
- if claude_plans_dir.exists():
86
- claude_plans = sorted(
87
- claude_plans_dir.glob("*.md"),
88
- key=lambda p: p.stat().st_mtime,
89
- reverse=True,
90
- )
91
- if claude_plans:
92
- plan_path = str(claude_plans[0])
93
-
94
- if not plan_path:
95
- for fallback in [
96
- project_root / "_output" / "cc-native" / "plans" / "current-plan.md",
97
- project_root / "_output" / "plans" / "current-plan.md",
98
- project_root / "plan.md",
99
- ]:
100
- if fallback.exists():
101
- plan_path = str(fallback)
102
- break
103
-
104
- return plan_path
105
-
106
-
107
- def on_plan_archive():
108
- """Archive plan on PermissionRequest:ExitPlanMode — file archival only, no state.json changes."""
109
- hook_input = load_hook_input()
110
- if not hook_input:
111
- log_warn("archive_plan", "No valid JSON input")
112
- return
113
-
114
- hook_event = hook_input.get("hook_event_name", "unknown")
115
- tool_name = hook_input.get("tool_name", "")
116
-
117
- log_info("archive_plan", f"Hook triggered: {hook_event}, tool: {tool_name}")
118
-
119
- # Only handle PermissionRequest for ExitPlanMode
120
- if not (hook_event == "PermissionRequest" and tool_name == "ExitPlanMode"):
121
- log_debug("archive_plan", "Skipping: not PermissionRequest:ExitPlanMode")
122
- return
123
-
124
- if hook_input.get("stop_hook_active", False):
125
- log_debug("archive_plan", "Stop hook active, skipping")
126
- return
127
-
128
- project_root = project_dir(hook_input)
129
- plan_path = _find_plan_path(hook_input, project_root)
130
-
131
- if not plan_path:
132
- log_warn("archive_plan", "Could not find plan file, skipping archival")
133
- return
134
-
135
- # Resolve plan path
136
- plan_file = Path(plan_path)
137
- if not plan_file.is_absolute():
138
- plan_file = project_root / plan_path
139
-
140
- log_debug("archive_plan", f"Resolved plan file: {plan_file}")
141
-
142
- if not plan_file.exists():
143
- log_error("archive_plan", f"Plan file not found: {plan_file}")
144
- return
145
-
146
- # Find context by session ID
147
- session_id = hook_input.get("session_id", "unknown")
148
- state = get_context_by_session_id(session_id, project_root)
149
-
150
- if not state:
151
- log_warn("archive_plan", "Could not determine context for session")
152
- return
153
-
154
- context_id = state.id
155
-
156
- # Archive the plan file (returns path, hash, signature)
157
- archived_path, plan_hash, plan_signature = archive_plan(
158
- str(plan_file), context_id, project_root
159
- )
160
-
161
- if archived_path:
162
- # Clean up debug logs
163
- try:
164
- context_path = get_context_dir(context_id, project_root)
165
- cleanup_debug_folder(context_path)
166
- except Exception as e:
167
- log_warn("archive_plan", f"could not clean debug folder: {e}")
168
-
169
- log_info("archive_plan", f"SUCCESS: archived plan for {context_id}")
170
- log_debug("archive_plan", f"Path: {archived_path}, hash: {plan_hash}")
171
- else:
172
- log_error("archive_plan", f"Could not archive plan for '{context_id}'")
173
-
174
-
175
- if __name__ == "__main__":
176
- from lib.base.hook_utils import run_hook
177
- run_hook(on_plan_archive, "archive_plan")
@@ -1,270 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Context monitor hook for proactive handoff warnings.
3
-
4
- This hook runs on PostToolUse for context-heavy tools and monitors
5
- context window usage. When context drops below a threshold, it injects
6
- a system reminder instructing Claude to wrap up and create a handoff document.
7
-
8
- Unlike UserPromptSubmit hooks, this fires DURING Claude's work,
9
- allowing proactive intervention without waiting for user input.
10
-
11
- Monitored tools (configured via settings.json matcher):
12
- - Task: Subagent responses can be huge
13
- - Read: File content loads into context
14
- - Bash: Command output can be large
15
- - WebFetch: Web content loads into context
16
-
17
- Hook input (from Claude Code):
18
- {
19
- "hook_event_name": "PostToolUse",
20
- "tool_name": "Task",
21
- "tool_input": {...},
22
- "tool_result": {...},
23
- "transcript_path": "/path/to/transcript.jsonl",
24
- "session_id": "abc123",
25
- "context_window": {
26
- "current_usage": {
27
- "cache_read_input_tokens": 0,
28
- "input_tokens": 12345,
29
- "cache_creation_input_tokens": 0,
30
- "output_tokens": 6789
31
- },
32
- "context_window_size": 200000
33
- },
34
- ...
35
- }
36
-
37
- Hook output:
38
- - Outputs JSON with additionalContext if context is low
39
- - This injects a system reminder into Claude's context
40
- - Plain stdout from PostToolUse only goes to verbose mode, not Claude
41
- - Using additionalContext ensures Claude sees and responds to the warning
42
-
43
- KNOWN LIMITATION: Context percentage won't match /context exactly.
44
- Hook JSON excludes system prompt, tools, MCP tokens. We add a baseline
45
- to compensate (~22.6k tokens typical). See:
46
- https://github.com/anthropics/claude-code/issues/13783
47
- """
48
- import sys
49
- from pathlib import Path
50
- from typing import Optional
51
-
52
- # Add parent directories to path for imports
53
- SCRIPT_DIR = Path(__file__).resolve().parent
54
- SHARED_LIB = SCRIPT_DIR.parent / "lib"
55
- sys.path.insert(0, str(SHARED_LIB.parent))
56
-
57
- from lib.base.hook_utils import emit_context, load_hook_input, get_context_percent_remaining, log_debug, log_info, log_warn, log_error, log_diagnostic
58
- from lib.base.utils import now_iso, project_dir
59
- from lib.context.context_store import (
60
- get_all_contexts,
61
- get_context_by_session_id,
62
- maybe_activate,
63
- save_state,
64
- )
65
-
66
- # Module-level flag: only save auto-state once per process lifetime
67
- _PROGRESSIVE_SAVE_MARKER = ".progressive-save-done"
68
-
69
- # Configuration
70
- SAVE_STATE_THRESHOLD = 60 # Silently save auto-state at 60% remaining
71
- HANDOFF_SUGGEST_THRESHOLD = 30 # Gentle nudge at 30% remaining (70% used)
72
- HANDOFF_PREPARE_THRESHOLD = 20 # Stronger warning at 20% remaining (80% used)
73
- CRITICAL_CONTEXT_THRESHOLD = 10 # Urgent warning at 10% remaining (90% used)
74
-
75
-
76
- def get_current_context_id(project_root: Path = None) -> Optional[str]:
77
- """Determine the current active context (most recently active)."""
78
- contexts = get_all_contexts(status="active", project_root=project_root)
79
- if contexts:
80
- return contexts[0].id
81
- return None
82
-
83
-
84
- def get_context_warning(
85
- percent_remaining: int,
86
- tokens_used: Optional[int],
87
- max_tokens: Optional[int],
88
- context_id: Optional[str],
89
- tool_name: str
90
- ) -> str:
91
- """Generate appropriate warning based on context level."""
92
- if tokens_used is not None and max_tokens is not None:
93
- tokens_used_k = tokens_used // 1000
94
- max_tokens_k = max_tokens // 1000
95
- usage_line = f"**Estimated usage**: ~{tokens_used_k}k / {max_tokens_k}k tokens"
96
- else:
97
- usage_line = f"**Estimated usage**: ~{percent_remaining}% remaining"
98
-
99
- context_line = f"\nContext ID: `{context_id}`" if context_id else ""
100
-
101
- if percent_remaining <= CRITICAL_CONTEXT_THRESHOLD:
102
- return f"""<system-reminder>
103
- ## CRITICAL CONTEXT WARNING ({percent_remaining}% remaining)
104
-
105
- {usage_line}
106
- **Triggered by**: {tool_name} tool completion
107
-
108
- **CRITICAL: Run `/handoff` now before context is compacted.**
109
- {context_line}
110
-
111
- You are about to lose context. Stop all other work and run `/handoff` immediately.
112
- </system-reminder>"""
113
-
114
- elif percent_remaining <= HANDOFF_PREPARE_THRESHOLD:
115
- return f"""<system-reminder>
116
- ## LOW CONTEXT WARNING ({percent_remaining}% remaining)
117
-
118
- {usage_line}
119
- **Triggered by**: {tool_name} tool completion
120
-
121
- **Context is getting low. Please finish your current task and run `/handoff`.**
122
- {context_line}
123
-
124
- **Actions:**
125
- 1. Complete your current atomic task (if 1-2 steps away)
126
- 2. Do NOT start new multi-step work
127
- 3. Run `/handoff` to generate a handoff document
128
- </system-reminder>"""
129
-
130
- else:
131
- return f"""<system-reminder>
132
- ## CONTEXT NOTICE ({percent_remaining}% remaining)
133
-
134
- {usage_line}
135
- **Triggered by**: {tool_name} tool completion
136
-
137
- **Consider preparing a handoff soon. When ready, run `/handoff` to generate a handoff document.**
138
- {context_line}
139
-
140
- Continue your current work, but avoid starting large new tasks.
141
- </system-reminder>"""
142
-
143
-
144
- def check_and_transition_mode(hook_input: dict) -> None:
145
- """
146
- Check if context mode needs to transition based on tool usage.
147
-
148
- Handles:
149
- - has_plan + implementation tool -> active (started implementing)
150
- - idle + implementation tool -> active
151
- """
152
- project_root = project_dir(hook_input)
153
- session_id = hook_input.get("session_id")
154
-
155
- if not session_id:
156
- return
157
-
158
- state = get_context_by_session_id(session_id, project_root)
159
- if not state:
160
- return
161
-
162
- # Implementation transitions only trigger on implementation tools
163
- implementation_tools = {"Edit", "Write", "Bash", "NotebookEdit"}
164
- tool_name = hook_input.get("tool_name", "")
165
-
166
- if tool_name not in implementation_tools:
167
- return
168
-
169
- permission_mode = hook_input.get("permission_mode", "default")
170
- maybe_activate(state.id, permission_mode, project_root=project_root, caller="context_monitor")
171
-
172
-
173
- def _try_progressive_save(hook_input: dict, percent_remaining: int) -> None:
174
- """Silently save state at SAVE_STATE_THRESHOLD (60%)."""
175
- try:
176
- session_id = hook_input.get("session_id", "")
177
- if not session_id:
178
- return
179
-
180
- project_root = project_dir(hook_input)
181
- state = get_context_by_session_id(session_id, project_root)
182
- if not state:
183
- return
184
-
185
- from lib.base.constants import get_context_dir
186
- marker_path = get_context_dir(state.id, project_root) / _PROGRESSIVE_SAVE_MARKER
187
- if marker_path.exists():
188
- try:
189
- saved_session = marker_path.read_text(encoding="utf-8").strip()
190
- if saved_session == session_id:
191
- return
192
- except OSError:
193
- pass
194
-
195
- log_info("context_monitor", f"Progressive save at {percent_remaining}% remaining")
196
-
197
- # Just update last_active and save state
198
- state.last_active = now_iso()
199
- save_state(state, project_root)
200
-
201
- try:
202
- marker_path.write_text(session_id, encoding="utf-8")
203
- except OSError:
204
- pass
205
-
206
- except Exception as e:
207
- log_warn("context_monitor", f"Progressive save error (non-fatal): {e}")
208
-
209
-
210
- def check_context_level(hook_input: dict) -> Optional[str]:
211
- """Check context level and return warning if low."""
212
- tool_name = hook_input.get("tool_name", "Unknown")
213
- percent_remaining, tokens_used, max_tokens = get_context_percent_remaining(hook_input)
214
-
215
- log_diagnostic("context_monitor", "receive", f"tool={tool_name}, pct_remaining={percent_remaining}",
216
- inputs={"tool_name": tool_name, "percent_remaining": percent_remaining,
217
- "tokens_used": tokens_used, "max_tokens": max_tokens})
218
-
219
- if percent_remaining is None:
220
- return None
221
-
222
- if percent_remaining > SAVE_STATE_THRESHOLD:
223
- return None
224
-
225
- if percent_remaining > HANDOFF_SUGGEST_THRESHOLD:
226
- _try_progressive_save(hook_input, percent_remaining)
227
- return None
228
-
229
- if tokens_used is not None and max_tokens is not None:
230
- log_info("context_monitor", f"Context: {percent_remaining}% remaining "
231
- f"(~{tokens_used//1000}k/{max_tokens//1000}k tokens)")
232
- else:
233
- log_info("context_monitor", f"Context: ~{percent_remaining}% remaining (from context.json)")
234
-
235
- project_root = project_dir(hook_input)
236
- context_id = get_current_context_id(project_root)
237
-
238
- threshold = ("critical" if percent_remaining <= CRITICAL_CONTEXT_THRESHOLD
239
- else "prepare" if percent_remaining <= HANDOFF_PREPARE_THRESHOLD
240
- else "suggest")
241
- log_diagnostic("context_monitor", "decide", f"Threshold={threshold} at {percent_remaining}%",
242
- decision=threshold, reasoning=f"{percent_remaining}% remaining",
243
- inputs={"context_id": context_id, "percent_remaining": percent_remaining})
244
-
245
- return get_context_warning(percent_remaining, tokens_used, max_tokens, context_id, tool_name)
246
-
247
-
248
- def main():
249
- """Main entry point for PostToolUse hook."""
250
- try:
251
- hook_input = load_hook_input()
252
- if not hook_input:
253
- return
254
-
255
- check_and_transition_mode(hook_input)
256
-
257
- warning = check_context_level(hook_input)
258
- if warning:
259
- emit_context(warning)
260
-
261
- except Exception as e:
262
- import traceback
263
- tb = traceback.format_exc()
264
- from lib.base.hook_utils import log_hook_error
265
- log_hook_error("context_monitor", e, "PostToolUse", traceback_str=tb)
266
-
267
-
268
- if __name__ == "__main__":
269
- from lib.base.hook_utils import run_hook
270
- run_hook(main, "context_monitor")