dual-brain 7.1.2 → 7.1.4
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/bin/dual-brain.mjs +38 -28
- package/mcp-server/index.mjs +1 -1
- package/package.json +44 -4
- package/src/decide.mjs +32 -0
- package/src/index.mjs +1 -1
- package/src/profile.mjs +7 -4
- package/src/session.mjs +50 -10
- package/src/tui.mjs +10 -1
- package/hooks/agent-fleet.mjs +0 -659
- package/hooks/context-guard.mjs +0 -468
- package/hooks/dag-scheduler.mjs +0 -1249
- package/hooks/head-guard.sh +0 -41
- package/hooks/hook-dispatch.mjs +0 -254
- package/hooks/ledger-analysis.mjs +0 -337
- package/hooks/parallelism-scaler.mjs +0 -572
- package/hooks/quality-tiers.mjs +0 -642
- package/src/test.mjs +0 -1374
package/hooks/context-guard.mjs
DELETED
|
@@ -1,468 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* context-guard.mjs — Keep the head (Opus) context window clean.
|
|
4
|
-
*
|
|
5
|
-
* The head orchestrates work but must never bloat its context with raw agent
|
|
6
|
-
* output, large code blocks, or verbose analysis. This module provides helpers
|
|
7
|
-
* to compress, summarize, and route information appropriately.
|
|
8
|
-
*
|
|
9
|
-
* Exports:
|
|
10
|
-
* compressAgentResult(result, maxLength?) — strip noise, return tight summary
|
|
11
|
-
* buildHandoff(fromAgent, toAgent, ctx) — minimal inter-agent payload
|
|
12
|
-
* estimateContextCost(message) — token estimate + routing hint
|
|
13
|
-
* formatHeadUpdate(agentType, taskId, result) — 1-line head-visible status
|
|
14
|
-
* shouldDelegate(description, ctxSize) — inline vs delegate decision
|
|
15
|
-
* buildAgentPipeline(intent, risk, cplx) — ordered agent type list
|
|
16
|
-
*
|
|
17
|
-
* CLI: node hooks/context-guard.mjs --estimate "some long text here"
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
import { classifyTask, INTENTS } from './task-classifier.mjs';
|
|
21
|
-
|
|
22
|
-
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
23
|
-
|
|
24
|
-
// Rough heuristic: average English token is ~4 chars (GPT/Claude tokenizers)
|
|
25
|
-
const CHARS_PER_TOKEN = 4;
|
|
26
|
-
|
|
27
|
-
// Context thresholds (in tokens)
|
|
28
|
-
const INLINE_LIMIT = 500; // small enough to paste into head context
|
|
29
|
-
const SUMMARIZE_LIMIT = 2000; // compress before showing to head
|
|
30
|
-
// anything above SUMMARIZE_LIMIT → delegate entirely
|
|
31
|
-
|
|
32
|
-
// Patterns to strip from raw agent output
|
|
33
|
-
const STRIP_PATTERNS = [
|
|
34
|
-
/```[\s\S]*?```/g, // fenced code blocks
|
|
35
|
-
/`[^`\n]{10,}`/g, // long inline code
|
|
36
|
-
/^\s*(at\s+\S+\s+\(.*\).*$)/gm, // stack trace lines
|
|
37
|
-
/^\s*Error:\s+.+\n(\s{2,}.+\n)*/gm, // error + indented detail
|
|
38
|
-
/\n{3,}/g, // triple+ blank lines → double
|
|
39
|
-
/^\s*\d+\s*[|│]\s*/gm, // line-number prefixes from cat -n style output
|
|
40
|
-
/^(DEBUG|TRACE|VERBOSE):.+$/gim, // debug log lines
|
|
41
|
-
];
|
|
42
|
-
|
|
43
|
-
// Words that signal a blocker/failure in free-form output
|
|
44
|
-
const BLOCKER_PATTERNS = /\b(error|fail(?:ed|ure)?|exception|blocked?|cannot|could not|unable|missing|not found|refused|rejected|timeout|abort)\b/i;
|
|
45
|
-
|
|
46
|
-
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Strip noise from raw text: code blocks, stack traces, debug lines, etc.
|
|
50
|
-
*/
|
|
51
|
-
function stripNoise(text) {
|
|
52
|
-
let out = String(text || '');
|
|
53
|
-
for (const pattern of STRIP_PATTERNS) {
|
|
54
|
-
out = out.replace(pattern, pattern.source === '\\n{3,}' ? '\n\n' : ' ');
|
|
55
|
-
}
|
|
56
|
-
return out.trim();
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Extract first N sentences from cleaned text.
|
|
61
|
-
*/
|
|
62
|
-
function firstSentences(text, n = 2) {
|
|
63
|
-
const sentences = text
|
|
64
|
-
.split(/(?<=[.!?])\s+/)
|
|
65
|
-
.map(s => s.trim())
|
|
66
|
-
.filter(s => s.length > 5);
|
|
67
|
-
return sentences.slice(0, n).join(' ');
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Detect outcome (success / fail / partial) from raw text.
|
|
72
|
-
*/
|
|
73
|
-
function detectOutcome(text) {
|
|
74
|
-
const lower = text.toLowerCase();
|
|
75
|
-
if (/\b(success(?:fully)?|completed?|done|all tests pass|no issues|lgtm)\b/.test(lower)) return 'success';
|
|
76
|
-
if (BLOCKER_PATTERNS.test(lower)) return 'fail';
|
|
77
|
-
return 'partial';
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Extract file paths mentioned in text (basic heuristic).
|
|
82
|
-
*/
|
|
83
|
-
function extractMentionedFiles(text) {
|
|
84
|
-
const matches = text.match(/\b[\w./\-]+\.\w{2,6}\b/g) || [];
|
|
85
|
-
// Filter out noise (URLs, version strings, etc.)
|
|
86
|
-
return [...new Set(matches.filter(f =>
|
|
87
|
-
!f.startsWith('http') && f.includes('/') || /\.(mjs|ts|js|json|md|py|go|rs|sh|yaml|yml|toml)$/.test(f)
|
|
88
|
-
))].slice(0, 10);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Extract key decisions from text — lines starting with decision verbs or
|
|
93
|
-
* "chose / decided / picked / will use" patterns.
|
|
94
|
-
*/
|
|
95
|
-
function extractKeyDecisions(text) {
|
|
96
|
-
const decisionRe = /^.{0,40}(chose|decided|picked|will use|using|switched|moved to|adopted|recommended|selected).{0,120}/im;
|
|
97
|
-
const bullets = text.match(/^[-*•]\s+.{10,100}/gm) || [];
|
|
98
|
-
const inline = text.match(decisionRe) || [];
|
|
99
|
-
return [...inline.map(s => s.trim()), ...bullets.map(s => s.replace(/^[-*•]\s+/, ''))].slice(0, 5);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Find open questions in the text.
|
|
104
|
-
*/
|
|
105
|
-
function extractOpenQuestions(text) {
|
|
106
|
-
return (text.match(/[A-Z][^?.!]*\?/g) || [])
|
|
107
|
-
.map(q => q.trim())
|
|
108
|
-
.filter(q => q.length > 15 && q.length < 150)
|
|
109
|
-
.slice(0, 3);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// ─── Core Functions ───────────────────────────────────────────────────────────
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Compress a raw agent result to at most `maxLength` characters.
|
|
116
|
-
* Strips code blocks, stack traces, and verbose explanations.
|
|
117
|
-
* Returns an object rather than a string so callers get structured data too.
|
|
118
|
-
*
|
|
119
|
-
* @param {string|object} result Raw agent output (string or object with .output)
|
|
120
|
-
* @param {number} maxLength Character cap for the summary field (default 300)
|
|
121
|
-
* @returns {{ outcome, summary, filesAffected, keyDecisions, blockers, originalLength }}
|
|
122
|
-
*/
|
|
123
|
-
function compressAgentResult(result, maxLength = 300) {
|
|
124
|
-
const raw = typeof result === 'string'
|
|
125
|
-
? result
|
|
126
|
-
: (result?.output ?? result?.message ?? result?.text ?? JSON.stringify(result));
|
|
127
|
-
|
|
128
|
-
const originalLength = raw.length;
|
|
129
|
-
const cleaned = stripNoise(raw);
|
|
130
|
-
const outcome = detectOutcome(raw);
|
|
131
|
-
const filesAffected = extractMentionedFiles(raw);
|
|
132
|
-
const keyDecisions = extractKeyDecisions(cleaned);
|
|
133
|
-
|
|
134
|
-
// Blocker extraction: grab the first matching sentence
|
|
135
|
-
const blockerMatch = raw.match(new RegExp(BLOCKER_PATTERNS.source + '.{0,200}', 'i'));
|
|
136
|
-
const blockers = blockerMatch ? [blockerMatch[0].slice(0, 120).trim()] : [];
|
|
137
|
-
|
|
138
|
-
// Summary: take the first 2 sentences of cleaned text, then truncate
|
|
139
|
-
let summary = firstSentences(cleaned, 2);
|
|
140
|
-
if (!summary && cleaned.length > 0) summary = cleaned.slice(0, 150);
|
|
141
|
-
if (summary.length > maxLength) summary = summary.slice(0, maxLength - 1) + '…';
|
|
142
|
-
|
|
143
|
-
return { outcome, summary, filesAffected, keyDecisions, blockers, originalLength };
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Build a minimal handoff payload from one agent to the next.
|
|
148
|
-
* Only carries what the next agent actually needs — not the full prior output.
|
|
149
|
-
*
|
|
150
|
-
* @param {string} fromAgent e.g. 'researcher', 'planner', 'worker'
|
|
151
|
-
* @param {string} toAgent e.g. 'worker', 'reviewer', 'tester'
|
|
152
|
-
* @param {object} context Raw output or structured result from fromAgent
|
|
153
|
-
* @returns {{ summary, keyDecisions, filesAffected, constraints, openQuestions }}
|
|
154
|
-
*/
|
|
155
|
-
function buildHandoff(fromAgent, toAgent, context) {
|
|
156
|
-
const compressed = compressAgentResult(context, 400);
|
|
157
|
-
|
|
158
|
-
// Derive constraints: things the next agent must respect
|
|
159
|
-
const raw = typeof context === 'string' ? context : JSON.stringify(context);
|
|
160
|
-
const constraintRe = /\b(must|should|cannot|don't|do not|never|always|required?|constraint)\b.{5,100}/gi;
|
|
161
|
-
const constraints = (raw.match(constraintRe) || [])
|
|
162
|
-
.map(s => s.trim().slice(0, 100))
|
|
163
|
-
.slice(0, 4);
|
|
164
|
-
|
|
165
|
-
return {
|
|
166
|
-
from: fromAgent,
|
|
167
|
-
to: toAgent,
|
|
168
|
-
summary: compressed.summary,
|
|
169
|
-
keyDecisions: compressed.keyDecisions,
|
|
170
|
-
filesAffected: compressed.filesAffected,
|
|
171
|
-
constraints,
|
|
172
|
-
openQuestions: extractOpenQuestions(raw),
|
|
173
|
-
outcome: compressed.outcome,
|
|
174
|
-
blockers: compressed.blockers,
|
|
175
|
-
};
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/**
|
|
179
|
-
* Estimate how many tokens a message would add to context.
|
|
180
|
-
* Uses ~4 chars/token heuristic (close enough for routing decisions).
|
|
181
|
-
*
|
|
182
|
-
* @param {string} message
|
|
183
|
-
* @returns {{ tokens, isHeavy, recommendation: 'inline'|'summarize'|'delegate' }}
|
|
184
|
-
*/
|
|
185
|
-
function estimateContextCost(message) {
|
|
186
|
-
const text = typeof message === 'string' ? message : JSON.stringify(message ?? '');
|
|
187
|
-
const tokens = Math.ceil(text.length / CHARS_PER_TOKEN);
|
|
188
|
-
const isHeavy = tokens > INLINE_LIMIT;
|
|
189
|
-
|
|
190
|
-
let recommendation;
|
|
191
|
-
if (tokens <= INLINE_LIMIT) {
|
|
192
|
-
recommendation = 'inline';
|
|
193
|
-
} else if (tokens <= SUMMARIZE_LIMIT) {
|
|
194
|
-
recommendation = 'summarize';
|
|
195
|
-
} else {
|
|
196
|
-
recommendation = 'delegate';
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
return { tokens, chars: text.length, isHeavy, recommendation };
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/**
|
|
203
|
-
* Format a single-line status update for the head's context.
|
|
204
|
-
* The head sees this — nothing more — when an agent finishes.
|
|
205
|
-
*
|
|
206
|
-
* Examples:
|
|
207
|
-
* "worker:task-3 completed — 2 files changed, tests pass"
|
|
208
|
-
* "brainstorm:task-1 done — 5 ideas, top pick: DAG scheduler"
|
|
209
|
-
* "debugger:task-2 failed — TypeError in auth.mjs line 42"
|
|
210
|
-
*
|
|
211
|
-
* @param {string} agentType e.g. 'worker', 'brainstorm', 'debugger'
|
|
212
|
-
* @param {string} taskId e.g. 'task-3'
|
|
213
|
-
* @param {string|object} result Raw agent output
|
|
214
|
-
* @returns {string}
|
|
215
|
-
*/
|
|
216
|
-
function formatHeadUpdate(agentType, taskId, result) {
|
|
217
|
-
const { outcome, summary, filesAffected, blockers } = compressAgentResult(result, 120);
|
|
218
|
-
|
|
219
|
-
const prefix = `${agentType}:${taskId}`;
|
|
220
|
-
const status = outcome === 'success' ? 'completed'
|
|
221
|
-
: outcome === 'fail' ? 'failed'
|
|
222
|
-
: 'partial';
|
|
223
|
-
|
|
224
|
-
// Build a tight detail string
|
|
225
|
-
const parts = [];
|
|
226
|
-
|
|
227
|
-
if (filesAffected.length > 0) {
|
|
228
|
-
parts.push(`${filesAffected.length} file${filesAffected.length > 1 ? 's' : ''} changed`);
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
if (outcome === 'fail' && blockers.length > 0) {
|
|
232
|
-
parts.push(blockers[0].slice(0, 80));
|
|
233
|
-
} else if (summary) {
|
|
234
|
-
// Use summary but keep it very tight
|
|
235
|
-
const tight = summary.replace(/\n/g, ' ').slice(0, 80);
|
|
236
|
-
parts.push(tight);
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
const detail = parts.join(', ');
|
|
240
|
-
return `${prefix} ${status}${detail ? ' — ' + detail : ''}`;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
/**
|
|
244
|
-
* Decide whether the head should handle a task inline or delegate it.
|
|
245
|
-
*
|
|
246
|
-
* Small read-only lookups under 100 chars are cheap enough to inline.
|
|
247
|
-
* Anything involving analysis, code reading, or multi-step reasoning
|
|
248
|
-
* should be delegated to preserve the head's context budget.
|
|
249
|
-
*
|
|
250
|
-
* @param {string} description Task description
|
|
251
|
-
* @param {number} currentContextSize Estimated current context size in tokens
|
|
252
|
-
* @returns {{ delegate: boolean, reason: string, recommendation: 'inline'|'delegate' }}
|
|
253
|
-
*/
|
|
254
|
-
function shouldDelegate(description, currentContextSize = 0) {
|
|
255
|
-
const desc = String(description || '');
|
|
256
|
-
|
|
257
|
-
// Short read-only queries are cheap
|
|
258
|
-
const isShort = desc.length < 100;
|
|
259
|
-
const readOnlyRe = /\b(what|where|list|show|find|which|how many|does|is|are|check)\b/i;
|
|
260
|
-
const isReadOnly = readOnlyRe.test(desc);
|
|
261
|
-
|
|
262
|
-
// Signals that demand delegation
|
|
263
|
-
const analysisRe = /\b(analyze|analyse|read|review|refactor|implement|write|build|fix|debug|test|compare|explain|design)\b/i;
|
|
264
|
-
const multiStepRe = /\b(and (also|then)|also|then|after (that|which)|step \d|first .* then)\b/i;
|
|
265
|
-
const isAnalytic = analysisRe.test(desc);
|
|
266
|
-
const isMultiStep = multiStepRe.test(desc);
|
|
267
|
-
|
|
268
|
-
// Context pressure: if head context is already large, be more aggressive
|
|
269
|
-
const contextHeavy = currentContextSize > 4000; // tokens
|
|
270
|
-
|
|
271
|
-
// Decision logic
|
|
272
|
-
if (isAnalytic || isMultiStep || contextHeavy) {
|
|
273
|
-
const reasons = [];
|
|
274
|
-
if (isAnalytic) reasons.push('requires analysis/code work');
|
|
275
|
-
if (isMultiStep) reasons.push('multi-step');
|
|
276
|
-
if (contextHeavy) reasons.push(`context at ${currentContextSize} tokens`);
|
|
277
|
-
return { delegate: true, recommendation: 'delegate', reason: reasons.join(', ') };
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
if (isShort && isReadOnly) {
|
|
281
|
-
return { delegate: false, recommendation: 'inline', reason: 'short read-only query' };
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
// Default: delegate anything ambiguous to protect head context
|
|
285
|
-
return { delegate: true, recommendation: 'delegate', reason: 'ambiguous — defaulting to delegate for safety' };
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
/**
|
|
289
|
-
* Given a task profile, return an ordered list of agent types to run.
|
|
290
|
-
* Agents are named by role, not model. The orchestrator maps roles to models.
|
|
291
|
-
*
|
|
292
|
-
* Pipeline examples:
|
|
293
|
-
* refactor auth module → ['analyst', 'research', 'planner', 'worker', 'reviewer']
|
|
294
|
-
* what's the best approach for X → ['brainstorm']
|
|
295
|
-
* fix this bug → ['debugger', 'worker', 'tester']
|
|
296
|
-
* add comprehensive tests → ['research', 'tester', 'reviewer']
|
|
297
|
-
*
|
|
298
|
-
* @param {string} intent From task-classifier INTENTS keys
|
|
299
|
-
* @param {string} risk 'low' | 'medium' | 'high' | 'critical'
|
|
300
|
-
* @param {string} complexity 'trivial' | 'simple' | 'moderate' | 'complex'
|
|
301
|
-
* @returns {string[]} Ordered agent type names
|
|
302
|
-
*/
|
|
303
|
-
function buildAgentPipeline(intent, risk, complexity) {
|
|
304
|
-
const LEVEL_ORDER = { low: 0, medium: 1, high: 2, critical: 3 };
|
|
305
|
-
const riskLevel = LEVEL_ORDER[risk] ?? 0;
|
|
306
|
-
const cplxLevel = { trivial: 0, simple: 1, moderate: 2, complex: 3 }[complexity] ?? 1;
|
|
307
|
-
|
|
308
|
-
const isCritical = riskLevel >= 3;
|
|
309
|
-
const isComplex = cplxLevel >= 2;
|
|
310
|
-
const isHighRisk = riskLevel >= 2;
|
|
311
|
-
const needsReview = isHighRisk || isComplex || isCritical;
|
|
312
|
-
|
|
313
|
-
let pipeline;
|
|
314
|
-
|
|
315
|
-
switch (intent) {
|
|
316
|
-
// ── Pure thinking / ideation ──
|
|
317
|
-
case 'architecture':
|
|
318
|
-
case 'planning':
|
|
319
|
-
case 'compare':
|
|
320
|
-
pipeline = ['brainstorm', 'planner'];
|
|
321
|
-
break;
|
|
322
|
-
|
|
323
|
-
// ── Explain / document: look up then write ──
|
|
324
|
-
case 'explain':
|
|
325
|
-
case 'document':
|
|
326
|
-
pipeline = ['research', 'worker'];
|
|
327
|
-
break;
|
|
328
|
-
|
|
329
|
-
// ── Search / format: lightweight, no review needed ──
|
|
330
|
-
case 'search':
|
|
331
|
-
pipeline = ['research'];
|
|
332
|
-
break;
|
|
333
|
-
|
|
334
|
-
case 'format':
|
|
335
|
-
pipeline = ['worker'];
|
|
336
|
-
break;
|
|
337
|
-
|
|
338
|
-
// ── Debug: diagnose → fix → verify ──
|
|
339
|
-
case 'debug':
|
|
340
|
-
pipeline = ['debugger', 'worker', 'tester'];
|
|
341
|
-
break;
|
|
342
|
-
|
|
343
|
-
// ── Test: understand existing code, write tests, review coverage ──
|
|
344
|
-
case 'test':
|
|
345
|
-
pipeline = ['research', 'tester', 'reviewer'];
|
|
346
|
-
break;
|
|
347
|
-
|
|
348
|
-
// ── Review / audit: read → assess ──
|
|
349
|
-
case 'review':
|
|
350
|
-
case 'security':
|
|
351
|
-
pipeline = ['research', 'reviewer'];
|
|
352
|
-
break;
|
|
353
|
-
|
|
354
|
-
// ── Refactor: plan before touching anything ──
|
|
355
|
-
case 'refactor':
|
|
356
|
-
pipeline = ['research', 'planner', 'worker', 'reviewer'];
|
|
357
|
-
break;
|
|
358
|
-
|
|
359
|
-
// ── Default edit: search → implement ──
|
|
360
|
-
case 'edit':
|
|
361
|
-
default:
|
|
362
|
-
pipeline = isComplex
|
|
363
|
-
? ['research', 'planner', 'worker']
|
|
364
|
-
: ['worker'];
|
|
365
|
-
break;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
// Prepend 'analyst' for complex or critical work (up-front risk analysis)
|
|
369
|
-
if (isCritical || (isComplex && needsReview)) {
|
|
370
|
-
if (pipeline[0] !== 'analyst') pipeline.unshift('analyst');
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
// Append 'reviewer' for high-risk / complex work (if not already present)
|
|
374
|
-
if (needsReview && !pipeline.includes('reviewer')) {
|
|
375
|
-
pipeline.push('reviewer');
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
return pipeline;
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
// ─── CLI ──────────────────────────────────────────────────────────────────────
|
|
382
|
-
|
|
383
|
-
if (process.argv[1] && new URL(import.meta.url).pathname === process.argv[1]) {
|
|
384
|
-
const args = process.argv.slice(2);
|
|
385
|
-
const flag = args[0];
|
|
386
|
-
const value = args.slice(1).join(' ') || args[0];
|
|
387
|
-
|
|
388
|
-
if (flag === '--estimate') {
|
|
389
|
-
const text = args.slice(1).join(' ');
|
|
390
|
-
if (!text) {
|
|
391
|
-
console.error('Usage: node hooks/context-guard.mjs --estimate "some text"');
|
|
392
|
-
process.exit(1);
|
|
393
|
-
}
|
|
394
|
-
const result = estimateContextCost(text);
|
|
395
|
-
console.log(JSON.stringify(result, null, 2));
|
|
396
|
-
|
|
397
|
-
} else if (flag === '--compress') {
|
|
398
|
-
const text = args.slice(1).join(' ');
|
|
399
|
-
if (!text) {
|
|
400
|
-
console.error('Usage: node hooks/context-guard.mjs --compress "agent output..."');
|
|
401
|
-
process.exit(1);
|
|
402
|
-
}
|
|
403
|
-
const result = compressAgentResult(text);
|
|
404
|
-
console.log(JSON.stringify(result, null, 2));
|
|
405
|
-
|
|
406
|
-
} else if (flag === '--pipeline') {
|
|
407
|
-
// node hooks/context-guard.mjs --pipeline "refactor auth" [--risk high] [--complexity complex]
|
|
408
|
-
const descParts = [];
|
|
409
|
-
let risk = 'medium';
|
|
410
|
-
let complexity = 'moderate';
|
|
411
|
-
|
|
412
|
-
for (let i = 1; i < args.length; i++) {
|
|
413
|
-
if (args[i] === '--risk') { risk = args[++i]; }
|
|
414
|
-
else if (args[i] === '--complexity') { complexity = args[++i]; }
|
|
415
|
-
else descParts.push(args[i]);
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
const description = descParts.join(' ');
|
|
419
|
-
if (!description && descParts.length === 0) {
|
|
420
|
-
console.error('Usage: node hooks/context-guard.mjs --pipeline "description" [--risk medium] [--complexity moderate]');
|
|
421
|
-
process.exit(1);
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
// If a full description was given, derive intent from task-classifier
|
|
425
|
-
let intent = 'edit';
|
|
426
|
-
if (description) {
|
|
427
|
-
const profile = classifyTask(description);
|
|
428
|
-
intent = profile.intent;
|
|
429
|
-
risk = profile.risk;
|
|
430
|
-
complexity = profile.complexity;
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
const pipeline = buildAgentPipeline(intent, risk, complexity);
|
|
434
|
-
console.log(JSON.stringify({ intent, risk, complexity, pipeline }, null, 2));
|
|
435
|
-
|
|
436
|
-
} else if (flag === '--delegate') {
|
|
437
|
-
const desc = args.slice(1).join(' ');
|
|
438
|
-
const ctxArg = args.find(a => a.startsWith('--context='));
|
|
439
|
-
const ctxSize = ctxArg ? parseInt(ctxArg.replace('--context=', ''), 10) : 0;
|
|
440
|
-
const result = shouldDelegate(desc, ctxSize);
|
|
441
|
-
console.log(JSON.stringify(result, null, 2));
|
|
442
|
-
|
|
443
|
-
} else {
|
|
444
|
-
console.log([
|
|
445
|
-
'context-guard.mjs — Head context management tools',
|
|
446
|
-
'',
|
|
447
|
-
'Usage:',
|
|
448
|
-
' node hooks/context-guard.mjs --estimate "text" # token estimate + routing hint',
|
|
449
|
-
' node hooks/context-guard.mjs --compress "agent output..." # compress to head-safe summary',
|
|
450
|
-
' node hooks/context-guard.mjs --pipeline "task description" # build agent pipeline',
|
|
451
|
-
' node hooks/context-guard.mjs --delegate "task description" # inline vs delegate',
|
|
452
|
-
'',
|
|
453
|
-
'Exports: compressAgentResult, buildHandoff, estimateContextCost,',
|
|
454
|
-
' formatHeadUpdate, shouldDelegate, buildAgentPipeline',
|
|
455
|
-
].join('\n'));
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
// ─── Exports ──────────────────────────────────────────────────────────────────
|
|
460
|
-
|
|
461
|
-
export {
|
|
462
|
-
compressAgentResult,
|
|
463
|
-
buildHandoff,
|
|
464
|
-
estimateContextCost,
|
|
465
|
-
formatHeadUpdate,
|
|
466
|
-
shouldDelegate,
|
|
467
|
-
buildAgentPipeline,
|
|
468
|
-
};
|