parallax-opencode 0.2.0

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/plugin.js ADDED
@@ -0,0 +1,609 @@
1
+ /**
2
+ * PARALLAX ENGINE -- Canonical TypeScript Plugin
3
+ *
4
+ * Consolidated source of truth for the Parallax Engine OpenCode plugin.
5
+ * Contains all 7 custom tools, mode state machine (free/plan/build/debug),
6
+ * protocol enforcement, friction-loop verification, skill injection,
7
+ * session state preservation, and trace recording.
8
+ *
9
+ * License: MIT
10
+ * Copyright (c) 2026 Master0fFate
11
+ */
12
+ import { tool } from "@opencode-ai/plugin";
13
+ import { readFileSync } from "fs";
14
+ import { homedir } from "os";
15
+ import { join } from "path";
16
+ import { detectProject, runVerify } from "./detect";
17
+ import { initTrace, addPhase, addWrite, exportTrace, getTrace, } from "./trace";
18
+ import { computeCoherenceScore } from "./score";
19
+ import { initDiscordRpc, getDiscordRpc, resolveAgent } from "./discord-rpc";
20
+ // ---------------------------------------------------------------------------
21
+ // Constants
22
+ // ---------------------------------------------------------------------------
23
+ const MAX_FRICTION_RETRIES = 3;
24
+ // ## BROKEN -- Discord RPC never shows presence. See src/discord-rpc.ts
25
+ const DISCORD_RPC_ENABLED = process.env.PARALLAX_DISCORD_RPC !== "false";
26
+ const CHECK_DEBOUNCE_MS = 1000;
27
+ const CONFIG_DIR = join(homedir(), ".config", "opencode");
28
+ // ---------------------------------------------------------------------------
29
+ // Module-level stores
30
+ // ---------------------------------------------------------------------------
31
+ const frictionStore = new Map();
32
+ const modeStore = new Map();
33
+ const protocolStore = new Map();
34
+ let currentSessionId = null;
35
+ let currentAgentName = null;
36
+ function sessionId() {
37
+ return currentSessionId || "default";
38
+ }
39
+ function getFriction(s = sessionId()) {
40
+ if (!frictionStore.has(s)) {
41
+ frictionStore.set(s, {
42
+ successes: 0,
43
+ trials: 0,
44
+ retriesLeft: MAX_FRICTION_RETRIES,
45
+ lastObservation: null,
46
+ });
47
+ }
48
+ return frictionStore.get(s);
49
+ }
50
+ function getMode(s = sessionId()) {
51
+ if (!modeStore.has(s)) {
52
+ modeStore.set(s, { mode: "free" });
53
+ }
54
+ return modeStore.get(s);
55
+ }
56
+ function getProtocol(s = sessionId()) {
57
+ if (!protocolStore.has(s)) {
58
+ protocolStore.set(s, {
59
+ ambiguityDone: false,
60
+ invariantsDone: false,
61
+ gateDone: false,
62
+ commitDone: false,
63
+ summaryDone: false,
64
+ writesBeforeGate: 0,
65
+ gateBlocked: false,
66
+ });
67
+ }
68
+ return protocolStore.get(s);
69
+ }
70
+ // ---------------------------------------------------------------------------
71
+ // Skill loader
72
+ // ---------------------------------------------------------------------------
73
+ const skillCache = {};
74
+ function loadSkill(name) {
75
+ if (name in skillCache)
76
+ return skillCache[name];
77
+ const path = join(CONFIG_DIR, "skills", name, "SKILL.md");
78
+ try {
79
+ const raw = readFileSync(path, "utf8");
80
+ skillCache[name] = raw.replace(/^---[\s\S]*?---\n*/, "");
81
+ }
82
+ catch {
83
+ skillCache[name] = null;
84
+ }
85
+ return skillCache[name];
86
+ }
87
+ // ---------------------------------------------------------------------------
88
+ // Utility
89
+ // ---------------------------------------------------------------------------
90
+ function truncate(s, maxLen) {
91
+ if (!s || s.length <= maxLen)
92
+ return s || "";
93
+ return s.slice(0, maxLen) + `\n[Truncated at ${maxLen} chars]`;
94
+ }
95
+ // ---------------------------------------------------------------------------
96
+ // Step labels & mode metadata
97
+ // ---------------------------------------------------------------------------
98
+ const STEP_LABELS = {
99
+ ambiguity: "Ambiguity Check",
100
+ invariants: "4 Invariants",
101
+ gate: "Verification Gate",
102
+ commit: "Commit Decision",
103
+ summary: "Summarize",
104
+ };
105
+ const MODE_META = {
106
+ free: { skill: null, label: null },
107
+ build: { skill: null, label: "PARALLAX BUILD MODE" },
108
+ plan: { skill: "parallax-plan", label: "PARALLAX PLAN MODE" },
109
+ debug: { skill: "parallax-debug", label: "PARALLAX DEBUG MODE" },
110
+ };
111
+ // ---------------------------------------------------------------------------
112
+ // Debounce timer
113
+ // ---------------------------------------------------------------------------
114
+ let debounceTimer = null;
115
+ // ---------------------------------------------------------------------------
116
+ // Plugin export
117
+ // ---------------------------------------------------------------------------
118
+ export default {
119
+ id: "parallax-engine",
120
+ server: async ({ client }) => {
121
+ if (DISCORD_RPC_ENABLED) {
122
+ initDiscordRpc().catch(() => { });
123
+ }
124
+ return {
125
+ // -----------------------------------------------------------------------
126
+ // Custom tools
127
+ // -----------------------------------------------------------------------
128
+ tool: {
129
+ // VERIFY
130
+ parallax_verify: tool({
131
+ description: "Run the project's verification command (cargo check, tsc, npm run lint, " +
132
+ "python compileall) and return the result. Use this instead of running " +
133
+ "checks manually via bash.",
134
+ args: {},
135
+ async execute() {
136
+ const result = runVerify();
137
+ if (!result) {
138
+ return "[parallax] No known project type -- skipping verification.";
139
+ }
140
+ if (result.exitCode === 0) {
141
+ return `[parallax] VERIFICATION PASSED (exit 0)\n${truncate(result.stdout, 500)}`;
142
+ }
143
+ return `[parallax] VERIFICATION FAILED (exit ${result.exitCode})\n${truncate(result.combined, 2000)}`;
144
+ },
145
+ }),
146
+ // ANALYZE
147
+ parallax_analyze: tool({
148
+ description: "Run structured Parallax multi-perspective analysis on a specific component " +
149
+ "or change. Surfaces edge cases, cross-cutting concerns, and verification " +
150
+ "criteria before you write code.",
151
+ args: {
152
+ topic: tool.schema.string().describe("The component, module, function, or change to analyze"),
153
+ },
154
+ async execute(args) {
155
+ addPhase(sessionId(), "mode_switch", { analysisTopic: args.topic });
156
+ return (`[parallax] ANALYSIS FRAMEWORK: ${args.topic}\n\n` +
157
+ `Apply these questions to "${args.topic}":\n\n` +
158
+ `NOMINAL CASE -- What does success look like for ${args.topic}?\n\n` +
159
+ `EDGE CASES:\n` +
160
+ `- Empty states / null / missing inputs\n` +
161
+ `- Boundary conditions / overflow\n` +
162
+ `- Error states / failure paths\n` +
163
+ `- Concurrency / race conditions\n` +
164
+ `- State transitions / interruption safety\n` +
165
+ `- Security (injection, credential exposure, path traversal)\n` +
166
+ `- Backward compatibility (migrations, deprecation)\n\n` +
167
+ `CROSS-CUTTING:\n` +
168
+ `- Error handling: does every failure path produce a clear message?\n` +
169
+ `- Observability: can we trace what happened?\n` +
170
+ `- Performance: hot paths, O(n^2), memory leaks\n` +
171
+ `- Testability: how would each component be tested?\n` +
172
+ `- Rollback: if this fails, how do we undo it?\n\n` +
173
+ `Use grep and read to investigate ${args.topic} in the codebase, ` +
174
+ `then proceed with the Parallax protocol.`);
175
+ },
176
+ }),
177
+ // CHECKIN -- protocol step tracking with ordering enforcement
178
+ parallax_checkin: tool({
179
+ description: "Mark a protocol step as complete. The plugin tracks this to enforce " +
180
+ "the protocol order. Call this after completing each step.",
181
+ args: {
182
+ step: tool.schema.string().describe("The protocol step to mark complete: ambiguity, invariants, gate, commit, summary"),
183
+ },
184
+ async execute(args) {
185
+ const p = getProtocol();
186
+ const step = args.step;
187
+ if (!STEP_LABELS[step]) {
188
+ return (`[parallax] Unknown step "${step}". ` +
189
+ `Valid: ${Object.keys(STEP_LABELS).join(", ")}`);
190
+ }
191
+ const sid = sessionId();
192
+ // Enforce ordering
193
+ if (step === "ambiguity" && !p.ambiguityDone) {
194
+ p.ambiguityDone = true;
195
+ addPhase(sid, "ambiguity_check");
196
+ return "[parallax] Step 1/6: Ambiguity Check marked complete.";
197
+ }
198
+ if (step === "invariants") {
199
+ if (!p.ambiguityDone) {
200
+ return "[parallax] ERROR: Complete Ambiguity Check first (Step 1).";
201
+ }
202
+ p.invariantsDone = true;
203
+ addPhase(sid, "four_invariants");
204
+ return "[parallax] Step 2/6: 4 Invariants marked complete.";
205
+ }
206
+ if (step === "gate") {
207
+ if (!p.invariantsDone) {
208
+ return "[parallax] ERROR: Complete 4 Invariants first (Step 2).";
209
+ }
210
+ p.gateDone = true;
211
+ addPhase(sid, "verification_gate");
212
+ return "[parallax] Step 3/6: Verification Gate marked complete.";
213
+ }
214
+ if (step === "commit") {
215
+ p.commitDone = true;
216
+ addPhase(sid, "commit_decision");
217
+ return "[parallax] Step 5/6: Commit Decision marked complete.";
218
+ }
219
+ if (step === "summary") {
220
+ p.summaryDone = true;
221
+ addPhase(sid, "summary");
222
+ return "[parallax] Step 6/6: Summary marked complete. Protocol finished.";
223
+ }
224
+ if (p[`${step}Done`]) {
225
+ return `[parallax] Step "${step}" was already completed.`;
226
+ }
227
+ return `[parallax] Unknown step state for "${step}".`;
228
+ },
229
+ }),
230
+ // MODE: PLAN
231
+ parallax_plan: tool({
232
+ description: "Switch to PLAN mode. Injects the Precision Architect skill for deep " +
233
+ "requirements elicitation and structured planning. Best for Phase 1-3 " +
234
+ "of the protocol. Use this when you need to fully spec out a feature " +
235
+ "before building.",
236
+ args: {},
237
+ async execute() {
238
+ getMode().mode = "plan";
239
+ addPhase(sessionId(), "mode_switch", { mode: "plan" });
240
+ return ("[parallax] PLAN mode activated. Precision Architect skill loaded. " +
241
+ "Elicit requirements fully before building.");
242
+ },
243
+ }),
244
+ // MODE: BUILD
245
+ parallax_build: tool({
246
+ description: "Switch to BUILD mode (default). Standard Parallax execution protocol. " +
247
+ "Best for Phase 4-5 execution work. Use this when you have a clear plan " +
248
+ "and need to write code.",
249
+ args: {},
250
+ async execute() {
251
+ getMode().mode = "build";
252
+ addPhase(sessionId(), "mode_switch", { mode: "build" });
253
+ return ("[parallax] BUILD mode activated. Standard Parallax execution protocol. " +
254
+ "Write clean code, verify with parallax_verify.");
255
+ },
256
+ }),
257
+ // MODE: DEBUG
258
+ parallax_debug: tool({
259
+ description: "Switch to DEBUG mode. Injects the Universal Auditor skill for " +
260
+ "comprehensive post-build audit. Best for Phase 6 review. Use this " +
261
+ "after building to audit quality, security, and correctness.",
262
+ args: {},
263
+ async execute() {
264
+ getMode().mode = "debug";
265
+ addPhase(sessionId(), "mode_switch", { mode: "debug" });
266
+ return ("[parallax] DEBUG mode activated. Universal Auditor skill loaded. " +
267
+ "Run a full audit pass.");
268
+ },
269
+ }),
270
+ // TRACE EXPORT -- export current session trace to file
271
+ parallax_trace_export: tool({
272
+ description: "Export the current session's structured reasoning trace to a JSON file. " +
273
+ "Traces capture protocol phases, writes, verifications, and coherence score. " +
274
+ "Use --pretty for human-readable formatting.",
275
+ args: {
276
+ pretty: tool.schema.boolean().optional().describe("Format output with indentation for human readability"),
277
+ },
278
+ async execute(args) {
279
+ const sid = sessionId();
280
+ const pretty = args.pretty === true;
281
+ const filePath = exportTrace(sid, pretty);
282
+ const trace = getTrace(sid);
283
+ // Compute and attach score
284
+ const breakdown = computeCoherenceScore(trace);
285
+ trace.coherenceScore = breakdown.total;
286
+ return (`[parallax] Trace exported: ${filePath}\n` +
287
+ `Session: ${sid}\n` +
288
+ `Phases: ${trace.phases.length}, Writes: ${trace.writes.length}\n` +
289
+ `Coherence Score: ${breakdown.total}/100`);
290
+ },
291
+ }),
292
+ },
293
+ // -----------------------------------------------------------------------
294
+ // Pre-write enforcement: protocol ordering + friction block
295
+ // -----------------------------------------------------------------------
296
+ "tool.execute.before": async (input) => {
297
+ if (!["write", "edit", "apply_patch"].includes(input.tool))
298
+ return;
299
+ if (DISCORD_RPC_ENABLED) {
300
+ const rpc = getDiscordRpc();
301
+ if (rpc.connected) {
302
+ rpc.updatePresence({
303
+ status: "coding",
304
+ mode: getMode().mode,
305
+ agent: resolveAgent(currentAgentName),
306
+ }).catch(() => { });
307
+ }
308
+ }
309
+ const p = getProtocol();
310
+ // Enforce ambiguity check before any write
311
+ if (!p.ambiguityDone) {
312
+ throw new Error(`[parallax] PROTOCOL VIOLATION: Ambiguity Check (Step 1) not completed.\n` +
313
+ `You MUST state HIGH/MEDIUM/LOW and ask clarifying questions ` +
314
+ `before writing code.\n` +
315
+ `Use parallax_checkin({ step: "ambiguity" }) after completing it.`);
316
+ }
317
+ // Warn after 3 writes without invariants checkin
318
+ if (!p.invariantsDone) {
319
+ p.writesBeforeGate++;
320
+ if (p.writesBeforeGate > 3) {
321
+ throw new Error(`[parallax] PROTOCOL VIOLATION: 4 Invariants (Step 2) not completed ` +
322
+ `after ${p.writesBeforeGate} writes.\n` +
323
+ `State: state ownership, feedback location, deletion blast radius, ` +
324
+ `timing concerns.\n` +
325
+ `Use parallax_checkin({ step: "invariants" }) after completing it.`);
326
+ }
327
+ }
328
+ // Friction block
329
+ const s = getFriction();
330
+ if (s.retriesLeft === 0 && s.lastObservation) {
331
+ throw new Error(`[parallax] Friction blocked: fix the outstanding issue first.\n` +
332
+ `${s.lastObservation}`);
333
+ }
334
+ },
335
+ // -----------------------------------------------------------------------
336
+ // Post-write debounced auto-verify (friction loop)
337
+ // -----------------------------------------------------------------------
338
+ "tool.execute.after": async (input) => {
339
+ if (!["write", "edit", "apply_patch"].includes(input.tool))
340
+ return;
341
+ if (DISCORD_RPC_ENABLED) {
342
+ const rpc = getDiscordRpc();
343
+ if (rpc.connected) {
344
+ rpc.updatePresence({
345
+ status: "coding",
346
+ mode: getMode().mode,
347
+ agent: resolveAgent(currentAgentName),
348
+ }).catch(() => { });
349
+ }
350
+ }
351
+ const s = getFriction();
352
+ if (s.retriesLeft === 0)
353
+ return;
354
+ const sid = sessionId();
355
+ // Record the file being written for trace
356
+ const fileName = input.args && typeof input.args.filePath === "string"
357
+ ? input.args.filePath
358
+ : input.args && typeof input.args.path === "string"
359
+ ? input.args.path
360
+ : `(${input.tool})`;
361
+ if (debounceTimer)
362
+ clearTimeout(debounceTimer);
363
+ debounceTimer = setTimeout(() => {
364
+ debounceTimer = null;
365
+ const result = runVerify();
366
+ if (!result) {
367
+ addWrite(sid, fileName, "skipped", s.retriesLeft);
368
+ return;
369
+ }
370
+ s.trials++;
371
+ if (result.exitCode === 0) {
372
+ s.successes++;
373
+ s.retriesLeft = MAX_FRICTION_RETRIES;
374
+ s.lastObservation = null;
375
+ addWrite(sid, fileName, "pass", s.retriesLeft);
376
+ client.app
377
+ .log({
378
+ body: {
379
+ service: "parallax",
380
+ level: "info",
381
+ message: `[parallax] Check passed (${s.successes} ok / ${s.trials} trials)`,
382
+ },
383
+ })
384
+ .catch(() => { });
385
+ }
386
+ else {
387
+ s.retriesLeft--;
388
+ s.lastObservation = truncate(result.combined, 2000);
389
+ addWrite(sid, fileName, "fail", s.retriesLeft);
390
+ const lvl = s.retriesLeft === 0 ? "error" : "warn";
391
+ client.app
392
+ .log({
393
+ body: {
394
+ service: "parallax",
395
+ level: lvl,
396
+ message: `[parallax] Check FAILED. ${s.retriesLeft} retries left.`,
397
+ extra: { output: s.lastObservation },
398
+ },
399
+ })
400
+ .catch(() => { });
401
+ }
402
+ }, CHECK_DEBOUNCE_MS);
403
+ },
404
+ // -----------------------------------------------------------------------
405
+ // Event hook: track session ID
406
+ // -----------------------------------------------------------------------
407
+ event: async (input) => {
408
+ if (input.event.type === "session.created") {
409
+ const props = input.event.properties || {};
410
+ const info = (props.info || {});
411
+ currentSessionId =
412
+ info.id ||
413
+ props.sessionID ||
414
+ info.sessionID ||
415
+ null;
416
+ // Agent name lives in Session.agent (v2 SDK types.gen.d.ts:590)
417
+ currentAgentName =
418
+ info.agent ||
419
+ props.agent ||
420
+ null;
421
+ // Initialize trace with session info
422
+ if (currentSessionId) {
423
+ initTrace(currentSessionId, process.cwd(), detectProject());
424
+ }
425
+ }
426
+ // Track agent switches (TAB to change agent in OpenCode TUI)
427
+ if (input.event.type === "session.next.agent.switched") {
428
+ const props = input.event.properties;
429
+ currentAgentName = props?.agent || null;
430
+ }
431
+ // Discord RPC: track session lifecycle
432
+ if (DISCORD_RPC_ENABLED) {
433
+ const rpc = getDiscordRpc();
434
+ const agent = resolveAgent(currentAgentName);
435
+ switch (input.event.type) {
436
+ case "session.created": {
437
+ rpc.startSession();
438
+ rpc.updatePresence({
439
+ status: "coding",
440
+ mode: getMode().mode,
441
+ agent,
442
+ }).catch(() => { });
443
+ break;
444
+ }
445
+ case "session.status": {
446
+ const props = input.event.properties;
447
+ const statusType = props?.status?.type;
448
+ if (statusType === "busy") {
449
+ rpc.updatePresence({
450
+ status: "coding",
451
+ mode: getMode().mode,
452
+ agent,
453
+ }).catch(() => { });
454
+ }
455
+ else if (statusType === "idle") {
456
+ rpc.updatePresence({
457
+ status: "waiting",
458
+ mode: getMode().mode,
459
+ agent,
460
+ }).catch(() => { });
461
+ }
462
+ break;
463
+ }
464
+ case "session.deleted": {
465
+ rpc.clearPresence().catch(() => { });
466
+ rpc.clearSession();
467
+ break;
468
+ }
469
+ case "session.idle": {
470
+ rpc.updatePresence({
471
+ status: "idle",
472
+ mode: getMode().mode,
473
+ agent,
474
+ }).catch(() => { });
475
+ break;
476
+ }
477
+ case "message.part.updated": {
478
+ rpc.updatePresence({
479
+ status: "thinking",
480
+ mode: getMode().mode,
481
+ agent,
482
+ }).catch(() => { });
483
+ break;
484
+ }
485
+ }
486
+ }
487
+ },
488
+ // -----------------------------------------------------------------------
489
+ // Chat hooks: detect model for Discord RPC
490
+ // -----------------------------------------------------------------------
491
+ "chat.message": async (input) => {
492
+ if (!DISCORD_RPC_ENABLED)
493
+ return;
494
+ const rpc = getDiscordRpc();
495
+ if (!rpc.connected)
496
+ return;
497
+ // Agent name comes directly from the hook input (v2 SDK)
498
+ if (input.agent)
499
+ currentAgentName = input.agent;
500
+ let modelName;
501
+ if (input.model?.modelID) {
502
+ modelName = input.model.modelID
503
+ .replace(/-\d{4}-\d{2}-\d{2}$/, "")
504
+ .replace(/-\d{8}$/, "");
505
+ }
506
+ rpc.updatePresence({
507
+ status: "thinking",
508
+ modelName,
509
+ mode: getMode().mode,
510
+ agent: resolveAgent(currentAgentName),
511
+ }).catch(() => { });
512
+ },
513
+ "chat.params": async (input) => {
514
+ if (!DISCORD_RPC_ENABLED)
515
+ return;
516
+ const rpc = getDiscordRpc();
517
+ if (!rpc.connected)
518
+ return;
519
+ // Agent name is required in chat.params
520
+ currentAgentName = input.agent;
521
+ let modelName;
522
+ if (input.model?.id) {
523
+ modelName = input.model.id
524
+ .replace(/-\d{4}-\d{2}-\d{2}$/, "")
525
+ .replace(/-\d{8}$/, "");
526
+ }
527
+ rpc.updatePresence({
528
+ status: "thinking",
529
+ modelName,
530
+ mode: getMode().mode,
531
+ agent: resolveAgent(currentAgentName),
532
+ }).catch(() => { });
533
+ },
534
+ // -----------------------------------------------------------------------
535
+ // System prompt transformation: inject protocol status + mode skill
536
+ // -----------------------------------------------------------------------
537
+ "experimental.chat.system.transform": async (_input, output) => {
538
+ const m = getMode();
539
+ const s = getFriction();
540
+ const p = getProtocol();
541
+ // Build protocol status block
542
+ const statusLines = [];
543
+ const steps = [
544
+ "ambiguity",
545
+ "invariants",
546
+ "gate",
547
+ "commit",
548
+ "summary",
549
+ ];
550
+ let currentStep = null;
551
+ for (const step of steps) {
552
+ const done = p[`${step}Done`];
553
+ const label = STEP_LABELS[step];
554
+ statusLines.push(` ${done ? "[DONE]" : "[PENDING]"} Step: ${label}`);
555
+ if (!done && !currentStep)
556
+ currentStep = label;
557
+ }
558
+ const activeStep = currentStep || "Complete";
559
+ const sys = output.system || (output.system = []);
560
+ sys.push(`\n## PARALLAX PROTOCOL STATUS\n\n` +
561
+ `Active Step: ${activeStep}\n${statusLines.join("\n")}`);
562
+ // Inject mode skill
563
+ if (m.mode !== "free") {
564
+ const meta = MODE_META[m.mode];
565
+ if (meta && meta.label)
566
+ sys.push(`\n=== ${meta.label} ===`);
567
+ if (meta && meta.skill) {
568
+ const content = loadSkill(meta.skill);
569
+ if (content)
570
+ sys.push(content);
571
+ }
572
+ if (m.mode === "build") {
573
+ sys.push("\nExecute the plan. Write clean code. Verify with parallax_verify " +
574
+ "after writes. Flag deferred items.");
575
+ }
576
+ }
577
+ // Inject friction state
578
+ if (s.lastObservation) {
579
+ sys.push(`\n## PARALLAX FRICTION STATE\n\n` +
580
+ `A previous check failed. Fix this before writing more code:\n\n` +
581
+ `${s.lastObservation}\n\nRetries remaining: ${s.retriesLeft}`);
582
+ }
583
+ },
584
+ // -----------------------------------------------------------------------
585
+ // Session compaction: preserve state across context window resets
586
+ // -----------------------------------------------------------------------
587
+ "experimental.session.compacting": async (_input, output) => {
588
+ const s = getFriction();
589
+ const m = getMode();
590
+ const p = getProtocol();
591
+ const sid = sessionId();
592
+ // Export trace to disk on compaction
593
+ try {
594
+ exportTrace(sid);
595
+ }
596
+ catch {
597
+ // Non-fatal: trace export is best-effort
598
+ }
599
+ const ctx = output.context || (output.context = []);
600
+ ctx.push(`## PARALLAX SESSION STATE\n` +
601
+ `- Mode: ${m.mode}\n` +
602
+ `- Ambiguity: ${p.ambiguityDone}, Invariants: ${p.invariantsDone}, ` +
603
+ `Gate: ${p.gateDone}\n` +
604
+ `- Friction: ${s.successes} ok / ${s.trials} trials, ` +
605
+ `Retries: ${s.retriesLeft}`);
606
+ },
607
+ };
608
+ }
609
+ };
@@ -0,0 +1,41 @@
1
+ /**
2
+ * PARALLAX ENGINE -- Coherence Score Computation
3
+ *
4
+ * Computes an evidence-based quality score (0-100) from a Parallax trace.
5
+ * Measures how well the agent followed the methodology.
6
+ *
7
+ * Score components:
8
+ * - Protocol Coverage (30%): Did all 5 protocol phases execute?
9
+ * - Verification Integrity (35%): Pass rate on first attempt?
10
+ * - Edge Case Coverage (20%): How many edge categories analyzed?
11
+ * - Timing Discipline (15%): Were phases in correct order?
12
+ *
13
+ * License: MIT
14
+ * Copyright (c) 2026 Master0fFate
15
+ */
16
+ import type { ParallaxTrace, ScoreBreakdown, ScoreEntry } from "./types";
17
+ /**
18
+ * Compute the coherence score (0-100) from a trace.
19
+ * Handles missing data and partial traces gracefully.
20
+ */
21
+ export declare function computeCoherenceScore(trace: ParallaxTrace): ScoreBreakdown;
22
+ /**
23
+ * Get a human-readable grade for a score.
24
+ */
25
+ export declare function scoreToGrade(score: number): string;
26
+ /**
27
+ * Format score breakdown for display.
28
+ */
29
+ export declare function formatScoreBreakdown(breakdown: ScoreBreakdown): string;
30
+ /**
31
+ * Record a score entry to the append-only scores file.
32
+ */
33
+ export declare function recordScore(entry: ScoreEntry): void;
34
+ /**
35
+ * Read all score entries from the scores file.
36
+ */
37
+ export declare function readScoreHistory(): ScoreEntry[];
38
+ /**
39
+ * Compute a simple sparkline representation of scores over time.
40
+ */
41
+ export declare function sparkline(scores: number[]): string;