aiwcli 0.10.1 → 0.10.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 (110) hide show
  1. package/dist/commands/clean.js +1 -0
  2. package/dist/commands/clear.d.ts +19 -2
  3. package/dist/commands/clear.js +351 -160
  4. package/dist/commands/init/index.d.ts +1 -17
  5. package/dist/commands/init/index.js +19 -104
  6. package/dist/lib/gitignore-manager.d.ts +9 -0
  7. package/dist/lib/gitignore-manager.js +121 -0
  8. package/dist/lib/template-installer.d.ts +7 -12
  9. package/dist/lib/template-installer.js +69 -193
  10. package/dist/lib/template-settings-reconstructor.d.ts +35 -0
  11. package/dist/lib/template-settings-reconstructor.js +130 -0
  12. package/dist/templates/_shared/hooks/__pycache__/archive_plan.cpython-313.pyc +0 -0
  13. package/dist/templates/_shared/hooks/__pycache__/session_end.cpython-313.pyc +0 -0
  14. package/dist/templates/_shared/hooks/archive_plan.py +10 -2
  15. package/dist/templates/_shared/hooks/session_end.py +37 -29
  16. package/dist/templates/_shared/lib/base/__pycache__/hook_utils.cpython-313.pyc +0 -0
  17. package/dist/templates/_shared/lib/base/__pycache__/inference.cpython-313.pyc +0 -0
  18. package/dist/templates/_shared/lib/base/__pycache__/logger.cpython-313.pyc +0 -0
  19. package/dist/templates/_shared/lib/base/__pycache__/stop_words.cpython-313.pyc +0 -0
  20. package/dist/templates/_shared/lib/base/__pycache__/utils.cpython-313.pyc +0 -0
  21. package/dist/templates/_shared/lib/base/hook_utils.py +8 -10
  22. package/dist/templates/_shared/lib/base/inference.py +51 -62
  23. package/dist/templates/_shared/lib/base/logger.py +35 -21
  24. package/dist/templates/_shared/lib/base/stop_words.py +8 -0
  25. package/dist/templates/_shared/lib/base/utils.py +29 -8
  26. package/dist/templates/_shared/lib/context/__pycache__/plan_manager.cpython-313.pyc +0 -0
  27. package/dist/templates/_shared/lib/context/plan_manager.py +101 -2
  28. package/dist/templates/_shared/lib-ts/base/atomic-write.ts +138 -0
  29. package/dist/templates/_shared/lib-ts/base/constants.ts +299 -0
  30. package/dist/templates/_shared/lib-ts/base/git-state.ts +58 -0
  31. package/dist/templates/_shared/lib-ts/base/hook-utils.ts +360 -0
  32. package/dist/templates/_shared/lib-ts/base/inference.ts +245 -0
  33. package/dist/templates/_shared/lib-ts/base/logger.ts +234 -0
  34. package/dist/templates/_shared/lib-ts/base/state-io.ts +114 -0
  35. package/dist/templates/_shared/lib-ts/base/stop-words.ts +184 -0
  36. package/dist/templates/_shared/lib-ts/base/subprocess-utils.ts +23 -0
  37. package/dist/templates/_shared/lib-ts/base/utils.ts +184 -0
  38. package/dist/templates/_shared/lib-ts/context/context-formatter.ts +432 -0
  39. package/dist/templates/_shared/lib-ts/context/context-selector.ts +497 -0
  40. package/dist/templates/_shared/lib-ts/context/context-store.ts +679 -0
  41. package/dist/templates/_shared/lib-ts/context/plan-manager.ts +292 -0
  42. package/dist/templates/_shared/lib-ts/context/task-tracker.ts +181 -0
  43. package/dist/templates/_shared/lib-ts/handoff/document-generator.ts +215 -0
  44. package/dist/templates/_shared/lib-ts/package.json +21 -0
  45. package/dist/templates/_shared/lib-ts/templates/formatters.ts +102 -0
  46. package/dist/templates/_shared/lib-ts/templates/plan-context.ts +65 -0
  47. package/dist/templates/_shared/lib-ts/tsconfig.json +13 -0
  48. package/dist/templates/_shared/lib-ts/types.ts +151 -0
  49. package/dist/templates/_shared/scripts/__pycache__/status_line.cpython-313.pyc +0 -0
  50. package/dist/templates/_shared/scripts/save_handoff.ts +359 -0
  51. package/dist/templates/_shared/scripts/status_line.py +17 -2
  52. package/dist/templates/cc-native/_cc-native/agents/ARCH-EVOLUTION.md +63 -0
  53. package/dist/templates/cc-native/_cc-native/agents/ARCH-PATTERNS.md +62 -0
  54. package/dist/templates/cc-native/_cc-native/agents/ARCH-STRUCTURE.md +63 -0
  55. package/dist/templates/cc-native/_cc-native/agents/{ASSUMPTION-CHAIN-TRACER.md → ASSUMPTION-TRACER.md} +6 -10
  56. package/dist/templates/cc-native/_cc-native/agents/CLARITY-AUDITOR.md +6 -10
  57. package/dist/templates/cc-native/_cc-native/agents/CLAUDE.md +74 -1
  58. package/dist/templates/cc-native/_cc-native/agents/COMPLETENESS-FEASIBILITY.md +67 -0
  59. package/dist/templates/cc-native/_cc-native/agents/COMPLETENESS-GAPS.md +71 -0
  60. package/dist/templates/cc-native/_cc-native/agents/COMPLETENESS-ORDERING.md +63 -0
  61. package/dist/templates/cc-native/_cc-native/agents/CONSTRAINT-VALIDATOR.md +73 -0
  62. package/dist/templates/cc-native/_cc-native/agents/DESIGN-ADR-VALIDATOR.md +62 -0
  63. package/dist/templates/cc-native/_cc-native/agents/DESIGN-SCALE-MATCHER.md +65 -0
  64. package/dist/templates/cc-native/_cc-native/agents/DEVILS-ADVOCATE.md +6 -9
  65. package/dist/templates/cc-native/_cc-native/agents/DOCUMENTATION-PHILOSOPHY.md +87 -0
  66. package/dist/templates/cc-native/_cc-native/agents/HANDOFF-READINESS.md +5 -9
  67. package/dist/templates/cc-native/_cc-native/agents/{HIDDEN-COMPLEXITY-DETECTOR.md → HIDDEN-COMPLEXITY.md} +6 -10
  68. package/dist/templates/cc-native/_cc-native/agents/INCREMENTAL-DELIVERY.md +67 -0
  69. package/dist/templates/cc-native/_cc-native/agents/PLAN-ORCHESTRATOR.md +91 -18
  70. package/dist/templates/cc-native/_cc-native/agents/RISK-DEPENDENCY.md +63 -0
  71. package/dist/templates/cc-native/_cc-native/agents/RISK-FMEA.md +67 -0
  72. package/dist/templates/cc-native/_cc-native/agents/RISK-PREMORTEM.md +72 -0
  73. package/dist/templates/cc-native/_cc-native/agents/RISK-REVERSIBILITY.md +75 -0
  74. package/dist/templates/cc-native/_cc-native/agents/SCOPE-BOUNDARY.md +78 -0
  75. package/dist/templates/cc-native/_cc-native/agents/SIMPLICITY-GUARDIAN.md +5 -9
  76. package/dist/templates/cc-native/_cc-native/agents/SKEPTIC.md +16 -12
  77. package/dist/templates/cc-native/_cc-native/agents/TESTDRIVEN-BEHAVIOR-AUDITOR.md +62 -0
  78. package/dist/templates/cc-native/_cc-native/agents/TESTDRIVEN-CHARACTERIZATION.md +72 -0
  79. package/dist/templates/cc-native/_cc-native/agents/TESTDRIVEN-FIRST-VALIDATOR.md +62 -0
  80. package/dist/templates/cc-native/_cc-native/agents/TESTDRIVEN-PYRAMID-ANALYZER.md +62 -0
  81. package/dist/templates/cc-native/_cc-native/agents/TRADEOFF-COSTS.md +68 -0
  82. package/dist/templates/cc-native/_cc-native/agents/TRADEOFF-STAKEHOLDERS.md +66 -0
  83. package/dist/templates/cc-native/_cc-native/agents/VERIFY-COVERAGE.md +75 -0
  84. package/dist/templates/cc-native/_cc-native/agents/VERIFY-STRENGTH.md +70 -0
  85. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-plan-review.cpython-313.pyc +0 -0
  86. package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.py +125 -40
  87. package/dist/templates/cc-native/_cc-native/lib/__pycache__/utils.cpython-313.pyc +0 -0
  88. package/dist/templates/cc-native/_cc-native/lib/utils.py +57 -13
  89. package/dist/templates/cc-native/_cc-native/plan-review.config.json +11 -7
  90. package/oclif.manifest.json +17 -2
  91. package/package.json +1 -1
  92. package/dist/lib/template-merger.d.ts +0 -47
  93. package/dist/lib/template-merger.js +0 -162
  94. package/dist/templates/cc-native/_cc-native/agents/ACCESSIBILITY-TESTER.md +0 -79
  95. package/dist/templates/cc-native/_cc-native/agents/ARCHITECT-REVIEWER.md +0 -48
  96. package/dist/templates/cc-native/_cc-native/agents/CODE-REVIEWER.md +0 -70
  97. package/dist/templates/cc-native/_cc-native/agents/COMPLETENESS-CHECKER.md +0 -59
  98. package/dist/templates/cc-native/_cc-native/agents/CONTEXT-EXTRACTOR.md +0 -92
  99. package/dist/templates/cc-native/_cc-native/agents/DOCUMENTATION-REVIEWER.md +0 -51
  100. package/dist/templates/cc-native/_cc-native/agents/FEASIBILITY-ANALYST.md +0 -57
  101. package/dist/templates/cc-native/_cc-native/agents/FRESH-PERSPECTIVE.md +0 -54
  102. package/dist/templates/cc-native/_cc-native/agents/INCENTIVE-MAPPER.md +0 -61
  103. package/dist/templates/cc-native/_cc-native/agents/PENETRATION-TESTER.md +0 -79
  104. package/dist/templates/cc-native/_cc-native/agents/PERFORMANCE-ENGINEER.md +0 -75
  105. package/dist/templates/cc-native/_cc-native/agents/PRECEDENT-FINDER.md +0 -70
  106. package/dist/templates/cc-native/_cc-native/agents/REVERSIBILITY-ANALYST.md +0 -61
  107. package/dist/templates/cc-native/_cc-native/agents/RISK-ASSESSOR.md +0 -58
  108. package/dist/templates/cc-native/_cc-native/agents/SECOND-ORDER-ANALYST.md +0 -61
  109. package/dist/templates/cc-native/_cc-native/agents/STAKEHOLDER-ADVOCATE.md +0 -55
  110. package/dist/templates/cc-native/_cc-native/agents/TRADE-OFF-ILLUMINATOR.md +0 -204
