aiwcli 0.9.6 → 0.9.7

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 (69) hide show
  1. package/dist/commands/init/index.d.ts +0 -8
  2. package/dist/commands/init/index.js +5 -35
  3. package/dist/lib/index.d.ts +3 -4
  4. package/dist/lib/index.js +3 -5
  5. package/dist/lib/settings-hierarchy.js +5 -16
  6. package/dist/lib/template-installer.d.ts +9 -0
  7. package/dist/lib/template-installer.js +3 -14
  8. package/dist/lib/template-merger.js +1 -12
  9. package/dist/lib/windsurf-hooks-hierarchy.js +2 -13
  10. package/dist/templates/_shared/hooks/__pycache__/__init__.cpython-313.pyc +0 -0
  11. package/dist/templates/_shared/hooks/__pycache__/archive_plan.cpython-313.pyc +0 -0
  12. package/dist/templates/_shared/hooks/__pycache__/context_enforcer.cpython-313.pyc +0 -0
  13. package/dist/templates/_shared/hooks/__pycache__/context_monitor.cpython-313.pyc +0 -0
  14. package/dist/templates/_shared/hooks/__pycache__/file-suggestion.cpython-313.pyc +0 -0
  15. package/dist/templates/_shared/hooks/__pycache__/session_start.cpython-313.pyc +0 -0
  16. package/dist/templates/_shared/hooks/__pycache__/task_create_atomicity.cpython-313.pyc +0 -0
  17. package/dist/templates/_shared/hooks/__pycache__/task_create_capture.cpython-313.pyc +0 -0
  18. package/dist/templates/_shared/hooks/__pycache__/task_update_capture.cpython-313.pyc +0 -0
  19. package/dist/templates/_shared/hooks/__pycache__/user_prompt_submit.cpython-313.pyc +0 -0
  20. package/dist/templates/_shared/hooks/context_enforcer.py +18 -68
  21. package/dist/templates/_shared/lib/__pycache__/__init__.cpython-313.pyc +0 -0
  22. package/dist/templates/_shared/lib/base/__pycache__/__init__.cpython-313.pyc +0 -0
  23. package/dist/templates/_shared/lib/base/__pycache__/atomic_write.cpython-313.pyc +0 -0
  24. package/dist/templates/_shared/lib/base/__pycache__/constants.cpython-313.pyc +0 -0
  25. package/dist/templates/_shared/lib/base/__pycache__/hook_utils.cpython-313.pyc +0 -0
  26. package/dist/templates/_shared/lib/base/__pycache__/inference.cpython-313.pyc +0 -0
  27. package/dist/templates/_shared/lib/base/__pycache__/stop_words.cpython-313.pyc +0 -0
  28. package/dist/templates/_shared/lib/base/__pycache__/subprocess_utils.cpython-313.pyc +0 -0
  29. package/dist/templates/_shared/lib/base/__pycache__/utils.cpython-313.pyc +0 -0
  30. package/dist/templates/_shared/lib/base/hook_utils.py +0 -24
  31. package/dist/templates/_shared/lib/base/stop_words.py +23 -0
  32. package/dist/templates/_shared/lib/context/__init__.py +0 -8
  33. package/dist/templates/_shared/lib/context/__pycache__/__init__.cpython-313.pyc +0 -0
  34. package/dist/templates/_shared/lib/context/__pycache__/cache.cpython-313.pyc +0 -0
  35. package/dist/templates/_shared/lib/context/__pycache__/context_extractor.cpython-313.pyc +0 -0
  36. package/dist/templates/_shared/lib/context/__pycache__/context_manager.cpython-313.pyc +0 -0
  37. package/dist/templates/_shared/lib/context/__pycache__/discovery.cpython-313.pyc +0 -0
  38. package/dist/templates/_shared/lib/context/__pycache__/event_log.cpython-313.pyc +0 -0
  39. package/dist/templates/_shared/lib/context/__pycache__/plan_archive.cpython-313.pyc +0 -0
  40. package/dist/templates/_shared/lib/context/__pycache__/task_sync.cpython-313.pyc +0 -0
  41. package/dist/templates/_shared/lib/context/plan_archive.py +0 -146
  42. package/dist/templates/_shared/lib/templates/__pycache__/__init__.cpython-313.pyc +0 -0
  43. package/dist/templates/_shared/lib/templates/__pycache__/formatters.cpython-313.pyc +0 -0
  44. package/dist/templates/_shared/lib/templates/__pycache__/persona_questions.cpython-313.pyc +0 -0
  45. package/dist/templates/_shared/lib/templates/__pycache__/plan_context.cpython-313.pyc +0 -0
  46. package/dist/templates/cc-native/.claude/settings.json +12 -12
  47. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/add_plan_context.cpython-313.pyc +0 -0
  48. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-plan-review.cpython-313.pyc +0 -0
  49. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/suggest-fresh-perspective.cpython-313.pyc +0 -0
  50. package/dist/templates/cc-native/_cc-native/hooks/add_plan_context.py +1 -6
  51. package/dist/templates/cc-native/_cc-native/lib/__init__.py +0 -4
  52. package/dist/templates/cc-native/_cc-native/lib/__pycache__/__init__.cpython-313.pyc +0 -0
  53. package/dist/templates/cc-native/_cc-native/lib/__pycache__/atomic_write.cpython-313.pyc +0 -0
  54. package/dist/templates/cc-native/_cc-native/lib/__pycache__/constants.cpython-313.pyc +0 -0
  55. package/dist/templates/cc-native/_cc-native/lib/__pycache__/debug.cpython-313.pyc +0 -0
  56. package/dist/templates/cc-native/_cc-native/lib/__pycache__/orchestrator.cpython-313.pyc +0 -0
  57. package/dist/templates/cc-native/_cc-native/lib/__pycache__/state.cpython-313.pyc +0 -0
  58. package/dist/templates/cc-native/_cc-native/lib/__pycache__/utils.cpython-313.pyc +0 -0
  59. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/__init__.cpython-313.pyc +0 -0
  60. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/agent.cpython-313.pyc +0 -0
  61. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/base.cpython-313.pyc +0 -0
  62. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/codex.cpython-313.pyc +0 -0
  63. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/gemini.cpython-313.pyc +0 -0
  64. package/dist/templates/cc-native/_cc-native/lib/reviewers/agent.py +6 -4
  65. package/dist/templates/cc-native/_cc-native/lib/state.py +11 -9
  66. package/dist/templates/cc-native/_cc-native/lib/utils.py +23 -106
  67. package/dist/templates/cc-native/_cc-native/scripts/__pycache__/aggregate_agents.cpython-313.pyc +0 -0
  68. package/oclif.manifest.json +1 -1
  69. package/package.json +1 -1
