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.
Files changed (84) hide show
  1. package/dist/templates/_shared/.claude/commands/handoff.md +44 -78
  2. package/dist/templates/_shared/hooks-ts/session_end.ts +16 -11
  3. package/dist/templates/_shared/hooks-ts/session_start.ts +25 -16
  4. package/dist/templates/_shared/hooks-ts/user_prompt_submit.ts +20 -8
  5. package/dist/templates/_shared/lib-ts/base/inference.ts +72 -23
  6. package/dist/templates/_shared/lib-ts/base/state-io.ts +12 -7
  7. package/dist/templates/_shared/lib-ts/context/context-formatter.ts +151 -29
  8. package/dist/templates/_shared/lib-ts/context/context-store.ts +35 -74
  9. package/dist/templates/_shared/lib-ts/types.ts +64 -63
  10. package/dist/templates/_shared/scripts/resolve_context.ts +14 -5
  11. package/dist/templates/_shared/scripts/resume_handoff.ts +41 -13
  12. package/dist/templates/_shared/scripts/save_handoff.ts +30 -31
  13. package/dist/templates/_shared/workflows/handoff.md +28 -6
  14. package/dist/templates/cc-native/.claude/commands/rlm/ask.md +136 -0
  15. package/dist/templates/cc-native/.claude/commands/rlm/index.md +21 -0
  16. package/dist/templates/cc-native/.claude/commands/rlm/overview.md +56 -0
  17. package/dist/templates/cc-native/TEMPLATE-SCHEMA.md +4 -4
  18. package/dist/templates/cc-native/_cc-native/agents/CLAUDE.md +1 -7
  19. package/dist/templates/cc-native/_cc-native/agents/plan-review/ARCH-EVOLUTION.md +62 -63
  20. package/dist/templates/cc-native/_cc-native/agents/plan-review/ARCH-PATTERNS.md +61 -62
  21. package/dist/templates/cc-native/_cc-native/agents/plan-review/ARCH-STRUCTURE.md +62 -63
  22. package/dist/templates/cc-native/_cc-native/agents/plan-review/ASSUMPTION-TRACER.md +56 -57
  23. package/dist/templates/cc-native/_cc-native/agents/plan-review/CLARITY-AUDITOR.md +53 -54
  24. package/dist/templates/cc-native/_cc-native/agents/plan-review/COMPLETENESS-FEASIBILITY.md +66 -67
  25. package/dist/templates/cc-native/_cc-native/agents/plan-review/COMPLETENESS-GAPS.md +70 -71
  26. package/dist/templates/cc-native/_cc-native/agents/plan-review/COMPLETENESS-ORDERING.md +62 -63
  27. package/dist/templates/cc-native/_cc-native/agents/plan-review/CONSTRAINT-VALIDATOR.md +72 -73
  28. package/dist/templates/cc-native/_cc-native/agents/plan-review/DESIGN-ADR-VALIDATOR.md +61 -62
  29. package/dist/templates/cc-native/_cc-native/agents/plan-review/DESIGN-SCALE-MATCHER.md +64 -65
  30. package/dist/templates/cc-native/_cc-native/agents/plan-review/DEVILS-ADVOCATE.md +56 -57
  31. package/dist/templates/cc-native/_cc-native/agents/plan-review/DOCUMENTATION-PHILOSOPHY.md +86 -87
  32. package/dist/templates/cc-native/_cc-native/agents/plan-review/HANDOFF-READINESS.md +59 -60
  33. package/dist/templates/cc-native/_cc-native/agents/plan-review/HIDDEN-COMPLEXITY.md +58 -59
  34. package/dist/templates/cc-native/_cc-native/agents/plan-review/INCREMENTAL-DELIVERY.md +66 -67
  35. package/dist/templates/cc-native/_cc-native/agents/plan-review/RISK-DEPENDENCY.md +62 -63
  36. package/dist/templates/cc-native/_cc-native/agents/plan-review/RISK-FMEA.md +66 -67
  37. package/dist/templates/cc-native/_cc-native/agents/plan-review/RISK-PREMORTEM.md +71 -72
  38. package/dist/templates/cc-native/_cc-native/agents/plan-review/RISK-REVERSIBILITY.md +74 -75
  39. package/dist/templates/cc-native/_cc-native/agents/plan-review/SCOPE-BOUNDARY.md +77 -78
  40. package/dist/templates/cc-native/_cc-native/agents/plan-review/SIMPLICITY-GUARDIAN.md +62 -63
  41. package/dist/templates/cc-native/_cc-native/agents/plan-review/SKEPTIC.md +68 -69
  42. package/dist/templates/cc-native/_cc-native/agents/plan-review/TESTDRIVEN-BEHAVIOR-AUDITOR.md +61 -62
  43. package/dist/templates/cc-native/_cc-native/agents/plan-review/TESTDRIVEN-CHARACTERIZATION.md +71 -72
  44. package/dist/templates/cc-native/_cc-native/agents/plan-review/TESTDRIVEN-FIRST-VALIDATOR.md +61 -62
  45. package/dist/templates/cc-native/_cc-native/agents/plan-review/TESTDRIVEN-PYRAMID-ANALYZER.md +61 -62
  46. package/dist/templates/cc-native/_cc-native/agents/plan-review/TRADEOFF-COSTS.md +67 -68
  47. package/dist/templates/cc-native/_cc-native/agents/plan-review/TRADEOFF-STAKEHOLDERS.md +65 -66
  48. package/dist/templates/cc-native/_cc-native/agents/plan-review/VERIFY-COVERAGE.md +74 -75
  49. package/dist/templates/cc-native/_cc-native/agents/plan-review/VERIFY-STRENGTH.md +69 -70
  50. package/dist/templates/cc-native/_cc-native/{plan-review.config.json → cc-native.config.json} +12 -0
  51. package/dist/templates/cc-native/_cc-native/hooks/CLAUDE.md +19 -2
  52. package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.ts +28 -1010
  53. package/dist/templates/cc-native/_cc-native/lib-ts/agent-selection.ts +163 -0
  54. package/dist/templates/cc-native/_cc-native/lib-ts/aggregate-agents.ts +1 -2
  55. package/dist/templates/cc-native/_cc-native/lib-ts/artifacts/format.ts +597 -0
  56. package/dist/templates/cc-native/_cc-native/lib-ts/artifacts/index.ts +26 -0
  57. package/dist/templates/cc-native/_cc-native/lib-ts/artifacts/tracker.ts +107 -0
  58. package/dist/templates/cc-native/_cc-native/lib-ts/artifacts/write.ts +119 -0
  59. package/dist/templates/cc-native/_cc-native/lib-ts/artifacts.ts +19 -821
  60. package/dist/templates/cc-native/_cc-native/lib-ts/cc-native-state.ts +36 -13
  61. package/dist/templates/cc-native/_cc-native/lib-ts/config.ts +3 -3
  62. package/dist/templates/cc-native/_cc-native/lib-ts/graduation.ts +132 -0
  63. package/dist/templates/cc-native/_cc-native/lib-ts/orchestrator.ts +1 -2
  64. package/dist/templates/cc-native/_cc-native/lib-ts/output-builder.ts +130 -0
  65. package/dist/templates/cc-native/_cc-native/lib-ts/plan-discovery.ts +80 -0
  66. package/dist/templates/cc-native/_cc-native/lib-ts/review-pipeline.ts +511 -0
  67. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/providers/orchestrator-claude-agent.ts +1 -1
  68. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/CLAUDE.md +480 -0
  69. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/embedding-indexer.ts +287 -0
  70. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/hyde.ts +148 -0
  71. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/index.ts +54 -0
  72. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/logger.ts +58 -0
  73. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/ollama-client.ts +208 -0
  74. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/retrieval-pipeline.ts +460 -0
  75. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/transcript-indexer.ts +447 -0
  76. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/transcript-loader.ts +280 -0
  77. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/transcript-searcher.ts +274 -0
  78. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/types.ts +201 -0
  79. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/vector-store.ts +278 -0
  80. package/dist/templates/cc-native/_cc-native/lib-ts/settings.ts +184 -0
  81. package/dist/templates/cc-native/_cc-native/lib-ts/state.ts +51 -17
  82. package/dist/templates/cc-native/_cc-native/lib-ts/types.ts +42 -3
  83. package/oclif.manifest.json +1 -1
  84. package/package.json +1 -1
