aiwcli 0.12.3 → 0.12.7

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 (125) hide show
  1. package/bin/dev.cmd +3 -3
  2. package/bin/dev.js +16 -16
  3. package/bin/run.cmd +3 -3
  4. package/bin/run.js +21 -21
  5. package/dist/commands/branch.js +7 -2
  6. package/dist/lib/bmad-installer.js +37 -37
  7. package/dist/lib/terminal.d.ts +2 -0
  8. package/dist/lib/terminal.js +57 -7
  9. package/dist/templates/CLAUDE.md +205 -205
  10. package/dist/templates/_shared/.claude/commands/handoff-resume.md +12 -64
  11. package/dist/templates/_shared/.claude/commands/handoff.md +12 -198
  12. package/dist/templates/_shared/.claude/settings.json +65 -65
  13. package/dist/templates/_shared/.codex/workflows/handoff.md +226 -226
  14. package/dist/templates/_shared/.windsurf/workflows/handoff.md +226 -226
  15. package/dist/templates/_shared/handoff-system/CLAUDE.md +421 -0
  16. package/dist/templates/_shared/{lib-ts/handoff → handoff-system/lib}/document-generator.ts +215 -216
  17. package/dist/templates/_shared/{lib-ts/handoff → handoff-system/lib}/handoff-reader.ts +157 -158
  18. package/dist/templates/_shared/{scripts → handoff-system/scripts}/resume_handoff.ts +373 -373
  19. package/dist/templates/_shared/{scripts → handoff-system/scripts}/save_handoff.ts +469 -358
  20. package/dist/templates/_shared/handoff-system/workflows/handoff-resume.md +66 -0
  21. package/dist/templates/_shared/{workflows → handoff-system/workflows}/handoff.md +254 -254
  22. package/dist/templates/_shared/hooks-ts/_utils/git-state.ts +2 -2
  23. package/dist/templates/_shared/hooks-ts/archive_plan.ts +159 -159
  24. package/dist/templates/_shared/hooks-ts/context_monitor.ts +147 -147
  25. package/dist/templates/_shared/hooks-ts/file-suggestion.ts +128 -128
  26. package/dist/templates/_shared/hooks-ts/pre_compact.ts +49 -49
  27. package/dist/templates/_shared/hooks-ts/session_end.ts +196 -183
  28. package/dist/templates/_shared/hooks-ts/session_start.ts +163 -151
  29. package/dist/templates/_shared/hooks-ts/task_create_capture.ts +48 -48
  30. package/dist/templates/_shared/hooks-ts/task_update_capture.ts +74 -74
  31. package/dist/templates/_shared/hooks-ts/user_prompt_submit.ts +93 -93
  32. package/dist/templates/_shared/lib-ts/CLAUDE.md +367 -367
  33. package/dist/templates/_shared/lib-ts/base/atomic-write.ts +138 -138
  34. package/dist/templates/_shared/lib-ts/base/constants.ts +303 -303
  35. package/dist/templates/_shared/lib-ts/base/git-state.ts +58 -58
  36. package/dist/templates/_shared/lib-ts/base/hook-utils.ts +582 -582
  37. package/dist/templates/_shared/lib-ts/base/inference.ts +301 -301
  38. package/dist/templates/_shared/lib-ts/base/logger.ts +247 -247
  39. package/dist/templates/_shared/lib-ts/base/state-io.ts +202 -130
  40. package/dist/templates/_shared/lib-ts/base/stop-words.ts +184 -184
  41. package/dist/templates/_shared/lib-ts/base/subprocess-utils.ts +56 -0
  42. package/dist/templates/_shared/lib-ts/base/utils.ts +184 -184
  43. package/dist/templates/_shared/lib-ts/context/context-formatter.ts +566 -560
  44. package/dist/templates/_shared/lib-ts/context/context-selector.ts +524 -515
  45. package/dist/templates/_shared/lib-ts/context/context-store.ts +712 -668
  46. package/dist/templates/_shared/lib-ts/context/plan-manager.ts +312 -312
  47. package/dist/templates/_shared/lib-ts/context/task-tracker.ts +185 -185
  48. package/dist/templates/_shared/lib-ts/package.json +20 -20
  49. package/dist/templates/_shared/lib-ts/templates/formatters.ts +102 -102
  50. package/dist/templates/_shared/lib-ts/templates/plan-context.ts +58 -58
  51. package/dist/templates/_shared/lib-ts/tsconfig.json +13 -13
  52. package/dist/templates/_shared/lib-ts/types.ts +186 -180
  53. package/dist/templates/_shared/scripts/resolve_context.ts +33 -33
  54. package/dist/templates/_shared/scripts/status_line.ts +690 -690
  55. package/dist/templates/cc-native/.claude/commands/{rlm → cc-native/rlm}/ask.md +136 -136
  56. package/dist/templates/cc-native/.claude/commands/{rlm → cc-native/rlm}/index.md +21 -21
  57. package/dist/templates/cc-native/.claude/commands/{rlm → cc-native/rlm}/overview.md +56 -56
  58. package/dist/templates/cc-native/.claude/commands/cc-native/specdev.md +10 -10
  59. package/dist/templates/cc-native/.windsurf/workflows/cc-native/fix.md +8 -8
  60. package/dist/templates/cc-native/.windsurf/workflows/cc-native/implement.md +8 -8
  61. package/dist/templates/cc-native/.windsurf/workflows/cc-native/research.md +8 -8
  62. package/dist/templates/cc-native/CC-NATIVE-README.md +189 -189
  63. package/dist/templates/cc-native/TEMPLATE-SCHEMA.md +304 -304
  64. package/dist/templates/cc-native/_cc-native/agents/CLAUDE.md +143 -143
  65. package/dist/templates/cc-native/_cc-native/agents/PLAN-ORCHESTRATOR.md +213 -213
  66. package/dist/templates/cc-native/_cc-native/agents/plan-questions/PLAN-QUESTIONER.md +70 -70
  67. package/dist/templates/cc-native/_cc-native/cc-native.config.json +96 -96
  68. package/dist/templates/cc-native/_cc-native/hooks/CLAUDE.md +247 -247
  69. package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.ts +76 -76
  70. package/dist/templates/cc-native/_cc-native/hooks/enhance_plan_post_subagent.ts +54 -54
  71. package/dist/templates/cc-native/_cc-native/hooks/enhance_plan_post_write.ts +51 -51
  72. package/dist/templates/cc-native/_cc-native/hooks/mark_questions_asked.ts +53 -53
  73. package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.ts +61 -61
  74. package/dist/templates/cc-native/_cc-native/lib-ts/agent-selection.ts +163 -163
  75. package/dist/templates/cc-native/_cc-native/lib-ts/aggregate-agents.ts +156 -156
  76. package/dist/templates/cc-native/_cc-native/lib-ts/artifacts/format.ts +597 -597
  77. package/dist/templates/cc-native/_cc-native/lib-ts/artifacts/index.ts +26 -26
  78. package/dist/templates/cc-native/_cc-native/lib-ts/artifacts/tracker.ts +107 -107
  79. package/dist/templates/cc-native/_cc-native/lib-ts/artifacts/write.ts +119 -119
  80. package/dist/templates/cc-native/_cc-native/lib-ts/artifacts.ts +21 -21
  81. package/dist/templates/cc-native/_cc-native/lib-ts/cc-native-state.ts +319 -319
  82. package/dist/templates/cc-native/_cc-native/lib-ts/cli-output-parser.ts +144 -144
  83. package/dist/templates/cc-native/_cc-native/lib-ts/config.ts +57 -57
  84. package/dist/templates/cc-native/_cc-native/lib-ts/constants.ts +83 -83
  85. package/dist/templates/cc-native/_cc-native/lib-ts/corroboration.ts +119 -119
  86. package/dist/templates/cc-native/_cc-native/lib-ts/debug.ts +79 -79
  87. package/dist/templates/cc-native/_cc-native/lib-ts/graduation.ts +132 -132
  88. package/dist/templates/cc-native/_cc-native/lib-ts/index.ts +116 -116
  89. package/dist/templates/cc-native/_cc-native/lib-ts/json-parser.ts +168 -168
  90. package/dist/templates/cc-native/_cc-native/lib-ts/orchestrator.ts +70 -70
  91. package/dist/templates/cc-native/_cc-native/lib-ts/output-builder.ts +130 -130
  92. package/dist/templates/cc-native/_cc-native/lib-ts/plan-discovery.ts +80 -80
  93. package/dist/templates/cc-native/_cc-native/lib-ts/plan-enhancement.ts +41 -41
  94. package/dist/templates/cc-native/_cc-native/lib-ts/plan-questions.ts +101 -101
  95. package/dist/templates/cc-native/_cc-native/lib-ts/review-pipeline.ts +511 -511
  96. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/agent.ts +71 -71
  97. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/base/base-agent.ts +217 -217
  98. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/index.ts +12 -12
  99. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/providers/claude-agent.ts +66 -65
  100. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/providers/codex-agent.ts +184 -184
  101. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/providers/gemini-agent.ts +39 -39
  102. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/providers/orchestrator-claude-agent.ts +196 -195
  103. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/schemas.ts +201 -201
  104. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/types.ts +21 -21
  105. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/CLAUDE.md +480 -480
  106. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/embedding-indexer.ts +287 -287
  107. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/hyde.ts +148 -148
  108. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/index.ts +54 -54
  109. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/logger.ts +58 -58
  110. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/ollama-client.ts +208 -208
  111. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/retrieval-pipeline.ts +460 -460
  112. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/transcript-indexer.ts +446 -447
  113. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/transcript-loader.ts +280 -280
  114. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/transcript-searcher.ts +274 -274
  115. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/types.ts +201 -201
  116. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/vector-store.ts +278 -278
  117. package/dist/templates/cc-native/_cc-native/lib-ts/settings.ts +184 -184
  118. package/dist/templates/cc-native/_cc-native/lib-ts/state.ts +275 -275
  119. package/dist/templates/cc-native/_cc-native/lib-ts/tsconfig.json +18 -18
  120. package/dist/templates/cc-native/_cc-native/lib-ts/types.ts +329 -329
  121. package/dist/templates/cc-native/_cc-native/lib-ts/verdict.ts +72 -72
  122. package/dist/templates/cc-native/_cc-native/workflows/specdev.md +9 -9
  123. package/oclif.manifest.json +1 -1
  124. package/package.json +108 -108
  125. package/dist/templates/cc-native/_cc-native/lib-ts/nul +0 -3