@@ -45,10 +45,6 @@ export default class Init extends BaseCommand {
45
45
  * @param templatePath - Template source path
46
46
  */
47
47
  private mergeWindsurfTemplateHooks;
48
- /**
49
- * Check if a path exists
50
- */
51
- private pathExists;
52
48
  /**
53
49
  * Perform post-installation actions.
54
50
  *
@@ -90,10 +86,6 @@ export default class Init extends BaseCommand {
90
86
  * @returns Wizard configuration result
91
87
  */
92
88
  private runInteractiveWizard;
93
- /**
94
- * Check if a file should be excluded from copying
95
- */
96
- private shouldExcludeFile;
97
89
  /**
98
90
  * Track method installation in settings.json
99
91
  *
@@ -8,8 +8,9 @@ import { detectUsername } from '../../lib/user-utils.js';
8
8
  import { updateGitignore } from '../../lib/gitignore-manager.js';
9
9
  import { mergeClaudeSettings } from '../../lib/hooks-merger.js';
10
10
  import { IdePathResolver } from '../../lib/ide-path-resolver.js';
11
+ import { pathExists } from '../../lib/paths.js';
11
12
  import { getTargetSettingsFile, readClaudeSettings, writeClaudeSettings } from '../../lib/settings-hierarchy.js';
12
- import { checkTemplateStatus, installTemplate } from '../../lib/template-installer.js';
13
+ import { checkTemplateStatus, installTemplate, shouldExclude } from '../../lib/template-installer.js';
13
14
  import { getAvailableTemplates, getTemplatePath } from '../../lib/template-resolver.js';
14
15
  import { getTargetHooksFile, readWindsurfHooks, writeWindsurfHooks } from '../../lib/windsurf-hooks-hierarchy.js';
15
16
  import { mergeWindsurfHooks } from '../../lib/windsurf-hooks-merger.js';
@@ -91,13 +92,13 @@ export default class Init extends BaseCommand {
91
92
  const containerDir = resolver.getAiwcliContainer();
92
93
  await fs.mkdir(containerDir, { recursive: true });
93
94
  const sharedDestPath = resolver.getSharedFolder();
94
- const sharedExists = await this.pathExists(sharedDestPath);
95
+ const sharedExists = await pathExists(sharedDestPath);
95
96
  if (!sharedExists) {
96
97
  const currentFilePath = fileURLToPath(import.meta.url);
97
98
  const currentDir = dirname(currentFilePath);
98
99
  const templatesRoot = join(dirname(dirname(currentDir)), 'templates');
99
100
  const sharedSrcPath = join(templatesRoot, '_shared');
100
- if (!(await this.pathExists(sharedSrcPath))) {
101
+ if (!(await pathExists(sharedSrcPath))) {
101
102
  this.error(`Shared folder not found at ${sharedSrcPath}. This indicates a corrupted installation.`, {
102
103
  exit: EXIT_CODES.ENVIRONMENT_ERROR,
103
104
  });
@@ -235,7 +236,7 @@ export default class Init extends BaseCommand {
235
236
  const operations = entries
236
237
  .filter((entry) => {
237
238
  // Standard exclusions (test files, cache, etc.)
238
- if (this.shouldExcludeFile(entry.name)) {
239
+ if (shouldExclude(entry.name)) {
239
240
  return false;
240
241
  }
241
242
  // Exclude IDE config folders if requested (used for _shared folder)
@@ -352,18 +353,6 @@ export default class Init extends BaseCommand {
352
353
  // Don't fail the entire installation if hook merging fails
353
354
  }
354
355
  }
355
- /**
356
- * Check if a path exists
357
- */
358
- async pathExists(path) {
359
- try {
360
- await fs.access(path);
361
- return true;
362
- }
363
- catch {
364
- return false;
365
- }
366
- }
367
356
  /**
368
357
  * Perform post-installation actions.
369
358
  *
@@ -511,25 +500,6 @@ export default class Init extends BaseCommand {
511
500
  confirmed,
512
501
  };
513
502
  }
514
- /**
515
- * Check if a file should be excluded from copying
516
- */
517
- shouldExcludeFile(name) {
518
- const excludedPatterns = [
519
- '_output',
520
- '__pycache__',
521
- '.pytest_cache',
522
- 'conftest.py',
523
- /^test_.*\.py$/,
524
- /.*\.pyc$/,
525
- ];
526
- return excludedPatterns.some((pattern) => {
527
- if (typeof pattern === 'string') {
528
- return name === pattern;
529
- }
530
- return pattern.test(name);
531
- });
532
- }
533
503
  /**
534
504
  * Track method installation in settings.json
535
505
  *
@@ -3,11 +3,10 @@
3
3
  * Re-exports all library modules from this barrel file.
4
4
  */
5
5
  export { type AiwcliConfig, getAiwDir, loadConfig, validateAiwDir, } from './config.js';
6
- export { debug, debugConfig, debugSpawn, debugVersion, isDebugEnabled, setDebugEnabled } from './debug.js';
7
- export { getAiwConfig, getAiwDir as getAiwDirFromEnv, isUsingLegacyEnvVars, loadEnvWithCompatibility, } from './env-compat.js';
8
- export { AiwError, ConfigNotFoundError, EnvironmentError, formatErrorMessage, InvalidUsageError, ProcessSpawnError, } from './errors.js';
6
+ export { debug, debugSpawn, debugVersion, isDebugEnabled, setDebugEnabled } from './debug.js';
7
+ export { AiwError, ConfigNotFoundError, EnvironmentError, InvalidUsageError, ProcessSpawnError, } from './errors.js';
9
8
  export { mergeArraysWithDedup, mergeConfigByEventType } from './generic-merge.js';
10
9
  export { branchExists, createWorktree, deleteBranch, deleteWorktreeFolder, getAllWorktrees, getCurrentBranch, getMainBranch, getWorktreePath, type GitCommandOptions, hasMergeRequest, hasUnpushedCommits, type WorktreeInfo, } from './git/index.js';
11
- export { expandPath, findWorkspaceRoot, getHomePath, getWorkspacePath, isWorkspace, normalizePath, pathExists, resolvePath, toUnixPath, toWindowsPath, } from './paths.js';
10
+ export { findWorkspaceRoot, isWorkspace, pathExists, resolvePath, } from './paths.js';
12
11
  export { spawnProcess, type SpawnProcessOptions } from './spawn.js';
13
12
  export { escapeShellArg, launchTerminal, type TerminalLaunchOptions, type TerminalLaunchResult, } from './terminal.js';
package/dist/lib/index.js CHANGED
@@ -5,17 +5,15 @@
5
5
  // Configuration resolution
6
6
  export { getAiwDir, loadConfig, validateAiwDir, } from './config.js';
7
7
  // Debug logging
8
- export { debug, debugConfig, debugSpawn, debugVersion, isDebugEnabled, setDebugEnabled } from './debug.js';
9
- // Environment variable compatibility
10
- export { getAiwConfig, getAiwDir as getAiwDirFromEnv, isUsingLegacyEnvVars, loadEnvWithCompatibility, } from './env-compat.js';
8
+ export { debug, debugSpawn, debugVersion, isDebugEnabled, setDebugEnabled } from './debug.js';
11
9
  // Custom error classes and utilities
12
- export { AiwError, ConfigNotFoundError, EnvironmentError, formatErrorMessage, InvalidUsageError, ProcessSpawnError, } from './errors.js';
10
+ export { AiwError, ConfigNotFoundError, EnvironmentError, InvalidUsageError, ProcessSpawnError, } from './errors.js';
13
11
  // Generic merge utilities
14
12
  export { mergeArraysWithDedup, mergeConfigByEventType } from './generic-merge.js';
15
13
  // Git utilities
16
14
  export { branchExists, createWorktree, deleteBranch, deleteWorktreeFolder, getAllWorktrees, getCurrentBranch, getMainBranch, getWorktreePath, hasMergeRequest, hasUnpushedCommits, } from './git/index.js';
17
15
  // Cross-platform path utilities
18
- export { expandPath, findWorkspaceRoot, getHomePath, getWorkspacePath, isWorkspace, normalizePath, pathExists, resolvePath, toUnixPath, toWindowsPath, } from './paths.js';
16
+ export { findWorkspaceRoot, isWorkspace, pathExists, resolvePath, } from './paths.js';
19
17
  // Process spawning utilities
20
18
  export { spawnProcess } from './spawn.js';
21
19
  // Cross-platform terminal launching
@@ -2,6 +2,7 @@ import { promises as fs } from 'node:fs';
2
2
  import { homedir } from 'node:os';
3
3
  import { join } from 'node:path';
4
4
  import { IdePathResolver } from './ide-path-resolver.js';
5
+ import { pathExists } from './paths.js';
5
6
  /**
6
7
  * Discover Claude settings files in the hierarchy
7
8
  *
@@ -20,36 +21,24 @@ export async function discoverSettingsFiles(projectDir) {
20
21
  locations.push({
21
22
  type: 'user',
22
23
  path: userSettingsPath,
23
- exists: await fileExists(userSettingsPath),
24
+ exists: await pathExists(userSettingsPath),
24
25
  });
25
26
  // Project settings (shared)
26
27
  const projectSettingsPath = join(projectDir, '.claude', 'settings.json');
27
28
  locations.push({
28
29
  type: 'project',
29
30
  path: projectSettingsPath,
30
- exists: await fileExists(projectSettingsPath),
31
+ exists: await pathExists(projectSettingsPath),
31
32
  });
32
33
  // Local project settings (gitignored)
33
34
  const localSettingsPath = join(projectDir, '.claude', 'settings.local.json');
34
35
  locations.push({
35
36
  type: 'local',
36
37
  path: localSettingsPath,
37
- exists: await fileExists(localSettingsPath),
38
+ exists: await pathExists(localSettingsPath),
38
39
  });
39
40
  return locations;
40
41
  }
41
- /**
42
- * Check if file exists
43
- */
44
- async function fileExists(path) {
45
- try {
46
- await fs.access(path);
47
- return true;
48
- }
49
- catch {
50
- return false;
51
- }
52
- }
53
42
  /**
54
43
  * Read Claude settings from file
55
44
  *
@@ -81,7 +70,7 @@ export async function writeClaudeSettings(path, settings) {
81
70
  const dir = join(path, '..');
82
71
  await fs.mkdir(dir, { recursive: true });
83
72
  // Backup existing file if it exists
84
- if (await fileExists(path)) {
73
+ if (await pathExists(path)) {
85
74
  const backupPath = `${path}.backup`;
86
75
  await fs.copyFile(path, backupPath);
87
76
  }
@@ -67,6 +67,15 @@ export interface InstallationResult {
67
67
  * @returns Status of template items
68
68
  */
69
69
  export declare function checkTemplateStatus(templatePath: string, targetDir: string, ides: string[], templateName: string): Promise<TemplateInstallationStatus>;
70
+ /**
71
+ * Patterns to exclude when copying template directories.
72
+ * These are development/test artifacts that shouldn't be packaged.
73
+ */
74
+ export declare const EXCLUDED_PATTERNS: (string | RegExp)[];
75
+ /**
76
+ * Check if a filename should be excluded from copying
77
+ */
78
+ export declare function shouldExclude(name: string): boolean;
70
79
  /**
71
80
  * Copy directory recursively with proper error handling.
72
81
  * Excludes test files, cache directories, and output folders.
@@ -1,6 +1,7 @@
1
1
  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
+ import { pathExists } from './paths.js';
4
5
  import { mergeTemplateContent } from './template-merger.js';
5
6
  /**
6
7
  * Deep merge two settings objects, combining hook arrays.
@@ -93,18 +94,6 @@ async function mergeSharedSettingsFromSource(targetDir, sourceSettingsPath) {
93
94
  return false;
94
95
  }
95
96
  }
96
- /**
97
- * Check if a path exists
98
- */
99
- async function pathExists(path) {
100
- try {
101
- await fs.access(path);
102
- return true;
103
- }
104
- catch {
105
- return false;
106
- }
107
- }
108
97
  /**
109
98
  * Check template installation status for a method.
110
99
  * Returns which items exist and which are missing.
@@ -178,7 +167,7 @@ export async function checkTemplateStatus(templatePath, targetDir, ides, templat
178
167
  * Patterns to exclude when copying template directories.
179
168
  * These are development/test artifacts that shouldn't be packaged.
180
169
  */
181
- const EXCLUDED_PATTERNS = [
170
+ export const EXCLUDED_PATTERNS = [
182
171
  '_output',
183
172
  '__pycache__',
184
173
  '.pytest_cache',
@@ -189,7 +178,7 @@ const EXCLUDED_PATTERNS = [
189
178
  /**
190
179
  * Check if a filename should be excluded from copying
191
180
  */
192
- function shouldExclude(name) {
181
+ export function shouldExclude(name) {
193
182
  return EXCLUDED_PATTERNS.some((pattern) => {
194
183
  if (typeof pattern === 'string') {
195
184
  return name === pattern;
@@ -1,22 +1,11 @@
1
1
  import { promises as fs } from 'node:fs';
2
2
  import { join } from 'node:path';
3
+ import { pathExists } from './paths.js';
3
4
  /**
4
5
  * Content folder types that should be recursively merged rather than skipped.
5
6
  * These are the canonical folder names used in templates for organizing content.
6
7
  */
7
8
  export const CONTENT_FOLDER_TYPES = ['agents', 'commands', 'workflows', 'tasks'];
8
- /**
9
- * Check if a path exists
10
- */
11
- async function pathExists(path) {
12
- try {
13
- await fs.access(path);
14
- return true;
15
- }
16
- catch {
17
- return false;
18
- }
19
- }
20
9
  /**
21
10
  * Recursively find folders matching the method name within a directory tree.
22
11
  * This finds folders like `.claude/commands/bmad/` when methodName is 'bmad'.
@@ -1,17 +1,6 @@
1
1
  import { promises as fs } from 'node:fs';
2
2
  import { join } from 'node:path';
3
- /**
4
- * Check if file exists
5
- */
6
- async function fileExists(path) {
7
- try {
8
- await fs.access(path);
9
- return true;
10
- }
11
- catch {
12
- return false;
13
- }
14
- }
3
+ import { pathExists } from './paths.js';
15
4
  /**
16
5
  * Read Windsurf hooks from file
17
6
  *
@@ -43,7 +32,7 @@ export async function writeWindsurfHooks(path, hooks) {
43
32
  const dir = join(path, '..');
44
33
  await fs.mkdir(dir, { recursive: true });
45
34
  // Backup existing file if it exists
46
- if (await fileExists(path)) {
35
+ if (await pathExists(path)) {
47
36
  const backupPath = `${path}.backup`;
48
37
  await fs.copyFile(path, backupPath);
49
38
  }
@@ -49,7 +49,6 @@ SCRIPT_DIR = Path(__file__).resolve().parent
49
49
  SHARED_LIB = SCRIPT_DIR.parent / "lib"
50
50
  sys.path.insert(0, str(SHARED_LIB.parent))
51
51
 
52
- from lib.base.hook_utils import load_hook_input
53
52
  from lib.base.subprocess_utils import is_internal_call
54
53
  from lib.context.context_manager import (
55
54
  Context,
@@ -300,18 +299,13 @@ def format_context_picker_stderr(contexts: List[Context]) -> str:
300
299
  return "\n".join(lines)
301
300
 
302
301
 
303
- def format_command_feedback(
304
- ended_contexts: List[Context],
305
- selected_context: Optional[Context],
306
- remaining_prompt: Optional[str] = None
307
- ) -> str:
302
+ def format_command_feedback(ended_contexts: List[Context], selected_context: Optional[Context]) -> str:
308
303
  """
309
304
  Format feedback about what context operations were performed.
310
305
 
311
306
  Args:
312
307
  ended_contexts: Contexts that were ended/completed
313
308
  selected_context: Context that was selected (if any)
314
- remaining_prompt: User's actual request after caret command (if any)
315
309
 
316
310
  Returns:
317
311
  Formatted feedback message
@@ -344,14 +338,6 @@ def format_command_feedback(
344
338
  lines.append(f'All work belongs to context "{selected_context.id}".')
345
339
  lines.append("Tasks created with TaskCreate will be persisted to this context.")
346
340
 
347
- # Add user's actual request if provided after caret command
348
- if remaining_prompt and remaining_prompt.strip():
349
- lines.append("")
350
- lines.append("---")
351
- lines.append("")
352
- lines.append("**User's actual request:**")
353
- lines.append(f"> {remaining_prompt}")
354
-
355
341
  return "\n".join(lines)
356
342
 
357
343
 
@@ -359,7 +345,7 @@ def determine_context(
359
345
  user_prompt: str,
360
346
  project_root: Path = None,
361
347
  session_id: str = None
362
- ) -> Tuple[Optional[str], str, Optional[str], Optional[str]]:
348
+ ) -> Tuple[Optional[str], str, Optional[str]]:
363
349
  """
364
350
  Determine which context this prompt belongs to.
365
351
 
@@ -369,7 +355,6 @@ def determine_context(
369
355
  - method: How context was determined (session_match, in_flight, caret_select,
370
356
  auto_created, single_context, blocked)
371
357
  - output: System reminder to inject, or None
372
- - remaining_prompt: Actual user request after caret command, or None
373
358
 
374
359
  Raises:
375
360
  BlockRequest: When request should be blocked to show picker to user
@@ -377,7 +362,7 @@ def determine_context(
377
362
  # 0. Skip context creation for internal subprocess calls (orchestrator, agents)
378
363
  if is_internal_call():
379
364
  eprint("[context_enforcer] Skipping: internal subprocess call")
380
- return (None, "skip_internal", None, None)
365
+ return (None, "skip_internal", None)
381
366
 
382
367
  # 1. Check if session already belongs to a context (HIGHEST PRIORITY)
383
368
  # This prevents context switching on subsequent prompts - one context per session
@@ -388,20 +373,11 @@ def determine_context(
388
373
  return (
389
374
  session_context.id,
390
375
  "session_match",
391
- format_active_context_reminder(session_context),
392
- None
376
+ format_active_context_reminder(session_context)
393
377
  )
394
378
 
395
379
  # 2. Check for bare "^" - show context picker
396
380
  if user_prompt.strip() == "^":
397
- # Pre-transition: Move any pending_implementation contexts to implementing
398
- # This ensures they appear selectable when user opens the picker
399
- in_flight = get_all_in_flight_contexts(project_root)
400
- for ctx in in_flight:
401
- if ctx.in_flight and ctx.in_flight.mode == "pending_implementation":
402
- update_plan_status(ctx.id, "implementing", project_root=project_root)
403
- eprint(f"[context_enforcer] Pre-transitioned {ctx.id} to implementing (bare caret)")
404
-
405
381
  contexts = get_all_contexts(status="active", project_root=project_root)
406
382
  if not contexts:
407
383
  raise BlockRequest(
@@ -431,7 +407,7 @@ def determine_context(
431
407
 
432
408
  # Don't auto-create for greetings or help commands
433
409
  if any(prompt_lower.startswith(p) or prompt_lower == p for p in skip_patterns):
434
- return (None, "no_context_needed", None, None)
410
+ return (None, "no_context_needed", None)
435
411
 
436
412
  # Auto-create context from prompt
437
413
  try:
@@ -443,12 +419,11 @@ def determine_context(
443
419
  return (
444
420
  new_context.id,
445
421
  "auto_created",
446
- format_context_created(new_context),
447
- None
422
+ format_context_created(new_context)
448
423
  )
449
424
  except Exception as e:
450
425
  eprint(f"[context_enforcer] Failed to create context: {e}")
451
- return (None, "creation_failed", None, None)
426
+ return (None, "creation_failed", None)
452
427
 
453
428
  elif len(in_flight_contexts) == 1:
454
429
  # Single in-flight context - auto-select it
@@ -456,15 +431,6 @@ def determine_context(
456
431
  mode = ctx.in_flight.mode if ctx.in_flight else "none"
457
432
  eprint(f"[context_enforcer] Auto-selected single in-flight context: {ctx.id} (mode={mode})")
458
433
 
459
- # Auto-transition pending_implementation to implementing
460
- # This ensures state updates immediately when context is selected,
461
- # rather than waiting for _update_in_flight_status() which has conditions
462
- if mode == "pending_implementation":
463
- update_plan_status(ctx.id, "implementing", project_root=project_root)
464
- ctx.in_flight.mode = "implementing" # Update local copy for display
465
- mode = "implementing" # Update local var for formatter selection
466
- eprint(f"[context_enforcer] Transitioned {ctx.id} to implementing")
467
-
468
434
  # Use mode-specific formatter for better continuation context
469
435
  if mode == "pending_implementation":
470
436
  output = format_pending_plan_continuation(ctx)
@@ -473,7 +439,7 @@ def determine_context(
473
439
  else:
474
440
  output = format_active_context_reminder(ctx)
475
441
 
476
- return (ctx.id, "auto_selected", output, None)
442
+ return (ctx.id, "auto_selected", output)
477
443
 
478
444
  else:
479
445
  # Multiple in-flight contexts - block and show picker
@@ -489,7 +455,7 @@ def _handle_caret_command(
489
455
  user_prompt: str,
490
456
  contexts: List[Context],
491
457
  project_root: Path
492
- ) -> Tuple[Optional[str], str, Optional[str], Optional[str]]:
458
+ ) -> Tuple[Optional[str], str, Optional[str]]:
493
459
  """
494
460
  Handle explicit caret commands (^E, ^S, ^0, ^N).
495
461
 
@@ -499,7 +465,7 @@ def _handle_caret_command(
499
465
  project_root: Project root directory
500
466
 
501
467
  Returns:
502
- Tuple of (context_id, method, output, remaining_prompt)
468
+ Tuple of (context_id, method, output)
503
469
 
504
470
  Raises:
505
471
  BlockRequest: When command is invalid or selection needed
@@ -539,8 +505,7 @@ def _handle_caret_command(
539
505
  return (
540
506
  new_context.id,
541
507
  "caret_new",
542
- format_context_created(new_context),
543
- None
508
+ format_context_created(new_context)
544
509
  )
545
510
  except Exception as e:
546
511
  eprint(f"[context_enforcer] Failed to create context: {e}")
@@ -577,8 +542,7 @@ def _handle_caret_command(
577
542
  return (
578
543
  new_context.id,
579
544
  "caret_new",
580
- output,
581
- None
545
+ output
582
546
  )
583
547
  except Exception as e:
584
548
  eprint(f"[context_enforcer] Failed to create context: {e}")
@@ -588,24 +552,11 @@ def _handle_caret_command(
588
552
  if cmd.select:
589
553
  selected_ctx = contexts[cmd.select - 1] # 1-indexed
590
554
  eprint(f"[context_enforcer] Caret-selected context: {selected_ctx.id}")
591
-
592
- # Auto-transition pending_implementation to implementing
593
- mode = selected_ctx.in_flight.mode if selected_ctx.in_flight else "none"
594
- if mode == "pending_implementation":
595
- update_plan_status(selected_ctx.id, "implementing", project_root=project_root)
596
- selected_ctx.in_flight.mode = "implementing"
597
- eprint(f"[context_enforcer] Transitioned {selected_ctx.id} to implementing")
598
-
599
- output = format_command_feedback(
600
- ended_contexts,
601
- selected_ctx,
602
- cmd.remaining_prompt if cmd.remaining_prompt else None
603
- )
555
+ output = format_command_feedback(ended_contexts, selected_ctx)
604
556
  return (
605
557
  selected_ctx.id,
606
558
  "caret_select",
607
- output,
608
- cmd.remaining_prompt if cmd.remaining_prompt else None
559
+ output
609
560
  )
610
561
 
611
562
  # Only ended contexts, no selection - refresh context list and block
@@ -637,10 +588,11 @@ def main():
637
588
  In production, use user_prompt_submit.py as the unified entry point.
638
589
  """
639
590
  try:
640
- hook_input = load_hook_input()
641
- if not hook_input:
591
+ input_data = sys.stdin.read().strip()
592
+ if not input_data:
642
593
  return
643
594
 
595
+ hook_input = json.loads(input_data)
644
596
  user_prompt = hook_input.get("prompt", "")
645
597
  if not user_prompt:
646
598
  return
@@ -648,10 +600,8 @@ def main():
648
600
  project_root = project_dir(hook_input)
649
601
 
650
602
  try:
651
- context_id, method, output, remaining_prompt = determine_context(user_prompt, project_root)
603
+ context_id, method, output = determine_context(user_prompt, project_root)
652
604
  eprint(f"[context_enforcer] Method: {method}, Context: {context_id}")
653
- if remaining_prompt:
654
- eprint(f"[context_enforcer] Remaining prompt: {remaining_prompt[:50]}...")
655
605
 
656
606
  if output:
657
607
  print(output)
@@ -19,30 +19,6 @@ from .utils import eprint
19
19
  F = TypeVar('F', bound=Callable[..., Any])
20
20
 
21
21
 
22
- def setup_hook_paths() -> Path:
23
- """
24
- Setup import paths for hooks.
25
-
26
- Call this at module level in hook scripts to ensure
27
- the shared lib directory is in sys.path.
28
-
29
- Returns:
30
- Path to the lib directory
31
-
32
- Example:
33
- SCRIPT_DIR = Path(__file__).resolve().parent
34
- SHARED_LIB = SCRIPT_DIR.parent / "lib"
35
- sys.path.insert(0, str(SHARED_LIB.parent))
36
- """
37
- # This function exists mainly for documentation
38
- # Actual path setup must happen at module level before imports
39
- hook_dir = Path(__file__).resolve().parent.parent.parent / "hooks"
40
- lib_dir = hook_dir.parent / "lib"
41
- if str(lib_dir.parent) not in sys.path:
42
- sys.path.insert(0, str(lib_dir.parent))
43
- return lib_dir
44
-
45
-
46
22
  def load_hook_input() -> Optional[Dict[str, Any]]:
47
23
  """
48
24
  Load and parse JSON from stdin.
@@ -187,4 +187,27 @@ STOP_WORDS = {
187
187
  # FRAGMENT WORDS (artifacts from contractions/tokenization)
188
188
  # ========================================================================
189
189
  're', 'pl', 'aiw', 've', 'll', 'doesn', 't', 's',
190
+
191
+ # ========================================================================
192
+ # CORPUS-DERIVED SHORT NOISE (2026-02 analysis of 131 docs)
193
+ # ========================================================================
194
+ # Contractions with punctuation stripped (I'm -> im, etc.)
195
+ 'im', 'ive', 'id', 'ill', 'youre', 'youve', 'youll',
196
+ 'hes', 'shes', 'weve', 'theyre', 'theyve', 'dont', 'doesnt',
197
+ 'didnt', 'wont', 'wouldnt', 'cant', 'couldnt', 'shouldnt', 'isnt',
198
+ 'arent', 'wasnt', 'werent', 'hasnt', 'havent', 'hadnt', 'lets',
199
+ 'thats', 'whats', 'heres', 'theres', 'whos',
200
+
201
+ # Filler/noise from corpus (>10% frequency, non-action)
202
+ 'etc', 'up', 'as', 'cc',
203
+
204
+ # Number words (common in plans but don't identify task)
205
+ 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'ten',
206
+
207
+ # Single letters (artifacts from lists, paths, variables)
208
+ 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'k', 'l', 'm', 'n', 'o', 'p',
209
+ 'q', 'r', 'u', 'v', 'w', 'x', 'y', 'z',
210
+
211
+ # Short adverbs/fillers from review
212
+ 'too', 'yes', 'ok', 'okay',
190
213
  }
@@ -50,10 +50,6 @@ from .task_sync import (
50
50
  )
51
51
  from .plan_archive import (
52
52
  archive_plan_to_context,
53
- get_active_context_for_plan,
54
- create_context_from_plan,
55
- mark_plan_implementation_started,
56
- mark_plan_completed,
57
53
  )
58
54
  from .context_extractor import (
59
55
  extract_context_id,
@@ -108,10 +104,6 @@ __all__ = [
108
104
  "generate_next_task_id",
109
105
  # Plan Archive
110
106
  "archive_plan_to_context",
111
- "get_active_context_for_plan",
112
- "create_context_from_plan",
113
- "mark_plan_implementation_started",
114
- "mark_plan_completed",
115
107
  # Context Extractor
116
108
  "extract_context_id",
117
109
  "extract_context_id_for_session",
@@ -99,149 +99,3 @@ def archive_plan_to_context(
99
99
  return str(archive_path), plan_hash
100
100
 
101
101
 
102
- def get_active_context_for_plan(
103
- plan_path: str,
104
- project_root: Path = None
105
- ) -> Optional[str]:
106
- """
107
- Determine which context a plan belongs to.
108
-
109
- Logic:
110
- 1. If exactly one active context exists -> use it
111
- 2. Check if plan path contains context hints
112
- 3. Return None if ambiguous
113
-
114
- Args:
115
- plan_path: Path to plan file
116
- project_root: Project root directory
117
-
118
- Returns:
119
- Context ID or None if cannot determine
120
- """
121
- active_contexts = get_all_contexts(status="active", project_root=project_root)
122
-
123
- # If exactly one active context, use it
124
- if len(active_contexts) == 1:
125
- return active_contexts[0].id
126
-
127
- # Check if plan path contains a context ID
128
- plan_path_lower = plan_path.lower()
129
- for ctx in active_contexts:
130
- if ctx.id.lower() in plan_path_lower:
131
- return ctx.id
132
-
133
- # Check plan filename for context hints
134
- plan_file = Path(plan_path)
135
- filename_lower = plan_file.stem.lower()
136
- for ctx in active_contexts:
137
- if ctx.id.lower() in filename_lower:
138
- return ctx.id
139
-
140
- # If no active contexts, return None (caller should create new context)
141
- if not active_contexts:
142
- return None
143
-
144
- # Multiple active contexts, cannot determine
145
- eprint(f"[plan_archive] Multiple active contexts, cannot determine target")
146
- return None
147
-
148
-
149
- def create_context_from_plan(
150
- plan_path: str,
151
- project_root: Path = None
152
- ) -> Optional[str]:
153
- """
154
- Create a new context based on a plan file.
155
-
156
- Extracts context ID and summary from plan filename/content.
157
-
158
- Args:
159
- plan_path: Path to plan file
160
- project_root: Project root directory
161
-
162
- Returns:
163
- Created context ID or None on error
164
- """
165
- plan_file = Path(plan_path)
166
- if not plan_file.exists():
167
- eprint(f"[plan_archive] Plan file not found: {plan_path}")
168
- return None
169
-
170
- # Generate context ID from plan filename
171
- context_id = sanitize_title(plan_file.stem, max_len=50)
172
-
173
- # Try to extract summary from plan content
174
- try:
175
- content = plan_file.read_text(encoding='utf-8')
176
- # Look for first heading as summary
177
- for line in content.splitlines():
178
- line = line.strip()
179
- if line.startswith('#'):
180
- summary = line.lstrip('#').strip()[:100]
181
- break
182
- else:
183
- summary = f"Implementation of {plan_file.stem}"
184
- except Exception:
185
- summary = f"Implementation of {plan_file.stem}"
186
-
187
- # Create the context
188
- try:
189
- context = create_context(
190
- context_id=context_id,
191
- summary=summary,
192
- method="cc-native",
193
- project_root=project_root
194
- )
195
- return context.id
196
- except ValueError as e:
197
- eprint(f"[plan_archive] Failed to create context: {e}")
198
- return None
199
-
200
-
201
- def mark_plan_implementation_started(
202
- context_id: str,
203
- project_root: Path = None
204
- ) -> bool:
205
- """
206
- Mark that plan implementation has started.
207
-
208
- Called by SessionStart after detecting pending_implementation.
209
- Prevents re-triggering on subsequent /clear commands.
210
-
211
- Args:
212
- context_id: Context identifier
213
- project_root: Project root directory
214
-
215
- Returns:
216
- True if successful
217
- """
218
- context = update_plan_status(
219
- context_id,
220
- status="implementing",
221
- project_root=project_root
222
- )
223
- return context is not None
224
-
225
-
226
- def mark_plan_completed(
227
- context_id: str,
228
- project_root: Path = None
229
- ) -> bool:
230
- """
231
- Mark that plan has been fully implemented.
232
-
233
- Called when all plan tasks are completed.
234
-
235
- Args:
236
- context_id: Context identifier
237
- project_root: Project root directory
238
-
239
- Returns:
240
- True if successful
241
- """
242
- context = update_plan_status(
243
- context_id,
244
- status="none",
245
- project_root=project_root
246
- )
247
- return context is not None
@@ -14,7 +14,7 @@
14
14
  "ides": [
15
15
  "claude"
16
16
  ],
17
- "installedAt": "2026-01-25T22:27:03.688Z"
17
+ "installedAt": "{{INSTALLED_AT}}"
18
18
  }
19
19
  },
20
20
  "hooks": {
@@ -51,16 +51,6 @@
51
51
  }
52
52
  ]
53
53
  },
54
- {
55
- "matcher": "Bash|Edit",
56
- "hooks": [
57
- {
58
- "type": "command",
59
- "command": "python .aiwcli/_cc-native/hooks/suggest-fresh-perspective.py",
60
- "timeout": 5000
61
- }
62
- ]
63
- },
64
54
  {
65
55
  "matcher": "TaskCreate",
66
56
  "hooks": [
@@ -90,6 +80,16 @@
90
80
  "timeout": 5000
91
81
  }
92
82
  ]
83
+ },
84
+ {
85
+ "matcher": "Bash|Edit",
86
+ "hooks": [
87
+ {
88
+ "type": "command",
89
+ "command": "python .aiwcli/_cc-native/hooks/suggest-fresh-perspective.py",
90
+ "timeout": 5000
91
+ }
92
+ ]
93
93
  }
94
94
  ],
95
95
  "PermissionRequest": [
@@ -137,4 +137,4 @@
137
137
  }
138
138
  ]
139
139
  }
140
- }
140
+ }
@@ -29,6 +29,7 @@ from utils import (
29
29
  eprint,
30
30
  was_questions_offered,
31
31
  mark_questions_offered,
32
+ project_dir,
32
33
  )
33
34
  from templates.plan_context import (
34
35
  get_evaluation_context_reminder,
@@ -43,12 +44,6 @@ def is_plan_file_write(payload: Dict[str, Any]) -> bool:
43
44
  return ".claude/plans/" in file_path.replace("\\", "/") and file_path.endswith(".md")
44
45
 
45
46
 
46
- def project_dir(payload: Dict[str, Any]) -> Path:
47
- """Get project directory from payload or environment."""
48
- p = os.environ.get("CLAUDE_PROJECT_DIR") or payload.get("cwd") or os.getcwd()
49
- return Path(p)
50
-
51
-
52
47
  def load_plan_context_config(proj_dir: Path) -> Dict[str, Any]:
53
48
  """Load planContext config with defaults."""
54
49
  config_path = proj_dir / "_cc-native" / "config.json"
@@ -11,8 +11,6 @@ from .utils import (
11
11
  eprint,
12
12
  sanitize_filename,
13
13
  sanitize_title,
14
- extract_plan_title,
15
- extract_task_from_context,
16
14
  find_plan_file,
17
15
  ReviewerResult,
18
16
  OrchestratorResult,
@@ -36,8 +34,6 @@ __all__ = [
36
34
  "eprint",
37
35
  "sanitize_filename",
38
36
  "sanitize_title",
39
- "extract_plan_title",
40
- "extract_task_from_context",
41
37
  "find_plan_file",
42
38
  # Dataclasses
43
39
  "ReviewerResult",
@@ -111,8 +111,9 @@ def run_agent_review(
111
111
 
112
112
  eprint(f"[{agent.name}] Found Claude CLI at: {claude_path}")
113
113
 
114
- # User prompt contains just the task and plan
115
- prompt = f"""Review the plan below and provide your assessment using StructuredOutput.
114
+ # User prompt - direct instruction to call StructuredOutput immediately
115
+ prompt = f"""IMMEDIATELY call StructuredOutput with your review of the plan below.
116
+ Do NOT output any text before calling StructuredOutput.
116
117
 
117
118
  PLAN:
118
119
  <<<
@@ -133,9 +134,10 @@ PLAN:
133
134
  "--setting-sources", "", # Disable user/project settings to avoid PAI context interference
134
135
  ]
135
136
 
136
- # Add system prompt if available (the markdown body with full persona)
137
+ # Add system prompt: prefix with single-turn instructions, then agent's persona
137
138
  if agent.system_prompt:
138
- cmd_args.extend(["--system-prompt", agent.system_prompt])
139
+ full_prompt = AGENT_REVIEW_PROMPT_PREFIX + "\n\n---\n\n" + agent.system_prompt
140
+ cmd_args.extend(["--system-prompt", full_prompt])
139
141
 
140
142
  eprint(f"[{agent.name}] Running with model: {agent.model}, timeout: {timeout}s")
141
143
 
@@ -21,6 +21,17 @@ except ImportError:
21
21
  from constants import validate_plan_path, PLANS_DIR
22
22
  from atomic_write import atomic_write
23
23
 
24
+ # Import canonical eprint from shared lib
25
+ try:
26
+ from ...lib.base.utils import eprint
27
+ except ImportError:
28
+ # Fallback for direct execution
29
+ import sys as _sys
30
+ from pathlib import Path as _Path
31
+ _shared_lib = _Path(__file__).resolve().parent.parent.parent / "_shared" / "lib"
32
+ _sys.path.insert(0, str(_shared_lib))
33
+ from base.utils import eprint
34
+
24
35
 
25
36
  # ---------------------------
26
37
  # Constants
@@ -35,15 +46,6 @@ DEFAULT_REVIEW_ITERATIONS: Dict[str, int] = {
35
46
  }
36
47
 
37
48
 
38
- # ---------------------------
39
- # Utilities
40
- # ---------------------------
41
-
42
- def eprint(*args: Any) -> None:
43
- """Print to stderr."""
44
- print(*args, file=sys.stderr)
45
-
46
-
47
49
  # ---------------------------
48
50
  # State File Management
49
51
  # ---------------------------
@@ -29,6 +29,29 @@ except ImportError:
29
29
  from atomic_write import atomic_write
30
30
  from constants import ENABLE_ROBUST_PLAN_WRITES
31
31
 
32
+ # Import canonical utilities from shared lib (with Windows bug fixes)
33
+ try:
34
+ from ...lib.base.utils import (
35
+ eprint,
36
+ now_local,
37
+ project_dir,
38
+ sanitize_filename,
39
+ sanitize_title,
40
+ )
41
+ except ImportError:
42
+ # Fallback for direct execution
43
+ import sys
44
+ from pathlib import Path
45
+ _shared_lib = Path(__file__).resolve().parent.parent.parent / "_shared" / "lib"
46
+ sys.path.insert(0, str(_shared_lib))
47
+ from base.utils import (
48
+ eprint,
49
+ now_local,
50
+ project_dir,
51
+ sanitize_filename,
52
+ sanitize_title,
53
+ )
54
+
32
55
 
33
56
  # ---------------------------
34
57
  # Constants
@@ -87,68 +110,6 @@ class ReviewerResult:
87
110
  err: str
88
111
 
89
112
 
90
- # ---------------------------
91
- # Core utilities
92
- # ---------------------------
93
-
94
- def eprint(*args: Any) -> None:
95
- """Print to stderr."""
96
- print(*args, file=sys.stderr)
97
-
98
-
99
- def now_local() -> datetime:
100
- """Get current local datetime."""
101
- return datetime.now()
102
-
103
-
104
- def project_dir(payload: Dict[str, Any]) -> Path:
105
- """Get project directory from payload or environment."""
106
- p = os.environ.get("CLAUDE_PROJECT_DIR") or payload.get("cwd") or os.getcwd()
107
- return Path(p)
108
-
109
-
110
- def sanitize_filename(s: str, max_len: int = 32) -> str:
111
- """Sanitize string for use in filename."""
112
- s = re.sub(r"[^A-Za-z0-9._-]+", "_", s)
113
- return s.strip("._-")[:max_len] or "unknown"
114
-
115
-
116
- def sanitize_title(s: str, max_len: int = 50) -> str:
117
- """Sanitize title for use in filename (with space-to-dash conversion)."""
118
- s = s.replace(' ', '-')
119
- s = re.sub(r"[^A-Za-z0-9._-]+", "_", s)
120
- s = re.sub(r"[-_]+", "-", s)
121
- return s.strip("._-")[:max_len] or "unknown"
122
-
123
-
124
- def extract_plan_title(plan: str) -> Optional[str]:
125
- """Extract title from '# Plan: <title>' line in plan content."""
126
- for line in plan.split('\n'):
127
- line = line.strip()
128
- if line.startswith('# Plan:'):
129
- title = line[7:].strip()
130
- return title if title else None
131
- return None
132
-
133
-
134
- def extract_task_from_context(plan: str) -> Optional[str]:
135
- """Extract Task from Evaluation Context section as fallback title."""
136
- # Look for **Task**: ... or **Task Summary**: ... patterns
137
- patterns = [
138
- r'\*\*Task\*\*:\s*(.+?)(?:\n|$)',
139
- r'\*\*Task Summary\*\*:\s*(.+?)(?:\n|$)',
140
- ]
141
- for pattern in patterns:
142
- match = re.search(pattern, plan)
143
- if match:
144
- task = match.group(1).strip()
145
- # Truncate to reasonable title length
146
- if len(task) > 50:
147
- task = task[:47] + "..."
148
- return task
149
- return None
150
-
151
-
152
113
  # ---------------------------
153
114
  # Plan hash deduplication
154
115
  # ---------------------------
@@ -398,50 +359,6 @@ def get_state_path_from_plan(plan_path: str) -> Path:
398
359
  return plan_file.with_suffix('.state.json')
399
360
 
400
361
 
401
- def load_state(plan_path: str) -> Optional[Dict[str, Any]]:
402
- """Load state file for this plan if it exists."""
403
- state_file = get_state_path_from_plan(plan_path)
404
-
405
- if not state_file.exists():
406
- return None
407
-
408
- try:
409
- return json.loads(state_file.read_text(encoding="utf-8"))
410
- except Exception as e:
411
- eprint(f"[utils] Failed to read state file: {e}")
412
- return None
413
-
414
-
415
- def save_state(plan_path: str, state: Dict[str, Any]) -> bool:
416
- """Save state file for this plan.
417
-
418
- Returns True on success, False on failure.
419
- """
420
- state_file = get_state_path_from_plan(plan_path)
421
- try:
422
- state_file.write_text(json.dumps(state, indent=2), encoding="utf-8")
423
- return True
424
- except Exception as e:
425
- eprint(f"[utils] Failed to save state file: {e}")
426
- return False
427
-
428
-
429
- def delete_state(plan_path: str) -> bool:
430
- """Delete state file after successful archive.
431
-
432
- Returns True if deleted or didn't exist, False on error.
433
- """
434
- state_file = get_state_path_from_plan(plan_path)
435
- try:
436
- if state_file.exists():
437
- state_file.unlink()
438
- eprint(f"[utils] Deleted state file: {state_file}")
439
- return True
440
- except Exception as e:
441
- eprint(f"[utils] Warning: failed to delete state file: {e}")
442
- return False
443
-
444
-
445
362
  def format_review_markdown(
446
363
  results: List[ReviewerResult],
447
364
  overall: str,
@@ -401,5 +401,5 @@
401
401
  ]
402
402
  }
403
403
  },
404
- "version": "0.9.6"
404
+ "version": "0.9.7"
405
405
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "aiwcli",
3
3
  "description": "AI Workflow CLI - Command-line interface for AI-powered workflows",
4
- "version": "0.9.6",
4
+ "version": "0.9.7",
5
5
  "author": "jofu-tofu",
6
6
  "bin": {
7
7
  "aiw": "./bin/run.js"