@@ -8,9 +8,9 @@
8
8
  */
9
9
 
10
10
  import * as fs from "node:fs";
11
- import * as _path from "node:path";
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: null | string): string {
45
+ export function formatRelativeTime(isoTimestamp: string | null): string {
46
46
  if (!isoTimestamp) return "unknown";
47
47
 
48
- const dt = parseIsoTimestamp(isoTimestamp);
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> | Task, key: string, defaultVal = ""): string {
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): [null | string, boolean, number] {
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, "utf8");
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.replaceAll(/^\[|\]$/g, "") : "Active";
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.replaceAll('_', " ") : "unknown";
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:**", ...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, "utf8"), "");
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 (error: any) {
231
- lines.push(`*Handoff document at \`${handoffPath}\` could not be read: ${error}*`, "");
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 (const [i, context_] of contexts.entries()) {
275
- const ctx = context_!;
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}`, ` ${ctx.summary}`);
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 (const [i, context_] of contexts.entries()) {
359
- const ctx = context_!;
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" || Boolean(ctx.handoff_path);
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}`, `| ${summary}`, `| [${timeStr}]`, "|");
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 as _logDebug, logError, logInfo, logWarn, setContextPath } from "../base/logger.js";
24
- import { dictToState as _dictToState, readStateJson, toDict as _toDict, writeStateJson } from "../base/state-io.js";
25
- import { generateContextId, nowIso } from "../base/utils.js";
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, "utf8");
37
+ const raw = fs.readFileSync(indexPath, "utf-8");
39
38
  return JSON.parse(raw) as IndexFile;
40
- } catch (error: any) {
41
- logWarn("context_store", `Failed to read index, recreating: ${error}`);
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, "utf8"));
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 (error: any) {
111
- logWarn("context_store", `Failed to migrate context.json for '${contextId}': ${error}`);
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, null | string] {
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: null | string,
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, "method" | "summary" | "tags">>,
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 (error: any) {
523
- logError("context_store", `Failed to move context to archive: ${error}`);
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, "utf8"));
643
- } catch (error_: any) {
644
- logWarn("context_store", `Failed to read archive index, recreating: ${error_}`);
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 (error: any) {
673
- logError("context_store", `Failed to restore context from archive: ${error}`);
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, "utf8")) as IndexFile;
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 (error: any) {
704
- logWarn("context_store", `Failed to read archive index: ${error}`);
664
+ } catch (e: any) {
665
+ logWarn("context_store", `Failed to read archive index: ${e}`);
705
666
  return false;
706
667
  }
707
668
  }