hivemind-context-governance 1.3.0

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 (139) hide show
  1. package/CHANGELOG.md +95 -0
  2. package/LICENSE +21 -0
  3. package/README.md +218 -0
  4. package/dist/cli/init.d.ts +23 -0
  5. package/dist/cli/init.d.ts.map +1 -0
  6. package/dist/cli/init.js +175 -0
  7. package/dist/cli/init.js.map +1 -0
  8. package/dist/cli.d.ts +14 -0
  9. package/dist/cli.d.ts.map +1 -0
  10. package/dist/cli.js +162 -0
  11. package/dist/cli.js.map +1 -0
  12. package/dist/dashboard/i18n/en.d.ts +43 -0
  13. package/dist/dashboard/i18n/en.d.ts.map +1 -0
  14. package/dist/dashboard/i18n/en.js +43 -0
  15. package/dist/dashboard/i18n/en.js.map +1 -0
  16. package/dist/dashboard/i18n/index.d.ts +10 -0
  17. package/dist/dashboard/i18n/index.d.ts.map +1 -0
  18. package/dist/dashboard/i18n/index.js +14 -0
  19. package/dist/dashboard/i18n/index.js.map +1 -0
  20. package/dist/dashboard/i18n/vi.d.ts +43 -0
  21. package/dist/dashboard/i18n/vi.d.ts.map +1 -0
  22. package/dist/dashboard/i18n/vi.js +43 -0
  23. package/dist/dashboard/i18n/vi.js.map +1 -0
  24. package/dist/dashboard/server.d.ts +23 -0
  25. package/dist/dashboard/server.d.ts.map +1 -0
  26. package/dist/dashboard/server.js +282 -0
  27. package/dist/dashboard/server.js.map +1 -0
  28. package/dist/hooks/compaction.d.ts +21 -0
  29. package/dist/hooks/compaction.d.ts.map +1 -0
  30. package/dist/hooks/compaction.js +79 -0
  31. package/dist/hooks/compaction.js.map +1 -0
  32. package/dist/hooks/index.d.ts +8 -0
  33. package/dist/hooks/index.d.ts.map +1 -0
  34. package/dist/hooks/index.js +7 -0
  35. package/dist/hooks/index.js.map +1 -0
  36. package/dist/hooks/session-lifecycle.d.ts +28 -0
  37. package/dist/hooks/session-lifecycle.d.ts.map +1 -0
  38. package/dist/hooks/session-lifecycle.js +94 -0
  39. package/dist/hooks/session-lifecycle.js.map +1 -0
  40. package/dist/hooks/tool-gate.d.ts +28 -0
  41. package/dist/hooks/tool-gate.d.ts.map +1 -0
  42. package/dist/hooks/tool-gate.js +152 -0
  43. package/dist/hooks/tool-gate.js.map +1 -0
  44. package/dist/index.d.ts +19 -0
  45. package/dist/index.d.ts.map +1 -0
  46. package/dist/index.js +159 -0
  47. package/dist/index.js.map +1 -0
  48. package/dist/lib/complexity.d.ts +41 -0
  49. package/dist/lib/complexity.d.ts.map +1 -0
  50. package/dist/lib/complexity.js +67 -0
  51. package/dist/lib/complexity.js.map +1 -0
  52. package/dist/lib/context-refresh.d.ts +46 -0
  53. package/dist/lib/context-refresh.d.ts.map +1 -0
  54. package/dist/lib/context-refresh.js +77 -0
  55. package/dist/lib/context-refresh.js.map +1 -0
  56. package/dist/lib/index.d.ts +10 -0
  57. package/dist/lib/index.d.ts.map +1 -0
  58. package/dist/lib/index.js +10 -0
  59. package/dist/lib/index.js.map +1 -0
  60. package/dist/lib/logging.d.ts +13 -0
  61. package/dist/lib/logging.d.ts.map +1 -0
  62. package/dist/lib/logging.js +40 -0
  63. package/dist/lib/logging.js.map +1 -0
  64. package/dist/lib/persistence.d.ts +15 -0
  65. package/dist/lib/persistence.d.ts.map +1 -0
  66. package/dist/lib/persistence.js +62 -0
  67. package/dist/lib/persistence.js.map +1 -0
  68. package/dist/lib/planning-fs.d.ts +34 -0
  69. package/dist/lib/planning-fs.d.ts.map +1 -0
  70. package/dist/lib/planning-fs.js +134 -0
  71. package/dist/lib/planning-fs.js.map +1 -0
  72. package/dist/lib/sentiment.d.ts +31 -0
  73. package/dist/lib/sentiment.d.ts.map +1 -0
  74. package/dist/lib/sentiment.js +158 -0
  75. package/dist/lib/sentiment.js.map +1 -0
  76. package/dist/schemas/brain-state.d.ts +60 -0
  77. package/dist/schemas/brain-state.d.ts.map +1 -0
  78. package/dist/schemas/brain-state.js +176 -0
  79. package/dist/schemas/brain-state.js.map +1 -0
  80. package/dist/schemas/config.d.ts +51 -0
  81. package/dist/schemas/config.d.ts.map +1 -0
  82. package/dist/schemas/config.js +128 -0
  83. package/dist/schemas/config.js.map +1 -0
  84. package/dist/schemas/hierarchy.d.ts +22 -0
  85. package/dist/schemas/hierarchy.d.ts.map +1 -0
  86. package/dist/schemas/hierarchy.js +27 -0
  87. package/dist/schemas/hierarchy.js.map +1 -0
  88. package/dist/schemas/index.d.ts +7 -0
  89. package/dist/schemas/index.d.ts.map +1 -0
  90. package/dist/schemas/index.js +7 -0
  91. package/dist/schemas/index.js.map +1 -0
  92. package/dist/tools/compact-session.d.ts +15 -0
  93. package/dist/tools/compact-session.d.ts.map +1 -0
  94. package/dist/tools/compact-session.js +76 -0
  95. package/dist/tools/compact-session.js.map +1 -0
  96. package/dist/tools/declare-intent.d.ts +15 -0
  97. package/dist/tools/declare-intent.d.ts.map +1 -0
  98. package/dist/tools/declare-intent.js +83 -0
  99. package/dist/tools/declare-intent.js.map +1 -0
  100. package/dist/tools/index.d.ts +14 -0
  101. package/dist/tools/index.d.ts.map +1 -0
  102. package/dist/tools/index.js +14 -0
  103. package/dist/tools/index.js.map +1 -0
  104. package/dist/tools/map-context.d.ts +15 -0
  105. package/dist/tools/map-context.d.ts.map +1 -0
  106. package/dist/tools/map-context.js +85 -0
  107. package/dist/tools/map-context.js.map +1 -0
  108. package/dist/tools/self-rate.d.ts +15 -0
  109. package/dist/tools/self-rate.d.ts.map +1 -0
  110. package/dist/tools/self-rate.js +76 -0
  111. package/dist/tools/self-rate.js.map +1 -0
  112. package/package.json +71 -0
  113. package/src/cli/init.ts +214 -0
  114. package/src/cli.ts +178 -0
  115. package/src/dashboard/i18n/en.ts +43 -0
  116. package/src/dashboard/i18n/index.ts +20 -0
  117. package/src/dashboard/i18n/vi.ts +43 -0
  118. package/src/dashboard/server.ts +372 -0
  119. package/src/hooks/compaction.ts +104 -0
  120. package/src/hooks/index.ts +8 -0
  121. package/src/hooks/session-lifecycle.ts +133 -0
  122. package/src/hooks/tool-gate.ts +205 -0
  123. package/src/index.ts +206 -0
  124. package/src/lib/complexity.ts +96 -0
  125. package/src/lib/context-refresh.ts +107 -0
  126. package/src/lib/index.ts +10 -0
  127. package/src/lib/logging.ts +53 -0
  128. package/src/lib/persistence.ts +83 -0
  129. package/src/lib/planning-fs.ts +187 -0
  130. package/src/lib/sentiment.ts +183 -0
  131. package/src/schemas/brain-state.ts +257 -0
  132. package/src/schemas/config.ts +184 -0
  133. package/src/schemas/hierarchy.ts +58 -0
  134. package/src/schemas/index.ts +7 -0
  135. package/src/tools/compact-session.ts +97 -0
  136. package/src/tools/declare-intent.ts +104 -0
  137. package/src/tools/index.ts +14 -0
  138. package/src/tools/map-context.ts +105 -0
  139. package/src/tools/self-rate.ts +86 -0
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Complexity Detection Module
3
+ *
4
+ * Detects when sessions get complex based on:
5
+ * - Files touched count (unique file paths)
6
+ * - Turn count (total tool calls since last declare_intent)
7
+ *
8
+ * Configurable thresholds (default: 3 files OR 5 turns)
9
+ */
10
+
11
+ import type { BrainState } from "../schemas/brain-state.js";
12
+
13
+ export interface ComplexityThreshold {
14
+ maxFiles: number;
15
+ maxTurns: number;
16
+ }
17
+
18
+ export interface ComplexityCheckResult {
19
+ isComplex: boolean;
20
+ filesCount: number;
21
+ turnsCount: number;
22
+ threshold: ComplexityThreshold;
23
+ message: string;
24
+ }
25
+
26
+ /** Default complexity threshold */
27
+ export const DEFAULT_COMPLEXITY_THRESHOLD: ComplexityThreshold = {
28
+ maxFiles: 3,
29
+ maxTurns: 5,
30
+ };
31
+
32
+ /**
33
+ * Check if current session complexity exceeds thresholds
34
+ */
35
+ export function checkComplexity(
36
+ state: BrainState,
37
+ threshold: ComplexityThreshold = DEFAULT_COMPLEXITY_THRESHOLD
38
+ ): ComplexityCheckResult {
39
+ const filesCount = state.metrics.files_touched.length;
40
+ const turnsCount = state.metrics.turn_count;
41
+
42
+ const filesExceeded = filesCount >= threshold.maxFiles;
43
+ const turnsExceeded = turnsCount >= threshold.maxTurns;
44
+ const isComplex = filesExceeded || turnsExceeded;
45
+
46
+ let message = "Complexity normal";
47
+ if (isComplex) {
48
+ const reasons: string[] = [];
49
+ if (filesExceeded) reasons.push(`${filesCount} files touched`);
50
+ if (turnsExceeded) reasons.push(`${turnsCount} turns`);
51
+ message = `Complexity rising (${reasons.join(", ")}). Consider declare_intent.`;
52
+ }
53
+
54
+ return {
55
+ isComplex,
56
+ filesCount,
57
+ turnsCount,
58
+ threshold,
59
+ message,
60
+ };
61
+ }
62
+
63
+ /**
64
+ * Check if nudge should be shown (only once per session)
65
+ */
66
+ export function shouldShowComplexityNudge(
67
+ state: BrainState,
68
+ nudgeShown: boolean
69
+ ): boolean {
70
+ if (nudgeShown) return false;
71
+
72
+ const check = checkComplexity(state);
73
+ return check.isComplex;
74
+ }
75
+
76
+ /**
77
+ * Format complexity for display
78
+ */
79
+ export function formatComplexity(state: BrainState): string {
80
+ const check = checkComplexity(state);
81
+ return `${check.filesCount} files, ${check.turnsCount} turns`;
82
+ }
83
+
84
+ /**
85
+ * Get complexity percentage (0-100)
86
+ * Based on how close to thresholds
87
+ */
88
+ export function getComplexityPercentage(
89
+ state: BrainState,
90
+ threshold: ComplexityThreshold = DEFAULT_COMPLEXITY_THRESHOLD
91
+ ): number {
92
+ const filesRatio = state.metrics.files_touched.length / threshold.maxFiles;
93
+ const turnsRatio = state.metrics.turn_count / threshold.maxTurns;
94
+ const maxRatio = Math.max(filesRatio, turnsRatio);
95
+ return Math.min(100, Math.round(maxRatio * 100));
96
+ }
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Context Refresh Trigger Module
3
+ *
4
+ * Triggers context refresh recommendations when negative sentiment
5
+ * signals exceed thresholds.
6
+ *
7
+ * Threshold: 2 negative signals within 5 turns triggers refresh warning
8
+ */
9
+
10
+ import type { BrainState } from "../schemas/brain-state.js";
11
+ import type { SentimentSignal } from "./sentiment.js";
12
+
13
+ export interface RefreshTriggerResult {
14
+ shouldRefresh: boolean;
15
+ signalCount: number;
16
+ windowSize: number;
17
+ message: string;
18
+ }
19
+
20
+ /** Default threshold configuration */
21
+ export const DEFAULT_REFRESH_THRESHOLD = {
22
+ signalCount: 2,
23
+ windowSize: 5, // turns
24
+ };
25
+
26
+ /**
27
+ * Check if context refresh should be triggered
28
+ * Based on signal count within recent turn window
29
+ */
30
+ export function checkRefreshTrigger(
31
+ state: BrainState,
32
+ threshold = DEFAULT_REFRESH_THRESHOLD
33
+ ): RefreshTriggerResult {
34
+ const currentTurn = state.metrics.turn_count;
35
+ const windowStart = Math.max(0, currentTurn - threshold.windowSize);
36
+
37
+ // Count signals within the window
38
+ const recentSignals = state.sentiment_signals.filter(
39
+ (signal) => signal.turn_number >= windowStart && signal.turn_number <= currentTurn
40
+ );
41
+
42
+ const shouldRefresh = recentSignals.length >= threshold.signalCount;
43
+
44
+ return {
45
+ shouldRefresh,
46
+ signalCount: recentSignals.length,
47
+ windowSize: threshold.windowSize,
48
+ message: shouldRefresh
49
+ ? `[ContextRefresh] Drift detected (${recentSignals.length} negative signals in last ${threshold.windowSize} turns). Consider compact_session.`
50
+ : `Context stable (${recentSignals.length} signals in last ${threshold.windowSize} turns)`,
51
+ };
52
+ }
53
+
54
+ /**
55
+ * Get recent signals for display
56
+ */
57
+ export function getRecentSignals(
58
+ state: BrainState,
59
+ windowSize: number = 5
60
+ ): SentimentSignal[] {
61
+ const currentTurn = state.metrics.turn_count;
62
+ const windowStart = Math.max(0, currentTurn - windowSize);
63
+
64
+ return state.sentiment_signals.filter(
65
+ (signal) => signal.turn_number >= windowStart && signal.turn_number <= currentTurn
66
+ );
67
+ }
68
+
69
+ /**
70
+ * Format signals for logging
71
+ */
72
+ export function formatSignalsForLog(signals: SentimentSignal[]): string {
73
+ if (signals.length === 0) return "No recent signals";
74
+
75
+ return signals
76
+ .map((s) => `[Turn ${s.turn_number}] ${s.type}: "${s.matched_text}"`)
77
+ .join("\n ");
78
+ }
79
+
80
+ /**
81
+ * Check if drift warning should be added to active.md
82
+ */
83
+ export function shouldAddDriftWarning(state: BrainState): boolean {
84
+ const result = checkRefreshTrigger(state);
85
+ return result.shouldRefresh;
86
+ }
87
+
88
+ /**
89
+ * Generate drift warning content for active.md
90
+ */
91
+ export function generateDriftWarning(state: BrainState): string {
92
+ const recentSignals = getRecentSignals(state);
93
+
94
+ return [
95
+ "",
96
+ "## ⚠️ Drift Warning",
97
+ "",
98
+ `**Detected**: ${recentSignals.length} negative signals in recent turns`,
99
+ "",
100
+ "**Recommendation**: Consider using `compact_session` to reset context,",
101
+ "or `map_context` to refocus on current objectives.",
102
+ "",
103
+ "**Recent Signals**:",
104
+ ...recentSignals.map((s) => `- Turn ${s.turn_number}: ${s.type} (${s.matched_text})`),
105
+ "",
106
+ ].join("\n");
107
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Lib barrel exports
3
+ */
4
+
5
+ export * from "./logging.js";
6
+ export * from "./persistence.js";
7
+ export * from "./planning-fs.js";
8
+ export * from "./sentiment.js";
9
+ export * from "./context-refresh.js";
10
+ export * from "./complexity.js";
@@ -0,0 +1,53 @@
1
+ /**
2
+ * TUI-safe logging
3
+ * File-based logging only - never use console.log
4
+ */
5
+
6
+ import { appendFile, mkdir } from "fs/promises";
7
+ import { dirname } from "path";
8
+
9
+ export interface Logger {
10
+ debug: (message: string) => Promise<void>;
11
+ info: (message: string) => Promise<void>;
12
+ warn: (message: string) => Promise<void>;
13
+ error: (message: string) => Promise<void>;
14
+ }
15
+
16
+ export async function createLogger(
17
+ logDir: string,
18
+ service: string
19
+ ): Promise<Logger> {
20
+ const logFile = `${logDir}/${service}.log`;
21
+
22
+ // Ensure log directory exists
23
+ try {
24
+ await mkdir(dirname(logFile), { recursive: true });
25
+ } catch {
26
+ // Directory may already exist
27
+ }
28
+
29
+ async function log(level: string, message: string): Promise<void> {
30
+ const timestamp = new Date().toISOString();
31
+ const line = `[${timestamp}] [${level.toUpperCase()}] ${message}\n`;
32
+ try {
33
+ await appendFile(logFile, line);
34
+ } catch {
35
+ // Fail silently - logging should never break the application
36
+ }
37
+ }
38
+
39
+ return {
40
+ debug: (msg) => log("debug", msg),
41
+ info: (msg) => log("info", msg),
42
+ warn: (msg) => log("warn", msg),
43
+ error: (msg) => log("error", msg),
44
+ };
45
+ }
46
+
47
+ // No-op logger for when logging is disabled
48
+ export const noopLogger: Logger = {
49
+ debug: async () => {},
50
+ info: async () => {},
51
+ warn: async () => {},
52
+ error: async () => {},
53
+ };
@@ -0,0 +1,83 @@
1
+ /**
2
+ * StateManager - Disk persistence for brain state
3
+ */
4
+
5
+ import { readFile, writeFile, mkdir } from "fs/promises";
6
+ import { existsSync } from "fs";
7
+ import { dirname, join } from "path";
8
+ import type { BrainState } from "../schemas/brain-state.js";
9
+ import type { HiveMindConfig } from "../schemas/config.js";
10
+ import { createBrainState } from "../schemas/brain-state.js";
11
+ import { createConfig } from "../schemas/config.js";
12
+
13
+ export interface StateManager {
14
+ load(): Promise<BrainState | null>;
15
+ save(state: BrainState): Promise<void>;
16
+ initialize(sessionId: string, config: HiveMindConfig): Promise<BrainState>;
17
+ exists(): boolean;
18
+ }
19
+
20
+ export function createStateManager(projectRoot: string): StateManager {
21
+ const brainPath = join(projectRoot, ".opencode", "planning", "brain.json");
22
+
23
+ return {
24
+ async load(): Promise<BrainState | null> {
25
+ try {
26
+ if (!existsSync(brainPath)) {
27
+ return null;
28
+ }
29
+ const data = await readFile(brainPath, "utf-8");
30
+ return JSON.parse(data) as BrainState;
31
+ } catch {
32
+ return null;
33
+ }
34
+ },
35
+
36
+ async save(state: BrainState): Promise<void> {
37
+ try {
38
+ await mkdir(dirname(brainPath), { recursive: true });
39
+ await writeFile(brainPath, JSON.stringify(state, null, 2));
40
+ } catch (error) {
41
+ throw new Error(`Failed to save brain state: ${error}`);
42
+ }
43
+ },
44
+
45
+ async initialize(
46
+ sessionId: string,
47
+ config: HiveMindConfig
48
+ ): Promise<BrainState> {
49
+ const state = createBrainState(sessionId, config);
50
+ await this.save(state);
51
+ return state;
52
+ },
53
+
54
+ exists(): boolean {
55
+ return existsSync(brainPath);
56
+ },
57
+ };
58
+ }
59
+
60
+ export async function loadConfig(projectRoot: string): Promise<HiveMindConfig> {
61
+ const configPath = join(projectRoot, ".opencode", "planning", "config.json");
62
+
63
+ try {
64
+ if (existsSync(configPath)) {
65
+ const data = await readFile(configPath, "utf-8");
66
+ const parsed = JSON.parse(data);
67
+ return createConfig(parsed);
68
+ }
69
+ } catch {
70
+ // Fall through to default
71
+ }
72
+
73
+ return createConfig();
74
+ }
75
+
76
+ export async function saveConfig(
77
+ projectRoot: string,
78
+ config: HiveMindConfig
79
+ ): Promise<void> {
80
+ const configPath = join(projectRoot, ".opencode", "planning", "config.json");
81
+ await mkdir(dirname(configPath), { recursive: true });
82
+ await writeFile(configPath, JSON.stringify(config, null, 2));
83
+ }
@@ -0,0 +1,187 @@
1
+ /**
2
+ * Planning File System
3
+ * Manages .opencode/planning/ directory structure
4
+ */
5
+
6
+ import { readFile, writeFile, mkdir, readdir } from "fs/promises";
7
+ import { existsSync } from "fs";
8
+ import { join } from "path";
9
+ import { parse, stringify } from "yaml";
10
+
11
+ export interface PlanningPaths {
12
+ projectRoot: string;
13
+ planningDir: string;
14
+ indexPath: string;
15
+ activePath: string;
16
+ archiveDir: string;
17
+ brainPath: string;
18
+ }
19
+
20
+ export function getPlanningPaths(projectRoot: string): PlanningPaths {
21
+ const planningDir = join(projectRoot, ".opencode", "planning");
22
+ return {
23
+ projectRoot,
24
+ planningDir,
25
+ indexPath: join(planningDir, "index.md"),
26
+ activePath: join(planningDir, "active.md"),
27
+ archiveDir: join(planningDir, "archive"),
28
+ brainPath: join(planningDir, "brain.json"),
29
+ };
30
+ }
31
+
32
+ export interface ActiveMdContent {
33
+ frontmatter: {
34
+ session_id?: string;
35
+ mode?: string;
36
+ governance_status?: string;
37
+ start_time?: number;
38
+ last_updated?: number;
39
+ };
40
+ body: string;
41
+ }
42
+
43
+ export async function initializePlanningDirectory(
44
+ projectRoot: string
45
+ ): Promise<PlanningPaths> {
46
+ const paths = getPlanningPaths(projectRoot);
47
+
48
+ // Create directories
49
+ await mkdir(paths.planningDir, { recursive: true });
50
+ await mkdir(paths.archiveDir, { recursive: true });
51
+
52
+ // Create index.md if not exists
53
+ if (!existsSync(paths.indexPath)) {
54
+ await writeFile(paths.indexPath, generateIndexTemplate());
55
+ }
56
+
57
+ // Create active.md if not exists
58
+ if (!existsSync(paths.activePath)) {
59
+ await writeFile(paths.activePath, generateActiveTemplate());
60
+ }
61
+
62
+ return paths;
63
+ }
64
+
65
+ export async function readActiveMd(
66
+ projectRoot: string
67
+ ): Promise<ActiveMdContent> {
68
+ const paths = getPlanningPaths(projectRoot);
69
+
70
+ try {
71
+ const content = await readFile(paths.activePath, "utf-8");
72
+ return parseActiveMd(content);
73
+ } catch {
74
+ return { frontmatter: {}, body: "" };
75
+ }
76
+ }
77
+
78
+ export function parseActiveMd(content: string): ActiveMdContent {
79
+ const yamlMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
80
+
81
+ if (yamlMatch) {
82
+ try {
83
+ const frontmatter = parse(yamlMatch[1]) || {};
84
+ const body = yamlMatch[2].trim();
85
+ return { frontmatter, body };
86
+ } catch {
87
+ // Fall through to default
88
+ }
89
+ }
90
+
91
+ return { frontmatter: {}, body: content.trim() };
92
+ }
93
+
94
+ export async function writeActiveMd(
95
+ projectRoot: string,
96
+ content: ActiveMdContent
97
+ ): Promise<void> {
98
+ const paths = getPlanningPaths(projectRoot);
99
+ const yamlContent = stringify(content.frontmatter);
100
+ const fullContent = `---\n${yamlContent}---\n\n${content.body}`;
101
+ await writeFile(paths.activePath, fullContent);
102
+ }
103
+
104
+ export async function archiveSession(
105
+ projectRoot: string,
106
+ sessionId: string,
107
+ content: string
108
+ ): Promise<void> {
109
+ const paths = getPlanningPaths(projectRoot);
110
+ const timestamp = new Date().toISOString().split("T")[0];
111
+ const archiveFile = join(paths.archiveDir, `session_${timestamp}_${sessionId}.md`);
112
+ await writeFile(archiveFile, content);
113
+ }
114
+
115
+ export async function updateIndexMd(
116
+ projectRoot: string,
117
+ summaryLine: string
118
+ ): Promise<void> {
119
+ const paths = getPlanningPaths(projectRoot);
120
+
121
+ try {
122
+ const content = await readFile(paths.indexPath, "utf-8");
123
+ const timestamp = new Date().toISOString().split("T")[0];
124
+ const entry = `- ${timestamp}: ${summaryLine}\n`;
125
+ const updated = content + entry;
126
+ await writeFile(paths.indexPath, updated);
127
+ } catch {
128
+ // If index doesn't exist, create it
129
+ await writeFile(paths.indexPath, generateIndexTemplate() + summaryLine + "\n");
130
+ }
131
+ }
132
+
133
+ export async function listArchives(projectRoot: string): Promise<string[]> {
134
+ const paths = getPlanningPaths(projectRoot);
135
+
136
+ try {
137
+ const files = await readdir(paths.archiveDir);
138
+ return files.filter((f) => f.endsWith(".md")).sort();
139
+ } catch {
140
+ return [];
141
+ }
142
+ }
143
+
144
+ export function generateIndexTemplate(): string {
145
+ return `# Project Trajectory
146
+
147
+ ## Goals
148
+ <!-- High-level goals only -->
149
+ -
150
+
151
+ ## Constraints
152
+ <!-- Project-wide constraints -->
153
+ -
154
+
155
+ ## Session History
156
+ <!-- Auto-updated by compact_session -->
157
+ `;
158
+ }
159
+
160
+ export function generateActiveTemplate(): string {
161
+ return `---
162
+ session_id: ""
163
+ mode: ""
164
+ governance_status: "LOCKED"
165
+ start_time: 0
166
+ last_updated: 0
167
+ ---
168
+
169
+ # Active Session
170
+
171
+ ## Current Focus
172
+ <!-- Updated via map_context -->
173
+
174
+ ## Completed
175
+ <!-- Items marked [x] get archived -->
176
+
177
+ ## Notes
178
+ <!-- Scratchpad - anything goes -->
179
+ `;
180
+ }
181
+
182
+ export async function resetActiveMd(projectRoot: string): Promise<void> {
183
+ await writeFile(
184
+ getPlanningPaths(projectRoot).activePath,
185
+ generateActiveTemplate()
186
+ );
187
+ }