parallax-opencode 0.2.0 → 0.3.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 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
- return "[parallax] Step 6/6: Summary marked complete. Protocol finished.";
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
- // Chat hooks: detect model for Discord RPC
660
+ // Shell environment injection (Phase 2.6)
490
661
  // -----------------------------------------------------------------------
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(() => { });
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 {};