aiwcli 0.12.1 → 0.12.3

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 (84) hide show
  1. package/dist/templates/_shared/.claude/commands/handoff.md +44 -78
  2. package/dist/templates/_shared/hooks-ts/session_end.ts +16 -11
  3. package/dist/templates/_shared/hooks-ts/session_start.ts +25 -16
  4. package/dist/templates/_shared/hooks-ts/user_prompt_submit.ts +20 -8
  5. package/dist/templates/_shared/lib-ts/base/inference.ts +72 -23
  6. package/dist/templates/_shared/lib-ts/base/state-io.ts +12 -7
  7. package/dist/templates/_shared/lib-ts/context/context-formatter.ts +151 -29
  8. package/dist/templates/_shared/lib-ts/context/context-store.ts +35 -74
  9. package/dist/templates/_shared/lib-ts/types.ts +64 -63
  10. package/dist/templates/_shared/scripts/resolve_context.ts +14 -5
  11. package/dist/templates/_shared/scripts/resume_handoff.ts +41 -13
  12. package/dist/templates/_shared/scripts/save_handoff.ts +30 -31
  13. package/dist/templates/_shared/workflows/handoff.md +28 -6
  14. package/dist/templates/cc-native/.claude/commands/rlm/ask.md +136 -0
  15. package/dist/templates/cc-native/.claude/commands/rlm/index.md +21 -0
  16. package/dist/templates/cc-native/.claude/commands/rlm/overview.md +56 -0
  17. package/dist/templates/cc-native/TEMPLATE-SCHEMA.md +4 -4
  18. package/dist/templates/cc-native/_cc-native/agents/CLAUDE.md +1 -7
  19. package/dist/templates/cc-native/_cc-native/agents/plan-review/ARCH-EVOLUTION.md +62 -63
  20. package/dist/templates/cc-native/_cc-native/agents/plan-review/ARCH-PATTERNS.md +61 -62
  21. package/dist/templates/cc-native/_cc-native/agents/plan-review/ARCH-STRUCTURE.md +62 -63
  22. package/dist/templates/cc-native/_cc-native/agents/plan-review/ASSUMPTION-TRACER.md +56 -57
  23. package/dist/templates/cc-native/_cc-native/agents/plan-review/CLARITY-AUDITOR.md +53 -54
  24. package/dist/templates/cc-native/_cc-native/agents/plan-review/COMPLETENESS-FEASIBILITY.md +66 -67
  25. package/dist/templates/cc-native/_cc-native/agents/plan-review/COMPLETENESS-GAPS.md +70 -71
  26. package/dist/templates/cc-native/_cc-native/agents/plan-review/COMPLETENESS-ORDERING.md +62 -63
  27. package/dist/templates/cc-native/_cc-native/agents/plan-review/CONSTRAINT-VALIDATOR.md +72 -73
  28. package/dist/templates/cc-native/_cc-native/agents/plan-review/DESIGN-ADR-VALIDATOR.md +61 -62
  29. package/dist/templates/cc-native/_cc-native/agents/plan-review/DESIGN-SCALE-MATCHER.md +64 -65
  30. package/dist/templates/cc-native/_cc-native/agents/plan-review/DEVILS-ADVOCATE.md +56 -57
  31. package/dist/templates/cc-native/_cc-native/agents/plan-review/DOCUMENTATION-PHILOSOPHY.md +86 -87
  32. package/dist/templates/cc-native/_cc-native/agents/plan-review/HANDOFF-READINESS.md +59 -60
  33. package/dist/templates/cc-native/_cc-native/agents/plan-review/HIDDEN-COMPLEXITY.md +58 -59
  34. package/dist/templates/cc-native/_cc-native/agents/plan-review/INCREMENTAL-DELIVERY.md +66 -67
  35. package/dist/templates/cc-native/_cc-native/agents/plan-review/RISK-DEPENDENCY.md +62 -63
  36. package/dist/templates/cc-native/_cc-native/agents/plan-review/RISK-FMEA.md +66 -67
  37. package/dist/templates/cc-native/_cc-native/agents/plan-review/RISK-PREMORTEM.md +71 -72
  38. package/dist/templates/cc-native/_cc-native/agents/plan-review/RISK-REVERSIBILITY.md +74 -75
  39. package/dist/templates/cc-native/_cc-native/agents/plan-review/SCOPE-BOUNDARY.md +77 -78
  40. package/dist/templates/cc-native/_cc-native/agents/plan-review/SIMPLICITY-GUARDIAN.md +62 -63
  41. package/dist/templates/cc-native/_cc-native/agents/plan-review/SKEPTIC.md +68 -69
  42. package/dist/templates/cc-native/_cc-native/agents/plan-review/TESTDRIVEN-BEHAVIOR-AUDITOR.md +61 -62
  43. package/dist/templates/cc-native/_cc-native/agents/plan-review/TESTDRIVEN-CHARACTERIZATION.md +71 -72
  44. package/dist/templates/cc-native/_cc-native/agents/plan-review/TESTDRIVEN-FIRST-VALIDATOR.md +61 -62
  45. package/dist/templates/cc-native/_cc-native/agents/plan-review/TESTDRIVEN-PYRAMID-ANALYZER.md +61 -62
  46. package/dist/templates/cc-native/_cc-native/agents/plan-review/TRADEOFF-COSTS.md +67 -68
  47. package/dist/templates/cc-native/_cc-native/agents/plan-review/TRADEOFF-STAKEHOLDERS.md +65 -66
  48. package/dist/templates/cc-native/_cc-native/agents/plan-review/VERIFY-COVERAGE.md +74 -75
  49. package/dist/templates/cc-native/_cc-native/agents/plan-review/VERIFY-STRENGTH.md +69 -70
  50. package/dist/templates/cc-native/_cc-native/{plan-review.config.json → cc-native.config.json} +12 -0
  51. package/dist/templates/cc-native/_cc-native/hooks/CLAUDE.md +19 -2
  52. package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.ts +28 -1010
  53. package/dist/templates/cc-native/_cc-native/lib-ts/agent-selection.ts +163 -0
  54. package/dist/templates/cc-native/_cc-native/lib-ts/aggregate-agents.ts +1 -2
  55. package/dist/templates/cc-native/_cc-native/lib-ts/artifacts/format.ts +597 -0
  56. package/dist/templates/cc-native/_cc-native/lib-ts/artifacts/index.ts +26 -0
  57. package/dist/templates/cc-native/_cc-native/lib-ts/artifacts/tracker.ts +107 -0
  58. package/dist/templates/cc-native/_cc-native/lib-ts/artifacts/write.ts +119 -0
  59. package/dist/templates/cc-native/_cc-native/lib-ts/artifacts.ts +19 -821
  60. package/dist/templates/cc-native/_cc-native/lib-ts/cc-native-state.ts +36 -13
  61. package/dist/templates/cc-native/_cc-native/lib-ts/config.ts +3 -3
  62. package/dist/templates/cc-native/_cc-native/lib-ts/graduation.ts +132 -0
  63. package/dist/templates/cc-native/_cc-native/lib-ts/orchestrator.ts +1 -2
  64. package/dist/templates/cc-native/_cc-native/lib-ts/output-builder.ts +130 -0
  65. package/dist/templates/cc-native/_cc-native/lib-ts/plan-discovery.ts +80 -0
  66. package/dist/templates/cc-native/_cc-native/lib-ts/review-pipeline.ts +511 -0
  67. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/providers/orchestrator-claude-agent.ts +1 -1
  68. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/CLAUDE.md +480 -0
  69. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/embedding-indexer.ts +287 -0
  70. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/hyde.ts +148 -0
  71. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/index.ts +54 -0
  72. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/logger.ts +58 -0
  73. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/ollama-client.ts +208 -0
  74. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/retrieval-pipeline.ts +460 -0
  75. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/transcript-indexer.ts +447 -0
  76. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/transcript-loader.ts +280 -0
  77. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/transcript-searcher.ts +274 -0
  78. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/types.ts +201 -0
  79. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/vector-store.ts +278 -0
  80. package/dist/templates/cc-native/_cc-native/lib-ts/settings.ts +184 -0
  81. package/dist/templates/cc-native/_cc-native/lib-ts/state.ts +51 -17
  82. package/dist/templates/cc-native/_cc-native/lib-ts/types.ts +42 -3
  83. package/oclif.manifest.json +1 -1
  84. package/package.json +1 -1
