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.
- package/bin/run.js +1 -1
- package/dist/commands/clear.js +28 -131
- package/dist/commands/init/index.js +3 -3
- package/dist/lib/gitignore-manager.d.ts +32 -0
- package/dist/lib/gitignore-manager.js +141 -2
- package/dist/templates/CLAUDE.md +8 -8
- package/dist/templates/_shared/.claude/commands/handoff-resume.md +64 -0
- package/dist/templates/_shared/.claude/commands/handoff.md +16 -10
- package/dist/templates/_shared/.claude/settings.json +7 -7
- package/dist/templates/_shared/hooks-ts/_utils/git-state.ts +2 -0
- package/dist/templates/_shared/hooks-ts/archive_plan.ts +159 -0
- package/dist/templates/_shared/hooks-ts/context_monitor.ts +147 -0
- package/dist/templates/_shared/hooks-ts/file-suggestion.ts +130 -0
- package/dist/templates/_shared/hooks-ts/pre_compact.ts +49 -0
- package/dist/templates/_shared/hooks-ts/session_end.ts +107 -0
- package/dist/templates/_shared/hooks-ts/session_start.ts +144 -0
- package/dist/templates/_shared/hooks-ts/task_create_capture.ts +48 -0
- package/dist/templates/_shared/hooks-ts/task_update_capture.ts +74 -0
- package/dist/templates/_shared/hooks-ts/user_prompt_submit.ts +83 -0
- package/dist/templates/_shared/lib-ts/CLAUDE.md +318 -0
- package/dist/templates/_shared/lib-ts/base/atomic-write.ts +12 -12
- package/dist/templates/_shared/lib-ts/base/constants.ts +22 -15
- package/dist/templates/_shared/lib-ts/base/git-state.ts +1 -1
- package/dist/templates/_shared/lib-ts/base/hook-utils.ts +129 -50
- package/dist/templates/_shared/lib-ts/base/inference.ts +28 -21
- package/dist/templates/_shared/lib-ts/base/logger.ts +15 -2
- package/dist/templates/_shared/lib-ts/base/state-io.ts +9 -7
- package/dist/templates/_shared/lib-ts/base/stop-words.ts +131 -131
- package/dist/templates/_shared/lib-ts/base/subprocess-utils.ts +142 -0
- package/dist/templates/_shared/lib-ts/base/utils.ts +69 -69
- package/dist/templates/_shared/lib-ts/context/context-formatter.ts +30 -24
- package/dist/templates/_shared/lib-ts/context/context-selector.ts +50 -32
- package/dist/templates/_shared/lib-ts/context/context-store.ts +76 -48
- package/dist/templates/_shared/lib-ts/context/plan-manager.ts +43 -23
- package/dist/templates/_shared/lib-ts/context/task-tracker.ts +10 -6
- package/dist/templates/_shared/lib-ts/handoff/document-generator.ts +11 -10
- package/dist/templates/_shared/lib-ts/handoff/handoff-reader.ts +158 -0
- package/dist/templates/_shared/lib-ts/templates/formatters.ts +6 -4
- package/dist/templates/_shared/lib-ts/types.ts +68 -55
- package/dist/templates/_shared/scripts/resolve_context.ts +24 -0
- package/dist/templates/_shared/scripts/resume_handoff.ts +345 -0
- package/dist/templates/_shared/scripts/save_handoff.ts +3 -3
- package/dist/templates/_shared/scripts/status_line.ts +687 -0
- package/dist/templates/cc-native/.claude/settings.json +175 -185
- package/dist/templates/cc-native/TEMPLATE-SCHEMA.md +15 -17
- package/dist/templates/cc-native/_cc-native/agents/CLAUDE.md +0 -2
- package/dist/templates/cc-native/_cc-native/hooks/CLAUDE.md +109 -135
- package/dist/templates/cc-native/_cc-native/hooks/add_plan_context.ts +119 -0
- package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.ts +1027 -0
- package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.ts +61 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/aggregate-agents.ts +156 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/artifacts.ts +792 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/cc-native-state.ts +199 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/cli-output-parser.ts +144 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/config.ts +57 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/constants.ts +83 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/corroboration.ts +115 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/debug.ts +80 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/index.ts +120 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/json-parser.ts +168 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/nul +3 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/orchestrator.ts +250 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/agent.ts +275 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/codex.ts +130 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/gemini.ts +107 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/index.ts +10 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/types.ts +23 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/state.ts +240 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/tsconfig.json +18 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/types.ts +385 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/verdict.ts +72 -0
- package/dist/templates/cc-native/_cc-native/plan-review.config.json +14 -1
- package/oclif.manifest.json +1 -1
- package/package.json +2 -2
- package/dist/templates/_shared/hooks/__init__.py +0 -16
- package/dist/templates/_shared/hooks/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/archive_plan.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/context_enforcer.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/context_monitor.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/file-suggestion.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/pre_compact.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/session_end.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/session_start.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/task_create_atomicity.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/task_create_capture.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/task_update_capture.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/user_prompt_submit.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/archive_plan.py +0 -177
- package/dist/templates/_shared/hooks/context_monitor.py +0 -270
- package/dist/templates/_shared/hooks/file-suggestion.py +0 -215
- package/dist/templates/_shared/hooks/pre_compact.py +0 -104
- package/dist/templates/_shared/hooks/session_end.py +0 -173
- package/dist/templates/_shared/hooks/session_start.py +0 -206
- package/dist/templates/_shared/hooks/task_create_capture.py +0 -108
- package/dist/templates/_shared/hooks/task_update_capture.py +0 -145
- package/dist/templates/_shared/hooks/user_prompt_submit.py +0 -139
- package/dist/templates/_shared/lib/__init__.py +0 -1
- package/dist/templates/_shared/lib/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__init__.py +0 -65
- package/dist/templates/_shared/lib/base/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/atomic_write.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/constants.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/hook_utils.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/inference.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/logger.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/stop_words.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/subprocess_utils.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/utils.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/atomic_write.py +0 -180
- package/dist/templates/_shared/lib/base/constants.py +0 -358
- package/dist/templates/_shared/lib/base/hook_utils.py +0 -339
- package/dist/templates/_shared/lib/base/inference.py +0 -307
- package/dist/templates/_shared/lib/base/logger.py +0 -305
- package/dist/templates/_shared/lib/base/stop_words.py +0 -221
- package/dist/templates/_shared/lib/base/subprocess_utils.py +0 -46
- package/dist/templates/_shared/lib/base/utils.py +0 -263
- package/dist/templates/_shared/lib/context/__init__.py +0 -102
- package/dist/templates/_shared/lib/context/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/cache.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/context_extractor.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/context_formatter.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/context_manager.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/context_selector.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/context_store.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/discovery.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/event_log.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/plan_archive.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/plan_manager.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/task_sync.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/task_tracker.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/context_formatter.py +0 -317
- package/dist/templates/_shared/lib/context/context_selector.py +0 -508
- package/dist/templates/_shared/lib/context/context_store.py +0 -653
- package/dist/templates/_shared/lib/context/plan_manager.py +0 -303
- package/dist/templates/_shared/lib/context/task_tracker.py +0 -188
- package/dist/templates/_shared/lib/handoff/__init__.py +0 -22
- package/dist/templates/_shared/lib/handoff/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/handoff/__pycache__/document_generator.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/handoff/document_generator.py +0 -278
- package/dist/templates/_shared/lib/templates/README.md +0 -206
- package/dist/templates/_shared/lib/templates/__init__.py +0 -36
- package/dist/templates/_shared/lib/templates/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/templates/__pycache__/formatters.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/templates/__pycache__/persona_questions.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/templates/__pycache__/plan_context.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/templates/formatters.py +0 -146
- package/dist/templates/_shared/lib/templates/plan_context.py +0 -73
- package/dist/templates/_shared/scripts/__pycache__/save_handoff.cpython-313.pyc +0 -0
- package/dist/templates/_shared/scripts/__pycache__/status_line.cpython-313.pyc +0 -0
- package/dist/templates/_shared/scripts/save_handoff.py +0 -357
- package/dist/templates/_shared/scripts/status_line.py +0 -716
- package/dist/templates/cc-native/.claude/commands/cc-native/fresh-perspective.md +0 -8
- package/dist/templates/cc-native/.windsurf/workflows/cc-native/fresh-perspective.md +0 -8
- package/dist/templates/cc-native/MIGRATION.md +0 -86
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/add_plan_context.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-plan-review.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/mark_questions_asked.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/plan_accepted.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/plan_questions_early.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/suggest-fresh-perspective.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/add_plan_context.py +0 -130
- package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.py +0 -954
- package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.py +0 -81
- package/dist/templates/cc-native/_cc-native/hooks/suggest-fresh-perspective.py +0 -340
- package/dist/templates/cc-native/_cc-native/lib/CLAUDE.md +0 -265
- package/dist/templates/cc-native/_cc-native/lib/__init__.py +0 -53
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/atomic_write.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/constants.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/debug.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/orchestrator.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/state.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/utils.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/constants.py +0 -45
- package/dist/templates/cc-native/_cc-native/lib/debug.py +0 -139
- package/dist/templates/cc-native/_cc-native/lib/orchestrator.py +0 -362
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__init__.py +0 -28
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/agent.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/base.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/codex.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/gemini.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/agent.py +0 -215
- package/dist/templates/cc-native/_cc-native/lib/reviewers/base.py +0 -88
- package/dist/templates/cc-native/_cc-native/lib/reviewers/codex.py +0 -124
- package/dist/templates/cc-native/_cc-native/lib/reviewers/gemini.py +0 -108
- package/dist/templates/cc-native/_cc-native/lib/state.py +0 -268
- package/dist/templates/cc-native/_cc-native/lib/utils.py +0 -1071
- package/dist/templates/cc-native/_cc-native/scripts/__pycache__/aggregate_agents.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/scripts/aggregate_agents.py +0 -168
- package/dist/templates/cc-native/_cc-native/workflows/fresh-perspective.md +0 -134
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"statusLine": {
|
|
3
3
|
"type": "command",
|
|
4
|
-
"command": "
|
|
4
|
+
"command": "bun .aiwcli/_shared/scripts/status_line.ts"
|
|
5
5
|
},
|
|
6
6
|
"fileSuggestion": {
|
|
7
7
|
"type": "command",
|
|
8
|
-
"command": "
|
|
8
|
+
"command": "bun .aiwcli/_shared/hooks-ts/file-suggestion.ts"
|
|
9
9
|
},
|
|
10
10
|
"hooks": {
|
|
11
11
|
"UserPromptSubmit": [
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"hooks": [
|
|
14
14
|
{
|
|
15
15
|
"type": "command",
|
|
16
|
-
"command": "
|
|
16
|
+
"command": "bun .aiwcli/_shared/hooks-ts/user_prompt_submit.ts",
|
|
17
17
|
"timeout": 5000
|
|
18
18
|
}
|
|
19
19
|
]
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"hooks": [
|
|
26
26
|
{
|
|
27
27
|
"type": "command",
|
|
28
|
-
"command": "
|
|
28
|
+
"command": "bun .aiwcli/_shared/hooks-ts/context_monitor.ts",
|
|
29
29
|
"timeout": 5000
|
|
30
30
|
}
|
|
31
31
|
]
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"hooks": [
|
|
36
36
|
{
|
|
37
37
|
"type": "command",
|
|
38
|
-
"command": "
|
|
38
|
+
"command": "bun .aiwcli/_shared/hooks-ts/task_create_capture.ts",
|
|
39
39
|
"timeout": 3000
|
|
40
40
|
}
|
|
41
41
|
]
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"hooks": [
|
|
46
46
|
{
|
|
47
47
|
"type": "command",
|
|
48
|
-
"command": "
|
|
48
|
+
"command": "bun .aiwcli/_shared/hooks-ts/task_update_capture.ts",
|
|
49
49
|
"timeout": 3000
|
|
50
50
|
}
|
|
51
51
|
]
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"hooks": [
|
|
56
56
|
{
|
|
57
57
|
"type": "command",
|
|
58
|
-
"command": "
|
|
58
|
+
"command": "bun .aiwcli/_shared/hooks-ts/archive_plan.ts",
|
|
59
59
|
"timeout": 5000
|
|
60
60
|
}
|
|
61
61
|
]
|
|
@@ -0,0 +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");
|
|
@@ -0,0 +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");
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* fileSuggestion hook: Suggest context-relevant files for Claude's file inclusion.
|
|
4
|
+
* Outputs a plain JSON array (NOT hookSpecificOutput).
|
|
5
|
+
*/
|
|
6
|
+
import * as fs from "node:fs";
|
|
7
|
+
import * as path from "node:path";
|
|
8
|
+
|
|
9
|
+
import { getContextFilePath, getContextHandoffsDir, getContextPlansDir, getContextReviewsDir, getProjectRoot } from "../lib-ts/base/constants.js";
|
|
10
|
+
import { loadHookInput, logDebug, logError, runHook } from "../lib-ts/base/hook-utils.js";
|
|
11
|
+
import { getAllContexts, getContextBySessionId } from "../lib-ts/context/context-store.js";
|
|
12
|
+
import type { ContextState } from "../lib-ts/types.js";
|
|
13
|
+
|
|
14
|
+
/** Get .md files sorted by mtime descending */
|
|
15
|
+
function getMdFilesByMtime(dir: string): string[] {
|
|
16
|
+
try {
|
|
17
|
+
if (!fs.existsSync(dir)) return [];
|
|
18
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
19
|
+
const mdFiles = entries
|
|
20
|
+
.filter(e => e.isFile() && e.name.endsWith(".md"))
|
|
21
|
+
.map(e => {
|
|
22
|
+
const fullPath = path.join(dir, e.name);
|
|
23
|
+
const stat = fs.statSync(fullPath);
|
|
24
|
+
return { path: fullPath, mtime: stat.mtimeMs };
|
|
25
|
+
})
|
|
26
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
27
|
+
return mdFiles.map(f => f.path);
|
|
28
|
+
} catch {
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Find latest folder-based document (subdirectory with index.md) */
|
|
34
|
+
function getLatestFolderDoc(dir: string): null | string {
|
|
35
|
+
try {
|
|
36
|
+
if (!fs.existsSync(dir)) return null;
|
|
37
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
38
|
+
const subdirs = entries
|
|
39
|
+
.filter(e => e.isDirectory())
|
|
40
|
+
.map(e => {
|
|
41
|
+
const indexPath = path.join(dir, e.name, "index.md");
|
|
42
|
+
if (fs.existsSync(indexPath)) {
|
|
43
|
+
const stat = fs.statSync(indexPath);
|
|
44
|
+
return { path: indexPath, mtime: stat.mtimeMs };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return null;
|
|
48
|
+
})
|
|
49
|
+
.filter((x): x is NonNullable<typeof x> => x !== null)
|
|
50
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
51
|
+
return subdirs.length > 0 ? subdirs[0].path : null;
|
|
52
|
+
} catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function main(): void {
|
|
58
|
+
const payload = loadHookInput();
|
|
59
|
+
if (!payload) {
|
|
60
|
+
console.log("[]");
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const projectRoot = getProjectRoot(payload.cwd);
|
|
66
|
+
const sessionId = payload.session_id;
|
|
67
|
+
|
|
68
|
+
// Find active context
|
|
69
|
+
let ctx: ContextState | null = null;
|
|
70
|
+
|
|
71
|
+
if (sessionId) {
|
|
72
|
+
ctx = getContextBySessionId(sessionId, projectRoot);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Fallback: single active (non-idle) context
|
|
76
|
+
if (!ctx) {
|
|
77
|
+
const all = getAllContexts("active", projectRoot);
|
|
78
|
+
const active = all.filter(c => c.status === "active" && c.mode !== "idle");
|
|
79
|
+
if (active.length === 1) {
|
|
80
|
+
ctx = active[0];
|
|
81
|
+
} else {
|
|
82
|
+
logDebug("file-suggestion", `Ambiguous: ${active.length} active non-idle contexts`);
|
|
83
|
+
console.log("[]");
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const suggestions: string[] = [];
|
|
89
|
+
|
|
90
|
+
// Context file
|
|
91
|
+
const ctxFile = getContextFilePath(ctx.id, projectRoot);
|
|
92
|
+
if (fs.existsSync(ctxFile)) suggestions.push(ctxFile);
|
|
93
|
+
|
|
94
|
+
// Plan files (most recent first)
|
|
95
|
+
const plansDir = getContextPlansDir(ctx.id, projectRoot);
|
|
96
|
+
suggestions.push(...getMdFilesByMtime(plansDir));
|
|
97
|
+
|
|
98
|
+
// Handoff files (prefer folder-based)
|
|
99
|
+
const handoffsDir = getContextHandoffsDir(ctx.id, projectRoot);
|
|
100
|
+
const latestHandoff = getLatestFolderDoc(handoffsDir);
|
|
101
|
+
if (latestHandoff) {
|
|
102
|
+
suggestions.push(latestHandoff);
|
|
103
|
+
} else {
|
|
104
|
+
// Legacy: only most recent flat .md file
|
|
105
|
+
const legacyHandoffs = getMdFilesByMtime(handoffsDir);
|
|
106
|
+
if (legacyHandoffs.length > 0) suggestions.push(legacyHandoffs[0]);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Review files (prefer folder-based under cc-native/)
|
|
110
|
+
const reviewsDir = getContextReviewsDir(ctx.id, projectRoot);
|
|
111
|
+
const ccNativeReviews = path.join(reviewsDir, "cc-native");
|
|
112
|
+
const latestReview = getLatestFolderDoc(ccNativeReviews);
|
|
113
|
+
if (latestReview) {
|
|
114
|
+
suggestions.push(latestReview);
|
|
115
|
+
} else {
|
|
116
|
+
// Fallback to flat review.md files
|
|
117
|
+
suggestions.push(...getMdFilesByMtime(reviewsDir));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Limit to 10
|
|
121
|
+
const limited = suggestions.slice(0, 10);
|
|
122
|
+
console.log(JSON.stringify(limited));
|
|
123
|
+
} catch (error) {
|
|
124
|
+
// Must output valid JSON array even on error — Claude Code expects it
|
|
125
|
+
logError("file-suggestion", `Error: ${error}`);
|
|
126
|
+
console.log("[]");
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
runHook(main, "file-suggestion");
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* PreCompact hook: Save state.json snapshot before context compaction.
|
|
4
|
+
* Captures git state and session metadata for recovery.
|
|
5
|
+
*/
|
|
6
|
+
import { getProjectRoot } from "../lib-ts/base/constants.js";
|
|
7
|
+
import { getGitState } from "../lib-ts/base/git-state.js";
|
|
8
|
+
import {
|
|
9
|
+
loadHookInput, logDebug, logError, logInfo, runHook,
|
|
10
|
+
} from "../lib-ts/base/hook-utils.js";
|
|
11
|
+
import { nowIso } from "../lib-ts/base/utils.js";
|
|
12
|
+
import { getContextBySessionId, saveState } from "../lib-ts/context/context-store.js";
|
|
13
|
+
|
|
14
|
+
function main(): void {
|
|
15
|
+
const payload = loadHookInput();
|
|
16
|
+
if (!payload) return;
|
|
17
|
+
|
|
18
|
+
const sessionId = payload.session_id;
|
|
19
|
+
if (!sessionId) {
|
|
20
|
+
logDebug("pre_compact", "No session_id, skipping");
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const projectRoot = getProjectRoot(payload.cwd);
|
|
25
|
+
const state = getContextBySessionId(sessionId, projectRoot);
|
|
26
|
+
if (!state) {
|
|
27
|
+
logDebug("pre_compact", `No context bound to session ${sessionId}`);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const gitState = getGitState(projectRoot);
|
|
32
|
+
|
|
33
|
+
state.last_session = {
|
|
34
|
+
...state.last_session,
|
|
35
|
+
session_id: sessionId,
|
|
36
|
+
saved_at: nowIso(),
|
|
37
|
+
save_reason: "pre_compact",
|
|
38
|
+
git_state: gitState,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const [ok, err] = saveState(state.id, state, projectRoot);
|
|
42
|
+
if (ok) {
|
|
43
|
+
logInfo("pre_compact", `Saved pre-compact snapshot for ${state.id}`);
|
|
44
|
+
} else {
|
|
45
|
+
logError("pre_compact", `Failed to save state: ${err}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
runHook(main, "pre_compact");
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* SessionEnd hook: Save session state, assign plan fields (fallback),
|
|
4
|
+
* stage has_plan/has_handoff for next session.
|
|
5
|
+
*/
|
|
6
|
+
import * as crypto from "node:crypto";
|
|
7
|
+
import * as fs from "node:fs";
|
|
8
|
+
|
|
9
|
+
import { getProjectRoot } from "../lib-ts/base/constants.js";
|
|
10
|
+
import { getGitState } from "../lib-ts/base/git-state.js";
|
|
11
|
+
import {
|
|
12
|
+
loadHookInput, logDebug, logDiagnostic, logError, logInfo, logWarn as _logWarn, runHook,
|
|
13
|
+
} from "../lib-ts/base/hook-utils.js";
|
|
14
|
+
import { nowIso } from "../lib-ts/base/utils.js";
|
|
15
|
+
import { getContextBySessionId, saveState } from "../lib-ts/context/context-store.js";
|
|
16
|
+
import {
|
|
17
|
+
extractPlanAnchors, findLatestPlan, generatePlanId, normalizePlanContent,
|
|
18
|
+
} from "../lib-ts/context/plan-manager.js";
|
|
19
|
+
|
|
20
|
+
function main(): void {
|
|
21
|
+
const payload = loadHookInput();
|
|
22
|
+
if (!payload) return;
|
|
23
|
+
|
|
24
|
+
const sessionId = payload.session_id;
|
|
25
|
+
if (!sessionId) {
|
|
26
|
+
logDebug("session_end", "No session_id, skipping");
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const projectRoot = getProjectRoot(payload.cwd);
|
|
31
|
+
const source = payload.source ?? "unknown";
|
|
32
|
+
const permissionMode = payload.permission_mode ?? "";
|
|
33
|
+
|
|
34
|
+
const state = getContextBySessionId(sessionId, projectRoot);
|
|
35
|
+
if (!state) {
|
|
36
|
+
logDebug("session_end", `No context bound to session ${sessionId}`);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Capture git state
|
|
41
|
+
const gitState = getGitState(projectRoot);
|
|
42
|
+
|
|
43
|
+
// Save session metadata
|
|
44
|
+
state.last_session = {
|
|
45
|
+
session_id: sessionId,
|
|
46
|
+
save_reason: source,
|
|
47
|
+
saved_at: nowIso(),
|
|
48
|
+
transcript_path: payload.transcript_path ?? undefined,
|
|
49
|
+
git_state: gitState,
|
|
50
|
+
};
|
|
51
|
+
state.last_active = nowIso();
|
|
52
|
+
|
|
53
|
+
// Plan fallback assignment (skip in plan mode — rejected plans shouldn't stage)
|
|
54
|
+
if (permissionMode !== "plan") {
|
|
55
|
+
// Step 1: Assign plan fields if missing
|
|
56
|
+
if (!state.plan_hash) {
|
|
57
|
+
const latestPlanPath = findLatestPlan(state.id, projectRoot);
|
|
58
|
+
if (latestPlanPath) {
|
|
59
|
+
try {
|
|
60
|
+
const content = fs.readFileSync(latestPlanPath, "utf8");
|
|
61
|
+
const normalized = normalizePlanContent(content);
|
|
62
|
+
const planHash = crypto.createHash("sha256")
|
|
63
|
+
.update(normalized, "utf-8")
|
|
64
|
+
.digest("hex")
|
|
65
|
+
.slice(0, 12);
|
|
66
|
+
|
|
67
|
+
state.plan_hash = planHash;
|
|
68
|
+
state.plan_path = latestPlanPath;
|
|
69
|
+
state.plan_signature = content.slice(0, 200);
|
|
70
|
+
state.plan_id = generatePlanId();
|
|
71
|
+
state.plan_anchors = extractPlanAnchors(content);
|
|
72
|
+
// Preserve plan_consumed if already true (plan was implemented) —
|
|
73
|
+
// resetting it would re-stage the plan and block handoff staging.
|
|
74
|
+
// Only set to false when no prior consumption has occurred.
|
|
75
|
+
state.plan_consumed = state.plan_consumed || false;
|
|
76
|
+
|
|
77
|
+
logInfo("session_end", `Assigned plan fallback: hash=${planHash}, path=${latestPlanPath}`);
|
|
78
|
+
} catch (error) {
|
|
79
|
+
logError("session_end", `Failed to read plan: ${error}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Step 2: Stage has_plan if conditions met
|
|
85
|
+
if (state.plan_hash && state.mode === "active" && !state.plan_consumed) {
|
|
86
|
+
state.mode = "has_plan";
|
|
87
|
+
logInfo("session_end", `Staged ${state.id}: active → has_plan`);
|
|
88
|
+
}
|
|
89
|
+
// If plan_consumed, skip — already consumed, don't re-stage
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Handoff staging (only if mode is still "active" — plan check may have changed it)
|
|
93
|
+
if (state.handoff_path && state.mode === "active" && !state.handoff_consumed) {
|
|
94
|
+
state.mode = "has_handoff";
|
|
95
|
+
logInfo("session_end", `Staged ${state.id}: active → has_handoff`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Save final state
|
|
99
|
+
const [ok, err] = saveState(state.id, state, projectRoot);
|
|
100
|
+
if (ok) {
|
|
101
|
+
logDiagnostic("session_end", "saved", `${state.id} mode=${state.mode}`);
|
|
102
|
+
} else {
|
|
103
|
+
logError("session_end", `Failed to save state: ${err}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
runHook(main, "session_end");
|