dual-brain 0.2.8 → 0.2.10
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 +208 -42
- package/package.json +9 -2
- package/src/agents/registry.mjs +405 -0
- package/src/collaboration.mjs +545 -0
- package/src/detect.mjs +73 -1
- package/src/dispatch.mjs +47 -5
- package/src/head.mjs +705 -263
- package/src/pipeline.mjs +387 -163
- package/src/profile.mjs +82 -1
- package/src/provider-context.mjs +257 -0
package/src/head.mjs
CHANGED
|
@@ -1,353 +1,795 @@
|
|
|
1
1
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
4
|
+
const STATE_DIR = join(process.cwd(), '.dualbrain');
|
|
5
|
+
const STATE_FILE = join(STATE_DIR, 'head-state.json');
|
|
6
|
+
|
|
7
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
8
|
+
// HEAD — Cognitive Judgment Pipeline
|
|
9
|
+
//
|
|
10
|
+
// Five artifacts flow through every turn:
|
|
11
|
+
// perceive → assess uncertainty → derive obligations → notice → deliberate
|
|
12
|
+
//
|
|
13
|
+
// SituationModel: what's happening (replaces classifyIntent)
|
|
14
|
+
// UncertaintyLedger: what HEAD knows vs suspects vs lacks (replaces checkConfidence)
|
|
15
|
+
// CareObligations: what HEAD is responsible for (replaces phase transitions)
|
|
16
|
+
// Noticings: what HEAD observes passively (replaces detectDrift)
|
|
17
|
+
// DeliberationResult: what HEAD decides to do and why (replaces processTurn)
|
|
18
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
19
|
+
|
|
20
|
+
// ── Values: these shape judgment, not rules to check ────────────────────────
|
|
21
|
+
|
|
22
|
+
export const HEAD_VALUES = {
|
|
23
|
+
selfHonesty: 'Say what you don\'t know. Never dress up guesses as facts.',
|
|
24
|
+
materialCare: 'The user\'s code, context, and time are precious. Don\'t waste them.',
|
|
25
|
+
curiosity: 'Notice what\'s off. Ask what you\'re not seeing.',
|
|
26
|
+
strategicPace: 'Know when to act fast and when to slow down.',
|
|
27
|
+
proactivity: 'Surface things the user should know, but only when it matters.',
|
|
28
|
+
restraint: 'Can do ≠ should do. Permission ≠ wisdom.',
|
|
29
|
+
honesty: 'Be honest about the material — its quality, risks, and gaps.',
|
|
30
|
+
consideration: 'Think about the user\'s actual situation, not the abstract task.',
|
|
16
31
|
};
|
|
17
32
|
|
|
18
|
-
// ──
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
/\b(parallel agents|dispatch|bump|install)\b/i,
|
|
31
|
-
],
|
|
32
|
-
approval: [
|
|
33
|
-
/^(yes|y|ok|sure|do it|go|approved|lgtm|ship it)\s*$/i,
|
|
34
|
-
/\b(go ahead|sounds good|let's do it|proceed)\b/i,
|
|
35
|
-
],
|
|
36
|
-
correction: [
|
|
37
|
-
/\b(no|stop|wait|hold|wrong|not that|don't|shouldn't|instead)\b/i,
|
|
38
|
-
/\b(actually|but|however)\b/i,
|
|
39
|
-
],
|
|
33
|
+
// ── Depth assessment: how much cognition does this deserve? ─────────────────
|
|
34
|
+
|
|
35
|
+
const DEPTH_SIGNALS = {
|
|
36
|
+
ambiguity: { weight: 3, test: (s) => s.ambiguity },
|
|
37
|
+
risk: { weight: 4, test: (s) => s.risk },
|
|
38
|
+
irreversibility: { weight: 4, test: (s) => s.reversibility === 'hard' ? 'high' : s.reversibility === 'moderate' ? 'medium' : 'low' },
|
|
39
|
+
scope: { weight: 2, test: (s) => s.scope === 'large' ? 'high' : s.scope === 'medium' ? 'medium' : 'low' },
|
|
40
|
+
priorFailures: { weight: 3, test: (s) => (s.priorFailures || 0) >= 2 ? 'high' : s.priorFailures >= 1 ? 'medium' : 'low' },
|
|
41
|
+
novelty: { weight: 2, test: (s) => s.novelty },
|
|
42
|
+
materialValue: { weight: 3, test: (s) => s.materialValue },
|
|
43
|
+
userStress: { weight: 2, test: (s) => s.userStress },
|
|
44
|
+
contextVolatility: { weight: 1, test: (s) => s.contextVolatility },
|
|
40
45
|
};
|
|
41
46
|
|
|
47
|
+
const LEVEL_SCORES = { low: 0, medium: 1, high: 2, critical: 3 };
|
|
48
|
+
|
|
42
49
|
/**
|
|
43
|
-
*
|
|
44
|
-
* Returns
|
|
50
|
+
* Assess how much deliberation this situation deserves.
|
|
51
|
+
* Returns 'reflexive' | 'light' | 'full' | 'deep'
|
|
52
|
+
*
|
|
53
|
+
* Reflexive: instant response, no deliberation (simple questions, greetings)
|
|
54
|
+
* Light: quick judgment, check obligations (standard tasks)
|
|
55
|
+
* Full: structured deliberation with uncertainty + obligations (complex/risky)
|
|
56
|
+
* Deep: full pipeline + pause for user input (ambiguous, novel, high-stakes)
|
|
45
57
|
*/
|
|
46
|
-
export function
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
for (const pattern of patterns) {
|
|
52
|
-
if (pattern.test(message)) {
|
|
53
|
-
scores[intent] += 1;
|
|
54
|
-
signals.push({ intent, pattern: pattern.source });
|
|
55
|
-
}
|
|
56
|
-
}
|
|
58
|
+
export function assessDepth(signals) {
|
|
59
|
+
let score = 0;
|
|
60
|
+
for (const [, cfg] of Object.entries(DEPTH_SIGNALS)) {
|
|
61
|
+
const level = cfg.test(signals) || 'low';
|
|
62
|
+
score += (LEVEL_SCORES[level] || 0) * cfg.weight;
|
|
57
63
|
}
|
|
58
64
|
|
|
59
|
-
|
|
60
|
-
if (
|
|
61
|
-
|
|
62
|
-
|
|
65
|
+
if (score <= 4) return 'reflexive';
|
|
66
|
+
if (score <= 12) return 'light';
|
|
67
|
+
if (score <= 22) return 'full';
|
|
68
|
+
return 'deep';
|
|
69
|
+
}
|
|
63
70
|
|
|
64
|
-
|
|
65
|
-
const sorted = Object.entries(scores).sort((a, b) => b[1] - a[1]);
|
|
66
|
-
const top = sorted[0];
|
|
67
|
-
const second = sorted[1];
|
|
71
|
+
// ── SituationModel: what's happening ────────────────────────────────────────
|
|
68
72
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
73
|
+
/**
|
|
74
|
+
* Build a situation model from user input and context.
|
|
75
|
+
* This replaces classifyIntent — instead of a label, HEAD gets a full picture.
|
|
76
|
+
*/
|
|
77
|
+
export function perceive(message, context = {}) {
|
|
78
|
+
const words = message.trim().split(/\s+/);
|
|
79
|
+
const isQuestion = /\?\s*$/.test(message.trim());
|
|
80
|
+
const isShort = words.length <= 5;
|
|
81
|
+
|
|
82
|
+
// Infer task shape from content, not regex labels
|
|
83
|
+
const taskShape = _inferTaskShape(message, context);
|
|
84
|
+
|
|
85
|
+
// Detect what the user is actually asking for vs what they said
|
|
86
|
+
const inferredGoal = _inferGoal(message, context);
|
|
87
|
+
|
|
88
|
+
// Detect urgency from language and context
|
|
89
|
+
const urgency = _assessUrgency(message, context);
|
|
90
|
+
|
|
91
|
+
// Material awareness — what code/files are relevant
|
|
92
|
+
const material = _assessMaterial(message, context);
|
|
93
|
+
|
|
94
|
+
// Relationship signals — should HEAD ask, act, or advise?
|
|
95
|
+
const relationship = _assessRelationship(message, context, taskShape);
|
|
72
96
|
|
|
73
97
|
return {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
98
|
+
raw: message,
|
|
99
|
+
explicitAsk: message.trim(),
|
|
100
|
+
inferredGoal,
|
|
101
|
+
urgency,
|
|
102
|
+
isQuestion,
|
|
103
|
+
isShort,
|
|
104
|
+
|
|
105
|
+
taskShape,
|
|
106
|
+
material,
|
|
107
|
+
relationship,
|
|
108
|
+
|
|
109
|
+
// Depth signals for adaptive processing
|
|
110
|
+
ambiguity: taskShape.ambiguity,
|
|
111
|
+
risk: taskShape.risk,
|
|
112
|
+
reversibility: taskShape.reversibility,
|
|
113
|
+
scope: taskShape.scope,
|
|
114
|
+
novelty: context.novelty || 'low',
|
|
115
|
+
materialValue: material.value,
|
|
116
|
+
userStress: urgency === 'high' ? 'high' : 'low',
|
|
117
|
+
contextVolatility: context.volatility || 'low',
|
|
118
|
+
priorFailures: context.priorFailures || 0,
|
|
79
119
|
};
|
|
80
120
|
}
|
|
81
121
|
|
|
82
|
-
|
|
122
|
+
function _inferTaskShape(message, context) {
|
|
123
|
+
const lower = message.toLowerCase();
|
|
124
|
+
|
|
125
|
+
// Scope: how big is this?
|
|
126
|
+
const fileCount = context.files?.length || 0;
|
|
127
|
+
const scope = fileCount > 5 ? 'large' : fileCount > 2 ? 'medium' : lower.length > 500 ? 'medium' : 'small';
|
|
128
|
+
|
|
129
|
+
// Risk: what could go wrong?
|
|
130
|
+
const riskSignals = [];
|
|
131
|
+
if (/\b(auth|secret|token|credential|password|key|session|permission)\b/i.test(message)) riskSignals.push('security-adjacent');
|
|
132
|
+
if (/\b(delete|remove|drop|destroy|reset|force|wipe)\b/i.test(message)) riskSignals.push('destructive-language');
|
|
133
|
+
if (/\b(deploy|publish|push|release|ship|migrate)\b/i.test(message)) riskSignals.push('external-effect');
|
|
134
|
+
if (/\b(database|db|schema|migration|table)\b/i.test(message)) riskSignals.push('data-mutation');
|
|
135
|
+
if (context.priorFailures >= 2) riskSignals.push('repeated-failure');
|
|
136
|
+
|
|
137
|
+
const risk = riskSignals.length >= 3 ? 'critical'
|
|
138
|
+
: riskSignals.length >= 2 ? 'high'
|
|
139
|
+
: riskSignals.length >= 1 ? 'medium'
|
|
140
|
+
: 'low';
|
|
141
|
+
|
|
142
|
+
// Reversibility: can this be undone?
|
|
143
|
+
const hasDestructive = riskSignals.includes('destructive-language') || riskSignals.includes('external-effect');
|
|
144
|
+
const reversibility = hasDestructive ? 'hard' : riskSignals.includes('data-mutation') ? 'moderate' : 'easy';
|
|
145
|
+
|
|
146
|
+
// Ambiguity: how clear is the request?
|
|
147
|
+
const ambiguitySignals = [];
|
|
148
|
+
if (/\b(maybe|might|could|should we|not sure|thinking about|what if|somehow)\b/i.test(message)) ambiguitySignals.push('hedging-language');
|
|
149
|
+
if (/\b(or|versus|vs|either|option|alternative)\b/i.test(message)) ambiguitySignals.push('considering-alternatives');
|
|
150
|
+
if (message.split('?').length > 2) ambiguitySignals.push('multiple-questions');
|
|
151
|
+
if (!context.files?.length && /\b(it|this|that|these|those)\b/i.test(message) && !context.recentFiles?.length) ambiguitySignals.push('vague-reference');
|
|
152
|
+
|
|
153
|
+
const ambiguity = ambiguitySignals.length >= 2 ? 'high' : ambiguitySignals.length >= 1 ? 'medium' : 'low';
|
|
154
|
+
|
|
155
|
+
// Type: what kind of work is this?
|
|
156
|
+
let type = 'unknown';
|
|
157
|
+
if (/\b(what|where|which|how many|show|list|find|search|explain|tell me)\b/i.test(message) || /\?\s*$/.test(message.trim())) type = 'answer';
|
|
158
|
+
if (/\b(fix|bug|error|broken|crash|fail|issue|wrong)\b/i.test(message)) type = 'debug';
|
|
159
|
+
if (/\b(build|create|add|implement|write|make|new)\b/i.test(message)) type = 'edit';
|
|
160
|
+
if (/\b(review|check|audit|look at|inspect)\b/i.test(message)) type = 'review';
|
|
161
|
+
if (/\b(research|investigate|explore|understand|dig into)\b/i.test(message)) type = 'research';
|
|
162
|
+
if (/\b(plan|design|architect|strategy|approach|think about|brainstorm)\b/i.test(message)) type = 'plan';
|
|
163
|
+
if (/\b(refactor|clean|reorganize|restructure|simplify)\b/i.test(message)) type = 'edit';
|
|
164
|
+
|
|
165
|
+
return { type, scope, risk, reversibility, ambiguity, riskSignals, ambiguitySignals };
|
|
166
|
+
}
|
|
83
167
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
168
|
+
function _inferGoal(message, context) {
|
|
169
|
+
// When the explicit ask might not match the real need
|
|
170
|
+
const lower = message.toLowerCase();
|
|
171
|
+
|
|
172
|
+
// "Fix the tests" when the real problem might be the code, not the tests
|
|
173
|
+
if (/fix.*(test|spec)/i.test(message) && context.recentFailures?.length) {
|
|
174
|
+
return 'May need to fix source code, not just tests';
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// "Make it work" — needs clarification
|
|
178
|
+
if (/\b(make it work|just work|get it working)\b/i.test(message)) {
|
|
179
|
+
return 'Vague success criteria — needs clarification on what "working" means';
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// "Do everything" — scope needs bounding
|
|
183
|
+
if (/\b(do everything|all of it|everything)\b/i.test(message)) {
|
|
184
|
+
return 'Unbounded scope — needs prioritization';
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function _assessUrgency(message, context) {
|
|
191
|
+
if (/\b(asap|urgent|now|immediately|hurry|quick|fast)\b/i.test(message)) return 'high';
|
|
192
|
+
if (/\b(when you get a chance|no rush|whenever|eventually)\b/i.test(message)) return 'low';
|
|
193
|
+
if (context.priorFailures >= 2) return 'high';
|
|
194
|
+
return 'medium';
|
|
99
195
|
}
|
|
100
196
|
|
|
101
|
-
function
|
|
197
|
+
function _assessMaterial(message, context) {
|
|
198
|
+
const touchedFiles = context.files || [];
|
|
199
|
+
const fragileAreas = [];
|
|
200
|
+
const existingPatterns = context.patterns || [];
|
|
201
|
+
|
|
202
|
+
// Detect fragile areas from file paths
|
|
203
|
+
for (const f of touchedFiles) {
|
|
204
|
+
if (/auth|session|token|secret|credential/i.test(f)) fragileAreas.push({ file: f, reason: 'security-sensitive' });
|
|
205
|
+
if (/migration|schema|database/i.test(f)) fragileAreas.push({ file: f, reason: 'data-layer' });
|
|
206
|
+
if (/config|env|settings/i.test(f)) fragileAreas.push({ file: f, reason: 'configuration' });
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const value = fragileAreas.length >= 2 ? 'high'
|
|
210
|
+
: fragileAreas.length >= 1 ? 'medium'
|
|
211
|
+
: touchedFiles.length > 5 ? 'medium'
|
|
212
|
+
: 'low';
|
|
213
|
+
|
|
102
214
|
return {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
activeTasks: [],
|
|
109
|
-
decisions: [],
|
|
110
|
-
contextEstimate: { messages: 0, estimatedTokens: 0, compactionRisk: 'low' },
|
|
111
|
-
driftSignals: [],
|
|
112
|
-
lastActivity: Date.now(),
|
|
113
|
-
created: Date.now(),
|
|
215
|
+
touchedFiles,
|
|
216
|
+
fragileAreas,
|
|
217
|
+
existingPatterns,
|
|
218
|
+
value,
|
|
219
|
+
userOwnedChanges: context.uncommittedFiles || [],
|
|
114
220
|
};
|
|
115
221
|
}
|
|
116
222
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
223
|
+
function _assessRelationship(message, context, taskShape) {
|
|
224
|
+
// Should HEAD ask before acting?
|
|
225
|
+
const shouldAsk = taskShape.ambiguity === 'high'
|
|
226
|
+
|| taskShape.risk === 'critical'
|
|
227
|
+
|| taskShape.reversibility === 'hard'
|
|
228
|
+
|| (taskShape.scope === 'large' && taskShape.ambiguity !== 'low');
|
|
229
|
+
|
|
230
|
+
// Is there likely a mismatch between what was asked and what's needed?
|
|
231
|
+
const likelyMismatch = !!(
|
|
232
|
+
(taskShape.type === 'debug' && context.priorFailures >= 2)
|
|
233
|
+
|| (taskShape.ambiguity === 'high' && taskShape.risk !== 'low')
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
// Might the user be assuming something wrong?
|
|
237
|
+
const wrongAssumption = !!(
|
|
238
|
+
context.staleContext
|
|
239
|
+
|| (context.priorFailures >= 2 && taskShape.type === 'debug')
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
return { shouldAsk, likelyMismatch, wrongAssumption };
|
|
124
243
|
}
|
|
125
244
|
|
|
126
|
-
// ──
|
|
245
|
+
// ── UncertaintyLedger: what HEAD knows vs doesn't ──────────────────────────
|
|
127
246
|
|
|
128
247
|
/**
|
|
129
|
-
*
|
|
248
|
+
* Build an uncertainty ledger from the situation model.
|
|
249
|
+
* Each entry: a claim, how confident HEAD is, what the evidence is,
|
|
250
|
+
* and what would change HEAD's mind.
|
|
130
251
|
*/
|
|
131
|
-
export function
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
if (
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
252
|
+
export function assessUncertainty(situation, context = {}) {
|
|
253
|
+
const ledger = [];
|
|
254
|
+
|
|
255
|
+
// Scope certainty
|
|
256
|
+
if (situation.taskShape.scope !== 'small') {
|
|
257
|
+
ledger.push({
|
|
258
|
+
claim: 'The change is contained to the identified files',
|
|
259
|
+
confidence: situation.material.touchedFiles.length > 0 ? 0.7 : 0.3,
|
|
260
|
+
basis: situation.material.touchedFiles.length > 0
|
|
261
|
+
? `${situation.material.touchedFiles.length} files identified`
|
|
262
|
+
: 'No files explicitly identified — scope unknown',
|
|
263
|
+
wouldChangeIf: 'Dependency analysis reveals cross-cutting concerns',
|
|
142
264
|
});
|
|
143
265
|
}
|
|
144
266
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
267
|
+
// Risk assessment certainty
|
|
268
|
+
if (situation.taskShape.risk !== 'low') {
|
|
269
|
+
ledger.push({
|
|
270
|
+
claim: `Risk level is ${situation.taskShape.risk}`,
|
|
271
|
+
confidence: situation.taskShape.riskSignals.length >= 2 ? 0.85 : 0.6,
|
|
272
|
+
basis: `Signals: ${situation.taskShape.riskSignals.join(', ') || 'none'}`,
|
|
273
|
+
wouldChangeIf: 'Closer inspection reveals the risky-looking code is actually isolated/tested',
|
|
274
|
+
});
|
|
275
|
+
}
|
|
152
276
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
277
|
+
// Goal understanding certainty
|
|
278
|
+
if (situation.inferredGoal) {
|
|
279
|
+
ledger.push({
|
|
280
|
+
claim: 'I understand what the user actually needs',
|
|
281
|
+
confidence: 0.5,
|
|
282
|
+
basis: `Explicit: "${situation.explicitAsk.slice(0, 80)}" but inferred: "${situation.inferredGoal}"`,
|
|
283
|
+
wouldChangeIf: 'User clarifies their actual goal',
|
|
284
|
+
});
|
|
285
|
+
} else if (situation.taskShape.ambiguity === 'high') {
|
|
286
|
+
ledger.push({
|
|
287
|
+
claim: 'I understand the request',
|
|
288
|
+
confidence: 0.4,
|
|
289
|
+
basis: `Ambiguity signals: ${situation.taskShape.ambiguitySignals.join(', ')}`,
|
|
290
|
+
wouldChangeIf: 'User provides more specific criteria or constraints',
|
|
291
|
+
});
|
|
292
|
+
}
|
|
167
293
|
|
|
168
|
-
//
|
|
294
|
+
// Prior failure uncertainty
|
|
295
|
+
if (situation.priorFailures >= 1) {
|
|
296
|
+
ledger.push({
|
|
297
|
+
claim: 'The same approach will work this time',
|
|
298
|
+
confidence: Math.max(0.1, 0.8 - (situation.priorFailures * 0.25)),
|
|
299
|
+
basis: `${situation.priorFailures} prior failure(s) on similar task`,
|
|
300
|
+
wouldChangeIf: 'A fundamentally different approach is tried',
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Material safety
|
|
305
|
+
if (situation.material.fragileAreas.length > 0) {
|
|
306
|
+
ledger.push({
|
|
307
|
+
claim: 'Changes won\'t break existing functionality',
|
|
308
|
+
confidence: 0.5,
|
|
309
|
+
basis: `Fragile areas: ${situation.material.fragileAreas.map(a => a.file).join(', ')}`,
|
|
310
|
+
wouldChangeIf: 'Tests exist and pass for the affected areas',
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Context freshness
|
|
315
|
+
if (context.contextAge === 'stale') {
|
|
316
|
+
ledger.push({
|
|
317
|
+
claim: 'My understanding of the codebase is current',
|
|
318
|
+
confidence: 0.3,
|
|
319
|
+
basis: 'Context may be outdated — files could have changed since last read',
|
|
320
|
+
wouldChangeIf: 'Fresh file reads confirm current state',
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return ledger;
|
|
325
|
+
}
|
|
169
326
|
|
|
170
327
|
/**
|
|
171
|
-
*
|
|
172
|
-
*
|
|
328
|
+
* Overall confidence from the uncertainty ledger.
|
|
329
|
+
* Not a boolean — a nuanced picture.
|
|
173
330
|
*/
|
|
174
|
-
export function
|
|
175
|
-
|
|
176
|
-
understandIntent: {
|
|
177
|
-
pass: !!(state.userGoal && state.intent !== 'unknown'),
|
|
178
|
-
question: 'Do I understand the user\'s intent?',
|
|
179
|
-
},
|
|
180
|
-
discussedApproach: {
|
|
181
|
-
pass: state.decisions.some(d => d.type === 'phase-transition' && d.to === 'discuss') || state.phase === 'plan',
|
|
182
|
-
question: 'Have we discussed the approach?',
|
|
183
|
-
},
|
|
184
|
-
honestAboutUnknowns: {
|
|
185
|
-
pass: state.driftSignals.length === 0 || state.driftSignals.every(s => s.resolved),
|
|
186
|
-
question: 'Am I honest about unknowns?',
|
|
187
|
-
},
|
|
188
|
-
reversible: {
|
|
189
|
-
pass: true, // default; caller should override for high-risk
|
|
190
|
-
question: 'Is this reversible?',
|
|
191
|
-
},
|
|
192
|
-
};
|
|
331
|
+
export function summarizeConfidence(ledger) {
|
|
332
|
+
if (ledger.length === 0) return { level: 'sufficient', score: 0.8, gaps: [] };
|
|
193
333
|
|
|
194
|
-
const
|
|
195
|
-
const
|
|
334
|
+
const avg = ledger.reduce((sum, e) => sum + e.confidence, 0) / ledger.length;
|
|
335
|
+
const gaps = ledger.filter(e => e.confidence < 0.5);
|
|
336
|
+
const blockers = ledger.filter(e => e.confidence < 0.3);
|
|
196
337
|
|
|
197
338
|
return {
|
|
198
|
-
|
|
199
|
-
score:
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
339
|
+
level: blockers.length > 0 ? 'insufficient' : gaps.length > 0 ? 'partial' : 'sufficient',
|
|
340
|
+
score: Math.round(avg * 100) / 100,
|
|
341
|
+
gaps: gaps.map(g => g.claim),
|
|
342
|
+
blockers: blockers.map(b => ({ claim: b.claim, wouldResolve: b.wouldChangeIf })),
|
|
343
|
+
entryCount: ledger.length,
|
|
203
344
|
};
|
|
204
345
|
}
|
|
205
346
|
|
|
206
|
-
// ──
|
|
347
|
+
// ── CareObligations: what HEAD is responsible for ──────────────────────────
|
|
348
|
+
|
|
349
|
+
const OBLIGATION_TYPES = {
|
|
350
|
+
preserveWork: { priority: 'critical', description: 'Don\'t destroy the user\'s uncommitted work' },
|
|
351
|
+
respectPatterns: { priority: 'high', description: 'Follow the codebase\'s existing patterns and conventions' },
|
|
352
|
+
minimizeBlast: { priority: 'high', description: 'Keep changes as small and focused as possible' },
|
|
353
|
+
verifyBeforeClaim:{ priority: 'high', description: 'Don\'t claim success without evidence' },
|
|
354
|
+
askBeforeIrreversi:{ priority: 'critical', description: 'Get permission before irreversible actions' },
|
|
355
|
+
distinguishIntent:{ priority: 'medium', description: 'Separate what the user asked from what might also be useful' },
|
|
356
|
+
protectSecrets: { priority: 'critical', description: 'Never expose, log, or transmit secrets' },
|
|
357
|
+
honestLimits: { priority: 'high', description: 'Admit when you don\'t know or aren\'t sure' },
|
|
358
|
+
contextCare: { priority: 'medium', description: 'Be economical with context — don\'t waste tokens on ceremony' },
|
|
359
|
+
timingAwareness: { priority: 'medium', description: 'Sense whether now is the right time to surface something' },
|
|
360
|
+
};
|
|
207
361
|
|
|
208
362
|
/**
|
|
209
|
-
*
|
|
363
|
+
* Derive which care obligations are active given the current situation.
|
|
210
364
|
*/
|
|
211
|
-
export function
|
|
212
|
-
const
|
|
365
|
+
export function deriveObligations(situation) {
|
|
366
|
+
const active = [];
|
|
213
367
|
|
|
214
|
-
//
|
|
215
|
-
|
|
216
|
-
|
|
368
|
+
// Always active
|
|
369
|
+
active.push({ ...OBLIGATION_TYPES.protectSecrets, type: 'protectSecrets', trigger: 'always' });
|
|
370
|
+
active.push({ ...OBLIGATION_TYPES.honestLimits, type: 'honestLimits', trigger: 'always' });
|
|
371
|
+
active.push({ ...OBLIGATION_TYPES.contextCare, type: 'contextCare', trigger: 'always' });
|
|
372
|
+
|
|
373
|
+
// Conditional obligations
|
|
374
|
+
if (situation.material.userOwnedChanges?.length > 0) {
|
|
375
|
+
active.push({ ...OBLIGATION_TYPES.preserveWork, type: 'preserveWork', trigger: `${situation.material.userOwnedChanges.length} uncommitted files` });
|
|
217
376
|
}
|
|
218
377
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
signals.push({ signal: 'no-acceptance-criteria', severity: 'medium', msg: 'Dispatch without acceptance criteria' });
|
|
378
|
+
if (situation.material.existingPatterns?.length > 0) {
|
|
379
|
+
active.push({ ...OBLIGATION_TYPES.respectPatterns, type: 'respectPatterns', trigger: `${situation.material.existingPatterns.length} existing patterns detected` });
|
|
222
380
|
}
|
|
223
381
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
signals.push({ signal: 'head-implementing', severity: 'critical', msg: 'HEAD attempting direct implementation' });
|
|
382
|
+
if (situation.taskShape.scope !== 'small' || situation.taskShape.risk !== 'low') {
|
|
383
|
+
active.push({ ...OBLIGATION_TYPES.minimizeBlast, type: 'minimizeBlast', trigger: `scope=${situation.taskShape.scope}, risk=${situation.taskShape.risk}` });
|
|
227
384
|
}
|
|
228
385
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
if (recentFailures.length >= 2) {
|
|
232
|
-
signals.push({ signal: 'repeated-failures', severity: 'high', msg: `${recentFailures.length} recent dispatch failures — consider changing approach` });
|
|
386
|
+
if (situation.taskShape.type === 'edit' || situation.taskShape.type === 'debug') {
|
|
387
|
+
active.push({ ...OBLIGATION_TYPES.verifyBeforeClaim, type: 'verifyBeforeClaim', trigger: `task type: ${situation.taskShape.type}` });
|
|
233
388
|
}
|
|
234
389
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
signals.push({ signal: 'context-pressure', severity: 'medium', msg: 'Context estimate exceeding 150k tokens — compaction risk' });
|
|
390
|
+
if (situation.taskShape.reversibility === 'hard' || situation.taskShape.risk === 'critical') {
|
|
391
|
+
active.push({ ...OBLIGATION_TYPES.askBeforeIrreversi, type: 'askBeforeIrreversi', trigger: `reversibility=${situation.taskShape.reversibility}, risk=${situation.taskShape.risk}` });
|
|
238
392
|
}
|
|
239
393
|
|
|
240
|
-
if (
|
|
241
|
-
|
|
394
|
+
if (situation.inferredGoal) {
|
|
395
|
+
active.push({ ...OBLIGATION_TYPES.distinguishIntent, type: 'distinguishIntent', trigger: `inferred goal differs: "${situation.inferredGoal}"` });
|
|
242
396
|
}
|
|
243
397
|
|
|
244
|
-
return
|
|
398
|
+
return active;
|
|
245
399
|
}
|
|
246
400
|
|
|
247
|
-
// ──
|
|
401
|
+
// ── Noticings: what HEAD observes passively ─────────────────────────────────
|
|
248
402
|
|
|
249
403
|
/**
|
|
250
|
-
*
|
|
404
|
+
* Passive observation layer.
|
|
405
|
+
* Runs on every turn — detects things the user hasn't asked about.
|
|
406
|
+
* Noticings are internal. Deliberation decides whether to surface them.
|
|
251
407
|
*/
|
|
252
|
-
export function
|
|
253
|
-
const
|
|
408
|
+
export function notice(situation, state, context = {}) {
|
|
409
|
+
const noticings = [];
|
|
410
|
+
|
|
411
|
+
// Drift: are we doing something different from what was discussed?
|
|
412
|
+
if (state.declaredGoal && situation.inferredGoal && state.declaredGoal !== situation.inferredGoal) {
|
|
413
|
+
noticings.push({
|
|
414
|
+
type: 'drift',
|
|
415
|
+
severity: 'medium',
|
|
416
|
+
observation: `Started with "${state.declaredGoal}" but current request implies "${situation.inferredGoal}"`,
|
|
417
|
+
shouldSurface: true,
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Repeated failure: same approach failing
|
|
422
|
+
if (situation.priorFailures >= 2) {
|
|
423
|
+
noticings.push({
|
|
424
|
+
type: 'pattern',
|
|
425
|
+
severity: 'high',
|
|
426
|
+
observation: `${situation.priorFailures} prior failures — the approach may be wrong, not just the execution`,
|
|
427
|
+
shouldSurface: true,
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Fragile area being touched without tests
|
|
432
|
+
for (const area of situation.material.fragileAreas) {
|
|
433
|
+
if (!context.hasTests?.[area.file]) {
|
|
434
|
+
noticings.push({
|
|
435
|
+
type: 'risk',
|
|
436
|
+
severity: 'medium',
|
|
437
|
+
observation: `${area.file} is ${area.reason} but has no test coverage`,
|
|
438
|
+
shouldSurface: situation.taskShape.type === 'edit',
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Context getting large
|
|
444
|
+
if (state.contextEstimate?.estimatedTokens > 120_000) {
|
|
445
|
+
const pct = Math.round((state.contextEstimate.estimatedTokens / 200_000) * 100);
|
|
446
|
+
noticings.push({
|
|
447
|
+
type: 'resource',
|
|
448
|
+
severity: state.contextEstimate.estimatedTokens > 160_000 ? 'high' : 'medium',
|
|
449
|
+
observation: `Context is at ~${pct}% capacity — consider wrapping up or handing off`,
|
|
450
|
+
shouldSurface: true,
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Scope creep: task growing beyond original ask
|
|
455
|
+
if (state.originalScope && situation.material.touchedFiles.length > state.originalScope * 2) {
|
|
456
|
+
noticings.push({
|
|
457
|
+
type: 'scope',
|
|
458
|
+
severity: 'medium',
|
|
459
|
+
observation: `Task has grown from ${state.originalScope} to ${situation.material.touchedFiles.length} files`,
|
|
460
|
+
shouldSurface: true,
|
|
461
|
+
});
|
|
462
|
+
}
|
|
254
463
|
|
|
255
|
-
|
|
256
|
-
if (
|
|
464
|
+
// Stale assumptions: acting on old information
|
|
465
|
+
if (context.contextAge === 'stale') {
|
|
466
|
+
noticings.push({
|
|
467
|
+
type: 'staleness',
|
|
468
|
+
severity: 'medium',
|
|
469
|
+
observation: 'Working from potentially outdated context — files may have changed',
|
|
470
|
+
shouldSurface: situation.taskShape.risk !== 'low',
|
|
471
|
+
});
|
|
472
|
+
}
|
|
257
473
|
|
|
258
|
-
//
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
474
|
+
// Opportunity: something useful HEAD noticed
|
|
475
|
+
if (context.opportunities?.length) {
|
|
476
|
+
for (const opp of context.opportunities.slice(0, 3)) {
|
|
477
|
+
noticings.push({
|
|
478
|
+
type: 'opportunity',
|
|
479
|
+
severity: 'low',
|
|
480
|
+
observation: opp,
|
|
481
|
+
shouldSurface: false, // opportunities are internal unless deliberation promotes them
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
}
|
|
264
485
|
|
|
265
|
-
return
|
|
486
|
+
return noticings;
|
|
266
487
|
}
|
|
267
488
|
|
|
268
|
-
// ──
|
|
489
|
+
// ── Deliberation: what HEAD decides to do ──────────────────────────────────
|
|
269
490
|
|
|
270
491
|
/**
|
|
271
|
-
*
|
|
492
|
+
* Full deliberation pipeline.
|
|
493
|
+
* Produces a structured decision with rationale — not just an action label.
|
|
272
494
|
*/
|
|
273
|
-
export function
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
495
|
+
export function deliberate(situation, uncertaintyLedger, obligations, noticings, state) {
|
|
496
|
+
const depth = assessDepth(situation);
|
|
497
|
+
const confidence = summarizeConfidence(uncertaintyLedger);
|
|
498
|
+
|
|
499
|
+
// ── Reflexive: instant response, minimal processing
|
|
500
|
+
if (depth === 'reflexive' && confidence.level === 'sufficient') {
|
|
501
|
+
return {
|
|
502
|
+
depth,
|
|
503
|
+
action: _reflexiveAction(situation),
|
|
504
|
+
rationale: 'Simple request with sufficient confidence',
|
|
505
|
+
confidence,
|
|
506
|
+
obligations: obligations.filter(o => o.priority === 'critical'),
|
|
507
|
+
surfaceNoticings: [],
|
|
508
|
+
shouldAskUser: false,
|
|
509
|
+
uncertainties: [],
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// ── Which noticings to surface?
|
|
514
|
+
const surfaceNoticings = noticings.filter(n => {
|
|
515
|
+
if (!n.shouldSurface) return false;
|
|
516
|
+
// Care obligation: timingAwareness — only surface if it's relevant right now
|
|
517
|
+
if (n.type === 'opportunity') return situation.taskShape.type === 'plan';
|
|
518
|
+
if (n.severity === 'high') return true;
|
|
519
|
+
if (n.severity === 'medium' && depth !== 'light') return true;
|
|
520
|
+
return false;
|
|
283
521
|
});
|
|
284
|
-
|
|
522
|
+
|
|
523
|
+
// ── Should HEAD ask the user before acting?
|
|
524
|
+
const shouldAskUser = _shouldAsk(situation, confidence, obligations, depth);
|
|
525
|
+
|
|
526
|
+
// ── Generate candidate actions
|
|
527
|
+
const candidates = _generateCandidates(situation, confidence, obligations);
|
|
528
|
+
|
|
529
|
+
// ── Select best action through obligation-weighted judgment
|
|
530
|
+
const chosen = _selectAction(candidates, obligations, confidence, situation);
|
|
531
|
+
|
|
532
|
+
// ── Build rationale
|
|
533
|
+
const rationale = _buildRationale(chosen, situation, confidence, obligations, surfaceNoticings);
|
|
534
|
+
|
|
535
|
+
return {
|
|
536
|
+
depth,
|
|
537
|
+
action: chosen,
|
|
538
|
+
rationale,
|
|
539
|
+
confidence,
|
|
540
|
+
obligations: obligations.filter(o => o.priority === 'critical' || o.priority === 'high'),
|
|
541
|
+
surfaceNoticings,
|
|
542
|
+
shouldAskUser,
|
|
543
|
+
uncertainties: confidence.gaps,
|
|
544
|
+
};
|
|
285
545
|
}
|
|
286
546
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
}
|
|
297
|
-
return
|
|
547
|
+
function _reflexiveAction(situation) {
|
|
548
|
+
if (situation.isQuestion && situation.taskShape.type === 'answer') {
|
|
549
|
+
return { type: 'respond', mode: 'direct' };
|
|
550
|
+
}
|
|
551
|
+
if (situation.isShort && /^(yes|y|ok|go|do it|sure|approved)\s*$/i.test(situation.raw.trim())) {
|
|
552
|
+
return { type: 'proceed', mode: 'approved' };
|
|
553
|
+
}
|
|
554
|
+
if (situation.isShort && /^(no|stop|wait|hold)\s*$/i.test(situation.raw.trim())) {
|
|
555
|
+
return { type: 'pause', mode: 'correction' };
|
|
556
|
+
}
|
|
557
|
+
return { type: 'respond', mode: 'direct' };
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function _shouldAsk(situation, confidence, obligations, depth) {
|
|
561
|
+
// Obligation-driven: ask before irreversible
|
|
562
|
+
if (obligations.some(o => o.type === 'askBeforeIrreversi')) return true;
|
|
563
|
+
|
|
564
|
+
// Confidence-driven: ask when insufficient
|
|
565
|
+
if (confidence.blockers?.length > 0) return true;
|
|
566
|
+
|
|
567
|
+
// Relationship-driven: user signals suggest asking
|
|
568
|
+
if (situation.relationship.shouldAsk) return true;
|
|
569
|
+
|
|
570
|
+
// Depth-driven: deep deliberation means this is complex enough to check
|
|
571
|
+
if (depth === 'deep' && confidence.level !== 'sufficient') return true;
|
|
572
|
+
|
|
573
|
+
// Intent mismatch: what user asked might not be what they need
|
|
574
|
+
if (situation.relationship.likelyMismatch) return true;
|
|
575
|
+
|
|
576
|
+
return false;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function _generateCandidates(situation, confidence, obligations) {
|
|
580
|
+
const candidates = [];
|
|
581
|
+
|
|
582
|
+
// Direct response (answer/explain)
|
|
583
|
+
if (situation.taskShape.type === 'answer' || situation.taskShape.type === 'research') {
|
|
584
|
+
candidates.push({
|
|
585
|
+
type: 'respond',
|
|
586
|
+
mode: 'direct',
|
|
587
|
+
fitness: situation.isQuestion ? 0.9 : 0.6,
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// Dispatch to worker agent
|
|
592
|
+
if (['edit', 'debug', 'review'].includes(situation.taskShape.type)) {
|
|
593
|
+
candidates.push({
|
|
594
|
+
type: 'dispatch',
|
|
595
|
+
mode: situation.taskShape.type,
|
|
596
|
+
fitness: confidence.level === 'sufficient' ? 0.85 : 0.4,
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// Plan first, then dispatch
|
|
601
|
+
if (situation.taskShape.scope !== 'small' || situation.taskShape.ambiguity !== 'low') {
|
|
602
|
+
candidates.push({
|
|
603
|
+
type: 'plan',
|
|
604
|
+
mode: 'structured',
|
|
605
|
+
fitness: situation.taskShape.ambiguity === 'high' ? 0.9 : 0.6,
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// Clarify with user
|
|
610
|
+
if (confidence.level === 'insufficient' || situation.relationship.wrongAssumption) {
|
|
611
|
+
candidates.push({
|
|
612
|
+
type: 'clarify',
|
|
613
|
+
mode: 'question',
|
|
614
|
+
fitness: confidence.level === 'insufficient' ? 0.95 : 0.7,
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// Think/discuss (architecture, design)
|
|
619
|
+
if (situation.taskShape.type === 'plan') {
|
|
620
|
+
candidates.push({
|
|
621
|
+
type: 'think',
|
|
622
|
+
mode: 'architecture',
|
|
623
|
+
fitness: 0.85,
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// Proceed (user gave approval)
|
|
628
|
+
if (situation.isShort && /^(yes|y|ok|go|do it|sure|approved)\s*$/i.test(situation.raw.trim())) {
|
|
629
|
+
candidates.push({
|
|
630
|
+
type: 'proceed',
|
|
631
|
+
mode: 'approved',
|
|
632
|
+
fitness: 0.95,
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
return candidates;
|
|
298
637
|
}
|
|
299
638
|
|
|
300
|
-
|
|
639
|
+
function _selectAction(candidates, obligations, confidence, situation) {
|
|
640
|
+
if (candidates.length === 0) return { type: 'clarify', mode: 'no-candidates' };
|
|
641
|
+
|
|
642
|
+
// Apply obligation penalties
|
|
643
|
+
for (const c of candidates) {
|
|
644
|
+
// Dispatch penalty when confidence is low
|
|
645
|
+
if (c.type === 'dispatch' && confidence.level !== 'sufficient') {
|
|
646
|
+
c.fitness *= 0.5;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// Dispatch penalty when irreversible and no approval
|
|
650
|
+
if (c.type === 'dispatch' && obligations.some(o => o.type === 'askBeforeIrreversi')) {
|
|
651
|
+
c.fitness *= 0.3;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// Plan bonus when scope is large
|
|
655
|
+
if (c.type === 'plan' && situation.taskShape.scope === 'large') {
|
|
656
|
+
c.fitness *= 1.3;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// Clarify bonus when goal is mismatched
|
|
660
|
+
if (c.type === 'clarify' && situation.relationship.likelyMismatch) {
|
|
661
|
+
c.fitness *= 1.5;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// Sort by fitness, pick best
|
|
666
|
+
candidates.sort((a, b) => b.fitness - a.fitness);
|
|
667
|
+
return candidates[0];
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
function _buildRationale(action, situation, confidence, obligations, noticings) {
|
|
671
|
+
const parts = [];
|
|
672
|
+
|
|
673
|
+
parts.push(`Action: ${action.type} (${action.mode})`);
|
|
674
|
+
|
|
675
|
+
if (confidence.level !== 'sufficient') {
|
|
676
|
+
parts.push(`Confidence: ${confidence.level} (${confidence.score}) — ${confidence.gaps.length} gap(s)`);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
const criticalObligations = obligations.filter(o => o.priority === 'critical');
|
|
680
|
+
if (criticalObligations.length > 0) {
|
|
681
|
+
parts.push(`Critical obligations: ${criticalObligations.map(o => o.type).join(', ')}`);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
if (noticings.length > 0) {
|
|
685
|
+
parts.push(`Surfacing ${noticings.length} noticing(s)`);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
if (situation.inferredGoal) {
|
|
689
|
+
parts.push(`Note: inferred goal may differ — "${situation.inferredGoal}"`);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
return parts.join('. ');
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// ── Full turn processor ─────────────────────────────────────────────────────
|
|
301
696
|
|
|
302
697
|
/**
|
|
303
|
-
*
|
|
698
|
+
* Process a complete turn through the cognitive pipeline.
|
|
699
|
+
* This replaces the old processTurn — same interface, fundamentally different internals.
|
|
304
700
|
*/
|
|
305
|
-
export function
|
|
306
|
-
|
|
307
|
-
|
|
701
|
+
export function processTurn(state, userMessage, context = {}) {
|
|
702
|
+
// 1. Perceive the situation
|
|
703
|
+
const situation = perceive(userMessage, context);
|
|
704
|
+
|
|
705
|
+
// 2. Assess depth — how much thinking does this deserve?
|
|
706
|
+
const depth = assessDepth(situation);
|
|
707
|
+
|
|
708
|
+
// 3. Build uncertainty ledger
|
|
709
|
+
const uncertainties = assessUncertainty(situation, context);
|
|
710
|
+
|
|
711
|
+
// 4. Derive care obligations
|
|
712
|
+
const obligations = deriveObligations(situation);
|
|
713
|
+
|
|
714
|
+
// 5. Passive noticing
|
|
715
|
+
const noticings = notice(situation, state, context);
|
|
716
|
+
|
|
717
|
+
// 6. Deliberate
|
|
718
|
+
const result = deliberate(situation, uncertainties, obligations, noticings, state);
|
|
719
|
+
|
|
720
|
+
// Update state
|
|
721
|
+
state.lastActivity = Date.now();
|
|
722
|
+
if (!state.declaredGoal && situation.taskShape.type !== 'answer') {
|
|
723
|
+
state.declaredGoal = situation.explicitAsk.slice(0, 200);
|
|
724
|
+
state.originalScope = situation.material.touchedFiles.length || 1;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// Track the turn
|
|
728
|
+
if (!state.turns) state.turns = [];
|
|
729
|
+
state.turns.push({
|
|
308
730
|
timestamp: Date.now(),
|
|
731
|
+
depth: result.depth,
|
|
732
|
+
action: result.action.type,
|
|
733
|
+
confidence: result.confidence.score,
|
|
734
|
+
obligationCount: result.obligations.length,
|
|
735
|
+
noticingCount: result.surfaceNoticings.length,
|
|
309
736
|
});
|
|
310
|
-
|
|
737
|
+
|
|
738
|
+
saveState(state);
|
|
739
|
+
|
|
740
|
+
return {
|
|
741
|
+
situation,
|
|
742
|
+
depth,
|
|
743
|
+
uncertainties,
|
|
744
|
+
obligations,
|
|
745
|
+
noticings,
|
|
746
|
+
result,
|
|
747
|
+
|
|
748
|
+
// Convenience fields for callers
|
|
749
|
+
shouldAskUser: result.shouldAskUser,
|
|
750
|
+
shouldDispatch: result.action.type === 'dispatch' || result.action.type === 'proceed',
|
|
751
|
+
shouldClarify: result.action.type === 'clarify',
|
|
752
|
+
shouldThink: result.action.type === 'think' || result.action.type === 'plan',
|
|
753
|
+
action: result.action,
|
|
754
|
+
rationale: result.rationale,
|
|
755
|
+
};
|
|
311
756
|
}
|
|
312
757
|
|
|
313
|
-
// ──
|
|
758
|
+
// ── State persistence ───────────────────────────────────────────────────────
|
|
314
759
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
phase: state.phase,
|
|
338
|
-
suggestedPhase,
|
|
339
|
-
transitioned: transitionResult.allowed && transitionResult.from !== transitionResult.to,
|
|
340
|
-
confidenceCheck,
|
|
341
|
-
shouldDispatch: suggestedPhase === 'dispatch' && (!confidenceCheck || confidenceCheck.ready),
|
|
342
|
-
shouldClarify: intent.ambiguous || intent.intent === 'unknown',
|
|
343
|
-
shouldDiscuss: intent.intent === 'discussion' || (suggestedPhase === 'dispatch' && confidenceCheck && !confidenceCheck.ready),
|
|
760
|
+
export function loadState() {
|
|
761
|
+
try {
|
|
762
|
+
if (existsSync(STATE_FILE)) {
|
|
763
|
+
const data = JSON.parse(readFileSync(STATE_FILE, 'utf8'));
|
|
764
|
+
if (Date.now() - (data.lastActivity || 0) > 30 * 60 * 1000) {
|
|
765
|
+
return freshState();
|
|
766
|
+
}
|
|
767
|
+
return data;
|
|
768
|
+
}
|
|
769
|
+
} catch {}
|
|
770
|
+
return freshState();
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
export function freshState() {
|
|
774
|
+
return {
|
|
775
|
+
sessionId: Date.now().toString(36) + Math.random().toString(36).slice(2, 6),
|
|
776
|
+
declaredGoal: null,
|
|
777
|
+
originalScope: null,
|
|
778
|
+
turns: [],
|
|
779
|
+
contextEstimate: { messages: 0, estimatedTokens: 0 },
|
|
780
|
+
lastActivity: Date.now(),
|
|
781
|
+
created: Date.now(),
|
|
344
782
|
};
|
|
783
|
+
}
|
|
345
784
|
|
|
346
|
-
|
|
347
|
-
|
|
785
|
+
export function saveState(state) {
|
|
786
|
+
state.lastActivity = Date.now();
|
|
787
|
+
mkdirSync(STATE_DIR, { recursive: true });
|
|
788
|
+
writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
|
|
348
789
|
}
|
|
349
790
|
|
|
350
|
-
// ── Exports
|
|
351
|
-
//
|
|
352
|
-
//
|
|
353
|
-
//
|
|
791
|
+
// ── Exports ─────────────────────────────────────────────────────────────────
|
|
792
|
+
// Core pipeline: perceive, assessUncertainty, deriveObligations, notice, deliberate
|
|
793
|
+
// Convenience: processTurn, assessDepth, summarizeConfidence
|
|
794
|
+
// State: loadState, freshState, saveState
|
|
795
|
+
// Values: HEAD_VALUES
|