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,40 +1,221 @@
1
1
  #!/usr/bin/env python3
2
- """Save a handoff document and set context status to handoff_pending.
2
+ """Save a handoff document with folder-based sharding.
3
3
 
4
4
  Usage:
5
5
  python .aiwcli/_shared/scripts/save_handoff.py <context_id> <<'EOF'
6
- # Your handoff markdown content here
6
+ # Your handoff markdown content here (with <!-- SECTION: name --> markers)
7
7
  EOF
8
8
 
9
9
  Or with a file:
10
10
  python .aiwcli/_shared/scripts/save_handoff.py <context_id> < handoff.md
11
11
 
12
12
  This script:
13
- 1. Reads handoff markdown content from stdin
14
- 2. Saves it to _output/contexts/{context_id}/handoffs/HANDOFF-{HHMM}.md
15
- 3. Sets in_flight.mode = "handoff_pending"
16
- 4. Records the event in events.jsonl
17
-
18
- The handoff will be automatically picked up by the next session.
13
+ 1. Parses sections from incoming markdown using <!-- SECTION: name --> markers
14
+ 2. Creates a timestamped folder at _output/contexts/{context_id}/handoffs/{YYYY-MM-DD-HHMM}/
15
+ 3. Writes sharded files:
16
+ - index.md (main entry point with navigation)
17
+ - completed-work.md, dead-ends.md, decisions.md, pending.md, context.md
18
+ - plan.md (copy of original plan if it exists)
19
+ 4. Records the event in events.jsonl (informational only)
19
20
  """
21
+ import json
22
+ import re
23
+ import subprocess
20
24
  import sys
21
25
  from datetime import datetime
22
26
  from pathlib import Path
27
+ from typing import Dict, Optional
23
28
 
24
29
  # Add parent directories to path for imports
25
30
  SCRIPT_DIR = Path(__file__).resolve().parent
26
31
  SHARED_ROOT = SCRIPT_DIR.parent
27
32
  sys.path.insert(0, str(SHARED_ROOT))
28
33
 
29
- from lib.context.context_manager import update_handoff_status, get_context
30
- from lib.base.utils import eprint, atomic_write
34
+ from lib.context.context_manager import get_context
35
+ from lib.base.utils import eprint
36
+ from lib.base.atomic_write import atomic_write
37
+ from lib.base.constants import get_handoff_folder_path
38
+
39
+
40
+ def parse_frontmatter(content: str) -> tuple[Dict[str, str], str]:
41
+ """Parse YAML frontmatter from markdown content.
42
+
43
+ Returns:
44
+ Tuple of (frontmatter dict, remaining content)
45
+ """
46
+ frontmatter = {}
47
+ remaining = content
48
+
49
+ if content.startswith('---'):
50
+ parts = content.split('---', 2)
51
+ if len(parts) >= 3:
52
+ fm_lines = parts[1].strip().split('\n')
53
+ for line in fm_lines:
54
+ if ':' in line:
55
+ key, value = line.split(':', 1)
56
+ frontmatter[key.strip()] = value.strip()
57
+ remaining = parts[2].strip()
58
+
59
+ return frontmatter, remaining
60
+
61
+
62
+ def parse_handoff_sections(content: str) -> Dict[str, str]:
63
+ """Parse markdown content by section markers.
64
+
65
+ Looks for <!-- SECTION: name --> markers and extracts content between them.
66
+
67
+ Returns:
68
+ Dict mapping section names to their content
69
+ """
70
+ sections = {}
71
+ current_section = None
72
+ current_content = []
73
+
74
+ for line in content.split('\n'):
75
+ if line.strip().startswith('<!-- SECTION:'):
76
+ # Save previous section
77
+ if current_section:
78
+ sections[current_section] = '\n'.join(current_content).strip()
79
+ # Extract new section name
80
+ match = re.search(r'<!-- SECTION:\s*(\S+)\s*-->', line)
81
+ if match:
82
+ current_section = match.group(1)
83
+ current_content = []
84
+ elif current_section:
85
+ current_content.append(line)
86
+
87
+ # Save final section
88
+ if current_section:
89
+ sections[current_section] = '\n'.join(current_content).strip()
90
+
91
+ return sections
92
+
93
+
94
+ def get_git_status() -> str:
95
+ """Get current git status."""
96
+ try:
97
+ result = subprocess.run(
98
+ ['git', 'status', '--short'],
99
+ capture_output=True,
100
+ text=True,
101
+ timeout=5
102
+ )
103
+ return result.stdout.strip() or "(no changes)"
104
+ except Exception:
105
+ return "(git status unavailable)"
106
+
107
+
108
+ def get_plan_path_from_context(context_id: str, project_root: Path) -> Optional[Path]:
109
+ """Get the plan path from context.json if available."""
110
+ context = get_context(context_id, project_root)
111
+ if not context or not context.in_flight:
112
+ return None
113
+
114
+ artifact_path = context.in_flight.artifact_path
115
+ if artifact_path:
116
+ plan_path = Path(artifact_path)
117
+ if plan_path.exists():
118
+ return plan_path
119
+
120
+ return None
121
+
122
+
123
+ def generate_index(
124
+ frontmatter: Dict[str, str],
125
+ sections: Dict[str, str],
126
+ git_status: str,
127
+ has_plan: bool,
128
+ ) -> str:
129
+ """Generate the index.md file with summary and navigation."""
130
+ now = datetime.now()
131
+
132
+ lines = [
133
+ "---",
134
+ "type: handoff",
135
+ f"context_id: {frontmatter.get('context_id', 'unknown')}",
136
+ f"created_at: {now.isoformat()}",
137
+ f"session_id: {frontmatter.get('session_id', 'unknown')}",
138
+ f"project: {frontmatter.get('project', 'unknown')}",
139
+ f"plan_path: {frontmatter.get('plan_document', 'none')}",
140
+ "---",
141
+ "",
142
+ f"# Session Handoff - {now.strftime('%Y-%m-%d %H:%M')}",
143
+ "",
144
+ ]
145
+
146
+ # Summary section
147
+ summary = sections.get('summary', '').strip()
148
+ if summary:
149
+ # Extract just the content (skip the ## Summary header if present)
150
+ summary_lines = summary.split('\n')
151
+ summary_text = '\n'.join(
152
+ line for line in summary_lines
153
+ if not line.strip().startswith('##')
154
+ ).strip()
155
+ lines.extend([
156
+ "## Summary",
157
+ summary_text,
158
+ "",
159
+ ])
160
+
161
+ # Navigation table
162
+ lines.extend([
163
+ "## Quick Navigation",
164
+ "",
165
+ "| Document | Purpose | Priority |",
166
+ "|----------|---------|----------|",
167
+ "| [Dead Ends](./dead-ends.md) | Failed approaches - DO NOT RETRY | Read First |",
168
+ "| [Pending](./pending.md) | Next steps and blockers | Action Items |",
169
+ "| [Completed Work](./completed-work.md) | Tasks finished this session | Reference |",
170
+ "| [Decisions](./decisions.md) | Technical choices and rationale | Reference |",
171
+ ])
172
+
173
+ if has_plan:
174
+ lines.append("| [Plan](./plan.md) | Original plan being implemented | Reference |")
175
+
176
+ lines.extend([
177
+ "| [Context](./context.md) | External requirements and notes | Reference |",
178
+ "",
179
+ "## Continuation Instructions",
180
+ "",
181
+ "To continue this work in a new session:",
182
+ "1. This index document provides the overview",
183
+ "2. **Read [Dead Ends](./dead-ends.md) first** to avoid repeating failed approaches",
184
+ "3. Check [Pending](./pending.md) for immediate next steps",
185
+ "4. Reference other documents as needed",
186
+ "",
187
+ "## Git Status at Handoff",
188
+ "```",
189
+ git_status,
190
+ "```",
191
+ "",
192
+ ])
193
+
194
+ return '\n'.join(lines)
195
+
196
+
197
+ def write_section_file(folder: Path, filename: str, title: str, content: str) -> bool:
198
+ """Write a section file with header."""
199
+ lines = [
200
+ f"# {title}",
201
+ "",
202
+ content if content else "(No content for this section)",
203
+ "",
204
+ ]
205
+
206
+ file_path = folder / filename
207
+ success, error = atomic_write(file_path, '\n'.join(lines))
208
+ if not success:
209
+ eprint(f"[save_handoff] Warning: Failed to write {filename}: {error}")
210
+ return False
211
+ return True
31
212
 
32
213
 
33
214
  def main():
34
215
  if len(sys.argv) < 2:
35
216
  print("Usage: python save_handoff.py <context_id> < content.md", file=sys.stderr)
36
217
  print(" python save_handoff.py <context_id> <<'EOF'", file=sys.stderr)
37
- print(" ... markdown content ...", file=sys.stderr)
218
+ print(" ... markdown content with <!-- SECTION: name --> markers ...", file=sys.stderr)
38
219
  print(" EOF", file=sys.stderr)
39
220
  sys.exit(1)
40
221
 
@@ -55,44 +236,109 @@ def main():
55
236
  print(f"Error: Context not found: {context_id}", file=sys.stderr)
56
237
  sys.exit(1)
57
238
 
58
- # Create handoffs directory
59
- handoffs_dir = project_root / "_output" / "contexts" / context_id / "handoffs"
60
- handoffs_dir.mkdir(parents=True, exist_ok=True)
239
+ # Parse frontmatter and sections
240
+ frontmatter, body = parse_frontmatter(content)
241
+ sections = parse_handoff_sections(body)
61
242
 
62
- # Generate filename with timestamp
63
- timestamp = datetime.now().strftime("%H%M")
64
- filename = f"HANDOFF-{timestamp}.md"
65
- file_path = handoffs_dir / filename
243
+ eprint(f"[save_handoff] Parsed {len(sections)} sections: {list(sections.keys())}")
66
244
 
67
- # Handle filename collision (add suffix if file exists)
68
- counter = 1
69
- while file_path.exists():
70
- filename = f"HANDOFF-{timestamp}-{counter}.md"
71
- file_path = handoffs_dir / filename
72
- counter += 1
245
+ # Create handoff folder
246
+ handoff_folder = get_handoff_folder_path(context_id, project_root)
247
+ handoff_folder.mkdir(parents=True, exist_ok=True)
248
+ eprint(f"[save_handoff] Created folder: {handoff_folder}")
73
249
 
74
- # Save the handoff document
75
- try:
76
- success, error = atomic_write(file_path, content)
77
- if not success:
78
- print(f"Error: Failed to write handoff: {error}", file=sys.stderr)
79
- sys.exit(1)
80
- except Exception as e:
81
- print(f"Error: Failed to write handoff: {e}", file=sys.stderr)
250
+ # Get git status
251
+ git_status = get_git_status()
252
+
253
+ # Check for plan
254
+ plan_path = get_plan_path_from_context(context_id, project_root)
255
+ has_plan = plan_path is not None
256
+
257
+ # Copy plan if exists
258
+ if plan_path:
259
+ try:
260
+ plan_content = plan_path.read_text(encoding='utf-8')
261
+ plan_dest = handoff_folder / "plan.md"
262
+ success, error = atomic_write(plan_dest, plan_content)
263
+ if success:
264
+ eprint(f"[save_handoff] Copied plan from {plan_path}")
265
+ else:
266
+ eprint(f"[save_handoff] Warning: Failed to copy plan: {error}")
267
+ except Exception as e:
268
+ eprint(f"[save_handoff] Warning: Failed to read plan: {e}")
269
+
270
+ # Write index.md
271
+ index_content = generate_index(frontmatter, sections, git_status, has_plan)
272
+ index_path = handoff_folder / "index.md"
273
+ success, error = atomic_write(index_path, index_content)
274
+ if not success:
275
+ print(f"Error: Failed to write index.md: {error}", file=sys.stderr)
82
276
  sys.exit(1)
83
277
 
84
- # Update context status
85
- try:
86
- update_handoff_status(context_id, str(file_path), project_root)
87
- except Exception as e:
88
- eprint(f"[save_handoff] Warning: Status update failed: {e}")
89
- # Don't exit - file was saved successfully
90
-
91
- # Output success message
92
- print(f"✓ Saved handoff: {file_path}")
93
- print(f"✓ Set status to handoff_pending for context: {context_id}")
278
+ # Write section files
279
+ section_mapping = {
280
+ 'completed': ('completed-work.md', 'Work Completed'),
281
+ 'dead-ends': ('dead-ends.md', 'Dead Ends - Do Not Retry'),
282
+ 'decisions': ('decisions.md', 'Key Decisions'),
283
+ 'pending': ('pending.md', 'Pending Issues'),
284
+ 'next-steps': ('pending.md', None), # Append to pending.md
285
+ 'files': ('completed-work.md', None), # Append to completed-work.md
286
+ 'context': ('context.md', 'Context for Future Sessions'),
287
+ }
288
+
289
+ # Track which files we've written with their content
290
+ file_contents: Dict[str, list] = {}
291
+
292
+ for section_name, (filename, title) in section_mapping.items():
293
+ section_content = sections.get(section_name, '')
294
+ if not section_content:
295
+ continue
296
+
297
+ if title is None:
298
+ # Append mode - add to existing content
299
+ if filename not in file_contents:
300
+ file_contents[filename] = []
301
+ file_contents[filename].append(section_content)
302
+ else:
303
+ # Write mode - set as main content with title
304
+ if filename not in file_contents:
305
+ file_contents[filename] = [f"# {title}", "", section_content]
306
+ else:
307
+ # Insert title at beginning if not present
308
+ file_contents[filename] = [f"# {title}", ""] + file_contents[filename] + ["", section_content]
309
+
310
+ # Write all accumulated content
311
+ for filename, content_parts in file_contents.items():
312
+ file_path = handoff_folder / filename
313
+ full_content = '\n'.join(content_parts) + '\n'
314
+ success, error = atomic_write(file_path, full_content)
315
+ if not success:
316
+ eprint(f"[save_handoff] Warning: Failed to write {filename}: {error}")
317
+
318
+ # Ensure all expected files exist (even if empty)
319
+ expected_files = ['completed-work.md', 'dead-ends.md', 'decisions.md', 'pending.md', 'context.md']
320
+ titles = {
321
+ 'completed-work.md': 'Work Completed',
322
+ 'dead-ends.md': 'Dead Ends - Do Not Retry',
323
+ 'decisions.md': 'Key Decisions',
324
+ 'pending.md': 'Pending Issues & Next Steps',
325
+ 'context.md': 'Context for Future Sessions',
326
+ }
327
+
328
+ for filename in expected_files:
329
+ file_path = handoff_folder / filename
330
+ if not file_path.exists():
331
+ write_section_file(handoff_folder, filename, titles[filename], "")
332
+
333
+ # Output success message (ASCII-safe for Windows)
334
+ print(f"[OK] Created handoff folder: {handoff_folder}")
335
+ print(f" - index.md (entry point with navigation)")
336
+
337
+ files_created = [f.name for f in handoff_folder.iterdir() if f.is_file() and f.name != 'index.md']
338
+ print(f" - {', '.join(sorted(files_created))}")
339
+
94
340
  print()
95
- print("The next session will automatically offer to resume from this handoff.")
341
+ print("Handoff document saved. Use this folder for context in the next session.")
96
342
 
97
343
 
98
344
  if __name__ == "__main__":
@@ -52,7 +52,7 @@ If no active context is found, inform the user and stop - handoffs require an ac
52
52
 
53
53
  ### Step 3: Generate Document
54
54
 
55
- Use this template:
55
+ Use this template. The `<!-- SECTION: name -->` markers are required for the save script to parse sections into sharded files.
56
56
 
57
57
  ```markdown
58
58
  ---
@@ -66,34 +66,40 @@ plan_document: {path to plan if provided, or "none"}
66
66
 
67
67
  # Session Handoff — {Date}
68
68
 
69
+ <!-- SECTION: summary -->
69
70
  ## Summary
70
71
  {2-3 sentences: what's different now vs. session start}
71
72
 
73
+ <!-- SECTION: completed -->
72
74
  ## Work Completed
73
75
  {Grouped by category if multiple areas. Specific file:function references.}
74
76
 
77
+ <!-- SECTION: dead-ends -->
75
78
  ## Dead Ends — Do Not Retry
76
- {Approaches that were tried and failed. Critical for avoiding wasted effort in future sessions.}
77
79
 
78
- ### {Problem/Goal attempted}
79
- | Approach Tried | Why It Failed | Time Spent |
80
- |----------------|---------------|------------|
81
- | {What was attempted} | {Specific reason: error, incompatibility, performance, etc.} | {Rough estimate} |
80
+ These approaches were attempted and failed. Do not retry without addressing the root cause.
82
81
 
83
- **What to try instead**: {If known, suggest alternative direction}
82
+ | Approach | Why It Failed | Time Spent | Alternative |
83
+ |----------|---------------|------------|-------------|
84
+ | {What was attempted} | {Specific reason} | {Rough estimate} | {What to try instead} |
84
85
 
86
+ <!-- SECTION: decisions -->
85
87
  ## Key Decisions
86
88
  {Technical choices with rationale. Format: **Decision**: Rationale. Trade-off: X.}
87
89
 
90
+ <!-- SECTION: pending -->
88
91
  ## Pending Issues
89
92
  - [ ] {Issue} — {severity: HIGH/MED/LOW} {optional workaround note}
90
93
 
94
+ <!-- SECTION: next-steps -->
91
95
  ## Next Steps
92
96
  1. {Actionable item with file:line reference if applicable}
93
97
 
98
+ <!-- SECTION: files -->
94
99
  ## Files Modified
95
100
  {Significant changes only. Skip formatting-only edits.}
96
101
 
102
+ <!-- SECTION: context -->
97
103
  ## Context for Future Sessions
98
104
  {Non-obvious context: env quirks, stakeholder requirements}
99
105
 
@@ -150,11 +156,12 @@ EOF
150
156
  ```
151
157
 
152
158
  This script:
153
- 1. Saves the handoff to `_output/contexts/{context_id}/handoffs/HANDOFF-{HHMM}.md`
154
- 2. Sets `in_flight.mode = "handoff_pending"`
155
- 3. Records the event in the context's event log
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)
156
163
 
157
- The next session will automatically detect the handoff and offer to resume.
164
+ Use the handoff folder for context in the next session.
158
165
 
159
166
  ## Dead Ends Section Guidelines
160
167
 
@@ -185,10 +192,14 @@ This section is critical for preventing context rot across sessions. Be specific
185
192
  After creating file, output:
186
193
 
187
194
  ```
188
- ✓ Created _output/contexts/{context_id}/handoffs/HANDOFF-{HHMM}.md
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)
189
199
 
190
200
  To continue next session:
191
- "Load _output/contexts/{context_id}/handoffs/HANDOFF-{HHMM}.md and continue from next steps"
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.
192
203
 
193
204
  ⚠️ {N} dead ends documented — avoid re-attempting these approaches
194
205
  ```
@@ -203,10 +214,13 @@ If plan was updated:
203
214
 
204
215
  ## Success Criteria
205
216
 
206
- - [ ] Handoff document created in context's `handoffs/` subfolder
207
- - [ ] Dead ends section captures all failed approaches with specific details
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
208
222
  - [ ] Next steps are actionable with file references
209
- - [ ] Git status reflects current state
223
+ - [ ] Git status included in index.md
210
224
  - [ ] If plan provided: checkboxes updated to reflect completion status
211
225
  - [ ] If plan provided: Session Progress Log appended
212
226
  - [ ] Context state updated to indicate handoff pending
@@ -28,6 +28,7 @@ Output: _output/cc-native/plans/{YYYY-MM-DD}/{slug}/reviews/
28
28
  """