@@ -1,159 +1,159 @@
1
- #!/usr/bin/env bun
2
- /**
3
- * PermissionRequest:ExitPlanMode hook: Archive plan file to context's plans/ folder.
4
- * Runs before user accepts/rejects. Silent output.
5
- * Uses top-level await because archivePlan is async.
6
- */
7
- import * as fs from "node:fs";
8
- import * as os from "node:os";
9
- import * as path from "node:path";
10
-
11
- import { getContextDir, getProjectRoot } from "../lib-ts/base/constants.js";
12
- import {
13
- loadHookInput, logDebug, logError, logInfo, logWarn, runHookAsync,
14
- } from "../lib-ts/base/hook-utils.js";
15
- import { getContextBySessionId } from "../lib-ts/context/context-store.js";
16
- import {
17
- archivePlan, extractPlanPathFromResult, findPlanPathInTranscript,
18
- } from "../lib-ts/context/plan-manager.js";
19
-
20
- /** Find the most recent .md file in a directory */
21
- function mostRecentMd(dir: string): null | string {
22
- try {
23
- if (!fs.existsSync(dir)) return null;
24
- const entries = fs.readdirSync(dir, { withFileTypes: true });
25
- let best: null | { mtime: number; path: string; } = null;
26
- for (const e of entries) {
27
- if (!e.isFile() || !e.name.endsWith(".md")) continue;
28
- const fullPath = path.join(dir, e.name);
29
- const stat = fs.statSync(fullPath);
30
- if (!best || stat.mtimeMs > best.mtime) {
31
- best = { path: fullPath, mtime: stat.mtimeMs };
32
- }
33
- }
34
-
35
- return best?.path ?? null;
36
- } catch {
37
- return null;
38
- }
39
- }
40
-
41
- /** Multi-strategy plan path discovery */
42
- function findPlanPath(payload: Record<string, any>, projectRoot: string): null | string {
43
- const toolResult = payload.tool_result as string | undefined;
44
- const toolInput = (payload.tool_input ?? {}) as Record<string, any>;
45
- const transcriptPath = payload.transcript_path as string | undefined;
46
-
47
- // Strategy 1: Extract from tool result
48
- if (toolResult) {
49
- const fromResult = extractPlanPathFromResult(toolResult);
50
- if (fromResult) {
51
- logDebug("archive_plan", `Found plan path in tool_result: ${fromResult}`);
52
- return fromResult;
53
- }
54
- }
55
-
56
- // Strategy 2: Check tool_input fields
57
- const inputPath = (toolInput.plan_path ?? toolInput.planPath) as string | undefined;
58
- if (inputPath) {
59
- logDebug("archive_plan", `Found plan path in tool_input: ${inputPath}`);
60
- return inputPath;
61
- }
62
-
63
- // Strategy 3: Parse transcript for most recent Write to .claude/plans/
64
- if (transcriptPath) {
65
- const fromTranscript = findPlanPathInTranscript(transcriptPath);
66
- if (fromTranscript) return fromTranscript;
67
- }
68
-
69
- // Strategy 4: Most recent .md in ~/.claude/plans/
70
- const claudePlansDir = path.join(os.homedir(), ".claude", "plans");
71
- const recentPlan = mostRecentMd(claudePlansDir);
72
- if (recentPlan) {
73
- logDebug("archive_plan", `Found plan in ~/.claude/plans/: ${recentPlan}`);
74
- return recentPlan;
75
- }
76
-
77
- // Strategy 5: Fallback paths
78
- const fallbacks = [
79
- path.join(projectRoot, "_output", "cc-native", "plans", "current-plan.md"),
80
- path.join(projectRoot, "_output", "plans", "current-plan.md"),
81
- path.join(projectRoot, "plan.md"),
82
- ];
83
- for (const fb of fallbacks) {
84
- if (fs.existsSync(fb)) {
85
- logDebug("archive_plan", `Found plan at fallback: ${fb}`);
86
- return fb;
87
- }
88
- }
89
-
90
- return null;
91
- }
92
-
93
- async function asyncMain(): Promise<void> {
94
- const payload = loadHookInput();
95
- if (!payload) return;
96
-
97
- // Validate event
98
- if (payload.hook_event_name !== "PermissionRequest" || payload.tool_name !== "ExitPlanMode") {
99
- return;
100
- }
101
-
102
- // Check stop flag
103
- if ((payload as any).stop_hook_active) {
104
- logDebug("archive_plan", "stop_hook_active set, skipping");
105
- return;
106
- }
107
-
108
- const projectRoot = getProjectRoot(payload.cwd);
109
- const sessionId = payload.session_id;
110
- if (!sessionId) {
111
- logWarn("archive_plan", "No session_id");
112
- return;
113
- }
114
-
115
- // Find plan path
116
- let planPath = findPlanPath(payload as Record<string, any>, projectRoot);
117
- if (!planPath) {
118
- logWarn("archive_plan", "Could not locate plan file");
119
- return;
120
- }
121
-
122
- // Resolve to absolute
123
- if (!path.isAbsolute(planPath)) {
124
- planPath = path.resolve(projectRoot, planPath);
125
- }
126
-
127
- // Verify exists
128
- if (!fs.existsSync(planPath)) {
129
- logWarn("archive_plan", `Plan file not found: ${planPath}`);
130
- return;
131
- }
132
-
133
- // Find bound context
134
- const state = getContextBySessionId(sessionId, projectRoot);
135
- if (!state) {
136
- logWarn("archive_plan", `No context bound to session ${sessionId}`);
137
- return;
138
- }
139
-
140
- // Archive the plan (async — uses AI for slug generation)
141
- const [archivedPath, planHash, _planSignature] = await archivePlan(planPath, state.id, projectRoot);
142
-
143
- if (archivedPath) {
144
- // Clean up debug logs (best effort, matches Python behavior)
145
- try {
146
- const ctxDir = getContextDir(state.id, projectRoot);
147
- const debugDir = path.join(ctxDir, "debug");
148
- if (fs.existsSync(debugDir)) {
149
- fs.rmSync(debugDir, { recursive: true, force: true });
150
- }
151
- } catch { /* best effort */ }
152
-
153
- logInfo("archive_plan", `Archived plan to ${archivedPath} (hash=${planHash})`);
154
- } else {
155
- logError("archive_plan", "archivePlan returned null");
156
- }
157
- }
158
-
159
- runHookAsync(asyncMain, "archive_plan");
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * PermissionRequest:ExitPlanMode hook: Archive plan file to context's plans/ folder.
4
+ * Runs before user accepts/rejects. Silent output.
5
+ * Uses top-level await because archivePlan is async.
6
+ */
7
+ import * as fs from "node:fs";
8
+ import * as os from "node:os";
9
+ import * as path from "node:path";
10
+
11
+ import { getContextDir, getProjectRoot } from "../lib-ts/base/constants.js";
12
+ import {
13
+ loadHookInput, logDebug, logError, logInfo, logWarn, runHookAsync,
14
+ } from "../lib-ts/base/hook-utils.js";
15
+ import { getContextBySessionId } from "../lib-ts/context/context-store.js";
16
+ import {
17
+ archivePlan, extractPlanPathFromResult, findPlanPathInTranscript,
18
+ } from "../lib-ts/context/plan-manager.js";
19
+
20
+ /** Find the most recent .md file in a directory */
21
+ function mostRecentMd(dir: string): null | string {
22
+ try {
23
+ if (!fs.existsSync(dir)) return null;
24
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
25
+ let best: null | { mtime: number; path: string; } = null;
26
+ for (const e of entries) {
27
+ if (!e.isFile() || !e.name.endsWith(".md")) continue;
28
+ const fullPath = path.join(dir, e.name);
29
+ const stat = fs.statSync(fullPath);
30
+ if (!best || stat.mtimeMs > best.mtime) {
31
+ best = { path: fullPath, mtime: stat.mtimeMs };
32
+ }
33
+ }
34
+
35
+ return best?.path ?? null;
36
+ } catch {
37
+ return null;
38
+ }
39
+ }
40
+
41
+ /** Multi-strategy plan path discovery */
42
+ function findPlanPath(payload: Record<string, any>, projectRoot: string): null | string {
43
+ const toolResult = payload.tool_result as string | undefined;
44
+ const toolInput = (payload.tool_input ?? {}) as Record<string, any>;
45
+ const transcriptPath = payload.transcript_path as string | undefined;
46
+
47
+ // Strategy 1: Extract from tool result
48
+ if (toolResult) {
49
+ const fromResult = extractPlanPathFromResult(toolResult);
50
+ if (fromResult) {
51
+ logDebug("archive_plan", `Found plan path in tool_result: ${fromResult}`);
52
+ return fromResult;
53
+ }
54
+ }
55
+
56
+ // Strategy 2: Check tool_input fields
57
+ const inputPath = (toolInput.plan_path ?? toolInput.planPath) as string | undefined;
58
+ if (inputPath) {
59
+ logDebug("archive_plan", `Found plan path in tool_input: ${inputPath}`);
60
+ return inputPath;
61
+ }
62
+
63
+ // Strategy 3: Parse transcript for most recent Write to .claude/plans/
64
+ if (transcriptPath) {
65
+ const fromTranscript = findPlanPathInTranscript(transcriptPath);
66
+ if (fromTranscript) return fromTranscript;
67
+ }
68
+
69
+ // Strategy 4: Most recent .md in ~/.claude/plans/
70
+ const claudePlansDir = path.join(os.homedir(), ".claude", "plans");
71
+ const recentPlan = mostRecentMd(claudePlansDir);
72
+ if (recentPlan) {
73
+ logDebug("archive_plan", `Found plan in ~/.claude/plans/: ${recentPlan}`);
74
+ return recentPlan;
75
+ }
76
+
77
+ // Strategy 5: Fallback paths
78
+ const fallbacks = [
79
+ path.join(projectRoot, "_output", "cc-native", "plans", "current-plan.md"),
80
+ path.join(projectRoot, "_output", "plans", "current-plan.md"),
81
+ path.join(projectRoot, "plan.md"),
82
+ ];
83
+ for (const fb of fallbacks) {
84
+ if (fs.existsSync(fb)) {
85
+ logDebug("archive_plan", `Found plan at fallback: ${fb}`);
86
+ return fb;
87
+ }
88
+ }
89
+
90
+ return null;
91
+ }
92
+
93
+ async function asyncMain(): Promise<void> {
94
+ const payload = loadHookInput();
95
+ if (!payload) return;
96
+
97
+ // Validate event
98
+ if (payload.hook_event_name !== "PermissionRequest" || payload.tool_name !== "ExitPlanMode") {
99
+ return;
100
+ }
101
+
102
+ // Check stop flag
103
+ if ((payload as any).stop_hook_active) {
104
+ logDebug("archive_plan", "stop_hook_active set, skipping");
105
+ return;
106
+ }
107
+
108
+ const projectRoot = getProjectRoot(payload.cwd);
109
+ const sessionId = payload.session_id;
110
+ if (!sessionId) {
111
+ logWarn("archive_plan", "No session_id");
112
+ return;
113
+ }
114
+
115
+ // Find plan path
116
+ let planPath = findPlanPath(payload as Record<string, any>, projectRoot);
117
+ if (!planPath) {
118
+ logWarn("archive_plan", "Could not locate plan file");
119
+ return;
120
+ }
121
+
122
+ // Resolve to absolute
123
+ if (!path.isAbsolute(planPath)) {
124
+ planPath = path.resolve(projectRoot, planPath);
125
+ }
126
+
127
+ // Verify exists
128
+ if (!fs.existsSync(planPath)) {
129
+ logWarn("archive_plan", `Plan file not found: ${planPath}`);
130
+ return;
131
+ }
132
+
133
+ // Find bound context
134
+ const state = getContextBySessionId(sessionId, projectRoot);
135
+ if (!state) {
136
+ logWarn("archive_plan", `No context bound to session ${sessionId}`);
137
+ return;
138
+ }
139
+
140
+ // Archive the plan (async — uses AI for slug generation)
141
+ const [archivedPath, planHash, _planSignature] = await archivePlan(planPath, state.id, projectRoot);
142
+
143
+ if (archivedPath) {
144
+ // Clean up debug logs (best effort, matches Python behavior)
145
+ try {
146
+ const ctxDir = getContextDir(state.id, projectRoot);
147
+ const debugDir = path.join(ctxDir, "debug");
148
+ if (fs.existsSync(debugDir)) {
149
+ fs.rmSync(debugDir, { recursive: true, force: true });
150
+ }
151
+ } catch { /* best effort */ }
152
+
153
+ logInfo("archive_plan", `Archived plan to ${archivedPath} (hash=${planHash})`);
154
+ } else {
155
+ logError("archive_plan", "archivePlan returned null");
156
+ }
157
+ }
158
+
159
+ runHookAsync(asyncMain, "archive_plan");
@@ -1,147 +1,147 @@
1
- #!/usr/bin/env bun
2
- /**
3
- * PostToolUse:* hook: Monitor context window usage, trigger mode transitions,
4
- * and progressive-save state when context runs low.
5
- */
6
- import { getProjectRoot } from "../lib-ts/base/constants.js";
7
- import {
8
- emitContext, getContextPercentRemaining, hookLog,
9
- loadHookInput,
10
- logDebug, logDiagnostic, logInfo, logWarn, runHook,
11
- } from "../lib-ts/base/hook-utils.js";
12
- import { nowIso } from "../lib-ts/base/utils.js";
13
- import { getContextBySessionId, maybeActivate, saveState } from "../lib-ts/context/context-store.js";
14
- import type { ContextState } from "../lib-ts/types.js";
15
-
16
- const WRITE_TOOLS = new Set(["Bash", "Edit", "NotebookEdit", "Write"]);
17
-
18
- const SAVE_STATE_THRESHOLD = 60;
19
-
20
- const CONTEXT_WARNING_30 = "## Context Window: ~30% Remaining\n\n" +
21
- "This session is approaching its context limit. Consider:\n\n" +
22
- "- Completing your current task, then pausing for the user to decide next steps\n" +
23
- "- If significant work remains, mention that `/handoff` can capture progress " +
24
- "for a fresh session\n\n" +
25
- "Do not rush or cut corners — finish the current task properly. " +
26
- "Just be aware that starting large new tasks may not complete before context runs out.";
27
-
28
- const CONTEXT_WARNING_15 = "## Context Window: ~15% Remaining — Wrap Up Now\n\n" +
29
- "Context is critically low. After completing your current step:\n\n" +
30
- "1. **Stop taking on new work**\n" +
31
- "2. Summarize what was accomplished and what remains\n" +
32
- "3. Offer to run `/handoff` so progress transfers to a fresh session\n\n" +
33
- "Do not start new multi-step tasks. Focus on clean closure.";
34
-
35
- const WARNING_THRESHOLDS = [
36
- { pct: 15, msg: CONTEXT_WARNING_15 }, // Most urgent first
37
- { pct: 30, msg: CONTEXT_WARNING_30 },
38
- ];
39
-
40
- /** Transition idle/has_plan → active when implementation tools are used. */
41
- function checkAndTransitionMode(
42
- state: ContextState,
43
- toolName: string | undefined,
44
- permissionMode: string,
45
- projectRoot: string,
46
- ): void {
47
- if (!toolName || !WRITE_TOOLS.has(toolName)) return;
48
- try {
49
- maybeActivate(state.id, permissionMode, projectRoot, "context_monitor");
50
- } catch (error) {
51
- logWarn("context_monitor", `maybeActivate failed (non-critical): ${error}`);
52
- }
53
- }
54
-
55
- /** Save state snapshot at SAVE_STATE_THRESHOLD. */
56
- function progressiveSave(
57
- state: ContextState,
58
- sessionId: string,
59
- projectRoot: string,
60
- ): void {
61
- state.last_session = {
62
- ...state.last_session,
63
- session_id: sessionId,
64
- saved_at: nowIso(),
65
- save_reason: "progressive_save",
66
- };
67
- state.last_active = nowIso();
68
-
69
- const [ok] = saveState(state.id, state, projectRoot);
70
- if (ok) {
71
- logInfo("context_monitor", `Progressive save for ${state.id}`);
72
- }
73
- }
74
-
75
- /** Emit context-low nudge if a new threshold is crossed. Fires at most once per threshold per session. */
76
- function checkContextWarnings(
77
- state: ContextState,
78
- pctRemaining: number,
79
- projectRoot: string,
80
- ): void {
81
- if (!state.last_session) {
82
- state.last_session = {};
83
- }
84
- const fired = state.last_session.context_warnings_fired ?? [];
85
-
86
- for (const { pct, msg } of WARNING_THRESHOLDS) {
87
- if (pctRemaining <= pct && !fired.includes(pct)) {
88
- emitContext(msg);
89
- state.last_session.context_warnings_fired = [...fired, pct];
90
- saveState(state.id, state, projectRoot);
91
- logInfo("context_monitor", `Context warning emitted at ${pct}% threshold`);
92
- return; // One warning per tool call — most urgent fires first
93
- }
94
- }
95
- }
96
-
97
- function main(): void {
98
- const payload = loadHookInput();
99
- if (!payload) return;
100
-
101
- const sessionId = payload.session_id;
102
- if (!sessionId) return;
103
-
104
- const projectRoot = getProjectRoot(payload.cwd);
105
- const permissionMode = payload.permission_mode ?? "";
106
- const toolName = payload.tool_name;
107
-
108
- // Initial context lookup
109
- let state = getContextBySessionId(sessionId, projectRoot);
110
- if (!state) {
111
- logDebug("context_monitor", `No context for session ${sessionId}`);
112
- return;
113
- }
114
-
115
- // Phase 1: Mode transition for write tools
116
- checkAndTransitionMode(state, toolName, permissionMode, projectRoot);
117
-
118
- // Phase 2: Context window check (log only, no warnings emitted)
119
- const [pctRemaining, tokensUsed, maxTokens] = getContextPercentRemaining(payload);
120
-
121
- logDiagnostic("context_monitor", "receive", `tool=${toolName ?? "Unknown"}, pct_remaining=${pctRemaining}`);
122
-
123
- if (pctRemaining === null) {
124
- logDebug("context_monitor", "No context window data available");
125
- return;
126
- }
127
-
128
- if (pctRemaining > SAVE_STATE_THRESHOLD) return;
129
-
130
- // Reload state after maybeActivate may have mutated it on disk
131
- state = getContextBySessionId(sessionId, projectRoot) ?? state;
132
-
133
- // Progressive save for ≤ 60%
134
- progressiveSave(state, sessionId, projectRoot);
135
-
136
- // Context-low warnings (independent of save threshold)
137
- checkContextWarnings(state, pctRemaining, projectRoot);
138
-
139
- // Log context level (file only)
140
- if (tokensUsed !== null && maxTokens !== null) {
141
- hookLog("info", "context_monitor", `Context: ${pctRemaining}% remaining (~${Math.floor(tokensUsed / 1000)}k/${Math.floor(maxTokens / 1000)}k tokens)`, { stderr: false });
142
- } else {
143
- hookLog("info", "context_monitor", `Context: ~${pctRemaining}% remaining (from context.json)`, { stderr: false });
144
- }
145
- }
146
-
147
- runHook(main, "context_monitor");
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * PostToolUse:* hook: Monitor context window usage, trigger mode transitions,
4
+ * and progressive-save state when context runs low.
5
+ */
6
+ import { getProjectRoot } from "../lib-ts/base/constants.js";
7
+ import {
8
+ emitContext, getContextPercentRemaining, hookLog,
9
+ loadHookInput,
10
+ logDebug, logDiagnostic, logInfo, logWarn, runHook,
11
+ } from "../lib-ts/base/hook-utils.js";
12
+ import { nowIso } from "../lib-ts/base/utils.js";
13
+ import { getContextBySessionId, maybeActivate, saveState } from "../lib-ts/context/context-store.js";
14
+ import type { ContextState } from "../lib-ts/types.js";
15
+
16
+ const WRITE_TOOLS = new Set(["Bash", "Edit", "NotebookEdit", "Write"]);
17
+
18
+ const SAVE_STATE_THRESHOLD = 60;
19
+
20
+ const CONTEXT_WARNING_30 = "## Context Window: ~30% Remaining\n\n" +
21
+ "This session is approaching its context limit. Consider:\n\n" +
22
+ "- Completing your current task, then pausing for the user to decide next steps\n" +
23
+ "- If significant work remains, mention that `/handoff` can capture progress " +
24
+ "for a fresh session\n\n" +
25
+ "Do not rush or cut corners — finish the current task properly. " +
26
+ "Just be aware that starting large new tasks may not complete before context runs out.";
27
+
28
+ const CONTEXT_WARNING_15 = "## Context Window: ~15% Remaining — Wrap Up Now\n\n" +
29
+ "Context is critically low. After completing your current step:\n\n" +
30
+ "1. **Stop taking on new work**\n" +
31
+ "2. Summarize what was accomplished and what remains\n" +
32
+ "3. Offer to run `/handoff` so progress transfers to a fresh session\n\n" +
33
+ "Do not start new multi-step tasks. Focus on clean closure.";
34
+
35
+ const WARNING_THRESHOLDS = [
36
+ { pct: 15, msg: CONTEXT_WARNING_15 }, // Most urgent first
37
+ { pct: 30, msg: CONTEXT_WARNING_30 },
38
+ ];
39
+
40
+ /** Transition idle/has_plan → active when implementation tools are used. */
41
+ function checkAndTransitionMode(
42
+ state: ContextState,
43
+ toolName: string | undefined,
44
+ permissionMode: string,
45
+ projectRoot: string,
46
+ ): void {
47
+ if (!toolName || !WRITE_TOOLS.has(toolName)) return;
48
+ try {
49
+ maybeActivate(state.id, permissionMode, projectRoot, "context_monitor");
50
+ } catch (error) {
51
+ logWarn("context_monitor", `maybeActivate failed (non-critical): ${error}`);
52
+ }
53
+ }
54
+
55
+ /** Save state snapshot at SAVE_STATE_THRESHOLD. */
56
+ function progressiveSave(
57
+ state: ContextState,
58
+ sessionId: string,
59
+ projectRoot: string,
60
+ ): void {
61
+ state.last_session = {
62
+ ...state.last_session,
63
+ session_id: sessionId,
64
+ saved_at: nowIso(),
65
+ save_reason: "progressive_save",
66
+ };
67
+ state.last_active = nowIso();
68
+
69
+ const [ok] = saveState(state.id, state, projectRoot);
70
+ if (ok) {
71
+ logInfo("context_monitor", `Progressive save for ${state.id}`);
72
+ }
73
+ }
74
+
75
+ /** Emit context-low nudge if a new threshold is crossed. Fires at most once per threshold per session. */
76
+ function checkContextWarnings(
77
+ state: ContextState,
78
+ pctRemaining: number,
79
+ projectRoot: string,
80
+ ): void {
81
+ if (!state.last_session) {
82
+ state.last_session = {};
83
+ }
84
+ const fired = state.last_session.context_warnings_fired ?? [];
85
+
86
+ for (const { pct, msg } of WARNING_THRESHOLDS) {
87
+ if (pctRemaining <= pct && !fired.includes(pct)) {
88
+ emitContext(msg);
89
+ state.last_session.context_warnings_fired = [...fired, pct];
90
+ saveState(state.id, state, projectRoot);
91
+ logInfo("context_monitor", `Context warning emitted at ${pct}% threshold`);
92
+ return; // One warning per tool call — most urgent fires first
93
+ }
94
+ }
95
+ }
96
+
97
+ function main(): void {
98
+ const payload = loadHookInput();
99
+ if (!payload) return;
100
+
101
+ const sessionId = payload.session_id;
102
+ if (!sessionId) return;
103
+
104
+ const projectRoot = getProjectRoot(payload.cwd);
105
+ const permissionMode = payload.permission_mode ?? "";
106
+ const toolName = payload.tool_name;
107
+
108
+ // Initial context lookup
109
+ let state = getContextBySessionId(sessionId, projectRoot);
110
+ if (!state) {
111
+ logDebug("context_monitor", `No context for session ${sessionId}`);
112
+ return;
113
+ }
114
+
115
+ // Phase 1: Mode transition for write tools
116
+ checkAndTransitionMode(state, toolName, permissionMode, projectRoot);
117
+
118
+ // Phase 2: Context window check (log only, no warnings emitted)
119
+ const [pctRemaining, tokensUsed, maxTokens] = getContextPercentRemaining(payload);
120
+
121
+ logDiagnostic("context_monitor", "receive", `tool=${toolName ?? "Unknown"}, pct_remaining=${pctRemaining}`);
122
+
123
+ if (pctRemaining === null) {
124
+ logDebug("context_monitor", "No context window data available");
125
+ return;
126
+ }
127
+
128
+ if (pctRemaining > SAVE_STATE_THRESHOLD) return;
129
+
130
+ // Reload state after maybeActivate may have mutated it on disk
131
+ state = getContextBySessionId(sessionId, projectRoot) ?? state;
132
+
133
+ // Progressive save for ≤ 60%
134
+ progressiveSave(state, sessionId, projectRoot);
135
+
136
+ // Context-low warnings (independent of save threshold)
137
+ checkContextWarnings(state, pctRemaining, projectRoot);
138
+
139
+ // Log context level (file only)
140
+ if (tokensUsed !== null && maxTokens !== null) {
141
+ hookLog("info", "context_monitor", `Context: ${pctRemaining}% remaining (~${Math.floor(tokensUsed / 1000)}k/${Math.floor(maxTokens / 1000)}k tokens)`, { stderr: false });
142
+ } else {
143
+ hookLog("info", "context_monitor", `Context: ~${pctRemaining}% remaining (from context.json)`, { stderr: false });
144
+ }
145
+ }
146
+
147
+ runHook(main, "context_monitor");