aiwcli 0.11.0 → 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.
Files changed (25) hide show
  1. package/dist/templates/_shared/hooks-ts/session_end.ts +4 -1
  2. package/dist/templates/_shared/lib-ts/base/git-state.ts +1 -1
  3. package/dist/templates/_shared/lib-ts/base/logger.ts +15 -18
  4. package/dist/templates/_shared/lib-ts/base/subprocess-utils.ts +18 -15
  5. package/dist/templates/_shared/lib-ts/context/plan-manager.ts +26 -30
  6. package/dist/templates/_shared/lib-ts/handoff/handoff-reader.ts +12 -13
  7. package/dist/templates/_shared/scripts/resume_handoff.ts +62 -38
  8. package/dist/templates/_shared/scripts/save_handoff.ts +24 -24
  9. package/dist/templates/_shared/scripts/status_line.ts +101 -147
  10. package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.ts +239 -133
  11. package/dist/templates/cc-native/_cc-native/lib-ts/aggregate-agents.ts +11 -12
  12. package/dist/templates/cc-native/_cc-native/lib-ts/artifacts.ts +139 -56
  13. package/dist/templates/cc-native/_cc-native/lib-ts/cli-output-parser.ts +22 -2
  14. package/dist/templates/cc-native/_cc-native/lib-ts/corroboration.ts +115 -0
  15. package/dist/templates/cc-native/_cc-native/lib-ts/index.ts +1 -0
  16. package/dist/templates/cc-native/_cc-native/lib-ts/json-parser.ts +7 -1
  17. package/dist/templates/cc-native/_cc-native/lib-ts/orchestrator.ts +5 -4
  18. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/agent.ts +133 -13
  19. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/codex.ts +6 -6
  20. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/gemini.ts +5 -4
  21. package/dist/templates/cc-native/_cc-native/lib-ts/state.ts +30 -33
  22. package/dist/templates/cc-native/_cc-native/lib-ts/types.ts +118 -43
  23. package/dist/templates/cc-native/_cc-native/plan-review.config.json +21 -0
  24. package/oclif.manifest.json +1 -1
  25. package/package.json +2 -2
