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/cli.js CHANGED
@@ -19,7 +19,7 @@
19
19
  import { existsSync, mkdirSync, writeFileSync } from "fs";
20
20
  import { join } from "path";
21
21
  import { listTraceFiles, loadTrace, exportTrace, } from "./trace";
22
- import { computeCoherenceScore, formatScoreBreakdown, readScoreHistory, sparkline, scoreToGrade, recordScore, } from "./score";
22
+ import { computeCoherenceScore, formatScoreBreakdown, readScoreHistory, sparkline, scoreToGrade, recordScore, computeWeeklyReport, } from "./score";
23
23
  // ---------------------------------------------------------------------------
24
24
  // Constants
25
25
  // ---------------------------------------------------------------------------
@@ -39,6 +39,11 @@ function showHelp() {
39
39
  console.log(` trace score <id> Show coherence score`);
40
40
  console.log(` trace export <id> Export trace to JSON file`);
41
41
  console.log(` trace trend Show score trend over time`);
42
+ console.log(` trace report --week Show weekly score report`);
43
+ console.log(` trace compare <a> <b> Side-by-side comparison of two traces`);
44
+ console.log(` trace compliance <id> Protocol compliance report`);
45
+ console.log(` gate [--session <id>] [--last] [--min-score <n>] Gate by coherence score`);
46
+ console.log(` pre-commit Pre-commit hook (runs gate --last --min-score 70)`);
42
47
  console.log(` help Show this help`);
43
48
  }
44
49
  function showVersion() {
@@ -162,6 +167,220 @@ async function cmdTraceTrend() {
162
167
  }
163
168
  return 0;
164
169
  }