29
29
 
30
30
  import json
31
+ import os
31
32
  import sys
32
33
  from concurrent.futures import ThreadPoolExecutor, as_completed
33
34
  from datetime import datetime
@@ -43,6 +44,9 @@ try:
43
44
  _shared = Path(__file__).parent.parent.parent / "_shared"
44
45
  sys.path.insert(0, str(_shared))
45
46
 
47
+ # Import subprocess utilities
48
+ from lib.base.subprocess_utils import is_internal_call
49
+
46
50
  from utils import (
47
51
  DEFAULT_DISPLAY,
48
52
  DEFAULT_SANITIZATION,
@@ -79,7 +83,7 @@ try:
79
83
  get_all_in_flight_contexts,
80
84
  get_all_contexts,
81
85
  )
82
- from lib.base.constants import get_context_reviews_dir
86
+ from lib.base.constants import get_context_reviews_dir, get_review_folder_path
83
87
  except ImportError as e:
84
88
  print(f"[cc-native-plan-review] Failed to import lib: {e}", file=sys.stderr)
85
89
  sys.exit(0) # Non-blocking failure
@@ -133,9 +137,10 @@ def get_active_context_for_review(session_id: str, project_root: Path) -> Option
133
137
 
134
138
  Strategy:
135
139
  1. Find context by session_id