@@ -69,7 +69,10 @@ function main(): void {
69
69
  state.plan_signature = content.slice(0, 200);
70
70
  state.plan_id = generatePlanId();
71
71
  state.plan_anchors = extractPlanAnchors(content);
72
- state.plan_consumed = false;
72
+ // Preserve plan_consumed if already true (plan was implemented) —
73
+ // resetting it would re-stage the plan and block handoff staging.
74
+ // Only set to false when no prior consumption has occurred.
75
+ state.plan_consumed = state.plan_consumed || false;
73
76
 
74
77
  logInfo("session_end", `Assigned plan fallback: hash=${planHash}, path=${latestPlanPath}`);
75
78
  } catch (error) {
@@ -23,7 +23,7 @@ export function getGitState(projectRoot: string): Record<string, any> {
23
23
  try {
24
24
  const status = execFileSync("git", ["status", "--short"], opts);
25
25
  if (status) {
26
- const files = status.trim().split("\n")
26
+ const files = status.trim().split(/\r?\n/)
27
27
  .filter(Boolean)
28
28
  .slice(0, 10)
29
29
  .map(line => line.trimStart().split(/\s+/).slice(1).join(" "));
@@ -32,32 +32,31 @@ const LEVELS: Record<string, number> = {
32
32
  const MAX_LOG_LINES = 10_000; // Max lines in global log before pruning
33
33
 
34
34
  // Module-level session ID cache
35
- let _cachedSessionId: null | string = null;
35
+ let _cachedSessionId: string | null = null;
36
36
 
37
37
  // Module-level context path cache (kept for external callers)
38
- let _cachedContextPath: null | string = null;
38
+ let _cachedContextPath: string | null = null;
39
39
  let _contextResolved = false;
40
40
 
41
41
  /**
42
42
  * Set the session ID for this process. All subsequent log calls include it.
43
43
  */
44
- export function setSessionId(sessionId: null | string): void {
44
+ export function setSessionId(sessionId: string | null): void {
45
45
  _cachedSessionId = sessionId;
46
46
  }
47
47
 
48
48
  /**
49
49
  * Set the context path for this process. Kept for external callers.
50
50
  */
51
- export function setContextPath(contextPath: null | string): void {
51
+ export function setContextPath(contextPath: string | null): void {
52
52
  _cachedContextPath = contextPath;
53
53
  _contextResolved = true;
54
54
  }
55
55
 
56
- export function getContextPath(): null | string {
56
+ export function getContextPath(): string | null {
57
57
  if (!_contextResolved) {
58
58
  _contextResolved = true; // Don't retry
59
- }
60
-
59
+ }
61
60
  return _cachedContextPath;
62
61
  }
63
62
 
@@ -91,8 +90,8 @@ export function hookLog(
91
90
  opts?: {
92
91
  component?: string;
93
92
  data?: any;
94
- stderr?: boolean;
95
93
  traceback_str?: string;
94
+ stderr?: boolean;
96
95
  },
97
96
  ): void {
98
97
  try {
@@ -137,8 +136,7 @@ export function hookLog(
137
136
  } catch {
138
137
  entry.data = String(opts.data);
139
138
  }
140
- }
141
-
139
+ }
142
140
  if (tracebackStr) entry.tb = tracebackStr.trimEnd();
143
141
 
144
142
  const line = JSON.stringify(entry) + "\n";
@@ -153,8 +151,8 @@ export function hookLog(
153
151
  // Line-count guard: prune to last MAX_LOG_LINES
154
152
  try {
155
153
  if (fs.existsSync(logPath)) {
156
- const content = fs.readFileSync(logPath, "utf8");
157
- const lines = content.split("\n");
154
+ const content = fs.readFileSync(logPath, "utf-8");
155
+ const lines = content.split(/\r?\n/);
158
156
  if (lines.length > MAX_LOG_LINES) {
159
157
  fs.writeFileSync(
160
158
  logPath,
@@ -206,11 +204,11 @@ export function logDiagnostic(
206
204
  phase: string,
207
205
  summary: string,
208
206
  opts?: {
209
- component?: string;
210
- data?: any;
211
- decision?: any;
212
207
  inputs?: any;
208
+ decision?: any;
213
209
  reasoning?: any;
210
+ component?: string;
211
+ data?: any;
214
212
  },
215
213
  ): void {
216
214
  const diagData: Record<string, any> = { phase };
@@ -219,8 +217,7 @@ export function logDiagnostic(
219
217
  if (opts?.reasoning !== undefined) diagData.reasoning = opts.reasoning;
220
218
  if (opts?.data && typeof opts.data === "object") {
221
219
  Object.assign(diagData, opts.data);
222
- }
223
-
220
+ }
224
221
  hookLog("debug", hookName, `[DIAG:${phase}] ${summary}`, {
225
222
  component: opts?.component ?? "diag",
226
223
  data: diagData,
@@ -238,7 +235,7 @@ export function logHookError(
238
235
  tracebackStr = "",
239
236
  ): void {
240
237
  const errStr = typeof error === "string" ? error : String(error);
241
- const msg = errStr.replaceAll(/[\n\r]/g, " ").slice(0, 200);
238
+ const msg = errStr.replace(/[\n\r]/g, " ").slice(0, 200);
242
239
  const errType =
243
240
  typeof error === "object" && error !== null
244
241
  ? error.constructor.name
@@ -3,7 +3,7 @@
3
3
  * See SPEC.md §5.10
4
4
  */
5
5
 
6
- import { execFile, execSync } from "node:child_process";
6
+ import { execSync, execFile } from "node:child_process";
7
7
 
8
8
  /**
9
9
  * Check if this is an internal subprocess call.
@@ -31,10 +31,10 @@ export function getInternalSubprocessEnv(): Record<string, string | undefined> {
31
31
  * execFileSync cannot spawn extensionless shell scripts.
32
32
  * Returns the first match or null if not found.
33
33
  */
34
- export function findExecutable(name: string): null | string {
34
+ export function findExecutable(name: string): string | null {
35
35
  try {
36
36
  const cmd = process.platform === "win32" ? `where ${name}` : `which ${name}`;
37
- const lines = execSync(cmd, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] })
37
+ const lines = execSync(cmd, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], shell: true })
38
38
  .trim()
39
39
  .split(/\r?\n/)
40
40
  .map((l) => l.trim())
@@ -62,11 +62,11 @@ export function findExecutable(name: string): null | string {
62
62
  */
63
63
  export interface ExecSyncError {
64
64
  killed: boolean;
65
- message: string;
66
- signal: null | string;
67
- status: null | number;
68
- stderr: Buffer | string;
65
+ signal: string | null;
69
66
  stdout: Buffer | string;
67
+ stderr: Buffer | string;
68
+ status: number | null;
69
+ message: string;
70
70
  }
71
71
 
72
72
  /** Check if an unknown error is an ExecSync error with process info. */
@@ -88,23 +88,25 @@ export function isExecSyncError(e: unknown): e is ExecSyncError {
88
88
  * Never throws — callers inspect fields to determine outcome.
89
89
  */
90
90
  export interface ExecResult {
91
+ stdout: string;
92
+ stderr: string;
91
93
  exitCode: number;
92
94
  killed: boolean;
93
- signal: null | string;
94
- stderr: string;
95
- stdout: string;
95
+ signal: string | null;
96
96
  }
97
97
 
98
98
  /** Options for execFileAsync. */
99
99
  export interface ExecAsyncOptions {
100
- /** Environment variables for the child process. */
101
- env?: Record<string, string | undefined>;
102
100
  /** Data piped to the child's stdin. */
103
101
  input?: string;
104
- /** Maximum bytes on stdout/stderr. Default: 10 MB. */
105
- maxBuffer?: number;
106
102
  /** Timeout in milliseconds (not seconds). */
107
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;
108
110
  }
109
111
 
110
112
  /**
@@ -129,6 +131,7 @@ export function execFileAsync(
129
131
  timeout: options?.timeout ?? 0,
130
132
  env: options?.env as NodeJS.ProcessEnv,
131
133
  maxBuffer: options?.maxBuffer ?? 10 * 1024 * 1024,
134
+ shell: options?.shell,
132
135
  },
133
136
  (error, stdout, stderr) => {
134
137
  if (error) {
@@ -154,7 +157,7 @@ export function execFileAsync(
154
157
  );
155
158
 
156
159
  // Pipe input to stdin if provided
157
- if (options?.input !== null && options?.input !== undefined && child.stdin) {
160
+ if (options?.input != null && child.stdin) {
158
161
  child.stdin.write(options.input);
159
162
  child.stdin.end();
160
163
  }
@@ -8,16 +8,14 @@
8
8
  * - extractPlanPathFromResult: parse plan path from ExitPlanMode output
9
9
  */
10
10
 
11
- import * as crypto from "node:crypto";
12
11
  import * as fs from "node:fs";
13
12
  import * as path from "node:path";
14
-
13
+ import * as crypto from "node:crypto";
14
+ import { getContextDir, getContextPlansDir, sanitizeTitle } from "../base/constants.js";
15
15
  import { atomicWrite } from "../base/atomic-write.js";
16
- import { getContextDir as _getContextDir, getContextPlansDir, sanitizeTitle } from "../base/constants.js";
17
- import { logDebug, logError, logInfo, logWarn } from "../base/logger.js";
18
- import { readStateJson } from "../base/state-io.js";
16
+ import { logDebug, logInfo, logWarn, logError } from "../base/logger.js";
19
17
  import { generateSlug } from "../base/utils.js";
20
- import type { ContextState as _ContextState } from "../types.js";
18
+ import type { ContextState } from "../types.js";
21
19
 
22
20
  // ---------------------------------------------------------------------------
23
21
  // Plan archival
@@ -32,11 +30,11 @@ import type { ContextState as _ContextState } from "../types.js";
32
30
  * Returns [archivedPath, planHash, planSignature] on success,
33
31
  * or [null, null, null] on error.
34
32
  */
35
- export async function archivePlan(
33
+ export function archivePlan(
36
34
  planPath: string,
37
35
  contextId: string,
38
36
  projectRoot?: string,
39
- ): Promise<[null | string, null | string, null | string]> {
37
+ ): [string | null, string | null, string | null] {
40
38
  if (!fs.existsSync(planPath)) {
41
39
  logWarn("plan_manager", `Plan file not found: ${planPath}`);
42
40
  return [null, null, null];
@@ -44,9 +42,9 @@ export async function archivePlan(
44
42
 
45
43
  let content: string;
46
44
  try {
47
- content = fs.readFileSync(planPath, "utf8");
48
- } catch (error_: any) {
49
- logError("plan_manager", `Failed to read plan: ${error_}`);
45
+ content = fs.readFileSync(planPath, "utf-8");
46
+ } catch (e: any) {
47
+ logError("plan_manager", `Failed to read plan: ${e}`);
50
48
  return [null, null, null];
51
49
  }
52
50
 
@@ -106,7 +104,7 @@ export async function archivePlan(
106
104
  * text suitable for the AI slug generator (which expects conversational input).
107
105
  */
108
106
  function extractPlanSummary(content: string): string {
109
- const lines = content.split("\n");
107
+ const lines = content.split(/\r?\n/);
110
108
  const parts: string[] = [];
111
109
  let firstParagraph = "";
112
110
 
@@ -117,12 +115,10 @@ function extractPlanSummary(content: string): string {
117
115
  const heading = trimmed.replace(/^#+\s*/, "");
118
116
  if (heading.length > 2) parts.push(heading);
119
117
  }
120
-
121
118
  // Grab first substantial non-heading line as context
122
119
  if (!firstParagraph && !trimmed.startsWith("#") && trimmed.length > 20) {
123
120
  firstParagraph = trimmed.slice(0, 120);
124
121
  }
125
-
126
122
  // Enough material for the AI
127
123
  if (parts.length >= 5) break;
128
124
  }
@@ -143,15 +139,17 @@ function extractPlanSummary(content: string): string {
143
139
  export function findLatestPlan(
144
140
  contextId: string,
145
141
  projectRoot?: string,
146
- ): null | string {
142
+ ): string | null {
147
143
  // 1. Check state.json plan_path first
148
144
  try {
149
- const state = readStateJson(contextId, projectRoot);
145
+ // Dynamic import to avoid circular dependency at module level
146
+ const stateIo = require("../base/state-io.js");
147
+ const state = stateIo.readStateJson(contextId, projectRoot);
150
148
  if (state?.plan_path && fs.existsSync(state.plan_path)) {
151
149
  return state.plan_path;
152
150
  }
153
- } catch (error: any) {
154
- logWarn("plan_manager", `Failed to check state.json plan_path: ${error}`);
151
+ } catch (e: any) {
152
+ logWarn("plan_manager", `Failed to check state.json plan_path: ${e}`);
155
153
  }
156
154
 
157
155
  // 2. Fall back to most recent .md in plans/ dir
@@ -184,7 +182,7 @@ export function findLatestPlan(
184
182
  * See SPEC.md §9.4
185
183
  */
186
184
  export function generatePlanId(): string {
187
- return crypto.randomUUID().replaceAll('-', "").slice(0, 8);
185
+ return crypto.randomUUID().replace(/-/g, "").slice(0, 8);
188
186
  }
189
187
 
190
188
  /**
@@ -193,8 +191,8 @@ export function generatePlanId(): string {
193
191
  * See SPEC.md §9.5
194
192
  */
195
193
  export function normalizePlanContent(text: string): string {
196
- let result = text.replaceAll(/<[^>]+>/g, "");
197
- result = result.replaceAll(/\s+/g, " ").trim();
194
+ let result = text.replace(/<[^>]+>/g, "");
195
+ result = result.replace(/\s+/g, " ").trim();
198
196
  return result;
199
197
  }
200
198
 
@@ -205,17 +203,15 @@ export function normalizePlanContent(text: string): string {
205
203
  */
206
204
  export function extractPlanAnchors(content: string, maxAnchors = 5): string[] {
207
205
  const anchors: string[] = [];
208
- for (const line of content.split("\n")) {
206
+ for (const line of content.split(/\r?\n/)) {
209
207
  const trimmed = line.trim();
210
208
  if (trimmed.startsWith("#") && trimmed.length > 3) {
211
209
  anchors.push(trimmed.slice(0, 80));
212
210
  } else if (anchors.length === 0 && trimmed.length > 20) {
213
211
  anchors.push(trimmed.slice(0, 80));
214
212
  }
215
-
216
213
  if (anchors.length >= maxAnchors) break;
217
214
  }
218
-
219
215
  return anchors;
220
216
  }
221
217
 
@@ -230,7 +226,7 @@ const MAX_TRANSCRIPT_SIZE = 50 * 1024 * 1024; // 50 MB
230
226
  * Searches in reverse for the most recent Write tool call targeting .claude/plans/.
231
227
  * See SPEC.md §9.7
232
228
  */
233
- export function findPlanPathInTranscript(transcriptPath: string): null | string {
229
+ export function findPlanPathInTranscript(transcriptPath: string): string | null {
234
230
  if (!transcriptPath) return null;
235
231
 
236
232
  if (!fs.existsSync(transcriptPath)) {
@@ -252,9 +248,9 @@ export function findPlanPathInTranscript(transcriptPath: string): null | string
252
248
 
253
249
  let lines: string[];
254
250
  try {
255
- lines = fs.readFileSync(transcriptPath, "utf8").split("\n");
256
- } catch (error: any) {
257
- logWarn("plan_manager", `Failed to read transcript: ${error}`);
251
+ lines = fs.readFileSync(transcriptPath, "utf-8").split(/\r?\n/);
252
+ } catch (e: any) {
253
+ logWarn("plan_manager", `Failed to read transcript: ${e}`);
258
254
  return null;
259
255
  }
260
256
 
@@ -286,7 +282,7 @@ export function findPlanPathInTranscript(transcriptPath: string): null | string
286
282
  if (!filePath) continue;
287
283
 
288
284
  // Check if path contains .claude/plans/ as consecutive parts
289
- const parts = filePath.replaceAll('\\', "/").split("/");
285
+ const parts = filePath.replace(/\\/g, "/").split("/");
290
286
  for (let j = 0; j < parts.length - 1; j++) {
291
287
  if (parts[j] === ".claude" && parts[j + 1] === "plans") {
292
288
  logInfo("plan_manager", `Extracted plan path from transcript: ${filePath}`);
@@ -309,7 +305,7 @@ export function findPlanPathInTranscript(transcriptPath: string): null | string
309
305
  * Parses the pattern: "Your plan has been saved to: <path>"
310
306
  * See SPEC.md §9.8
311
307
  */
312
- export function extractPlanPathFromResult(toolResult: string): null | string {
308
+ export function extractPlanPathFromResult(toolResult: string): string | null {
313
309
  if (!toolResult) return null;
314
310
  const match = toolResult.match(/Your plan has been saved to:\s*(.+\.md)/);
315
311
  return match ? match[1]!.trim() : null;
@@ -7,8 +7,7 @@
7
7
  */
8
8
 
9
9
  import * as fs from "node:fs";
10
- import * as path from "node:path";
11
-
10
+ import * as path from "node:path";
12
11
  import { getContextHandoffsDir } from "../base/constants.js";
13
12
  import { getContext } from "../context/context-store.js";
14
13
  import type { HandoffSections } from "../types.js";
@@ -19,7 +18,7 @@ import type { HandoffSections } from "../types.js";
19
18
  * (YYYY-MM-DD-HHMM format ensures lexicographic = chronological).
20
19
  * Returns full path to most recent folder, or null.
21
20
  */
22
- export function findLatestHandoff(contextId: string, projectRoot?: string): null | string {
21
+ export function findLatestHandoff(contextId: string, projectRoot?: string): string | null {
23
22
  const handoffsDir = getContextHandoffsDir(contextId, projectRoot);
24
23
 
25
24
  try {
@@ -30,7 +29,7 @@ export function findLatestHandoff(contextId: string, projectRoot?: string): null
30
29
  .sort();
31
30
 
32
31
  if (entries.length === 0) return null;
33
- return path.join(handoffsDir, entries.at(-1)!);
32
+ return path.join(handoffsDir, entries[entries.length - 1]!);
34
33
  } catch {
35
34
  return null;
36
35
  }
@@ -65,7 +64,7 @@ export function readHandoffSections(handoffFolder: string): HandoffSections {
65
64
  const filePath = path.join(handoffFolder, filename);
66
65
  try {
67
66
  if (fs.existsSync(filePath)) {
68
- sections[key as keyof HandoffSections] = fs.readFileSync(filePath, "utf8");
67
+ sections[key as keyof HandoffSections] = fs.readFileSync(filePath, "utf-8");
69
68
  }
70
69
  } catch {
71
70
  // graceful — leave as null
@@ -87,11 +86,11 @@ export function getHandoffTimestamp(handoffFolder: string): Date | null {
87
86
 
88
87
  const [, year, month, day, hour, minute] = match;
89
88
  const date = new Date(
90
- Number.parseInt(year!, 10),
91
- Number.parseInt(month!, 10) - 1,
92
- Number.parseInt(day!, 10),
93
- Number.parseInt(hour!, 10),
94
- Number.parseInt(minute!, 10),
89
+ parseInt(year!, 10),
90
+ parseInt(month!, 10) - 1,
91
+ parseInt(day!, 10),
92
+ parseInt(hour!, 10),
93
+ parseInt(minute!, 10),
95
94
  );
96
95
 
97
96
  return isNaN(date.getTime()) ? null : date;
@@ -106,12 +105,12 @@ export function getHandoffPlanReference(
106
105
  handoffFolder: string,
107
106
  contextId: string,
108
107
  projectRoot?: string,
109
- ): null | string {
108
+ ): string | null {
110
109
  // Try plan.md frontmatter
111
110
  const planMdPath = path.join(handoffFolder, "plan.md");
112
111
  try {
113
112
  if (fs.existsSync(planMdPath)) {
114
- const content = fs.readFileSync(planMdPath, "utf8");
113
+ const content = fs.readFileSync(planMdPath, "utf-8");
115
114
  const frontmatter = parseFrontmatter(content);
116
115
  if (frontmatter["plan_path"]) {
117
116
  const pp = frontmatter["plan_path"];
@@ -146,7 +145,7 @@ function parseFrontmatter(content: string): Record<string, string> {
146
145
  const parts = content.split("---", 3);
147
146
  if (parts.length < 3) return frontmatter;
148
147
 
149
- for (const line of parts[1]!.trim().split("\n")) {
148
+ for (const line of parts[1]!.trim().split(/\r?\n/)) {
150
149
  const colonIdx = line.indexOf(":");
151
150
  if (colonIdx !== -1) {
152
151
  const key = line.slice(0, colonIdx).trim();