aiwcli 0.10.1 → 0.10.3
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/clean.js +1 -0
- package/dist/commands/clear.d.ts +19 -2
- package/dist/commands/clear.js +351 -160
- package/dist/commands/init/index.d.ts +1 -17
- package/dist/commands/init/index.js +19 -104
- package/dist/lib/gitignore-manager.d.ts +9 -0
- package/dist/lib/gitignore-manager.js +121 -0
- package/dist/lib/template-installer.d.ts +7 -12
- package/dist/lib/template-installer.js +69 -193
- package/dist/lib/template-settings-reconstructor.d.ts +35 -0
- package/dist/lib/template-settings-reconstructor.js +130 -0
- package/dist/templates/_shared/hooks/__pycache__/archive_plan.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/session_end.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/archive_plan.py +10 -2
- package/dist/templates/_shared/hooks/session_end.py +37 -29
- 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__/utils.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/hook_utils.py +8 -10
- package/dist/templates/_shared/lib/base/inference.py +51 -62
- package/dist/templates/_shared/lib/base/logger.py +35 -21
- package/dist/templates/_shared/lib/base/stop_words.py +8 -0
- package/dist/templates/_shared/lib/base/utils.py +29 -8
- package/dist/templates/_shared/lib/context/__pycache__/plan_manager.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/plan_manager.py +101 -2
- package/dist/templates/_shared/lib-ts/base/atomic-write.ts +138 -0
- package/dist/templates/_shared/lib-ts/base/constants.ts +299 -0
- package/dist/templates/_shared/lib-ts/base/git-state.ts +58 -0
- package/dist/templates/_shared/lib-ts/base/hook-utils.ts +360 -0
- package/dist/templates/_shared/lib-ts/base/inference.ts +245 -0
- package/dist/templates/_shared/lib-ts/base/logger.ts +234 -0
- package/dist/templates/_shared/lib-ts/base/state-io.ts +114 -0
- package/dist/templates/_shared/lib-ts/base/stop-words.ts +184 -0
- package/dist/templates/_shared/lib-ts/base/subprocess-utils.ts +23 -0
- package/dist/templates/_shared/lib-ts/base/utils.ts +184 -0
- package/dist/templates/_shared/lib-ts/context/context-formatter.ts +432 -0
- package/dist/templates/_shared/lib-ts/context/context-selector.ts +497 -0
- package/dist/templates/_shared/lib-ts/context/context-store.ts +679 -0
- package/dist/templates/_shared/lib-ts/context/plan-manager.ts +292 -0
- package/dist/templates/_shared/lib-ts/context/task-tracker.ts +181 -0
- package/dist/templates/_shared/lib-ts/handoff/document-generator.ts +215 -0
- package/dist/templates/_shared/lib-ts/package.json +21 -0
- package/dist/templates/_shared/lib-ts/templates/formatters.ts +102 -0
- package/dist/templates/_shared/lib-ts/templates/plan-context.ts +65 -0
- package/dist/templates/_shared/lib-ts/tsconfig.json +13 -0
- package/dist/templates/_shared/lib-ts/types.ts +151 -0
- package/dist/templates/_shared/scripts/__pycache__/status_line.cpython-313.pyc +0 -0
- package/dist/templates/_shared/scripts/save_handoff.ts +359 -0
- package/dist/templates/_shared/scripts/status_line.py +17 -2
- package/dist/templates/cc-native/_cc-native/agents/ARCH-EVOLUTION.md +63 -0
- package/dist/templates/cc-native/_cc-native/agents/ARCH-PATTERNS.md +62 -0
- package/dist/templates/cc-native/_cc-native/agents/ARCH-STRUCTURE.md +63 -0
- package/dist/templates/cc-native/_cc-native/agents/{ASSUMPTION-CHAIN-TRACER.md → ASSUMPTION-TRACER.md} +6 -10
- package/dist/templates/cc-native/_cc-native/agents/CLARITY-AUDITOR.md +6 -10
- package/dist/templates/cc-native/_cc-native/agents/CLAUDE.md +74 -1
- package/dist/templates/cc-native/_cc-native/agents/COMPLETENESS-FEASIBILITY.md +67 -0
- package/dist/templates/cc-native/_cc-native/agents/COMPLETENESS-GAPS.md +71 -0
- package/dist/templates/cc-native/_cc-native/agents/COMPLETENESS-ORDERING.md +63 -0
- package/dist/templates/cc-native/_cc-native/agents/CONSTRAINT-VALIDATOR.md +73 -0
- package/dist/templates/cc-native/_cc-native/agents/DESIGN-ADR-VALIDATOR.md +62 -0
- package/dist/templates/cc-native/_cc-native/agents/DESIGN-SCALE-MATCHER.md +65 -0
- package/dist/templates/cc-native/_cc-native/agents/DEVILS-ADVOCATE.md +6 -9
- package/dist/templates/cc-native/_cc-native/agents/DOCUMENTATION-PHILOSOPHY.md +87 -0
- package/dist/templates/cc-native/_cc-native/agents/HANDOFF-READINESS.md +5 -9
- package/dist/templates/cc-native/_cc-native/agents/{HIDDEN-COMPLEXITY-DETECTOR.md → HIDDEN-COMPLEXITY.md} +6 -10
- package/dist/templates/cc-native/_cc-native/agents/INCREMENTAL-DELIVERY.md +67 -0
- package/dist/templates/cc-native/_cc-native/agents/PLAN-ORCHESTRATOR.md +91 -18
- package/dist/templates/cc-native/_cc-native/agents/RISK-DEPENDENCY.md +63 -0
- package/dist/templates/cc-native/_cc-native/agents/RISK-FMEA.md +67 -0
- package/dist/templates/cc-native/_cc-native/agents/RISK-PREMORTEM.md +72 -0
- package/dist/templates/cc-native/_cc-native/agents/RISK-REVERSIBILITY.md +75 -0
- package/dist/templates/cc-native/_cc-native/agents/SCOPE-BOUNDARY.md +78 -0
- package/dist/templates/cc-native/_cc-native/agents/SIMPLICITY-GUARDIAN.md +5 -9
- package/dist/templates/cc-native/_cc-native/agents/SKEPTIC.md +16 -12
- package/dist/templates/cc-native/_cc-native/agents/TESTDRIVEN-BEHAVIOR-AUDITOR.md +62 -0
- package/dist/templates/cc-native/_cc-native/agents/TESTDRIVEN-CHARACTERIZATION.md +72 -0
- package/dist/templates/cc-native/_cc-native/agents/TESTDRIVEN-FIRST-VALIDATOR.md +62 -0
- package/dist/templates/cc-native/_cc-native/agents/TESTDRIVEN-PYRAMID-ANALYZER.md +62 -0
- package/dist/templates/cc-native/_cc-native/agents/TRADEOFF-COSTS.md +68 -0
- package/dist/templates/cc-native/_cc-native/agents/TRADEOFF-STAKEHOLDERS.md +66 -0
- package/dist/templates/cc-native/_cc-native/agents/VERIFY-COVERAGE.md +75 -0
- package/dist/templates/cc-native/_cc-native/agents/VERIFY-STRENGTH.md +70 -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/cc-native-plan-review.py +125 -40
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/utils.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/utils.py +57 -13
- package/dist/templates/cc-native/_cc-native/plan-review.config.json +11 -7
- package/oclif.manifest.json +17 -2
- package/package.json +1 -1
- package/dist/lib/template-merger.d.ts +0 -47
- package/dist/lib/template-merger.js +0 -162
- package/dist/templates/cc-native/_cc-native/agents/ACCESSIBILITY-TESTER.md +0 -79
- package/dist/templates/cc-native/_cc-native/agents/ARCHITECT-REVIEWER.md +0 -48
- package/dist/templates/cc-native/_cc-native/agents/CODE-REVIEWER.md +0 -70
- package/dist/templates/cc-native/_cc-native/agents/COMPLETENESS-CHECKER.md +0 -59
- package/dist/templates/cc-native/_cc-native/agents/CONTEXT-EXTRACTOR.md +0 -92
- package/dist/templates/cc-native/_cc-native/agents/DOCUMENTATION-REVIEWER.md +0 -51
- package/dist/templates/cc-native/_cc-native/agents/FEASIBILITY-ANALYST.md +0 -57
- package/dist/templates/cc-native/_cc-native/agents/FRESH-PERSPECTIVE.md +0 -54
- package/dist/templates/cc-native/_cc-native/agents/INCENTIVE-MAPPER.md +0 -61
- package/dist/templates/cc-native/_cc-native/agents/PENETRATION-TESTER.md +0 -79
- package/dist/templates/cc-native/_cc-native/agents/PERFORMANCE-ENGINEER.md +0 -75
- package/dist/templates/cc-native/_cc-native/agents/PRECEDENT-FINDER.md +0 -70
- package/dist/templates/cc-native/_cc-native/agents/REVERSIBILITY-ANALYST.md +0 -61
- package/dist/templates/cc-native/_cc-native/agents/RISK-ASSESSOR.md +0 -58
- package/dist/templates/cc-native/_cc-native/agents/SECOND-ORDER-ANALYST.md +0 -61
- package/dist/templates/cc-native/_cc-native/agents/STAKEHOLDER-ADVOCATE.md +0 -55
- package/dist/templates/cc-native/_cc-native/agents/TRADE-OFF-ILLUMINATOR.md +0 -204
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plan lifecycle management — archival, lookup, and path extraction.
|
|
3
|
+
* See SPEC.md §9
|
|
4
|
+
*
|
|
5
|
+
* Provides pure-data operations on plan files:
|
|
6
|
+
* - archivePlan: copy plan to context plans/ folder, compute hash + signature
|
|
7
|
+
* - findLatestPlan: locate the most relevant plan for a context
|
|
8
|
+
* - extractPlanPathFromResult: parse plan path from ExitPlanMode output
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import * as fs from "node:fs";
|
|
12
|
+
import * as path from "node:path";
|
|
13
|
+
import * as crypto from "node:crypto";
|
|
14
|
+
import { getContextDir, getContextPlansDir, sanitizeTitle } from "../base/constants.js";
|
|
15
|
+
import { atomicWrite } from "../base/atomic-write.js";
|
|
16
|
+
import { readStateJson } from "../base/state-io.js";
|
|
17
|
+
import { logDebug, logInfo, logWarn, logError } from "../base/logger.js";
|
|
18
|
+
import type { ContextState } from "../types.js";
|
|
19
|
+
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Plan archival
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Archive a plan file to the context's plans/ folder.
|
|
26
|
+
* Computes a content hash and signature.
|
|
27
|
+
* Does NOT modify state.json or mode.
|
|
28
|
+
* See SPEC.md §9.2
|
|
29
|
+
*
|
|
30
|
+
* Returns [archivedPath, planHash, planSignature] on success,
|
|
31
|
+
* or [null, null, null] on error.
|
|
32
|
+
*/
|
|
33
|
+
export async function archivePlan(
|
|
34
|
+
planPath: string,
|
|
35
|
+
contextId: string,
|
|
36
|
+
projectRoot?: string,
|
|
37
|
+
): Promise<[string | null, string | null, string | null]> {
|
|
38
|
+
if (!fs.existsSync(planPath)) {
|
|
39
|
+
logWarn("plan_manager", `Plan file not found: ${planPath}`);
|
|
40
|
+
return [null, null, null];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let content: string;
|
|
44
|
+
try {
|
|
45
|
+
content = fs.readFileSync(planPath, "utf-8");
|
|
46
|
+
} catch (e: any) {
|
|
47
|
+
logError("plan_manager", `Failed to read plan: ${e}`);
|
|
48
|
+
return [null, null, null];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Compute hash and signature
|
|
52
|
+
const planHash = crypto.createHash("sha256").update(content, "utf-8").digest("hex").slice(0, 12);
|
|
53
|
+
const planSignature = content.slice(0, 200);
|
|
54
|
+
|
|
55
|
+
// Ensure plans directory exists
|
|
56
|
+
const plansDir = getContextPlansDir(contextId, projectRoot);
|
|
57
|
+
fs.mkdirSync(plansDir, { recursive: true });
|
|
58
|
+
|
|
59
|
+
// Generate archive filename: YYYY-MM-DD-HHMM-<slug>.md
|
|
60
|
+
const now = new Date();
|
|
61
|
+
const dateStr = [
|
|
62
|
+
now.getFullYear(),
|
|
63
|
+
"-",
|
|
64
|
+
String(now.getMonth() + 1).padStart(2, "0"),
|
|
65
|
+
"-",
|
|
66
|
+
String(now.getDate()).padStart(2, "0"),
|
|
67
|
+
"-",
|
|
68
|
+
String(now.getHours()).padStart(2, "0"),
|
|
69
|
+
String(now.getMinutes()).padStart(2, "0"),
|
|
70
|
+
].join("");
|
|
71
|
+
|
|
72
|
+
// Try AI inference for a descriptive slug
|
|
73
|
+
let slug: string | null = null;
|
|
74
|
+
try {
|
|
75
|
+
const { generateContextIdSlug } = await import("../base/inference.js");
|
|
76
|
+
const aiSlug = generateContextIdSlug(content.slice(0, 500), 5);
|
|
77
|
+
if (aiSlug) {
|
|
78
|
+
slug = sanitizeTitle(aiSlug, 60);
|
|
79
|
+
}
|
|
80
|
+
} catch {
|
|
81
|
+
// AI inference unavailable — use filename fallback
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Fallback: use plan filename
|
|
85
|
+
if (!slug) {
|
|
86
|
+
const stem = path.basename(planPath, path.extname(planPath));
|
|
87
|
+
slug = sanitizeTitle(stem, 30);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
let archiveName = `${dateStr}-${slug}.md`;
|
|
91
|
+
let archivePath = path.join(plansDir, archiveName);
|
|
92
|
+
|
|
93
|
+
// Handle filename collisions
|
|
94
|
+
let counter = 2;
|
|
95
|
+
while (fs.existsSync(archivePath)) {
|
|
96
|
+
archiveName = `${dateStr}-${slug}-${counter}.md`;
|
|
97
|
+
archivePath = path.join(plansDir, archiveName);
|
|
98
|
+
counter++;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Write archived plan atomically
|
|
102
|
+
const [success, error] = atomicWrite(archivePath, content);
|
|
103
|
+
if (!success) {
|
|
104
|
+
logError("plan_manager", `Failed to write archive: ${error}`);
|
|
105
|
+
return [null, null, null];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
logInfo("plan_manager", `Archived plan to: ${archivePath}`);
|
|
109
|
+
return [archivePath, planHash, planSignature];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
// Plan lookup
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Find the most relevant plan file for a context.
|
|
118
|
+
* Priority: state.json plan_path > most recent .md in plans/ dir.
|
|
119
|
+
* See SPEC.md §9.3
|
|
120
|
+
*/
|
|
121
|
+
export function findLatestPlan(
|
|
122
|
+
contextId: string,
|
|
123
|
+
projectRoot?: string,
|
|
124
|
+
): string | null {
|
|
125
|
+
// 1. Check state.json plan_path first
|
|
126
|
+
try {
|
|
127
|
+
const state = readStateJson(contextId, projectRoot);
|
|
128
|
+
if (state?.plan_path && fs.existsSync(state.plan_path)) {
|
|
129
|
+
return state.plan_path;
|
|
130
|
+
}
|
|
131
|
+
} catch (e: any) {
|
|
132
|
+
logWarn("plan_manager", `Failed to check state.json plan_path: ${e}`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// 2. Fall back to most recent .md in plans/ dir
|
|
136
|
+
const plansDir = getContextPlansDir(contextId, projectRoot);
|
|
137
|
+
if (fs.existsSync(plansDir)) {
|
|
138
|
+
try {
|
|
139
|
+
const files = fs.readdirSync(plansDir)
|
|
140
|
+
.filter(f => f.endsWith(".md"))
|
|
141
|
+
.map(f => {
|
|
142
|
+
const fullPath = path.join(plansDir, f);
|
|
143
|
+
return { path: fullPath, mtime: fs.statSync(fullPath).mtimeMs };
|
|
144
|
+
})
|
|
145
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
146
|
+
|
|
147
|
+
if (files.length > 0) {
|
|
148
|
+
return files[0]!.path;
|
|
149
|
+
}
|
|
150
|
+
} catch { /* ignore */ }
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
// Plan identification and normalization
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Generate a short unique plan identifier (8 hex chars).
|
|
162
|
+
* See SPEC.md §9.4
|
|
163
|
+
*/
|
|
164
|
+
export function generatePlanId(): string {
|
|
165
|
+
return crypto.randomUUID().replace(/-/g, "").slice(0, 8);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Aggressively normalize plan content for hashing.
|
|
170
|
+
* Strips all XML/HTML tags and collapses whitespace.
|
|
171
|
+
* See SPEC.md §9.5
|
|
172
|
+
*/
|
|
173
|
+
export function normalizePlanContent(text: string): string {
|
|
174
|
+
let result = text.replace(/<[^>]+>/g, "");
|
|
175
|
+
result = result.replace(/\s+/g, " ").trim();
|
|
176
|
+
return result;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Extract structural anchors from plan content.
|
|
181
|
+
* Returns markdown headings + first substantial paragraph as short strings.
|
|
182
|
+
* See SPEC.md §9.6
|
|
183
|
+
*/
|
|
184
|
+
export function extractPlanAnchors(content: string, maxAnchors = 5): string[] {
|
|
185
|
+
const anchors: string[] = [];
|
|
186
|
+
for (const line of content.split("\n")) {
|
|
187
|
+
const trimmed = line.trim();
|
|
188
|
+
if (trimmed.startsWith("#") && trimmed.length > 3) {
|
|
189
|
+
anchors.push(trimmed.slice(0, 80));
|
|
190
|
+
} else if (anchors.length === 0 && trimmed.length > 20) {
|
|
191
|
+
anchors.push(trimmed.slice(0, 80));
|
|
192
|
+
}
|
|
193
|
+
if (anchors.length >= maxAnchors) break;
|
|
194
|
+
}
|
|
195
|
+
return anchors;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ---------------------------------------------------------------------------
|
|
199
|
+
// Transcript-based plan path extraction
|
|
200
|
+
// ---------------------------------------------------------------------------
|
|
201
|
+
|
|
202
|
+
const MAX_TRANSCRIPT_SIZE = 50 * 1024 * 1024; // 50 MB
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Find the plan file path by parsing the session transcript JSONL.
|
|
206
|
+
* Searches in reverse for the most recent Write tool call targeting .claude/plans/.
|
|
207
|
+
* See SPEC.md §9.7
|
|
208
|
+
*/
|
|
209
|
+
export function findPlanPathInTranscript(transcriptPath: string): string | null {
|
|
210
|
+
if (!transcriptPath) return null;
|
|
211
|
+
|
|
212
|
+
if (!fs.existsSync(transcriptPath)) {
|
|
213
|
+
logDebug("plan_manager", `Transcript not found: ${transcriptPath}`);
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
let size: number;
|
|
218
|
+
try {
|
|
219
|
+
size = fs.statSync(transcriptPath).size;
|
|
220
|
+
} catch {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (size > MAX_TRANSCRIPT_SIZE) {
|
|
225
|
+
logWarn("plan_manager", `Transcript too large (${size} bytes), skipping`);
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
let lines: string[];
|
|
230
|
+
try {
|
|
231
|
+
lines = fs.readFileSync(transcriptPath, "utf-8").split("\n");
|
|
232
|
+
} catch (e: any) {
|
|
233
|
+
logWarn("plan_manager", `Failed to read transcript: ${e}`);
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
238
|
+
const line = lines[i]!.trim();
|
|
239
|
+
if (!line) continue;
|
|
240
|
+
|
|
241
|
+
let data: any;
|
|
242
|
+
try {
|
|
243
|
+
data = JSON.parse(line);
|
|
244
|
+
} catch {
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
let contentArr: any;
|
|
249
|
+
try {
|
|
250
|
+
contentArr = data.message?.content;
|
|
251
|
+
} catch {
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (!Array.isArray(contentArr)) continue;
|
|
256
|
+
|
|
257
|
+
for (const block of contentArr) {
|
|
258
|
+
if (typeof block !== "object" || block === null) continue;
|
|
259
|
+
if (block.type !== "tool_use" || block.name !== "Write") continue;
|
|
260
|
+
|
|
261
|
+
const filePath = block.input?.file_path;
|
|
262
|
+
if (!filePath) continue;
|
|
263
|
+
|
|
264
|
+
// Check if path contains .claude/plans/ as consecutive parts
|
|
265
|
+
const parts = filePath.replace(/\\/g, "/").split("/");
|
|
266
|
+
for (let j = 0; j < parts.length - 1; j++) {
|
|
267
|
+
if (parts[j] === ".claude" && parts[j + 1] === "plans") {
|
|
268
|
+
logInfo("plan_manager", `Extracted plan path from transcript: ${filePath}`);
|
|
269
|
+
return filePath;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
logDebug("plan_manager", "No plan Write found in transcript");
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// ---------------------------------------------------------------------------
|
|
280
|
+
// Path extraction from tool output
|
|
281
|
+
// ---------------------------------------------------------------------------
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Extract plan file path from ExitPlanMode tool result.
|
|
285
|
+
* Parses the pattern: "Your plan has been saved to: <path>"
|
|
286
|
+
* See SPEC.md §9.8
|
|
287
|
+
*/
|
|
288
|
+
export function extractPlanPathFromResult(toolResult: string): string | null {
|
|
289
|
+
if (!toolResult) return null;
|
|
290
|
+
const match = toolResult.match(/Your plan has been saved to:\s*(.+\.md)/);
|
|
291
|
+
return match ? match[1]!.trim() : null;
|
|
292
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task tracker — direct state.json CRUD for tasks.
|
|
3
|
+
* See SPEC.md §10
|
|
4
|
+
*
|
|
5
|
+
* Writes tasks directly to the tasks[] array in state.json.
|
|
6
|
+
* Uses state-io for I/O to avoid circular imports with context-store.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { readStateJson, writeStateJson, toDict } from "../base/state-io.js";
|
|
10
|
+
import { logWarn } from "../base/logger.js";
|
|
11
|
+
import { nowIso } from "../base/utils.js";
|
|
12
|
+
import type { ContextState, Task } from "../types.js";
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Public API
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Scan tasks[] for highest aiw-N, return aiw-(N+1).
|
|
20
|
+
* See SPEC.md §10.2
|
|
21
|
+
*/
|
|
22
|
+
export function generateNextTaskId(contextId: string, projectRoot?: string): string {
|
|
23
|
+
const state = readStateJson(contextId, projectRoot);
|
|
24
|
+
const tasks = state?.tasks ?? [];
|
|
25
|
+
|
|
26
|
+
let maxNum = 0;
|
|
27
|
+
for (const t of tasks) {
|
|
28
|
+
const match = /^aiw-(\d+)$/.exec(t.id);
|
|
29
|
+
if (match) {
|
|
30
|
+
const num = parseInt(match[1]!, 10);
|
|
31
|
+
if (num > maxNum) maxNum = num;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return `aiw-${maxNum + 1}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Add a new task to state.json tasks[] and return the task object.
|
|
40
|
+
* See SPEC.md §10.3
|
|
41
|
+
*/
|
|
42
|
+
export function addTask(
|
|
43
|
+
contextId: string,
|
|
44
|
+
subject: string,
|
|
45
|
+
description = "",
|
|
46
|
+
activeForm = "",
|
|
47
|
+
sessionId = "",
|
|
48
|
+
projectRoot?: string,
|
|
49
|
+
): Task | null {
|
|
50
|
+
const state = readStateJson(contextId, projectRoot);
|
|
51
|
+
if (!state) return null;
|
|
52
|
+
|
|
53
|
+
const taskId = generateNextTaskId(contextId, projectRoot);
|
|
54
|
+
const task: Task = {
|
|
55
|
+
id: taskId,
|
|
56
|
+
subject,
|
|
57
|
+
description,
|
|
58
|
+
active_form: activeForm,
|
|
59
|
+
status: "pending",
|
|
60
|
+
created_at: nowIso(),
|
|
61
|
+
completed_at: null,
|
|
62
|
+
evidence: "",
|
|
63
|
+
work_summary: "",
|
|
64
|
+
files_changed: [],
|
|
65
|
+
session_id: sessionId,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
state.tasks.push(task);
|
|
69
|
+
state.last_active = nowIso();
|
|
70
|
+
|
|
71
|
+
const [success] = writeStateJson(contextId, state, projectRoot);
|
|
72
|
+
return success ? task : null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Find task by task_id in tasks[], update fields, return true on success.
|
|
77
|
+
* See SPEC.md §10.4
|
|
78
|
+
*/
|
|
79
|
+
export function updateTask(
|
|
80
|
+
contextId: string,
|
|
81
|
+
taskId: string,
|
|
82
|
+
opts?: {
|
|
83
|
+
status?: string;
|
|
84
|
+
evidence?: string;
|
|
85
|
+
work_summary?: string;
|
|
86
|
+
files_changed?: string[];
|
|
87
|
+
session_id?: string;
|
|
88
|
+
},
|
|
89
|
+
projectRoot?: string,
|
|
90
|
+
): boolean {
|
|
91
|
+
const state = readStateJson(contextId, projectRoot);
|
|
92
|
+
if (!state) return false;
|
|
93
|
+
|
|
94
|
+
for (const task of state.tasks) {
|
|
95
|
+
if (task.id === taskId) {
|
|
96
|
+
if (opts?.status !== undefined) {
|
|
97
|
+
task.status = opts.status as Task["status"];
|
|
98
|
+
if (opts.status === "completed") {
|
|
99
|
+
task.completed_at = nowIso();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (opts?.evidence) task.evidence = opts.evidence;
|
|
103
|
+
if (opts?.work_summary) task.work_summary = opts.work_summary;
|
|
104
|
+
if (opts?.files_changed !== undefined) task.files_changed = opts.files_changed;
|
|
105
|
+
if (opts?.session_id) task.session_id = opts.session_id;
|
|
106
|
+
state.last_active = nowIso();
|
|
107
|
+
const [success] = writeStateJson(contextId, state, projectRoot);
|
|
108
|
+
return success;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
logWarn("task_tracker", `Task '${taskId}' not found in context '${contextId}'`);
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Remove task from tasks[] and return true on success.
|
|
118
|
+
* See SPEC.md §10.5
|
|
119
|
+
*/
|
|
120
|
+
export function deleteTask(
|
|
121
|
+
contextId: string,
|
|
122
|
+
taskId: string,
|
|
123
|
+
projectRoot?: string,
|
|
124
|
+
): boolean {
|
|
125
|
+
const state = readStateJson(contextId, projectRoot);
|
|
126
|
+
if (!state) return false;
|
|
127
|
+
|
|
128
|
+
const originalLen = state.tasks.length;
|
|
129
|
+
state.tasks = state.tasks.filter(t => t.id !== taskId);
|
|
130
|
+
|
|
131
|
+
if (state.tasks.length === originalLen) {
|
|
132
|
+
logWarn("task_tracker", `Task '${taskId}' not found in context '${contextId}'`);
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
state.last_active = nowIso();
|
|
137
|
+
const [success] = writeStateJson(contextId, state, projectRoot);
|
|
138
|
+
return success;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Return tasks[] from state.json.
|
|
143
|
+
* See SPEC.md §10.6
|
|
144
|
+
*/
|
|
145
|
+
export function getTasks(contextId: string, projectRoot?: string): Task[] {
|
|
146
|
+
const state = readStateJson(contextId, projectRoot);
|
|
147
|
+
if (!state) return [];
|
|
148
|
+
return state.tasks;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Partition tasks and format as markdown checklist.
|
|
153
|
+
* See SPEC.md §10.7
|
|
154
|
+
*/
|
|
155
|
+
export function generateTaskSummary(contextId: string, projectRoot?: string): string {
|
|
156
|
+
const tasks = getTasks(contextId, projectRoot);
|
|
157
|
+
if (tasks.length === 0) return "No tasks in this context.";
|
|
158
|
+
|
|
159
|
+
const completed = tasks.filter(t => t.status === "completed");
|
|
160
|
+
const inProgress = tasks.filter(t => t.status === "in_progress");
|
|
161
|
+
const pending = tasks.filter(t => t.status === "pending");
|
|
162
|
+
const blocked = tasks.filter(t => t.status === "blocked");
|
|
163
|
+
|
|
164
|
+
const lines: string[] = [`### Tasks (${tasks.length} total)`, ""];
|
|
165
|
+
|
|
166
|
+
for (const t of completed) {
|
|
167
|
+
const ws = t.work_summary ? `\n Work: ${t.work_summary}` : "";
|
|
168
|
+
lines.push(`- [x] ${t.id}: ${t.subject}${ws}`);
|
|
169
|
+
}
|
|
170
|
+
for (const t of inProgress) {
|
|
171
|
+
lines.push(`- [~] ${t.id}: ${t.subject}`);
|
|
172
|
+
}
|
|
173
|
+
for (const t of pending) {
|
|
174
|
+
lines.push(`- [ ] ${t.id}: ${t.subject}`);
|
|
175
|
+
}
|
|
176
|
+
for (const t of blocked) {
|
|
177
|
+
lines.push(`- [!] ${t.id}: ${t.subject}`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return lines.join("\n");
|
|
181
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handoff document generator for context-aware session management.
|
|
3
|
+
* See SPEC.md §12
|
|
4
|
+
*
|
|
5
|
+
* Creates structured handoff documents when a session needs to transfer
|
|
6
|
+
* work to a new session (typically due to context window limits).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import * as fs from "node:fs";
|
|
10
|
+
import * as path from "node:path";
|
|
11
|
+
import * as crypto from "node:crypto";
|
|
12
|
+
import { getContextHandoffsDir, getContextDir } from "../base/constants.js";
|
|
13
|
+
import { atomicWrite } from "../base/atomic-write.js";
|
|
14
|
+
import { logInfo, logError } from "../base/logger.js";
|
|
15
|
+
import { nowIso } from "../base/utils.js";
|
|
16
|
+
import { getContext, saveState } from "../context/context-store.js";
|
|
17
|
+
import { getTasks } from "../context/task-tracker.js";
|
|
18
|
+
import { renderTaskList, formatContinuationHeader, formatReason } from "../templates/formatters.js";
|
|
19
|
+
import type { HandoffDocument, Task } from "../types.js";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Generate and save a handoff document for a context.
|
|
23
|
+
* See SPEC.md §12.2
|
|
24
|
+
*/
|
|
25
|
+
export function generateHandoffDocument(
|
|
26
|
+
contextId: string,
|
|
27
|
+
reason = "low_context",
|
|
28
|
+
workSummary = "",
|
|
29
|
+
nextSteps?: string[],
|
|
30
|
+
importantNotes?: string[],
|
|
31
|
+
completedThisSession?: string[],
|
|
32
|
+
projectRoot?: string,
|
|
33
|
+
): HandoffDocument | null {
|
|
34
|
+
const context = getContext(contextId, projectRoot);
|
|
35
|
+
if (!context) {
|
|
36
|
+
logError("handoff", `Context '${contextId}' not found`);
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Generate session ID
|
|
41
|
+
const sessionId = crypto.randomUUID().slice(0, 8);
|
|
42
|
+
|
|
43
|
+
// Get pending tasks from state.json
|
|
44
|
+
const allTasks = getTasks(contextId, projectRoot);
|
|
45
|
+
const pendingTasks = allTasks.filter(
|
|
46
|
+
t => t.status === "pending" || t.status === "in_progress" || t.status === "blocked",
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
// Build document
|
|
50
|
+
const now = nowIso();
|
|
51
|
+
const contextDir = getContextDir(contextId, projectRoot);
|
|
52
|
+
|
|
53
|
+
const doc: HandoffDocument = {
|
|
54
|
+
context_id: contextId,
|
|
55
|
+
context_summary: context.summary,
|
|
56
|
+
session_id: sessionId,
|
|
57
|
+
reason,
|
|
58
|
+
created_at: now,
|
|
59
|
+
plan_path: context.plan_path,
|
|
60
|
+
context_folder: contextDir,
|
|
61
|
+
events_log_path: path.join(contextDir, "state.json"),
|
|
62
|
+
active_tasks: pendingTasks,
|
|
63
|
+
completed_tasks_this_session: (completedThisSession ?? []).map(s => ({ subject: s })),
|
|
64
|
+
work_summary: workSummary,
|
|
65
|
+
next_steps: nextSteps ?? [],
|
|
66
|
+
important_notes: importantNotes ?? [],
|
|
67
|
+
file_path: null,
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// Compute file path BEFORE rendering markdown
|
|
71
|
+
const handoffsDir = getContextHandoffsDir(contextId, projectRoot);
|
|
72
|
+
fs.mkdirSync(handoffsDir, { recursive: true });
|
|
73
|
+
|
|
74
|
+
const d = new Date();
|
|
75
|
+
const dateStr = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
76
|
+
const filename = `${dateStr}-session-${sessionId}.md`;
|
|
77
|
+
const filePath = path.join(handoffsDir, filename);
|
|
78
|
+
|
|
79
|
+
// Set file_path on doc BEFORE rendering markdown
|
|
80
|
+
doc.file_path = filePath;
|
|
81
|
+
|
|
82
|
+
// Generate markdown content
|
|
83
|
+
const markdown = renderHandoffMarkdown(doc);
|
|
84
|
+
|
|
85
|
+
// Save to handoffs folder
|
|
86
|
+
const [success, error] = atomicWrite(filePath, markdown);
|
|
87
|
+
if (!success) {
|
|
88
|
+
logError("handoff", `Failed to write handoff document: ${error}`);
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
logInfo("handoff", `Created handoff document: ${filePath}`);
|
|
93
|
+
return doc;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Render handoff document as markdown.
|
|
98
|
+
*/
|
|
99
|
+
function renderHandoffMarkdown(doc: HandoffDocument): string {
|
|
100
|
+
const lines: string[] = [
|
|
101
|
+
formatContinuationHeader("handoff", doc.context_id),
|
|
102
|
+
"",
|
|
103
|
+
`**Created**: ${doc.created_at}`,
|
|
104
|
+
`**Context ID**: ${doc.context_id}`,
|
|
105
|
+
`**Session ID**: ${doc.session_id}`,
|
|
106
|
+
`**Reason**: ${formatReason(doc.reason)}`,
|
|
107
|
+
"",
|
|
108
|
+
"## Links",
|
|
109
|
+
"",
|
|
110
|
+
];
|
|
111
|
+
|
|
112
|
+
// Plan link
|
|
113
|
+
if (doc.plan_path) {
|
|
114
|
+
const planName = path.basename(doc.plan_path);
|
|
115
|
+
lines.push(`- **Plan**: [${planName}](${doc.plan_path})`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
lines.push(
|
|
119
|
+
`- **Context Folder**: \`${doc.context_folder}\``,
|
|
120
|
+
`- **Events Log**: \`${doc.events_log_path}\``,
|
|
121
|
+
"",
|
|
122
|
+
"## Current State",
|
|
123
|
+
"",
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
// Active tasks
|
|
127
|
+
lines.push(renderTaskList(doc.active_tasks, "Active Tasks", true).trimEnd());
|
|
128
|
+
lines.push("");
|
|
129
|
+
|
|
130
|
+
// Completed this session
|
|
131
|
+
if (doc.completed_tasks_this_session.length > 0) {
|
|
132
|
+
lines.push(renderTaskList(
|
|
133
|
+
doc.completed_tasks_this_session as any[],
|
|
134
|
+
"Completed This Session",
|
|
135
|
+
false,
|
|
136
|
+
).trimEnd());
|
|
137
|
+
lines.push("");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Work summary
|
|
141
|
+
if (doc.work_summary) {
|
|
142
|
+
lines.push("## Context Summary", "", doc.work_summary, "");
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Next steps
|
|
146
|
+
if (doc.next_steps.length > 0) {
|
|
147
|
+
lines.push("## Next Steps", "");
|
|
148
|
+
for (let i = 0; i < doc.next_steps.length; i++) {
|
|
149
|
+
lines.push(`${i + 1}. ${doc.next_steps[i]}`);
|
|
150
|
+
}
|
|
151
|
+
lines.push("");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Important notes
|
|
155
|
+
if (doc.important_notes.length > 0) {
|
|
156
|
+
lines.push("## Important Notes", "");
|
|
157
|
+
for (const note of doc.important_notes) {
|
|
158
|
+
lines.push(`- ${note}`);
|
|
159
|
+
}
|
|
160
|
+
lines.push("");
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Continuation prompt
|
|
164
|
+
lines.push(
|
|
165
|
+
"---",
|
|
166
|
+
"",
|
|
167
|
+
"**Continuation Prompt**:",
|
|
168
|
+
"```",
|
|
169
|
+
`Continue working on context "${doc.context_id}".`,
|
|
170
|
+
"",
|
|
171
|
+
`Handoff document: ${doc.file_path ?? "See above"}`,
|
|
172
|
+
"",
|
|
173
|
+
"Read the handoff document, restore tasks with TaskCreate, and continue implementation.",
|
|
174
|
+
"```",
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
return lines.join("\n");
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Generate the prompt to paste into new session for continuation.
|
|
182
|
+
* See SPEC.md §12.3
|
|
183
|
+
*/
|
|
184
|
+
export function getHandoffContinuationPrompt(doc: HandoffDocument): string {
|
|
185
|
+
return `Continue working on context "${doc.context_id}".\n\nHandoff document: ${doc.file_path}\n\nRead the handoff document, restore tasks with TaskCreate, and continue implementation.`;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Generate system reminder for low context warning.
|
|
190
|
+
* See SPEC.md §12.4
|
|
191
|
+
*/
|
|
192
|
+
export function getLowContextWarning(contextRemainingPercent: number, contextId: string): string {
|
|
193
|
+
return `<system-reminder>
|
|
194
|
+
## LOW CONTEXT WARNING (${contextRemainingPercent}% remaining)
|
|
195
|
+
|
|
196
|
+
Your context window is running low. Please:
|
|
197
|
+
|
|
198
|
+
1. **Finish current task** if 1-2 steps away, OR save current progress
|
|
199
|
+
2. **Create handoff document** by calling:
|
|
200
|
+
\`\`\`python
|
|
201
|
+
from _shared.lib.handoff import generate_handoff_document
|
|
202
|
+
doc = generate_handoff_document(
|
|
203
|
+
context_id="${contextId}",
|
|
204
|
+
reason="low_context",
|
|
205
|
+
work_summary="<describe current work>",
|
|
206
|
+
next_steps=["<step 1>", "<step 2>"],
|
|
207
|
+
important_notes=["<key decision 1>"]
|
|
208
|
+
)
|
|
209
|
+
\`\`\`
|
|
210
|
+
3. **Ask permission** to clear and paste continuation prompt
|
|
211
|
+
|
|
212
|
+
After creating handoff, ask the user:
|
|
213
|
+
"Context is low. I've created a handoff document. May I clear and continue in a new session?"
|
|
214
|
+
</system-reminder>`;
|
|
215
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "lib-ts-tests",
|
|
3
|
+
"private": true,
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"test": "mocha",
|
|
7
|
+
"test:unit": "mocha '__tests__/base/**/*.test.ts' '__tests__/templates/**/*.test.ts'",
|
|
8
|
+
"test:contract": "mocha '__tests__/context/**/*.test.ts' '__tests__/handoff/**/*.test.ts'",
|
|
9
|
+
"test:integration": "mocha '__tests__/integration/**/*.test.ts'",
|
|
10
|
+
"test:parity": "mocha '__tests__/integration/python-parity.test.ts'",
|
|
11
|
+
"fixtures": "python __tests__/fixtures/generate_fixtures.py"
|
|
12
|
+
},
|
|
13
|
+
"devDependencies": {
|
|
14
|
+
"mocha": "^10.0.0",
|
|
15
|
+
"chai": "^5.0.0",
|
|
16
|
+
"@types/mocha": "^10.0.0",
|
|
17
|
+
"@types/sinon": "^17.0.0",
|
|
18
|
+
"sinon": "^17.0.0",
|
|
19
|
+
"typescript": "^5.0.0"
|
|
20
|
+
}
|
|
21
|
+
}
|