136
- 2. Fallback: Single in-flight context
137
- 3. Fallback: Single planning context
138
- 4. Return None if multiple or no contexts found
140
+ 2. Fallback: Single context in 'planning' mode
141
+ 3. Return None if multiple planning contexts or no planning contexts found
142
+
143
+ Only triggers for contexts in 'planning' mode, not 'handoff_pending' or other modes.
139
144
 
140
145
  Args:
141
146
  session_id: Current session ID
@@ -150,23 +155,21 @@ def get_active_context_for_review(session_id: str, project_root: Path) -> Option
150
155
  eprint(f"[cc-native-plan-review] Found context by session_id: {context.id}")
151
156
  return context
152
157
 
153
- # Strategy 2: Single in-flight context
158
+ # Strategy 2: Single planning context (only planning mode)
154
159
  in_flight = get_all_in_flight_contexts(project_root)
155
- if len(in_flight) == 1:
156
- eprint(f"[cc-native-plan-review] Found single in-flight context: {in_flight[0].id}")
157
- return in_flight[0]
158
-
159
- # Strategy 3: Single planning context
160
160
  planning_contexts = [c for c in in_flight if c.in_flight and c.in_flight.mode == "planning"]
161
161
  if len(planning_contexts) == 1:
162
162
  eprint(f"[cc-native-plan-review] Found single planning context: {planning_contexts[0].id}")