@@ -8,6 +8,20 @@ Generate a handoff document summarizing the current session's work, decisions, a
8
8
  - `/handoff path/to/PLAN.md` - with plan document integration
9
9
  - Phrases like "write a handoff", "create a session summary", "document what we did", "end session with notes"
10
10
 
11
+ ## Philosophy
12
+
13
+ Handoffs exist to minimize the token and time cost of session startup. A precise handoff lets the next session hit the ground running without rediscovering context through Explore agents or file searches.
14
+
15
+ **Think: "What would I need to continue this work with zero prior context?"**
16
+
17
+ Key principles:
18
+ - **Absolute paths over vague references** — `C:\project\src\auth\handler.ts:45` not "handler.ts line 45"
19
+ - **Specific locations over general areas** — Name the function/class/section, not just the file
20
+ - **Enough detail to skip discovery** — Include enough context that the next session doesn't need to grep or search
21
+ - **Self-contained decisions** — Capture the "why" and the "why not" so alternatives aren't re-explored
22
+
23
+ A good handoff pays for itself in the first minute of the next session.
24
+
11
25
  ## Arguments
12
26
 
13
27
  - `$ARGUMENTS` - Optional path to a plan document. If provided, the handoff will:
@@ -17,19 +31,7 @@ Generate a handoff document summarizing the current session's work, decisions, a
17
31
 
18
32
  ## Process
19
33
 
20
- ### Step 1: Get Context ID
21
-
22
- Resolve the active context ID programmatically:
23
-
24
- ```bash
25
- bun .aiwcli/_shared/scripts/resolve_context.ts
26
- ```
27
-
28
- This prints the active context ID to stdout. Use its output as `{context_id}` in subsequent steps.
29
-
30
- If the script exits with an error (no active context found), inform the user and stop — handoffs require an active context.
31
-
32
- ### Step 2: Gather Information
34
+ ### Step 1: Gather Information
33
35
 
