principles-disciple 1.7.6 → 1.7.8
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/dist/commands/context.js +5 -15
- package/dist/commands/evolution-status.js +2 -9
- package/dist/commands/export.js +61 -8
- package/dist/commands/nocturnal-review.d.ts +24 -0
- package/dist/commands/nocturnal-review.js +265 -0
- package/dist/commands/nocturnal-rollout.d.ts +27 -0
- package/dist/commands/nocturnal-rollout.js +671 -0
- package/dist/commands/nocturnal-train.d.ts +25 -0
- package/dist/commands/nocturnal-train.js +919 -0
- package/dist/commands/pain.js +8 -21
- package/dist/constants/tools.d.ts +2 -2
- package/dist/constants/tools.js +1 -1
- package/dist/core/adaptive-thresholds.d.ts +186 -0
- package/dist/core/adaptive-thresholds.js +300 -0
- package/dist/core/config.d.ts +2 -38
- package/dist/core/config.js +6 -61
- package/dist/core/event-log.d.ts +1 -2
- package/dist/core/event-log.js +0 -3
- package/dist/core/evolution-engine.js +1 -21
- package/dist/core/evolution-reducer.d.ts +7 -1
- package/dist/core/evolution-reducer.js +56 -4
- package/dist/core/evolution-types.d.ts +61 -9
- package/dist/core/evolution-types.js +31 -9
- package/dist/core/external-training-contract.d.ts +276 -0
- package/dist/core/external-training-contract.js +269 -0
- package/dist/core/local-worker-routing.d.ts +175 -0
- package/dist/core/local-worker-routing.js +525 -0
- package/dist/core/model-deployment-registry.d.ts +218 -0
- package/dist/core/model-deployment-registry.js +503 -0
- package/dist/core/model-training-registry.d.ts +295 -0
- package/dist/core/model-training-registry.js +475 -0
- package/dist/core/nocturnal-arbiter.d.ts +159 -0
- package/dist/core/nocturnal-arbiter.js +534 -0
- package/dist/core/nocturnal-candidate-scoring.d.ts +137 -0
- package/dist/core/nocturnal-candidate-scoring.js +266 -0
- package/dist/core/nocturnal-compliance.d.ts +175 -0
- package/dist/core/nocturnal-compliance.js +824 -0
- package/dist/core/nocturnal-dataset.d.ts +224 -0
- package/dist/core/nocturnal-dataset.js +443 -0
- package/dist/core/nocturnal-executability.d.ts +85 -0
- package/dist/core/nocturnal-executability.js +331 -0
- package/dist/core/nocturnal-export.d.ts +124 -0
- package/dist/core/nocturnal-export.js +275 -0
- package/dist/core/nocturnal-paths.d.ts +124 -0
- package/dist/core/nocturnal-paths.js +214 -0
- package/dist/core/nocturnal-trajectory-extractor.d.ts +242 -0
- package/dist/core/nocturnal-trajectory-extractor.js +307 -0
- package/dist/core/nocturnal-trinity.d.ts +311 -0
- package/dist/core/nocturnal-trinity.js +880 -0
- package/dist/core/paths.d.ts +6 -0
- package/dist/core/paths.js +6 -0
- package/dist/core/principle-training-state.d.ts +121 -0
- package/dist/core/principle-training-state.js +321 -0
- package/dist/core/promotion-gate.d.ts +238 -0
- package/dist/core/promotion-gate.js +529 -0
- package/dist/core/session-tracker.d.ts +10 -0
- package/dist/core/session-tracker.js +14 -0
- package/dist/core/shadow-observation-registry.d.ts +217 -0
- package/dist/core/shadow-observation-registry.js +308 -0
- package/dist/core/training-program.d.ts +233 -0
- package/dist/core/training-program.js +433 -0
- package/dist/core/trajectory.d.ts +95 -1
- package/dist/core/trajectory.js +220 -6
- package/dist/core/workspace-context.d.ts +0 -6
- package/dist/core/workspace-context.js +0 -12
- package/dist/hooks/bash-risk.d.ts +6 -6
- package/dist/hooks/bash-risk.js +8 -8
- package/dist/hooks/gate-block-helper.js +1 -1
- package/dist/hooks/gate.d.ts +1 -1
- package/dist/hooks/gate.js +2 -2
- package/dist/hooks/gfi-gate.d.ts +3 -3
- package/dist/hooks/gfi-gate.js +15 -14
- package/dist/hooks/pain.js +6 -9
- package/dist/hooks/progressive-trust-gate.d.ts +21 -49
- package/dist/hooks/progressive-trust-gate.js +51 -204
- package/dist/hooks/prompt.d.ts +11 -11
- package/dist/hooks/prompt.js +158 -72
- package/dist/hooks/subagent.js +43 -6
- package/dist/i18n/commands.js +8 -8
- package/dist/index.js +129 -28
- package/dist/service/evolution-worker.d.ts +42 -4
- package/dist/service/evolution-worker.js +321 -13
- package/dist/service/nocturnal-runtime.d.ts +183 -0
- package/dist/service/nocturnal-runtime.js +352 -0
- package/dist/service/nocturnal-service.d.ts +163 -0
- package/dist/service/nocturnal-service.js +787 -0
- package/dist/service/nocturnal-target-selector.d.ts +145 -0
- package/dist/service/nocturnal-target-selector.js +315 -0
- package/dist/service/phase3-input-filter.d.ts +2 -23
- package/dist/service/phase3-input-filter.js +3 -27
- package/dist/service/runtime-summary-service.d.ts +0 -10
- package/dist/service/runtime-summary-service.js +1 -54
- package/dist/tools/deep-reflect.js +2 -1
- package/dist/types/event-types.d.ts +2 -10
- package/dist/types/runtime-summary.d.ts +1 -8
- package/dist/types.d.ts +0 -3
- package/dist/types.js +0 -2
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/templates/langs/en/skills/pd-mentor/SKILL.md +5 -5
- package/templates/langs/zh/skills/pd-mentor/SKILL.md +5 -5
- package/templates/pain_settings.json +0 -6
- package/dist/commands/trust.d.ts +0 -4
- package/dist/commands/trust.js +0 -78
- package/dist/core/trust-engine.d.ts +0 -96
- package/dist/core/trust-engine.js +0 -286
|
@@ -0,0 +1,880 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nocturnal Trinity — Three-Stage Reflection Chain
|
|
3
|
+
* ================================================
|
|
4
|
+
*
|
|
5
|
+
* PURPOSE: Upgrade single-reflector nocturnal sample generation to a
|
|
6
|
+
* Dreamer -> Philosopher -> Scribe Trinity chain that produces higher quality
|
|
7
|
+
* decision-point samples through structured multi-stage reflection.
|
|
8
|
+
*
|
|
9
|
+
* TRINITY STAGES:
|
|
10
|
+
* 1. Dreamer — Generates multiple candidate corrections/alternatives
|
|
11
|
+
* 2. Philosopher — Provides principle-grounded critique and ranking
|
|
12
|
+
* 3. Scribe — Produces the final structured artifact draft using tournament selection
|
|
13
|
+
*
|
|
14
|
+
* DESIGN CONSTRAINTS:
|
|
15
|
+
* - All stage I/O is structured JSON contracts (not prose)
|
|
16
|
+
* - Any malformed stage output fails the entire chain closed
|
|
17
|
+
* - Single-reflector fallback is preserved via useTrinity flag
|
|
18
|
+
* - Trinity mode is configurable but defaults to enabled
|
|
19
|
+
* - Final artifact still passes arbiter + executability validation
|
|
20
|
+
* - Telemetry records chain mode, stage outcomes, candidate counts
|
|
21
|
+
* - Tournament selection is deterministic (same inputs → same winner)
|
|
22
|
+
*
|
|
23
|
+
* RUNTIME ADAPTER:
|
|
24
|
+
* - useStubs=true: uses synchronous stub implementations (no external calls)
|
|
25
|
+
* - useStubs=false: requires a TrinityRuntimeAdapter for real subagent execution
|
|
26
|
+
* - Adapter uses ONLY public plugin runtime APIs (api.runtime.subagent.*)
|
|
27
|
+
*/
|
|
28
|
+
import { randomUUID } from 'crypto';
|
|
29
|
+
import * as fs from 'fs';
|
|
30
|
+
import * as path from 'path';
|
|
31
|
+
import * as url from 'url';
|
|
32
|
+
import { computeThinkingModelDelta } from './nocturnal-trajectory-extractor.js';
|
|
33
|
+
import { runTournament, DEFAULT_SCORING_WEIGHTS, } from './nocturnal-candidate-scoring.js';
|
|
34
|
+
import { DEFAULT_THRESHOLDS, getEffectiveThresholds, } from './adaptive-thresholds.js';
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// Role Prompt Loading
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
|
|
39
|
+
const AGENTS_DIR = path.join(__dirname, '../agents');
|
|
40
|
+
function loadRolePrompt(filename) {
|
|
41
|
+
const filePath = path.join(AGENTS_DIR, filename);
|
|
42
|
+
try {
|
|
43
|
+
return fs.readFileSync(filePath, 'utf-8');
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
console.warn(`[Trinity] Could not load role prompt: ${filename}`);
|
|
47
|
+
return '';
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
// OpenClaw Runtime Adapter
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
/**
|
|
54
|
+
* OpenClaw-backed Trinity runtime adapter.
|
|
55
|
+
* Uses ONLY public plugin runtime APIs (api.runtime.subagent.*).
|
|
56
|
+
* Does NOT depend on OpenClaw internals.
|
|
57
|
+
*/
|
|
58
|
+
export class OpenClawTrinityRuntimeAdapter {
|
|
59
|
+
api;
|
|
60
|
+
stageTimeoutMs;
|
|
61
|
+
constructor(api, stageTimeoutMs = 180_000) {
|
|
62
|
+
this.api = api;
|
|
63
|
+
this.stageTimeoutMs = stageTimeoutMs;
|
|
64
|
+
}
|
|
65
|
+
async invokeDreamer(snapshot, principleId, maxCandidates) {
|
|
66
|
+
const sessionKey = `agent:main:subagent:ne-dreamer-${randomUUID()}`;
|
|
67
|
+
const systemPrompt = loadRolePrompt('nocturnal-dreamer.md');
|
|
68
|
+
const prompt = this.buildDreamerPrompt(snapshot, principleId, maxCandidates);
|
|
69
|
+
try {
|
|
70
|
+
const { runId } = await this.api.runtime.subagent.run({
|
|
71
|
+
sessionKey,
|
|
72
|
+
message: prompt,
|
|
73
|
+
extraSystemPrompt: systemPrompt,
|
|
74
|
+
deliver: false,
|
|
75
|
+
});
|
|
76
|
+
const result = await this.api.runtime.subagent.waitForRun({
|
|
77
|
+
runId,
|
|
78
|
+
timeoutMs: this.stageTimeoutMs,
|
|
79
|
+
});
|
|
80
|
+
if (result.status !== 'ok') {
|
|
81
|
+
return {
|
|
82
|
+
valid: false,
|
|
83
|
+
candidates: [],
|
|
84
|
+
reason: `Dreamer subagent failed: ${result.error ?? result.status}`,
|
|
85
|
+
generatedAt: new Date().toISOString(),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
const messages = await this.api.runtime.subagent.getSessionMessages({
|
|
89
|
+
sessionKey,
|
|
90
|
+
limit: 5,
|
|
91
|
+
});
|
|
92
|
+
const outputText = this.extractAssistantText(messages.messages);
|
|
93
|
+
return this.parseDreamerOutput(outputText);
|
|
94
|
+
}
|
|
95
|
+
finally {
|
|
96
|
+
await this.api.runtime.subagent.deleteSession({
|
|
97
|
+
sessionKey,
|
|
98
|
+
deleteTranscript: true,
|
|
99
|
+
}).catch(() => { });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
async invokePhilosopher(dreamerOutput, principleId) {
|
|
103
|
+
const sessionKey = `agent:main:subagent:ne-philosopher-${randomUUID()}`;
|
|
104
|
+
const systemPrompt = loadRolePrompt('nocturnal-philosopher.md');
|
|
105
|
+
const prompt = this.buildPhilosopherPrompt(dreamerOutput, principleId);
|
|
106
|
+
try {
|
|
107
|
+
const { runId } = await this.api.runtime.subagent.run({
|
|
108
|
+
sessionKey,
|
|
109
|
+
message: prompt,
|
|
110
|
+
extraSystemPrompt: systemPrompt,
|
|
111
|
+
deliver: false,
|
|
112
|
+
});
|
|
113
|
+
const result = await this.api.runtime.subagent.waitForRun({
|
|
114
|
+
runId,
|
|
115
|
+
timeoutMs: this.stageTimeoutMs,
|
|
116
|
+
});
|
|
117
|
+
if (result.status !== 'ok') {
|
|
118
|
+
return {
|
|
119
|
+
valid: false,
|
|
120
|
+
judgments: [],
|
|
121
|
+
overallAssessment: '',
|
|
122
|
+
reason: `Philosopher subagent failed: ${result.error ?? result.status}`,
|
|
123
|
+
generatedAt: new Date().toISOString(),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
const messages = await this.api.runtime.subagent.getSessionMessages({
|
|
127
|
+
sessionKey,
|
|
128
|
+
limit: 5,
|
|
129
|
+
});
|
|
130
|
+
const outputText = this.extractAssistantText(messages.messages);
|
|
131
|
+
return this.parsePhilosopherOutput(outputText);
|
|
132
|
+
}
|
|
133
|
+
finally {
|
|
134
|
+
await this.api.runtime.subagent.deleteSession({
|
|
135
|
+
sessionKey,
|
|
136
|
+
deleteTranscript: true,
|
|
137
|
+
}).catch(() => { });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
async invokeScribe(dreamerOutput, philosopherOutput, snapshot, principleId, telemetry, config) {
|
|
141
|
+
const sessionKey = `agent:main:subagent:ne-scribe-${randomUUID()}`;
|
|
142
|
+
const systemPrompt = loadRolePrompt('nocturnal-scribe.md');
|
|
143
|
+
const prompt = this.buildScribePrompt(dreamerOutput, philosopherOutput, snapshot, principleId);
|
|
144
|
+
try {
|
|
145
|
+
const { runId } = await this.api.runtime.subagent.run({
|
|
146
|
+
sessionKey,
|
|
147
|
+
message: prompt,
|
|
148
|
+
extraSystemPrompt: systemPrompt,
|
|
149
|
+
deliver: false,
|
|
150
|
+
});
|
|
151
|
+
const result = await this.api.runtime.subagent.waitForRun({
|
|
152
|
+
runId,
|
|
153
|
+
timeoutMs: this.stageTimeoutMs,
|
|
154
|
+
});
|
|
155
|
+
if (result.status !== 'ok') {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
const messages = await this.api.runtime.subagent.getSessionMessages({
|
|
159
|
+
sessionKey,
|
|
160
|
+
limit: 5,
|
|
161
|
+
});
|
|
162
|
+
const outputText = this.extractAssistantText(messages.messages);
|
|
163
|
+
return this.parseScribeOutput(outputText, snapshot, principleId, telemetry);
|
|
164
|
+
}
|
|
165
|
+
finally {
|
|
166
|
+
await this.api.runtime.subagent.deleteSession({
|
|
167
|
+
sessionKey,
|
|
168
|
+
deleteTranscript: true,
|
|
169
|
+
}).catch(() => { });
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
async close() {
|
|
173
|
+
// Nothing to clean up in this implementation
|
|
174
|
+
}
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
176
|
+
// Private Helper Methods
|
|
177
|
+
// ---------------------------------------------------------------------------
|
|
178
|
+
extractAssistantText(messages) {
|
|
179
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
180
|
+
const msg = messages[i];
|
|
181
|
+
if (msg.role === 'assistant') {
|
|
182
|
+
return msg.text ?? msg.content ?? '';
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return '';
|
|
186
|
+
}
|
|
187
|
+
buildDreamerPrompt(snapshot, principleId, maxCandidates) {
|
|
188
|
+
return `Target Principle: ${principleId}
|
|
189
|
+
|
|
190
|
+
Session Snapshot:
|
|
191
|
+
- Session ID: ${snapshot.sessionId}
|
|
192
|
+
- Assistant Turns: ${snapshot.stats.totalAssistantTurns}
|
|
193
|
+
- Tool Calls: ${snapshot.stats.totalToolCalls}
|
|
194
|
+
- Failures: ${snapshot.stats.failureCount}
|
|
195
|
+
- Pain Events: ${snapshot.stats.totalPainEvents}
|
|
196
|
+
- Gate Blocks: ${snapshot.stats.totalGateBlocks}
|
|
197
|
+
|
|
198
|
+
Please analyze this session and generate ${maxCandidates} candidate corrections. Each candidate should identify a bad decision and propose a better alternative grounded in the target principle.
|
|
199
|
+
|
|
200
|
+
Respond with ONLY a valid JSON object matching the DreamerOutput contract.`;
|
|
201
|
+
}
|
|
202
|
+
buildPhilosopherPrompt(dreamerOutput, principleId) {
|
|
203
|
+
const candidatesJson = JSON.stringify(dreamerOutput.candidates, null, 2);
|
|
204
|
+
return `Target Principle: ${principleId}
|
|
205
|
+
|
|
206
|
+
Dreamer's Candidates:
|
|
207
|
+
${candidatesJson}
|
|
208
|
+
|
|
209
|
+
Please evaluate each candidate and rank them by principle alignment, specificity, and actionability. Respond with ONLY a valid JSON object matching the PhilosopherOutput contract.`;
|
|
210
|
+
}
|
|
211
|
+
buildScribePrompt(dreamerOutput, philosopherOutput, snapshot, principleId) {
|
|
212
|
+
const candidatesJson = JSON.stringify(dreamerOutput.candidates, null, 2);
|
|
213
|
+
const judgmentsJson = JSON.stringify(philosopherOutput.judgments, null, 2);
|
|
214
|
+
return `Target Principle: ${principleId}
|
|
215
|
+
Session ID: ${snapshot.sessionId}
|
|
216
|
+
|
|
217
|
+
Dreamer's Candidates:
|
|
218
|
+
${candidatesJson}
|
|
219
|
+
|
|
220
|
+
Philosopher's Judgments:
|
|
221
|
+
${judgmentsJson}
|
|
222
|
+
|
|
223
|
+
Select the best candidate (Philosopher's rank 1) and synthesize it into a final TrinityDraftArtifact. Respond with ONLY a valid JSON object.`;
|
|
224
|
+
}
|
|
225
|
+
parseDreamerOutput(text) {
|
|
226
|
+
const json = this.extractJson(text);
|
|
227
|
+
if (!json) {
|
|
228
|
+
return {
|
|
229
|
+
valid: false,
|
|
230
|
+
candidates: [],
|
|
231
|
+
reason: 'Failed to parse Dreamer output as JSON',
|
|
232
|
+
generatedAt: new Date().toISOString(),
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
try {
|
|
236
|
+
const parsed = JSON.parse(json);
|
|
237
|
+
// Validate required structure
|
|
238
|
+
if (typeof parsed.valid !== 'boolean') {
|
|
239
|
+
return {
|
|
240
|
+
valid: false,
|
|
241
|
+
candidates: [],
|
|
242
|
+
reason: 'Dreamer output missing "valid" field',
|
|
243
|
+
generatedAt: new Date().toISOString(),
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
if (!Array.isArray(parsed.candidates)) {
|
|
247
|
+
return {
|
|
248
|
+
valid: false,
|
|
249
|
+
candidates: [],
|
|
250
|
+
reason: 'Dreamer output missing "candidates" array',
|
|
251
|
+
generatedAt: new Date().toISOString(),
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
return {
|
|
255
|
+
valid: parsed.valid,
|
|
256
|
+
candidates: parsed.candidates,
|
|
257
|
+
reason: parsed.reason,
|
|
258
|
+
generatedAt: parsed.generatedAt ?? new Date().toISOString(),
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
return {
|
|
263
|
+
valid: false,
|
|
264
|
+
candidates: [],
|
|
265
|
+
reason: `JSON parse error: ${text.slice(0, 100)}`,
|
|
266
|
+
generatedAt: new Date().toISOString(),
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
parsePhilosopherOutput(text) {
|
|
271
|
+
const json = this.extractJson(text);
|
|
272
|
+
if (!json) {
|
|
273
|
+
return {
|
|
274
|
+
valid: false,
|
|
275
|
+
judgments: [],
|
|
276
|
+
overallAssessment: '',
|
|
277
|
+
reason: 'Failed to parse Philosopher output as JSON',
|
|
278
|
+
generatedAt: new Date().toISOString(),
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
try {
|
|
282
|
+
const parsed = JSON.parse(json);
|
|
283
|
+
if (typeof parsed.valid !== 'boolean') {
|
|
284
|
+
return {
|
|
285
|
+
valid: false,
|
|
286
|
+
judgments: [],
|
|
287
|
+
overallAssessment: '',
|
|
288
|
+
reason: 'Philosopher output missing "valid" field',
|
|
289
|
+
generatedAt: new Date().toISOString(),
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
if (!Array.isArray(parsed.judgments)) {
|
|
293
|
+
return {
|
|
294
|
+
valid: false,
|
|
295
|
+
judgments: [],
|
|
296
|
+
overallAssessment: '',
|
|
297
|
+
reason: 'Philosopher output missing "judgments" array',
|
|
298
|
+
generatedAt: new Date().toISOString(),
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
return {
|
|
302
|
+
valid: parsed.valid,
|
|
303
|
+
judgments: parsed.judgments,
|
|
304
|
+
overallAssessment: parsed.overallAssessment ?? '',
|
|
305
|
+
reason: parsed.reason,
|
|
306
|
+
generatedAt: parsed.generatedAt ?? new Date().toISOString(),
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
catch {
|
|
310
|
+
return {
|
|
311
|
+
valid: false,
|
|
312
|
+
judgments: [],
|
|
313
|
+
overallAssessment: '',
|
|
314
|
+
reason: `JSON parse error: ${text.slice(0, 100)}`,
|
|
315
|
+
generatedAt: new Date().toISOString(),
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
parseScribeOutput(text, snapshot, principleId, telemetry) {
|
|
320
|
+
const json = this.extractJson(text);
|
|
321
|
+
if (!json) {
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
try {
|
|
325
|
+
const parsed = JSON.parse(json);
|
|
326
|
+
if (typeof parsed.selectedCandidateIndex !== 'number') {
|
|
327
|
+
return null;
|
|
328
|
+
}
|
|
329
|
+
return {
|
|
330
|
+
selectedCandidateIndex: parsed.selectedCandidateIndex,
|
|
331
|
+
badDecision: parsed.badDecision ?? '',
|
|
332
|
+
betterDecision: parsed.betterDecision ?? '',
|
|
333
|
+
rationale: parsed.rationale ?? '',
|
|
334
|
+
sessionId: snapshot.sessionId,
|
|
335
|
+
principleId,
|
|
336
|
+
sourceSnapshotRef: `snapshot-${snapshot.sessionId}-${Date.now()}`,
|
|
337
|
+
telemetry: {
|
|
338
|
+
chainMode: 'trinity',
|
|
339
|
+
usedStubs: false,
|
|
340
|
+
dreamerPassed: true,
|
|
341
|
+
philosopherPassed: true,
|
|
342
|
+
scribePassed: true,
|
|
343
|
+
candidateCount: parsed.candidateCount ?? 0,
|
|
344
|
+
selectedCandidateIndex: parsed.selectedCandidateIndex,
|
|
345
|
+
stageFailures: [],
|
|
346
|
+
},
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
catch {
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Extract JSON object from text that may contain markdown code blocks.
|
|
355
|
+
*/
|
|
356
|
+
extractJson(text) {
|
|
357
|
+
// Try direct parse first
|
|
358
|
+
try {
|
|
359
|
+
JSON.parse(text);
|
|
360
|
+
return text;
|
|
361
|
+
}
|
|
362
|
+
catch {
|
|
363
|
+
// Try extracting from markdown code blocks
|
|
364
|
+
}
|
|
365
|
+
// Match triple-backtick JSON blocks
|
|
366
|
+
const codeBlockMatch = text.match(/```(?:json)?\s*\n?([\s\S]*?)\n?```/);
|
|
367
|
+
if (codeBlockMatch) {
|
|
368
|
+
const extracted = codeBlockMatch[1].trim();
|
|
369
|
+
try {
|
|
370
|
+
JSON.parse(extracted);
|
|
371
|
+
return extracted;
|
|
372
|
+
}
|
|
373
|
+
catch {
|
|
374
|
+
// Not valid JSON
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
// Try to find first { and last } to extract JSON object
|
|
378
|
+
const firstBrace = text.indexOf('{');
|
|
379
|
+
const lastBrace = text.lastIndexOf('}');
|
|
380
|
+
if (firstBrace !== -1 && lastBrace !== -1 && lastBrace > firstBrace) {
|
|
381
|
+
const extracted = text.slice(firstBrace, lastBrace + 1);
|
|
382
|
+
try {
|
|
383
|
+
JSON.parse(extracted);
|
|
384
|
+
return extracted;
|
|
385
|
+
}
|
|
386
|
+
catch {
|
|
387
|
+
// Not valid JSON
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return null;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
// ---------------------------------------------------------------------------
|
|
394
|
+
// Stub Stage Implementations (Phase 2 — no real subagent calls)
|
|
395
|
+
// ---------------------------------------------------------------------------
|
|
396
|
+
/**
|
|
397
|
+
* STUB DREAMER — generates synthetic candidates for testing.
|
|
398
|
+
*
|
|
399
|
+
* In production, this would call the actual Dreamer subagent.
|
|
400
|
+
* The stub generates plausible candidates based on snapshot signals.
|
|
401
|
+
*/
|
|
402
|
+
function invokeStubDreamer(snapshot, principleId, maxCandidates) {
|
|
403
|
+
const hasFailures = snapshot.stats.failureCount > 0;
|
|
404
|
+
const hasPain = snapshot.stats.totalPainEvents > 0;
|
|
405
|
+
const hasGateBlocks = snapshot.stats.totalGateBlocks > 0;
|
|
406
|
+
const candidates = [];
|
|
407
|
+
// Generate candidates based on available signals
|
|
408
|
+
// NOTE: betterDecision includes thinking model patterns so computeThinkingModelDelta > 0
|
|
409
|
+
// (these activate T-03, T-05, T-08 patterns respectively)
|
|
410
|
+
if (hasGateBlocks) {
|
|
411
|
+
candidates.push({
|
|
412
|
+
candidateIndex: 0,
|
|
413
|
+
badDecision: 'Proceeded with a tool call despite receiving a gate block, bypassing the safety check',
|
|
414
|
+
betterDecision: 'Review docs/gateblocks.md and verify authorization requirements first; based on the evidence, this irreversible action must be reviewed before proceeding',
|
|
415
|
+
rationale: 'Respecting gate blocks prevents unintended system modifications',
|
|
416
|
+
confidence: 0.95,
|
|
417
|
+
});
|
|
418
|
+
if (maxCandidates >= 2) {
|
|
419
|
+
candidates.push({
|
|
420
|
+
candidateIndex: 1,
|
|
421
|
+
badDecision: 'Retried the same operation immediately after gate block without understanding why',
|
|
422
|
+
betterDecision: 'Check the gatekeeper source first to diagnose the block reason; this is irreversible, so we must be certain before proceeding',
|
|
423
|
+
rationale: 'Understanding why a gate blocked prevents repeated blocks',
|
|
424
|
+
confidence: 0.85,
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
if (maxCandidates >= 3) {
|
|
428
|
+
candidates.push({
|
|
429
|
+
candidateIndex: 2,
|
|
430
|
+
badDecision: 'Modified the target of the blocked operation to bypass the check',
|
|
431
|
+
betterDecision: 'Review docs/auth.md first to understand the authorization structure, then request proper review before any change',
|
|
432
|
+
rationale: 'Proper authorization ensures accountability and prevents unintended changes',
|
|
433
|
+
confidence: 0.75,
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
else if (hasPain) {
|
|
438
|
+
candidates.push({
|
|
439
|
+
candidateIndex: 0,
|
|
440
|
+
badDecision: 'Continued executing operations without pausing to address accumulated pain signals',
|
|
441
|
+
betterDecision: 'Check logs/pain.json first to analyze pain signals; this error indicates we should stop and reconsider before proceeding',
|
|
442
|
+
rationale: 'Pain signals indicate accumulated friction or error conditions',
|
|
443
|
+
confidence: 0.90,
|
|
444
|
+
});
|
|
445
|
+
if (maxCandidates >= 2) {
|
|
446
|
+
candidates.push({
|
|
447
|
+
candidateIndex: 1,
|
|
448
|
+
badDecision: 'Ignored warning pain events and proceeded with high-risk operations',
|
|
449
|
+
betterDecision: 'Review src/pain-detector.ts first; based on the evidence, this indicates a deeper issue we must not ignore',
|
|
450
|
+
rationale: 'Addressing friction reduces error rates and improves outcomes',
|
|
451
|
+
confidence: 0.80,
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
if (maxCandidates >= 3) {
|
|
455
|
+
candidates.push({
|
|
456
|
+
candidateIndex: 2,
|
|
457
|
+
badDecision: 'Retried failing operations without analyzing why they caused pain',
|
|
458
|
+
betterDecision: 'Analyze logs/errors.json first to identify the failure pattern; this suggests we should stop and rethink before retrying',
|
|
459
|
+
rationale: 'Pattern analysis prevents recurring pain from the same source',
|
|
460
|
+
confidence: 0.70,
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
else if (hasFailures) {
|
|
465
|
+
candidates.push({
|
|
466
|
+
candidateIndex: 0,
|
|
467
|
+
badDecision: 'Retried a failing operation without diagnosing the root cause',
|
|
468
|
+
betterDecision: 'Verify config.json preconditions first, based on the error in logs/failure.json, before retrying',
|
|
469
|
+
rationale: 'Diagnosing failures before retry prevents repeated failures',
|
|
470
|
+
confidence: 0.92,
|
|
471
|
+
});
|
|
472
|
+
if (maxCandidates >= 2) {
|
|
473
|
+
candidates.push({
|
|
474
|
+
candidateIndex: 1,
|
|
475
|
+
badDecision: 'Continued to the next operation after a failure without addressing it',
|
|
476
|
+
betterDecision: 'Check docs/debugging.md first to diagnose what failed; we must not ignore this when the action is irreversible',
|
|
477
|
+
rationale: 'Unaddressed failures compound and cause larger issues',
|
|
478
|
+
confidence: 0.85,
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
if (maxCandidates >= 3) {
|
|
482
|
+
candidates.push({
|
|
483
|
+
candidateIndex: 2,
|
|
484
|
+
badDecision: 'Assumed the failure was transient and retried without investigation',
|
|
485
|
+
betterDecision: 'Verify src/validator.ts state first; this error indicates a deeper problem before assuming resolution',
|
|
486
|
+
rationale: 'Verification prevents cascading failures from unresolved issues',
|
|
487
|
+
confidence: 0.78,
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
else {
|
|
492
|
+
// No signal available - cannot generate meaningful candidates
|
|
493
|
+
// Return empty candidates array to trigger invalid output
|
|
494
|
+
// (Real Dreamer would also fail with no signal)
|
|
495
|
+
return {
|
|
496
|
+
valid: false,
|
|
497
|
+
candidates: [],
|
|
498
|
+
reason: 'No signal available for candidate generation (failureCount=0, painEvents=0, gateBlocks=0)',
|
|
499
|
+
generatedAt: new Date().toISOString(),
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
// Ensure we don't exceed maxCandidates
|
|
503
|
+
const limitedCandidates = candidates.slice(0, Math.min(candidates.length, maxCandidates));
|
|
504
|
+
return {
|
|
505
|
+
valid: limitedCandidates.length > 0,
|
|
506
|
+
candidates: limitedCandidates,
|
|
507
|
+
generatedAt: new Date().toISOString(),
|
|
508
|
+
reason: limitedCandidates.length === 0 ? 'No signal available for candidate generation' : undefined,
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* STUB PHILOSOPHER — ranks candidates based on simple heuristics.
|
|
513
|
+
*
|
|
514
|
+
* In production, this would call the actual Philosopher subagent.
|
|
515
|
+
* The stub applies principle alignment heuristics.
|
|
516
|
+
*/
|
|
517
|
+
function invokeStubPhilosopher(dreamerOutput, principleId) {
|
|
518
|
+
if (!dreamerOutput.valid || dreamerOutput.candidates.length === 0) {
|
|
519
|
+
return {
|
|
520
|
+
valid: false,
|
|
521
|
+
judgments: [],
|
|
522
|
+
overallAssessment: '',
|
|
523
|
+
reason: 'No candidates to judge',
|
|
524
|
+
generatedAt: new Date().toISOString(),
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
// Simple heuristic scoring based on candidate structure
|
|
528
|
+
const judgments = dreamerOutput.candidates.map((candidate) => {
|
|
529
|
+
let principleAligned = true;
|
|
530
|
+
let score = candidate.confidence;
|
|
531
|
+
// Heuristic: longer rationales tend to be more principled
|
|
532
|
+
if (candidate.rationale.length < 30) {
|
|
533
|
+
score *= 0.8;
|
|
534
|
+
principleAligned = false;
|
|
535
|
+
}
|
|
536
|
+
// Heuristic: betterDecision should be actionable (contain verbs)
|
|
537
|
+
const actionableVerbs = ['read', 'check', 'verify', 'edit', 'write', 'search', 'review', 'analyze'];
|
|
538
|
+
const hasActionable = actionableVerbs.some((v) => candidate.betterDecision.toLowerCase().includes(v));
|
|
539
|
+
if (!hasActionable) {
|
|
540
|
+
score *= 0.85;
|
|
541
|
+
principleAligned = false;
|
|
542
|
+
}
|
|
543
|
+
// Heuristic: badDecision should be specific (not generic)
|
|
544
|
+
const genericPatterns = ['something went wrong', 'it did not work', 'mistake was made'];
|
|
545
|
+
const isGeneric = genericPatterns.some((p) => candidate.badDecision.toLowerCase().includes(p));
|
|
546
|
+
if (isGeneric) {
|
|
547
|
+
score *= 0.75;
|
|
548
|
+
principleAligned = false;
|
|
549
|
+
}
|
|
550
|
+
return {
|
|
551
|
+
candidateIndex: candidate.candidateIndex,
|
|
552
|
+
critique: `Candidate ${candidate.candidateIndex} scored ${score.toFixed(2)}. ${principleAligned
|
|
553
|
+
? 'Principle-aligned with specific actionable alternative.'
|
|
554
|
+
: 'May need refinement for principle alignment.'}`,
|
|
555
|
+
principleAligned,
|
|
556
|
+
score: Math.min(1, Math.max(0, score)),
|
|
557
|
+
rank: 0, // Will be set after sorting
|
|
558
|
+
};
|
|
559
|
+
});
|
|
560
|
+
// Sort by score descending and assign ranks
|
|
561
|
+
judgments.sort((a, b) => b.score - a.score);
|
|
562
|
+
judgments.forEach((j, idx) => {
|
|
563
|
+
j.rank = idx + 1;
|
|
564
|
+
});
|
|
565
|
+
const topJudgment = judgments[0];
|
|
566
|
+
return {
|
|
567
|
+
valid: true,
|
|
568
|
+
judgments,
|
|
569
|
+
overallAssessment: `Best candidate is #${topJudgment.candidateIndex} with score ${topJudgment.score.toFixed(2)}. ${topJudgment.principleAligned ? 'Well-aligned with principle.' : 'Alignment could be improved.'}`,
|
|
570
|
+
generatedAt: new Date().toISOString(),
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* STUB SCRIBE — synthesizes best candidate into final artifact using tournament selection.
|
|
575
|
+
*
|
|
576
|
+
* In production, this would call the actual Scribe subagent.
|
|
577
|
+
* The stub uses tournament selection (scoring + thresholds) to pick the winner.
|
|
578
|
+
*/
|
|
579
|
+
function invokeStubScribe(dreamerOutput, philosopherOutput, snapshot, principleId, telemetry, config) {
|
|
580
|
+
if (!dreamerOutput.valid || !philosopherOutput.valid) {
|
|
581
|
+
return null;
|
|
582
|
+
}
|
|
583
|
+
// Get thresholds (from config or state, or defaults)
|
|
584
|
+
const thresholds = config.thresholds ?? (config.stateDir ? getEffectiveThresholds(config.stateDir) : { ...DEFAULT_THRESHOLDS });
|
|
585
|
+
const weights = config.scoringWeights ?? DEFAULT_SCORING_WEIGHTS;
|
|
586
|
+
// Run tournament selection
|
|
587
|
+
const tournamentResult = runTournament(dreamerOutput.candidates, philosopherOutput.judgments, thresholds, weights);
|
|
588
|
+
if (!tournamentResult.success || !tournamentResult.winner) {
|
|
589
|
+
// Tournament failed — no eligible candidate
|
|
590
|
+
return null;
|
|
591
|
+
}
|
|
592
|
+
const winner = tournamentResult.winner;
|
|
593
|
+
// Update telemetry with tournament info
|
|
594
|
+
const updatedTelemetry = {
|
|
595
|
+
...telemetry,
|
|
596
|
+
tournamentTrace: tournamentResult.trace,
|
|
597
|
+
winnerAggregateScore: winner.scores.aggregate,
|
|
598
|
+
winnerThresholdPassed: winner.thresholdPassed,
|
|
599
|
+
eligibleCandidateCount: tournamentResult.rankedCandidates.filter((c) => c.thresholdPassed).length,
|
|
600
|
+
};
|
|
601
|
+
return {
|
|
602
|
+
selectedCandidateIndex: winner.candidateIndex,
|
|
603
|
+
badDecision: winner.candidate.badDecision,
|
|
604
|
+
betterDecision: winner.candidate.betterDecision,
|
|
605
|
+
rationale: winner.candidate.rationale,
|
|
606
|
+
sessionId: snapshot.sessionId,
|
|
607
|
+
principleId,
|
|
608
|
+
sourceSnapshotRef: `snapshot-${snapshot.sessionId}-${Date.now()}`,
|
|
609
|
+
telemetry: updatedTelemetry,
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* Execute the Trinity chain using stubs (synchronous).
|
|
614
|
+
* Use runTrinityAsync for real subagent execution via runtime adapter.
|
|
615
|
+
*
|
|
616
|
+
* @param options - Trinity execution options
|
|
617
|
+
* @returns TrinityResult with final artifact or failure info
|
|
618
|
+
*/
|
|
619
|
+
export function runTrinity(options) {
|
|
620
|
+
const { snapshot, principleId, config } = options;
|
|
621
|
+
// Stub path: use synchronous stub implementations
|
|
622
|
+
if (config.useStubs) {
|
|
623
|
+
return runTrinityWithStubs(snapshot, principleId, config);
|
|
624
|
+
}
|
|
625
|
+
// Real execution path: requires runtimeAdapter
|
|
626
|
+
// This is handled asynchronously in runTrinityAsync
|
|
627
|
+
const errorMsg = '[Trinity] useStubs=false requires a runtimeAdapter. Use runTrinityAsync for real subagent execution.';
|
|
628
|
+
const failures = [{ stage: 'dreamer', reason: errorMsg }];
|
|
629
|
+
const telemetry = {
|
|
630
|
+
chainMode: 'trinity',
|
|
631
|
+
usedStubs: false,
|
|
632
|
+
dreamerPassed: false,
|
|
633
|
+
philosopherPassed: false,
|
|
634
|
+
scribePassed: false,
|
|
635
|
+
candidateCount: 0,
|
|
636
|
+
selectedCandidateIndex: -1,
|
|
637
|
+
stageFailures: [`Configuration: ${errorMsg}`],
|
|
638
|
+
};
|
|
639
|
+
console.error(`[Trinity] ERROR: ${errorMsg}`);
|
|
640
|
+
return {
|
|
641
|
+
success: false,
|
|
642
|
+
telemetry,
|
|
643
|
+
failures,
|
|
644
|
+
fallbackOccurred: false,
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
/**
|
|
648
|
+
* Execute the Trinity chain with real subagent runtime (asynchronous).
|
|
649
|
+
* Requires config.runtimeAdapter to be set.
|
|
650
|
+
*
|
|
651
|
+
* @param options - Trinity execution options
|
|
652
|
+
* @returns Promise<TrinityResult> with final artifact or failure info
|
|
653
|
+
*/
|
|
654
|
+
export async function runTrinityAsync(options) {
|
|
655
|
+
const { snapshot, principleId, config } = options;
|
|
656
|
+
if (config.useStubs) {
|
|
657
|
+
// Stub path: use synchronous stubs
|
|
658
|
+
return runTrinityWithStubs(snapshot, principleId, config);
|
|
659
|
+
}
|
|
660
|
+
if (!config.runtimeAdapter) {
|
|
661
|
+
const errorMsg = '[Trinity] useStubs=false requires config.runtimeAdapter to be set.';
|
|
662
|
+
const failures = [{ stage: 'dreamer', reason: errorMsg }];
|
|
663
|
+
const telemetry = {
|
|
664
|
+
chainMode: 'trinity',
|
|
665
|
+
usedStubs: false,
|
|
666
|
+
dreamerPassed: false,
|
|
667
|
+
philosopherPassed: false,
|
|
668
|
+
scribePassed: false,
|
|
669
|
+
candidateCount: 0,
|
|
670
|
+
selectedCandidateIndex: -1,
|
|
671
|
+
stageFailures: [`Configuration: ${errorMsg}`],
|
|
672
|
+
};
|
|
673
|
+
console.error(`[Trinity] ERROR: ${errorMsg}`);
|
|
674
|
+
return {
|
|
675
|
+
success: false,
|
|
676
|
+
telemetry,
|
|
677
|
+
failures,
|
|
678
|
+
fallbackOccurred: false,
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
const adapter = config.runtimeAdapter;
|
|
682
|
+
const telemetry = {
|
|
683
|
+
chainMode: 'trinity',
|
|
684
|
+
usedStubs: false,
|
|
685
|
+
dreamerPassed: false,
|
|
686
|
+
philosopherPassed: false,
|
|
687
|
+
scribePassed: false,
|
|
688
|
+
candidateCount: 0,
|
|
689
|
+
selectedCandidateIndex: -1,
|
|
690
|
+
stageFailures: [],
|
|
691
|
+
};
|
|
692
|
+
const failures = [];
|
|
693
|
+
try {
|
|
694
|
+
// Step 1: Dreamer — generate candidates via real subagent
|
|
695
|
+
const dreamerOutput = await adapter.invokeDreamer(snapshot, principleId, config.maxCandidates);
|
|
696
|
+
if (!dreamerOutput.valid || dreamerOutput.candidates.length === 0) {
|
|
697
|
+
failures.push({
|
|
698
|
+
stage: 'dreamer',
|
|
699
|
+
reason: dreamerOutput.reason ?? 'No valid candidates generated',
|
|
700
|
+
});
|
|
701
|
+
telemetry.stageFailures.push(`Dreamer: ${dreamerOutput.reason ?? 'failed'}`);
|
|
702
|
+
return { success: false, telemetry, failures, fallbackOccurred: false };
|
|
703
|
+
}
|
|
704
|
+
telemetry.dreamerPassed = true;
|
|
705
|
+
telemetry.candidateCount = dreamerOutput.candidates.length;
|
|
706
|
+
// Step 2: Philosopher — rank candidates via real subagent
|
|
707
|
+
const philosopherOutput = await adapter.invokePhilosopher(dreamerOutput, principleId);
|
|
708
|
+
if (!philosopherOutput.valid || philosopherOutput.judgments.length === 0) {
|
|
709
|
+
failures.push({
|
|
710
|
+
stage: 'philosopher',
|
|
711
|
+
reason: philosopherOutput.reason ?? 'No judgments produced',
|
|
712
|
+
});
|
|
713
|
+
telemetry.stageFailures.push(`Philosopher: ${philosopherOutput.reason ?? 'failed'}`);
|
|
714
|
+
return { success: false, telemetry, failures, fallbackOccurred: false };
|
|
715
|
+
}
|
|
716
|
+
telemetry.philosopherPassed = true;
|
|
717
|
+
// Step 3: Scribe — synthesize final artifact via real subagent
|
|
718
|
+
const draftArtifact = await adapter.invokeScribe(dreamerOutput, philosopherOutput, snapshot, principleId, telemetry, config);
|
|
719
|
+
if (!draftArtifact) {
|
|
720
|
+
failures.push({ stage: 'scribe', reason: 'Failed to synthesize artifact from candidates' });
|
|
721
|
+
telemetry.stageFailures.push('Scribe: synthesis failed');
|
|
722
|
+
return { success: false, telemetry, failures, fallbackOccurred: false };
|
|
723
|
+
}
|
|
724
|
+
telemetry.scribePassed = true;
|
|
725
|
+
telemetry.selectedCandidateIndex = draftArtifact.selectedCandidateIndex;
|
|
726
|
+
if (draftArtifact.telemetry) {
|
|
727
|
+
telemetry.tournamentTrace = draftArtifact.telemetry.tournamentTrace;
|
|
728
|
+
telemetry.winnerAggregateScore = draftArtifact.telemetry.winnerAggregateScore;
|
|
729
|
+
telemetry.winnerThresholdPassed = draftArtifact.telemetry.winnerThresholdPassed;
|
|
730
|
+
telemetry.eligibleCandidateCount = draftArtifact.telemetry.eligibleCandidateCount;
|
|
731
|
+
}
|
|
732
|
+
return { success: true, artifact: draftArtifact, telemetry, failures: [], fallbackOccurred: false };
|
|
733
|
+
}
|
|
734
|
+
finally {
|
|
735
|
+
if (adapter.close) {
|
|
736
|
+
await adapter.close().catch(() => { });
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
741
|
+
* Internal: Run Trinity chain with stub implementations (synchronous).
|
|
742
|
+
*/
|
|
743
|
+
function runTrinityWithStubs(snapshot, principleId, config) {
|
|
744
|
+
const telemetry = {
|
|
745
|
+
chainMode: 'trinity',
|
|
746
|
+
usedStubs: true,
|
|
747
|
+
dreamerPassed: false,
|
|
748
|
+
philosopherPassed: false,
|
|
749
|
+
scribePassed: false,
|
|
750
|
+
candidateCount: 0,
|
|
751
|
+
selectedCandidateIndex: -1,
|
|
752
|
+
stageFailures: [],
|
|
753
|
+
};
|
|
754
|
+
const failures = [];
|
|
755
|
+
// Step 1: Dreamer — generate candidates (stub)
|
|
756
|
+
const dreamerOutput = invokeStubDreamer(snapshot, principleId, config.maxCandidates);
|
|
757
|
+
if (!dreamerOutput.valid || dreamerOutput.candidates.length === 0) {
|
|
758
|
+
failures.push({
|
|
759
|
+
stage: 'dreamer',
|
|
760
|
+
reason: dreamerOutput.reason ?? 'No valid candidates generated',
|
|
761
|
+
});
|
|
762
|
+
telemetry.stageFailures.push(`Dreamer: ${dreamerOutput.reason ?? 'failed'}`);
|
|
763
|
+
return {
|
|
764
|
+
success: false,
|
|
765
|
+
telemetry,
|
|
766
|
+
failures,
|
|
767
|
+
fallbackOccurred: false,
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
telemetry.dreamerPassed = true;
|
|
771
|
+
telemetry.candidateCount = dreamerOutput.candidates.length;
|
|
772
|
+
// Step 2: Philosopher — rank candidates (stub)
|
|
773
|
+
const philosopherOutput = invokeStubPhilosopher(dreamerOutput, principleId);
|
|
774
|
+
if (!philosopherOutput.valid || philosopherOutput.judgments.length === 0) {
|
|
775
|
+
failures.push({
|
|
776
|
+
stage: 'philosopher',
|
|
777
|
+
reason: philosopherOutput.reason ?? 'No judgments produced',
|
|
778
|
+
});
|
|
779
|
+
telemetry.stageFailures.push(`Philosopher: ${philosopherOutput.reason ?? 'failed'}`);
|
|
780
|
+
return {
|
|
781
|
+
success: false,
|
|
782
|
+
telemetry,
|
|
783
|
+
failures,
|
|
784
|
+
fallbackOccurred: false,
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
telemetry.philosopherPassed = true;
|
|
788
|
+
// Step 3: Scribe — produce final artifact using tournament selection (stub)
|
|
789
|
+
const draftArtifact = invokeStubScribe(dreamerOutput, philosopherOutput, snapshot, principleId, telemetry, config);
|
|
790
|
+
if (!draftArtifact) {
|
|
791
|
+
failures.push({
|
|
792
|
+
stage: 'scribe',
|
|
793
|
+
reason: 'Failed to synthesize artifact from candidates',
|
|
794
|
+
});
|
|
795
|
+
telemetry.stageFailures.push('Scribe: synthesis failed');
|
|
796
|
+
return {
|
|
797
|
+
success: false,
|
|
798
|
+
telemetry,
|
|
799
|
+
failures,
|
|
800
|
+
fallbackOccurred: false,
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
telemetry.scribePassed = true;
|
|
804
|
+
telemetry.selectedCandidateIndex = draftArtifact.selectedCandidateIndex;
|
|
805
|
+
if (draftArtifact.telemetry) {
|
|
806
|
+
telemetry.tournamentTrace = draftArtifact.telemetry.tournamentTrace;
|
|
807
|
+
telemetry.winnerAggregateScore = draftArtifact.telemetry.winnerAggregateScore;
|
|
808
|
+
telemetry.winnerThresholdPassed = draftArtifact.telemetry.winnerThresholdPassed;
|
|
809
|
+
telemetry.eligibleCandidateCount = draftArtifact.telemetry.eligibleCandidateCount;
|
|
810
|
+
}
|
|
811
|
+
return {
|
|
812
|
+
success: true,
|
|
813
|
+
artifact: draftArtifact,
|
|
814
|
+
telemetry,
|
|
815
|
+
failures: [],
|
|
816
|
+
fallbackOccurred: false,
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
/**
|
|
820
|
+
* Validate a TrinityDraftArtifact before passing to arbiter.
|
|
821
|
+
*/
|
|
822
|
+
export function validateDraftArtifact(draft) {
|
|
823
|
+
const failures = [];
|
|
824
|
+
if (!draft.badDecision || draft.badDecision.trim().length === 0) {
|
|
825
|
+
failures.push('badDecision is required and non-empty');
|
|
826
|
+
}
|
|
827
|
+
if (!draft.betterDecision || draft.betterDecision.trim().length === 0) {
|
|
828
|
+
failures.push('betterDecision is required and non-empty');
|
|
829
|
+
}
|
|
830
|
+
if (!draft.rationale || draft.rationale.trim().length < 20) {
|
|
831
|
+
failures.push('rationale must be at least 20 characters');
|
|
832
|
+
}
|
|
833
|
+
if (!draft.principleId || draft.principleId.trim().length === 0) {
|
|
834
|
+
failures.push('principleId is required');
|
|
835
|
+
}
|
|
836
|
+
if (!draft.sessionId || draft.sessionId.trim().length === 0) {
|
|
837
|
+
failures.push('sessionId is required');
|
|
838
|
+
}
|
|
839
|
+
// badDecision should not be identical to betterDecision
|
|
840
|
+
if (typeof draft.badDecision === 'string' &&
|
|
841
|
+
typeof draft.betterDecision === 'string' &&
|
|
842
|
+
draft.badDecision.trim().length > 0 &&
|
|
843
|
+
draft.betterDecision.trim().length > 0 &&
|
|
844
|
+
draft.badDecision.trim() === draft.betterDecision.trim()) {
|
|
845
|
+
failures.push('badDecision and betterDecision cannot be identical');
|
|
846
|
+
}
|
|
847
|
+
return {
|
|
848
|
+
valid: failures.length === 0,
|
|
849
|
+
failures,
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
/**
|
|
853
|
+
* Convert a TrinityDraftArtifact to a NocturnalArtifact-compatible structure.
|
|
854
|
+
*/
|
|
855
|
+
export function draftToArtifact(draft) {
|
|
856
|
+
// Compute reflection quality metrics
|
|
857
|
+
const thinkingModelDelta = draft.thinkingModelDelta ?? computeThinkingModelDelta(draft.badDecision, draft.betterDecision);
|
|
858
|
+
// planningRatioGain requires an improved snapshot — Trinity draft doesn't have one, so default to 0
|
|
859
|
+
const planningRatioGain = draft.planningRatioGain ?? 0;
|
|
860
|
+
return {
|
|
861
|
+
artifactId: randomUUID(),
|
|
862
|
+
sessionId: draft.sessionId,
|
|
863
|
+
principleId: draft.principleId,
|
|
864
|
+
sourceSnapshotRef: draft.sourceSnapshotRef,
|
|
865
|
+
badDecision: draft.badDecision,
|
|
866
|
+
betterDecision: draft.betterDecision,
|
|
867
|
+
rationale: draft.rationale,
|
|
868
|
+
createdAt: new Date().toISOString(),
|
|
869
|
+
thinkingModelDelta,
|
|
870
|
+
planningRatioGain,
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
// ---------------------------------------------------------------------------
|
|
874
|
+
// Default Configuration
|
|
875
|
+
// ---------------------------------------------------------------------------
|
|
876
|
+
export const DEFAULT_TRINITY_CONFIG = {
|
|
877
|
+
useTrinity: true,
|
|
878
|
+
maxCandidates: 3,
|
|
879
|
+
useStubs: false, // Real subagent execution is the default; set useStubs=true for stub-only mode
|
|
880
|
+
};
|