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.
Files changed (28) hide show
  1. package/README.md +19 -35
  2. package/dist/lib/template-installer.js +38 -0
  3. package/dist/templates/_shared/.claude/commands/handoff.md +219 -7
  4. package/dist/templates/_shared/.codex/workflows/handoff.md +219 -7
  5. package/dist/templates/_shared/.windsurf/workflows/handoff.md +219 -7
  6. package/dist/templates/_shared/hooks/context_enforcer.py +9 -5
  7. package/dist/templates/_shared/hooks/context_monitor.py +28 -10
  8. package/dist/templates/_shared/hooks/file-suggestion.py +45 -15
  9. package/dist/templates/_shared/hooks/user_prompt_submit.py +0 -10
  10. package/dist/templates/_shared/lib/base/constants.py +45 -0
  11. package/dist/templates/_shared/lib/base/inference.py +44 -21
  12. package/dist/templates/_shared/lib/base/subprocess_utils.py +46 -0
  13. package/dist/templates/_shared/lib/base/utils.py +5 -3
  14. package/dist/templates/_shared/lib/context/__init__.py +0 -8
  15. package/dist/templates/_shared/lib/context/cache.py +2 -4
  16. package/dist/templates/_shared/lib/context/context_manager.py +1 -118
  17. package/dist/templates/_shared/lib/context/discovery.py +8 -50
  18. package/dist/templates/_shared/lib/handoff/document_generator.py +2 -5
  19. package/dist/templates/_shared/lib/templates/README.md +0 -1
  20. package/dist/templates/_shared/lib/templates/formatters.py +0 -1
  21. package/dist/templates/_shared/scripts/save_handoff.py +289 -43
  22. package/dist/templates/_shared/workflows/handoff.md +30 -16
  23. package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.py +41 -20
  24. package/dist/templates/cc-native/_cc-native/lib/orchestrator.py +9 -0
  25. package/dist/templates/cc-native/_cc-native/lib/reviewers/agent.py +9 -0
  26. package/dist/templates/cc-native/_cc-native/lib/utils.py +123 -10
  27. package/oclif.manifest.json +1 -1
  28. 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
- description: Generate a session handoff document when ending work
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
- Generate a handoff document summarizing the current session's work, decisions, and pending items.
67
+ # Session Handoff {Date}
7
68
 
8
- ## Arguments
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
- - `$ARGUMENTS` - Optional path to a plan document to update with progress tracking
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
- Load and execute `_shared/workflows/handoff.md`.
215
+ ## Success Criteria
13
216
 
14
- Follow the Gather Analyze Write → Update Context pattern.
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, handoff_pending
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 == "handoff_pending":
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 from pending_implementation to implementing.
194
+ Check if context needs to transition to implementing mode.
195
195
 
196
- This handles the case where a plan was approved and implementation has started,
197
- but the context mode wasn't updated. If we're seeing tool usage (Edit, Write, Bash)
198
- and the context is in "pending_implementation", we transition to "implementing".
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
- # Check if we need to transition
222
- if context.in_flight and context.in_flight.mode == "pending_implementation":
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, estimates context usage,
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 (most recent first)
53
- - Reviews (most recent first)
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 directory
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
- handoff_files = list(handoffs_dir.glob("*.md"))
83
- handoff_files.sort(key=lambda p: p.stat().st_mtime, reverse=True)
84
- files.extend([str(p) for p in handoff_files])
85
- eprint(f"[file-suggestion] Found {len(handoff_files)} handoffs in {context_id}")
86
-
87
- # Get reviews directory (includes cc-native subdirectory)
88
- reviews_dir = get_context_reviews_dir(context_id, project_root)
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.md files in reviews/ and subdirectories (e.g., reviews/cc-native/review.md)
91
- review_files = list(reviews_dir.glob("**/review.md"))
92
- review_files.sort(key=lambda p: p.stat().st_mtime, reverse=True)
93
- files.extend([str(p) for p in review_files])
94
- eprint(f"[file-suggestion] Found {len(review_files)} reviews in {context_id}")
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
- # System prompt for generating context ID summaries
130
- CONTEXT_ID_SYSTEM_PROMPT = """Generate a 10-word summary of what the user wants to do. Start with a gerund (verb ending in -ing).
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
- - Exactly 10 words
134
- - Start with gerund: Creating, Fixing, Adding, Updating, Implementing, etc.
135
- - Be specific about the task
136
- - No punctuation
137
- - No quotes
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" -> "Adding user authentication with JWT tokens to the web app"
141
- - "Fix the bug in the login flow" -> "Fixing critical bug in user login flow validation logic"
142
- - "Can you help me refactor this code" -> "Refactoring legacy code for better maintainability and cleaner architecture"
143
- - "Update the README with new instructions" -> "Updating README with new setup instructions and configuration examples"
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 10-word summary, nothing else."""
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 semantic 10-word summary of a user prompt.
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
- 10-word summary string or None if failed
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
- # Validate it starts with a gerund (capital letter + letters + "ing")
180
- import re
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 roughly 10 words (allow 8-12 for flexibility)
207
+ # Validate 3-10 words (allow flexibility for short prompts)
185
208
  words = summary.split()
186
- if len(words) < 8 or len(words) > 12:
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'