@heretek-ai/epistemic-swarm 0.7.23 → 0.7.25
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-plugin/plugin.json +1 -1
- package/package.json +1 -1
- package/plugins/antigravity/README.md +19 -10
- package/plugins/antigravity/antigravity_hooks.py +118 -20
- package/plugins/antigravity/commands/audit.md +6 -5
- package/plugins/antigravity/commands/brainstorm.md +9 -0
- package/plugins/antigravity/commands/brainstorming.md +6 -8
- package/plugins/antigravity/commands/darkharvest.md +6 -5
- package/plugins/antigravity/commands/domainexpansion.md +8 -5
- package/plugins/antigravity/commands/factory.md +10 -5
- package/plugins/antigravity/commands/grill.md +6 -5
- package/plugins/antigravity/commands/scout.md +6 -5
- package/plugins/antigravity/commands/swarm-config.md +4 -5
- package/plugins/antigravity/commands/swarm.md +6 -3
- package/plugins/antigravity/hook_bus_bridge.js +352 -0
- package/plugins/antigravity/hooks.json +1 -1
- package/plugins/antigravity/plugin.json +1 -1
- package/plugins/opencode/analysis-bridge.js +86 -32
- package/plugins/opencode/hook-bus.js +81 -19
- package/plugins/opencode/index.js +6 -9
- package/plugins/opencode/weight-signals.js +2 -2
- package/prompts/agent_brainstormer.md +29 -0
- package/prompts/agent_darkharvest.md +30 -0
- package/runner/__pycache__/__init__.cpython-311.pyc +0 -0
- package/runner/__pycache__/auctioneer.cpython-311.pyc +0 -0
- package/runner/__pycache__/auditor_engine.cpython-311.pyc +0 -0
- package/runner/__pycache__/claim_store.cpython-311.pyc +0 -0
- package/runner/__pycache__/claim_witness.cpython-311.pyc +0 -0
- package/runner/__pycache__/darkharvest_claims.cpython-311.pyc +0 -0
- package/runner/__pycache__/errors.cpython-311.pyc +0 -0
- package/runner/__pycache__/living_dossiers.cpython-311.pyc +0 -0
- package/runner/__pycache__/mcp_protocol.cpython-311.pyc +0 -0
- package/runner/__pycache__/mcp_server.cpython-311.pyc +0 -0
- package/runner/__pycache__/path_safety.cpython-311.pyc +0 -0
- package/runner/__pycache__/pcrb.cpython-311.pyc +0 -0
- package/runner/__pycache__/pcrb_verify.cpython-311.pyc +0 -0
- package/runner/__pycache__/preflight.cpython-311.pyc +0 -0
- package/runner/__pycache__/refinement.cpython-311.pyc +0 -0
- package/runner/__pycache__/research_swarm.cpython-311.pyc +0 -0
- package/runner/__pycache__/retrieval_log.cpython-311.pyc +0 -0
- package/runner/__pycache__/schema_validate.cpython-311.pyc +0 -0
- package/runner/__pycache__/schemas.cpython-311.pyc +0 -0
- package/runner/__pycache__/state_machine.cpython-311.pyc +0 -0
- package/runner/antigravity_hooks.py +118 -20
- package/runner/auditor_engine.py +194 -44
- package/runner/claim_store.py +64 -20
- package/runner/claim_witness.py +243 -14
- package/runner/pcrb.py +34 -11
- package/runner/refinement.py +140 -31
- package/runner/schemas.py +2 -0
- package/runner/tests/__pycache__/test_analysis_bridge.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_antigravity_plugin.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_auction_order.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_backends.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_bet1_spike.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_claim_store.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_claim_witness.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_claude_plugin.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_config_schema.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_contract_tooling.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_darkharvest_audit.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_dep_health.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_domain_packs.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_dossier_contract.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_factory.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_fleet_seam.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_frontier_concurrency.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_frontier_mirror_standalone.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_grounding.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_hardening_contract.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_hook_bus.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_living_dossiers.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_manifest_concurrency.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_mcp_server.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_nk_hardening.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_opencode_ux.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_opencode_v2_registrars.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_pcrb.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_phase08_waiver_reopens.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_refinement.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_search_anomaly.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_search_provider.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_search_provider_plugin.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_swarm.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_swarm_config.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_sweep_regressions.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_webcache.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_weight_signals.cpython-311.pyc +0 -0
- package/runner/tests/test_analysis_bridge.py +95 -1
- package/runner/tests/test_antigravity_plugin.py +164 -9
- package/runner/tests/test_dep_health.py +111 -0
- package/runner/tests/test_hook_bus.py +161 -7
- package/runner/tests/test_nk_hardening.py +1046 -0
- package/runner/tests/test_weight_signals.py +40 -0
- package/schemas/audit_report.schema.json +3 -0
- package/scripts/__pycache__/bet1_advisory_spike.cpython-311.pyc +0 -0
- package/scripts/__pycache__/build_adapters.cpython-311.pyc +0 -0
- package/scripts/__pycache__/check_docs.cpython-311.pyc +0 -0
- package/scripts/__pycache__/divergence_experiment.cpython-311.pyc +0 -0
- package/scripts/__pycache__/gen_schemas.cpython-311.pyc +0 -0
- package/scripts/build_adapters.py +1 -0
- package/skills/epistemic_search/scripts/__pycache__/search.cpython-311.pyc +0 -0
- package/skills/grilling/__pycache__/__init__.cpython-311.pyc +0 -0
- package/skills/grilling/__pycache__/socratic_tree.cpython-311.pyc +0 -0
- package/skills/research_cache/__pycache__/__init__.cpython-311.pyc +0 -0
- package/skills/research_cache/__pycache__/hasher.cpython-311.pyc +0 -0
- package/skills/swarm_config/__pycache__/__init__.cpython-311.pyc +0 -0
- package/skills/swarm_config/__pycache__/configure.cpython-311.pyc +0 -0
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Antigravity Hook Bus & Quality Gates Bridge.
|
|
3
|
+
*
|
|
4
|
+
* Connects Antigravity's lifecycle events (PreToolUse, PostToolUse, PreInvocation, Stop)
|
|
5
|
+
* to IUMBTEMS's 6-event Hook Bus and the three automated Quality Gates:
|
|
6
|
+
* 1. Analysis Bridge (Biome CLI static analysis for JS/TS)
|
|
7
|
+
* 2. Dependency Health Gate (OSV.dev vulnerability & freshness scanner)
|
|
8
|
+
* 3. Weight Signals (esbuild bundle weight & coverage deltas vs baseline)
|
|
9
|
+
*
|
|
10
|
+
* Also provides:
|
|
11
|
+
* - Pre-commit gating on .research/config.json with optimistic concurrency (expected_hash)
|
|
12
|
+
* - Session context & compaction preservation (buildCompactionContext)
|
|
13
|
+
* - Living dossier claim degradation stop-gating
|
|
14
|
+
*
|
|
15
|
+
* Contract:
|
|
16
|
+
* - Zero runtime dependencies; node builtins only.
|
|
17
|
+
* - Fail-open: internal errors log and allow, never throwing into host.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import {
|
|
21
|
+
createHookBus,
|
|
22
|
+
gatePreCommit,
|
|
23
|
+
FAST_TIMEOUT_MS,
|
|
24
|
+
SLOW_TIMEOUT_MS,
|
|
25
|
+
normalizeVerdictEx,
|
|
26
|
+
} from '../opencode/hook-bus.js';
|
|
27
|
+
import { registerAnalysisBridge } from '../opencode/analysis-bridge.js';
|
|
28
|
+
import { registerDepHealth } from '../opencode/dep-health.js';
|
|
29
|
+
import { registerWeightSignals } from '../opencode/weight-signals.js';
|
|
30
|
+
import { buildCompactionContext, readLedgerDegraded } from '../opencode/index.js';
|
|
31
|
+
import { loadConfig, validateConfig } from '../opencode/config-io.js';
|
|
32
|
+
import { existsSync, readFileSync, appendFileSync, writeFileSync, unlinkSync } from 'node:fs';
|
|
33
|
+
import path from 'node:path';
|
|
34
|
+
import { fileURLToPath } from 'node:url';
|
|
35
|
+
|
|
36
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
37
|
+
const __dirname = path.dirname(__filename);
|
|
38
|
+
const PROJECT_ROOT = path.resolve(__dirname, '../..');
|
|
39
|
+
|
|
40
|
+
export function resolveWorkspaceRoot(payload = {}) {
|
|
41
|
+
const ws = payload.workspacePaths || [];
|
|
42
|
+
if (Array.isArray(ws) && ws.length > 0 && ws[0]) {
|
|
43
|
+
return path.resolve(ws[0]);
|
|
44
|
+
}
|
|
45
|
+
if (payload.workspaceRoot) {
|
|
46
|
+
return path.resolve(payload.workspaceRoot);
|
|
47
|
+
}
|
|
48
|
+
if (process.env.IUMBTEMS_WORKSPACE) {
|
|
49
|
+
return path.resolve(process.env.IUMBTEMS_WORKSPACE);
|
|
50
|
+
}
|
|
51
|
+
if (process.env.IUMBTEMS_ROOT) {
|
|
52
|
+
return path.resolve(process.env.IUMBTEMS_ROOT);
|
|
53
|
+
}
|
|
54
|
+
let curr = path.resolve(process.cwd());
|
|
55
|
+
while (curr !== path.dirname(curr)) {
|
|
56
|
+
if (existsSync(path.join(curr, '.research')) || existsSync(path.join(curr, '.git'))) {
|
|
57
|
+
return curr;
|
|
58
|
+
}
|
|
59
|
+
curr = path.dirname(curr);
|
|
60
|
+
}
|
|
61
|
+
return path.resolve(process.cwd());
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Instantiate a hook bus populated with all quality gate subscribers.
|
|
66
|
+
*/
|
|
67
|
+
export async function createConfiguredBus(root) {
|
|
68
|
+
const bus = createHookBus();
|
|
69
|
+
const mockHost = {
|
|
70
|
+
location: { directory: root },
|
|
71
|
+
session: {
|
|
72
|
+
hook: () => ({ dispose: () => {} }),
|
|
73
|
+
prompt: () => Promise.resolve(),
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
await registerAnalysisBridge(mockHost, { bus, root });
|
|
79
|
+
} catch {
|
|
80
|
+
/* fail open */
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
await registerDepHealth(mockHost, { bus, root });
|
|
85
|
+
} catch {
|
|
86
|
+
/* fail open */
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
await registerWeightSignals(mockHost, { bus });
|
|
91
|
+
} catch {
|
|
92
|
+
/* fail open */
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return bus;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Handle pre-tool-use:
|
|
100
|
+
* - Check pre-commit gating on .research/config.json
|
|
101
|
+
* - Emit pre-tool-use on the bus (Analysis Bridge)
|
|
102
|
+
*/
|
|
103
|
+
export async function handlePreTool(payload = {}) {
|
|
104
|
+
const root = resolveWorkspaceRoot(payload);
|
|
105
|
+
const toolCall = payload.toolCall || {};
|
|
106
|
+
const toolName = String(toolCall.name || '').toLowerCase();
|
|
107
|
+
const args = toolCall.args || {};
|
|
108
|
+
|
|
109
|
+
// Check if tool is attempting to edit .research/config.json
|
|
110
|
+
const targetFile = String(args.TargetFile || args.file || args.path || '');
|
|
111
|
+
const isConfigTarget =
|
|
112
|
+
targetFile.endsWith('.research/config.json') ||
|
|
113
|
+
targetFile === '.research/config.json' ||
|
|
114
|
+
(targetFile.includes('.research') && targetFile.includes('config.json'));
|
|
115
|
+
|
|
116
|
+
if (isConfigTarget && (toolName === 'write_to_file' || toolName === 'replace_file_content')) {
|
|
117
|
+
const bus = createHookBus();
|
|
118
|
+
const content = args.CodeContent || args.ReplacementContent || '';
|
|
119
|
+
let parsedUpdates = null;
|
|
120
|
+
try {
|
|
121
|
+
parsedUpdates = JSON.parse(content);
|
|
122
|
+
} catch {
|
|
123
|
+
// If content is not full JSON (e.g. replace_file_content chunk), attempt validation if possible
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (parsedUpdates && typeof parsedUpdates === 'object') {
|
|
127
|
+
const problems = validateConfig(parsedUpdates);
|
|
128
|
+
if (problems && problems.length > 0) {
|
|
129
|
+
return {
|
|
130
|
+
decision: 'deny',
|
|
131
|
+
allowed: false,
|
|
132
|
+
reason: `Pre-commit validation rejected .research/config.json update: ${problems.join('; ')}`,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const preCommitResult = await gatePreCommit(bus, {
|
|
137
|
+
updates: parsedUpdates,
|
|
138
|
+
root,
|
|
139
|
+
expected_hash: args.expected_hash || args.expectedHash || parsedUpdates.expected_hash || parsedUpdates.expectedHash,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
if (!preCommitResult.allowed) {
|
|
143
|
+
return {
|
|
144
|
+
decision: 'deny',
|
|
145
|
+
allowed: false,
|
|
146
|
+
reason: `Pre-commit gate denied config write: ${preCommitResult.reason || 'optimistic concurrency or validation failure'}`,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const bus = await createConfiguredBus(root);
|
|
153
|
+
const busResult = await bus.emit('pre-tool-use', {
|
|
154
|
+
tool: toolName,
|
|
155
|
+
args,
|
|
156
|
+
sessionID: payload.conversationId,
|
|
157
|
+
targetFile,
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
if (!busResult.allowed) {
|
|
161
|
+
return {
|
|
162
|
+
decision: 'deny',
|
|
163
|
+
allowed: false,
|
|
164
|
+
reason: busResult.reason || 'Blocked by Hook Bus pre-tool-use policy',
|
|
165
|
+
annotations: busResult.annotations || [],
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
decision: 'allow',
|
|
171
|
+
allowed: true,
|
|
172
|
+
annotations: busResult.annotations || [],
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Handle post-tool-use:
|
|
178
|
+
* - Emit post-tool-use on the bus (triggers Analysis Bridge, Dep Health, Weight Signals)
|
|
179
|
+
* - Persist annotations to .research/quality_gates.jsonl
|
|
180
|
+
*/
|
|
181
|
+
export async function handlePostTool(payload = {}) {
|
|
182
|
+
const root = resolveWorkspaceRoot(payload);
|
|
183
|
+
const toolCall = payload.toolCall || {};
|
|
184
|
+
const toolName = String(toolCall.name || '').toLowerCase();
|
|
185
|
+
const args = toolCall.args || {};
|
|
186
|
+
const targetFile = String(args.TargetFile || args.file || args.path || '');
|
|
187
|
+
|
|
188
|
+
const bus = await createConfiguredBus(root);
|
|
189
|
+
const busResult = await bus.emit('post-tool-use', {
|
|
190
|
+
tool: toolName,
|
|
191
|
+
args,
|
|
192
|
+
result: payload.result,
|
|
193
|
+
error: payload.error,
|
|
194
|
+
sessionID: payload.conversationId,
|
|
195
|
+
targetFile,
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
const annotations = busResult.annotations || [];
|
|
199
|
+
if (annotations.length > 0) {
|
|
200
|
+
try {
|
|
201
|
+
const qgLog = path.join(root, '.research', 'quality_gates.jsonl');
|
|
202
|
+
const lines = annotations.map((a) =>
|
|
203
|
+
JSON.stringify({
|
|
204
|
+
at: new Date().toISOString(),
|
|
205
|
+
stepIdx: payload.stepIdx,
|
|
206
|
+
tool: toolName,
|
|
207
|
+
annotation: a,
|
|
208
|
+
})
|
|
209
|
+
);
|
|
210
|
+
appendFileSync(qgLog, lines.join('\n') + '\n', 'utf-8');
|
|
211
|
+
} catch {
|
|
212
|
+
/* ignore file log error */
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
allowed: busResult.allowed !== false,
|
|
218
|
+
annotations,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Handle pre-invocation:
|
|
224
|
+
* - Return compaction context from buildCompactionContext
|
|
225
|
+
* - Collect and clear any pending quality gate annotations
|
|
226
|
+
*/
|
|
227
|
+
export async function handlePreInvocation(payload = {}) {
|
|
228
|
+
const root = resolveWorkspaceRoot(payload);
|
|
229
|
+
const compaction = buildCompactionContext(root);
|
|
230
|
+
|
|
231
|
+
let pendingAnnotations = [];
|
|
232
|
+
const qgLog = path.join(root, '.research', 'quality_gates.jsonl');
|
|
233
|
+
if (existsSync(qgLog)) {
|
|
234
|
+
try {
|
|
235
|
+
const content = readFileSync(qgLog, 'utf-8');
|
|
236
|
+
const lines = content.trim().split('\n').filter(Boolean);
|
|
237
|
+
for (const line of lines) {
|
|
238
|
+
try {
|
|
239
|
+
const entry = JSON.parse(line);
|
|
240
|
+
if (entry.annotation) {
|
|
241
|
+
pendingAnnotations.push(entry.annotation);
|
|
242
|
+
}
|
|
243
|
+
} catch {
|
|
244
|
+
/* ignore bad json line */
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
// Truncate the file so annotations are reported once
|
|
248
|
+
writeFileSync(qgLog, '', 'utf-8');
|
|
249
|
+
} catch {
|
|
250
|
+
/* fail open */
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return {
|
|
255
|
+
compactionContext: compaction,
|
|
256
|
+
annotations: pendingAnnotations,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Handle stop:
|
|
262
|
+
* - Emit stop on the bus
|
|
263
|
+
* - Check degraded claims in .research/ledger/claim_status.json
|
|
264
|
+
*/
|
|
265
|
+
export async function handleStop(payload = {}) {
|
|
266
|
+
const root = resolveWorkspaceRoot(payload);
|
|
267
|
+
|
|
268
|
+
// If a STOP file was placed, allow immediately
|
|
269
|
+
if (existsSync(path.join(root, '.factory', 'STOP'))) {
|
|
270
|
+
return { decision: 'allow', allowed: true };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const bus = await createConfiguredBus(root);
|
|
274
|
+
const busResult = await bus.emit('stop', {
|
|
275
|
+
sessionID: payload.conversationId,
|
|
276
|
+
terminationReason: payload.terminationReason,
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
if (!busResult.allowed) {
|
|
280
|
+
return {
|
|
281
|
+
decision: 'continue',
|
|
282
|
+
allowed: false,
|
|
283
|
+
reason: busResult.reason || 'Blocked by Hook Bus stop policy',
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const degraded = readLedgerDegraded(root);
|
|
288
|
+
const total = (degraded.stale || []).length + (degraded.suspect || []).length;
|
|
289
|
+
if (total > 0) {
|
|
290
|
+
return {
|
|
291
|
+
decision: 'continue',
|
|
292
|
+
allowed: false,
|
|
293
|
+
reason: `Epistemic audit warning: ${total} claims are degraded (${degraded.stale.length} STALE, ${degraded.suspect.length} SUSPECT) due to source retractions. Review before finishing.`,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return { decision: 'allow', allowed: true };
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* CLI dispatcher for subprocess calls.
|
|
302
|
+
*/
|
|
303
|
+
async function main() {
|
|
304
|
+
const action = process.argv[2] || 'pre_tool';
|
|
305
|
+
let inputData = '';
|
|
306
|
+
|
|
307
|
+
if (process.argv[3]) {
|
|
308
|
+
inputData = process.argv[3];
|
|
309
|
+
} else {
|
|
310
|
+
try {
|
|
311
|
+
inputData = readFileSync(0, 'utf-8');
|
|
312
|
+
} catch {
|
|
313
|
+
inputData = '{}';
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
let payload = {};
|
|
318
|
+
try {
|
|
319
|
+
payload = JSON.parse(inputData.trim() || '{}');
|
|
320
|
+
} catch {
|
|
321
|
+
payload = {};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
let result = {};
|
|
325
|
+
try {
|
|
326
|
+
if (action === 'pre_tool' || action === 'pre-tool-use') {
|
|
327
|
+
result = await handlePreTool(payload);
|
|
328
|
+
} else if (action === 'post_tool' || action === 'post-tool-use') {
|
|
329
|
+
result = await handlePostTool(payload);
|
|
330
|
+
} else if (action === 'pre_invocation' || action === 'session-start') {
|
|
331
|
+
result = await handlePreInvocation(payload);
|
|
332
|
+
} else if (action === 'stop') {
|
|
333
|
+
result = await handleStop(payload);
|
|
334
|
+
} else {
|
|
335
|
+
result = { decision: 'allow', allowed: true };
|
|
336
|
+
}
|
|
337
|
+
} catch (err) {
|
|
338
|
+
result = {
|
|
339
|
+
decision: 'allow',
|
|
340
|
+
allowed: true,
|
|
341
|
+
error: String(err?.message || err),
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
process.stdout.write(JSON.stringify(result));
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (process.argv[1] && process.argv[1].endsWith('hook_bus_bridge.js')) {
|
|
349
|
+
main().catch(() => {
|
|
350
|
+
process.stdout.write(JSON.stringify({ decision: 'allow', allowed: true }));
|
|
351
|
+
});
|
|
352
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://antigravity.google/schemas/v1/plugin.json",
|
|
3
3
|
"name": "epistemic-swarm",
|
|
4
|
-
"version": "0.7.
|
|
4
|
+
"version": "0.7.25",
|
|
5
5
|
"description": "IUMBTEMS (Epistemic Swarm) \u2014 High-Integrity Dialectic Research, Architecture Auditing, Competitor Teardowns, and Socratic Planning for Antigravity.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Heretek AI",
|
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
* pinned devDependency (Biome, single dep, zero-config, lint+format JS/TS).
|
|
23
23
|
*
|
|
24
24
|
* Phase evidence (cited per programmer brief):
|
|
25
|
-
* - Bus contract (six
|
|
26
|
-
* tool/eventId audit guarantees)
|
|
25
|
+
* - Bus contract (six events, 3 host-enforced / 1 advisory / 2 bus-level,
|
|
26
|
+
* tiered enforcement, fail-open, tool/eventId audit guarantees)
|
|
27
27
|
* [VERIFIED: sha256:f2c0843b598ba6f19cc0e208f8d02e0250c70615e3c484474d50635d0b5cdef8
|
|
28
28
|
* `file:///home/john/Projects/IUMBTEMS/plugins/opencode/hook-bus.js`]
|
|
29
29
|
* - Host emit points (pre/post-tool-use own-tools scope, pre-commit both
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
*/
|
|
45
45
|
|
|
46
46
|
import { spawn } from 'node:child_process';
|
|
47
|
-
import { existsSync } from 'node:fs';
|
|
47
|
+
import { existsSync, realpathSync } from 'node:fs';
|
|
48
48
|
import path from 'node:path';
|
|
49
49
|
import { fileURLToPath } from 'node:url';
|
|
50
50
|
import { FAST_TIMEOUT_MS } from './hook-bus.js';
|
|
@@ -84,10 +84,32 @@ export function isJsTsFile(p) {
|
|
|
84
84
|
return typeof p === 'string' && /\.(m|c)?[jt]sx?$/i.test(p.trim());
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
function pushCandidate(out, seen, v) {
|
|
87
|
+
function pushCandidate(out, seen, v, root, skipped) {
|
|
88
88
|
if (typeof v !== 'string') return;
|
|
89
89
|
const t = v.trim();
|
|
90
|
-
if (!t ||
|
|
90
|
+
if (!t || t.includes('\0') || t.startsWith('-')) return;
|
|
91
|
+
if (!isJsTsFile(t)) return;
|
|
92
|
+
const resolvedRoot = root ? path.resolve(root) : process.cwd();
|
|
93
|
+
const abs = path.resolve(resolvedRoot, t);
|
|
94
|
+
const rel = path.relative(resolvedRoot, abs);
|
|
95
|
+
if (rel.startsWith('..') || path.isAbsolute(rel)) {
|
|
96
|
+
if (skipped) skipped.push(t);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
if (existsSync(abs)) {
|
|
101
|
+
const real = realpathSync(abs);
|
|
102
|
+
const realRoot = realpathSync(resolvedRoot);
|
|
103
|
+
const realRel = path.relative(realRoot, real);
|
|
104
|
+
if (realRel.startsWith('..') || path.isAbsolute(realRel)) {
|
|
105
|
+
if (skipped) skipped.push(t);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
} catch {
|
|
110
|
+
/* fail safe */
|
|
111
|
+
}
|
|
112
|
+
if (seen.has(t)) return;
|
|
91
113
|
seen.add(t);
|
|
92
114
|
out.push(t);
|
|
93
115
|
}
|
|
@@ -97,28 +119,31 @@ function pushCandidate(out, seen, v) {
|
|
|
97
119
|
* conventional locator fields (`file`, `files`, `path`, `paths`,
|
|
98
120
|
* `filename`, `filenames`) plus a one-level scan of `args` values (plain
|
|
99
121
|
* strings or arrays of strings, e.g. an edit tool's file argument).
|
|
100
|
-
*
|
|
122
|
+
* Confines targets to the project root, rejects option-like flags (`-x`),
|
|
123
|
+
* de-duplicates, and caps at `MAX_TARGET_FILES`. Never throws.
|
|
101
124
|
*/
|
|
102
|
-
export function collectTargets(payload) {
|
|
125
|
+
export function collectTargets(payload, opts = {}) {
|
|
103
126
|
try {
|
|
104
127
|
const out = [];
|
|
105
128
|
const seen = new Set();
|
|
129
|
+
const skipped = Array.isArray(opts?.skipped) ? opts.skipped : null;
|
|
130
|
+
const root = opts?.root || opts?.cwd || process.cwd();
|
|
106
131
|
if (!payload || typeof payload !== 'object') return out;
|
|
107
132
|
for (const key of ['file', 'files', 'path', 'paths', 'filename', 'filenames']) {
|
|
108
133
|
const v = payload[key];
|
|
109
134
|
if (Array.isArray(v)) {
|
|
110
|
-
for (const item of v) pushCandidate(out, seen, item);
|
|
135
|
+
for (const item of v) pushCandidate(out, seen, item, root, skipped);
|
|
111
136
|
} else {
|
|
112
|
-
pushCandidate(out, seen, v);
|
|
137
|
+
pushCandidate(out, seen, v, root, skipped);
|
|
113
138
|
}
|
|
114
139
|
}
|
|
115
140
|
const args = payload.args;
|
|
116
141
|
if (args && typeof args === 'object') {
|
|
117
142
|
for (const v of Object.values(args)) {
|
|
118
143
|
if (Array.isArray(v)) {
|
|
119
|
-
for (const item of v) pushCandidate(out, seen, item);
|
|
144
|
+
for (const item of v) pushCandidate(out, seen, item, root, skipped);
|
|
120
145
|
} else {
|
|
121
|
-
pushCandidate(out, seen, v);
|
|
146
|
+
pushCandidate(out, seen, v, root, skipped);
|
|
122
147
|
}
|
|
123
148
|
}
|
|
124
149
|
}
|
|
@@ -187,8 +212,16 @@ function errorFinding(tool, eventId, targets, message) {
|
|
|
187
212
|
});
|
|
188
213
|
}
|
|
189
214
|
|
|
190
|
-
/** Locate the Biome binary:
|
|
191
|
-
export function findAnalyzerBin() {
|
|
215
|
+
/** Locate the Biome binary: project devDep first, then repo devDep, then PATH fallback. */
|
|
216
|
+
export function findAnalyzerBin(root) {
|
|
217
|
+
try {
|
|
218
|
+
if (root) {
|
|
219
|
+
const projectLocal = path.join(root, 'node_modules', '.bin', 'biome');
|
|
220
|
+
if (existsSync(projectLocal)) return projectLocal;
|
|
221
|
+
}
|
|
222
|
+
} catch {
|
|
223
|
+
/* fall through */
|
|
224
|
+
}
|
|
192
225
|
try {
|
|
193
226
|
const local = path.join(PKG_ROOT, 'node_modules', '.bin', 'biome');
|
|
194
227
|
if (existsSync(local)) return local;
|
|
@@ -241,7 +274,7 @@ export function defaultRunAnalyzer(files, opts = {}) {
|
|
|
241
274
|
let child;
|
|
242
275
|
try {
|
|
243
276
|
child = spawn(
|
|
244
|
-
findAnalyzerBin(),
|
|
277
|
+
findAnalyzerBin(opts.root || opts.cwd),
|
|
245
278
|
[
|
|
246
279
|
'lint',
|
|
247
280
|
'--reporter=json',
|
|
@@ -249,6 +282,7 @@ export function defaultRunAnalyzer(files, opts = {}) {
|
|
|
249
282
|
'--diagnostic-level=info',
|
|
250
283
|
'--colors=off',
|
|
251
284
|
'--files-ignore-unknown=true',
|
|
285
|
+
'--',
|
|
252
286
|
...files,
|
|
253
287
|
],
|
|
254
288
|
{ cwd: opts.cwd || process.cwd() },
|
|
@@ -319,37 +353,50 @@ export async function analyzePayload(payload, opts = {}) {
|
|
|
319
353
|
try {
|
|
320
354
|
const tool = resolveTool(payload);
|
|
321
355
|
const eventId = resolveEventId(payload);
|
|
322
|
-
const
|
|
323
|
-
|
|
356
|
+
const root = opts.root || opts.cwd || process.cwd();
|
|
357
|
+
const skipped = [];
|
|
358
|
+
const targets = collectTargets(payload, { root, skipped });
|
|
359
|
+
const skippedAnnotations = skipped.slice(0, 10).map((s) => toFinding({
|
|
360
|
+
tool,
|
|
361
|
+
eventId,
|
|
362
|
+
rule: 'analysis-bridge/skipped-target',
|
|
363
|
+
file: s,
|
|
364
|
+
line: null,
|
|
365
|
+
message: `target path outside project root or invalid: ${s} (skipped)`,
|
|
366
|
+
severity: 'info',
|
|
367
|
+
}));
|
|
368
|
+
|
|
369
|
+
if (targets.length === 0) return { allowed: true, annotations: skippedAnnotations };
|
|
324
370
|
const timeoutMs = Number(opts.timeoutMs ?? ANALYZER_TIMEOUT_MS);
|
|
325
371
|
const budget = Number.isFinite(timeoutMs) && timeoutMs >= 0 ? timeoutMs : ANALYZER_TIMEOUT_MS;
|
|
326
372
|
const runAnalyzer = typeof opts.runAnalyzer === 'function' ? opts.runAnalyzer : defaultRunAnalyzer;
|
|
327
373
|
const res = await runBounded(
|
|
328
|
-
runAnalyzer(targets, { timeoutMs: budget, cwd: opts.cwd }),
|
|
374
|
+
runAnalyzer(targets, { timeoutMs: budget, cwd: opts.cwd, root }),
|
|
329
375
|
budget,
|
|
330
376
|
);
|
|
331
377
|
if (!res || typeof res !== 'object') {
|
|
332
|
-
return { allowed: true, annotations: [errorFinding(tool, eventId, targets, 'malformed analyzer result')] };
|
|
378
|
+
return { allowed: true, annotations: [...skippedAnnotations, errorFinding(tool, eventId, targets, 'malformed analyzer result')] };
|
|
333
379
|
}
|
|
334
|
-
if (res.status === 'missing') return { allowed: true, annotations:
|
|
380
|
+
if (res.status === 'missing') return { allowed: true, annotations: skippedAnnotations };
|
|
335
381
|
if (res.status === 'timeout') {
|
|
336
|
-
return { allowed: true, annotations: [timeoutFinding(tool, eventId, targets, budget)] };
|
|
382
|
+
return { allowed: true, annotations: [...skippedAnnotations, timeoutFinding(tool, eventId, targets, budget)] };
|
|
337
383
|
}
|
|
338
384
|
if (res.status === 'error') {
|
|
339
|
-
return { allowed: true, annotations: [errorFinding(tool, eventId, targets, res.message || 'unknown error')] };
|
|
385
|
+
return { allowed: true, annotations: [...skippedAnnotations, errorFinding(tool, eventId, targets, res.message || 'unknown error')] };
|
|
340
386
|
}
|
|
341
387
|
if (res.status === 'ok' && Array.isArray(res.findings)) {
|
|
342
388
|
const findings = res.findings
|
|
389
|
+
.filter((f) => f && typeof f === 'object' && !Array.isArray(f))
|
|
343
390
|
.slice(0, MAX_FINDINGS)
|
|
344
391
|
.map((f) => toFinding({ ...f, tool: f?.tool ?? tool, eventId: f?.eventId ?? eventId }));
|
|
345
|
-
return { allowed: true, annotations: findings };
|
|
392
|
+
return { allowed: true, annotations: [...skippedAnnotations, ...findings] };
|
|
346
393
|
}
|
|
347
394
|
if (typeof res.stdout === 'string') {
|
|
348
395
|
// Default-runner raw form: parse Biome JSON reporter output here so
|
|
349
396
|
// injected mocks may return either parsed findings or raw stdout.
|
|
350
|
-
return { allowed: true, annotations: parseBiomeJson(res.stdout, tool, eventId) };
|
|
397
|
+
return { allowed: true, annotations: [...skippedAnnotations, ...parseBiomeJson(res.stdout, tool, eventId)] };
|
|
351
398
|
}
|
|
352
|
-
return { allowed: true, annotations: [errorFinding(tool, eventId, targets, 'malformed analyzer result')] };
|
|
399
|
+
return { allowed: true, annotations: [...skippedAnnotations, errorFinding(tool, eventId, targets, 'malformed analyzer result')] };
|
|
353
400
|
} catch (err) {
|
|
354
401
|
// Last resort: fail-open allow with a single error annotation.
|
|
355
402
|
try {
|
|
@@ -433,7 +480,7 @@ export function createBridgeHandler(bus, opts = {}) {
|
|
|
433
480
|
const result = await analyzePayload(payload, runOpts);
|
|
434
481
|
const annotations = Array.isArray(result?.annotations) ? result.annotations : [];
|
|
435
482
|
if (annotations.length === 0) {
|
|
436
|
-
const targets = collectTargets(payload);
|
|
483
|
+
const targets = collectTargets(payload, runOpts);
|
|
437
484
|
if (targets.length === 0) {
|
|
438
485
|
return { verdict: 'allow', reason: 'analysis-bridge: no js/ts targets; skipped' };
|
|
439
486
|
}
|
|
@@ -461,14 +508,21 @@ export function createBridgeHandler(bus, opts = {}) {
|
|
|
461
508
|
}
|
|
462
509
|
}
|
|
463
510
|
}
|
|
464
|
-
const
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
511
|
+
const nonSkipped = annotations.filter((f) => f.rule !== 'analysis-bridge/skipped-target');
|
|
512
|
+
if (nonSkipped.length > 0) {
|
|
513
|
+
const files = new Set(nonSkipped.map((f) => f.file).filter(Boolean));
|
|
514
|
+
notifySummary(runOpts, {
|
|
515
|
+
title: 'IUMBTEMS analysis',
|
|
516
|
+
message: `analysis-bridge: ${nonSkipped.length} finding(s) in ${files.size} file(s) (advisory; action allowed)`,
|
|
517
|
+
});
|
|
518
|
+
return {
|
|
519
|
+
verdict: 'advisory',
|
|
520
|
+
reason: `analysis-bridge: ${nonSkipped.length} finding(s) in ${files.size} file(s) (advisory; action allowed)`,
|
|
521
|
+
};
|
|
522
|
+
}
|
|
469
523
|
return {
|
|
470
|
-
verdict: '
|
|
471
|
-
reason: `analysis-bridge: ${annotations.length}
|
|
524
|
+
verdict: 'allow',
|
|
525
|
+
reason: `analysis-bridge: ${annotations.length} target(s) skipped (advisory; action allowed)`,
|
|
472
526
|
};
|
|
473
527
|
} catch {
|
|
474
528
|
return { verdict: 'allow', reason: 'analysis-bridge: internal fault; fail-open allow' };
|