aiwcli 0.10.3 → 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/run.js +1 -1
- package/dist/commands/clear.js +28 -131
- package/dist/commands/init/index.js +3 -3
- package/dist/lib/gitignore-manager.d.ts +32 -0
- package/dist/lib/gitignore-manager.js +141 -2
- package/dist/templates/CLAUDE.md +8 -8
- package/dist/templates/_shared/.claude/commands/handoff-resume.md +64 -0
- package/dist/templates/_shared/.claude/commands/handoff.md +16 -10
- package/dist/templates/_shared/.claude/settings.json +7 -7
- package/dist/templates/_shared/hooks-ts/_utils/git-state.ts +2 -0
- package/dist/templates/_shared/hooks-ts/archive_plan.ts +159 -0
- package/dist/templates/_shared/hooks-ts/context_monitor.ts +147 -0
- package/dist/templates/_shared/hooks-ts/file-suggestion.ts +130 -0
- package/dist/templates/_shared/hooks-ts/pre_compact.ts +49 -0
- package/dist/templates/_shared/hooks-ts/session_end.ts +107 -0
- package/dist/templates/_shared/hooks-ts/session_start.ts +144 -0
- package/dist/templates/_shared/hooks-ts/task_create_capture.ts +48 -0
- package/dist/templates/_shared/hooks-ts/task_update_capture.ts +74 -0
- package/dist/templates/_shared/hooks-ts/user_prompt_submit.ts +83 -0
- package/dist/templates/_shared/lib-ts/CLAUDE.md +318 -0
- package/dist/templates/_shared/lib-ts/base/atomic-write.ts +12 -12
- package/dist/templates/_shared/lib-ts/base/constants.ts +22 -15
- package/dist/templates/_shared/lib-ts/base/git-state.ts +1 -1
- package/dist/templates/_shared/lib-ts/base/hook-utils.ts +129 -50
- package/dist/templates/_shared/lib-ts/base/inference.ts +28 -21
- package/dist/templates/_shared/lib-ts/base/logger.ts +15 -2
- package/dist/templates/_shared/lib-ts/base/state-io.ts +9 -7
- package/dist/templates/_shared/lib-ts/base/stop-words.ts +131 -131
- package/dist/templates/_shared/lib-ts/base/subprocess-utils.ts +142 -0
- package/dist/templates/_shared/lib-ts/base/utils.ts +69 -69
- package/dist/templates/_shared/lib-ts/context/context-formatter.ts +30 -24
- package/dist/templates/_shared/lib-ts/context/context-selector.ts +50 -32
- package/dist/templates/_shared/lib-ts/context/context-store.ts +76 -48
- package/dist/templates/_shared/lib-ts/context/plan-manager.ts +43 -23
- package/dist/templates/_shared/lib-ts/context/task-tracker.ts +10 -6
- package/dist/templates/_shared/lib-ts/handoff/document-generator.ts +11 -10
- package/dist/templates/_shared/lib-ts/handoff/handoff-reader.ts +158 -0
- package/dist/templates/_shared/lib-ts/templates/formatters.ts +6 -4
- package/dist/templates/_shared/lib-ts/types.ts +68 -55
- package/dist/templates/_shared/scripts/resolve_context.ts +24 -0
- package/dist/templates/_shared/scripts/resume_handoff.ts +345 -0
- package/dist/templates/_shared/scripts/save_handoff.ts +3 -3
- package/dist/templates/_shared/scripts/status_line.ts +687 -0
- package/dist/templates/cc-native/.claude/settings.json +175 -185
- package/dist/templates/cc-native/TEMPLATE-SCHEMA.md +15 -17
- package/dist/templates/cc-native/_cc-native/agents/CLAUDE.md +0 -2
- package/dist/templates/cc-native/_cc-native/hooks/CLAUDE.md +109 -135
- package/dist/templates/cc-native/_cc-native/hooks/add_plan_context.ts +119 -0
- package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.ts +1027 -0
- package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.ts +61 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/aggregate-agents.ts +156 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/artifacts.ts +792 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/cc-native-state.ts +199 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/cli-output-parser.ts +144 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/config.ts +57 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/constants.ts +83 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/corroboration.ts +115 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/debug.ts +80 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/index.ts +120 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/json-parser.ts +168 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/nul +3 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/orchestrator.ts +250 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/agent.ts +275 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/codex.ts +130 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/gemini.ts +107 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/index.ts +10 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/types.ts +23 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/state.ts +240 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/tsconfig.json +18 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/types.ts +385 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/verdict.ts +72 -0
- package/dist/templates/cc-native/_cc-native/plan-review.config.json +14 -1
- package/oclif.manifest.json +1 -1
- package/package.json +2 -2
- package/dist/templates/_shared/hooks/__init__.py +0 -16
- 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__/pre_compact.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/session_end.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/archive_plan.py +0 -177
- package/dist/templates/_shared/hooks/context_monitor.py +0 -270
- package/dist/templates/_shared/hooks/file-suggestion.py +0 -215
- package/dist/templates/_shared/hooks/pre_compact.py +0 -104
- package/dist/templates/_shared/hooks/session_end.py +0 -173
- package/dist/templates/_shared/hooks/session_start.py +0 -206
- package/dist/templates/_shared/hooks/task_create_capture.py +0 -108
- package/dist/templates/_shared/hooks/task_update_capture.py +0 -145
- package/dist/templates/_shared/hooks/user_prompt_submit.py +0 -139
- package/dist/templates/_shared/lib/__init__.py +0 -1
- package/dist/templates/_shared/lib/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__init__.py +0 -65
- 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__/logger.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/atomic_write.py +0 -180
- package/dist/templates/_shared/lib/base/constants.py +0 -358
- package/dist/templates/_shared/lib/base/hook_utils.py +0 -339
- package/dist/templates/_shared/lib/base/inference.py +0 -307
- package/dist/templates/_shared/lib/base/logger.py +0 -305
- package/dist/templates/_shared/lib/base/stop_words.py +0 -221
- package/dist/templates/_shared/lib/base/subprocess_utils.py +0 -46
- package/dist/templates/_shared/lib/base/utils.py +0 -263
- package/dist/templates/_shared/lib/context/__init__.py +0 -102
- 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_formatter.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__/context_selector.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/context_store.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__/plan_manager.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/__pycache__/task_tracker.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/context_formatter.py +0 -317
- package/dist/templates/_shared/lib/context/context_selector.py +0 -508
- package/dist/templates/_shared/lib/context/context_store.py +0 -653
- package/dist/templates/_shared/lib/context/plan_manager.py +0 -303
- package/dist/templates/_shared/lib/context/task_tracker.py +0 -188
- package/dist/templates/_shared/lib/handoff/__init__.py +0 -22
- package/dist/templates/_shared/lib/handoff/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/handoff/__pycache__/document_generator.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/handoff/document_generator.py +0 -278
- package/dist/templates/_shared/lib/templates/README.md +0 -206
- package/dist/templates/_shared/lib/templates/__init__.py +0 -36
- 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/_shared/lib/templates/formatters.py +0 -146
- package/dist/templates/_shared/lib/templates/plan_context.py +0 -73
- package/dist/templates/_shared/scripts/__pycache__/save_handoff.cpython-313.pyc +0 -0
- package/dist/templates/_shared/scripts/__pycache__/status_line.cpython-313.pyc +0 -0
- package/dist/templates/_shared/scripts/save_handoff.py +0 -357
- package/dist/templates/_shared/scripts/status_line.py +0 -716
- package/dist/templates/cc-native/.claude/commands/cc-native/fresh-perspective.md +0 -8
- package/dist/templates/cc-native/.windsurf/workflows/cc-native/fresh-perspective.md +0 -8
- package/dist/templates/cc-native/MIGRATION.md +0 -86
- 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__/mark_questions_asked.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/plan_accepted.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/plan_questions_early.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 +0 -130
- package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.py +0 -954
- package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.py +0 -81
- package/dist/templates/cc-native/_cc-native/hooks/suggest-fresh-perspective.py +0 -340
- package/dist/templates/cc-native/_cc-native/lib/CLAUDE.md +0 -265
- package/dist/templates/cc-native/_cc-native/lib/__init__.py +0 -53
- 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/constants.py +0 -45
- package/dist/templates/cc-native/_cc-native/lib/debug.py +0 -139
- package/dist/templates/cc-native/_cc-native/lib/orchestrator.py +0 -362
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__init__.py +0 -28
- 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 +0 -215
- package/dist/templates/cc-native/_cc-native/lib/reviewers/base.py +0 -88
- package/dist/templates/cc-native/_cc-native/lib/reviewers/codex.py +0 -124
- package/dist/templates/cc-native/_cc-native/lib/reviewers/gemini.py +0 -108
- package/dist/templates/cc-native/_cc-native/lib/state.py +0 -268
- package/dist/templates/cc-native/_cc-native/lib/utils.py +0 -1071
- package/dist/templates/cc-native/_cc-native/scripts/__pycache__/aggregate_agents.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/scripts/aggregate_agents.py +0 -168
- package/dist/templates/cc-native/_cc-native/workflows/fresh-perspective.md +0 -134
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
* See SPEC.md §5.10
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { execSync, execFile } from "node:child_process";
|
|
7
|
+
|
|
6
8
|
/**
|
|
7
9
|
* Check if this is an internal subprocess call.
|
|
8
10
|
* All hooks should check this and return early to prevent recursion.
|
|
@@ -21,3 +23,143 @@ export function getInternalSubprocessEnv(): Record<string, string | undefined> {
|
|
|
21
23
|
AIWCLI_INTERNAL_CALL: "true",
|
|
22
24
|
};
|
|
23
25
|
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Find an executable on the system PATH.
|
|
29
|
+
* Uses `where` on Windows, `which` on Unix.
|
|
30
|
+
* On Windows, prefers .cmd/.exe over extensionless shims since
|
|
31
|
+
* execFileSync cannot spawn extensionless shell scripts.
|
|
32
|
+
* Returns the first match or null if not found.
|
|
33
|
+
*/
|
|
34
|
+
export function findExecutable(name: string): string | null {
|
|
35
|
+
try {
|
|
36
|
+
const cmd = process.platform === "win32" ? `where ${name}` : `which ${name}`;
|
|
37
|
+
const lines = execSync(cmd, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], shell: true })
|
|
38
|
+
.trim()
|
|
39
|
+
.split(/\r?\n/)
|
|
40
|
+
.map((l) => l.trim())
|
|
41
|
+
.filter(Boolean);
|
|
42
|
+
|
|
43
|
+
if (lines.length === 0) return null;
|
|
44
|
+
|
|
45
|
+
// On Windows, `where` may return an extensionless shim first (e.g. npm creates
|
|
46
|
+
// both `claude` and `claude.cmd`). execFileSync can't spawn the extensionless
|
|
47
|
+
// one, so prefer .cmd or .exe.
|
|
48
|
+
if (process.platform === "win32") {
|
|
49
|
+
const preferred = lines.find((l) => /\.(cmd|exe)$/i.test(l));
|
|
50
|
+
return preferred ?? lines[0] ?? null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return lines[0] ?? null;
|
|
54
|
+
} catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Type guard for Node.js child_process exec errors.
|
|
61
|
+
* ExecSync throws objects with these extra properties on non-zero exit or timeout.
|
|
62
|
+
*/
|
|
63
|
+
export interface ExecSyncError {
|
|
64
|
+
killed: boolean;
|
|
65
|
+
signal: string | null;
|
|
66
|
+
stdout: Buffer | string;
|
|
67
|
+
stderr: Buffer | string;
|
|
68
|
+
status: number | null;
|
|
69
|
+
message: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Check if an unknown error is an ExecSync error with process info. */
|
|
73
|
+
export function isExecSyncError(e: unknown): e is ExecSyncError {
|
|
74
|
+
return (
|
|
75
|
+
typeof e === "object" &&
|
|
76
|
+
e !== null &&
|
|
77
|
+
"killed" in e &&
|
|
78
|
+
"signal" in e
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
// Async Subprocess Execution
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Result from an async subprocess execution.
|
|
88
|
+
* Never throws — callers inspect fields to determine outcome.
|
|
89
|
+
*/
|
|
90
|
+
export interface ExecResult {
|
|
91
|
+
stdout: string;
|
|
92
|
+
stderr: string;
|
|
93
|
+
exitCode: number;
|
|
94
|
+
killed: boolean;
|
|
95
|
+
signal: string | null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Options for execFileAsync. */
|
|
99
|
+
export interface ExecAsyncOptions {
|
|
100
|
+
/** Data piped to the child's stdin. */
|
|
101
|
+
input?: string;
|
|
102
|
+
/** Timeout in milliseconds (not seconds). */
|
|
103
|
+
timeout?: number;
|
|
104
|
+
/** Environment variables for the child process. */
|
|
105
|
+
env?: Record<string, string | undefined>;
|
|
106
|
+
/** Maximum bytes on stdout/stderr. Default: 10 MB. */
|
|
107
|
+
maxBuffer?: number;
|
|
108
|
+
/** Use shell for execution. Required on Windows for .cmd files. */
|
|
109
|
+
shell?: boolean;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Async subprocess execution that does NOT block the event loop.
|
|
114
|
+
* Drop-in replacement for execFileSync in Promise-based parallel patterns.
|
|
115
|
+
*
|
|
116
|
+
* Returns ExecResult on both success and non-zero exit.
|
|
117
|
+
* On timeout: result.killed = true, result.signal = "SIGTERM".
|
|
118
|
+
* On spawn failure: result.exitCode = -1, result.stderr contains error.
|
|
119
|
+
*/
|
|
120
|
+
export function execFileAsync(
|
|
121
|
+
file: string,
|
|
122
|
+
args: string[],
|
|
123
|
+
options?: ExecAsyncOptions,
|
|
124
|
+
): Promise<ExecResult> {
|
|
125
|
+
return new Promise((resolve) => {
|
|
126
|
+
const child = execFile(
|
|
127
|
+
file,
|
|
128
|
+
args,
|
|
129
|
+
{
|
|
130
|
+
encoding: "utf-8",
|
|
131
|
+
timeout: options?.timeout ?? 0,
|
|
132
|
+
env: options?.env as NodeJS.ProcessEnv,
|
|
133
|
+
maxBuffer: options?.maxBuffer ?? 10 * 1024 * 1024,
|
|
134
|
+
shell: options?.shell,
|
|
135
|
+
},
|
|
136
|
+
(error, stdout, stderr) => {
|
|
137
|
+
if (error) {
|
|
138
|
+
// execFile callback error includes process exit info
|
|
139
|
+
const errObj = error as unknown as Record<string, unknown>;
|
|
140
|
+
resolve({
|
|
141
|
+
stdout: String(stdout ?? ""),
|
|
142
|
+
stderr: String(stderr ?? ""),
|
|
143
|
+
exitCode: typeof errObj.code === "number" ? errObj.code : (error as any).status ?? 1,
|
|
144
|
+
killed: Boolean(errObj.killed),
|
|
145
|
+
signal: typeof errObj.signal === "string" ? errObj.signal : null,
|
|
146
|
+
});
|
|
147
|
+
} else {
|
|
148
|
+
resolve({
|
|
149
|
+
stdout: String(stdout ?? ""),
|
|
150
|
+
stderr: String(stderr ?? ""),
|
|
151
|
+
exitCode: 0,
|
|
152
|
+
killed: false,
|
|
153
|
+
signal: null,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
// Pipe input to stdin if provided
|
|
160
|
+
if (options?.input != null && child.stdin) {
|
|
161
|
+
child.stdin.write(options.input);
|
|
162
|
+
child.stdin.end();
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
}
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { sanitizeTitle } from "./constants.js";
|
|
7
|
+
import { logDebug, logError, logWarn } from "./logger.js";
|
|
7
8
|
import { STOP_WORDS } from "./stop-words.js";
|
|
8
|
-
import { logDebug, logWarn, logError } from "./logger.js";
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Print to stderr. For terminal-only UX messages, not diagnostics.
|
|
@@ -77,22 +77,79 @@ export function parseIsoTimestamp(isoStr: string): Date | null {
|
|
|
77
77
|
export function cleanTextForSlug(text: string): string {
|
|
78
78
|
if (!text) return "";
|
|
79
79
|
let result = text.toLowerCase();
|
|
80
|
-
result = result.
|
|
81
|
-
result = result.
|
|
82
|
-
result = result.
|
|
80
|
+
result = result.replaceAll('\'', ""); // i'm -> im, you're -> youre
|
|
81
|
+
result = result.replaceAll(/[^a-z0-9\s]/g, " "); // punctuation -> spaces
|
|
82
|
+
result = result.replaceAll(/\s+/g, " ").trim();
|
|
83
83
|
return result;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
/**
|
|
87
|
+
* Generate a slug from text using AI inference with stop-word fallbacks.
|
|
88
|
+
* Pipeline: AI inference → stop-word post-filter → stop-word fallback → word-length fallback.
|
|
89
|
+
* Reusable by both context ID generation and plan archival.
|
|
90
|
+
* See SPEC.md §14.2
|
|
91
|
+
*/
|
|
92
|
+
export function generateSlug(
|
|
93
|
+
text: string,
|
|
94
|
+
maxLen = 150,
|
|
95
|
+
fallbackSlug = "context",
|
|
96
|
+
): string {
|
|
97
|
+
if (!text || !text.trim()) return fallbackSlug;
|
|
98
|
+
|
|
99
|
+
let slug: null | string = null;
|
|
100
|
+
const cleanedText = cleanTextForSlug(text);
|
|
101
|
+
|
|
102
|
+
// Tier 1: AI inference via generateContextIdSlug (sync — uses execFileSync)
|
|
103
|
+
try {
|
|
104
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports, no-undef
|
|
105
|
+
const { generateContextIdSlug } = require("./inference.js");
|
|
106
|
+
const aiSlug = generateContextIdSlug(text);
|
|
107
|
+
if (aiSlug) {
|
|
108
|
+
const filteredWords = aiSlug
|
|
109
|
+
.split(/\s+/)
|
|
110
|
+
.filter(
|
|
111
|
+
(w: string) => !STOP_WORDS.has(w.toLowerCase()) && w.length > 1,
|
|
112
|
+
);
|
|
113
|
+
if (filteredWords.length >= 5) {
|
|
114
|
+
slug = sanitizeTitle(filteredWords.join(" "), maxLen);
|
|
115
|
+
} else {
|
|
116
|
+
logDebug(
|
|
117
|
+
"utils",
|
|
118
|
+
`AI slug too generic after stop-word filter (${filteredWords.length} words remain), using fallback`,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
} catch (error: any) {
|
|
123
|
+
logWarn("utils", `AI slug generation failed, using fallback: ${error}`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Tier 2: Stop-word filtering on cleaned text
|
|
127
|
+
if (!slug) {
|
|
128
|
+
const words = cleanedText
|
|
129
|
+
.split(/\s+/)
|
|
130
|
+
.filter((w) => !STOP_WORDS.has(w) && w.length > 1)
|
|
131
|
+
.slice(0, 12);
|
|
132
|
+
slug = words.length >= 3
|
|
133
|
+
? sanitizeTitle(words.join(" "), maxLen)
|
|
134
|
+
: sanitizeTitle(
|
|
135
|
+
cleanedText.split(/\s+/).filter((w) => w.length > 2).slice(0, 6).join(" "),
|
|
136
|
+
maxLen,
|
|
137
|
+
) || fallbackSlug;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return slug;
|
|
141
|
+
}
|
|
142
|
+
|
|
86
143
|
/**
|
|
87
144
|
* Generate a context ID from a summary string.
|
|
88
145
|
* Format: YYMMDD-HHMM-slug
|
|
89
|
-
*
|
|
146
|
+
* Delegates slug generation to generateSlug().
|
|
90
147
|
* See SPEC.md §14.2
|
|
91
148
|
*/
|
|
92
|
-
export
|
|
149
|
+
export function generateContextId(
|
|
93
150
|
summary: string,
|
|
94
151
|
existingIds?: Set<string>,
|
|
95
|
-
):
|
|
152
|
+
): string {
|
|
96
153
|
const now = new Date();
|
|
97
154
|
const yy = String(now.getFullYear()).slice(2);
|
|
98
155
|
const mm = String(now.getMonth() + 1).padStart(2, "0");
|
|
@@ -104,70 +161,12 @@ export async function generateContextId(
|
|
|
104
161
|
let baseId: string;
|
|
105
162
|
|
|
106
163
|
try {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
let slug: string | null = null;
|
|
111
|
-
// Pre-clean for Tier 2/3: strip punctuation so stop words match
|
|
112
|
-
const cleanedSummary = cleanTextForSlug(summary);
|
|
113
|
-
|
|
114
|
-
// Tier 1: AI inference (imported dynamically to avoid circular deps)
|
|
115
|
-
try {
|
|
116
|
-
const { generateContextIdSlug } = await import("./inference.js");
|
|
117
|
-
const aiSlug = generateContextIdSlug(summary);
|
|
118
|
-
if (aiSlug) {
|
|
119
|
-
const filteredWords = aiSlug
|
|
120
|
-
.split(/\s+/)
|
|
121
|
-
.filter(
|
|
122
|
-
(w: string) => !STOP_WORDS.has(w.toLowerCase()) && w.length > 1,
|
|
123
|
-
);
|
|
124
|
-
if (filteredWords.length >= 5) {
|
|
125
|
-
slug = sanitizeTitle(filteredWords.join(" "), 150);
|
|
126
|
-
} else {
|
|
127
|
-
logDebug(
|
|
128
|
-
"utils",
|
|
129
|
-
`AI slug too generic after stop-word filter (${filteredWords.length} words remain), using fallback`,
|
|
130
|
-
);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
} catch (e: any) {
|
|
134
|
-
logWarn("utils", `AI context ID slug failed, using fallback: ${e}`);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// Tier 2: Stop-word filtering on cleaned text
|
|
138
|
-
if (!slug) {
|
|
139
|
-
try {
|
|
140
|
-
const words = cleanedSummary
|
|
141
|
-
.split(/\s+/)
|
|
142
|
-
.filter((w) => !STOP_WORDS.has(w) && w.length > 1)
|
|
143
|
-
.slice(0, 12);
|
|
144
|
-
if (words.length >= 3) {
|
|
145
|
-
slug = sanitizeTitle(words.join(" "), 150);
|
|
146
|
-
} else {
|
|
147
|
-
logDebug("utils", `Tier 2 too few content words (${words.length}), falling through to Tier 3`);
|
|
148
|
-
}
|
|
149
|
-
} catch (e: any) {
|
|
150
|
-
logWarn("utils", `Stop-word fallback failed: ${e}`);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// Tier 3: Simple word-length filter on cleaned text
|
|
155
|
-
if (!slug || slug === "unknown") {
|
|
156
|
-
const words = cleanedSummary
|
|
157
|
-
.split(/\s+/)
|
|
158
|
-
.filter((w) => w.length > 2)
|
|
159
|
-
.slice(0, 6);
|
|
160
|
-
slug = words.length > 0
|
|
161
|
-
? sanitizeTitle(words.join(" "), 150)
|
|
162
|
-
: "context";
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
baseId = `${timestamp}-${slug}`;
|
|
166
|
-
}
|
|
167
|
-
} catch (e: any) {
|
|
164
|
+
const slug = generateSlug(summary);
|
|
165
|
+
baseId = `${timestamp}-${slug}`;
|
|
166
|
+
} catch (error: any) {
|
|
168
167
|
logError(
|
|
169
168
|
"utils",
|
|
170
|
-
`Context ID generation failed entirely, using timestamp: ${
|
|
169
|
+
`Context ID generation failed entirely, using timestamp: ${error}`,
|
|
171
170
|
);
|
|
172
171
|
baseId = `${timestamp}-context`;
|
|
173
172
|
}
|
|
@@ -180,5 +179,6 @@ export async function generateContextId(
|
|
|
180
179
|
while (existingIds.has(`${baseId}-${counter}`)) {
|
|
181
180
|
counter++;
|
|
182
181
|
}
|
|
182
|
+
|
|
183
183
|
return `${baseId}-${counter}`;
|
|
184
184
|
}
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import * as fs from "node:fs";
|
|
11
|
-
import * as
|
|
11
|
+
import * as _path from "node:path";
|
|
12
|
+
|
|
12
13
|
import { parseIsoTimestamp } from "../base/utils.js";
|
|
13
14
|
import type { ContextState, Task } from "../types.js";
|
|
14
15
|
|
|
@@ -41,10 +42,10 @@ export function getModeDisplay(mode: string): string {
|
|
|
41
42
|
* Format ISO timestamp as '2 hours ago', 'yesterday', etc.
|
|
42
43
|
* See SPEC.md §11.3
|
|
43
44
|
*/
|
|
44
|
-
export function formatRelativeTime(isoTimestamp:
|
|
45
|
+
export function formatRelativeTime(isoTimestamp: null | string): string {
|
|
45
46
|
if (!isoTimestamp) return "unknown";
|
|
46
47
|
|
|
47
|
-
|
|
48
|
+
const dt = parseIsoTimestamp(isoTimestamp);
|
|
48
49
|
if (!dt) return isoTimestamp.slice(0, 16);
|
|
49
50
|
|
|
50
51
|
const now = new Date();
|
|
@@ -61,8 +62,10 @@ export function formatRelativeTime(isoTimestamp: string | null): string {
|
|
|
61
62
|
if (diffMin === 0) return "just now";
|
|
62
63
|
return diffMin === 1 ? "1 minute ago" : `${diffMin} minutes ago`;
|
|
63
64
|
}
|
|
65
|
+
|
|
64
66
|
return diffHours === 1 ? "1 hour ago" : `${diffHours} hours ago`;
|
|
65
67
|
}
|
|
68
|
+
|
|
66
69
|
if (diffDays === 1) return "yesterday";
|
|
67
70
|
if (diffDays < 7) return `${diffDays} days ago`;
|
|
68
71
|
|
|
@@ -77,21 +80,23 @@ export function formatRelativeTime(isoTimestamp: string | null): string {
|
|
|
77
80
|
// Internal helpers
|
|
78
81
|
// ---------------------------------------------------------------------------
|
|
79
82
|
|
|
80
|
-
function taskAttr(task:
|
|
83
|
+
function taskAttr(task: Record<string, any> | Task, key: string, defaultVal = ""): string {
|
|
81
84
|
if (typeof task === "object" && task !== null) {
|
|
82
85
|
return (task as any)[key] ?? defaultVal;
|
|
83
86
|
}
|
|
87
|
+
|
|
84
88
|
return defaultVal;
|
|
85
89
|
}
|
|
86
90
|
|
|
87
|
-
function readPlanContent(planPath: string): [
|
|
91
|
+
function readPlanContent(planPath: string): [null | string, boolean, number] {
|
|
88
92
|
try {
|
|
89
93
|
if (!fs.existsSync(planPath)) return [null, false, 0];
|
|
90
|
-
const content = fs.readFileSync(planPath, "
|
|
94
|
+
const content = fs.readFileSync(planPath, "utf8");
|
|
91
95
|
const total = content.length;
|
|
92
96
|
if (total > MAX_PLAN_INLINE_CHARS) {
|
|
93
97
|
return [content.slice(0, MAX_PLAN_INLINE_CHARS), true, total];
|
|
94
98
|
}
|
|
99
|
+
|
|
95
100
|
return [content, false, total];
|
|
96
101
|
} catch {
|
|
97
102
|
return [null, false, 0];
|
|
@@ -100,7 +105,7 @@ function readPlanContent(planPath: string): [string | null, boolean, number] {
|
|
|
100
105
|
|
|
101
106
|
function modeLabel(ctx: ContextState): string {
|
|
102
107
|
const d = getModeDisplay(ctx.mode ?? "idle");
|
|
103
|
-
return d ? d.
|
|
108
|
+
return d ? d.replaceAll(/^\[|\]$/g, "") : "Active";
|
|
104
109
|
}
|
|
105
110
|
|
|
106
111
|
/**
|
|
@@ -119,7 +124,7 @@ export function buildRestoreSections(
|
|
|
119
124
|
const savedAt = lastSession.saved_at ?? "";
|
|
120
125
|
if (savedAt) {
|
|
121
126
|
const reason = lastSession.save_reason ?? "";
|
|
122
|
-
const reasonDisplay = reason ? reason.
|
|
127
|
+
const reasonDisplay = reason ? reason.replaceAll('_', " ") : "unknown";
|
|
123
128
|
sections.push(`**Last session ended:** ${formatRelativeTime(savedAt)} (${reasonDisplay})`);
|
|
124
129
|
}
|
|
125
130
|
}
|
|
@@ -138,6 +143,7 @@ export function buildRestoreSections(
|
|
|
138
143
|
buckets[s]!.push(taskAttr(t, "subject"));
|
|
139
144
|
}
|
|
140
145
|
}
|
|
146
|
+
|
|
141
147
|
if (Object.values(buckets).some(b => b.length > 0)) {
|
|
142
148
|
sections.push("", `### Previous Work (${tasks.length} tasks)`, "");
|
|
143
149
|
const marks: Record<string, string> = {
|
|
@@ -195,8 +201,7 @@ function resumeBlock(ctx: ContextState, projectRoot: string | undefined, modeTex
|
|
|
195
201
|
];
|
|
196
202
|
const restore = buildRestoreSections(ctx, projectRoot, true);
|
|
197
203
|
if (restore) lines.push(restore);
|
|
198
|
-
lines.push("", "---", "", "**Instructions:**");
|
|
199
|
-
lines.push(...instructions);
|
|
204
|
+
lines.push("", "---", "", "**Instructions:**", ...instructions);
|
|
200
205
|
return lines.join("\n");
|
|
201
206
|
}
|
|
202
207
|
|
|
@@ -218,12 +223,12 @@ export function formatHandoffContinuation(ctx: ContextState, projectRoot?: strin
|
|
|
218
223
|
|
|
219
224
|
try {
|
|
220
225
|
if (handoffPath && fs.existsSync(handoffPath)) {
|
|
221
|
-
lines.push("### Previous Session Handoff", "", fs.readFileSync(handoffPath, "
|
|
226
|
+
lines.push("### Previous Session Handoff", "", fs.readFileSync(handoffPath, "utf8"), "");
|
|
222
227
|
} else {
|
|
223
228
|
lines.push(`*Handoff document not found at \`${handoffPath}\`*`, "");
|
|
224
229
|
}
|
|
225
|
-
} catch (
|
|
226
|
-
lines.push(`*Handoff document at \`${handoffPath}\` could not be read: ${
|
|
230
|
+
} catch (error: any) {
|
|
231
|
+
lines.push(`*Handoff document at \`${handoffPath}\` could not be read: ${error}*`, "");
|
|
227
232
|
}
|
|
228
233
|
|
|
229
234
|
const restore = buildRestoreSections(ctx, projectRoot, true);
|
|
@@ -266,20 +271,21 @@ export function formatContextList(contexts: ContextState[]): string {
|
|
|
266
271
|
if (contexts.length === 0) return "No active contexts found.";
|
|
267
272
|
|
|
268
273
|
const lines = ["## Active Contexts\n"];
|
|
269
|
-
for (
|
|
270
|
-
const ctx =
|
|
274
|
+
for (const [i, context_] of contexts.entries()) {
|
|
275
|
+
const ctx = context_!;
|
|
271
276
|
const timeStr = formatRelativeTime(ctx.last_active);
|
|
272
277
|
const md = getModeDisplay(ctx.mode ?? "idle");
|
|
273
278
|
const si = md ? ` ${md}` : "";
|
|
274
|
-
lines.push(`**${i + 1}. ${ctx.id}**${si}`);
|
|
275
|
-
lines.push(` ${ctx.summary}`);
|
|
279
|
+
lines.push(`**${i + 1}. ${ctx.id}**${si}`, ` ${ctx.summary}`);
|
|
276
280
|
if (ctx.method) {
|
|
277
281
|
lines.push(` Method: ${ctx.method} | Last active: ${timeStr}`);
|
|
278
282
|
} else {
|
|
279
283
|
lines.push(` Last active: ${timeStr}`);
|
|
280
284
|
}
|
|
285
|
+
|
|
281
286
|
lines.push("");
|
|
282
287
|
}
|
|
288
|
+
|
|
283
289
|
return lines.join("\n");
|
|
284
290
|
}
|
|
285
291
|
|
|
@@ -349,11 +355,11 @@ export function formatContextPickerStderr(contexts: ContextState[]): string {
|
|
|
349
355
|
];
|
|
350
356
|
|
|
351
357
|
let selectableCount = 0;
|
|
352
|
-
for (
|
|
353
|
-
const ctx =
|
|
358
|
+
for (const [i, context_] of contexts.entries()) {
|
|
359
|
+
const ctx = context_!;
|
|
354
360
|
const timeStr = formatRelativeTime(ctx.last_active);
|
|
355
361
|
const mode = ctx.mode ?? "idle";
|
|
356
|
-
const isSelectable = mode === "active" ||
|
|
362
|
+
const isSelectable = mode === "active" || Boolean(ctx.handoff_path);
|
|
357
363
|
if (isSelectable) selectableCount++;
|
|
358
364
|
|
|
359
365
|
let status = "";
|
|
@@ -366,10 +372,7 @@ export function formatContextPickerStderr(contexts: ContextState[]): string {
|
|
|
366
372
|
const summary = ctx.summary.length > 48 ? ctx.summary.slice(0, 45) + "..." : ctx.summary;
|
|
367
373
|
const selTag = isSelectable ? " [selectable]" : " [end only]";
|
|
368
374
|
|
|
369
|
-
lines.push(`| ^${i + 1} ${ctx.id}${status}${selTag}
|
|
370
|
-
lines.push(`| ${summary}`);
|
|
371
|
-
lines.push(`| [${timeStr}]`);
|
|
372
|
-
lines.push("|");
|
|
375
|
+
lines.push(`| ^${i + 1} ${ctx.id}${status}${selTag}`, `| ${summary}`, `| [${timeStr}]`, "|");
|
|
373
376
|
}
|
|
374
377
|
|
|
375
378
|
lines.push(
|
|
@@ -394,6 +397,7 @@ export function formatContextPickerStderr(contexts: ContextState[]): string {
|
|
|
394
397
|
"+----------------------------------------------------------------+",
|
|
395
398
|
);
|
|
396
399
|
}
|
|
400
|
+
|
|
397
401
|
lines.push("");
|
|
398
402
|
return lines.join("\n");
|
|
399
403
|
}
|
|
@@ -413,6 +417,7 @@ export function formatCommandFeedback(
|
|
|
413
417
|
const s = ctx.summary.length > 50 ? ctx.summary.slice(0, 50) + "..." : ctx.summary;
|
|
414
418
|
lines.push(`- **${ctx.id}**: ${s}`);
|
|
415
419
|
}
|
|
420
|
+
|
|
416
421
|
lines.push("");
|
|
417
422
|
}
|
|
418
423
|
|
|
@@ -428,5 +433,6 @@ export function formatCommandFeedback(
|
|
|
428
433
|
"Tasks created with TaskCreate will be persisted to this context.",
|
|
429
434
|
);
|
|
430
435
|
}
|
|
436
|
+
|
|
431
437
|
return lines.join("\n");
|
|
432
438
|
}
|