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,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