@@ -2,98 +2,6 @@ import { promises as fs } from 'node:fs';
2
2
  import { dirname, join } from 'node:path';
3
3
  import { IdePathResolver } from './ide-path-resolver.js';
4
4
  import { pathExists } from './paths.js';
5
- import { mergeTemplateContent } from './template-merger.js';
6
- /**
7
- * Deep merge two settings objects, combining hook arrays.
8
- * Used to merge _shared/settings.json into .claude/settings.json
9
- */
10
- function deepMergeSettings(target, source) {
11
- const result = { ...target };
12
- for (const key of Object.keys(source)) {
13
- // Skip comment fields
14
- if (key.startsWith('$') || key.startsWith('_')) {
15
- continue;
16
- }
17
- const sourceValue = source[key];
18
- const targetValue = result[key];
19
- if (key === 'hooks' && typeof sourceValue === 'object' && sourceValue !== null) {
20
- // Special handling for hooks - merge by event type
21
- result[key] = mergeHooks(targetValue || {}, sourceValue);
22
- }
23
- else if (Array.isArray(sourceValue) && Array.isArray(targetValue)) {
24
- // Concatenate arrays
25
- result[key] = [...targetValue, ...sourceValue];
26
- }
27
- else if (typeof sourceValue === 'object' && sourceValue !== null && typeof targetValue === 'object' && targetValue !== null) {
28
- // Recursively merge objects
29
- result[key] = deepMergeSettings(targetValue, sourceValue);
30
- }
31
- else {
32
- // Override with source value
33
- result[key] = sourceValue;
34
- }
35
- }
36
- return result;
37
- }
38
- /**
39
- * Merge hook configurations, combining arrays for each event type.
40
- */
41
- function mergeHooks(target, source) {
42
- const result = { ...target };
43
- for (const eventType of Object.keys(source)) {
44
- const targetHooks = result[eventType];
45
- const sourceHooks = source[eventType];
46
- if (targetHooks && sourceHooks) {
47
- // Append source hooks to existing event type
48
- result[eventType] = [...targetHooks, ...sourceHooks];
49
- }
50
- else if (sourceHooks) {
51
- // New event type
52
- result[eventType] = sourceHooks;
53
- }
54
- }
55
- return result;
56
- }
57
- /**
58
- * Merge settings from a source settings.json file into the IDE settings file.
59
- * Reads from the provided source path and merges into .claude/settings.json at project root.
60
- *
61
- * @param targetDir - Project root directory
62
- * @param sourceSettingsPath - Absolute path to source settings.json file
63
- * @returns true if merge successful, false otherwise
64
- */
65
- async function mergeSharedSettingsFromSource(targetDir, sourceSettingsPath) {
66
- const resolver = new IdePathResolver(targetDir);
67
- const ideSettingsPath = resolver.getClaudeSettings();
68
- // Check if source settings exists
69
- if (!(await pathExists(sourceSettingsPath))) {
70
- return false;
71
- }
72
- try {
73
- // Read source settings
74
- const sourceContent = await fs.readFile(sourceSettingsPath, 'utf8');
75
- const sourceSettings = JSON.parse(sourceContent);
76
- // Read IDE settings (create empty object if doesn't exist)
77
- let ideSettings = {};
78
- if (await pathExists(ideSettingsPath)) {
79
- const ideContent = await fs.readFile(ideSettingsPath, 'utf8');
80
- ideSettings = JSON.parse(ideContent);
81
- }
82
- else {
83
- // Create .claude directory if it doesn't exist
84
- await fs.mkdir(dirname(ideSettingsPath), { recursive: true });
85
- }
86
- // Merge source settings into IDE settings
87
- const mergedSettings = deepMergeSettings(ideSettings, sourceSettings);
88
- // Write merged settings back
89
- await fs.writeFile(ideSettingsPath, JSON.stringify(mergedSettings, null, 4) + '\n', 'utf8');
90
- return true;
91
- }
92
- catch {
93
- // Silently fail on parse/write errors
94
- return false;
95
- }
96
- }
97
5
  /**
98
6
  * Check template installation status for a method.
99
7
  * Returns which items exist and which are missing.
@@ -186,32 +94,6 @@ export function shouldExclude(name) {
186
94
  return pattern.test(name);
187
95
  });
188
96
  }
189
- /**
190
- * Merge source directory into destination, skipping existing files.
191
- * Unlike copyDir, this preserves existing files in destination.
192
- *
193
- * @param src - Source directory path
194
- * @param dest - Destination directory path
195
- */
196
- async function mergeDirectory(src, dest) {
197
- await fs.mkdir(dest, { recursive: true });
198
- const entries = await fs.readdir(src, { withFileTypes: true });
199
- for (const entry of entries) {
200
- if (shouldExclude(entry.name))
201
- continue;
202
- const srcPath = join(src, entry.name);
203
- const destPath = join(dest, entry.name);
204
- if (entry.isDirectory()) {
205
- await mergeDirectory(srcPath, destPath);
206
- }
207
- else {
208
- // Skip if file already exists
209
- if (!(await pathExists(destPath))) {
210
- await fs.copyFile(srcPath, destPath);
211
- }
212
- }
213
- }
214
- }
215
97
  /**
216
98
  * Copy directory recursively with proper error handling.
217
99
  * Excludes test files, cache directories, and output folders.
@@ -249,20 +131,45 @@ export async function copyDir(src, dest, excludeIdeFolders = false) {
249
131
  });
250
132
  await Promise.all(operations);
251
133
  }
134
+ /**
135
+ * Merge source directory into destination, skipping existing files.
136
+ * Unlike copyDir, this preserves existing files in destination.
137
+ *
138
+ * @param src - Source directory path
139
+ * @param dest - Destination directory path
140
+ */
141
+ async function mergeDirectory(src, dest) {
142
+ await fs.mkdir(dest, { recursive: true });
143
+ const entries = await fs.readdir(src, { withFileTypes: true });
144
+ const ops = entries
145
+ .filter((entry) => !shouldExclude(entry.name))
146
+ .map(async (entry) => {
147
+ const srcPath = join(src, entry.name);
148
+ const destPath = join(dest, entry.name);
149
+ if (entry.isDirectory()) {
150
+ await mergeDirectory(srcPath, destPath);
151
+ }
152
+ else if (!(await pathExists(destPath))) {
153
+ await fs.copyFile(srcPath, destPath);
154
+ }
155
+ });
156
+ await Promise.all(ops);
157
+ }
252
158
  /**
253
159
  * Install template with IDE-specific folder selection.
254
- * Supports selective installation - only installs items that don't already exist.
255
160
  *
256
161
  * Template structure:
257
- * - Non-dot folders (e.g., _bmad/, GSR/) are installed if not already present
258
- * - Dot folders (e.g., .claude/, .windsurf/) are installed only if matching IDE flag and not already present
162
+ * - Non-dot folders (e.g., _bmad/, GSR/) .aiwcli/ (always overwritten)
163
+ * - _shared/ .aiwcli/_shared/ (always overwritten)
164
+ * - IDE dot folders (e.g., .claude/) → decomposed into method-owned subdirs
165
+ *
166
+ * Settings reconstruction is handled separately by the caller via reconstructIdeSettings().
259
167
  *
260
168
  * @param config - Installation configuration
261
- * @param skipExisting - If true, skip items that already exist (default: true for regeneration support)
262
- * @returns Installation result with list of installed and skipped folders
169
+ * @returns Installation result with list of installed folders
263
170
  * @throws Error if template doesn't exist or requested IDE folder not found
264
171
  */