34
36
  1. Review conversation history for:
35
37
  - Completed tasks and implementations
@@ -37,21 +39,15 @@ If the script exits with an error (no active context found), inform the user and
37
39
  - Failed approaches (to avoid repeating)
38
40
  - External context (deadlines, stakeholder requirements)
39
41
 
40
- 2. Check git status if available:
41
- ```bash
42
- git status --short
43
- git diff --stat
44
- ```
45
-
46
- 3. Look for TODOs/FIXMEs mentioned in session
42
+ 2. Look for TODOs/FIXMEs mentioned in session
47
43
 
48
- 4. **If plan document provided**: Read the plan and identify:
44
+ 3. **If plan document provided**: Read the plan and identify:
49
45
  - Tasks that are now completed
50
46
  - Tasks that are partially done
51
47
  - Tasks that were attempted but blocked
52
48
  - New tasks discovered during implementation
53
49
 
54
- ### Step 3: Generate Document
50
+ ### Step 2: Generate Document
55
51
 
56
52
  Use this template. The `<!-- SECTION: name -->` markers are required for the save script to parse sections into sharded files.
57
53
 
@@ -61,7 +57,6 @@ title: Session Handoff
61
57
  date: {ISO timestamp}
62
58
  session_id: {conversation ID if available}
63
59
  project: {project name from package.json, Cargo.toml, or directory name}
64
- context_id: {context_id from Step 1}
65
60
  plan_document: {path to plan if provided, or "none"}
66
61
  ---
67
62
 
@@ -69,11 +64,24 @@ plan_document: {path to plan if provided, or "none"}
69
64
 
70
65
  <!-- SECTION: summary -->
71
66
  ## Summary
