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.
- package/dist/templates/_shared/hooks-ts/session_end.ts +4 -1
- package/dist/templates/_shared/lib-ts/base/git-state.ts +1 -1
- package/dist/templates/_shared/lib-ts/base/logger.ts +15 -18
- package/dist/templates/_shared/lib-ts/base/subprocess-utils.ts +18 -15
- package/dist/templates/_shared/lib-ts/context/plan-manager.ts +26 -30
- package/dist/templates/_shared/lib-ts/handoff/handoff-reader.ts +12 -13
- package/dist/templates/_shared/scripts/resume_handoff.ts +62 -38
- package/dist/templates/_shared/scripts/save_handoff.ts +24 -24
- package/dist/templates/_shared/scripts/status_line.ts +101 -147
- package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.ts +239 -133
- package/dist/templates/cc-native/_cc-native/lib-ts/aggregate-agents.ts +11 -12
- package/dist/templates/cc-native/_cc-native/lib-ts/artifacts.ts +139 -56
- package/dist/templates/cc-native/_cc-native/lib-ts/cli-output-parser.ts +22 -2
- package/dist/templates/cc-native/_cc-native/lib-ts/corroboration.ts +115 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/index.ts +1 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/json-parser.ts +7 -1
- package/dist/templates/cc-native/_cc-native/lib-ts/orchestrator.ts +5 -4
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/agent.ts +133 -13
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/codex.ts +6 -6
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/gemini.ts +5 -4
- package/dist/templates/cc-native/_cc-native/lib-ts/state.ts +30 -33
- package/dist/templates/cc-native/_cc-native/lib-ts/types.ts +118 -43
- package/dist/templates/cc-native/_cc-native/plan-review.config.json +21 -0
- package/oclif.manifest.json +1 -1
- 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
|
-
|
|
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(
|
|
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:
|
|
35
|
+
let _cachedSessionId: string | null = null;
|
|
36
36
|
|
|
37
37
|
// Module-level context path cache (kept for external callers)
|
|
38
|
-
let _cachedContextPath:
|
|
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:
|
|
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:
|
|
51
|
+
export function setContextPath(contextPath: string | null): void {
|
|
52
52
|
_cachedContextPath = contextPath;
|
|
53
53
|
_contextResolved = true;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
export function getContextPath():
|
|
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, "
|
|
157
|
-
const lines = content.split(
|
|
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.
|
|
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 {
|
|
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):
|
|
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
|
-
|
|
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:
|
|
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
|
|
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 {
|
|
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
|
|
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
|
|
33
|
+
export function archivePlan(
|
|
36
34
|
planPath: string,
|
|
37
35
|
contextId: string,
|
|
38
36
|
projectRoot?: string,
|
|
39
|
-
):
|
|
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, "
|
|
48
|
-
} catch (
|
|
49
|
-
logError("plan_manager", `Failed to read plan: ${
|
|
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(
|
|
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
|
-
):
|
|
142
|
+
): string | null {
|
|
147
143
|
// 1. Check state.json plan_path first
|
|
148
144
|
try {
|
|
149
|
-
|
|
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 (
|
|
154
|
-
logWarn("plan_manager", `Failed to check state.json plan_path: ${
|
|
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().
|
|
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.
|
|
197
|
-
result = result.
|
|
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(
|
|
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):
|
|
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, "
|
|
256
|
-
} catch (
|
|
257
|
-
logWarn("plan_manager", `Failed to read transcript: ${
|
|
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.
|
|
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):
|
|
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):
|
|
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.
|
|
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, "
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
):
|
|
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, "
|
|
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(
|
|
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();
|