aiwcli 0.12.1 → 0.12.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/templates/_shared/.claude/commands/handoff.md +44 -78
- package/dist/templates/_shared/hooks-ts/session_end.ts +16 -11
- package/dist/templates/_shared/hooks-ts/session_start.ts +25 -16
- package/dist/templates/_shared/hooks-ts/user_prompt_submit.ts +20 -8
- package/dist/templates/_shared/lib-ts/base/inference.ts +72 -23
- package/dist/templates/_shared/lib-ts/base/state-io.ts +12 -7
- package/dist/templates/_shared/lib-ts/context/context-formatter.ts +151 -29
- package/dist/templates/_shared/lib-ts/context/context-store.ts +35 -74
- package/dist/templates/_shared/lib-ts/types.ts +64 -63
- package/dist/templates/_shared/scripts/resolve_context.ts +14 -5
- package/dist/templates/_shared/scripts/resume_handoff.ts +41 -13
- package/dist/templates/_shared/scripts/save_handoff.ts +30 -31
- package/dist/templates/_shared/workflows/handoff.md +28 -6
- package/dist/templates/cc-native/.claude/commands/rlm/ask.md +136 -0
- package/dist/templates/cc-native/.claude/commands/rlm/index.md +21 -0
- package/dist/templates/cc-native/.claude/commands/rlm/overview.md +56 -0
- package/dist/templates/cc-native/TEMPLATE-SCHEMA.md +4 -4
- package/dist/templates/cc-native/_cc-native/agents/CLAUDE.md +1 -7
- package/dist/templates/cc-native/_cc-native/agents/plan-review/ARCH-EVOLUTION.md +62 -63
- package/dist/templates/cc-native/_cc-native/agents/plan-review/ARCH-PATTERNS.md +61 -62
- package/dist/templates/cc-native/_cc-native/agents/plan-review/ARCH-STRUCTURE.md +62 -63
- package/dist/templates/cc-native/_cc-native/agents/plan-review/ASSUMPTION-TRACER.md +56 -57
- package/dist/templates/cc-native/_cc-native/agents/plan-review/CLARITY-AUDITOR.md +53 -54
- package/dist/templates/cc-native/_cc-native/agents/plan-review/COMPLETENESS-FEASIBILITY.md +66 -67
- package/dist/templates/cc-native/_cc-native/agents/plan-review/COMPLETENESS-GAPS.md +70 -71
- package/dist/templates/cc-native/_cc-native/agents/plan-review/COMPLETENESS-ORDERING.md +62 -63
- package/dist/templates/cc-native/_cc-native/agents/plan-review/CONSTRAINT-VALIDATOR.md +72 -73
- package/dist/templates/cc-native/_cc-native/agents/plan-review/DESIGN-ADR-VALIDATOR.md +61 -62
- package/dist/templates/cc-native/_cc-native/agents/plan-review/DESIGN-SCALE-MATCHER.md +64 -65
- package/dist/templates/cc-native/_cc-native/agents/plan-review/DEVILS-ADVOCATE.md +56 -57
- package/dist/templates/cc-native/_cc-native/agents/plan-review/DOCUMENTATION-PHILOSOPHY.md +86 -87
- package/dist/templates/cc-native/_cc-native/agents/plan-review/HANDOFF-READINESS.md +59 -60
- package/dist/templates/cc-native/_cc-native/agents/plan-review/HIDDEN-COMPLEXITY.md +58 -59
- package/dist/templates/cc-native/_cc-native/agents/plan-review/INCREMENTAL-DELIVERY.md +66 -67
- package/dist/templates/cc-native/_cc-native/agents/plan-review/RISK-DEPENDENCY.md +62 -63
- package/dist/templates/cc-native/_cc-native/agents/plan-review/RISK-FMEA.md +66 -67
- package/dist/templates/cc-native/_cc-native/agents/plan-review/RISK-PREMORTEM.md +71 -72
- package/dist/templates/cc-native/_cc-native/agents/plan-review/RISK-REVERSIBILITY.md +74 -75
- package/dist/templates/cc-native/_cc-native/agents/plan-review/SCOPE-BOUNDARY.md +77 -78
- package/dist/templates/cc-native/_cc-native/agents/plan-review/SIMPLICITY-GUARDIAN.md +62 -63
- package/dist/templates/cc-native/_cc-native/agents/plan-review/SKEPTIC.md +68 -69
- package/dist/templates/cc-native/_cc-native/agents/plan-review/TESTDRIVEN-BEHAVIOR-AUDITOR.md +61 -62
- package/dist/templates/cc-native/_cc-native/agents/plan-review/TESTDRIVEN-CHARACTERIZATION.md +71 -72
- package/dist/templates/cc-native/_cc-native/agents/plan-review/TESTDRIVEN-FIRST-VALIDATOR.md +61 -62
- package/dist/templates/cc-native/_cc-native/agents/plan-review/TESTDRIVEN-PYRAMID-ANALYZER.md +61 -62
- package/dist/templates/cc-native/_cc-native/agents/plan-review/TRADEOFF-COSTS.md +67 -68
- package/dist/templates/cc-native/_cc-native/agents/plan-review/TRADEOFF-STAKEHOLDERS.md +65 -66
- package/dist/templates/cc-native/_cc-native/agents/plan-review/VERIFY-COVERAGE.md +74 -75
- package/dist/templates/cc-native/_cc-native/agents/plan-review/VERIFY-STRENGTH.md +69 -70
- package/dist/templates/cc-native/_cc-native/{plan-review.config.json → cc-native.config.json} +12 -0
- package/dist/templates/cc-native/_cc-native/hooks/CLAUDE.md +19 -2
- package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.ts +28 -1010
- package/dist/templates/cc-native/_cc-native/lib-ts/agent-selection.ts +163 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/aggregate-agents.ts +1 -2
- package/dist/templates/cc-native/_cc-native/lib-ts/artifacts/format.ts +597 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/artifacts/index.ts +26 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/artifacts/tracker.ts +107 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/artifacts/write.ts +119 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/artifacts.ts +19 -821
- package/dist/templates/cc-native/_cc-native/lib-ts/cc-native-state.ts +36 -13
- package/dist/templates/cc-native/_cc-native/lib-ts/config.ts +3 -3
- package/dist/templates/cc-native/_cc-native/lib-ts/graduation.ts +132 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/orchestrator.ts +1 -2
- package/dist/templates/cc-native/_cc-native/lib-ts/output-builder.ts +130 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/plan-discovery.ts +80 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/review-pipeline.ts +511 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/providers/orchestrator-claude-agent.ts +1 -1
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/CLAUDE.md +480 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/embedding-indexer.ts +287 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/hyde.ts +148 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/index.ts +54 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/logger.ts +58 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/ollama-client.ts +208 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/retrieval-pipeline.ts +460 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/transcript-indexer.ts +447 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/transcript-loader.ts +280 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/transcript-searcher.ts +274 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/types.ts +201 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/vector-store.ts +278 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/settings.ts +184 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/state.ts +51 -17
- package/dist/templates/cc-native/_cc-native/lib-ts/types.ts +42 -3
- package/oclif.manifest.json +1 -1
- package/package.json +1 -1
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import * as fs from "node:fs";
|
|
11
|
-
import * as
|
|
12
|
-
|
|
11
|
+
import * as path from "node:path";
|
|
13
12
|
import { parseIsoTimestamp } from "../base/utils.js";
|
|
13
|
+
import { getContextDir } from "../base/constants.js";
|
|
14
14
|
import type { ContextState, Task } from "../types.js";
|
|
15
15
|
|
|
16
16
|
const MAX_PLAN_INLINE_CHARS = 30_000;
|
|
@@ -42,10 +42,10 @@ export function getModeDisplay(mode: string): string {
|
|
|
42
42
|
* Format ISO timestamp as '2 hours ago', 'yesterday', etc.
|
|
43
43
|
* See SPEC.md §11.3
|
|
44
44
|
*/
|
|
45
|
-
export function formatRelativeTime(isoTimestamp:
|
|
45
|
+
export function formatRelativeTime(isoTimestamp: string | null): string {
|
|
46
46
|
if (!isoTimestamp) return "unknown";
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
let dt = parseIsoTimestamp(isoTimestamp);
|
|
49
49
|
if (!dt) return isoTimestamp.slice(0, 16);
|
|
50
50
|
|
|
51
51
|
const now = new Date();
|
|
@@ -62,10 +62,8 @@ export function formatRelativeTime(isoTimestamp: null | string): string {
|
|
|
62
62
|
if (diffMin === 0) return "just now";
|
|
63
63
|
return diffMin === 1 ? "1 minute ago" : `${diffMin} minutes ago`;
|
|
64
64
|
}
|
|
65
|
-
|
|
66
65
|
return diffHours === 1 ? "1 hour ago" : `${diffHours} hours ago`;
|
|
67
66
|
}
|
|
68
|
-
|
|
69
67
|
if (diffDays === 1) return "yesterday";
|
|
70
68
|
if (diffDays < 7) return `${diffDays} days ago`;
|
|
71
69
|
|
|
@@ -80,23 +78,21 @@ export function formatRelativeTime(isoTimestamp: null | string): string {
|
|
|
80
78
|
// Internal helpers
|
|
81
79
|
// ---------------------------------------------------------------------------
|
|
82
80
|
|
|
83
|
-
function taskAttr(task: Record<string, any
|
|
81
|
+
function taskAttr(task: Task | Record<string, any>, key: string, defaultVal = ""): string {
|
|
84
82
|
if (typeof task === "object" && task !== null) {
|
|
85
83
|
return (task as any)[key] ?? defaultVal;
|
|
86
84
|
}
|
|
87
|
-
|
|
88
85
|
return defaultVal;
|
|
89
86
|
}
|
|
90
87
|
|
|
91
|
-
function readPlanContent(planPath: string): [
|
|
88
|
+
function readPlanContent(planPath: string): [string | null, boolean, number] {
|
|
92
89
|
try {
|
|
93
90
|
if (!fs.existsSync(planPath)) return [null, false, 0];
|
|
94
|
-
const content = fs.readFileSync(planPath, "
|
|
91
|
+
const content = fs.readFileSync(planPath, "utf-8");
|
|
95
92
|
const total = content.length;
|
|
96
93
|
if (total > MAX_PLAN_INLINE_CHARS) {
|
|
97
94
|
return [content.slice(0, MAX_PLAN_INLINE_CHARS), true, total];
|
|
98
95
|
}
|
|
99
|
-
|
|
100
96
|
return [content, false, total];
|
|
101
97
|
} catch {
|
|
102
98
|
return [null, false, 0];
|
|
@@ -105,7 +101,7 @@ function readPlanContent(planPath: string): [null | string, boolean, number] {
|
|
|
105
101
|
|
|
106
102
|
function modeLabel(ctx: ContextState): string {
|
|
107
103
|
const d = getModeDisplay(ctx.mode ?? "idle");
|
|
108
|
-
return d ? d.
|
|
104
|
+
return d ? d.replace(/^\[|\]$/g, "") : "Active";
|
|
109
105
|
}
|
|
110
106
|
|
|
111
107
|
/**
|
|
@@ -124,7 +120,7 @@ export function buildRestoreSections(
|
|
|
124
120
|
const savedAt = lastSession.saved_at ?? "";
|
|
125
121
|
if (savedAt) {
|
|
126
122
|
const reason = lastSession.save_reason ?? "";
|
|
127
|
-
const reasonDisplay = reason ? reason.
|
|
123
|
+
const reasonDisplay = reason ? reason.replace(/_/g, " ") : "unknown";
|
|
128
124
|
sections.push(`**Last session ended:** ${formatRelativeTime(savedAt)} (${reasonDisplay})`);
|
|
129
125
|
}
|
|
130
126
|
}
|
|
@@ -143,7 +139,6 @@ export function buildRestoreSections(
|
|
|
143
139
|
buckets[s]!.push(taskAttr(t, "subject"));
|
|
144
140
|
}
|
|
145
141
|
}
|
|
146
|
-
|
|
147
142
|
if (Object.values(buckets).some(b => b.length > 0)) {
|
|
148
143
|
sections.push("", `### Previous Work (${tasks.length} tasks)`, "");
|
|
149
144
|
const marks: Record<string, string> = {
|
|
@@ -201,7 +196,8 @@ function resumeBlock(ctx: ContextState, projectRoot: string | undefined, modeTex
|
|
|
201
196
|
];
|
|
202
197
|
const restore = buildRestoreSections(ctx, projectRoot, true);
|
|
203
198
|
if (restore) lines.push(restore);
|
|
204
|
-
lines.push("", "---", "", "**Instructions:**"
|
|
199
|
+
lines.push("", "---", "", "**Instructions:**");
|
|
200
|
+
lines.push(...instructions);
|
|
205
201
|
return lines.join("\n");
|
|
206
202
|
}
|
|
207
203
|
|
|
@@ -223,12 +219,12 @@ export function formatHandoffContinuation(ctx: ContextState, projectRoot?: strin
|
|
|
223
219
|
|
|
224
220
|
try {
|
|
225
221
|
if (handoffPath && fs.existsSync(handoffPath)) {
|
|
226
|
-
lines.push("### Previous Session Handoff", "", fs.readFileSync(handoffPath, "
|
|
222
|
+
lines.push("### Previous Session Handoff", "", fs.readFileSync(handoffPath, "utf-8"), "");
|
|
227
223
|
} else {
|
|
228
224
|
lines.push(`*Handoff document not found at \`${handoffPath}\`*`, "");
|
|
229
225
|
}
|
|
230
|
-
} catch (
|
|
231
|
-
lines.push(`*Handoff document at \`${handoffPath}\` could not be read: ${
|
|
226
|
+
} catch (e: any) {
|
|
227
|
+
lines.push(`*Handoff document at \`${handoffPath}\` could not be read: ${e}*`, "");
|
|
232
228
|
}
|
|
233
229
|
|
|
234
230
|
const restore = buildRestoreSections(ctx, projectRoot, true);
|
|
@@ -271,21 +267,20 @@ export function formatContextList(contexts: ContextState[]): string {
|
|
|
271
267
|
if (contexts.length === 0) return "No active contexts found.";
|
|
272
268
|
|
|
273
269
|
const lines = ["## Active Contexts\n"];
|
|
274
|
-
for (
|
|
275
|
-
const ctx =
|
|
270
|
+
for (let i = 0; i < contexts.length; i++) {
|
|
271
|
+
const ctx = contexts[i]!;
|
|
276
272
|
const timeStr = formatRelativeTime(ctx.last_active);
|
|
277
273
|
const md = getModeDisplay(ctx.mode ?? "idle");
|
|
278
274
|
const si = md ? ` ${md}` : "";
|
|
279
|
-
lines.push(`**${i + 1}. ${ctx.id}**${si}
|
|
275
|
+
lines.push(`**${i + 1}. ${ctx.id}**${si}`);
|
|
276
|
+
lines.push(` ${ctx.summary}`);
|
|
280
277
|
if (ctx.method) {
|
|
281
278
|
lines.push(` Method: ${ctx.method} | Last active: ${timeStr}`);
|
|
282
279
|
} else {
|
|
283
280
|
lines.push(` Last active: ${timeStr}`);
|
|
284
281
|
}
|
|
285
|
-
|
|
286
282
|
lines.push("");
|
|
287
283
|
}
|
|
288
|
-
|
|
289
284
|
return lines.join("\n");
|
|
290
285
|
}
|
|
291
286
|
|
|
@@ -355,11 +350,11 @@ export function formatContextPickerStderr(contexts: ContextState[]): string {
|
|
|
355
350
|
];
|
|
356
351
|
|
|
357
352
|
let selectableCount = 0;
|
|
358
|
-
for (
|
|
359
|
-
const ctx =
|
|
353
|
+
for (let i = 0; i < contexts.length; i++) {
|
|
354
|
+
const ctx = contexts[i]!;
|
|
360
355
|
const timeStr = formatRelativeTime(ctx.last_active);
|
|
361
356
|
const mode = ctx.mode ?? "idle";
|
|
362
|
-
const isSelectable = mode === "active" ||
|
|
357
|
+
const isSelectable = mode === "active" || !!ctx.handoff_path;
|
|
363
358
|
if (isSelectable) selectableCount++;
|
|
364
359
|
|
|
365
360
|
let status = "";
|
|
@@ -372,7 +367,10 @@ export function formatContextPickerStderr(contexts: ContextState[]): string {
|
|
|
372
367
|
const summary = ctx.summary.length > 48 ? ctx.summary.slice(0, 45) + "..." : ctx.summary;
|
|
373
368
|
const selTag = isSelectable ? " [selectable]" : " [end only]";
|
|
374
369
|
|
|
375
|
-
lines.push(`| ^${i + 1} ${ctx.id}${status}${selTag}
|
|
370
|
+
lines.push(`| ^${i + 1} ${ctx.id}${status}${selTag}`);
|
|
371
|
+
lines.push(`| ${summary}`);
|
|
372
|
+
lines.push(`| [${timeStr}]`);
|
|
373
|
+
lines.push("|");
|
|
376
374
|
}
|
|
377
375
|
|
|
378
376
|
lines.push(
|
|
@@ -397,7 +395,6 @@ export function formatContextPickerStderr(contexts: ContextState[]): string {
|
|
|
397
395
|
"+----------------------------------------------------------------+",
|
|
398
396
|
);
|
|
399
397
|
}
|
|
400
|
-
|
|
401
398
|
lines.push("");
|
|
402
399
|
return lines.join("\n");
|
|
403
400
|
}
|
|
@@ -417,7 +414,6 @@ export function formatCommandFeedback(
|
|
|
417
414
|
const s = ctx.summary.length > 50 ? ctx.summary.slice(0, 50) + "..." : ctx.summary;
|
|
418
415
|
lines.push(`- **${ctx.id}**: ${s}`);
|
|
419
416
|
}
|
|
420
|
-
|
|
421
417
|
lines.push("");
|
|
422
418
|
}
|
|
423
419
|
|
|
@@ -433,6 +429,132 @@ export function formatCommandFeedback(
|
|
|
433
429
|
"Tasks created with TaskCreate will be persisted to this context.",
|
|
434
430
|
);
|
|
435
431
|
}
|
|
432
|
+
return lines.join("\n");
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// ---------------------------------------------------------------------------
|
|
436
|
+
// Context Inventory
|
|
437
|
+
// ---------------------------------------------------------------------------
|
|
438
|
+
|
|
439
|
+
/** Collector function: scans one aspect of the context folder, returns markdown or null. */
|
|
440
|
+
type InventoryCollector = (
|
|
441
|
+
contextId: string,
|
|
442
|
+
contextDir: string,
|
|
443
|
+
state: ContextState,
|
|
444
|
+
) => string | null;
|
|
445
|
+
|
|
446
|
+
/** Descriptions for known context subfolders. */
|
|
447
|
+
const KNOWN_FOLDERS: Record<string, string> = {
|
|
448
|
+
"plans": "Archived implementation plans from plan mode",
|
|
449
|
+
"session-transcripts": "JSONL records of previous agent sessions — read these to understand prior work",
|
|
450
|
+
"handoffs": "Structured briefing documents for session continuity",
|
|
451
|
+
"reviews": "Plan review artifacts (reviewer verdicts, corroboration reports)",
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
function collectFolderPath(contextId: string, contextDir: string, state: ContextState): string | null {
|
|
455
|
+
if (!fs.existsSync(contextDir)) return null;
|
|
456
|
+
return `**Context folder:** \`${contextDir}\`\n**State file:** \`${path.join(contextDir, "state.json")}\` — contains session history, task records, plan/handoff metadata`;
|
|
457
|
+
}
|
|
436
458
|
|
|
459
|
+
function collectStatePointers(contextId: string, contextDir: string, state: ContextState): string | null {
|
|
460
|
+
const pointers: string[] = [];
|
|
461
|
+
if (state.plan_path) {
|
|
462
|
+
const exists = fs.existsSync(state.plan_path);
|
|
463
|
+
pointers.push(`- **Active plan:** \`${state.plan_path}\`${exists ? "" : " (not found)"}`);
|
|
464
|
+
}
|
|
465
|
+
if (state.handoff_path) {
|
|
466
|
+
const exists = fs.existsSync(state.handoff_path);
|
|
467
|
+
pointers.push(`- **Active handoff:** \`${state.handoff_path}\`${exists ? "" : " (not found)"}`);
|
|
468
|
+
}
|
|
469
|
+
if (pointers.length === 0) return null;
|
|
470
|
+
return "**Key artifacts:**\n" + pointers.join("\n");
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function countFilesRecursive(dirPath: string): number {
|
|
474
|
+
let count = 0;
|
|
475
|
+
try {
|
|
476
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
477
|
+
for (const entry of entries) {
|
|
478
|
+
if (entry.isFile()) {
|
|
479
|
+
count++;
|
|
480
|
+
} else if (entry.isDirectory()) {
|
|
481
|
+
count += countFilesRecursive(path.join(dirPath, entry.name));
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
} catch { /* permission errors, etc. */ }
|
|
485
|
+
return count;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function collectFolderInventory(contextId: string, contextDir: string, state: ContextState): string | null {
|
|
489
|
+
if (!fs.existsSync(contextDir)) return null;
|
|
490
|
+
let entries: fs.Dirent[];
|
|
491
|
+
try {
|
|
492
|
+
entries = fs.readdirSync(contextDir, { withFileTypes: true });
|
|
493
|
+
} catch { return null; }
|
|
494
|
+
|
|
495
|
+
const dirs = entries.filter(e => e.isDirectory()).sort((a, b) => a.name.localeCompare(b.name));
|
|
496
|
+
if (dirs.length === 0) return null;
|
|
497
|
+
|
|
498
|
+
const lines: string[] = ["**Available folders:**"];
|
|
499
|
+
for (const dir of dirs) {
|
|
500
|
+
const dirPath = path.join(contextDir, dir.name);
|
|
501
|
+
const desc = KNOWN_FOLDERS[dir.name] ?? "Project-specific artifacts";
|
|
502
|
+
const fileCount = countFilesRecursive(dirPath);
|
|
503
|
+
lines.push(`- \`${dir.name}/\` — ${desc} (${fileCount} file${fileCount !== 1 ? "s" : ""})`);
|
|
504
|
+
}
|
|
437
505
|
return lines.join("\n");
|
|
438
506
|
}
|
|
507
|
+
|
|
508
|
+
function collectSessionStats(contextId: string, contextDir: string, state: ContextState): string | null {
|
|
509
|
+
const sessionCount = (state.session_ids ?? []).length;
|
|
510
|
+
if (sessionCount === 0) return null;
|
|
511
|
+
|
|
512
|
+
const transcriptsDir = path.join(contextDir, "session-transcripts");
|
|
513
|
+
let transcriptCount = 0;
|
|
514
|
+
let timeRange = "";
|
|
515
|
+
|
|
516
|
+
if (fs.existsSync(transcriptsDir)) {
|
|
517
|
+
try {
|
|
518
|
+
const files = fs.readdirSync(transcriptsDir).filter(f => f.endsWith(".jsonl")).sort();
|
|
519
|
+
transcriptCount = files.length;
|
|
520
|
+
if (files.length > 1) {
|
|
521
|
+
const oldest = files[0]!.slice(0, 10);
|
|
522
|
+
const newest = files[files.length - 1]!.slice(0, 10);
|
|
523
|
+
if (oldest !== newest) timeRange = ` (${oldest} to ${newest})`;
|
|
524
|
+
}
|
|
525
|
+
} catch { /* ignore */ }
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
let line = `**Sessions:** ${sessionCount} total`;
|
|
529
|
+
if (transcriptCount > 0) {
|
|
530
|
+
line += `, ${transcriptCount} transcript${transcriptCount !== 1 ? "s" : ""} archived${timeRange}`;
|
|
531
|
+
}
|
|
532
|
+
return line;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/** Ordered list of inventory collectors. Append new collectors here. */
|
|
536
|
+
const INVENTORY_COLLECTORS: InventoryCollector[] = [
|
|
537
|
+
collectFolderPath,
|
|
538
|
+
collectStatePointers,
|
|
539
|
+
collectFolderInventory,
|
|
540
|
+
collectSessionStats,
|
|
541
|
+
];
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Build a markdown inventory of resources available in the context folder.
|
|
545
|
+
* Returns null if the context folder doesn't exist yet (brand new context).
|
|
546
|
+
*/
|
|
547
|
+
export function buildContextInventory(
|
|
548
|
+
state: ContextState,
|
|
549
|
+
projectRoot: string,
|
|
550
|
+
): string | null {
|
|
551
|
+
const contextDir = getContextDir(state.id, projectRoot);
|
|
552
|
+
if (!fs.existsSync(contextDir)) return null;
|
|
553
|
+
|
|
554
|
+
const sections = INVENTORY_COLLECTORS
|
|
555
|
+
.map(c => c(state.id, contextDir, state))
|
|
556
|
+
.filter((s): s is string => s !== null);
|
|
557
|
+
|
|
558
|
+
if (sections.length === 0) return null;
|
|
559
|
+
return "### Context Resources\n\n" + sections.join("\n\n");
|
|
560
|
+
}
|
|
@@ -9,21 +9,20 @@
|
|
|
9
9
|
|
|
10
10
|
import * as fs from "node:fs";
|
|
11
11
|
import * as path from "node:path";
|
|
12
|
-
|
|
12
|
+
import { readStateJson, writeStateJson, toDict, dictToState } from "../base/state-io.js";
|
|
13
13
|
import { atomicWrite } from "../base/atomic-write.js";
|
|
14
14
|
import {
|
|
15
|
-
getArchiveContextDir,
|
|
16
|
-
getArchiveDir,
|
|
17
|
-
getArchiveIndexPath,
|
|
18
15
|
getContextDir,
|
|
19
16
|
getContextsDir,
|
|
20
17
|
getIndexPath,
|
|
18
|
+
getArchiveDir,
|
|
19
|
+
getArchiveContextDir,
|
|
20
|
+
getArchiveIndexPath,
|
|
21
21
|
validateContextId,
|
|
22
22
|
} from "../base/constants.js";
|
|
23
|
-
import { logDebug
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
import type { ContextState, IndexEntry, IndexFile, Mode } from "../types.js";
|
|
23
|
+
import { logDebug, logInfo, logWarn, logError, setContextPath } from "../base/logger.js";
|
|
24
|
+
import { nowIso, generateContextId } from "../base/utils.js";
|
|
25
|
+
import type { ContextState, IndexFile, IndexEntry, Mode } from "../types.js";
|
|
27
26
|
|
|
28
27
|
const INDEX_VERSION = "3.0";
|
|
29
28
|
|
|
@@ -35,13 +34,12 @@ function loadIndex(projectRoot?: string): IndexFile {
|
|
|
35
34
|
const indexPath = getIndexPath(projectRoot);
|
|
36
35
|
if (fs.existsSync(indexPath)) {
|
|
37
36
|
try {
|
|
38
|
-
const raw = fs.readFileSync(indexPath, "
|
|
37
|
+
const raw = fs.readFileSync(indexPath, "utf-8");
|
|
39
38
|
return JSON.parse(raw) as IndexFile;
|
|
40
|
-
} catch (
|
|
41
|
-
logWarn("context_store", `Failed to read index, recreating: ${
|
|
39
|
+
} catch (e: any) {
|
|
40
|
+
logWarn("context_store", `Failed to read index, recreating: ${e}`);
|
|
42
41
|
}
|
|
43
42
|
}
|
|
44
|
-
|
|
45
43
|
return { version: INDEX_VERSION, updated_at: nowIso(), sessions: {}, contexts: {} };
|
|
46
44
|
}
|
|
47
45
|
|
|
@@ -52,7 +50,6 @@ function saveIndex(index: IndexFile, projectRoot?: string): boolean {
|
|
|
52
50
|
if (!success) {
|
|
53
51
|
logWarn("context_store", `Failed to write index: ${error}`);
|
|
54
52
|
}
|
|
55
|
-
|
|
56
53
|
return success;
|
|
57
54
|
}
|
|
58
55
|
|
|
@@ -72,7 +69,7 @@ function migrateContextJson(contextId: string, projectRoot?: string): ContextSta
|
|
|
72
69
|
if (!fs.existsSync(legacyPath)) return null;
|
|
73
70
|
|
|
74
71
|
try {
|
|
75
|
-
const data = JSON.parse(fs.readFileSync(legacyPath, "
|
|
72
|
+
const data = JSON.parse(fs.readFileSync(legacyPath, "utf-8"));
|
|
76
73
|
const inFlight = data.in_flight ?? {};
|
|
77
74
|
const oldMode = inFlight.mode ?? "none";
|
|
78
75
|
const MODE_MIGRATION: Record<string, string> = {
|
|
@@ -107,8 +104,8 @@ function migrateContextJson(contextId: string, projectRoot?: string): ContextSta
|
|
|
107
104
|
last_session: null,
|
|
108
105
|
tasks: [],
|
|
109
106
|
};
|
|
110
|
-
} catch (
|
|
111
|
-
logWarn("context_store", `Failed to migrate context.json for '${contextId}': ${
|
|
107
|
+
} catch (e: any) {
|
|
108
|
+
logWarn("context_store", `Failed to migrate context.json for '${contextId}': ${e}`);
|
|
112
109
|
return null;
|
|
113
110
|
}
|
|
114
111
|
}
|
|
@@ -137,7 +134,7 @@ export function saveState(
|
|
|
137
134
|
contextId: string,
|
|
138
135
|
state: ContextState,
|
|
139
136
|
projectRoot?: string,
|
|
140
|
-
): [boolean,
|
|
137
|
+
): [boolean, string | null] {
|
|
141
138
|
// Ensure the state ID matches
|
|
142
139
|
state.id = contextId;
|
|
143
140
|
|
|
@@ -155,12 +152,10 @@ export function saveState(
|
|
|
155
152
|
if (!index.sessions) index.sessions = {} as Record<string, string>;
|
|
156
153
|
index.sessions[sid] = contextId;
|
|
157
154
|
}
|
|
158
|
-
|
|
159
155
|
const indexOk = saveIndex(index, projectRoot);
|
|
160
156
|
if (!indexOk) {
|
|
161
157
|
return [true, "state.json saved but index.json update failed"];
|
|
162
158
|
}
|
|
163
|
-
|
|
164
159
|
return [true, null];
|
|
165
160
|
}
|
|
166
161
|
|
|
@@ -170,7 +165,7 @@ export function saveState(
|
|
|
170
165
|
* See SPEC.md §7.4
|
|
171
166
|
*/
|
|
172
167
|
export function createContext(
|
|
173
|
-
contextId:
|
|
168
|
+
contextId: string | null,
|
|
174
169
|
summary: string,
|
|
175
170
|
method = "",
|
|
176
171
|
projectRoot?: string,
|
|
@@ -190,7 +185,6 @@ export function createContext(
|
|
|
190
185
|
} catch { /* ignore */ }
|
|
191
186
|
}
|
|
192
187
|
}
|
|
193
|
-
|
|
194
188
|
contextId = generateContextId(summary, existingIds);
|
|
195
189
|
}
|
|
196
190
|
|
|
@@ -219,6 +213,7 @@ export function createContext(
|
|
|
219
213
|
plan_id: null,
|
|
220
214
|
plan_anchors: [],
|
|
221
215
|
plan_consumed: false,
|
|
216
|
+
plan_hash_consumed: null,
|
|
222
217
|
handoff_path: null,
|
|
223
218
|
handoff_consumed: false,
|
|
224
219
|
session_ids: [],
|
|
@@ -241,7 +236,6 @@ export function getContext(contextId: string, projectRoot?: string): ContextStat
|
|
|
241
236
|
} catch {
|
|
242
237
|
return null;
|
|
243
238
|
}
|
|
244
|
-
|
|
245
239
|
return loadState(contextId, projectRoot);
|
|
246
240
|
}
|
|
247
241
|
|
|
@@ -279,7 +273,6 @@ export function getAllContexts(
|
|
|
279
273
|
try {
|
|
280
274
|
if (!fs.statSync(fullPath).isDirectory()) continue;
|
|
281
275
|
} catch { continue; }
|
|
282
|
-
|
|
283
276
|
const state = loadState(entry, projectRoot);
|
|
284
277
|
if (state && (!status || state.status === status)) {
|
|
285
278
|
results.push(state);
|
|
@@ -298,7 +291,7 @@ export function getAllContexts(
|
|
|
298
291
|
*/
|
|
299
292
|
export function updateContext(
|
|
300
293
|
contextId: string,
|
|
301
|
-
updates: Partial<Pick<ContextState, "
|
|
294
|
+
updates: Partial<Pick<ContextState, "summary" | "tags" | "method">>,
|
|
302
295
|
projectRoot?: string,
|
|
303
296
|
): ContextState | null {
|
|
304
297
|
const state = getContext(contextId, projectRoot);
|
|
@@ -348,7 +341,6 @@ export function getContextBySessionId(
|
|
|
348
341
|
return state;
|
|
349
342
|
}
|
|
350
343
|
}
|
|
351
|
-
|
|
352
344
|
return null;
|
|
353
345
|
}
|
|
354
346
|
|
|
@@ -380,7 +372,6 @@ export function bindSession(
|
|
|
380
372
|
if (!state.session_ids.includes(sessionId)) {
|
|
381
373
|
state.session_ids.push(sessionId);
|
|
382
374
|
}
|
|
383
|
-
|
|
384
375
|
state.last_active = nowIso();
|
|
385
376
|
|
|
386
377
|
const [success] = saveState(contextId, state, projectRoot);
|
|
@@ -396,13 +387,14 @@ export function updateMode(
|
|
|
396
387
|
mode: Mode,
|
|
397
388
|
projectRoot?: string,
|
|
398
389
|
opts?: {
|
|
399
|
-
handoff_consumed?: boolean;
|
|
400
|
-
plan_anchors?: string[];
|
|
401
|
-
plan_consumed?: boolean;
|
|
402
|
-
plan_hash?: string;
|
|
403
|
-
plan_id?: string;
|
|
404
390
|
plan_path?: string;
|
|
391
|
+
plan_hash?: string;
|
|
405
392
|
plan_signature?: string;
|
|
393
|
+
plan_id?: string;
|
|
394
|
+
plan_anchors?: string[];
|
|
395
|
+
plan_consumed?: boolean;
|
|
396
|
+
plan_hash_consumed?: string;
|
|
397
|
+
handoff_consumed?: boolean;
|
|
406
398
|
},
|
|
407
399
|
): ContextState | null {
|
|
408
400
|
const state = getContext(contextId, projectRoot);
|
|
@@ -418,6 +410,7 @@ export function updateMode(
|
|
|
418
410
|
if (opts.plan_id !== undefined) state.plan_id = opts.plan_id;
|
|
419
411
|
if (opts.plan_anchors !== undefined) state.plan_anchors = opts.plan_anchors;
|
|
420
412
|
if (opts.plan_consumed !== undefined) state.plan_consumed = opts.plan_consumed;
|
|
413
|
+
if (opts.plan_hash_consumed !== undefined) state.plan_hash_consumed = opts.plan_hash_consumed;
|
|
421
414
|
if (opts.handoff_consumed !== undefined) state.handoff_consumed = opts.handoff_consumed;
|
|
422
415
|
}
|
|
423
416
|
|
|
@@ -429,6 +422,7 @@ export function updateMode(
|
|
|
429
422
|
state.plan_id = null;
|
|
430
423
|
state.plan_anchors = [];
|
|
431
424
|
state.plan_consumed = false;
|
|
425
|
+
state.plan_hash_consumed = null;
|
|
432
426
|
state.handoff_consumed = false;
|
|
433
427
|
}
|
|
434
428
|
|
|
@@ -500,7 +494,6 @@ export function archiveContext(contextId: string, projectRoot?: string): Context
|
|
|
500
494
|
logWarn("context_store", `Cannot archive: context '${contextId}' not found`);
|
|
501
495
|
return null;
|
|
502
496
|
}
|
|
503
|
-
|
|
504
497
|
if (state.status !== "completed") {
|
|
505
498
|
logWarn("context_store", `Cannot archive: context '${contextId}' not completed`);
|
|
506
499
|
return null;
|
|
@@ -519,8 +512,8 @@ export function archiveContext(contextId: string, projectRoot?: string): Context
|
|
|
519
512
|
|
|
520
513
|
try {
|
|
521
514
|
fs.renameSync(sourceDir, archiveDest);
|
|
522
|
-
} catch (
|
|
523
|
-
logError("context_store", `Failed to move context to archive: ${
|
|
515
|
+
} catch (e: any) {
|
|
516
|
+
logError("context_store", `Failed to move context to archive: ${e}`);
|
|
524
517
|
return null;
|
|
525
518
|
}
|
|
526
519
|
|
|
@@ -531,7 +524,6 @@ export function archiveContext(contextId: string, projectRoot?: string): Context
|
|
|
531
524
|
for (const [sid, cid] of Object.entries(sessions)) {
|
|
532
525
|
if (cid === contextId) delete sessions[sid];
|
|
533
526
|
}
|
|
534
|
-
|
|
535
527
|
saveIndex(index, projectRoot);
|
|
536
528
|
|
|
537
529
|
// Add to archive index
|
|
@@ -551,7 +543,6 @@ export function reopenContext(contextId: string, projectRoot?: string): ContextS
|
|
|
551
543
|
if (!state) {
|
|
552
544
|
state = restoreFromArchive(contextId, projectRoot);
|
|
553
545
|
}
|
|
554
|
-
|
|
555
546
|
if (!state) return null;
|
|
556
547
|
|
|
557
548
|
if (state.status === "active") {
|
|
@@ -592,34 +583,6 @@ export function createContextFromPrompt(
|
|
|
592
583
|
);
|
|
593
584
|
}
|
|
594
585
|
|
|
595
|
-
/**
|
|
596
|
-
* Find the active context ID programmatically.
|
|
597
|
-
* Checks CONTEXT_ID env var first, then searches for the single active context.
|
|
598
|
-
* Returns null if no active context or multiple active contexts found.
|
|
599
|
-
*/
|
|
600
|
-
export function findActiveContextId(projectRoot?: string): null | string {
|
|
601
|
-
// Env var takes priority
|
|
602
|
-
const envId = process.env.CONTEXT_ID;
|
|
603
|
-
if (envId) {
|
|
604
|
-
const ctx = getContext(envId, projectRoot);
|
|
605
|
-
if (ctx) return ctx.id;
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
// Search for active contexts
|
|
609
|
-
const active = getAllContexts("active", projectRoot)
|
|
610
|
-
.filter(c => c.mode === "active" || c.mode === "has_plan" || c.mode === "has_handoff");
|
|
611
|
-
|
|
612
|
-
if (active.length === 1) return active[0]!.id;
|
|
613
|
-
if (active.length > 1) {
|
|
614
|
-
// Multiple active — try to find the most recently active
|
|
615
|
-
const sorted = active.sort((a, b) =>
|
|
616
|
-
(b.last_active ?? "").localeCompare(a.last_active ?? ""),
|
|
617
|
-
);
|
|
618
|
-
return sorted[0]!.id;
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
return null;
|
|
622
|
-
}
|
|
623
586
|
|
|
624
587
|
// ---------------------------------------------------------------------------
|
|
625
588
|
// Archive helpers
|
|
@@ -639,9 +602,9 @@ function updateArchiveIndex(state: ContextState, projectRoot?: string): boolean
|
|
|
639
602
|
|
|
640
603
|
if (fs.existsSync(archiveIndexPath)) {
|
|
641
604
|
try {
|
|
642
|
-
archiveIndex = JSON.parse(fs.readFileSync(archiveIndexPath, "
|
|
643
|
-
} catch (
|
|
644
|
-
logWarn("context_store", `Failed to read archive index, recreating: ${
|
|
605
|
+
archiveIndex = JSON.parse(fs.readFileSync(archiveIndexPath, "utf-8"));
|
|
606
|
+
} catch (e: any) {
|
|
607
|
+
logWarn("context_store", `Failed to read archive index, recreating: ${e}`);
|
|
645
608
|
}
|
|
646
609
|
}
|
|
647
610
|
|
|
@@ -653,7 +616,6 @@ function updateArchiveIndex(state: ContextState, projectRoot?: string): boolean
|
|
|
653
616
|
if (!success) {
|
|
654
617
|
logWarn("context_store", `Failed to write archive index: ${error}`);
|
|
655
618
|
}
|
|
656
|
-
|
|
657
619
|
return success;
|
|
658
620
|
}
|
|
659
621
|
|
|
@@ -669,8 +631,8 @@ function restoreFromArchive(contextId: string, projectRoot?: string): ContextSta
|
|
|
669
631
|
|
|
670
632
|
try {
|
|
671
633
|
fs.renameSync(archiveDir, activeDir);
|
|
672
|
-
} catch (
|
|
673
|
-
logError("context_store", `Failed to restore context from archive: ${
|
|
634
|
+
} catch (e: any) {
|
|
635
|
+
logError("context_store", `Failed to restore context from archive: ${e}`);
|
|
674
636
|
return null;
|
|
675
637
|
}
|
|
676
638
|
|
|
@@ -687,7 +649,7 @@ function removeFromArchiveIndex(contextId: string, projectRoot?: string): boolea
|
|
|
687
649
|
if (!fs.existsSync(archiveIndexPath)) return true;
|
|
688
650
|
|
|
689
651
|
try {
|
|
690
|
-
const archiveIndex = JSON.parse(fs.readFileSync(archiveIndexPath, "
|
|
652
|
+
const archiveIndex = JSON.parse(fs.readFileSync(archiveIndexPath, "utf-8")) as IndexFile;
|
|
691
653
|
if (archiveIndex.contexts[contextId]) {
|
|
692
654
|
delete archiveIndex.contexts[contextId];
|
|
693
655
|
archiveIndex.updated_at = nowIso();
|
|
@@ -698,10 +660,9 @@ function removeFromArchiveIndex(contextId: string, projectRoot?: string): boolea
|
|
|
698
660
|
return false;
|
|
699
661
|
}
|
|
700
662
|
}
|
|
701
|
-
|
|
702
663
|
return true;
|
|
703
|
-
} catch (
|
|
704
|
-
logWarn("context_store", `Failed to read archive index: ${
|
|
664
|
+
} catch (e: any) {
|
|
665
|
+
logWarn("context_store", `Failed to read archive index: ${e}`);
|
|
705
666
|
return false;
|
|
706
667
|
}
|
|
707
668
|
}
|