72
- {2-3 sentences: what's different now vs. session start}
67
+ {2-3 sentences covering: (1) session goal, (2) what changed technically, (3) current state}
68
+
69
+ Example: "Session goal: Implement JWT refresh tokens. Added refresh token generation in `C:\project\src\auth\tokens.ts:145-203` and rotation endpoint. Feature works but needs rate-limiting before production (see Pending Issues)."
73
70
 
74
71
  <!-- SECTION: completed -->
75
72
  ## Work Completed
76
- {Grouped by category if multiple areas. Specific file:function references.}
73
+
74
+ **Bad (requires exploration):**
75
+ - Fixed auth bug
76
+ - Updated tests
77
+
78
+ **Good (exploration-free):**
79
+ - Auth: Fixed JWT validation race condition in `C:\project\src\auth\handler.ts:67-89`
80
+ - Issue: Token validation happened after user session creation → security gap
81
+ - Fix: Moved validation to middleware, added async lock
82
+ - Tests updated: `C:\project\tests\auth\handler.test.ts:45` now covers concurrent validation
83
+
84
+ {Your work completed here, following the "Good" example format with absolute paths}
77
85
 
78
86
  <!-- SECTION: dead-ends -->
79
87
  ## Dead Ends — Do Not Retry
@@ -94,11 +102,11 @@ These approaches were attempted and failed. Do not retry without addressing the
94
102
 
95
103
  <!-- SECTION: next-steps -->
96
104
  ## Next Steps
97
- 1. {Actionable item with file:line reference if applicable}
105
+ 1. {Actionable item with absolute path and line number, e.g., C:\path\to\file.ts:123}
98
106
 
99
107
  <!-- SECTION: files -->
100
108
  ## Files Modified
101
- {Significant changes only. Skip formatting-only edits.}
109
+ {Absolute paths with line numbers for significant changes. Skip formatting-only edits.}
102
110
 
103
111
  <!-- SECTION: context -->
104
112
  ## Context for Future Sessions
@@ -106,7 +114,7 @@ These approaches were attempted and failed. Do not retry without addressing the
106
114
 
107
115
  ```
108
116
 
109
- ### Step 4: Update Plan Document (if provided)
117
+ ### Step 3: Update Plan Document (if provided)
110
118
 
111
119
  If a plan document path was provided in `$ARGUMENTS`:
112
120
 
@@ -146,27 +154,17 @@ If a plan document path was provided in `$ARGUMENTS`:
146
154
  5. **If no plan document was provided**:
147
155
  - Skip plan creation - the handoff document serves as the session record
148
156
 
149
- ### Step 5: Save and Update Status
157
+ ### Step 4: Save and Update Status
150
158
 
151
159
  Instead of writing the file directly, pipe your handoff content to the save script:
152
160
 
153
161
  ```bash
154
- bun .aiwcli/_shared/scripts/save_handoff.ts "{context_id}" <<'EOF'
155
- {Your complete handoff markdown content from Step 3}
162
+ bun .aiwcli/_shared/scripts/save_handoff.ts <<'EOF'
163
+ {Your complete handoff markdown content from Step 2}
156
164
  EOF
157
165
  ```
158
166
 
159
- This script:
160
- 1. Creates a folder at `_output/contexts/{context_id}/handoffs/{YYYY-MM-DD-HHMM}/`
161
- 2. Parses sections and writes sharded files (index.md, completed-work.md, dead-ends.md, etc.)
162
- 3. Copies the current plan (if any) to plan.md
163
- 4. Sets `handoff_path` to the index.md path and `handoff_consumed = false` in state.json
164
- 5. Mode stays `active` — staging happens later via session_end hook
165
-
166
- When the session ends, `session_end.ts` stages `active → has_handoff` (if handoff_path
167
- exists and handoff_consumed is false). On next `/clear`, `session_start.ts` picks up the
168
- `has_handoff` state, binds the new session, transitions to `active`, and injects the
169
- handoff content via `formatHandoffContinuation()`.
167
+ The script will automatically find the active context and create the handoff folder structure.
170
168
 
171
169
  ## Dead Ends Section Guidelines
172
170
 
@@ -194,39 +192,7 @@ This section is critical for preventing context rot across sessions. Be specific
194
192
 
195
193
  ## Post-Generation Output
196
194
 
197
- After creating file, output:
198
-
199
- ```
200
- Created handoff folder: _output/contexts/{context_id}/handoffs/{YYYY-MM-DD-HHMM}/
201
- - index.md (entry point with navigation)
202
- - completed-work.md, dead-ends.md, decisions.md, pending.md, context.md
203
- - plan.md (copy of current plan, if any)
204
-
205
- To continue next session:
206
- Automatic: Handoff restored on next /clear via session_start hook.
207
- Manual: Use /handoff-resume to explicitly load handoff context at any time.
208
- Read dead-ends.md first to avoid repeating failed approaches.
209
-
210
- ⚠️ {N} dead ends documented — avoid re-attempting these approaches
211
- ```
212
-
213
- If plan was updated:
214
- ```
215
- ✓ Updated plan document: {path}
216
- - {N} items marked complete
217
- - {N} items partially complete
218
- - {N} new items added
219
- ```
220
-
221
- ## Success Criteria
222
-
223
- - [ ] Handoff folder created at `handoffs/{YYYY-MM-DD-HHMM}/`
224
- - [ ] index.md contains summary and navigation table
225
- - [ ] All section files created (completed-work.md, dead-ends.md, etc.)
226
- - [ ] Dead ends use structured table format for quick scanning
227
- - [ ] plan.md copied from context if plan exists
228
- - [ ] Next steps are actionable with file references
229
- - [ ] Git status included in index.md
230
- - [ ] If plan provided: checkboxes updated to reflect completion status
231
- - [ ] If plan provided: Session Progress Log appended
232
- - [ ] State has handoff_path set and handoff_consumed = false
195
+ After saving, confirm:
196
+ - Handoff folder location
197
+ - Number of dead ends documented (if any)
198
+ - Plan update summary (if plan was provided)
@@ -3,20 +3,19 @@
3
3
  * SessionEnd hook: Save session state, assign plan fields (fallback),
4
4
  * stage has_plan/has_handoff for next session.
5
5
  */
6
- import * as crypto from "node:crypto";
7
6
  import * as fs from "node:fs";
8
- import * as path from "node:path";
9
-
10
- import { getProjectRoot, getContextDir } from "../lib-ts/base/constants.js";
11
- import { getGitState } from "../lib-ts/base/git-state.js";
7
+ import * as crypto from "node:crypto";
8
+ import * as path from "node:path";
12
9
  import {
13
10
  loadHookInput, runHook, logDebug, logInfo, logWarn, logError, logDiagnostic,
14
11
  } from "../lib-ts/base/hook-utils.js";
12
+ import { getProjectRoot, getContextDir } from "../lib-ts/base/constants.js";
15
13
  import { nowIso } from "../lib-ts/base/utils.js";
16
14
  import { getContextBySessionId, saveState } from "../lib-ts/context/context-store.js";
17
15
  import {
18
16
  findLatestPlan, normalizePlanContent, generatePlanId, extractPlanAnchors,
19
17
  } from "../lib-ts/context/plan-manager.js";
18
+ import { getGitState } from "../lib-ts/base/git-state.js";
20
19
 
21
20
  /**
22
21
  * Archive session transcript to context's session-transcripts/ folder.
@@ -63,8 +62,8 @@ function archiveTranscript(
63
62
  try {
64
63
  fs.copyFileSync(transcriptPath, archivePath);
65
64
  return archivePath;
66
- } catch (error) {
67
- logError("session_end", `Failed to copy transcript: ${error}`);
65
+ } catch (e) {
66
+ logError("session_end", `Failed to copy transcript: ${e}`);
68
67
  return null;
69
68
  }
70
69
  }
@@ -116,8 +115,8 @@ function main(): void {
116
115
  if (archived) {
117
116
  logInfo("session_end", `Archived transcript: ${path.basename(archived)}`);
118
117
  }
119
- } catch (error) {
120
- logError("session_end", `Transcript archival failed: ${error}`);
118
+ } catch (e) {
119
+ logError("session_end", `Transcript archival failed: ${e}`);
121
120
  }
122
121
  }
123
122
 
@@ -146,12 +145,18 @@ function main(): void {
146
145
  state.plan_consumed = state.plan_consumed || false;
147
146
 
148
147
  logInfo("session_end", `Assigned plan fallback: hash=${planHash}, path=${latestPlanPath}`);
149
- } catch (error) {
150
- logError("session_end", `Failed to read plan: ${error}`);
148
+ } catch (e) {
149
+ logError("session_end", `Failed to read plan: ${e}`);
151
150
  }
152
151
  }
153
152
  }
154
153
 
154
+ // NEW PLAN DETECTION: If plan_hash differs from last consumed hash, reset consumed flag
155
+ if (state.plan_hash && state.plan_hash_consumed && state.plan_hash !== state.plan_hash_consumed) {
156
+ logInfo("session_end", `New plan detected: ${state.plan_hash} != ${state.plan_hash_consumed}, resetting plan_consumed`);
157
+ state.plan_consumed = false;
158
+ }
159
+
155
160
  // Step 2: Stage has_plan if conditions met
156
161
  if (state.plan_hash && state.mode === "active" && !state.plan_consumed) {
157
162
  state.mode = "has_plan";
@@ -3,17 +3,18 @@
3
3
  * SessionStart hook: Restore context after /clear (plan/handoff) or compaction.
4
4
  * Routes by source field to appropriate handler.
5
5
  */
6
- import { getProjectRoot } from "../lib-ts/base/constants.js";
7
6
  import {
8
7
  loadHookInput, emitContext, runHook, runHookAsync,
9
8
  logDebug, logInfo, logError, logDiagnostic,
10
9
  } from "../lib-ts/base/hook-utils.js";
11
- import {
12
- buildRestoreSections, formatHandoffContinuation, getModeDisplay,
13
- } from "../lib-ts/context/context-formatter.js";
10
+ import { getProjectRoot } from "../lib-ts/base/constants.js";
14
11
  import {
15
12
  getContextBySessionId, getAllContexts, bindSession, updateMode,
16
13
  } from "../lib-ts/context/context-store.js";
14
+ import {
15
+ buildRestoreSections, formatHandoffContinuation, getModeDisplay,
16
+ buildContextInventory,
17
+ } from "../lib-ts/context/context-formatter.js";
17
18
  import type { ContextState } from "../lib-ts/types.js";
18
19
 
19
20
  /**
@@ -39,6 +40,9 @@ function handleCompactRestore(sessionId: string, projectRoot: string): void {
39
40
  const restore = buildRestoreSections(state, projectRoot, true);
40
41
  if (restore) sections.push(restore);
41
42
 
43
+ const inventory = buildContextInventory(state, projectRoot);
44
+ if (inventory) sections.push("", inventory);
45
+
42
46
  sections.push(
43
47
  "",
44
48
  "---",
@@ -63,7 +67,10 @@ async function handleClearRestore(sessionId: string, projectRoot: string): Promi
63
67
  const ctx = hasPlan[0]!;
64
68
 
65
69
  bindSession(ctx.id, sessionId, projectRoot);
66
- updateMode(ctx.id, "active", projectRoot, { plan_consumed: true });
70
+ updateMode(ctx.id, "active", projectRoot, {
71
+ plan_consumed: true,
72
+ plan_hash_consumed: ctx.plan_hash // Track which hash was consumed
73
+ });
67
74
 
68
75
  logInfo("session_start", `Clear restore: ${ctx.id} has_plan → active (plan_consumed=true)`);
69
76
 
@@ -79,6 +86,9 @@ async function handleClearRestore(sessionId: string, projectRoot: string): Promi
79
86
  const restore = buildRestoreSections(ctx, projectRoot, false);
80
87
  if (restore) sections.push(restore);
81
88
 
89
+ const inventory = buildContextInventory(ctx, projectRoot);
90
+ if (inventory) sections.push("", inventory);
91
+
82
92
  sections.push(
83
93
  "",
84
94
  "---",
@@ -100,7 +110,9 @@ async function handleClearRestore(sessionId: string, projectRoot: string): Promi
100
110
  logInfo("session_start", `Clear restore: ${ctx.id} has_handoff → active (handoff_consumed=true)`);
101
111
 
102
112
  const handoffContent = formatHandoffContinuation(ctx, projectRoot);
103
- emitContext(handoffContent);
113
+ const handoffInventory = buildContextInventory(ctx, projectRoot);
114
+ const combined = handoffInventory ? handoffContent + "\n\n" + handoffInventory : handoffContent;
115
+ emitContext(combined);
104
116
  return;
105
117
  }
106
118
 
@@ -124,18 +136,15 @@ async function main(): Promise<void> {
124
136
  logDiagnostic("session_start", "entry", `source=${source}, session=${sessionId}`);
125
137
 
126
138
  switch (source) {
127
- case "clear": {
128
- await handleClearRestore(sessionId, projectRoot);
129
- break;
130
- }
131
- case "compact": {
139
+ case "compact":
132
140
  handleCompactRestore(sessionId, projectRoot);
133
- break;
134
- }
135
- default: {
141
+ break;
142
+ case "clear":
143
+ await handleClearRestore(sessionId, projectRoot);
144
+ break;
145
+ default:
136
146
  logDebug("session_start", `Unhandled source: ${source}`);
137
- break;
138
- }
147
+ break;
139
148
  }
140
149
  }
141
150
 
@@ -6,14 +6,15 @@
6
6
  * Uses emitContext() for output — context text is passed via hookSpecificOutput JSON.
7
7
  * Catches BlockRequest and uses emitBlock() to block the prompt.
8
8
  */
9
- import { getProjectRoot } from "../lib-ts/base/constants.js";
10
9
  import {
11
10
  loadHookInput, runHookAsync, logDebug, logInfo, logWarn, logBlocking, logDiagnostic, hookLog, emitContext, emitBlock,
12
11
  } from "../lib-ts/base/hook-utils.js";
13
- import { determineContext, BlockRequest } from "../lib-ts/context/context-selector.js";
12
+ import { getProjectRoot } from "../lib-ts/base/constants.js";
14
13
  import {
15
14
  getContextBySessionId, bindSession, maybeActivate, saveState,
16
15
  } from "../lib-ts/context/context-store.js";
16
+ import { determineContext, BlockRequest } from "../lib-ts/context/context-selector.js";
17
+ import { buildContextInventory } from "../lib-ts/context/context-formatter.js";
17
18
 
18
19
  async function asyncMain(): Promise<void> {
19
20
  const payload = loadHookInput();
@@ -38,8 +39,8 @@ async function asyncMain(): Promise<void> {
38
39
  // Returning user — context already bound (stderr: false to avoid "hook error" display)
39
40
  try {
40
41
  maybeActivate(existingCtx.id, permissionMode, projectRoot, "user_prompt_submit");
41
- } catch (error) {
42
- hookLog("warn", "user_prompt_submit", `maybeActivate failed (non-critical): ${error}`, { stderr: false });
42
+ } catch (e) {
43
+ hookLog("warn", "user_prompt_submit", `maybeActivate failed (non-critical): ${e}`, { stderr: false });
43
44
  }
44
45
  hookLog("debug", "user_prompt_submit", `Session bound to ${existingCtx.id}`, { stderr: false });
45
46
  } else if (prompt) {
@@ -64,12 +65,23 @@ async function asyncMain(): Promise<void> {
64
65
  if (outputText) {
65
66
  outputs.push(outputText);
66
67
  }
67
- } catch (error) {
68
- if (error instanceof BlockRequest) {
69
- emitBlock((error as Error).message);
68
+
69
+ // Append context folder inventory
70
+ try {
71
+ const boundState = getContextBySessionId(sessionId, projectRoot);
72
+ if (boundState) {
73
+ const inventory = buildContextInventory(boundState, projectRoot);
74
+ if (inventory) outputs.push(inventory);
75
+ }
76
+ } catch (e) {
77
+ logWarn("user_prompt_submit", `Inventory failed (non-critical): ${e}`);
78
+ }
79
+ } catch (e) {
80
+ if (e instanceof BlockRequest) {
81
+ emitBlock((e as Error).message);
70
82
  return;
71
83
  }
72
- throw error; // Re-throw unexpected errors
84
+ throw e; // Re-throw unexpected errors
73
85
  }
74
86
  }
75
87
 
@@ -5,11 +5,10 @@
5
5
  */
6
6
 
7
7
  import { execFileSync } from "node:child_process";
8
-
9
8
  import { logDebug, logWarn } from "./logger.js";
10
9
  import { STOP_WORDS } from "./stop-words.js";
11
- import { cleanTextForSlug } from "./utils.js";
12
10
  import type { InferenceResult } from "../types.js";
11
+ import { execFileAsync, getInternalSubprocessEnv, shellQuoteWin } from "./subprocess-utils.js";
13
12
 
14
13
  // Model configurations §6.1
15
14
  const MODELS: Record<string, string> = {
@@ -35,8 +34,8 @@ export function inference(
35
34
  timeout?: number,
36
35
  ): InferenceResult {
37
36
  const startTime = Date.now();
38
- const model = MODELS[level] ?? MODELS["fast"] ?? "claude-3-haiku-20240307";
39
- const timeoutSec = timeout ?? TIMEOUTS[level] ?? 15;
37
+ const model = MODELS[level] ?? MODELS.fast;
38
+ const timeoutSec = timeout ?? TIMEOUTS[level] ?? TIMEOUTS.fast;
40
39
  const fullPrompt = `${systemPrompt}\n\n${userPrompt}`;
41
40
 
42
41
  // Remove ANTHROPIC_API_KEY to force subscription auth
@@ -45,6 +44,8 @@ export function inference(
45
44
 
46
45
  try {
47
46
  const isWin = process.platform === "win32";
47
+ let stdout: string;
48
+
48
49
  // On Windows with shell:true, Node.js sets windowsVerbatimArguments —
49
50
  // args are joined with spaces, NOT individually quoted. We must manually
50
51
  // wrap multi-word/special-char args in "..." for cmd.exe parsing.
@@ -52,10 +53,10 @@ export function inference(
52
53
  const empty = isWin ? '""' : "";
53
54
  let promptArg = fullPrompt;
54
55
  if (isWin) {
55
- promptArg = '"' + fullPrompt.replaceAll(/\r?\n/g, " ").replaceAll('"', '""') + '"';
56
+ promptArg = '"' + fullPrompt.replace(/\r?\n/g, " ").replace(/"/g, '""') + '"';
56
57
  }
57
58
 
58
- const stdout = execFileSync(
59
+ stdout = execFileSync(
59
60
  "claude",
60
61
  ["--model", model, "--print", "--setting-sources", empty, "-p", promptArg],
61
62
  {
@@ -73,10 +74,10 @@ export function inference(
73
74
  output: stdout.trim(),
74
75
  latency_ms: latencyMs,
75
76
  };
76
- } catch (error: any) {
77
+ } catch (e: any) {
77
78
  const latencyMs = Date.now() - startTime;
78
79
 
79
- if (error.code === "ETIMEDOUT" || error.killed) {
80
+ if (e.code === "ETIMEDOUT" || e.killed) {
80
81
  return {
81
82
  success: false,
82
83
  output: "",
@@ -85,7 +86,7 @@ export function inference(
85
86
  };
86
87
  }
87
88
 
88
- if (error.code === "ENOENT") {
89
+ if (e.code === "ENOENT") {
89
90
  return {
90
91
  success: false,
91
92
  output: "",
@@ -95,11 +96,11 @@ export function inference(
95
96
  }
96
97
 
97
98
  // Non-zero exit code
98
- if (error.status !== undefined && error.status !== 0) {
99
+ if (e.status !== undefined && e.status !== 0) {
99
100
  return {
100
101
  success: false,
101
- output: (error.stdout ?? "").toString().trim(),
102
- error: (error.stderr ?? "").toString().trim() || `Exit code: ${error.status}`,
102
+ output: (e.stdout ?? "").toString().trim(),
103
+ error: (e.stderr ?? "").toString().trim() || `Exit code: ${e.status}`,
103
104
  latency_ms: latencyMs,
104
105
  };
105
106
  }
@@ -107,7 +108,7 @@ export function inference(
107
108
  return {
108
109
  success: false,
109
110
  output: "",
110
- error: String(error),
111
+ error: String(e),
111
112
  latency_ms: latencyMs,
112
113
  };
113
114
  }
@@ -133,13 +134,13 @@ Output ONLY the keywords separated by spaces, nothing else.`;
133
134
  export function generateSemanticSummary(
134
135
  prompt: string,
135
136
  timeout = 15,
136
- ): null | string {
137
+ ): string | null {
137
138
  const result = inference(CONTEXT_ID_SYSTEM_PROMPT, prompt, "standard", timeout);
138
139
 
139
140
  if (!result.success || !result.output) return null;
140
141
 
141
142
  let summary = result.output.trim();
142
- summary = summary.replaceAll(/^["']+|["']+$/g, "");
143
+ summary = summary.replace(/^["']+|["']+$/g, "");
143
144
  summary = summary.replace(/[.!?]+$/, "");
144
145
 
145
146
  // Filter stop words
@@ -194,7 +195,7 @@ Respond with ONLY a JSON object: {"slug": "your 8-12 word phrase here"}`;
194
195
  export function generateContextIdSlug(
195
196
  prompt: string,
196
197
  timeout = 3,
197
- ): null | string {
198
+ ): string | null {
198
199
  const truncated = prompt.slice(0, 500);
199
200
 
200
201
  const result = inference(CONTEXT_ID_SLUG_PROMPT, truncated, "fast", timeout);
@@ -207,7 +208,7 @@ export function generateContextIdSlug(
207
208
  const raw = result.output.trim();
208
209
 
209
210
  // Parse JSON response, fall back to raw text
210
- let slug: null | string = null;
211
+ let slug: string | null = null;
211
212
  try {
212
213
  const parsed = JSON.parse(raw);
213
214
  if (parsed && typeof parsed === "object" && "slug" in parsed) {
@@ -220,11 +221,11 @@ export function generateContextIdSlug(
220
221
  if (!slug) slug = raw;
221
222
 
222
223
  // Clean up
223
- slug = slug.replaceAll(/^["'`]+|["'`]+$/g, "");
224
+ slug = slug.replace(/^["'`]+|["'`]+$/g, "");
224
225
  slug = slug.replace(/[.!?]+$/, "");
225
- slug = slug.replaceAll('-', " ");
226
- slug = slug.replaceAll(/[^a-zA-Z0-9 ]/g, "");
227
- slug = slug.replaceAll(/\s+/g, " ").trim();
226
+ slug = slug.replace(/-/g, " ");
227
+ slug = slug.replace(/[^a-zA-Z0-9 ]/g, "");
228
+ slug = slug.replace(/\s+/g, " ").trim();
228
229
 
229
230
  const words = slug.split(" ");
230
231
 
@@ -239,13 +240,61 @@ export function generateContextIdSlug(
239
240
  return resultSlug;
240
241
  }
241
242
 
243
+ /**
244
+ * Async version of inference() that does NOT block the event loop.
245
+ * Use for parallel AI calls (e.g., Stage 3 parallel summarizers).
246
+ * Uses execFileAsync and getInternalSubprocessEnv for proper subprocess isolation.
247
+ */
248
+ export async function inferenceAsync(
249
+ systemPrompt: string,
250
+ userPrompt: string,
251
+ level = "fast",
252
+ timeout?: number,
253
+ ): Promise<InferenceResult> {
254
+ const startTime = Date.now();
255
+ const model = (level in MODELS ? MODELS[level] : undefined) ?? MODELS.fast;
256
+ const timeoutSec = timeout ?? (level in TIMEOUTS ? TIMEOUTS[level] : undefined) ?? TIMEOUTS.fast;
257
+ const timeoutMs = timeoutSec * 1000;
258
+ const fullPrompt = `${systemPrompt}\n\n${userPrompt}`;
259
+
260
+ const env = getInternalSubprocessEnv();
261
+ delete env.ANTHROPIC_API_KEY;
262
+
263
+ const isWin = process.platform === "win32";
264
+ const empty = isWin ? '""' : "";
265
+ const promptArg = isWin
266
+ ? shellQuoteWin(fullPrompt.replace(/\r?\n/g, " "))
267
+ : fullPrompt;
268
+
269
+ const result = await execFileAsync(
270
+ "claude",
271
+ ["--model", model, "--print", "--setting-sources", empty, "-p", promptArg],
272
+ { timeout: timeoutMs, env, shell: isWin },
273
+ );
274
+
275
+ const latencyMs = Date.now() - startTime;
276
+
277
+ if (result.killed) {
278
+ return { success: false, output: "", error: `Timeout after ${timeoutSec}s`, latency_ms: latencyMs };
279
+ }
280
+ if (result.exitCode !== 0) {
281
+ return {
282
+ success: false,
283
+ output: result.stdout.trim(),
284
+ error: result.stderr.trim() || `Exit code: ${result.exitCode}`,
285
+ latency_ms: latencyMs,
286
+ };
287
+ }
288
+ return { success: true, output: result.stdout.trim(), latency_ms: latencyMs };
289
+ }
290
+
242
291
  /**
243
292
  * Filter stop words from text.
244
293
  * See SPEC.md §6.4
245
294
  */
246
295
  function filterStopWords(text: string): string {
247
- const cleaned = cleanTextForSlug(text);
248
- return cleaned
296
+ return text
297
+ .toLowerCase()
249
298
  .split(/\s+/)
250
299
  .filter((w) => !STOP_WORDS.has(w) && w.length > 1)
251
300
  .join(" ");
@@ -6,9 +6,8 @@
6
6
 
7
7
  import * as fs from "node:fs";
8
8
  import * as path from "node:path";
9
-
10
- import { atomicWrite } from "./atomic-write.js";
11
9
  import { getContextDir } from "./constants.js";
10
+ import { atomicWrite } from "./atomic-write.js";
12
11
  import { logWarn } from "./logger.js";
13
12
  import type { ContextState, Mode } from "../types.js";
14
13
 
@@ -31,7 +30,6 @@ export function toDict(state: ContextState): Record<string, unknown> {
31
30
  result[key] = value;
32
31
  }
33
32
  }
34
-
35
33
  return result;
36
34
  }
37
35
 
@@ -54,11 +52,11 @@ export function readStateJson(
54
52
  if (!fs.existsSync(sp)) return null;
55
53
 
56
54
  try {
57
- const raw = fs.readFileSync(sp, "utf8");
55
+ const raw = fs.readFileSync(sp, "utf-8");
58
56
  const data = JSON.parse(raw) as Record<string, any>;
59
57
  return dictToState(data);
60
- } catch (error: any) {
61
- logWarn("state_io", `Failed to read state.json for '${contextId}': ${error}`);
58
+ } catch (e: any) {
59
+ logWarn("state_io", `Failed to read state.json for '${contextId}': ${e}`);
62
60
  return null;
63
61
  }
64
62
  }
@@ -71,7 +69,7 @@ export function writeStateJson(
71
69
  contextId: string,
72
70
  state: ContextState,
73
71
  projectRoot?: string,
74
- ): [boolean, null | string] {
72
+ ): [boolean, string | null] {
75
73
  const sp = statePath(contextId, projectRoot);
76
74
  const dir = path.dirname(sp);
77
75
  fs.mkdirSync(dir, { recursive: true });
@@ -112,6 +110,13 @@ export function dictToState(data: Record<string, any>): ContextState {
112
110
  if ("handoff_path" in data) state.handoff_path = data.handoff_path;
113
111
  if ("last_session" in data) state.last_session = data.last_session;
114
112
 
113
+ // Migration: plan_hash_consumed (added in multi-plan context fix)
114
+ if ("plan_hash_consumed" in data) {
115
+ state.plan_hash_consumed = data.plan_hash_consumed;
116
+ } else {
117
+ state.plan_hash_consumed = null; // Default for old contexts
118
+ }
119
+
115
120
  // Preserve method-specific extension data (e.g., cc_native) that isn't
116
121
  // part of the core ContextState interface. Without this, round-trip
117
122
  // read→write cycles silently drop extension fields.