163
163
  return planning_contexts[0]
164
164
 
165
- # Multiple or no contexts found
166
- if len(in_flight) > 1:
167
- eprint(f"[cc-native-plan-review] Multiple in-flight contexts ({len(in_flight)}), falling back to legacy")
165
+ # Multiple or no planning contexts found
166
+ if len(planning_contexts) > 1:
167
+ eprint(f"[cc-native-plan-review] Multiple planning contexts ({len(planning_contexts)}), cannot determine which to use")
168
+ elif len(in_flight) > 0:
169
+ modes = [c.in_flight.mode if c.in_flight else "none" for c in in_flight]
170
+ eprint(f"[cc-native-plan-review] Found {len(in_flight)} in-flight context(s) with modes {modes}, but none in 'planning' mode")
168
171
  else:
169
- eprint("[cc-native-plan-review] No in-flight contexts found, falling back to legacy")
172
+ eprint("[cc-native-plan-review] No in-flight contexts found")
170
173
  return None
171
174
 
172
175
 
@@ -423,6 +426,11 @@ def load_agent_library(proj_dir: Path, settings: Optional[Dict[str, Any]] = None
423
426
  def main() -> int:
424
427
  eprint("[cc-native-plan-review] Unified hook started (PreToolUse)")
425
428
 
429
+ # Skip if internal subprocess call (orchestrator, agents)
430
+ if is_internal_call():
431
+ eprint("[cc-native-plan-review] Skipping: internal subprocess call")
432
+ return 0
433
+
426
434
  try:
427
435
  payload = json.load(sys.stdin)
428
436
  except json.JSONDecodeError as e:
@@ -582,11 +590,8 @@ def main() -> int:
582
590
  # Use orchestrator result from phase 1
583
591
  detected_complexity = orch_result.complexity
584
592
 
585
- if orch_result.complexity == "simple" and not orch_result.selected_agents:
586
- eprint("[cc-native-plan-review] Orchestrator determined: simple complexity, no agent review needed")
587
- else:
588
- selected_names = set(orch_result.selected_agents)
589
- selected_agents = [a for a in enabled_agents if a.name in selected_names]
593
+ selected_names = set(orch_result.selected_agents)
594
+ selected_agents = [a for a in enabled_agents if a.name in selected_names]
590
595
 
591
596
  if not selected_agents and selected_names:
592
597
  eprint(f"[cc-native-plan-review] Warning: orchestrator selected unknown agents: {selected_names}")
@@ -659,9 +664,20 @@ def main() -> int:
659
664
  display_settings = {**plan_settings.get("display", {}), **agent_settings.get("display", {})}
660
665
  combined_settings = {"display": display_settings}
661
666
 
667
+ # Get current iteration number for folder naming
668
+ current_iteration = 1
669
+ if iteration_state:
670
+ current_iteration = iteration_state.get("current", 1)
671
+
672
+ # Create review folder with datetime and iteration in name
673
+ review_folder = get_review_folder_path(active_context.id, current_iteration, base)
674
+ review_folder.mkdir(parents=True, exist_ok=True)
675
+ eprint(f"[cc-native-plan-review] Created review folder: {review_folder}")
676
+
662
677
  review_file = write_combined_artifacts(
663
678
  base, plan, combined_result, payload, combined_settings,
664
- context_reviews_dir=reviews_dir
679
+ review_folder=review_folder,
680
+ iteration=current_iteration,
665
681
  )
666
682
  eprint(f"[cc-native-plan-review] Saved review: {review_file}")
667
683
 
@@ -704,6 +720,11 @@ def main() -> int:
704
720
  else:
705
721
  # Final iteration - increment current and save state
706
722
  iteration_state["current"] = iteration_state.get("current", 1) + 1
723
+ # Also increment max by 1 to allow another review cycle if the user rejects
724
+ # the plan and requests changes. Without this, once iterations are exhausted,
725
+ # the hook would skip review entirely (line ~498) even if the user sent the
726
+ # planner back to revise. This ensures rejected plans can always be re-reviewed.
727
+ iteration_state["max"] = iteration_state.get("max", 1) + 1
707
728
  save_iteration_state(reviews_dir, iteration_state)
708
729
 
709
730
  # Build output with correct Claude Code hook format
@@ -18,6 +18,11 @@ sys.path.insert(0, str(_lib_dir))
18
18
  from utils import OrchestratorResult, eprint, parse_json_maybe
19
19
  from reviewers.base import AgentConfig, OrchestratorConfig
20
20
 
21
+ # Import shared subprocess utilities
22
+ _shared_lib = Path(__file__).resolve().parent.parent.parent / "_shared" / "lib" / "base"
23
+ sys.path.insert(0, str(_shared_lib))
24
+ from subprocess_utils import get_internal_subprocess_env
25
+
21
26
 
22
27
  # ---------------------------
23
28
  # Constants
@@ -199,6 +204,9 @@ PLAN:
199
204
 
200
205
  eprint(f"[orchestrator] Running with model: {config.model}, timeout: {config.timeout}s")
201
206
 
207
+ # Get environment for internal subprocess (bypasses hooks)
208
+ env = get_internal_subprocess_env()
209
+
202
210
  try:
203
211
  p = subprocess.run(
204
212
  cmd_args,
@@ -208,6 +216,7 @@ PLAN:
208
216
  timeout=config.timeout,
209
217
  encoding="utf-8",
210
218
  errors="replace",
219
+ env=env,
211
220
  )
212
221
  except subprocess.TimeoutExpired:
213
222
  eprint(f"[orchestrator] TIMEOUT after {config.timeout}s, falling back to medium complexity")
@@ -18,6 +18,11 @@ sys.path.insert(0, str(_lib_dir))
18
18
  from utils import ReviewerResult, eprint, parse_json_maybe, coerce_to_review
19
19
  from .base import AgentConfig, AGENT_REVIEW_PROMPT_PREFIX
20
20
 
21
+ # Import shared subprocess utilities
22
+ _shared_lib = Path(__file__).resolve().parent.parent.parent.parent / "_shared" / "lib" / "base"
23
+ sys.path.insert(0, str(_shared_lib))
24
+ from subprocess_utils import get_internal_subprocess_env
25
+
21
26
 
22
27
  def _parse_claude_output(raw: str) -> Optional[Dict[str, Any]]:
23
28
  """Parse Claude CLI JSON output, handling various formats.
@@ -125,6 +130,9 @@ PLAN:
125
130
 
126
131
  eprint(f"[{agent.name}] Running with model: {agent.model}, timeout: {timeout}s, max-turns: {max_turns}")
127
132
 
133
+ # Get environment for internal subprocess (bypasses hooks)
134
+ env = get_internal_subprocess_env()
135
+
128
136
  try:
129
137
  p = subprocess.run(
130
138
  cmd_args,
@@ -134,6 +142,7 @@ PLAN:
134
142
  timeout=timeout,
135
143
  encoding="utf-8",
136
144
  errors="replace",
145
+ env=env,
137
146
  )
138
147
  except subprocess.TimeoutExpired:
139
148
  eprint(f"[{agent.name}] TIMEOUT after {timeout}s")