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/LICENSE +21 -0
- package/README.md +217 -0
- package/agents/parallax.md +100 -0
- package/dist/cli.d.ts +19 -0
- package/dist/cli.js +236 -0
- package/dist/detect.d.ts +23 -0
- package/dist/detect.js +65 -0
- package/dist/discord-rpc.d.ts +79 -0
- package/dist/discord-rpc.js +297 -0
- package/dist/plugin.d.ts +99 -0
- package/dist/plugin.js +609 -0
- package/dist/score.d.ts +41 -0
- package/dist/score.js +160 -0
- package/dist/trace.d.ts +62 -0
- package/dist/trace.js +206 -0
- package/dist/types.d.ts +94 -0
- package/dist/types.js +10 -0
- package/dist-standalone/parallax-engine.d.ts +99 -0
- package/dist-standalone/parallax-engine.js +35239 -0
- package/package.json +65 -0
- package/postinstall.mjs +14 -0
- package/scripts/install.mjs +129 -0
- package/scripts/publish.mjs +82 -0
- package/skills/parallax/SKILL.md +76 -0
- package/skills/parallax-debug/SKILL.md +163 -0
- package/skills/parallax-plan/SKILL.md +191 -0
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
|
+
};
|
package/dist/score.d.ts
ADDED
|
@@ -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;
|