aiwcli 0.9.0 → 0.9.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/README.md +19 -35
- package/dist/lib/template-installer.js +38 -0
- package/dist/templates/_shared/.claude/commands/handoff.md +219 -7
- package/dist/templates/_shared/.codex/workflows/handoff.md +219 -7
- package/dist/templates/_shared/.windsurf/workflows/handoff.md +219 -7
- package/dist/templates/_shared/hooks/context_enforcer.py +9 -5
- package/dist/templates/_shared/hooks/context_monitor.py +28 -10
- package/dist/templates/_shared/hooks/file-suggestion.py +45 -15
- package/dist/templates/_shared/hooks/user_prompt_submit.py +0 -10
- package/dist/templates/_shared/lib/base/constants.py +45 -0
- package/dist/templates/_shared/lib/base/inference.py +44 -21
- package/dist/templates/_shared/lib/base/subprocess_utils.py +46 -0
- package/dist/templates/_shared/lib/base/utils.py +5 -3
- package/dist/templates/_shared/lib/context/__init__.py +0 -8
- package/dist/templates/_shared/lib/context/cache.py +2 -4
- package/dist/templates/_shared/lib/context/context_manager.py +1 -118
- package/dist/templates/_shared/lib/context/discovery.py +8 -50
- package/dist/templates/_shared/lib/handoff/document_generator.py +2 -5
- package/dist/templates/_shared/lib/templates/README.md +0 -1
- package/dist/templates/_shared/lib/templates/formatters.py +0 -1
- package/dist/templates/_shared/scripts/save_handoff.py +289 -43
- package/dist/templates/_shared/workflows/handoff.md +30 -16
- package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.py +41 -20
- package/dist/templates/cc-native/_cc-native/lib/orchestrator.py +9 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/agent.py +9 -0
- package/dist/templates/cc-native/_cc-native/lib/utils.py +123 -10
- package/oclif.manifest.json +1 -1
- package/package.json +1 -1
|
@@ -1,14 +1,226 @@
|
|
|
1
|
+
# Handoff Workflow
|
|
2
|
+
|
|
3
|
+
Generate a handoff document summarizing the current session's work, decisions, and pending items. Optionally update a plan document to track completed vs remaining tasks.
|
|
4
|
+
|
|
5
|
+
## Triggers
|
|
6
|
+
|
|
7
|
+
- `/handoff` command
|
|
8
|
+
- `/handoff path/to/PLAN.md` - with plan document integration
|
|
9
|
+
- Phrases like "write a handoff", "create a session summary", "document what we did", "end session with notes"
|
|
10
|
+
|
|
11
|
+
## Arguments
|
|
12
|
+
|
|
13
|
+
- `$ARGUMENTS` - Optional path to a plan document. If provided, the handoff will:
|
|
14
|
+
1. Mark completed items in the plan with `[x]`
|
|
15
|
+
2. Add notes about partial progress
|
|
16
|
+
3. Append a "Session Progress" section to the plan
|
|
17
|
+
|
|
18
|
+
## Process
|
|
19
|
+
|
|
20
|
+
### Step 1: Get Context ID
|
|
21
|
+
|
|
22
|
+
Extract the `context_id` from the system reminder injected by the context enforcer hook.
|
|
23
|
+
|
|
24
|
+
Look for the pattern in the system reminder:
|
|
25
|
+
```
|
|
26
|
+
Active Context: {context_id}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
If no active context is found, inform the user and stop - handoffs require an active context.
|
|
30
|
+
|
|
31
|
+
### Step 2: Gather Information
|
|
32
|
+
|
|
33
|
+
1. Review conversation history for:
|
|
34
|
+
- Completed tasks and implementations
|
|
35
|
+
- Key decisions and their rationale
|
|
36
|
+
- Failed approaches (to avoid repeating)
|
|
37
|
+
- External context (deadlines, stakeholder requirements)
|
|
38
|
+
|
|
39
|
+
2. Check git status if available:
|
|
40
|
+
```bash
|
|
41
|
+
git status --short
|
|
42
|
+
git diff --stat
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
3. Look for TODOs/FIXMEs mentioned in session
|
|
46
|
+
|
|
47
|
+
4. **If plan document provided**: Read the plan and identify:
|
|
48
|
+
- Tasks that are now completed
|
|
49
|
+
- Tasks that are partially done
|
|
50
|
+
- Tasks that were attempted but blocked
|
|
51
|
+
- New tasks discovered during implementation
|
|
52
|
+
|
|
53
|
+
### Step 3: Generate Document
|
|
54
|
+
|
|
55
|
+
Use this template. The `<!-- SECTION: name -->` markers are required for the save script to parse sections into sharded files.
|
|
56
|
+
|
|
57
|
+
```markdown
|
|
1
58
|
---
|
|
2
|
-
|
|
59
|
+
title: Session Handoff
|
|
60
|
+
date: {ISO timestamp}
|
|
61
|
+
session_id: {conversation ID if available}
|
|
62
|
+
project: {project name from package.json, Cargo.toml, or directory name}
|
|
63
|
+
context_id: {context_id from Step 1}
|
|
64
|
+
plan_document: {path to plan if provided, or "none"}
|
|
3
65
|
---
|
|
4
|
-
# Session Handoff
|
|
5
66
|
|
|
6
|
-
|
|
67
|
+
# Session Handoff — {Date}
|
|
7
68
|
|
|
8
|
-
|
|
69
|
+
<!-- SECTION: summary -->
|
|
70
|
+
## Summary
|
|
71
|
+
{2-3 sentences: what's different now vs. session start}
|
|
72
|
+
|
|
73
|
+
<!-- SECTION: completed -->
|
|
74
|
+
## Work Completed
|
|
75
|
+
{Grouped by category if multiple areas. Specific file:function references.}
|
|
76
|
+
|
|
77
|
+
<!-- SECTION: dead-ends -->
|
|
78
|
+
## Dead Ends — Do Not Retry
|
|
79
|
+
|
|
80
|
+
These approaches were attempted and failed. Do not retry without addressing the root cause.
|
|
81
|
+
|
|
82
|
+
| Approach | Why It Failed | Time Spent | Alternative |
|
|
83
|
+
|----------|---------------|------------|-------------|
|
|
84
|
+
| {What was attempted} | {Specific reason} | {Rough estimate} | {What to try instead} |
|
|
85
|
+
|
|
86
|
+
<!-- SECTION: decisions -->
|
|
87
|
+
## Key Decisions
|
|
88
|
+
{Technical choices with rationale. Format: **Decision**: Rationale. Trade-off: X.}
|
|
89
|
+
|
|
90
|
+
<!-- SECTION: pending -->
|
|
91
|
+
## Pending Issues
|
|
92
|
+
- [ ] {Issue} — {severity: HIGH/MED/LOW} {optional workaround note}
|
|
93
|
+
|
|
94
|
+
<!-- SECTION: next-steps -->
|
|
95
|
+
## Next Steps
|
|
96
|
+
1. {Actionable item with file:line reference if applicable}
|
|
97
|
+
|
|
98
|
+
<!-- SECTION: files -->
|
|
99
|
+
## Files Modified
|
|
100
|
+
{Significant changes only. Skip formatting-only edits.}
|
|
101
|
+
|
|
102
|
+
<!-- SECTION: context -->
|
|
103
|
+
## Context for Future Sessions
|
|
104
|
+
{Non-obvious context: env quirks, stakeholder requirements}
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Step 4: Update Plan Document (if provided)
|
|
109
|
+
|
|
110
|
+
If a plan document path was provided in `$ARGUMENTS`:
|
|
111
|
+
|
|
112
|
+
1. **Read the plan document**
|
|
113
|
+
2. **Identify completed items**:
|
|
114
|
+
- Find checkboxes `- [ ]` that match completed work
|
|
115
|
+
- Change them to `- [x]`
|
|
116
|
+
3. **Add progress notes** to items that are partially complete:
|
|
117
|
+
- Append `(partial: {brief status})` to the line
|
|
118
|
+
4. **Append Session Progress section** at the bottom:
|
|
119
|
+
|
|
120
|
+
```markdown
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Session Progress Log
|
|
125
|
+
|
|
126
|
+
### {Date} — Session {session_id or timestamp}
|
|
127
|
+
|
|
128
|
+
**Completed this session:**
|
|
129
|
+
- [x] {Task from plan that was completed}
|
|
130
|
+
- [x] {Another completed task}
|
|
131
|
+
|
|
132
|
+
**Partially completed:**
|
|
133
|
+
- {Task} — {current state, what remains}
|
|
134
|
+
|
|
135
|
+
**Blocked/Deferred:**
|
|
136
|
+
- {Task} — {reason, what's needed}
|
|
137
|
+
|
|
138
|
+
**New items discovered:**
|
|
139
|
+
- [ ] {New task not in original plan}
|
|
140
|
+
- [ ] {Another new task}
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
5. **If no plan document was provided**:
|
|
146
|
+
- Skip plan creation - the handoff document serves as the session record
|
|
147
|
+
|
|
148
|
+
### Step 5: Save and Update Status
|
|
149
|
+
|
|
150
|
+
Instead of writing the file directly, pipe your handoff content to the save script:
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
python .aiwcli/_shared/scripts/save_handoff.py "{context_id}" <<'EOF'
|
|
154
|
+
{Your complete handoff markdown content from Step 3}
|
|
155
|
+
EOF
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
This script:
|
|
159
|
+
1. Creates a folder at `_output/contexts/{context_id}/handoffs/{YYYY-MM-DD-HHMM}/`
|
|
160
|
+
2. Parses sections and writes sharded files (index.md, completed-work.md, dead-ends.md, etc.)
|
|
161
|
+
3. Copies the current plan (if any) to plan.md
|
|
162
|
+
4. Records the event in the context's event log (informational only)
|
|
163
|
+
|
|
164
|
+
Use the handoff folder for context in the next session.
|
|
165
|
+
|
|
166
|
+
## Dead Ends Section Guidelines
|
|
167
|
+
|
|
168
|
+
This section is critical for preventing context rot across sessions. Be specific:
|
|
169
|
+
|
|
170
|
+
**Bad (too vague):**
|
|
171
|
+
> - Tried using library X, didn't work
|
|
172
|
+
|
|
173
|
+
**Good (actionable):**
|
|
174
|
+
> ### Fixing the race condition in SessionStore
|
|
175
|
+
> | Approach Tried | Why It Failed |
|
|
176
|
+
> |----------------|---------------|
|
|
177
|
+
> | `async-mutex` package | Deadlock when nested calls to `getSession()` |
|
|
178
|
+
> | Redis WATCH/MULTI | Our Redis 6.x cluster doesn't support WATCH in cluster mode |
|
|
179
|
+
> | In-memory lock Map | Works single-node but breaks in horizontal scaling |
|
|
180
|
+
>
|
|
181
|
+
> **What to try instead**: Upgrade to Redis 7.x which supports WATCH in cluster mode, or use Redlock algorithm
|
|
182
|
+
|
|
183
|
+
**Capture these dead ends:**
|
|
184
|
+
- Packages/libraries that had incompatibilities
|
|
185
|
+
- Approaches that caused new bugs or regressions
|
|
186
|
+
- Solutions that worked locally but failed in CI/staging/prod
|
|
187
|
+
- Configurations that conflicted with existing setup
|
|
188
|
+
- Rabbit holes that consumed significant time without progress
|
|
189
|
+
|
|
190
|
+
## Post-Generation Output
|
|
191
|
+
|
|
192
|
+
After creating file, output:
|
|
193
|
+
|
|
194
|
+
```
|
|
195
|
+
✓ Created handoff folder: _output/contexts/{context_id}/handoffs/{YYYY-MM-DD-HHMM}/
|
|
196
|
+
- index.md (entry point with navigation)
|
|
197
|
+
- completed-work.md, dead-ends.md, decisions.md, pending.md, context.md
|
|
198
|
+
- plan.md (copy of current plan, if any)
|
|
199
|
+
|
|
200
|
+
To continue next session:
|
|
201
|
+
The index.md will be automatically suggested when you start a new session.
|
|
202
|
+
Read dead-ends.md first to avoid repeating failed approaches.
|
|
203
|
+
|
|
204
|
+
⚠️ {N} dead ends documented — avoid re-attempting these approaches
|
|
205
|
+
```
|
|
9
206
|
|
|
10
|
-
|
|
207
|
+
If plan was updated:
|
|
208
|
+
```
|
|
209
|
+
✓ Updated plan document: {path}
|
|
210
|
+
- {N} items marked complete
|
|
211
|
+
- {N} items partially complete
|
|
212
|
+
- {N} new items added
|
|
213
|
+
```
|
|
11
214
|
|
|
12
|
-
|
|
215
|
+
## Success Criteria
|
|
13
216
|
|
|
14
|
-
|
|
217
|
+
- [ ] Handoff folder created at `handoffs/{YYYY-MM-DD-HHMM}/`
|
|
218
|
+
- [ ] index.md contains summary and navigation table
|
|
219
|
+
- [ ] All section files created (completed-work.md, dead-ends.md, etc.)
|
|
220
|
+
- [ ] Dead ends use structured table format for quick scanning
|
|
221
|
+
- [ ] plan.md copied from context if plan exists
|
|
222
|
+
- [ ] Next steps are actionable with file references
|
|
223
|
+
- [ ] Git status included in index.md
|
|
224
|
+
- [ ] If plan provided: checkboxes updated to reflect completion status
|
|
225
|
+
- [ ] If plan provided: Session Progress Log appended
|
|
226
|
+
- [ ] Context state updated to indicate handoff pending
|
|
@@ -13,7 +13,7 @@ Context selection priority:
|
|
|
13
13
|
- 1 in-flight context -> Auto-select that context
|
|
14
14
|
- Multiple in-flight contexts -> Block and show picker
|
|
15
15
|
|
|
16
|
-
In-flight modes: planning, pending_implementation, implementing
|
|
16
|
+
In-flight modes: planning, pending_implementation, implementing
|
|
17
17
|
|
|
18
18
|
Prefix syntax:
|
|
19
19
|
- ^: Show context picker (bare caret)
|
|
@@ -37,6 +37,7 @@ Hook output:
|
|
|
37
37
|
- Exit 2 + stderr: Block request, show context picker to user
|
|
38
38
|
"""
|
|
39
39
|
import json
|
|
40
|
+
import os
|
|
40
41
|
import re
|
|
41
42
|
import sys
|
|
42
43
|
from dataclasses import dataclass
|
|
@@ -48,6 +49,7 @@ SCRIPT_DIR = Path(__file__).resolve().parent
|
|
|
48
49
|
SHARED_LIB = SCRIPT_DIR.parent / "lib"
|
|
49
50
|
sys.path.insert(0, str(SHARED_LIB.parent))
|
|
50
51
|
|
|
52
|
+
from lib.base.subprocess_utils import is_internal_call
|
|
51
53
|
from lib.context.context_manager import (
|
|
52
54
|
Context,
|
|
53
55
|
get_all_contexts,
|
|
@@ -61,7 +63,6 @@ from lib.context.discovery import (
|
|
|
61
63
|
get_in_flight_context,
|
|
62
64
|
format_active_context_reminder,
|
|
63
65
|
format_context_created,
|
|
64
|
-
format_handoff_continuation,
|
|
65
66
|
format_pending_plan_continuation,
|
|
66
67
|
format_implementation_continuation,
|
|
67
68
|
_format_relative_time,
|
|
@@ -358,6 +359,11 @@ def determine_context(
|
|
|
358
359
|
Raises:
|
|
359
360
|
BlockRequest: When request should be blocked to show picker to user
|
|
360
361
|
"""
|
|
362
|
+
# 0. Skip context creation for internal subprocess calls (orchestrator, agents)
|
|
363
|
+
if is_internal_call():
|
|
364
|
+
eprint("[context_enforcer] Skipping: internal subprocess call")
|
|
365
|
+
return (None, "skip_internal", None)
|
|
366
|
+
|
|
361
367
|
# 1. Check if session already belongs to a context (HIGHEST PRIORITY)
|
|
362
368
|
# This prevents context switching on subsequent prompts - one context per session
|
|
363
369
|
if session_id:
|
|
@@ -426,9 +432,7 @@ def determine_context(
|
|
|
426
432
|
eprint(f"[context_enforcer] Auto-selected single in-flight context: {ctx.id} (mode={mode})")
|
|
427
433
|
|
|
428
434
|
# Use mode-specific formatter for better continuation context
|
|
429
|
-
if mode == "
|
|
430
|
-
output = format_handoff_continuation(ctx)
|
|
431
|
-
elif mode == "pending_implementation":
|
|
435
|
+
if mode == "pending_implementation":
|
|
432
436
|
output = format_pending_plan_continuation(ctx)
|
|
433
437
|
elif mode == "implementing":
|
|
434
438
|
output = format_implementation_continuation(ctx)
|
|
@@ -191,11 +191,17 @@ Context ID: `{context_id}`"""
|
|
|
191
191
|
|
|
192
192
|
def check_and_transition_mode(hook_input: dict) -> None:
|
|
193
193
|
"""
|
|
194
|
-
Check if context needs to transition
|
|
194
|
+
Check if context needs to transition to implementing mode.
|
|
195
195
|
|
|
196
|
-
This handles
|
|
197
|
-
|
|
198
|
-
|
|
196
|
+
This handles two cases:
|
|
197
|
+
1. Plan was approved (pending_implementation) and implementation tools are used
|
|
198
|
+
2. Plan was in planning mode but permission_mode is no longer "plan"
|
|
199
|
+
(e.g., after /clear which clears permissions and pastes the plan)
|
|
200
|
+
|
|
201
|
+
If we're seeing tool usage (Edit, Write, Bash) and either:
|
|
202
|
+
- Context is in "pending_implementation", OR
|
|
203
|
+
- Context is in "planning" and permission_mode is not "plan"
|
|
204
|
+
We transition to "implementing".
|
|
199
205
|
|
|
200
206
|
Args:
|
|
201
207
|
hook_input: Hook input data from Claude Code
|
|
@@ -218,11 +224,22 @@ def check_and_transition_mode(hook_input: dict) -> None:
|
|
|
218
224
|
if not context:
|
|
219
225
|
return
|
|
220
226
|
|
|
221
|
-
|
|
222
|
-
|
|
227
|
+
if not context.in_flight:
|
|
228
|
+
return
|
|
229
|
+
|
|
230
|
+
current_mode = context.in_flight.mode
|
|
231
|
+
permission_mode = hook_input.get("permission_mode", "default")
|
|
232
|
+
|
|
233
|
+
# Transition from pending_implementation to implementing
|
|
234
|
+
if current_mode == "pending_implementation":
|
|
223
235
|
eprint(f"[context_monitor] Transitioning {context.id} from pending_implementation to implementing")
|
|
224
236
|
update_plan_status(context.id, "implementing", project_root=project_root)
|
|
225
237
|
|
|
238
|
+
# Transition from planning to implementing if permission_mode is not "plan"
|
|
239
|
+
elif current_mode == "planning" and permission_mode != "plan":
|
|
240
|
+
eprint(f"[context_monitor] Transitioning {context.id} from planning to implementing (permission_mode={permission_mode})")
|
|
241
|
+
update_plan_status(context.id, "implementing", project_root=project_root)
|
|
242
|
+
|
|
226
243
|
|
|
227
244
|
def check_context_level(hook_input: dict) -> Optional[str]:
|
|
228
245
|
"""
|
|
@@ -261,9 +278,6 @@ def check_context_level(hook_input: dict) -> Optional[str]:
|
|
|
261
278
|
eprint(f"[context_monitor] Context: {percent_remaining}% remaining "
|
|
262
279
|
f"(~{tokens_used//1000}k/{max_tokens//1000}k tokens)")
|
|
263
280
|
|
|
264
|
-
# Check mode transition (file I/O)
|
|
265
|
-
check_and_transition_mode(hook_input)
|
|
266
|
-
|
|
267
281
|
# Get current context for handoff info (file I/O)
|
|
268
282
|
project_root = project_dir(hook_input)
|
|
269
283
|
context_id = get_current_context_id(project_root)
|
|
@@ -283,7 +297,7 @@ def main():
|
|
|
283
297
|
"""
|
|
284
298
|
Main entry point for PostToolUse hook.
|
|
285
299
|
|
|
286
|
-
Reads hook input from stdin,
|
|
300
|
+
Reads hook input from stdin, checks for mode transitions,
|
|
287
301
|
and prints system reminder if context is low.
|
|
288
302
|
"""
|
|
289
303
|
try:
|
|
@@ -298,6 +312,10 @@ def main():
|
|
|
298
312
|
except json.JSONDecodeError:
|
|
299
313
|
return
|
|
300
314
|
|
|
315
|
+
# Always check for mode transitions on implementation tools
|
|
316
|
+
# This handles the case where /clear pastes the plan with non-plan permission mode
|
|
317
|
+
check_and_transition_mode(hook_input)
|
|
318
|
+
|
|
301
319
|
# Check context level
|
|
302
320
|
warning = check_context_level(hook_input)
|
|
303
321
|
|
|
@@ -49,8 +49,8 @@ def get_context_files(context_id: str, project_root: Path) -> List[str]:
|
|
|
49
49
|
Collects:
|
|
50
50
|
- Context file (context.json)
|
|
51
51
|
- Plans (most recent first)
|
|
52
|
-
- Handoffs (
|
|
53
|
-
- Reviews (
|
|
52
|
+
- Handoffs: index.md from subdirectories (folder-based) OR flat .md files (legacy)
|
|
53
|
+
- Reviews: index.md from subdirectories (folder-based) OR flat review.md (legacy)
|
|
54
54
|
|
|
55
55
|
Args:
|
|
56
56
|
context_id: Context identifier
|
|
@@ -76,22 +76,52 @@ def get_context_files(context_id: str, project_root: Path) -> List[str]:
|
|
|
76
76
|
files.extend([str(p) for p in plan_files])
|
|
77
77
|
eprint(f"[file-suggestion] Found {len(plan_files)} plans in {context_id}")
|
|
78
78
|
|
|
79
|
-
# Get handoffs
|
|
79
|
+
# Get handoffs - prefer folder-based (index.md in subdirectories), fall back to legacy
|
|
80
80
|
handoffs_dir = get_context_handoffs_dir(context_id, project_root)
|
|
81
81
|
if handoffs_dir.exists():
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
82
|
+
# Find handoff folders (named like YYYY-MM-DD-HHMM or YYYY-MM-DD-HHMM-N)
|
|
83
|
+
handoff_folders = sorted(
|
|
84
|
+
[d for d in handoffs_dir.iterdir() if d.is_dir()],
|
|
85
|
+
key=lambda d: d.name,
|
|
86
|
+
reverse=True # Most recent first (alphabetically sorts by date)
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
if handoff_folders:
|
|
90
|
+
# Use folder-based: get index.md from most recent folder only
|
|
91
|
+
index_file = handoff_folders[0] / "index.md"
|
|
92
|
+
if index_file.exists():
|
|
93
|
+
files.append(str(index_file))
|
|
94
|
+
eprint(f"[file-suggestion] Found handoff folder: {handoff_folders[0].name}")
|
|
95
|
+
else:
|
|
96
|
+
# Legacy support: flat .md files directly in handoffs/
|
|
97
|
+
legacy_handoffs = [f for f in handoffs_dir.glob("*.md") if f.is_file()]
|
|
98
|
+
legacy_handoffs.sort(key=lambda p: p.stat().st_mtime, reverse=True)
|
|
99
|
+
if legacy_handoffs:
|
|
100
|
+
files.append(str(legacy_handoffs[0])) # Only most recent legacy
|
|
101
|
+
eprint(f"[file-suggestion] Found {len(legacy_handoffs)} legacy handoffs in {context_id}")
|
|
102
|
+
|
|
103
|
+
# Get reviews - prefer folder-based (index.md in subdirectories), fall back to legacy
|
|
104
|
+
reviews_dir = get_context_reviews_dir(context_id, project_root) / "cc-native"
|
|
89
105
|
if reviews_dir.exists():
|
|
90
|
-
# Find review
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
106
|
+
# Find review folders (named like YYYY-MM-DD-HHMM-iteration-N)
|
|
107
|
+
review_folders = sorted(
|
|
108
|
+
[d for d in reviews_dir.iterdir() if d.is_dir()],
|
|
109
|
+
key=lambda d: d.name,
|
|
110
|
+
reverse=True # Most recent first
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
if review_folders:
|
|
114
|
+
# Use folder-based: get index.md from most recent folder only
|
|
115
|
+
index_file = review_folders[0] / "index.md"
|
|
116
|
+
if index_file.exists():
|
|
117
|
+
files.append(str(index_file))
|
|
118
|
+
eprint(f"[file-suggestion] Found review folder: {review_folders[0].name}")
|
|
119
|
+
else:
|
|
120
|
+
# Legacy support: flat review.md directly in cc-native/
|
|
121
|
+
legacy_review = reviews_dir / "review.md"
|
|
122
|
+
if legacy_review.exists():
|
|
123
|
+
files.append(str(legacy_review))
|
|
124
|
+
eprint(f"[file-suggestion] Found legacy review.md in {context_id}")
|
|
95
125
|
|
|
96
126
|
return files
|
|
97
127
|
|
|
@@ -33,7 +33,6 @@ from lib.base.utils import eprint, project_dir
|
|
|
33
33
|
from lib.context.context_manager import (
|
|
34
34
|
update_context_session_id,
|
|
35
35
|
update_plan_status,
|
|
36
|
-
clear_handoff_status,
|
|
37
36
|
get_context,
|
|
38
37
|
get_context_by_session_id,
|
|
39
38
|
)
|
|
@@ -47,7 +46,6 @@ def _update_in_flight_status(context_id: str, hook_input: dict, project_root: Pa
|
|
|
47
46
|
"""
|
|
48
47
|
Update context in-flight status based on permission mode.
|
|
49
48
|
|
|
50
|
-
- If handoff_pending: clear it (handoff has been consumed by this session)
|
|
51
49
|
- If permission_mode == "plan": set to "planning"
|
|
52
50
|
- If permission_mode in ["acceptEdits", "bypassPermissions"]: set to "implementing"
|
|
53
51
|
"""
|
|
@@ -59,14 +57,6 @@ def _update_in_flight_status(context_id: str, hook_input: dict, project_root: Pa
|
|
|
59
57
|
permission_mode = hook_input.get("permission_mode", "default")
|
|
60
58
|
eprint(f"[user_prompt_submit] Current mode: {current_mode}, permission_mode: {permission_mode}")
|
|
61
59
|
|
|
62
|
-
# Clear handoff_pending if set (session resumption clears the handoff)
|
|
63
|
-
if current_mode == "handoff_pending":
|
|
64
|
-
clear_handoff_status(context_id, project_root)
|
|
65
|
-
eprint(f"[user_prompt_submit] Cleared handoff_pending status")
|
|
66
|
-
# Refresh context after clearing
|
|
67
|
-
context = get_context(context_id, project_root)
|
|
68
|
-
current_mode = context.in_flight.mode if context and context.in_flight else "none"
|
|
69
|
-
|
|
70
60
|
# Set status based on permission mode
|
|
71
61
|
if permission_mode == "plan":
|
|
72
62
|
if current_mode != "planning":
|
|
@@ -297,3 +297,48 @@ def get_archive_index_path(project_root: Path = None) -> Path:
|
|
|
297
297
|
Path to _output/contexts/archive/index.json
|
|
298
298
|
"""
|
|
299
299
|
return get_archive_dir(project_root) / INDEX_FILENAME
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def get_handoff_folder_path(context_id: str, project_root: Path = None) -> Path:
|
|
303
|
+
"""Get path for a new handoff folder with datetime naming.
|
|
304
|
+
|
|
305
|
+
Returns: _output/contexts/{context_id}/handoffs/{YYYY-MM-DD-HHMM}/
|
|
306
|
+
Handles collisions by appending -N suffix if folder exists.
|
|
307
|
+
|
|
308
|
+
Args:
|
|
309
|
+
context_id: Context identifier
|
|
310
|
+
project_root: Project root directory (default: cwd)
|
|
311
|
+
|
|
312
|
+
Returns:
|
|
313
|
+
Path to new handoff folder (not yet created)
|
|
314
|
+
"""
|
|
315
|
+
from datetime import datetime
|
|
316
|
+
handoffs_dir = get_context_handoffs_dir(context_id, project_root)
|
|
317
|
+
timestamp = datetime.now().strftime("%Y-%m-%d-%H%M")
|
|
318
|
+
folder = handoffs_dir / timestamp
|
|
319
|
+
|
|
320
|
+
counter = 1
|
|
321
|
+
while folder.exists():
|
|
322
|
+
folder = handoffs_dir / f"{timestamp}-{counter}"
|
|
323
|
+
counter += 1
|
|
324
|
+
|
|
325
|
+
return folder
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def get_review_folder_path(context_id: str, iteration: int, project_root: Path = None) -> Path:
|
|
329
|
+
"""Get path for a new review folder with datetime and iteration naming.
|
|
330
|
+
|
|
331
|
+
Returns: _output/contexts/{context_id}/reviews/cc-native/{YYYY-MM-DD-HHMM-iteration-N}/
|
|
332
|
+
|
|
333
|
+
Args:
|
|
334
|
+
context_id: Context identifier
|
|
335
|
+
iteration: Iteration number (1-based)
|
|
336
|
+
project_root: Project root directory (default: cwd)
|
|
337
|
+
|
|
338
|
+
Returns:
|
|
339
|
+
Path to new review folder (not yet created)
|
|
340
|
+
"""
|
|
341
|
+
from datetime import datetime
|
|
342
|
+
reviews_dir = get_context_reviews_dir(context_id, project_root) / "cc-native"
|
|
343
|
+
timestamp = datetime.now().strftime("%Y-%m-%d-%H%M")
|
|
344
|
+
return reviews_dir / f"{timestamp}-iteration-{iteration}"
|
|
@@ -126,28 +126,54 @@ def inference(
|
|
|
126
126
|
)
|
|
127
127
|
|
|
128
128
|
|
|
129
|
-
#
|
|
130
|
-
|
|
129
|
+
# Minimum word length for context IDs (filters out short words like "I", "a", "to", "is")
|
|
130
|
+
MIN_WORD_LENGTH = 3
|
|
131
|
+
|
|
132
|
+
# Maximum number of words in context ID
|
|
133
|
+
MAX_WORDS = 10
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def filter_short_words(text: str) -> str:
|
|
137
|
+
"""Filter words by minimum length, keeping only words with 3+ characters.
|
|
138
|
+
|
|
139
|
+
This replaces the stopword list approach - a 3-char minimum naturally
|
|
140
|
+
filters out most function words (the, to, a, an, of, on, is, it, etc.)
|
|
141
|
+
"""
|
|
142
|
+
# Handle contractions by replacing apostrophes before splitting
|
|
143
|
+
text = text.replace("'", " ").replace("'", " ")
|
|
144
|
+
words = text.lower().split()
|
|
145
|
+
# Filter to words with 3+ characters and limit to MAX_WORDS
|
|
146
|
+
filtered = [w for w in words if len(w) >= MIN_WORD_LENGTH][:MAX_WORDS]
|
|
147
|
+
return ' '.join(filtered)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
# System prompt for generating context ID summaries (recognition-focused, not summarization)
|
|
151
|
+
CONTEXT_ID_SYSTEM_PROMPT = """Generate a memorable context ID that helps instantly recall what was being worked on.
|
|
152
|
+
|
|
153
|
+
This is for RECOGNITION - the user will see this ID in a list and needs to immediately remember "oh yeah, THAT session."
|
|
131
154
|
|
|
132
155
|
Rules:
|
|
133
|
-
-
|
|
134
|
-
-
|
|
135
|
-
-
|
|
136
|
-
-
|
|
137
|
-
-
|
|
156
|
+
- Output 3-6 words that capture the DISTINCTIVE essence
|
|
157
|
+
- Lead with the ACTION (verb) being taken
|
|
158
|
+
- Include the SPECIFIC target (file name, feature name, component name)
|
|
159
|
+
- Prefer proper nouns and specific names over generic categories
|
|
160
|
+
- NO function words: the, to, with, for, in, a, an, of, on, is, it, and, or
|
|
161
|
+
- NO generic padding: code, project, app, webapp, system, implementation
|
|
162
|
+
- No punctuation, no quotes
|
|
138
163
|
|
|
139
164
|
Examples:
|
|
140
|
-
- "I want to add user authentication" -> "
|
|
141
|
-
- "Fix the bug in the login flow
|
|
142
|
-
- "Can you help me refactor
|
|
143
|
-
- "Update the README with new instructions" -> "
|
|
165
|
+
- "I want to add user authentication using JWT" -> "adding jwt auth user-service"
|
|
166
|
+
- "Fix the bug in the login flow where users get redirected to a 404" -> "fixing login 404 redirect bug"
|
|
167
|
+
- "Can you help me refactor the PaymentProcessor class" -> "refactoring paymentprocessor class"
|
|
168
|
+
- "Update the README with new installation instructions" -> "readme installation instructions"
|
|
169
|
+
- "Search for where the context ID extraction prompt is" -> "search context extraction prompt"
|
|
144
170
|
|
|
145
|
-
Output ONLY the
|
|
171
|
+
Output ONLY the words separated by spaces, nothing else."""
|
|
146
172
|
|
|
147
173
|
|
|
148
174
|
def generate_semantic_summary(prompt: str, timeout: int = 15) -> Optional[str]:
|
|
149
175
|
"""
|
|
150
|
-
Generate a
|
|
176
|
+
Generate a keyword summary of a user prompt.
|
|
151
177
|
|
|
152
178
|
Uses Sonnet for quality inference. Returns None if inference fails.
|
|
153
179
|
|
|
@@ -156,9 +182,8 @@ def generate_semantic_summary(prompt: str, timeout: int = 15) -> Optional[str]:
|
|
|
156
182
|
timeout: Timeout in seconds (default 15)
|
|
157
183
|
|
|
158
184
|
Returns:
|
|
159
|
-
|
|
185
|
+
Keyword summary string (5-10 words) or None if failed
|
|
160
186
|
"""
|
|
161
|
-
# Pass full prompt - AI can summarize any length into 10 words
|
|
162
187
|
result = inference(
|
|
163
188
|
system_prompt=CONTEXT_ID_SYSTEM_PROMPT,
|
|
164
189
|
user_prompt=prompt,
|
|
@@ -176,14 +201,12 @@ def generate_semantic_summary(prompt: str, timeout: int = 15) -> Optional[str]:
|
|
|
176
201
|
# Remove trailing punctuation
|
|
177
202
|
summary = summary.rstrip('.!?')
|
|
178
203
|
|
|
179
|
-
#
|
|
180
|
-
|
|
181
|
-
if not re.match(r'^[A-Z][a-z]*ing\b', summary):
|
|
182
|
-
return None
|
|
204
|
+
# Filter to words with 3+ characters (removes short function words)
|
|
205
|
+
summary = filter_short_words(summary)
|
|
183
206
|
|
|
184
|
-
# Validate
|
|
207
|
+
# Validate 3-10 words (allow flexibility for short prompts)
|
|
185
208
|
words = summary.split()
|
|
186
|
-
if len(words) <
|
|
209
|
+
if len(words) < 3 or len(words) > 10:
|
|
187
210
|
return None
|
|
188
211
|
|
|
189
212
|
return summary
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Utilities for subprocess calls that invoke Claude Code CLI.
|
|
2
|
+
|
|
3
|
+
Provides centralized management of environment flags for internal subprocess calls
|
|
4
|
+
(orchestrator, agents, inference) to prevent recursion and unnecessary hook overhead.
|
|
5
|
+
"""
|
|
6
|
+
import os
|
|
7
|
+
from typing import Dict
|
|
8
|
+
|
|
9
|
+
# Environment variable names - single source of truth
|
|
10
|
+
ENV_INTERNAL_CALL = "AIWCLI_INTERNAL_CALL"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_internal_subprocess_env() -> Dict[str, str]:
|
|
14
|
+
"""Get environment dict for internal Claude Code subprocess calls.
|
|
15
|
+
|
|
16
|
+
This prevents internal subprocess calls (orchestrator, agents, inference)
|
|
17
|
+
from triggering hooks that would cause recursion or unnecessary overhead.
|
|
18
|
+
|
|
19
|
+
All hooks should check is_internal_call() and return early if True.
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
Environment dict with AIWCLI_INTERNAL_CALL flag set
|
|
23
|
+
|
|
24
|
+
Example:
|
|
25
|
+
>>> env = get_internal_subprocess_env()
|
|
26
|
+
>>> subprocess.run(['claude', '--agent', 'my-agent'], env=env)
|
|
27
|
+
"""
|
|
28
|
+
env = os.environ.copy()
|
|
29
|
+
env[ENV_INTERNAL_CALL] = 'true'
|
|
30
|
+
return env
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def is_internal_call() -> bool:
|
|
34
|
+
"""Check if current process is an internal subprocess call.
|
|
35
|
+
|
|
36
|
+
Hooks should check this at the beginning and return early to avoid
|
|
37
|
+
recursion and unnecessary processing for internal subprocess calls.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
True if this is an internal call that should skip hooks
|
|
41
|
+
|
|
42
|
+
Example:
|
|
43
|
+
>>> if is_internal_call():
|
|
44
|
+
... return # Skip hook processing
|
|
45
|
+
"""
|
|
46
|
+
return os.environ.get(ENV_INTERNAL_CALL) == 'true'
|