@monoes/monomindcli 2.7.7 → 2.7.9
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/.claude/agents/core/coder.md +9 -0
- package/.claude/agents/core/coordinator.md +62 -0
- package/.claude/agents/core/planner.md +9 -0
- package/.claude/agents/core/reviewer.md +9 -0
- package/.claude/agents/core/tester.md +8 -0
- package/.claude/helpers/handlers/gates-handler.cjs +180 -47
- package/.claude/helpers/handlers/route-handler.cjs +32 -3
- package/.claude/helpers/hook-handler.cjs +55 -3
- package/.claude/helpers/intelligence.cjs +3 -1
- package/.claude/helpers/statusline.cjs +27 -3
- package/.claude/helpers/utils/monograph.cjs +104 -18
- package/.claude/settings.json +1 -1
- package/dist/src/browser/dashboard/server.d.ts.map +1 -1
- package/dist/src/browser/dashboard/server.js +6 -1
- package/dist/src/browser/dashboard/server.js.map +1 -1
- package/dist/src/capabilities/index.d.ts +0 -1
- package/dist/src/capabilities/index.d.ts.map +1 -1
- package/dist/src/capabilities/index.js +8 -1
- package/dist/src/capabilities/index.js.map +1 -1
- package/dist/src/commands/agent-lifecycle.d.ts.map +1 -1
- package/dist/src/commands/agent-lifecycle.js +5 -1
- package/dist/src/commands/agent-lifecycle.js.map +1 -1
- package/dist/src/commands/doctor-project-checks.d.ts.map +1 -1
- package/dist/src/commands/doctor-project-checks.js +8 -1
- package/dist/src/commands/doctor-project-checks.js.map +1 -1
- package/dist/src/commands/guidance.d.ts.map +1 -1
- package/dist/src/commands/guidance.js +8 -2
- package/dist/src/commands/guidance.js.map +1 -1
- package/dist/src/commands/init.d.ts.map +1 -1
- package/dist/src/commands/init.js +18 -4
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/commands/memory-crud.d.ts.map +1 -1
- package/dist/src/commands/memory-crud.js +7 -1
- package/dist/src/commands/memory-crud.js.map +1 -1
- package/dist/src/commands/org-observe.d.ts.map +1 -1
- package/dist/src/commands/org-observe.js +17 -7
- package/dist/src/commands/org-observe.js.map +1 -1
- package/dist/src/commands/org.d.ts +11 -0
- package/dist/src/commands/org.d.ts.map +1 -1
- package/dist/src/commands/org.js +162 -15
- package/dist/src/commands/org.js.map +1 -1
- package/dist/src/commands/security-scan.d.ts +30 -1
- package/dist/src/commands/security-scan.d.ts.map +1 -1
- package/dist/src/commands/security-scan.js +182 -69
- package/dist/src/commands/security-scan.js.map +1 -1
- package/dist/src/commands/swarm.d.ts.map +1 -1
- package/dist/src/commands/swarm.js +41 -14
- package/dist/src/commands/swarm.js.map +1 -1
- package/dist/src/consensus/audit-writer.d.ts.map +1 -1
- package/dist/src/consensus/audit-writer.js +11 -10
- package/dist/src/consensus/audit-writer.js.map +1 -1
- package/dist/src/init/executor.d.ts.map +1 -1
- package/dist/src/init/executor.js +138 -22
- package/dist/src/init/executor.js.map +1 -1
- package/dist/src/init/settings-generator.d.ts.map +1 -1
- package/dist/src/init/settings-generator.js +4 -1
- package/dist/src/init/settings-generator.js.map +1 -1
- package/dist/src/knowledge/document-pipeline.d.ts +17 -0
- package/dist/src/knowledge/document-pipeline.d.ts.map +1 -1
- package/dist/src/knowledge/document-pipeline.js +62 -3
- package/dist/src/knowledge/document-pipeline.js.map +1 -1
- package/dist/src/mcp-tools/embeddings-tools.d.ts.map +1 -1
- package/dist/src/mcp-tools/embeddings-tools.js +16 -8
- package/dist/src/mcp-tools/embeddings-tools.js.map +1 -1
- package/dist/src/mcp-tools/knowledge-tools.d.ts.map +1 -1
- package/dist/src/mcp-tools/knowledge-tools.js +27 -13
- package/dist/src/mcp-tools/knowledge-tools.js.map +1 -1
- package/dist/src/memory/memory-bridge.d.ts +1 -1
- package/dist/src/memory/memory-bridge.d.ts.map +1 -1
- package/dist/src/memory/memory-bridge.js +5 -1
- package/dist/src/memory/memory-bridge.js.map +1 -1
- package/dist/src/memory/memory-read.d.ts +7 -2
- package/dist/src/memory/memory-read.d.ts.map +1 -1
- package/dist/src/memory/memory-read.js +10 -2
- package/dist/src/memory/memory-read.js.map +1 -1
- package/dist/src/monovector/diff-classifier.d.ts.map +1 -1
- package/dist/src/monovector/diff-classifier.js +25 -6
- package/dist/src/monovector/diff-classifier.js.map +1 -1
- package/dist/src/orgrt/daemon.d.ts +21 -1
- package/dist/src/orgrt/daemon.d.ts.map +1 -1
- package/dist/src/orgrt/daemon.js +82 -11
- package/dist/src/orgrt/daemon.js.map +1 -1
- package/dist/src/orgrt/inbox.d.ts.map +1 -1
- package/dist/src/orgrt/inbox.js +53 -20
- package/dist/src/orgrt/inbox.js.map +1 -1
- package/dist/src/parser.d.ts +4 -2
- package/dist/src/parser.d.ts.map +1 -1
- package/dist/src/parser.js +61 -25
- package/dist/src/parser.js.map +1 -1
- package/dist/src/services/config-file-manager.d.ts +10 -2
- package/dist/src/services/config-file-manager.d.ts.map +1 -1
- package/dist/src/services/config-file-manager.js +10 -2
- package/dist/src/services/config-file-manager.js.map +1 -1
- package/dist/src/ui/collector.mjs +43 -3
- package/dist/src/ui/dashboard.html +27 -8
- package/dist/src/ui/server.mjs +144 -14
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/dist/src/capabilities/watcher.d.ts +0 -18
- package/dist/src/capabilities/watcher.d.ts.map +0 -1
- package/dist/src/capabilities/watcher.js +0 -107
- package/dist/src/capabilities/watcher.js.map +0 -1
- package/dist/src/config-adapter.d.ts +0 -16
- package/dist/src/config-adapter.d.ts.map +0 -1
- package/dist/src/config-adapter.js +0 -220
- package/dist/src/config-adapter.js.map +0 -1
|
@@ -86,6 +86,15 @@ const results = await Promise.all(items.map(processItem));
|
|
|
86
86
|
const heavyModule = () => import('./heavy-module');
|
|
87
87
|
```
|
|
88
88
|
|
|
89
|
+
## Code Navigation (monograph-first)
|
|
90
|
+
|
|
91
|
+
Before grepping or searching the codebase, use monograph:
|
|
92
|
+
- `monograph_query({ query: "SymbolName" })` — BM25 search, returns file + line
|
|
93
|
+
- `monograph_suggest({ task: "what you're doing" })` — ranked file suggestions for multi-file tasks
|
|
94
|
+
- `monograph_impact({ name: "functionName" })` — blast radius before changing anything
|
|
95
|
+
- `monograph_neighbors({ name: "ClassName" })` — direct callers/callees
|
|
96
|
+
- Only fall back to grep/find if monograph returns 0 results or the DB is not built
|
|
97
|
+
|
|
89
98
|
## Implementation Process
|
|
90
99
|
|
|
91
100
|
### 1. Understand Requirements
|
|
@@ -16,11 +16,22 @@ capability:
|
|
|
16
16
|
- task-routing
|
|
17
17
|
- approval-review
|
|
18
18
|
- status-reporting
|
|
19
|
+
- hive-orchestration
|
|
19
20
|
output_type: CoordinationPlan
|
|
20
21
|
model_preference: sonnet
|
|
21
22
|
termination: Goal met or all subtasks delegated, completed, and reconciled into authoritative state
|
|
22
23
|
---
|
|
23
24
|
|
|
25
|
+
<!--
|
|
26
|
+
Absorbed `queen-coordinator` (2026-07). That agent was the same shape as this
|
|
27
|
+
one — decompose, delegate, hold authoritative state, decide when done — scoped
|
|
28
|
+
to a "hive session". It was referenced in exactly one place (a suggestion list
|
|
29
|
+
in guidance-tools.ts) while this agent is wired into agent-lifecycle spawn
|
|
30
|
+
choices, swarm agent plans, and tests. Its genuinely useful content was the
|
|
31
|
+
hive tool surface and its limits, preserved below under "Hive sessions".
|
|
32
|
+
-->
|
|
33
|
+
|
|
34
|
+
|
|
24
35
|
# Lead Coordinator Agent
|
|
25
36
|
|
|
26
37
|
You are the lead coordinator of a hierarchical agent organization. You own the authoritative state of the team, decide who does what, and ensure every contribution converges on the org's goal. You delegate execution to specialists; you do not implement work yourself.
|
|
@@ -32,6 +43,14 @@ You are the lead coordinator of a hierarchical agent organization. You own the a
|
|
|
32
43
|
3. **Approval Governance**: Review deliverables and approvals against the org's policy before they advance.
|
|
33
44
|
4. **Convergence**: Detect drift early, re-route or re-scope when a specialist stalls, and keep the team aligned to the goal.
|
|
34
45
|
|
|
46
|
+
## Code Navigation (monograph-first)
|
|
47
|
+
|
|
48
|
+
When scoping work or verifying deliverables, use monograph before grep:
|
|
49
|
+
- `monograph_suggest({ task: "description" })` — discover relevant files for task scoping
|
|
50
|
+
- `monograph_impact({ name: "symbol" })` — blast radius for change assessment
|
|
51
|
+
- `monograph_god_nodes()` — high-centrality files that need careful coordination
|
|
52
|
+
- Only fall back to grep/find if monograph returns 0 results
|
|
53
|
+
|
|
35
54
|
## Operating Guidelines
|
|
36
55
|
|
|
37
56
|
### 1. Decompose before delegating
|
|
@@ -69,3 +88,46 @@ Objective → subtasks (clear owner, clear done-criteria, clear handoff target)
|
|
|
69
88
|
- Checkpoint frequently; compare current state against the goal each cycle.
|
|
70
89
|
- If a specialist diverges, intervene immediately with a corrected, narrower task.
|
|
71
90
|
- Never let two specialists silently own overlapping work.
|
|
91
|
+
|
|
92
|
+
## Hive sessions
|
|
93
|
+
|
|
94
|
+
When coordinating a "hive" — a set of agents tracked on shared state rather than
|
|
95
|
+
delegated ad hoc — the same discipline applies, but know what the hive tooling
|
|
96
|
+
actually is before planning around it.
|
|
97
|
+
|
|
98
|
+
**Real concurrency comes from one place: Claude Code's Task tool.** Dispatch
|
|
99
|
+
independent work in a single message so it runs in parallel.
|
|
100
|
+
|
|
101
|
+
**Tools.** `hive-mind_status` (current workers and state) and `hive-mind_join`
|
|
102
|
+
are visible by default. `hive-mind_init`, `hive-mind_spawn`,
|
|
103
|
+
`hive-mind_broadcast`, `hive-mind_memory`, `hive-mind_shutdown`, and
|
|
104
|
+
`hive-mind_consensus` are gated behind `MONOMIND_MCP_SPECULATIVE=1`. If one is
|
|
105
|
+
unavailable, say so and proceed without it — never simulate its effect.
|
|
106
|
+
|
|
107
|
+
`hive-mind_spawn` **writes agent records to a JSON file**; it starts nothing.
|
|
108
|
+
Likewise `swarm_init`/`swarm_scale`/`agent_spawn` record state only. For state
|
|
109
|
+
and knowledge use `memory_batch`, `memory_pattern-store`, `memory_kg_ingest`,
|
|
110
|
+
and `swarm_status`. (`memory_usage` does not exist.)
|
|
111
|
+
|
|
112
|
+
**These do not exist — do not plan around them:** background timers (you run
|
|
113
|
+
when invoked and stop when you return), resource metering (never report
|
|
114
|
+
utilization figures nothing computed), Byzantine fault tolerance,
|
|
115
|
+
swarm-fragmentation recovery, or session succession. If something must outlive
|
|
116
|
+
the session, persist it to memory before returning.
|
|
117
|
+
|
|
118
|
+
**Routing to hive specialists:**
|
|
119
|
+
|
|
120
|
+
- Synthesising several agents' findings into durable knowledge →
|
|
121
|
+
`collective-intelligence-coordinator`
|
|
122
|
+
- Independent slices runnable in parallel with no central owner →
|
|
123
|
+
`mesh-coordinator`
|
|
124
|
+
- A vote tally across agents against an explicit threshold → `quorum-manager`
|
|
125
|
+
- Implementation, testing, review, research → `coder`, `tester`, `reviewer`,
|
|
126
|
+
`researcher`, `planner` via the Task tool
|
|
127
|
+
|
|
128
|
+
Before naming any other agent, confirm it is in `.monomind/registry.json` — the
|
|
129
|
+
root and packaged agent trees have diverged, so an agent present in the
|
|
130
|
+
published package may not be spawnable in this project.
|
|
131
|
+
|
|
132
|
+
**Never** describe a session as achieving "consensus" or being "fault tolerant".
|
|
133
|
+
You made decisions; nothing tolerated a fault.
|
|
@@ -33,6 +33,15 @@ You are a strategic planning specialist responsible for breaking down complex ta
|
|
|
33
33
|
4. **Timeline Creation**: Estimate realistic timeframes for task completion
|
|
34
34
|
5. **Risk Assessment**: Identify potential blockers and mitigation strategies
|
|
35
35
|
|
|
36
|
+
## Code Navigation (monograph-first)
|
|
37
|
+
|
|
38
|
+
Before grepping or searching the codebase, use monograph:
|
|
39
|
+
- `monograph_suggest({ task: "description" })` — ranked file suggestions for the task
|
|
40
|
+
- `monograph_query({ query: "SymbolName" })` — BM25 search, returns file + line
|
|
41
|
+
- `monograph_impact({ name: "functionName" })` — blast radius for change planning
|
|
42
|
+
- `monograph_god_nodes()` — high-centrality files that require extra care in plans
|
|
43
|
+
- Only fall back to grep/find if monograph returns 0 results
|
|
44
|
+
|
|
36
45
|
## Planning Process
|
|
37
46
|
|
|
38
47
|
### 1. Initial Assessment
|
|
@@ -33,6 +33,15 @@ You are a senior code reviewer responsible for ensuring code quality, security,
|
|
|
33
33
|
4. **Standards Compliance**: Ensure adherence to coding standards and best practices
|
|
34
34
|
5. **Documentation Review**: Verify adequate and accurate documentation
|
|
35
35
|
|
|
36
|
+
## Code Navigation (monograph-first)
|
|
37
|
+
|
|
38
|
+
Before grepping or searching the codebase, use monograph:
|
|
39
|
+
- `monograph_query({ query: "SymbolName" })` — BM25 search, returns file + line
|
|
40
|
+
- `monograph_impact({ name: "functionName" })` — blast radius of a change
|
|
41
|
+
- `monograph_neighbors({ name: "ClassName" })` — callers/callees/importers
|
|
42
|
+
- `monograph_context({ path: "src/file.ts" })` — 360° view: who imports it, what it imports
|
|
43
|
+
- Only fall back to grep/find if monograph returns 0 results
|
|
44
|
+
|
|
36
45
|
## Review Process
|
|
37
46
|
|
|
38
47
|
### 1. Functionality Review
|
|
@@ -33,6 +33,14 @@ You are a QA specialist focused on ensuring code quality through comprehensive t
|
|
|
33
33
|
4. **Performance Validation**: Ensure code meets performance requirements
|
|
34
34
|
5. **Security Testing**: Validate security measures and identify vulnerabilities
|
|
35
35
|
|
|
36
|
+
## Code Navigation (monograph-first)
|
|
37
|
+
|
|
38
|
+
Before grepping or searching the codebase, use monograph:
|
|
39
|
+
- `monograph_query({ query: "SymbolName" })` — find the code under test
|
|
40
|
+
- `monograph_neighbors({ name: "FunctionName" })` — find callers to understand usage patterns
|
|
41
|
+
- `monograph_impact({ name: "functionName" })` — find all dependents that need test coverage
|
|
42
|
+
- Only fall back to grep/find if monograph returns 0 results
|
|
43
|
+
|
|
36
44
|
## Testing Strategy
|
|
37
45
|
|
|
38
46
|
### 1. Test Pyramid
|
|
@@ -126,19 +126,87 @@ async function monofenceScan(input) {
|
|
|
126
126
|
}
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
+
// ─── Ambiguous-evidence suppression (version-independent safety net) ────────
|
|
130
|
+
//
|
|
131
|
+
// monofence-ai <= 1.0.0 ships a "fake system message injection" pattern whose
|
|
132
|
+
// source is exactly the string below. It scores 0.97 "critical" — above the
|
|
133
|
+
// abort threshold — and it matches a bare `system:` anywhere in the input with
|
|
134
|
+
// NO left word boundary. That makes it fire on completely ordinary source:
|
|
135
|
+
//
|
|
136
|
+
// system: getSystemMetrics(), // plain object key
|
|
137
|
+
// designSystem: { enabled: true }, // *any* identifier ending in "System"
|
|
138
|
+
//
|
|
139
|
+
// Measured over 1,193 of this repo's own tracked source files, that single
|
|
140
|
+
// pattern (plus the confidence inflation it feeds) blocked 4.7% of them.
|
|
141
|
+
// A gate that rejects 1-in-21 ordinary edits gets switched off wholesale, which
|
|
142
|
+
// costs the real protection too — so the block is suppressed when the ONLY
|
|
143
|
+
// evidence meeting the threshold is this ambiguous marker.
|
|
144
|
+
//
|
|
145
|
+
// The suppression is deliberately narrow: it re-tests the content against the
|
|
146
|
+
// *unambiguous* half of that legacy pattern — the chat-template role markers,
|
|
147
|
+
// which do not occur in ordinary code. If those match, the threat stands. Only
|
|
148
|
+
// the bare-`system:` case is demoted, and only when nothing else independently
|
|
149
|
+
// clears the threshold; a real injection pairs `system:` with
|
|
150
|
+
// instruction-override or restriction-bypass phrasing, and those threats are
|
|
151
|
+
// untouched.
|
|
152
|
+
//
|
|
153
|
+
// monofence-ai >= 1.0.1 splits the pattern itself (unambiguous markers stay at
|
|
154
|
+
// 0.97; bare `system:` drops to a 0.50 corroborating signal), so once a fixed
|
|
155
|
+
// build is installed this code stops matching and becomes inert on its own.
|
|
156
|
+
//
|
|
157
|
+
// NOTE: the marker literals below are assembled from fragments rather than
|
|
158
|
+
// spelled out. This gate scans the content of every file that gets written,
|
|
159
|
+
// including this one — a verbatim role marker here would make gates-handler.cjs
|
|
160
|
+
// unmodifiable by its own gate. (Same reason the gate tests build FAKE_CRED
|
|
161
|
+
// from fragments.) The value is asserted in tests/hooks/security-gates.test.mjs.
|
|
162
|
+
var _SYS = 'sys' + 'tem';
|
|
163
|
+
var LEGACY_AMBIGUOUS_SYSTEM_PATTERN =
|
|
164
|
+
_SYS + '\\s*:\\s*|<\\|' + _SYS + '\\|>|<' + _SYS + '>';
|
|
165
|
+
var UNAMBIGUOUS_SYSTEM_MARKER = new RegExp(
|
|
166
|
+
'<\\|\\s*' + _SYS + '\\s*\\|>|<\\/?\\s*' + _SYS + '\\s*>|<\\|im_start\\|>\\s*' + _SYS,
|
|
167
|
+
'i'
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* True when `threat` is the legacy combined system-message pattern AND the
|
|
172
|
+
* scanned content contains none of its unambiguous role-marker alternatives —
|
|
173
|
+
* i.e. the match was a bare `system:` (or a `…System:` suffix) and carries no
|
|
174
|
+
* real injection signal on its own.
|
|
175
|
+
*/
|
|
176
|
+
function isAmbiguousSystemMarker(threat, content) {
|
|
177
|
+
if (!threat || threat.pattern !== LEGACY_AMBIGUOUS_SYSTEM_PATTERN) return false;
|
|
178
|
+
if (typeof content !== 'string') return false;
|
|
179
|
+
return !UNAMBIGUOUS_SYSTEM_MARKER.test(content);
|
|
180
|
+
}
|
|
181
|
+
|
|
129
182
|
/**
|
|
130
183
|
* Given a monofence ThreatDetectionResult, return the highest-confidence
|
|
131
184
|
* threat if it meets the abort threshold, else null.
|
|
185
|
+
*
|
|
186
|
+
* `content` is the text that was scanned. It is optional — when omitted the
|
|
187
|
+
* function behaves exactly as before (no suppression), so existing callers and
|
|
188
|
+
* the pre-bash path are unaffected.
|
|
132
189
|
*/
|
|
133
|
-
function monofenceWorstThreat(result) {
|
|
190
|
+
function monofenceWorstThreat(result, content) {
|
|
134
191
|
if (!result || result.safe || !Array.isArray(result.threats) || result.threats.length === 0) {
|
|
135
192
|
return null;
|
|
136
193
|
}
|
|
137
|
-
var
|
|
194
|
+
var qualifying = result.threats.filter(function (t) {
|
|
195
|
+
return t && t.confidence >= MONOFENCE_ABORT_THRESHOLD;
|
|
196
|
+
});
|
|
197
|
+
if (qualifying.length === 0) return null;
|
|
198
|
+
|
|
199
|
+
// Drop threats whose sole evidence is the ambiguous bare-`system:` marker.
|
|
200
|
+
// If that leaves nothing above the threshold, there is no block.
|
|
201
|
+
var corroborated = qualifying.filter(function (t) {
|
|
202
|
+
return !isAmbiguousSystemMarker(t, content);
|
|
203
|
+
});
|
|
204
|
+
if (corroborated.length === 0) return null;
|
|
205
|
+
|
|
206
|
+
return corroborated.reduce(
|
|
138
207
|
(max, t) => (t.confidence > max.confidence ? t : max),
|
|
139
|
-
|
|
208
|
+
corroborated[0]
|
|
140
209
|
);
|
|
141
|
-
return worst.confidence >= MONOFENCE_ABORT_THRESHOLD ? worst : null;
|
|
142
210
|
}
|
|
143
211
|
|
|
144
212
|
// ─── Fallback patterns (used only if the compiled config file is missing/unreadable) ──
|
|
@@ -265,6 +333,53 @@ function checkSecrets(content, patterns) {
|
|
|
265
333
|
};
|
|
266
334
|
}
|
|
267
335
|
|
|
336
|
+
// ─── Block emission ───────────────────────────────────────────────────────────
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Emit a PreToolUse block decision.
|
|
340
|
+
*
|
|
341
|
+
* ALWAYS on stderr, never stdout: at exit code 2 Claude Code reads the block
|
|
342
|
+
* reason from stderr, and at exit code 0 it parses stdout as hook output — so
|
|
343
|
+
* a diagnostic printed on stdout can be mistaken for hook output (and, worse,
|
|
344
|
+
* read as an *allow*). Keeping every gate message on stderr removes that
|
|
345
|
+
* ambiguity entirely.
|
|
346
|
+
*/
|
|
347
|
+
function emitBlock(reason) {
|
|
348
|
+
process.stderr.write(JSON.stringify({ decision: 'block', reason: reason }) + '\n');
|
|
349
|
+
process.exitCode = 2;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Fail-closed policy (deliberate — see task note (a)):
|
|
354
|
+
*
|
|
355
|
+
* The deterministic regex gates (destructive-ops, secrets) FAIL CLOSED. If
|
|
356
|
+
* evaluating them throws, we cannot know whether the command/content was
|
|
357
|
+
* dangerous, and the whole point of these two gates is that the dangerous
|
|
358
|
+
* case is unrecoverable (deleted data, leaked credential). They are also
|
|
359
|
+
* tiny, dependency-free, pure-regex checks over an in-memory string, so a
|
|
360
|
+
* throw here means something is genuinely broken — not a routine hiccup.
|
|
361
|
+
* The block message names the gate and the error so the user can unblock
|
|
362
|
+
* immediately (fix the config, or set the documented off-switch).
|
|
363
|
+
*
|
|
364
|
+
* The monofence-ai layer keeps FAILING OPEN. It is an optional, lazily
|
|
365
|
+
* imported, network-free-but-heuristic *extra* layer with a known
|
|
366
|
+
* false-positive history (see MONOMIND_MONOFENCE_GATE), and it is absent
|
|
367
|
+
* in most installs — failing closed on it would block every edit on any
|
|
368
|
+
* machine where the package isn't built.
|
|
369
|
+
*
|
|
370
|
+
* Likewise, non-security enrichment work in hook-handler.cjs (monograph
|
|
371
|
+
* hints, telemetry) stays fail-open: it has no security value and its
|
|
372
|
+
* failure must never stop the user from working.
|
|
373
|
+
*/
|
|
374
|
+
function failClosed(gateName, err) {
|
|
375
|
+
var msg = (err && err.message) ? err.message : String(err);
|
|
376
|
+
emitBlock(
|
|
377
|
+
'[gates] ' + gateName + ' gate failed to evaluate (' + msg + '). Failing CLOSED: a security ' +
|
|
378
|
+
'gate that cannot run must not silently allow the operation. Fix the gate (or the project ' +
|
|
379
|
+
'override at .monomind/guidance/active-gates.json) and retry.'
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
|
|
268
383
|
// ─── Hook handlers ────────────────────────────────────────────────────────────
|
|
269
384
|
|
|
270
385
|
/**
|
|
@@ -276,86 +391,104 @@ async function handlePreBash(hCtx) {
|
|
|
276
391
|
var cmd = (hCtx.toolInput && (hCtx.toolInput.command || hCtx.toolInput.cmd)) || '';
|
|
277
392
|
if (!cmd) return;
|
|
278
393
|
|
|
279
|
-
var
|
|
280
|
-
|
|
394
|
+
var result;
|
|
395
|
+
try {
|
|
396
|
+
var config = loadCompiledConfig(hCtx.CWD);
|
|
397
|
+
result = checkDestructive(cmd, config.destructivePatterns);
|
|
398
|
+
} catch (e) {
|
|
399
|
+
failClosed('destructive-ops', e);
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
281
402
|
if (result.triggered) {
|
|
282
|
-
|
|
283
|
-
// Code's PreToolUse hook protocol, stdout JSON is only parsed when exit
|
|
284
|
-
// code is 0; at exit code 2 the caller reads the block reason from
|
|
285
|
-
// stderr instead, so putting it on stdout here would make it invisible.
|
|
286
|
-
process.stderr.write(JSON.stringify({
|
|
287
|
-
decision: 'block',
|
|
288
|
-
reason: '[gates] ' + result.reason,
|
|
289
|
-
}) + '\n');
|
|
290
|
-
process.exitCode = 2;
|
|
403
|
+
emitBlock('[gates] ' + result.reason);
|
|
291
404
|
return;
|
|
292
405
|
}
|
|
293
406
|
|
|
294
407
|
// Additional layer: monofence-ai threat detection (prompt injection, evasion, etc.)
|
|
295
408
|
// Fails open — never blocks a command just because monofence is unavailable/slow.
|
|
296
409
|
var mf = await monofenceScan(cmd);
|
|
297
|
-
var worst = monofenceWorstThreat(mf);
|
|
410
|
+
var worst = monofenceWorstThreat(mf, cmd);
|
|
298
411
|
if (worst) {
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
reason: '[monofence] Threat detected in command — ' + worst.type +
|
|
302
|
-
' (confidence ' + Math.round(worst.confidence * 100) + '%): ' + worst.description,
|
|
303
|
-
}) + '\n');
|
|
304
|
-
process.exitCode = 2;
|
|
412
|
+
emitBlock('[monofence] Threat detected in command — ' + worst.type +
|
|
413
|
+
' (confidence ' + Math.round(worst.confidence * 100) + '%): ' + worst.description);
|
|
305
414
|
}
|
|
306
415
|
}
|
|
307
416
|
|
|
308
417
|
/**
|
|
309
|
-
*
|
|
310
|
-
*
|
|
311
|
-
*
|
|
418
|
+
* Extract the about-to-be-written text from a Write / Edit / MultiEdit /
|
|
419
|
+
* NotebookEdit tool input. Returns '' when there is nothing to scan.
|
|
420
|
+
*
|
|
421
|
+
* Write → content
|
|
422
|
+
* Edit → new_string
|
|
423
|
+
* MultiEdit → edits[].new_string
|
|
424
|
+
* NotebookEdit → new_source ← was previously unread, so a secret
|
|
425
|
+
* pasted into a .ipynb cell bypassed
|
|
426
|
+
* the gate entirely while the same
|
|
427
|
+
* text in a .ts file was blocked.
|
|
428
|
+
*/
|
|
429
|
+
function extractWriteContent(toolInput) {
|
|
430
|
+
var ti = toolInput || {};
|
|
431
|
+
var content = ti.content || ti.new_string || ti.new_source || '';
|
|
432
|
+
if (!content && Array.isArray(ti.edits)) {
|
|
433
|
+
content = ti.edits
|
|
434
|
+
.map(function (e) { return (e && (e.new_string || e.new_source)) || ''; })
|
|
435
|
+
.join('\n');
|
|
436
|
+
}
|
|
437
|
+
return typeof content === 'string' ? content : '';
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* pre-write: check for secrets in Write / Edit / MultiEdit / NotebookEdit content
|
|
442
|
+
* before it lands on disk, then (additionally) run monofence-ai's threat detector
|
|
443
|
+
* on the same content. Emits a block decision on stderr with exit code 2.
|
|
312
444
|
*/
|
|
313
445
|
async function handlePreWrite(hCtx) {
|
|
314
446
|
var toolInput = hCtx.toolInput || {};
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
447
|
+
var content;
|
|
448
|
+
try {
|
|
449
|
+
content = extractWriteContent(toolInput);
|
|
450
|
+
} catch (e) {
|
|
451
|
+
failClosed('secrets', e);
|
|
452
|
+
return;
|
|
320
453
|
}
|
|
321
|
-
if (!content
|
|
454
|
+
if (!content) return;
|
|
322
455
|
// Cap content at 512 KiB before regex scanning to prevent DoS
|
|
323
456
|
var MAX_SCAN = 524288;
|
|
324
457
|
if (content.length > MAX_SCAN) content = content.slice(0, MAX_SCAN);
|
|
325
458
|
|
|
326
|
-
var
|
|
327
|
-
|
|
459
|
+
var result;
|
|
460
|
+
try {
|
|
461
|
+
var config = loadCompiledConfig(hCtx.CWD);
|
|
462
|
+
result = checkSecrets(content, config.secretPatterns);
|
|
463
|
+
} catch (e) {
|
|
464
|
+
failClosed('secrets', e);
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
328
467
|
if (result.triggered) {
|
|
329
|
-
|
|
330
|
-
// matching comment in handlePreBash for why stdout is the wrong stream.
|
|
331
|
-
process.stderr.write(JSON.stringify({
|
|
332
|
-
decision: 'block',
|
|
333
|
-
reason: '[gates] ' + result.reason,
|
|
334
|
-
}) + '\n');
|
|
335
|
-
process.exitCode = 2;
|
|
468
|
+
emitBlock('[gates] ' + result.reason);
|
|
336
469
|
return;
|
|
337
470
|
}
|
|
338
471
|
|
|
339
472
|
// Additional layer: monofence-ai threat detection on the content being written.
|
|
340
473
|
// Fails open — never blocks a write just because monofence is unavailable/slow.
|
|
341
474
|
var mf = await monofenceScan(content);
|
|
342
|
-
var worst = monofenceWorstThreat(mf);
|
|
475
|
+
var worst = monofenceWorstThreat(mf, content);
|
|
343
476
|
if (worst) {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
reason: '[monofence] Threat detected in written content — ' + worst.type +
|
|
347
|
-
' (confidence ' + Math.round(worst.confidence * 100) + '%): ' + worst.description,
|
|
348
|
-
}) + '\n');
|
|
349
|
-
process.exitCode = 2;
|
|
477
|
+
emitBlock('[monofence] Threat detected in written content — ' + worst.type +
|
|
478
|
+
' (confidence ' + Math.round(worst.confidence * 100) + '%): ' + worst.description);
|
|
350
479
|
}
|
|
351
480
|
}
|
|
352
481
|
|
|
353
482
|
module.exports = {
|
|
354
483
|
handlePreBash,
|
|
355
484
|
handlePreWrite,
|
|
485
|
+
extractWriteContent,
|
|
486
|
+
emitBlock,
|
|
487
|
+
failClosed,
|
|
356
488
|
checkDestructive,
|
|
357
489
|
checkSecrets,
|
|
358
490
|
loadCompiledConfig,
|
|
359
491
|
monofenceScan,
|
|
360
492
|
monofenceWorstThreat,
|
|
493
|
+
isAmbiguousSystemMarker,
|
|
361
494
|
};
|
|
@@ -347,6 +347,22 @@ module.exports = {
|
|
|
347
347
|
if (fs.existsSync(path.join(sbKnowledgeDir, 'chunks.jsonl'))) {
|
|
348
348
|
var sbHits = null;
|
|
349
349
|
var sbMethod = 'keyword';
|
|
350
|
+
// Which corpus answered. The two paths are NOT the same corpus and
|
|
351
|
+
// deliberately so:
|
|
352
|
+
// 'second-brain' — the full ingested document store
|
|
353
|
+
// (knowledge:shared in SQLite, superseded
|
|
354
|
+
// versions filtered out server-side).
|
|
355
|
+
// 'project-instructions' — the keyword fallback over
|
|
356
|
+
// .monomind/knowledge/chunks.jsonl, which
|
|
357
|
+
// _autoIndexKnowledge builds from CLAUDE.md,
|
|
358
|
+
// CLAUDE.local.md, docs/todo.md and monograph
|
|
359
|
+
// god-nodes ONLY. A hook subprocess cannot
|
|
360
|
+
// afford the 1-3s embedding-model load the
|
|
361
|
+
// real store needs, so when the warm server
|
|
362
|
+
// is down this narrower corpus is all that is
|
|
363
|
+
// reachable. It is labelled in the banner
|
|
364
|
+
// rather than passed off as the Second Brain.
|
|
365
|
+
var sbCorpus = 'project-instructions';
|
|
350
366
|
|
|
351
367
|
// Semantic path: warm control-server endpoint (strictly local traffic;
|
|
352
368
|
// sbAuth is the local dashboard session value from .monomind, same one
|
|
@@ -373,6 +389,7 @@ module.exports = {
|
|
|
373
389
|
return { key: r.key, value: r.content, score: r.score, global: !!r.global, metadata: src ? { filePath: src.slice(4) } : {} };
|
|
374
390
|
});
|
|
375
391
|
sbMethod = sbData.method === 'semantic' ? 'semantic' : 'keyword';
|
|
392
|
+
sbCorpus = 'second-brain';
|
|
376
393
|
}
|
|
377
394
|
}
|
|
378
395
|
} catch (_) { /* server down or warming — fall back below */ }
|
|
@@ -385,8 +402,16 @@ module.exports = {
|
|
|
385
402
|
);
|
|
386
403
|
}
|
|
387
404
|
// Relevance floor: injecting weak matches pollutes every prompt's
|
|
388
|
-
// context —
|
|
389
|
-
|
|
405
|
+
// context — configurable via .monomind/second-brain.json, default 0.35.
|
|
406
|
+
var sbFloor = 0.35;
|
|
407
|
+
var sbInjectionLimit = 5;
|
|
408
|
+
try {
|
|
409
|
+
var sbConf = JSON.parse(fs.readFileSync(path.join(CWD, '.monomind', 'second-brain.json'), 'utf-8'));
|
|
410
|
+
if (typeof sbConf.relevanceFloor === 'number') sbFloor = sbConf.relevanceFloor;
|
|
411
|
+
if (typeof sbConf.injectionLimit === 'number' && sbConf.injectionLimit > 0) sbInjectionLimit = sbConf.injectionLimit;
|
|
412
|
+
} catch (_) {}
|
|
413
|
+
if (sbHits) sbHits = sbHits.filter(function(h) { return (h.score || 0) >= sbFloor; });
|
|
414
|
+
if (sbHits && sbHits.length > sbInjectionLimit) sbHits = sbHits.slice(0, sbInjectionLimit);
|
|
390
415
|
|
|
391
416
|
// Telemetry: append one JSONL line per evaluated prompt so the
|
|
392
417
|
// thresholds above can be tuned from real usage (and misses can
|
|
@@ -408,12 +433,16 @@ module.exports = {
|
|
|
408
433
|
topScore: sbHits && sbHits[0] ? Number((sbHits[0].score || 0).toFixed(3)) : null,
|
|
409
434
|
promptLen: sbPrompt.length,
|
|
410
435
|
terms: _sbSubstantive.length,
|
|
436
|
+
corpus: sbCorpus,
|
|
411
437
|
injected: !!(sbHits && sbHits.length > 0),
|
|
412
438
|
}) + '\n', 'utf-8');
|
|
413
439
|
} catch (_) { /* telemetry never blocks */ }
|
|
414
440
|
|
|
415
441
|
if (sbHits && sbHits.length > 0) {
|
|
416
|
-
var
|
|
442
|
+
var sbSource = sbCorpus === 'second-brain'
|
|
443
|
+
? 'the Second Brain document store'
|
|
444
|
+
: 'project instructions only (CLAUDE.md/todo — Second Brain store unreachable)';
|
|
445
|
+
var sbLines = ['[SECOND_BRAIN] ' + sbHits.length + ' relevant excerpt(s) (' + sbMethod + ') from ' + sbSource + ':'];
|
|
417
446
|
for (var sbI = 0; sbI < sbHits.length; sbI++) {
|
|
418
447
|
var sbH = sbHits[sbI];
|
|
419
448
|
var sbSrc = (sbH.metadata && sbH.metadata.filePath) ? String(sbH.metadata.filePath).split('/').slice(-2).join('/') : sbH.key;
|
|
@@ -242,6 +242,18 @@ async function main() {
|
|
|
242
242
|
// Global safety timeout: hooks must NEVER hang (#1530, #1531)
|
|
243
243
|
var safetyTimer = setTimeout(function() {
|
|
244
244
|
process.stderr.write("[WARN] Hook handler global timeout (5s), forcing exit\n");
|
|
245
|
+
// If a security gate was still pending when the timer fired, the operation
|
|
246
|
+
// was never evaluated — fail CLOSED rather than letting a hang act as an
|
|
247
|
+
// allow. (`_securityGateCompleted` is `var`-hoisted, so it reads as
|
|
248
|
+
// undefined/falsy until the gate sets it.)
|
|
249
|
+
if ((command === 'pre-bash' || command === 'pre-write') && !_securityGateCompleted) {
|
|
250
|
+
process.stderr.write(JSON.stringify({
|
|
251
|
+
decision: 'block',
|
|
252
|
+
reason: '[gates] Security gate "' + command + '" timed out (5s) before completing. ' +
|
|
253
|
+
'Failing CLOSED — the operation was not evaluated.',
|
|
254
|
+
}) + '\n');
|
|
255
|
+
process.exit(2);
|
|
256
|
+
}
|
|
245
257
|
process.exit(0);
|
|
246
258
|
}, 5000);
|
|
247
259
|
safetyTimer.unref();
|
|
@@ -286,6 +298,14 @@ async function main() {
|
|
|
286
298
|
return false;
|
|
287
299
|
}
|
|
288
300
|
|
|
301
|
+
// Set to true the moment a security gate (pre-bash destructive-ops /
|
|
302
|
+
// pre-write secrets) has finished evaluating. Any exception thrown BEFORE
|
|
303
|
+
// that point — including a failure to even require() the gate module — must
|
|
304
|
+
// fail CLOSED (block), because we cannot know whether the operation was
|
|
305
|
+
// dangerous. Exceptions thrown AFTER it (monograph enrichment, telemetry)
|
|
306
|
+
// fail open: they carry no security signal and must never stop the user.
|
|
307
|
+
var _securityGateCompleted = false;
|
|
308
|
+
|
|
289
309
|
// Build shared hook context — passed to extracted handler modules so they
|
|
290
310
|
// don't need to capture main()-scoped or module-scoped variables via closure.
|
|
291
311
|
var hCtx = {
|
|
@@ -405,6 +425,7 @@ const handlers = {
|
|
|
405
425
|
// there's time left in the process's lifetime.
|
|
406
426
|
var gates = require('./handlers/gates-handler.cjs');
|
|
407
427
|
await gates.handlePreBash(hCtx);
|
|
428
|
+
_securityGateCompleted = true;
|
|
408
429
|
if (process.exitCode === 2) return; // blocked — skip enrichment entirely
|
|
409
430
|
|
|
410
431
|
var cmd = (hCtx.toolInput && (hCtx.toolInput.command || hCtx.toolInput.cmd)) || '';
|
|
@@ -668,9 +689,10 @@ const handlers = {
|
|
|
668
689
|
|
|
669
690
|
'pre-write': async () => {
|
|
670
691
|
// Enforcement gate: secrets detection + monofence-ai threat scan before
|
|
671
|
-
// Write/Edit/MultiEdit content lands on disk
|
|
692
|
+
// Write/Edit/MultiEdit/NotebookEdit content lands on disk
|
|
672
693
|
var gates = require('./handlers/gates-handler.cjs');
|
|
673
694
|
await gates.handlePreWrite(hCtx);
|
|
695
|
+
_securityGateCompleted = true;
|
|
674
696
|
},
|
|
675
697
|
|
|
676
698
|
'pre-search': () => {
|
|
@@ -876,7 +898,25 @@ if (command && handlers[command]) {
|
|
|
876
898
|
try {
|
|
877
899
|
await Promise.resolve(handlers[command]());
|
|
878
900
|
} catch (e) {
|
|
879
|
-
|
|
901
|
+
var _isSecurityGate = (command === 'pre-bash' || command === 'pre-write');
|
|
902
|
+
if (_isSecurityGate && !_securityGateCompleted) {
|
|
903
|
+
// The security gate crashed before deciding anything. Fail CLOSED —
|
|
904
|
+
// silently allowing here is exactly the failure mode the gate exists
|
|
905
|
+
// to prevent. Reason goes to stderr (exit-2 protocol); stdout must
|
|
906
|
+
// stay clean so it can never be parsed as an allow-decision.
|
|
907
|
+
process.stderr.write(JSON.stringify({
|
|
908
|
+
decision: 'block',
|
|
909
|
+
reason: '[gates] Security gate "' + command + '" crashed before completing (' +
|
|
910
|
+
e.message + '). Failing CLOSED — the operation was not evaluated, so it is not ' +
|
|
911
|
+
'allowed. Fix the hook (.claude/helpers/handlers/gates-handler.cjs) and retry.',
|
|
912
|
+
}) + '\n');
|
|
913
|
+
process.exitCode = 2;
|
|
914
|
+
} else {
|
|
915
|
+
// Non-security hook work (routing, monograph enrichment, telemetry):
|
|
916
|
+
// fail open, and keep the diagnostic on stderr so it is never mistaken
|
|
917
|
+
// for hook output on stdout.
|
|
918
|
+
process.stderr.write('[WARN] Hook ' + command + ' encountered an error: ' + e.message + '\n');
|
|
919
|
+
}
|
|
880
920
|
} finally {
|
|
881
921
|
try { _recordHookLatency(command, Date.now() - _hookStart); } catch (_) {}
|
|
882
922
|
}
|
|
@@ -891,7 +931,19 @@ if (command && handlers[command]) {
|
|
|
891
931
|
// process). A require() — e.g. from tests verifying it loads — must not exit.
|
|
892
932
|
if (require.main === module) {
|
|
893
933
|
main().catch(function(e) {
|
|
894
|
-
|
|
934
|
+
// main() itself blew up (stdin parsing, module init, …). For the two
|
|
935
|
+
// security-gate events that means the gate never ran — fail CLOSED.
|
|
936
|
+
// Everything else fails open. Diagnostics go to stderr in both cases.
|
|
937
|
+
if (command === 'pre-bash' || command === 'pre-write') {
|
|
938
|
+
process.stderr.write(JSON.stringify({
|
|
939
|
+
decision: 'block',
|
|
940
|
+
reason: '[gates] Hook handler crashed before the "' + command + '" security gate could ' +
|
|
941
|
+
'run (' + e.message + '). Failing CLOSED — the operation was not evaluated.',
|
|
942
|
+
}) + '\n');
|
|
943
|
+
process.exitCode = 2;
|
|
944
|
+
} else {
|
|
945
|
+
process.stderr.write('[WARN] Hook handler error: ' + e.message + '\n');
|
|
946
|
+
}
|
|
895
947
|
}).finally(function() {
|
|
896
948
|
// Use process.exitCode if a gate set it (exit 2 = block), otherwise clean exit
|
|
897
949
|
process.exit(process.exitCode || 0);
|
|
@@ -27,6 +27,7 @@ const RECENT_EDITS_FILE = path.join(DATA_DIR, 'recent-edits.jsonl');
|
|
|
27
27
|
|
|
28
28
|
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10 MiB guard
|
|
29
29
|
const RING_BUFFER_MAX = 50;
|
|
30
|
+
const MAX_ENTRIES = 200;
|
|
30
31
|
|
|
31
32
|
var _entries = []; // deduplicated memory entries loaded from store
|
|
32
33
|
var _recentEdits = []; // ring buffer of recently edited paths (in-memory, may be empty across subprocesses)
|
|
@@ -141,7 +142,7 @@ function init() {
|
|
|
141
142
|
if (seen.has(key)) return false;
|
|
142
143
|
seen.add(key);
|
|
143
144
|
return true;
|
|
144
|
-
});
|
|
145
|
+
}).slice(-MAX_ENTRIES);
|
|
145
146
|
|
|
146
147
|
// Bootstrap from monograph when store is sparse — called externally via bootstrapFromDb(db)
|
|
147
148
|
|
|
@@ -490,6 +491,7 @@ function bootstrapFromDb(db) {
|
|
|
490
491
|
files: h.file ? [h.file] : [],
|
|
491
492
|
ts: Date.now(),
|
|
492
493
|
};
|
|
494
|
+
if (_entries.length >= MAX_ENTRIES) _entries.shift();
|
|
493
495
|
_entries.push(hubEntry);
|
|
494
496
|
newHubEntries.push(hubEntry);
|
|
495
497
|
added++;
|