265
- export async function installTemplate(config, skipExisting = true) {
172
+ export async function installTemplate(config) {
266
173
  const { templateName, targetDir, ides, templatePath } = config;
267
174
  // Verify template exists
268
175
  try {
@@ -295,108 +202,77 @@ export async function installTemplate(config, skipExisting = true) {
295
202
  `Available: ${availableIdes.join(', ')}`);
296
203
  }
297
204
  const installedFolders = [];
298
- const skippedFolders = [];
299
- const mergedFolders = [];
300
- let mergedFileCount = 0;
301
205
  // Create .aiwcli container folder for method-specific files
302
206
  const resolver = new IdePathResolver(targetDir);
303
207
  const containerDir = resolver.getAiwcliContainer();
304
208
  await fs.mkdir(containerDir, { recursive: true });
305
- // Install non-dot folders into .aiwcli/ container (skip if already exist and skipExisting is true)
209
+ // Install non-dot folders into .aiwcli/ container (always overwrite)
306
210
  const nonDotInstalls = nonDotFolders.map(async (folder) => {
307
211
  const srcPath = join(templatePath, folder);
308
- // Destination is inside .aiwcli/ container
309
212
  const destPath = join(containerDir, folder);
310
- if (skipExisting && (await pathExists(destPath))) {
311
- return { folder, skipped: true };
312
- }
313
213
  await copyDir(srcPath, destPath);
314
- return { folder, skipped: false };
214
+ return folder;
315
215
  });
316
216
  const nonDotResults = await Promise.all(nonDotInstalls);
317
- for (const result of nonDotResults) {
318
- if (result.skipped) {
319
- skippedFolders.push(result.folder);
320
- }
321
- else {
322
- installedFolders.push(result.folder);
323
- }
324
- }
217
+ installedFolders.push(...nonDotResults);
325
218
  // Install root-level _shared directory (shared across all templates)
326
- // This is at templates/_shared, not inside the specific template directory
327
219
  // Exclude IDE config folders (.claude, .windsurf) - they are used for settings merging only
328
220
  const templatesRoot = dirname(templatePath);
329
221
  const rootSharedSrc = join(templatesRoot, '_shared');
330
222
  const rootSharedDest = join(containerDir, '_shared');
331
223
  if (await pathExists(rootSharedSrc)) {
332
- if (skipExisting && (await pathExists(rootSharedDest))) {
333
- skippedFolders.push('_shared');
334
- }
335
- else {
336
- await copyDir(rootSharedSrc, rootSharedDest, true); // excludeIdeFolders = true
337
- installedFolders.push('_shared');
338
- }
339
- // Merge shared IDE folders (commands, workflows) into project IDE folders
340
- // This handles _shared/.claude/, _shared/.windsurf/, etc.
224
+ await copyDir(rootSharedSrc, rootSharedDest, true); // excludeIdeFolders = true
225
+ installedFolders.push('_shared');
226
+ // Copy shared IDE content (e.g., _shared/.claude/commands/handoff.md)
227
+ // These are non-method-owned files that live in IDE folders
341
228
  const sharedIdeInstalls = ides.map(async (ide) => {
342
229
  const sharedIdeFolder = join(rootSharedSrc, `.${ide}`);
343
230
  if (await pathExists(sharedIdeFolder)) {
344
231
  const destIdeFolder = resolver.getIdeDir(ide);
345
232
  await fs.mkdir(destIdeFolder, { recursive: true });
346
- // Recursively copy, but skip files that already exist
233
+ // Merge shared IDE content, skipping files that already exist
347
234
  await mergeDirectory(sharedIdeFolder, destIdeFolder);
348
235
  }
349
236
  });
350
237
  await Promise.all(sharedIdeInstalls);
351
238
  }
352
- // Install matching IDE folders
353
- // If folder exists, merge content recursively by looking for method name folders
239
+ // Install method-owned IDE content (decomposed approach)
240
+ // Instead of copying entire .claude/ from template, only copy method-namespaced subdirectories
354
241
  const ideInstalls = ides.map(async (ide) => {
355
242
  const folderName = dotFolders.get(ide);
356
- if (folderName) {
357
- const srcPath = join(templatePath, folderName);
358
- const destPath = resolver.getIdeDir(ide);
359
- if (await pathExists(destPath)) {
360
- if (skipExisting) {
361
- // Folder exists - merge template content by finding method-named folders
362
- const mergeResult = await mergeTemplateContent(srcPath, destPath, templateName);
363
- return {
364
- folder: folderName,
365
- skipped: false,
366
- merged: true,
367
- mergedFiles: mergeResult.copiedFiles.length,
368
- };
369
- }
370
- // skipExisting is false, so overwrite
371
- await copyDir(srcPath, destPath);
372
- return { folder: folderName, skipped: false, merged: false, mergedFiles: 0 };
243
+ if (!folderName)
244
+ return null;
245
+ const srcIdePath = join(templatePath, folderName);
246
+ const destIdePath = resolver.getIdeDir(ide);
247
+ await fs.mkdir(destIdePath, { recursive: true });
248
+ // Scan the template IDE folder for subdirectories and copy method-owned content
249
+ const ideEntries = await fs.readdir(srcIdePath, { withFileTypes: true });
250
+ const subdirOps = ideEntries
251
+ .filter((entry) => entry.isDirectory())
252
+ .map(async (entry) => {
253
+ const subdirSrc = join(srcIdePath, entry.name);
254
+ const subdirDest = join(destIdePath, entry.name);
255
+ // Check for method-namespaced child within this subdirectory
256
+ const methodChildSrc = join(subdirSrc, templateName);
257
+ if (await pathExists(methodChildSrc)) {
258
+ // Copy only the method-namespaced subdirectory (overwrite)
259
+ const methodChildDest = join(subdirDest, templateName);
260
+ await copyDir(methodChildSrc, methodChildDest);
373
261
  }
374
- await copyDir(srcPath, destPath);
375
- return { folder: folderName, skipped: false, merged: false, mergedFiles: 0 };
376
- }
377
- return null;
262
+ else {
263
+ // No method-namespaced child copy the entire subdirectory, merging with existing
264
+ await mergeDirectory(subdirSrc, subdirDest);
265
+ }
266
+ });
267
+ await Promise.all(subdirOps);
268
+ return folderName;
378
269
  });
379
270
  const ideResults = (await Promise.all(ideInstalls)).filter((result) => result !== null);
380
- for (const result of ideResults) {
381
- if (result.merged) {
382
- mergedFolders.push(result.folder);
383
- mergedFileCount += result.mergedFiles;
384
- }
385
- else if (result.skipped) {
386
- skippedFolders.push(result.folder);
387
- }
388
- else {
389
- installedFolders.push(result.folder);
390
- }
391
- }
392
- // Settings merging is now handled by the caller via mergeMethodsSettings()
393
- // This allows unified merging of _shared + method-specific settings
271
+ installedFolders.push(...ideResults);
272
+ // Settings reconstruction is handled by the caller via reconstructIdeSettings()
394
273
  return {
395
274
  installedFolders,
396
- skippedFolders,
397
- mergedFolders,
398
- mergedFileCount,
399
- sharedSettingsMerged: false, // Deprecated, kept for backwards compatibility
275
+ sharedSettingsMerged: false,
400
276
  templatePath,
401
277
  };
402
278
  }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Unified reconstruction of shared IDE settings files.
3
+ *
4
+ * Both `aiw init` and `aiw clear` use the same operation: reconstruct shared
5
+ * settings from the union of all active templates. Install adds a template to
6
+ * the list; clear removes one. The reconstruction logic is identical.
7
+ *
8
+ * Template files fall into two categories:
9
+ * 1. **Method-owned** — lives in method-namespaced paths (e.g., `.claude/commands/cc-native/`),
10
+ * owned exclusively by one template. Handled by copy/delete, not by this module.
11
+ * 2. **Shared** — lives in common locations (e.g., `settings.json`), multiple templates
12
+ * contribute. Handled by this module via reconstruction from source.
13
+ *
14
+ * @module lib/template-settings-reconstructor
15
+ */
16
+ /**
17
+ * Reconstruct .claude/settings.json and .windsurf/hooks.json from the union
18
+ * of all specified templates.
19
+ *
20
+ * The function:
21
+ * 1. Starts with empty settings
22
+ * 2. Merges _shared template settings (always included)
23
+ * 3. For each active template, merges its template-source settings
24
+ * 4. Writes the result to the IDE settings file
25
+ *
26
+ * Uses mergeClaudeSettings() from hooks-merger.ts for dedup-aware merging.
27
+ *
28
+ * Install calls: reconstructIdeSettings(targetDir, [...existingTemplates, newTemplate], ides)
29
+ * Clear calls: reconstructIdeSettings(targetDir, existingTemplates.filter(t => t !== removed), ides)
30
+ *
31
+ * @param targetDir - Project root directory
32
+ * @param activeTemplates - Template names to include (e.g., ['cc-native', 'bmad'])
33
+ * @param ides - IDEs to reconstruct for (e.g., ['claude', 'windsurf'])
34
+ */
35
+ export declare function reconstructIdeSettings(targetDir: string, activeTemplates: string[], ides: string[]): Promise<void>;
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Unified reconstruction of shared IDE settings files.
3
+ *
4
+ * Both `aiw init` and `aiw clear` use the same operation: reconstruct shared
5
+ * settings from the union of all active templates. Install adds a template to
6
+ * the list; clear removes one. The reconstruction logic is identical.
7
+ *
8
+ * Template files fall into two categories:
9
+ * 1. **Method-owned** — lives in method-namespaced paths (e.g., `.claude/commands/cc-native/`),
10
+ * owned exclusively by one template. Handled by copy/delete, not by this module.
11
+ * 2. **Shared** — lives in common locations (e.g., `settings.json`), multiple templates
12
+ * contribute. Handled by this module via reconstruction from source.
13
+ *
14
+ * @module lib/template-settings-reconstructor
15
+ */
16
+ import { dirname, join } from 'node:path';
17
+ import { fileURLToPath } from 'node:url';
18
+ import { mergeClaudeSettings } from './hooks-merger.js';
19
+ import { IdePathResolver } from './ide-path-resolver.js';
20
+ import { readClaudeSettings, writeClaudeSettings } from './settings-hierarchy.js';
21
+ import { getTemplatePath } from './template-resolver.js';
22
+ import { getTargetHooksFile, readWindsurfHooks, writeWindsurfHooks } from './windsurf-hooks-hierarchy.js';
23
+ import { mergeWindsurfHooks } from './windsurf-hooks-merger.js';
24
+ /**
25
+ * Get the path to the _shared template directory.
26
+ *
27
+ * @returns Absolute path to the _shared template
28
+ */
29
+ function getSharedTemplatePath() {
30
+ const currentFilePath = fileURLToPath(import.meta.url);
31
+ const currentDir = dirname(currentFilePath);
32
+ return join(currentDir, '..', 'templates', '_shared');
33
+ }
34
+ /**
35
+ * Reconstruct .claude/settings.json and .windsurf/hooks.json from the union
36
+ * of all specified templates.
37
+ *
38
+ * The function:
39
+ * 1. Starts with empty settings
40
+ * 2. Merges _shared template settings (always included)
41
+ * 3. For each active template, merges its template-source settings
42
+ * 4. Writes the result to the IDE settings file
43
+ *
44
+ * Uses mergeClaudeSettings() from hooks-merger.ts for dedup-aware merging.
45
+ *
46
+ * Install calls: reconstructIdeSettings(targetDir, [...existingTemplates, newTemplate], ides)
47
+ * Clear calls: reconstructIdeSettings(targetDir, existingTemplates.filter(t => t !== removed), ides)
48
+ *
49
+ * @param targetDir - Project root directory
50
+ * @param activeTemplates - Template names to include (e.g., ['cc-native', 'bmad'])
51
+ * @param ides - IDEs to reconstruct for (e.g., ['claude', 'windsurf'])
52
+ */
53
+ export async function reconstructIdeSettings(targetDir, activeTemplates, ides) {
54
+ const sharedTemplatePath = getSharedTemplatePath();
55
+ if (ides.includes('claude')) {
56
+ await reconstructClaudeSettings(targetDir, activeTemplates, sharedTemplatePath);
57
+ }
58
+ if (ides.includes('windsurf')) {
59
+ await reconstructWindsurfHooks(targetDir, activeTemplates, sharedTemplatePath);
60
+ }
61
+ }
62
+ /**
63
+ * Reconstruct .claude/settings.json from scratch using template sources.
64
+ */
65
+ async function reconstructClaudeSettings(targetDir, activeTemplates, sharedTemplatePath) {
66
+ const resolver = new IdePathResolver(targetDir);
67
+ const settingsPath = resolver.getClaudeSettings();
68
+ // Read existing settings to preserve non-template fields (methods tracking, etc.)
69
+ const existingSettings = await readClaudeSettings(settingsPath);
70
+ // Preserve the methods tracking from existing settings
71
+ const methodsTracking = existingSettings?.methods;
72
+ // Start from empty and merge all template settings
73
+ let reconstructed = {};
74
+ // 1. Merge _shared template settings
75
+ const sharedSettingsPath = join(sharedTemplatePath, '.claude', 'settings.json');
76
+ const sharedSettings = await readClaudeSettings(sharedSettingsPath);
77
+ if (sharedSettings) {
78
+ reconstructed = mergeClaudeSettings(reconstructed, sharedSettings);
79
+ }
80
+ // 2. Merge each active template's settings (sequential for deterministic merge order)
81
+ for (const template of activeTemplates) {
82
+ try {
83
+ const templatePath = await getTemplatePath(template); // eslint-disable-line no-await-in-loop
84
+ const templateSettingsPath = join(templatePath, '.claude', 'settings.json');
85
+ const templateSettings = await readClaudeSettings(templateSettingsPath); // eslint-disable-line no-await-in-loop
86
+ if (templateSettings) {
87
+ reconstructed = mergeClaudeSettings(reconstructed, templateSettings);
88
+ }
89
+ }
90
+ catch {
91
+ // Template not found — skip
92
+ }
93
+ }
94
+ // 3. Restore methods tracking
95
+ if (methodsTracking && Object.keys(methodsTracking).length > 0) {
96
+ reconstructed.methods = methodsTracking;
97
+ }
98
+ // 4. Write reconstructed settings
99
+ await writeClaudeSettings(settingsPath, reconstructed);
100
+ }
101
+ /**
102
+ * Reconstruct .windsurf/hooks.json from scratch using template sources.
103
+ */
104
+ async function reconstructWindsurfHooks(targetDir, activeTemplates, sharedTemplatePath) {
105
+ const hooksPath = getTargetHooksFile(targetDir);
106
+ // Start from empty
107
+ let reconstructed = { hooks: {} };
108
+ // 1. Merge _shared template hooks
109
+ const sharedHooksPath = join(sharedTemplatePath, '.windsurf', 'hooks.json');
110
+ const sharedHooks = await readWindsurfHooks(sharedHooksPath);
111
+ if (sharedHooks) {
112
+ reconstructed = mergeWindsurfHooks(reconstructed, sharedHooks);
113
+ }
114
+ // 2. Merge each active template's hooks (sequential for deterministic merge order)
115
+ for (const template of activeTemplates) {
116
+ try {
117
+ const templatePath = await getTemplatePath(template); // eslint-disable-line no-await-in-loop
118
+ const templateHooksPath = join(templatePath, '.windsurf', 'hooks.json');
119
+ const templateHooks = await readWindsurfHooks(templateHooksPath); // eslint-disable-line no-await-in-loop
120
+ if (templateHooks) {
121
+ reconstructed = mergeWindsurfHooks(reconstructed, templateHooks);
122
+ }
123
+ }
124
+ catch {
125
+ // Template not found — skip
126
+ }
127
+ }
128
+ // 3. Write reconstructed hooks
129
+ await writeWindsurfHooks(hooksPath, reconstructed);
130
+ }
@@ -39,7 +39,7 @@ from lib.base.hook_utils import load_hook_input, log_debug, log_info, log_warn,
39
39
  from lib.base.utils import project_dir
40
40
  from lib.base.constants import get_context_dir
41
41
  from lib.context.context_store import get_context_by_session_id
42
- from lib.context.plan_manager import archive_plan, extract_plan_path_from_result
42
+ from lib.context.plan_manager import archive_plan, extract_plan_path_from_result, find_plan_path_in_transcript
43
43
 
44
44
  # Import debug cleanup function from cc-native lib
45
45
  _cc_native_lib = SCRIPT_DIR.parent / "_cc-native" / "lib"
@@ -70,7 +70,15 @@ def _find_plan_path(hook_input: dict, project_root: Path) -> Optional[str]:
70
70
  if not plan_path:
71
71
  plan_path = tool_input.get("plan_path") or tool_input.get("planPath")
72
72
 
73
- # Search standard locations
73
+ # Parse transcript for most recent Write to .claude/plans/
74
+ if not plan_path:
75
+ transcript_path = hook_input.get("transcript_path")
76
+ if transcript_path:
77
+ plan_path = find_plan_path_in_transcript(transcript_path)
78
+ if plan_path:
79
+ log_info("archive_plan", f"Found plan path via transcript: {plan_path}")
80
+
81
+ # Search standard locations (mtime-based fallback)
74
82
  if not plan_path:
75
83
  log_debug("archive_plan", "No plan_path found, searching standard locations...")
76
84
  claude_plans_dir = Path.home() / ".claude" / "plans"
@@ -106,35 +106,43 @@ def main():
106
106
  }
107
107
  state.last_active = now_iso()
108
108
 
109
- # Fallback: assign plan fields if PostToolUse:ExitPlanMode didn't fire.
110
- # When ExitPlanMode triggers /clear, the session terminates before PostToolUse
111
- # hooks can run, so plan_accepted.py never fires. Detect this by checking
112
- # for an archived plan that hasn't been assigned yet.
113
- if not state.plan_hash:
114
- latest_plan_path = find_latest_plan(state.id, project_root)
115
- if latest_plan_path:
116
- try:
117
- content = Path(latest_plan_path).read_text(encoding="utf-8")
118
- normalized = normalize_plan_content(content)
119
- state.plan_hash = hashlib.sha256(normalized.encode("utf-8")).hexdigest()[:12]
120
- state.plan_path = latest_plan_path
121
- state.plan_signature = content[:200]
122
- state.plan_id = generate_plan_id()
123
- state.plan_anchors = extract_plan_anchors(content)
124
- state.plan_consumed = False
125
- log_info("session_end", f"Fallback: assigned archived plan for {state.id} (hash: {state.plan_hash})")
126
- except Exception as e:
127
- log_warn("session_end", f"Fallback plan assignment failed: {e}")
128
-
129
- # If a plan is assigned, not yet consumed, and mode is active, stage it for next session
130
- if state.plan_hash and state.mode == "active" and not state.plan_consumed:
131
- state.mode = "has_plan"
132
- log_info("session_end", f"Staged plan for next session: {state.id} -> has_plan")
133
- elif state.plan_hash and state.mode == "active" and state.plan_consumed:
134
- log_debug("session_end", f"Plan already consumed for {state.id}, not re-staging")
135
- log_diagnostic("session_end", "decide", f"Skipping re-stage for {state.id}",
136
- decision="skip_restage", reasoning="plan_hash exists but plan_consumed=True",
137
- inputs={"plan_hash": state.plan_hash, "plan_consumed": True})
109
+ # Only assign plan fields and stage if NOT in plan mode.
110
+ # If permission_mode == "plan", ExitPlanMode was rejected (user pressed Escape),
111
+ # so we should not stage the archived plan for the next session.
112
+ permission_mode = hook_input.get("permission_mode", "default")
113
+
114
+ if permission_mode != "plan":
115
+ # Fallback: assign plan fields if PostToolUse:ExitPlanMode didn't fire.
116
+ # When ExitPlanMode triggers /clear, the session terminates before PostToolUse
117
+ # hooks can run, so plan_accepted.py never fires. Detect this by checking
118
+ # for an archived plan that hasn't been assigned yet.
119
+ if not state.plan_hash:
120
+ latest_plan_path = find_latest_plan(state.id, project_root)
121
+ if latest_plan_path:
122
+ try:
123
+ content = Path(latest_plan_path).read_text(encoding="utf-8")
124
+ normalized = normalize_plan_content(content)
125
+ state.plan_hash = hashlib.sha256(normalized.encode("utf-8")).hexdigest()[:12]
126
+ state.plan_path = latest_plan_path
127
+ state.plan_signature = content[:200]
128
+ state.plan_id = generate_plan_id()
129
+ state.plan_anchors = extract_plan_anchors(content)
130
+ state.plan_consumed = False
131
+ log_info("session_end", f"Fallback: assigned archived plan for {state.id} (hash: {state.plan_hash})")
132
+ except Exception as e:
133
+ log_warn("session_end", f"Fallback plan assignment failed: {e}")
134
+
135
+ # If a plan is assigned, not yet consumed, and mode is active, stage it for next session
136
+ if state.plan_hash and state.mode == "active" and not state.plan_consumed:
137
+ state.mode = "has_plan"
138
+ log_info("session_end", f"Staged plan for next session: {state.id} -> has_plan")
139
+ elif state.plan_hash and state.mode == "active" and state.plan_consumed:
140
+ log_debug("session_end", f"Plan already consumed for {state.id}, not re-staging")
141
+ log_diagnostic("session_end", "decide", f"Skipping re-stage for {state.id}",
142
+ decision="skip_restage", reasoning="plan_hash exists but plan_consumed=True",
143
+ inputs={"plan_hash": state.plan_hash, "plan_consumed": True})
144
+ else:
145
+ log_info("session_end", f"Plan mode active (rejected), skipping plan staging for {state.id}")
138
146
 
139
147
  # Handoff staging (mirrors plan staging above)
140
148
  # Note: if plan already set has_plan, mode != "active" so handoff check skips (plan takes priority)