170
+ async function cmdTraceReport() {
171
+ const history = readScoreHistory();
172
+ if (history.length === 0) {
173
+ console.log("No score history found.");
174
+ return 0;
175
+ }
176
+ const weeks = computeWeeklyReport(history);
177
+ console.log(`Weekly Report (${weeks.length} weeks):`);
178
+ for (const w of weeks) {
179
+ console.log(` ${w.weekStart}: avg ${w.avg}/100 (${w.count} sessions, best: ${w.best}, worst: ${w.worst})`);
180
+ }
181
+ return 0;
182
+ }
183
+ async function cmdTraceCompare(a, b) {
184
+ const traceA = loadTrace(a);
185
+ const traceB = loadTrace(b);
186
+ if (!traceA) {
187
+ console.error(`Trace not found: ${a}`);
188
+ return 1;
189
+ }
190
+ if (!traceB) {
191
+ console.error(`Trace not found: ${b}`);
192
+ return 1;
193
+ }
194
+ const scoreA = computeCoherenceScore(traceA);
195
+ const scoreB = computeCoherenceScore(traceB);
196
+ const passRate = (trace) => {
197
+ const known = trace.writes.filter((w) => w.verification !== "unknown");
198
+ if (known.length === 0)
199
+ return "N/A";
200
+ const passes = known.filter((w) => w.verification === "pass").length;
201
+ return `${Math.round((passes / known.length) * 100)}%`;
202
+ };
203
+ const frictionRetries = (trace) => trace.writes.reduce((sum, w) => sum + (3 - w.frictionRetriesLeft), 0);
204
+ const rows = [
205
+ [
206
+ "Coherence Score",
207
+ `${scoreA.total}/100 (${scoreToGrade(scoreA.total)})`,
208
+ `${scoreB.total}/100 (${scoreToGrade(scoreB.total)})`,
209
+ `${scoreB.total - scoreA.total >= 0 ? "+" : ""}${scoreB.total - scoreA.total}`,
210
+ ],
211
+ [
212
+ "Protocol Coverage",
213
+ `${scoreA.protocolCoverage}/30`,
214
+ `${scoreB.protocolCoverage}/30`,
215
+ `${scoreB.protocolCoverage - scoreA.protocolCoverage >= 0 ? "+" : ""}${scoreB.protocolCoverage - scoreA.protocolCoverage}`,
216
+ ],
217
+ [
218
+ "Verification Pass %",
219
+ passRate(traceA),
220
+ passRate(traceB),
221
+ "",
222
+ ],
223
+ [
224
+ "Writes",
225
+ `${traceA.writes.length}`,
226
+ `${traceB.writes.length}`,
227
+ `${traceB.writes.length - traceA.writes.length >= 0 ? "+" : ""}${traceB.writes.length - traceA.writes.length}`,
228
+ ],
229
+ [
230
+ "Friction Retries",
231
+ `${frictionRetries(traceA)}`,
232
+ `${frictionRetries(traceB)}`,
233
+ `${frictionRetries(traceB) - frictionRetries(traceA) >= 0 ? "+" : ""}${frictionRetries(traceB) - frictionRetries(traceA)}`,
234
+ ],
235
+ ];
236
+ const colWidths = [22, 17, 17, 7];
237
+ const pad = (text, width) => text.padEnd(width);
238
+ console.log("Trace Comparison:");
239
+ console.log(` ${pad("Metric", colWidths[0])} | ${pad("Session A", colWidths[1])} | ${pad("Session B", colWidths[2])} | Delta`);
240
+ console.log(` ${"-".repeat(colWidths[0])}-+-${"-".repeat(colWidths[1])}-+-${"-".repeat(colWidths[2])}-+-------`);
241
+ for (const row of rows) {
242
+ console.log(` ${pad(row[0], colWidths[0])} | ${pad(row[1], colWidths[1])} | ${pad(row[2], colWidths[2])} | ${row[3]}`);
243
+ }
244
+ return 0;
245
+ }
246
+ async function cmdTraceCompliance(id) {
247
+ const trace = loadTrace(id);
248
+ if (!trace) {
249
+ console.error(`Trace not found: ${id}`);
250
+ return 1;
251
+ }
252
+ const REQUIRED_PHASES = [
253
+ "ambiguity_check",
254
+ "four_invariants",
255
+ "verification_gate",
256
+ "commit_decision",
257
+ "summary",
258
+ ];
259
+ const DISPLAY_NAMES = {
260
+ ambiguity_check: "Ambiguity Check",
261
+ four_invariants: "4 Invariants",
262
+ verification_gate: "Verification Gate",
263
+ commit_decision: "Commit Decision",
264
+ summary: "Summary",
265
+ };
266
+ const phaseByName = new Map(trace.phases.map((p) => [p.phase, p]));
267
+ const invariantsPhase = phaseByName.get("four_invariants");
268
+ // Detect violations: writes before invariants completed
269
+ const violations = [];
270
+ if (invariantsPhase) {
271
+ const invariantsTime = new Date(invariantsPhase.timestamp).getTime();
272
+ const writesBefore = trace.writes.filter((w) => new Date(w.timestamp).getTime() < invariantsTime);
273
+ if (writesBefore.length > 0) {
274
+ const fileList = writesBefore.map((w) => w.file).join(", ");
275
+ violations.push(`${writesBefore.length} writes without invariants checkin (files: ${fileList})`);
276
+ }
277
+ }
278
+ else {
279
+ violations.push(`${trace.writes.length} writes without invariants checkin (files: ${trace.writes.map((w) => w.file).join(", ")})`);
280
+ }
281
+ console.log(`Protocol Compliance: ${id}`);
282
+ for (const phase of REQUIRED_PHASES) {
283
+ const record = phaseByName.get(phase);
284
+ const name = DISPLAY_NAMES[phase] ?? phase;
285
+ if (record) {
286
+ console.log(` [PASS] ${name.padEnd(20)} - completed at ${record.timestamp}`);
287
+ }
288
+ else {
289
+ console.log(` [FAIL] ${name.padEnd(20)} - not completed`);
290
+ }
291
+ }
292
+ if (violations.length > 0) {
293
+ console.log("");
294
+ console.log(" Violations:");
295
+ for (const v of violations) {
296
+ console.log(` - ${v}`);
297
+ }
298
+ }
299
+ return 0;
300
+ }
301
+ async function cmdGate() {
302
+ const args = process.argv.slice(2);
303
+ let minScore = 70;
304
+ let sessionId = null;
305
+ for (let i = 1; i < args.length; i++) {
306
+ if (args[i] === "--min-score" && args[i + 1]) {
307
+ const val = parseInt(args[i + 1], 10);
308
+ if (!isNaN(val) && val >= 0 && val <= 100) {
309
+ minScore = val;
310
+ i++;
311
+ }
312
+ }
313
+ else if (args[i] === "--session" && args[i + 1]) {
314
+ sessionId = args[i + 1];
315
+ i++;
316
+ }
317
+ else if (args[i] === "--last") {
318
+ // --last is the default behavior
319
+ }
320
+ }
321
+ if (sessionId) {
322
+ const trace = loadTrace(sessionId);
323
+ if (!trace) {
324
+ console.error(`Trace not found: ${sessionId}`);
325
+ return 1;
326
+ }
327
+ const score = computeCoherenceScore(trace);
328
+ console.log(`Session: ${sessionId}`);
329
+ console.log(`Coherence Score: ${score.total}/100 (${scoreToGrade(score.total)})`);
330
+ console.log(`Threshold: ${minScore}/100`);
331
+ if (score.total >= minScore) {
332
+ console.log(`Result: PASS`);
333
+ return 0;
334
+ }
335
+ console.log(`Result: FAIL`);
336
+ return 1;
337
+ }
338
+ const files = listTraceFiles();
339
+ if (files.length === 0) {
340
+ console.error("No traces found");
341
+ return 2;
342
+ }
343
+ files.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
344
+ const mostRecent = files[0];
345
+ const trace = loadTrace(mostRecent.sessionId);
346
+ if (!trace) {
347
+ console.error(`Failed to load trace: ${mostRecent.sessionId}`);
348
+ return 1;
349
+ }
350
+ const score = computeCoherenceScore(trace);
351
+ console.log(`Session: ${mostRecent.sessionId}`);
352
+ console.log(`Coherence Score: ${score.total}/100 (${scoreToGrade(score.total)})`);
353
+ console.log(`Threshold: ${minScore}/100`);
354
+ if (score.total >= minScore) {
355
+ console.log(`Result: PASS`);
356
+ return 0;
357
+ }
358
+ console.log(`Result: FAIL`);
359
+ return 1;
360
+ }
361
+ async function cmdPreCommit() {
362
+ const gitDir = join(process.cwd(), ".git");
363
+ if (!existsSync(gitDir)) {
364
+ console.log("Parallax pre-commit: skipped (not in a git repository)");
365
+ return 0;
366
+ }
367
+ const files = listTraceFiles();
368
+ if (files.length === 0) {
369
+ console.log("Parallax pre-commit: skipped (no traces found)");
370
+ return 0;
371
+ }
372
+ files.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
373
+ const mostRecent = files[0];
374
+ const trace = loadTrace(mostRecent.sessionId);
375
+ if (!trace) {
376
+ console.log("Parallax pre-commit: skipped (failed to load trace)");
377
+ return 0;
378
+ }
379
+ const score = computeCoherenceScore(trace);
380
+ const pass = score.total >= 70;
381
+ console.log(`Parallax gate: score ${score.total}/100 (${pass ? "PASS" : "FAIL"})`);
382
+ return pass ? 0 : 1;
383
+ }
165
384
  // ---------------------------------------------------------------------------
