parallax-opencode 0.2.0 → 0.3.1
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/README.md +260 -217
- package/dist/cli.js +239 -2
- package/dist/plugin.d.ts +15 -13
- package/dist/plugin.js +276 -128
- package/dist/score.d.ts +16 -0
- package/dist/score.js +60 -0
- package/dist/tests/detect.test.d.ts +1 -0
- package/dist/tests/detect.test.js +75 -0
- package/dist/tests/friction.test.d.ts +1 -0
- package/dist/tests/friction.test.js +87 -0
- package/dist/tests/protocol.test.d.ts +1 -0
- package/dist/tests/protocol.test.js +80 -0
- package/dist/tests/score.test.d.ts +1 -0
- package/dist/tests/score.test.js +106 -0
- package/dist/tests/trace.test.d.ts +1 -0
- package/dist/tests/trace.test.js +109 -0
- package/dist/types.d.ts +11 -2
- package/dist-standalone/parallax-engine.d.ts +15 -13
- package/dist-standalone/parallax-engine.js +241 -34594
- package/package.json +65 -65
- package/dist/discord-rpc.d.ts +0 -79
- package/dist/discord-rpc.js +0 -297
package/dist/plugin.js
CHANGED
|
@@ -10,21 +10,21 @@
|
|
|
10
10
|
* Copyright (c) 2026 Master0fFate
|
|
11
11
|
*/
|
|
12
12
|
import { tool } from "@opencode-ai/plugin";
|
|
13
|
-
import { readFileSync } from "fs";
|
|
13
|
+
import { readFileSync, writeFileSync, existsSync } from "fs";
|
|
14
14
|
import { homedir } from "os";
|
|
15
15
|
import { join } from "path";
|
|
16
16
|
import { detectProject, runVerify } from "./detect";
|
|
17
17
|
import { initTrace, addPhase, addWrite, exportTrace, getTrace, } from "./trace";
|
|
18
18
|
import { computeCoherenceScore } from "./score";
|
|
19
|
-
import { initDiscordRpc, getDiscordRpc, resolveAgent } from "./discord-rpc";
|
|
20
19
|
// ---------------------------------------------------------------------------
|
|
21
20
|
// Constants
|
|
22
21
|
// ---------------------------------------------------------------------------
|
|
23
22
|
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
23
|
const CHECK_DEBOUNCE_MS = 1000;
|
|
24
|
+
const STATE_DEBOUNCE_MS = 100;
|
|
27
25
|
const CONFIG_DIR = join(homedir(), ".config", "opencode");
|
|
26
|
+
const STATE_FILE = join(".parallax", "state.json");
|
|
27
|
+
const CONFIG_FILE = join(".parallax", "config.json");
|
|
28
28
|
// ---------------------------------------------------------------------------
|
|
29
29
|
// Module-level stores
|
|
30
30
|
// ---------------------------------------------------------------------------
|
|
@@ -59,6 +59,7 @@ function getProtocol(s = sessionId()) {
|
|
|
59
59
|
ambiguityDone: false,
|
|
60
60
|
invariantsDone: false,
|
|
61
61
|
gateDone: false,
|
|
62
|
+
designDone: false,
|
|
62
63
|
commitDone: false,
|
|
63
64
|
summaryDone: false,
|
|
64
65
|
writesBeforeGate: 0,
|
|
@@ -68,6 +69,67 @@ function getProtocol(s = sessionId()) {
|
|
|
68
69
|
return protocolStore.get(s);
|
|
69
70
|
}
|
|
70
71
|
// ---------------------------------------------------------------------------
|
|
72
|
+
// Config loader
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
let configCache = null;
|
|
75
|
+
let configCacheLoaded = false;
|
|
76
|
+
function loadConfig() {
|
|
77
|
+
if (configCacheLoaded)
|
|
78
|
+
return configCache || {};
|
|
79
|
+
configCacheLoaded = true;
|
|
80
|
+
try {
|
|
81
|
+
if (existsSync(CONFIG_FILE)) {
|
|
82
|
+
const raw = readFileSync(CONFIG_FILE, "utf8");
|
|
83
|
+
configCache = JSON.parse(raw);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// Invalid JSON or missing file -> use defaults
|
|
88
|
+
}
|
|
89
|
+
return configCache || {};
|
|
90
|
+
}
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
// State persistence (Phase 2.1)
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
let stateDebounceTimer = null;
|
|
95
|
+
function writeState() {
|
|
96
|
+
if (stateDebounceTimer)
|
|
97
|
+
clearTimeout(stateDebounceTimer);
|
|
98
|
+
stateDebounceTimer = setTimeout(() => {
|
|
99
|
+
stateDebounceTimer = null;
|
|
100
|
+
try {
|
|
101
|
+
const s = getFriction();
|
|
102
|
+
const m = getMode();
|
|
103
|
+
const p = getProtocol();
|
|
104
|
+
const state = {
|
|
105
|
+
sessionId: currentSessionId,
|
|
106
|
+
sessionStart: getTrace(sessionId()).session.startedAt,
|
|
107
|
+
mode: m.mode,
|
|
108
|
+
friction: {
|
|
109
|
+
successes: s.successes,
|
|
110
|
+
trials: s.trials,
|
|
111
|
+
retriesLeft: s.retriesLeft,
|
|
112
|
+
lastObservation: s.lastObservation,
|
|
113
|
+
},
|
|
114
|
+
protocol: {
|
|
115
|
+
ambiguityDone: p.ambiguityDone,
|
|
116
|
+
invariantsDone: p.invariantsDone,
|
|
117
|
+
gateDone: p.gateDone,
|
|
118
|
+
designDone: p.designDone,
|
|
119
|
+
commitDone: p.commitDone,
|
|
120
|
+
summaryDone: p.summaryDone,
|
|
121
|
+
writesBeforeGate: p.writesBeforeGate,
|
|
122
|
+
gateBlocked: p.gateBlocked,
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
writeFileSync(STATE_FILE, JSON.stringify(state, null, 2), "utf8");
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
// Best-effort: don't crash the plugin if disk is full
|
|
129
|
+
}
|
|
130
|
+
}, STATE_DEBOUNCE_MS);
|
|
131
|
+
}
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
71
133
|
// Skill loader
|
|
72
134
|
// ---------------------------------------------------------------------------
|
|
73
135
|
const skillCache = {};
|
|
@@ -99,6 +161,7 @@ const STEP_LABELS = {
|
|
|
99
161
|
ambiguity: "Ambiguity Check",
|
|
100
162
|
invariants: "4 Invariants",
|
|
101
163
|
gate: "Verification Gate",
|
|
164
|
+
design: "Design Doc",
|
|
102
165
|
commit: "Commit Decision",
|
|
103
166
|
summary: "Summarize",
|
|
104
167
|
};
|
|
@@ -118,9 +181,6 @@ let debounceTimer = null;
|
|
|
118
181
|
export default {
|
|
119
182
|
id: "parallax-engine",
|
|
120
183
|
server: async ({ client }) => {
|
|
121
|
-
if (DISCORD_RPC_ENABLED) {
|
|
122
|
-
initDiscordRpc().catch(() => { });
|
|
123
|
-
}
|
|
124
184
|
return {
|
|
125
185
|
// -----------------------------------------------------------------------
|
|
126
186
|
// Custom tools
|
|
@@ -179,7 +239,7 @@ export default {
|
|
|
179
239
|
description: "Mark a protocol step as complete. The plugin tracks this to enforce " +
|
|
180
240
|
"the protocol order. Call this after completing each step.",
|
|
181
241
|
args: {
|
|
182
|
-
step: tool.schema.string().describe("The protocol step to mark complete: ambiguity, invariants, gate, commit, summary"),
|
|
242
|
+
step: tool.schema.string().describe("The protocol step to mark complete: ambiguity, invariants, gate, design, commit, summary"),
|
|
183
243
|
},
|
|
184
244
|
async execute(args) {
|
|
185
245
|
const p = getProtocol();
|
|
@@ -189,10 +249,12 @@ export default {
|
|
|
189
249
|
`Valid: ${Object.keys(STEP_LABELS).join(", ")}`);
|
|
190
250
|
}
|
|
191
251
|
const sid = sessionId();
|
|
252
|
+
const cfg = loadConfig();
|
|
192
253
|
// Enforce ordering
|
|
193
254
|
if (step === "ambiguity" && !p.ambiguityDone) {
|
|
194
255
|
p.ambiguityDone = true;
|
|
195
256
|
addPhase(sid, "ambiguity_check");
|
|
257
|
+
writeState();
|
|
196
258
|
return "[parallax] Step 1/6: Ambiguity Check marked complete.";
|
|
197
259
|
}
|
|
198
260
|
if (step === "invariants") {
|
|
@@ -201,6 +263,7 @@ export default {
|
|
|
201
263
|
}
|
|
202
264
|
p.invariantsDone = true;
|
|
203
265
|
addPhase(sid, "four_invariants");
|
|
266
|
+
writeState();
|
|
204
267
|
return "[parallax] Step 2/6: 4 Invariants marked complete.";
|
|
205
268
|
}
|
|
206
269
|
if (step === "gate") {
|
|
@@ -209,17 +272,56 @@ export default {
|
|
|
209
272
|
}
|
|
210
273
|
p.gateDone = true;
|
|
211
274
|
addPhase(sid, "verification_gate");
|
|
275
|
+
writeState();
|
|
212
276
|
return "[parallax] Step 3/6: Verification Gate marked complete.";
|
|
213
277
|
}
|
|
278
|
+
if (step === "design") {
|
|
279
|
+
if (!p.gateDone && cfg.designDocRequired) {
|
|
280
|
+
return "[parallax] ERROR: Complete Verification Gate first (Step 3).";
|
|
281
|
+
}
|
|
282
|
+
p.designDone = true;
|
|
283
|
+
addPhase(sid, "design_check");
|
|
284
|
+
writeState();
|
|
285
|
+
return "[parallax] Step 4/6: Design Doc marked complete.";
|
|
286
|
+
}
|
|
214
287
|
if (step === "commit") {
|
|
215
288
|
p.commitDone = true;
|
|
216
289
|
addPhase(sid, "commit_decision");
|
|
290
|
+
writeState();
|
|
217
291
|
return "[parallax] Step 5/6: Commit Decision marked complete.";
|
|
218
292
|
}
|
|
219
293
|
if (step === "summary") {
|
|
220
294
|
p.summaryDone = true;
|
|
221
295
|
addPhase(sid, "summary");
|
|
222
|
-
|
|
296
|
+
writeState();
|
|
297
|
+
// Phase 2.3: Post-session retrospective
|
|
298
|
+
const trace = getTrace(sid);
|
|
299
|
+
const breakdown = computeCoherenceScore(trace);
|
|
300
|
+
const s = getFriction();
|
|
301
|
+
const passCount = trace.writes.filter((w) => w.verification === "pass").length;
|
|
302
|
+
const failCount = trace.writes.filter((w) => w.verification === "fail").length;
|
|
303
|
+
const retrospective = [
|
|
304
|
+
`[parallax] Step 6/6: Summary marked complete. Protocol finished.`,
|
|
305
|
+
``,
|
|
306
|
+
`## Session Retrospective`,
|
|
307
|
+
``,
|
|
308
|
+
`**What was built:** ${trace.writes.length} writes across ${trace.phases.length} phases`,
|
|
309
|
+
`**Verification:** ${passCount} passed, ${failCount} failed`,
|
|
310
|
+
`**Coherence Score:** ${breakdown.total}/100`,
|
|
311
|
+
`**Friction:** ${s.successes} ok / ${s.trials} trials, ${s.retriesLeft} retries remaining`,
|
|
312
|
+
``,
|
|
313
|
+
`**Review Focus:**`,
|
|
314
|
+
failCount > 0
|
|
315
|
+
? `- ${failCount} verification failures -- review the failed files`
|
|
316
|
+
: `- No verification failures`,
|
|
317
|
+
breakdown.total < 60
|
|
318
|
+
? `- Low coherence score (${breakdown.total}/100) -- protocol steps may have been skipped`
|
|
319
|
+
: ``,
|
|
320
|
+
breakdown.edgeCaseCoverage < 10
|
|
321
|
+
? `- Low edge case coverage (${breakdown.edgeCaseCoverage}/20) -- consider running parallax_analyze on critical paths`
|
|
322
|
+
: ``,
|
|
323
|
+
].filter(Boolean).join("\n");
|
|
324
|
+
return retrospective;
|
|
223
325
|
}
|
|
224
326
|
if (p[`${step}Done`]) {
|
|
225
327
|
return `[parallax] Step "${step}" was already completed.`;
|
|
@@ -237,6 +339,7 @@ export default {
|
|
|
237
339
|
async execute() {
|
|
238
340
|
getMode().mode = "plan";
|
|
239
341
|
addPhase(sessionId(), "mode_switch", { mode: "plan" });
|
|
342
|
+
writeState();
|
|
240
343
|
return ("[parallax] PLAN mode activated. Precision Architect skill loaded. " +
|
|
241
344
|
"Elicit requirements fully before building.");
|
|
242
345
|
},
|
|
@@ -250,6 +353,7 @@ export default {
|
|
|
250
353
|
async execute() {
|
|
251
354
|
getMode().mode = "build";
|
|
252
355
|
addPhase(sessionId(), "mode_switch", { mode: "build" });
|
|
356
|
+
writeState();
|
|
253
357
|
return ("[parallax] BUILD mode activated. Standard Parallax execution protocol. " +
|
|
254
358
|
"Write clean code, verify with parallax_verify.");
|
|
255
359
|
},
|
|
@@ -263,6 +367,7 @@ export default {
|
|
|
263
367
|
async execute() {
|
|
264
368
|
getMode().mode = "debug";
|
|
265
369
|
addPhase(sessionId(), "mode_switch", { mode: "debug" });
|
|
370
|
+
writeState();
|
|
266
371
|
return ("[parallax] DEBUG mode activated. Universal Auditor skill loaded. " +
|
|
267
372
|
"Run a full audit pass.");
|
|
268
373
|
},
|
|
@@ -289,6 +394,136 @@ export default {
|
|
|
289
394
|
`Coherence Score: ${breakdown.total}/100`);
|
|
290
395
|
},
|
|
291
396
|
}),
|
|
397
|
+
// TRACE PR COMMENT -- generates markdown for PR description (Phase 1.1)
|
|
398
|
+
parallax_trace_pr_comment: tool({
|
|
399
|
+
description: "Generate a formatted markdown summary of the current session trace " +
|
|
400
|
+
"suitable for pasting into a GitHub PR comment. Shows coherence score, " +
|
|
401
|
+
"protocol phases completed, write verification summary, and friction stats. " +
|
|
402
|
+
"The AI should call this at session end and paste the output into the PR.",
|
|
403
|
+
args: {},
|
|
404
|
+
async execute() {
|
|
405
|
+
const sid = sessionId();
|
|
406
|
+
const trace = getTrace(sid);
|
|
407
|
+
const breakdown = computeCoherenceScore(trace);
|
|
408
|
+
const s = getFriction();
|
|
409
|
+
if (trace.writes.length === 0) {
|
|
410
|
+
return (`## Parallax Trace -- Planning Session\n\n` +
|
|
411
|
+
`**Session:** ${sid}\n` +
|
|
412
|
+
`**Protocol Steps:** ${trace.phases.length} phases recorded\n` +
|
|
413
|
+
`**Coherence Score:** ${breakdown.total}/100\n\n` +
|
|
414
|
+
`*No code was written in this session.*`);
|
|
415
|
+
}
|
|
416
|
+
const passCount = trace.writes.filter((w) => w.verification === "pass").length;
|
|
417
|
+
const failCount = trace.writes.filter((w) => w.verification === "fail").length;
|
|
418
|
+
const passRate = trace.writes.length > 0
|
|
419
|
+
? Math.round((passCount / trace.writes.length) * 100)
|
|
420
|
+
: 0;
|
|
421
|
+
const phaseTimeline = trace.phases
|
|
422
|
+
.filter((p) => p.phase !== "execution" && p.phase !== "mode_switch")
|
|
423
|
+
.map((p) => {
|
|
424
|
+
const label = p.phase.replace(/_/g, " ");
|
|
425
|
+
return `- [x] ${label} (${p.timestamp.slice(11, 19)})`;
|
|
426
|
+
})
|
|
427
|
+
.join("\n");
|
|
428
|
+
const writeSummary = trace.writes
|
|
429
|
+
.slice(0, 20)
|
|
430
|
+
.map((w) => {
|
|
431
|
+
const icon = w.verification === "pass" ? "[OK]" : w.verification === "fail" ? "[FAIL]" : "[SKIP]";
|
|
432
|
+
const file = w.file.length > 60 ? "..." + w.file.slice(-57) : w.file;
|
|
433
|
+
return `- ${icon} \`${file}\``;
|
|
434
|
+
})
|
|
435
|
+
.join("\n");
|
|
436
|
+
const more = trace.writes.length > 20
|
|
437
|
+
? `\n*...and ${trace.writes.length - 20} more writes*\n`
|
|
438
|
+
: "";
|
|
439
|
+
return [
|
|
440
|
+
`## Parallax Trace`,
|
|
441
|
+
``,
|
|
442
|
+
`| Metric | Value |`,
|
|
443
|
+
`|---|---|`,
|
|
444
|
+
`| **Coherence Score** | **${breakdown.total}/100** |`,
|
|
445
|
+
`| Protocol Coverage | ${breakdown.protocolCoverage}/30 |`,
|
|
446
|
+
`| Verification Integrity | ${breakdown.verificationIntegrity}/35 |`,
|
|
447
|
+
`| Edge Case Coverage | ${breakdown.edgeCaseCoverage}/20 |`,
|
|
448
|
+
`| Timing Discipline | ${breakdown.timingDiscipline}/15 |`,
|
|
449
|
+
``,
|
|
450
|
+
`**Session:** \`${sid}\``,
|
|
451
|
+
``,
|
|
452
|
+
`### Protocol Phases`,
|
|
453
|
+
phaseTimeline,
|
|
454
|
+
``,
|
|
455
|
+
`### Verification Summary`,
|
|
456
|
+
`- ${passCount} passed, ${failCount} failed (${passRate}% pass rate)`,
|
|
457
|
+
`- ${s.trials} trials, ${s.successes} successes`,
|
|
458
|
+
`- Friction retries consumed: ${3 - s.retriesLeft}`,
|
|
459
|
+
``,
|
|
460
|
+
`### Files Changed`,
|
|
461
|
+
writeSummary,
|
|
462
|
+
more,
|
|
463
|
+
``,
|
|
464
|
+
`> Full trace: \`.parallax/traces/${sid}.json\``,
|
|
465
|
+
].join("\n");
|
|
466
|
+
},
|
|
467
|
+
}),
|
|
468
|
+
// TRACE VIEW -- inline trace viewer (Phase 1.2)
|
|
469
|
+
parallax_trace_view: tool({
|
|
470
|
+
description: "Show the current session's complete reasoning trace in the chat. " +
|
|
471
|
+
"Displays ambiguity assessment, 4 invariants analysis, verification gate " +
|
|
472
|
+
"results, every write with pass/fail status, commit decision, and summary. " +
|
|
473
|
+
"Use this when the user asks to see the trace.",
|
|
474
|
+
args: {},
|
|
475
|
+
async execute() {
|
|
476
|
+
const sid = sessionId();
|
|
477
|
+
const trace = getTrace(sid);
|
|
478
|
+
const breakdown = computeCoherenceScore(trace);
|
|
479
|
+
const s = getFriction();
|
|
480
|
+
const p = getProtocol();
|
|
481
|
+
const stepStatus = (done, label) => done ? `[DONE] ${label}` : `[PENDING] ${label}`;
|
|
482
|
+
const writesList = trace.writes.length === 0
|
|
483
|
+
? "*No writes recorded yet.*"
|
|
484
|
+
: trace.writes
|
|
485
|
+
.slice(-30)
|
|
486
|
+
.map((w) => {
|
|
487
|
+
const icon = w.verification === "pass" ? "OK" : w.verification === "fail" ? "FAIL" : "SKIP";
|
|
488
|
+
const file = w.file.length > 80 ? "..." + w.file.slice(-77) : w.file;
|
|
489
|
+
return ` ${icon} | ${file} | retries left: ${w.frictionRetriesLeft}`;
|
|
490
|
+
})
|
|
491
|
+
.join("\n");
|
|
492
|
+
const more = trace.writes.length > 30
|
|
493
|
+
? `\n ... and ${trace.writes.length - 30} more writes (see full trace at .parallax/traces/${sid}.json)`
|
|
494
|
+
: "";
|
|
495
|
+
return [
|
|
496
|
+
`## Parallax Session Trace`,
|
|
497
|
+
`**Session:** \`${sid}\``,
|
|
498
|
+
`**Mode:** ${getMode().mode.toUpperCase()}`,
|
|
499
|
+
``,
|
|
500
|
+
`### Coherence Score: ${breakdown.total}/100`,
|
|
501
|
+
` Protocol Coverage: ${breakdown.protocolCoverage}/30`,
|
|
502
|
+
` Verification Integrity: ${breakdown.verificationIntegrity}/35`,
|
|
503
|
+
` Edge Case Coverage: ${breakdown.edgeCaseCoverage}/20`,
|
|
504
|
+
` Timing Discipline: ${breakdown.timingDiscipline}/15`,
|
|
505
|
+
``,
|
|
506
|
+
`### Protocol Progress`,
|
|
507
|
+
` ${stepStatus(p.ambiguityDone, "1. Ambiguity Check")}`,
|
|
508
|
+
` ${stepStatus(p.invariantsDone, "2. 4 Invariants")}`,
|
|
509
|
+
` ${stepStatus(p.gateDone, "3. Verification Gate")}`,
|
|
510
|
+
` ${stepStatus(p.designDone, "4. Design Doc (optional)")}`,
|
|
511
|
+
` ${stepStatus(p.commitDone, "5. Commit Decision")}`,
|
|
512
|
+
` ${stepStatus(p.summaryDone, "6. Summary")}`,
|
|
513
|
+
``,
|
|
514
|
+
`### Friction`,
|
|
515
|
+
` Successes: ${s.successes} / Trials: ${s.trials}`,
|
|
516
|
+
` Retries remaining: ${s.retriesLeft}`,
|
|
517
|
+
s.lastObservation ? ` Last error: ${s.lastObservation.slice(0, 200)}` : "",
|
|
518
|
+
``,
|
|
519
|
+
`### Writes (last 30)`,
|
|
520
|
+
writesList,
|
|
521
|
+
more,
|
|
522
|
+
``,
|
|
523
|
+
`> Full trace JSON: \`.parallax/traces/${sid}.json\``,
|
|
524
|
+
].filter(Boolean).join("\n");
|
|
525
|
+
},
|
|
526
|
+
}),
|
|
292
527
|
},
|
|
293
528
|
// -----------------------------------------------------------------------
|
|
294
529
|
// Pre-write enforcement: protocol ordering + friction block
|
|
@@ -296,17 +531,8 @@ export default {
|
|
|
296
531
|
"tool.execute.before": async (input) => {
|
|
297
532
|
if (!["write", "edit", "apply_patch"].includes(input.tool))
|
|
298
533
|
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
534
|
const p = getProtocol();
|
|
535
|
+
const cfg = loadConfig();
|
|
310
536
|
// Enforce ambiguity check before any write
|
|
311
537
|
if (!p.ambiguityDone) {
|
|
312
538
|
throw new Error(`[parallax] PROTOCOL VIOLATION: Ambiguity Check (Step 1) not completed.\n` +
|
|
@@ -314,9 +540,17 @@ export default {
|
|
|
314
540
|
`before writing code.\n` +
|
|
315
541
|
`Use parallax_checkin({ step: "ambiguity" }) after completing it.`);
|
|
316
542
|
}
|
|
543
|
+
// Phase 3.2: Design doc enforcement (opt-in)
|
|
544
|
+
if (cfg.designDocRequired && !p.designDone && p.invariantsDone && !process.env.PARALLAX_FORCE) {
|
|
545
|
+
throw new Error(`[parallax] PROTOCOL VIOLATION: Design Doc (Step 4) required by project config.\n` +
|
|
546
|
+
`Complete a design document before writing code for non-trivial changes.\n` +
|
|
547
|
+
`Use parallax_checkin({ step: "design" }) after completing it.\n` +
|
|
548
|
+
`Override: set PARALLAX_FORCE=1 to bypass.`);
|
|
549
|
+
}
|
|
317
550
|
// Warn after 3 writes without invariants checkin
|
|
318
551
|
if (!p.invariantsDone) {
|
|
319
552
|
p.writesBeforeGate++;
|
|
553
|
+
writeState();
|
|
320
554
|
if (p.writesBeforeGate > 3) {
|
|
321
555
|
throw new Error(`[parallax] PROTOCOL VIOLATION: 4 Invariants (Step 2) not completed ` +
|
|
322
556
|
`after ${p.writesBeforeGate} writes.\n` +
|
|
@@ -338,16 +572,6 @@ export default {
|
|
|
338
572
|
"tool.execute.after": async (input) => {
|
|
339
573
|
if (!["write", "edit", "apply_patch"].includes(input.tool))
|
|
340
574
|
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
575
|
const s = getFriction();
|
|
352
576
|
if (s.retriesLeft === 0)
|
|
353
577
|
return;
|
|
@@ -373,6 +597,7 @@ export default {
|
|
|
373
597
|
s.retriesLeft = MAX_FRICTION_RETRIES;
|
|
374
598
|
s.lastObservation = null;
|
|
375
599
|
addWrite(sid, fileName, "pass", s.retriesLeft);
|
|
600
|
+
writeState();
|
|
376
601
|
client.app
|
|
377
602
|
.log({
|
|
378
603
|
body: {
|
|
@@ -387,6 +612,7 @@ export default {
|
|
|
387
612
|
s.retriesLeft--;
|
|
388
613
|
s.lastObservation = truncate(result.combined, 2000);
|
|
389
614
|
addWrite(sid, fileName, "fail", s.retriesLeft);
|
|
615
|
+
writeState();
|
|
390
616
|
const lvl = s.retriesLeft === 0 ? "error" : "warn";
|
|
391
617
|
client.app
|
|
392
618
|
.log({
|
|
@@ -421,6 +647,7 @@ export default {
|
|
|
421
647
|
// Initialize trace with session info
|
|
422
648
|
if (currentSessionId) {
|
|
423
649
|
initTrace(currentSessionId, process.cwd(), detectProject());
|
|
650
|
+
writeState();
|
|
424
651
|
}
|
|
425
652
|
}
|
|
426
653
|
// Track agent switches (TAB to change agent in OpenCode TUI)
|
|
@@ -428,108 +655,16 @@ export default {
|
|
|
428
655
|
const props = input.event.properties;
|
|
429
656
|
currentAgentName = props?.agent || null;
|
|
430
657
|
}
|
|
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
658
|
},
|
|
488
659
|
// -----------------------------------------------------------------------
|
|
489
|
-
//
|
|
660
|
+
// Shell environment injection (Phase 2.6)
|
|
490
661
|
// -----------------------------------------------------------------------
|
|
491
|
-
"
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
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(() => { });
|
|
662
|
+
"shell.env": async (input, output) => {
|
|
663
|
+
const m = getMode();
|
|
664
|
+
const s = getFriction();
|
|
665
|
+
output.env.PARALLAX_MODE = m.mode;
|
|
666
|
+
output.env.PARALLAX_SESSION_ID = currentSessionId || "";
|
|
667
|
+
output.env.PARALLAX_FRICTION_RETRIES = String(s.retriesLeft);
|
|
533
668
|
},
|
|
534
669
|
// -----------------------------------------------------------------------
|
|
535
670
|
// System prompt transformation: inject protocol status + mode skill
|
|
@@ -538,12 +673,25 @@ export default {
|
|
|
538
673
|
const m = getMode();
|
|
539
674
|
const s = getFriction();
|
|
540
675
|
const p = getProtocol();
|
|
676
|
+
// Phase 2.5: Multi-agent protocol sharing -- carry state to new agent
|
|
677
|
+
if (currentAgentName) {
|
|
678
|
+
const sys = output.system || (output.system = []);
|
|
679
|
+
sys.push(`\n## PARALLAX AGENT CONTEXT\n` +
|
|
680
|
+
`You are now operating as agent "${currentAgentName}". ` +
|
|
681
|
+
`Parallax protocol state carries over:\n` +
|
|
682
|
+
`- Mode: ${m.mode.toUpperCase()}\n` +
|
|
683
|
+
`- Ambiguity: ${p.ambiguityDone ? "DONE" : "PENDING"}\n` +
|
|
684
|
+
`- Invariants: ${p.invariantsDone ? "DONE" : "PENDING"}\n` +
|
|
685
|
+
`- Gate: ${p.gateDone ? "DONE" : "PENDING"}\n` +
|
|
686
|
+
`- Friction: ${s.retriesLeft} retries remaining`);
|
|
687
|
+
}
|
|
541
688
|
// Build protocol status block
|
|
542
689
|
const statusLines = [];
|
|
543
690
|
const steps = [
|
|
544
691
|
"ambiguity",
|
|
545
692
|
"invariants",
|
|
546
693
|
"gate",
|
|
694
|
+
"design",
|
|
547
695
|
"commit",
|
|
548
696
|
"summary",
|
|
549
697
|
];
|
package/dist/score.d.ts
CHANGED
|
@@ -39,3 +39,19 @@ export declare function readScoreHistory(): ScoreEntry[];
|
|
|
39
39
|
* Compute a simple sparkline representation of scores over time.
|
|
40
40
|
*/
|
|
41
41
|
export declare function sparkline(scores: number[]): string;
|
|
42
|
+
export declare function computeWeeklyReport(history: ScoreEntry[]): {
|
|
43
|
+
weekStart: string;
|
|
44
|
+
avg: number;
|
|
45
|
+
count: number;
|
|
46
|
+
best: number;
|
|
47
|
+
worst: number;
|
|
48
|
+
}[];
|
|
49
|
+
export declare function detectFailurePatterns(trace: ParallaxTrace): {
|
|
50
|
+
file: string;
|
|
51
|
+
failures: number;
|
|
52
|
+
}[];
|
|
53
|
+
export declare function computePerProjectStats(history: ScoreEntry[]): {
|
|
54
|
+
project: string;
|
|
55
|
+
sessions: number;
|
|
56
|
+
avgScore: number;
|
|
57
|
+
}[];
|
package/dist/score.js
CHANGED
|
@@ -158,3 +158,63 @@ export function sparkline(scores) {
|
|
|
158
158
|
const max = Math.max(...scores, 1);
|
|
159
159
|
return scores.map((s) => chars[Math.min(Math.floor((s / max) * (chars.length - 1)), chars.length - 1)]).join("");
|
|
160
160
|
}
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
// Analytics / Phase 5
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
function getISOWeek(dateStr) {
|
|
165
|
+
const d = new Date(dateStr);
|
|
166
|
+
const utc = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()));
|
|
167
|
+
const dayNum = utc.getUTCDay() || 7;
|
|
168
|
+
utc.setUTCDate(utc.getUTCDate() + 4 - dayNum);
|
|
169
|
+
const yearStart = new Date(Date.UTC(utc.getUTCFullYear(), 0, 1));
|
|
170
|
+
const weekNo = Math.ceil((((utc.getTime() - yearStart.getTime()) / 86400000) + 1) / 7);
|
|
171
|
+
return `${utc.getUTCFullYear()}-W${String(weekNo).padStart(2, "0")}`;
|
|
172
|
+
}
|
|
173
|
+
export function computeWeeklyReport(history) {
|
|
174
|
+
const weeks = new Map();
|
|
175
|
+
for (const entry of history) {
|
|
176
|
+
const week = getISOWeek(entry.date);
|
|
177
|
+
if (!weeks.has(week))
|
|
178
|
+
weeks.set(week, []);
|
|
179
|
+
weeks.get(week).push(entry);
|
|
180
|
+
}
|
|
181
|
+
return [...weeks.entries()]
|
|
182
|
+
.map(([week, entries]) => {
|
|
183
|
+
const scores = entries.map((e) => e.score);
|
|
184
|
+
return {
|
|
185
|
+
weekStart: week,
|
|
186
|
+
avg: Math.round(scores.reduce((a, b) => a + b, 0) / scores.length),
|
|
187
|
+
count: entries.length,
|
|
188
|
+
best: Math.max(...scores),
|
|
189
|
+
worst: Math.min(...scores),
|
|
190
|
+
};
|
|
191
|
+
})
|
|
192
|
+
.sort((a, b) => a.weekStart.localeCompare(b.weekStart));
|
|
193
|
+
}
|
|
194
|
+
export function detectFailurePatterns(trace) {
|
|
195
|
+
const grouped = new Map();
|
|
196
|
+
for (const w of trace.writes) {
|
|
197
|
+
if (w.verification === "fail") {
|
|
198
|
+
grouped.set(w.file, (grouped.get(w.file) ?? 0) + 1);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return [...grouped.entries()]
|
|
202
|
+
.map(([file, failures]) => ({ file, failures }))
|
|
203
|
+
.sort((a, b) => b.failures - a.failures);
|
|
204
|
+
}
|
|
205
|
+
export function computePerProjectStats(history) {
|
|
206
|
+
const grouped = new Map();
|
|
207
|
+
for (const entry of history) {
|
|
208
|
+
const key = entry.project ?? "unknown";
|
|
209
|
+
if (!grouped.has(key))
|
|
210
|
+
grouped.set(key, []);
|
|
211
|
+
grouped.get(key).push(entry);
|
|
212
|
+
}
|
|
213
|
+
return [...grouped.entries()]
|
|
214
|
+
.map(([project, entries]) => ({
|
|
215
|
+
project,
|
|
216
|
+
sessions: entries.length,
|
|
217
|
+
avgScore: Math.round(entries.reduce((a, e) => a + e.score, 0) / entries.length),
|
|
218
|
+
}))
|
|
219
|
+
.sort((a, b) => b.sessions - a.sessions);
|
|
220
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|