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.
- package/CHANGELOG.md +95 -0
- package/LICENSE +21 -0
- package/README.md +218 -0
- package/dist/cli/init.d.ts +23 -0
- package/dist/cli/init.d.ts.map +1 -0
- package/dist/cli/init.js +175 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/cli.d.ts +14 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +162 -0
- package/dist/cli.js.map +1 -0
- package/dist/dashboard/i18n/en.d.ts +43 -0
- package/dist/dashboard/i18n/en.d.ts.map +1 -0
- package/dist/dashboard/i18n/en.js +43 -0
- package/dist/dashboard/i18n/en.js.map +1 -0
- package/dist/dashboard/i18n/index.d.ts +10 -0
- package/dist/dashboard/i18n/index.d.ts.map +1 -0
- package/dist/dashboard/i18n/index.js +14 -0
- package/dist/dashboard/i18n/index.js.map +1 -0
- package/dist/dashboard/i18n/vi.d.ts +43 -0
- package/dist/dashboard/i18n/vi.d.ts.map +1 -0
- package/dist/dashboard/i18n/vi.js +43 -0
- package/dist/dashboard/i18n/vi.js.map +1 -0
- package/dist/dashboard/server.d.ts +23 -0
- package/dist/dashboard/server.d.ts.map +1 -0
- package/dist/dashboard/server.js +282 -0
- package/dist/dashboard/server.js.map +1 -0
- package/dist/hooks/compaction.d.ts +21 -0
- package/dist/hooks/compaction.d.ts.map +1 -0
- package/dist/hooks/compaction.js +79 -0
- package/dist/hooks/compaction.js.map +1 -0
- package/dist/hooks/index.d.ts +8 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/index.js +7 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/hooks/session-lifecycle.d.ts +28 -0
- package/dist/hooks/session-lifecycle.d.ts.map +1 -0
- package/dist/hooks/session-lifecycle.js +94 -0
- package/dist/hooks/session-lifecycle.js.map +1 -0
- package/dist/hooks/tool-gate.d.ts +28 -0
- package/dist/hooks/tool-gate.d.ts.map +1 -0
- package/dist/hooks/tool-gate.js +152 -0
- package/dist/hooks/tool-gate.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +159 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/complexity.d.ts +41 -0
- package/dist/lib/complexity.d.ts.map +1 -0
- package/dist/lib/complexity.js +67 -0
- package/dist/lib/complexity.js.map +1 -0
- package/dist/lib/context-refresh.d.ts +46 -0
- package/dist/lib/context-refresh.d.ts.map +1 -0
- package/dist/lib/context-refresh.js +77 -0
- package/dist/lib/context-refresh.js.map +1 -0
- package/dist/lib/index.d.ts +10 -0
- package/dist/lib/index.d.ts.map +1 -0
- package/dist/lib/index.js +10 -0
- package/dist/lib/index.js.map +1 -0
- package/dist/lib/logging.d.ts +13 -0
- package/dist/lib/logging.d.ts.map +1 -0
- package/dist/lib/logging.js +40 -0
- package/dist/lib/logging.js.map +1 -0
- package/dist/lib/persistence.d.ts +15 -0
- package/dist/lib/persistence.d.ts.map +1 -0
- package/dist/lib/persistence.js +62 -0
- package/dist/lib/persistence.js.map +1 -0
- package/dist/lib/planning-fs.d.ts +34 -0
- package/dist/lib/planning-fs.d.ts.map +1 -0
- package/dist/lib/planning-fs.js +134 -0
- package/dist/lib/planning-fs.js.map +1 -0
- package/dist/lib/sentiment.d.ts +31 -0
- package/dist/lib/sentiment.d.ts.map +1 -0
- package/dist/lib/sentiment.js +158 -0
- package/dist/lib/sentiment.js.map +1 -0
- package/dist/schemas/brain-state.d.ts +60 -0
- package/dist/schemas/brain-state.d.ts.map +1 -0
- package/dist/schemas/brain-state.js +176 -0
- package/dist/schemas/brain-state.js.map +1 -0
- package/dist/schemas/config.d.ts +51 -0
- package/dist/schemas/config.d.ts.map +1 -0
- package/dist/schemas/config.js +128 -0
- package/dist/schemas/config.js.map +1 -0
- package/dist/schemas/hierarchy.d.ts +22 -0
- package/dist/schemas/hierarchy.d.ts.map +1 -0
- package/dist/schemas/hierarchy.js +27 -0
- package/dist/schemas/hierarchy.js.map +1 -0
- package/dist/schemas/index.d.ts +7 -0
- package/dist/schemas/index.d.ts.map +1 -0
- package/dist/schemas/index.js +7 -0
- package/dist/schemas/index.js.map +1 -0
- package/dist/tools/compact-session.d.ts +15 -0
- package/dist/tools/compact-session.d.ts.map +1 -0
- package/dist/tools/compact-session.js +76 -0
- package/dist/tools/compact-session.js.map +1 -0
- package/dist/tools/declare-intent.d.ts +15 -0
- package/dist/tools/declare-intent.d.ts.map +1 -0
- package/dist/tools/declare-intent.js +83 -0
- package/dist/tools/declare-intent.js.map +1 -0
- package/dist/tools/index.d.ts +14 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +14 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/map-context.d.ts +15 -0
- package/dist/tools/map-context.d.ts.map +1 -0
- package/dist/tools/map-context.js +85 -0
- package/dist/tools/map-context.js.map +1 -0
- package/dist/tools/self-rate.d.ts +15 -0
- package/dist/tools/self-rate.d.ts.map +1 -0
- package/dist/tools/self-rate.js +76 -0
- package/dist/tools/self-rate.js.map +1 -0
- package/package.json +71 -0
- package/src/cli/init.ts +214 -0
- package/src/cli.ts +178 -0
- package/src/dashboard/i18n/en.ts +43 -0
- package/src/dashboard/i18n/index.ts +20 -0
- package/src/dashboard/i18n/vi.ts +43 -0
- package/src/dashboard/server.ts +372 -0
- package/src/hooks/compaction.ts +104 -0
- package/src/hooks/index.ts +8 -0
- package/src/hooks/session-lifecycle.ts +133 -0
- package/src/hooks/tool-gate.ts +205 -0
- package/src/index.ts +206 -0
- package/src/lib/complexity.ts +96 -0
- package/src/lib/context-refresh.ts +107 -0
- package/src/lib/index.ts +10 -0
- package/src/lib/logging.ts +53 -0
- package/src/lib/persistence.ts +83 -0
- package/src/lib/planning-fs.ts +187 -0
- package/src/lib/sentiment.ts +183 -0
- package/src/schemas/brain-state.ts +257 -0
- package/src/schemas/config.ts +184 -0
- package/src/schemas/hierarchy.ts +58 -0
- package/src/schemas/index.ts +7 -0
- package/src/tools/compact-session.ts +97 -0
- package/src/tools/declare-intent.ts +104 -0
- package/src/tools/index.ts +14 -0
- package/src/tools/map-context.ts +105 -0
- 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
|
+
}
|
package/src/lib/index.ts
ADDED
|
@@ -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
|
+
}
|