166
385
  // Command routing
167
386
  // ---------------------------------------------------------------------------
@@ -208,12 +427,30 @@ export async function main() {
208
427
  return cmdTraceExport(args[2]);
209
428
  case "trend":
210
429
  return cmdTraceTrend();
430
+ case "report":
431
+ return cmdTraceReport();
432
+ case "compare":
433
+ if (!args[2] || !args[3]) {
434
+ console.error("Usage: parallax trace compare <session-a> <session-b>");
435
+ return 1;
436
+ }
437
+ return cmdTraceCompare(args[2], args[3]);
438
+ case "compliance":
439
+ if (!args[2]) {
440
+ console.error("Usage: parallax trace compliance <session-id>");
441
+ return 1;
442
+ }
443
+ return cmdTraceCompliance(args[2]);
211
444
  default:
212
445
  console.error(`Unknown trace command: ${sub}`);
213
- console.error("Usage: parallax trace <list|show|score|export|trend>");
446
+ console.error("Usage: parallax trace <list|show|score|export|trend|report|compare|compliance>");
214
447
  return 1;
215
448
  }
216
449
  }
450
+ case "gate":
451
+ return cmdGate();
452
+ case "pre-commit":
453
+ return cmdPreCommit();
217
454
  default:
218
455
  console.error(`Unknown command: ${cmd}`);
219
456
  console.error("Run 'parallax help' for usage.");
package/dist/plugin.d.ts CHANGED
@@ -60,6 +60,16 @@ declare const _default: {
60
60
  pretty?: boolean | undefined;
61
61
  }, context: import("@opencode-ai/plugin").ToolContext): Promise<import("@opencode-ai/plugin").ToolResult>;
62
62
  };
63
+ parallax_trace_pr_comment: {
64
+ description: string;
65
+ args: {};
66
+ execute(args: Record<string, never>, context: import("@opencode-ai/plugin").ToolContext): Promise<import("@opencode-ai/plugin").ToolResult>;
67
+ };
68
+ parallax_trace_view: {
69
+ description: string;
70
+ args: {};
71
+ execute(args: Record<string, never>, context: import("@opencode-ai/plugin").ToolContext): Promise<import("@opencode-ai/plugin").ToolResult>;
72
+ };
63
73
  };
64
74
  "tool.execute.before": (input: {
65
75
  tool: string;
@@ -74,19 +84,11 @@ declare const _default: {
74
84
  properties?: Record<string, unknown>;
75
85
  };
76
86
  }) => Promise<void>;
77
- "chat.message": (input: {
78
- sessionID: string;
79
- agent?: string;
80
- model?: {
81
- modelID?: string;
82
- };
83
- }) => Promise<void>;
84
- "chat.params": (input: {
85
- sessionID: string;
86
- agent: string;
87
- model: {
88
- id: string;
89
- };
87
+ "shell.env": (input: {
88
+ cwd: string;
89
+ sessionID?: string;
90
+ }, output: {
91
+ env: Record<string, string>;
90
92
  }) => Promise<void>;
91
93
  "experimental.chat.system.transform": (_input: unknown, output: {
92
94
  system?: string[];