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,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compaction Hook — Preserves hierarchy context across LLM compaction.
|
|
3
|
+
*
|
|
4
|
+
* Reads current brain state hierarchy and injects it into
|
|
5
|
+
* post-compaction context via output.context.push().
|
|
6
|
+
*
|
|
7
|
+
* P3: try/catch — never break compaction
|
|
8
|
+
* Budget-capped ≤500 tokens (~2000 chars)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { Logger } from "../lib/logging.js"
|
|
12
|
+
import { createStateManager } from "../lib/persistence.js"
|
|
13
|
+
|
|
14
|
+
/** Budget in characters (~500 tokens at ~4 chars/token) */
|
|
15
|
+
const INJECTION_BUDGET_CHARS = 2000
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Creates the compaction hook.
|
|
19
|
+
*
|
|
20
|
+
* Hook factory pattern: captured logger + directory.
|
|
21
|
+
*/
|
|
22
|
+
export function createCompactionHook(log: Logger, directory: string) {
|
|
23
|
+
const stateManager = createStateManager(directory)
|
|
24
|
+
|
|
25
|
+
return async (
|
|
26
|
+
_input: { sessionID: string },
|
|
27
|
+
output: { context: string[] }
|
|
28
|
+
): Promise<void> => {
|
|
29
|
+
try {
|
|
30
|
+
const state = await stateManager.load()
|
|
31
|
+
if (!state) {
|
|
32
|
+
await log.debug("Compaction: no brain state to preserve")
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const lines: string[] = []
|
|
37
|
+
lines.push("=== HiveMind Context (post-compaction) ===")
|
|
38
|
+
lines.push("")
|
|
39
|
+
|
|
40
|
+
// Session info
|
|
41
|
+
lines.push(
|
|
42
|
+
`Session: ${state.session.id} | Status: ${state.session.governance_status} | Mode: ${state.session.mode}`
|
|
43
|
+
)
|
|
44
|
+
lines.push("")
|
|
45
|
+
|
|
46
|
+
// Hierarchy — the most important context to preserve
|
|
47
|
+
lines.push("## Current Hierarchy")
|
|
48
|
+
if (state.hierarchy.trajectory) {
|
|
49
|
+
lines.push(`Trajectory: ${state.hierarchy.trajectory}`)
|
|
50
|
+
}
|
|
51
|
+
if (state.hierarchy.tactic) {
|
|
52
|
+
lines.push(`Tactic: ${state.hierarchy.tactic}`)
|
|
53
|
+
}
|
|
54
|
+
if (state.hierarchy.action) {
|
|
55
|
+
lines.push(`Action: ${state.hierarchy.action}`)
|
|
56
|
+
}
|
|
57
|
+
if (
|
|
58
|
+
!state.hierarchy.trajectory &&
|
|
59
|
+
!state.hierarchy.tactic &&
|
|
60
|
+
!state.hierarchy.action
|
|
61
|
+
) {
|
|
62
|
+
lines.push("No hierarchy set. Use map_context to set focus.")
|
|
63
|
+
}
|
|
64
|
+
lines.push("")
|
|
65
|
+
|
|
66
|
+
// Metrics
|
|
67
|
+
lines.push(
|
|
68
|
+
`Turns: ${state.metrics.turn_count} | Drift: ${state.metrics.drift_score}/100 | Files: ${state.metrics.files_touched.length}`
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
if (state.metrics.files_touched.length > 0) {
|
|
72
|
+
const maxFiles = 10
|
|
73
|
+
const shown = state.metrics.files_touched.slice(0, maxFiles)
|
|
74
|
+
lines.push(`Files: ${shown.join(", ")}`)
|
|
75
|
+
if (state.metrics.files_touched.length > maxFiles) {
|
|
76
|
+
lines.push(
|
|
77
|
+
` ... and ${state.metrics.files_touched.length - maxFiles} more`
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
lines.push("")
|
|
83
|
+
lines.push("=== End HiveMind Context ===")
|
|
84
|
+
|
|
85
|
+
// Budget enforcement
|
|
86
|
+
let context = lines.join("\n")
|
|
87
|
+
if (context.length > INJECTION_BUDGET_CHARS) {
|
|
88
|
+
context =
|
|
89
|
+
context.slice(0, INJECTION_BUDGET_CHARS - 30) +
|
|
90
|
+
"\n=== End HiveMind Context ==="
|
|
91
|
+
await log.warn(
|
|
92
|
+
`Compaction injection truncated: ${lines.join("\n").length} → ${context.length} chars`
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
output.context.push(context)
|
|
97
|
+
|
|
98
|
+
await log.debug(`Compaction: injected ${context.length} chars`)
|
|
99
|
+
} catch (error) {
|
|
100
|
+
// P3: Never break compaction — this is critical
|
|
101
|
+
await log.error(`Compaction hook error: ${error}`)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook barrel exports — HiveMind governance hooks
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export { createToolGateHook } from "./tool-gate.js"
|
|
6
|
+
export type { ToolGateResult } from "./tool-gate.js"
|
|
7
|
+
export { createSessionLifecycleHook } from "./session-lifecycle.js"
|
|
8
|
+
export { createCompactionHook } from "./compaction.js"
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session Lifecycle Hook — Initialize/load brain state on session events.
|
|
3
|
+
*
|
|
4
|
+
* Handles:
|
|
5
|
+
* - Session start: load existing state or create fresh
|
|
6
|
+
* - System prompt injection: current hierarchy + governance status
|
|
7
|
+
*
|
|
8
|
+
* P3: try/catch — never break session lifecycle
|
|
9
|
+
* P5: Config cached in closure
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { Logger } from "../lib/logging.js"
|
|
13
|
+
import type { HiveMindConfig } from "../schemas/config.js"
|
|
14
|
+
import { generateAgentBehaviorPrompt } from "../schemas/config.js"
|
|
15
|
+
import { createStateManager } from "../lib/persistence.js"
|
|
16
|
+
import {
|
|
17
|
+
createBrainState,
|
|
18
|
+
generateSessionId,
|
|
19
|
+
} from "../schemas/brain-state.js"
|
|
20
|
+
import { initializePlanningDirectory } from "../lib/planning-fs.js"
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Creates the session lifecycle hook (system prompt transform).
|
|
24
|
+
*
|
|
25
|
+
* Injects current session context into the system prompt:
|
|
26
|
+
* - Hierarchy state (trajectory/tactic/action)
|
|
27
|
+
* - Governance status (LOCKED/OPEN)
|
|
28
|
+
* - Session metrics (drift score, turn count)
|
|
29
|
+
*
|
|
30
|
+
* Budget: ≤250 tokens (~1000 chars). ADD, not REPLACE.
|
|
31
|
+
*/
|
|
32
|
+
export function createSessionLifecycleHook(
|
|
33
|
+
log: Logger,
|
|
34
|
+
directory: string,
|
|
35
|
+
config: HiveMindConfig
|
|
36
|
+
) {
|
|
37
|
+
const stateManager = createStateManager(directory)
|
|
38
|
+
const BUDGET_CHARS = 1000
|
|
39
|
+
|
|
40
|
+
return async (
|
|
41
|
+
input: { sessionID?: string },
|
|
42
|
+
output: { system: string[] }
|
|
43
|
+
): Promise<void> => {
|
|
44
|
+
try {
|
|
45
|
+
if (!input.sessionID) return
|
|
46
|
+
|
|
47
|
+
// Load or create brain state
|
|
48
|
+
let state = await stateManager.load()
|
|
49
|
+
if (!state) {
|
|
50
|
+
// First session — initialize
|
|
51
|
+
await initializePlanningDirectory(directory)
|
|
52
|
+
const sessionId = generateSessionId()
|
|
53
|
+
state = createBrainState(sessionId, config)
|
|
54
|
+
await stateManager.save(state)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const lines: string[] = []
|
|
58
|
+
lines.push("<hivemind-governance>")
|
|
59
|
+
|
|
60
|
+
// Session status
|
|
61
|
+
lines.push(
|
|
62
|
+
`Session: ${state.session.governance_status} | Mode: ${state.session.mode} | Governance: ${state.session.governance_mode}`
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
// Hierarchy context
|
|
66
|
+
if (state.hierarchy.trajectory) {
|
|
67
|
+
lines.push(`Trajectory: ${state.hierarchy.trajectory}`)
|
|
68
|
+
}
|
|
69
|
+
if (state.hierarchy.tactic) {
|
|
70
|
+
lines.push(`Tactic: ${state.hierarchy.tactic}`)
|
|
71
|
+
}
|
|
72
|
+
if (state.hierarchy.action) {
|
|
73
|
+
lines.push(`Action: ${state.hierarchy.action}`)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// No hierarchy = prompt to declare intent
|
|
77
|
+
if (
|
|
78
|
+
!state.hierarchy.trajectory &&
|
|
79
|
+
!state.hierarchy.tactic &&
|
|
80
|
+
!state.hierarchy.action
|
|
81
|
+
) {
|
|
82
|
+
if (config.governance_mode === "strict") {
|
|
83
|
+
lines.push(
|
|
84
|
+
"No intent declared. Use declare_intent to unlock the session before writing."
|
|
85
|
+
)
|
|
86
|
+
} else {
|
|
87
|
+
lines.push(
|
|
88
|
+
"Tip: Use declare_intent to set your work focus for better tracking."
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Metrics summary
|
|
94
|
+
lines.push(
|
|
95
|
+
`Turns: ${state.metrics.turn_count} | Drift: ${state.metrics.drift_score}/100 | Files: ${state.metrics.files_touched.length}`
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
// Drift warning
|
|
99
|
+
if (state.metrics.drift_score < 50) {
|
|
100
|
+
lines.push(
|
|
101
|
+
"⚠ High drift detected. Use map_context to re-focus."
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
lines.push("</hivemind-governance>")
|
|
106
|
+
|
|
107
|
+
// Inject mandatory agent behavior configuration
|
|
108
|
+
const agentConfigPrompt = generateAgentBehaviorPrompt(config.agent_behavior)
|
|
109
|
+
lines.push("")
|
|
110
|
+
lines.push(agentConfigPrompt)
|
|
111
|
+
|
|
112
|
+
// Budget enforcement
|
|
113
|
+
let injection = lines.join("\n")
|
|
114
|
+
if (injection.length > BUDGET_CHARS) {
|
|
115
|
+
injection =
|
|
116
|
+
injection.slice(0, BUDGET_CHARS - 30) +
|
|
117
|
+
"\n</agent-configuration>"
|
|
118
|
+
await log.warn(
|
|
119
|
+
`System injection truncated: ${lines.join("\n").length} → ${injection.length} chars`
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
output.system.push(injection)
|
|
124
|
+
|
|
125
|
+
await log.debug(
|
|
126
|
+
`Session lifecycle: injected ${injection.length} chars`
|
|
127
|
+
)
|
|
128
|
+
} catch (error) {
|
|
129
|
+
// P3: Never break session lifecycle
|
|
130
|
+
await log.error(`Session lifecycle hook error: ${error}`)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool Gate Hook — Configurable governance enforcement.
|
|
3
|
+
*
|
|
4
|
+
* Intercepts tool calls and applies governance based on mode:
|
|
5
|
+
* - strict: blocks writes until declare_intent called
|
|
6
|
+
* - assisted: warns but allows
|
|
7
|
+
* - permissive: silently allows, tracks for metrics
|
|
8
|
+
*
|
|
9
|
+
* P3: try/catch — never break tool execution
|
|
10
|
+
* P5: Config cached in closure — single disk read on first call
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { Logger } from "../lib/logging.js"
|
|
14
|
+
import type { HiveMindConfig } from "../schemas/config.js"
|
|
15
|
+
import {
|
|
16
|
+
isSessionLocked,
|
|
17
|
+
incrementTurnCount,
|
|
18
|
+
shouldTriggerDriftWarning,
|
|
19
|
+
addFileTouched,
|
|
20
|
+
calculateDriftScore,
|
|
21
|
+
setComplexityNudgeShown,
|
|
22
|
+
} from "../schemas/brain-state.js"
|
|
23
|
+
import { createStateManager } from "../lib/persistence.js"
|
|
24
|
+
import { checkComplexity } from "../lib/complexity.js"
|
|
25
|
+
|
|
26
|
+
/** Tools that are always allowed regardless of governance state */
|
|
27
|
+
const EXEMPT_TOOLS = new Set([
|
|
28
|
+
"declare_intent",
|
|
29
|
+
"map_context",
|
|
30
|
+
"compact_session",
|
|
31
|
+
"read",
|
|
32
|
+
"grep",
|
|
33
|
+
"glob",
|
|
34
|
+
"find",
|
|
35
|
+
"list",
|
|
36
|
+
"search",
|
|
37
|
+
])
|
|
38
|
+
|
|
39
|
+
/** Write/edit tools that governance should gate */
|
|
40
|
+
const WRITE_TOOLS = new Set([
|
|
41
|
+
"write",
|
|
42
|
+
"edit",
|
|
43
|
+
"create",
|
|
44
|
+
"delete",
|
|
45
|
+
"rename",
|
|
46
|
+
"move",
|
|
47
|
+
"patch",
|
|
48
|
+
])
|
|
49
|
+
|
|
50
|
+
function isExemptTool(toolName: string): boolean {
|
|
51
|
+
// Check full name match
|
|
52
|
+
if (EXEMPT_TOOLS.has(toolName)) return true
|
|
53
|
+
// Check if tool name starts with an exempt prefix
|
|
54
|
+
for (const exempt of EXEMPT_TOOLS) {
|
|
55
|
+
if (toolName.startsWith(exempt)) return true
|
|
56
|
+
}
|
|
57
|
+
return false
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function isWriteTool(toolName: string): boolean {
|
|
61
|
+
for (const w of WRITE_TOOLS) {
|
|
62
|
+
if (toolName.includes(w)) return true
|
|
63
|
+
}
|
|
64
|
+
return false
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface ToolGateResult {
|
|
68
|
+
allowed: boolean
|
|
69
|
+
error?: string
|
|
70
|
+
warning?: string
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Creates the tool gate hook.
|
|
75
|
+
*
|
|
76
|
+
* Hook factory pattern — captures config + logger + directory.
|
|
77
|
+
*/
|
|
78
|
+
export function createToolGateHook(
|
|
79
|
+
log: Logger,
|
|
80
|
+
directory: string,
|
|
81
|
+
config: HiveMindConfig
|
|
82
|
+
) {
|
|
83
|
+
const stateManager = createStateManager(directory)
|
|
84
|
+
|
|
85
|
+
return async (input: {
|
|
86
|
+
sessionID: string
|
|
87
|
+
tool: string
|
|
88
|
+
}): Promise<ToolGateResult> => {
|
|
89
|
+
try {
|
|
90
|
+
const { tool: toolName } = input
|
|
91
|
+
|
|
92
|
+
// Always allow exempt tools (governance tools + read-only)
|
|
93
|
+
if (isExemptTool(toolName)) {
|
|
94
|
+
return { allowed: true }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Load brain state
|
|
98
|
+
let state = await stateManager.load()
|
|
99
|
+
|
|
100
|
+
// No state = no session initialized yet
|
|
101
|
+
if (!state) {
|
|
102
|
+
switch (config.governance_mode) {
|
|
103
|
+
case "strict":
|
|
104
|
+
return {
|
|
105
|
+
allowed: false,
|
|
106
|
+
error:
|
|
107
|
+
"SESSION NOT INITIALIZED. Use 'declare_intent' to start a session.",
|
|
108
|
+
}
|
|
109
|
+
case "assisted":
|
|
110
|
+
await log.warn(
|
|
111
|
+
`Tool "${toolName}" called without session. Consider using declare_intent.`
|
|
112
|
+
)
|
|
113
|
+
return {
|
|
114
|
+
allowed: true,
|
|
115
|
+
warning: "No session initialized. Consider calling declare_intent.",
|
|
116
|
+
}
|
|
117
|
+
case "permissive":
|
|
118
|
+
return { allowed: true }
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Session is locked — governance gate
|
|
123
|
+
if (state && isSessionLocked(state)) {
|
|
124
|
+
switch (config.governance_mode) {
|
|
125
|
+
case "strict":
|
|
126
|
+
if (isWriteTool(toolName)) {
|
|
127
|
+
return {
|
|
128
|
+
allowed: false,
|
|
129
|
+
error:
|
|
130
|
+
"SESSION LOCKED. Use 'declare_intent' to unlock before writing.",
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return { allowed: true }
|
|
134
|
+
case "assisted":
|
|
135
|
+
if (isWriteTool(toolName)) {
|
|
136
|
+
await log.warn(
|
|
137
|
+
`Write tool "${toolName}" used while session locked.`
|
|
138
|
+
)
|
|
139
|
+
return {
|
|
140
|
+
allowed: true,
|
|
141
|
+
warning:
|
|
142
|
+
"Session locked. Declare your intent for better tracking.",
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return { allowed: true }
|
|
146
|
+
case "permissive":
|
|
147
|
+
return { allowed: true }
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Session is open — track activity
|
|
152
|
+
if (state) {
|
|
153
|
+
state = incrementTurnCount(state)
|
|
154
|
+
|
|
155
|
+
// Track file touches for write tools (tool name used as proxy)
|
|
156
|
+
if (isWriteTool(toolName)) {
|
|
157
|
+
state = addFileTouched(state, `[via ${toolName}]`)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Update drift score
|
|
161
|
+
state.metrics.drift_score = calculateDriftScore(state)
|
|
162
|
+
|
|
163
|
+
// Save updated state
|
|
164
|
+
await stateManager.save(state)
|
|
165
|
+
|
|
166
|
+
// Check drift warning
|
|
167
|
+
if (
|
|
168
|
+
shouldTriggerDriftWarning(
|
|
169
|
+
state,
|
|
170
|
+
config.max_turns_before_warning
|
|
171
|
+
)
|
|
172
|
+
) {
|
|
173
|
+
await log.warn(
|
|
174
|
+
`Drift warning: ${state.metrics.turn_count} turns, score: ${state.metrics.drift_score}/100. Consider using map_context to re-focus.`
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
if (config.governance_mode !== "permissive") {
|
|
178
|
+
return {
|
|
179
|
+
allowed: true,
|
|
180
|
+
warning: `Drift detected (${state.metrics.drift_score}/100). Use map_context to re-focus.`,
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Check complexity and show nudge (once per session)
|
|
186
|
+
const complexityCheck = checkComplexity(state)
|
|
187
|
+
if (complexityCheck.isComplex && !state.complexity_nudge_shown) {
|
|
188
|
+
await log.warn(
|
|
189
|
+
`[Nudge] ${complexityCheck.message}`
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
// Mark nudge as shown
|
|
193
|
+
state = setComplexityNudgeShown(state)
|
|
194
|
+
await stateManager.save(state)
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return { allowed: true }
|
|
199
|
+
} catch (error) {
|
|
200
|
+
// P3: Never break tool execution
|
|
201
|
+
await log.error(`Tool gate error: ${error}`)
|
|
202
|
+
return { allowed: true }
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HiveMind Context Governance — OpenCode Plugin Entry Point
|
|
3
|
+
*
|
|
4
|
+
* Context-aware governance layer that prevents drift and manages
|
|
5
|
+
* session state across lifecycles. 3 tools, 3 hooks, configurable modes.
|
|
6
|
+
*
|
|
7
|
+
* CRITICAL: NO console.log anywhere — breaks TUI rendering.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { Plugin } from "@opencode-ai/plugin"
|
|
11
|
+
import { existsSync } from "node:fs"
|
|
12
|
+
import { join } from "node:path"
|
|
13
|
+
import { createLogger } from "./lib/logging.js"
|
|
14
|
+
import { loadConfig } from "./lib/persistence.js"
|
|
15
|
+
import {
|
|
16
|
+
createToolGateHook,
|
|
17
|
+
createSessionLifecycleHook,
|
|
18
|
+
createCompactionHook,
|
|
19
|
+
} from "./hooks/index.js"
|
|
20
|
+
import {
|
|
21
|
+
createDeclareIntentTool,
|
|
22
|
+
createMapContextTool,
|
|
23
|
+
createCompactSessionTool,
|
|
24
|
+
createSelfRateTool,
|
|
25
|
+
} from "./tools/index.js"
|
|
26
|
+
import { createStateManager } from "./lib/persistence.js"
|
|
27
|
+
import { trackToolCall, addSentimentSignals } from "./schemas/brain-state.js"
|
|
28
|
+
import { detectSentiment } from "./lib/sentiment.js"
|
|
29
|
+
import { checkRefreshTrigger } from "./lib/context-refresh.js"
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Plugin factory — hook factory pattern with captured state.
|
|
33
|
+
*
|
|
34
|
+
* GUARD: If .opencode/planning/ doesn't exist, the plugin was not initialized.
|
|
35
|
+
* Return empty hooks to avoid zombie directory creation.
|
|
36
|
+
* The user must run `hivemind init` first.
|
|
37
|
+
*/
|
|
38
|
+
const hivemind: Plugin = async ({ directory }) => {
|
|
39
|
+
// Init guard: skip if not initialized
|
|
40
|
+
const planningDir = join(directory, ".opencode", "planning")
|
|
41
|
+
if (!existsSync(planningDir)) {
|
|
42
|
+
return {}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const log = await createLogger(
|
|
46
|
+
join(directory, ".opencode", "planning", "logs"),
|
|
47
|
+
"hivemind-core"
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
await log.info(`HiveMind loaded for ${directory}`)
|
|
51
|
+
|
|
52
|
+
// Load config
|
|
53
|
+
const config = await loadConfig(directory)
|
|
54
|
+
|
|
55
|
+
// Create hook instances
|
|
56
|
+
const toolGateHook = createToolGateHook(log, directory, config)
|
|
57
|
+
const sessionLifecycleHook = createSessionLifecycleHook(
|
|
58
|
+
log,
|
|
59
|
+
directory,
|
|
60
|
+
config
|
|
61
|
+
)
|
|
62
|
+
const compactionHook = createCompactionHook(log, directory)
|
|
63
|
+
|
|
64
|
+
// Create tool instances with directory bound via closure
|
|
65
|
+
const declare_intent = createDeclareIntentTool(directory)
|
|
66
|
+
const map_context = createMapContextTool(directory)
|
|
67
|
+
const compact_session = createCompactSessionTool(directory)
|
|
68
|
+
const self_rate = createSelfRateTool(directory)
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
/**
|
|
72
|
+
* Session lifecycle events.
|
|
73
|
+
*/
|
|
74
|
+
event: async ({ event }) => {
|
|
75
|
+
try {
|
|
76
|
+
await log.info(`event: ${event.type}`)
|
|
77
|
+
} catch {
|
|
78
|
+
// P3: Never crash on event handling
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Tool gate — configurable governance enforcement.
|
|
84
|
+
* Logs warnings/errors based on governance mode.
|
|
85
|
+
* Uses soft enforcement — log, don't hard-block.
|
|
86
|
+
*/
|
|
87
|
+
"tool.execute.before": async (input) => {
|
|
88
|
+
const result = await toolGateHook({
|
|
89
|
+
sessionID: input.sessionID,
|
|
90
|
+
tool: input.tool,
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
if (!result.allowed) {
|
|
94
|
+
await log.warn(
|
|
95
|
+
`GOVERNANCE BLOCK: ${result.error ?? "Operation blocked."} (tool: ${input.tool})`
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (result.warning) {
|
|
100
|
+
await log.warn(result.warning)
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Tool execution tracking — auto-rating on completion.
|
|
106
|
+
* Tracks successful tool calls for auto-health calculation.
|
|
107
|
+
*/
|
|
108
|
+
"tool.execute.after": async (_input) => {
|
|
109
|
+
try {
|
|
110
|
+
const stateManager = createStateManager(directory)
|
|
111
|
+
let state = await stateManager.load()
|
|
112
|
+
|
|
113
|
+
if (state) {
|
|
114
|
+
// Track successful tool call
|
|
115
|
+
state = trackToolCall(state, true)
|
|
116
|
+
await stateManager.save(state)
|
|
117
|
+
|
|
118
|
+
// Log health score periodically (every 10 calls)
|
|
119
|
+
if (state.metrics.total_tool_calls % 10 === 0) {
|
|
120
|
+
await log.info(
|
|
121
|
+
`[AutoHealth] Score: ${state.metrics.auto_health_score}/100 (${state.metrics.successful_tool_calls}/${state.metrics.total_tool_calls} successful)`
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
} catch {
|
|
126
|
+
// P3: Never crash on tracking
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* System prompt — injects hierarchy + governance status.
|
|
132
|
+
* Budget: ≤250 tokens. ADD, not REPLACE.
|
|
133
|
+
*/
|
|
134
|
+
"experimental.chat.system.transform": async (input, output) => {
|
|
135
|
+
await sessionLifecycleHook(input, output)
|
|
136
|
+
},
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Chat message hook — sentiment detection on user messages.
|
|
140
|
+
* Detects negative signals (frustration, confusion) and updates drift score.
|
|
141
|
+
*/
|
|
142
|
+
"chat.message": async (_input, output) => {
|
|
143
|
+
try {
|
|
144
|
+
// Extract text from user message parts
|
|
145
|
+
const messageText = output.parts
|
|
146
|
+
.filter((p: any) => p.type === "text")
|
|
147
|
+
.map((p: any) => p.text ?? p.content ?? "")
|
|
148
|
+
.join(" ")
|
|
149
|
+
|
|
150
|
+
if (!messageText) return
|
|
151
|
+
|
|
152
|
+
const stateManager = createStateManager(directory)
|
|
153
|
+
let state = await stateManager.load()
|
|
154
|
+
|
|
155
|
+
if (state) {
|
|
156
|
+
const signals = detectSentiment(
|
|
157
|
+
messageText,
|
|
158
|
+
state.metrics.turn_count,
|
|
159
|
+
"user"
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
if (signals.length > 0) {
|
|
163
|
+
state = addSentimentSignals(state, signals)
|
|
164
|
+
await stateManager.save(state)
|
|
165
|
+
|
|
166
|
+
await log.info(
|
|
167
|
+
`[Sentiment] Detected ${signals.length} signals: ${signals.map((s) => s.type).join(", ")}`
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
// Check if refresh trigger threshold reached
|
|
171
|
+
const refreshCheck = checkRefreshTrigger(state)
|
|
172
|
+
if (refreshCheck.shouldRefresh) {
|
|
173
|
+
await log.warn(refreshCheck.message)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
} catch {
|
|
178
|
+
// P3: Never crash on sentiment detection
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Compaction — preserves hierarchy context across LLM compaction.
|
|
184
|
+
* Budget: ≤500 tokens.
|
|
185
|
+
*/
|
|
186
|
+
"experimental.session.compacting": async (input, output) => {
|
|
187
|
+
await compactionHook(input, output)
|
|
188
|
+
},
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* 4 lifecycle tools:
|
|
192
|
+
* declare_intent — unlock session, set mode/focus
|
|
193
|
+
* map_context — update hierarchy level
|
|
194
|
+
* compact_session — archive + reset
|
|
195
|
+
* self_rate — agent self-assessment
|
|
196
|
+
*/
|
|
197
|
+
tool: {
|
|
198
|
+
declare_intent,
|
|
199
|
+
map_context,
|
|
200
|
+
compact_session,
|
|
201
|
+
self_rate,
|
|
202
|
+
},
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export default hivemind
|