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.
- package/dist/commands/init/index.d.ts +0 -8
- package/dist/commands/init/index.js +5 -35
- package/dist/lib/index.d.ts +3 -4
- package/dist/lib/index.js +3 -5
- package/dist/lib/settings-hierarchy.js +5 -16
- package/dist/lib/template-installer.d.ts +9 -0
- package/dist/lib/template-installer.js +3 -14
- package/dist/lib/template-merger.js +1 -12
- package/dist/lib/windsurf-hooks-hierarchy.js +2 -13
- package/dist/templates/_shared/hooks/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/archive_plan.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/context_enforcer.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/context_monitor.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/file-suggestion.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/session_start.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/task_create_atomicity.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/task_create_capture.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/task_update_capture.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/user_prompt_submit.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/context_enforcer.py +18 -68
- package/dist/templates/_shared/lib/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/atomic_write.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/constants.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/hook_utils.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/inference.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/stop_words.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/subprocess_utils.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/utils.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/hook_utils.py +0 -24
- package/dist/templates/_shared/lib/base/stop_words.py +23 -0
- package/dist/templates/_shared/lib/context/__init__.py +0 -8
- package/dist/templates/_shared/lib/context/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/cache.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/context_extractor.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/context_manager.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/discovery.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/event_log.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/plan_archive.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/task_sync.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/plan_archive.py +0 -146
- package/dist/templates/_shared/lib/templates/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/templates/__pycache__/formatters.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/templates/__pycache__/persona_questions.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/templates/__pycache__/plan_context.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/.claude/settings.json +12 -12
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/add_plan_context.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-plan-review.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/suggest-fresh-perspective.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/add_plan_context.py +1 -6
- package/dist/templates/cc-native/_cc-native/lib/__init__.py +0 -4
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/atomic_write.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/constants.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/debug.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/orchestrator.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/state.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/utils.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/agent.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/base.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/codex.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/gemini.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/agent.py +6 -4
- package/dist/templates/cc-native/_cc-native/lib/state.py +11 -9
- package/dist/templates/cc-native/_cc-native/lib/utils.py +23 -106
- package/dist/templates/cc-native/_cc-native/scripts/__pycache__/aggregate_agents.cpython-313.pyc +0 -0
- package/oclif.manifest.json +1 -1
- 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
|
|
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
|
|
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 (
|
|
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
|
*
|
package/dist/lib/index.d.ts
CHANGED
|
@@ -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,
|
|
7
|
-
export {
|
|
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 {
|
|
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,
|
|
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,
|
|
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 {
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
35
|
+
if (await pathExists(path)) {
|
|
47
36
|
const backupPath = `${path}.backup`;
|
|
48
37
|
await fs.copyFile(path, backupPath);
|
|
49
38
|
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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]
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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]
|
|
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
|
|
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
|
-
|
|
641
|
-
if not
|
|
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
|
|
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)
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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",
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"ides": [
|
|
15
15
|
"claude"
|
|
16
16
|
],
|
|
17
|
-
"installedAt": "
|
|
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
|
+
}
|
package/dist/templates/cc-native/_cc-native/hooks/__pycache__/add_plan_context.cpython-313.pyc
CHANGED
|
Binary file
|
package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-plan-review.cpython-313.pyc
CHANGED
|
Binary file
|
|
Binary file
|
|
@@ -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",
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/__init__.cpython-313.pyc
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/gemini.cpython-313.pyc
CHANGED
|
Binary file
|
|
@@ -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
|
|
115
|
-
prompt = f"""
|
|
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
|
|
137
|
+
# Add system prompt: prefix with single-turn instructions, then agent's persona
|
|
137
138
|
if agent.system_prompt:
|
|
138
|
-
|
|
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,
|
package/dist/templates/cc-native/_cc-native/scripts/__pycache__/aggregate_agents.cpython-313.pyc
CHANGED
|
Binary file
|
package/oclif.manifest.json
CHANGED