@magnusekdahl/parallix 1.2.0 → 1.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.
Files changed (127) hide show
  1. package/index.js +190 -185
  2. package/lib/agents/agents.js +737 -838
  3. package/lib/agents/agents.ts +930 -0
  4. package/lib/agents/claude-telemetry.js +149 -154
  5. package/lib/agents/claude-telemetry.ts +233 -0
  6. package/lib/agents/claude.js +121 -131
  7. package/lib/agents/claude.ts +156 -0
  8. package/lib/agents/codex-telemetry.js +176 -164
  9. package/lib/agents/codex-telemetry.ts +205 -0
  10. package/lib/agents/codex.js +168 -201
  11. package/lib/agents/codex.ts +236 -0
  12. package/lib/agents/limit-hit.js +216 -226
  13. package/lib/agents/limit-hit.ts +262 -0
  14. package/lib/agents/mistral-telemetry.js +7 -10
  15. package/lib/agents/mistral-telemetry.ts +44 -0
  16. package/lib/agents/mistral.js +40 -58
  17. package/lib/agents/mistral.ts +84 -0
  18. package/lib/agents/opencode-export.js +126 -127
  19. package/lib/agents/opencode-export.ts +160 -0
  20. package/lib/agents/opencode-telemetry.js +282 -261
  21. package/lib/agents/opencode-telemetry.ts +350 -0
  22. package/lib/agents/opencode.js +263 -305
  23. package/lib/agents/opencode.ts +370 -0
  24. package/lib/agents/stage-telemetry.js +23 -24
  25. package/lib/agents/stage-telemetry.ts +47 -0
  26. package/lib/commands/active.js +502 -579
  27. package/lib/commands/active.ts +665 -0
  28. package/lib/commands/checkpoint.js +98 -74
  29. package/lib/commands/checkpoint.ts +79 -0
  30. package/lib/commands/config.js +61 -31
  31. package/lib/commands/config.ts +47 -0
  32. package/lib/commands/coverage-gate.js +271 -246
  33. package/lib/commands/coverage-gate.ts +362 -0
  34. package/lib/commands/diff.js +140 -119
  35. package/lib/commands/diff.ts +125 -0
  36. package/lib/commands/draft.js +770 -923
  37. package/lib/commands/draft.ts +1021 -0
  38. package/lib/commands/handoff.js +588 -608
  39. package/lib/commands/handoff.ts +694 -0
  40. package/lib/commands/integrate.js +1332 -1463
  41. package/lib/commands/integrate.ts +1688 -0
  42. package/lib/commands/mission-start.js +279 -237
  43. package/lib/commands/mission-start.ts +263 -0
  44. package/lib/commands/rebase.js +606 -582
  45. package/lib/commands/rebase.ts +635 -0
  46. package/lib/commands/repair-handoff.js +181 -179
  47. package/lib/commands/repair-handoff.ts +228 -0
  48. package/lib/commands/resolve-conflict.js +121 -102
  49. package/lib/commands/resolve-conflict.ts +112 -0
  50. package/lib/commands/review.js +3 -4
  51. package/lib/commands/review.ts +14 -0
  52. package/lib/commands/setup-review.js +4 -5
  53. package/lib/commands/setup-review.ts +14 -0
  54. package/lib/commands/setup.js +3 -3
  55. package/lib/commands/setup.ts +2 -0
  56. package/lib/commands/stats-backfill.js +394 -359
  57. package/lib/commands/stats-backfill.ts +418 -0
  58. package/lib/commands/stats.js +1424 -1665
  59. package/lib/commands/stats.ts +2186 -0
  60. package/lib/commands/status.js +219 -200
  61. package/lib/commands/status.ts +219 -0
  62. package/lib/commands/verify.js +3 -1
  63. package/lib/commands/verify.ts +2 -0
  64. package/lib/core/fmt.js +140 -211
  65. package/lib/core/fmt.ts +189 -0
  66. package/lib/core/git.js +121 -102
  67. package/lib/core/git.ts +146 -0
  68. package/lib/core/gitignore.js +59 -106
  69. package/lib/core/gitignore.ts +103 -0
  70. package/lib/core/mission-utils.js +833 -875
  71. package/lib/core/mission-utils.ts +998 -0
  72. package/lib/core/nels.js +112 -127
  73. package/lib/core/nels.ts +189 -0
  74. package/lib/core/persistent-data-migration.js +247 -205
  75. package/lib/core/persistent-data-migration.ts +255 -0
  76. package/lib/core/product-config.js +400 -476
  77. package/lib/core/product-config.ts +518 -0
  78. package/lib/core/runtime-matrix.js +41 -87
  79. package/lib/core/runtime-matrix.ts +84 -0
  80. package/lib/core/spawn-tee.js +152 -192
  81. package/lib/core/spawn-tee.ts +202 -0
  82. package/lib/core/state-map.js +92 -92
  83. package/lib/core/state-map.ts +115 -0
  84. package/lib/core/storage.js +101 -129
  85. package/lib/core/storage.ts +182 -0
  86. package/lib/core/subagent-limit.js +15 -16
  87. package/lib/core/subagent-limit.ts +25 -0
  88. package/lib/core/verification.js +148 -138
  89. package/lib/core/verification.ts +200 -0
  90. package/lib/index.js +142 -59
  91. package/lib/index.ts +134 -0
  92. package/lib/review/rebase.js +142 -145
  93. package/lib/review/rebase.ts +188 -0
  94. package/lib/review/review-adapter.js +99 -87
  95. package/lib/review/review-adapter.ts +198 -0
  96. package/lib/review/review-artifacts.js +486 -629
  97. package/lib/review/review-artifacts.ts +622 -0
  98. package/lib/review/review-commands.js +1220 -1373
  99. package/lib/review/review-commands.ts +1579 -0
  100. package/lib/review/review-events.js +544 -886
  101. package/lib/review/review-events.ts +954 -0
  102. package/lib/review/review-loop.js +945 -1020
  103. package/lib/review/review-loop.ts +1174 -0
  104. package/lib/review/review-polling.js +86 -115
  105. package/lib/review/review-polling.ts +194 -0
  106. package/lib/review/review-prompts.js +154 -189
  107. package/lib/review/review-prompts.ts +182 -0
  108. package/lib/review/review-state.js +246 -225
  109. package/lib/review/review-state.ts +353 -0
  110. package/lib/review/review.js +63 -84
  111. package/lib/review/review.ts +138 -0
  112. package/lib/tools/backlog.js +694 -680
  113. package/lib/tools/backlog.ts +835 -0
  114. package/lib/tools/forgejo.js +1270 -1405
  115. package/lib/tools/forgejo.ts +1793 -0
  116. package/lib/tools/gatekeeper.js +106 -96
  117. package/lib/tools/gatekeeper.ts +124 -0
  118. package/lib/tools/redgreen.js +200 -181
  119. package/lib/tools/redgreen.ts +221 -0
  120. package/lib/tools/sessions.js +58 -52
  121. package/lib/tools/sessions.ts +81 -0
  122. package/lib/tools/setup-review.js +902 -975
  123. package/lib/tools/setup-review.ts +1152 -0
  124. package/package.json +7 -5
  125. package/prompts/draft.md +1 -0
  126. package/prompts/portfolio.md +1 -1
  127. package/px.js +226 -210
@@ -0,0 +1,255 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import * as storage from './storage.js';
4
+
5
+ /** Cached stats module — loaded lazily to break circular dependency with `../commands/stats.js`. */
6
+ let _stats: any = null;
7
+
8
+ /** Lazily load the stats module to break the circular dependency. */
9
+ function getStats(): any {
10
+ if (!_stats) {_stats = require('../commands/stats.js');}
11
+ return _stats;
12
+ }
13
+
14
+ /** Resolve the canonical stats CSV headers. */
15
+ function getStatsHeaders(): string[] {
16
+ return getStats().STATS_HEADERS;
17
+ }
18
+
19
+ const ESSENTIAL_STATS_COLUMNS = ['date', 'mission', 'classification', 'implementer', 'pr_fix_rounds'];
20
+
21
+ /**
22
+ * Validate that a stats row has all essential columns populated.
23
+ * Throws on the first missing column found.
24
+ * @param row - Row object to validate
25
+ * @param rowIndex - Zero-based row index (for error messages)
26
+ * @param filePath - Source file path (for error messages)
27
+ */
28
+ function validateStatsRow(row: Record<string, unknown>, rowIndex: number, filePath: string): void {
29
+ const missing = ESSENTIAL_STATS_COLUMNS.filter(col => !(col in row) || row[col] === '');
30
+ if (missing.length > 0) {
31
+ throw new Error(
32
+ `Malformed telemetry row ${rowIndex + 1} in ${path.resolve(filePath)}: ` +
33
+ `missing essential columns: ${missing.join(', ')}`
34
+ );
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Parse a single CSV line, respecting quoted fields and escaped quotes.
40
+ * @param line - Raw CSV line string
41
+ * @returns Array of parsed field values
42
+ */
43
+ function parseCsvLine(line: string): string[] {
44
+ const values: string[] = [];
45
+ let value = '';
46
+ let quoted = false;
47
+ for (let index = 0; index < line.length; index += 1) {
48
+ const char = line[index];
49
+ if (char === '"') {
50
+ if (quoted && line[index + 1] === '"') {
51
+ value += '"';
52
+ index += 1;
53
+ } else {
54
+ quoted = !quoted;
55
+ }
56
+ } else if (char === ',' && !quoted) {
57
+ values.push(value);
58
+ value = '';
59
+ } else {
60
+ value += char;
61
+ }
62
+ }
63
+ values.push(value);
64
+ return values;
65
+ }
66
+
67
+ /**
68
+ * Escape a single CSV value — wraps in quotes if it contains commas, double-quotes, or newlines.
69
+ * @param value - Value to escape
70
+ * @returns Escaped CSV-safe string
71
+ */
72
+ function escapeCsvValue(value: unknown): string {
73
+ const text = String(value ?? '');
74
+ return /[",\n]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text;
75
+ }
76
+
77
+ /**
78
+ * Minimal CSV loader — avoids circular dependency with stats module (which
79
+ * imports this module at the top). Defined locally so `migrateStats()` can
80
+ * read any schema without triggering a require-cycle.
81
+ */
82
+ function loadCsv(filePath: string): { headers: string[]; rows: Record<string, unknown>[] } {
83
+ if (!fs.existsSync(filePath)) {
84
+ return { headers: [], rows: [] };
85
+ }
86
+ const content = fs.readFileSync(filePath, 'utf8');
87
+ const lines = content.split('\n').filter(line => line.trim());
88
+ if (lines.length === 0) {
89
+ return { headers: [], rows: [] };
90
+ }
91
+ const headers = parseCsvLine(lines[0]);
92
+ const rows: Record<string, unknown>[] = [];
93
+ for (let i = 1; i < lines.length; i += 1) {
94
+ const values = parseCsvLine(lines[i]);
95
+ const row: Record<string, unknown> = {};
96
+ headers.forEach((header, idx) => {
97
+ row[header] = values[idx] || '';
98
+ });
99
+ rows.push(row);
100
+ }
101
+ return { headers, rows };
102
+ }
103
+
104
+ /**
105
+ * Read a stats CSV file with any schema (legacy 5-col or extended 21-col).
106
+ * Returns rows as arrays keyed by the 21-column STATS_HEADERS, with missing
107
+ * columns filled by normalizeStatsRow defaults.
108
+ */
109
+ function readStatsRows(filePath: string, options: { defaultRepo?: string } = {}): string[][] {
110
+ if (!filePath || !fs.existsSync(filePath)) {return [];}
111
+ const data = loadCsv(filePath);
112
+ if (data.headers.length === 0) {return [];}
113
+ const headers = getStatsHeaders();
114
+ const opts = options;
115
+ const validated = data.rows.map((row, idx) => {
116
+ validateStatsRow(row, idx, filePath);
117
+ return row;
118
+ });
119
+ return validated
120
+ .map(row => getStats().normalizeStatsRow(row, { repo: opts.defaultRepo }))
121
+ .map((row: Record<string, unknown>) => headers.map((h: string) => String(row[h] || '')));
122
+ }
123
+
124
+ /**
125
+ * Serialize an array of stats rows back to CSV format with canonical headers.
126
+ * @param rows - Array of row arrays (each row is an array of string values)
127
+ * @returns CSV string with headers and data rows
128
+ */
129
+ function serializeStatsRows(rows: string[][]): string {
130
+ const headers = getStatsHeaders();
131
+ return `${[
132
+ headers.join(','),
133
+ ...rows.map(row => row.map(escapeCsvValue).join(','))
134
+ ].join('\n')}\n`;
135
+ }
136
+
137
+ /**
138
+ * Merge stats from one or more source CSV files into the destination file.
139
+ * Deduplicates full rows. Creates the destination directory if needed.
140
+ * When no source data is available, returns early without writing a header-only file.
141
+ * @param options - Migration configuration (sourcePaths, sourcePath, destinationPath)
142
+ * @returns Result with destination path, import counts, and optional warning
143
+ */
144
+ function migrateStats(options: { sourcePaths?: string[]; sourcePath?: string; destinationPath?: string } = {}): { destinationPath: string; imported: number; rows: number; warn?: string } {
145
+ const opts = options;
146
+ const sourcePaths = (opts.sourcePaths || [opts.sourcePath].filter(Boolean)) as string[];
147
+ const destinationPath = opts.destinationPath || storage.resolveStatsPath({ ensureDir: true });
148
+ const sourceRows = sourcePaths.flatMap((sourcePath: string) => readStatsRows(sourcePath, {}));
149
+
150
+ // Fresh-install guard: when no source file exists or all sources are empty,
151
+ // there is no telemetry to import. Returning early prevents writing a
152
+ // header-only destination file that would masquerade as valid stats.
153
+ if (sourceRows.length === 0) {
154
+ return { destinationPath, imported: 0, rows: 0, warn: 'no source data available' };
155
+ }
156
+
157
+ const destinationRows = readStatsRows(destinationPath, {});
158
+ const rows: string[][] = [];
159
+ const seen = new Set<string>();
160
+
161
+ for (const row of [...destinationRows, ...sourceRows]) {
162
+ const key = JSON.stringify(row);
163
+ if (!seen.has(key)) {
164
+ seen.add(key);
165
+ rows.push(row);
166
+ }
167
+ }
168
+
169
+ const content = serializeStatsRows(rows);
170
+ const current = fs.existsSync(destinationPath) ? fs.readFileSync(destinationPath, 'utf8') : null;
171
+ if (current !== content) {storage.writeFileAtomic(destinationPath, content);}
172
+ return { destinationPath, imported: rows.length - destinationRows.length, rows: rows.length };
173
+ }
174
+
175
+ interface BlocklistSource { filePath: string; payload: Record<string, unknown>; blocklist: Record<string, unknown>; }
176
+
177
+ function readBlocklistSource(filePath: string, warn: (...args: unknown[]) => void, hardFailure = false): BlocklistSource | null {
178
+ if (!filePath || !fs.existsSync(filePath)) {return null;}
179
+ try {
180
+ const payload = JSON.parse(fs.readFileSync(filePath, 'utf8')) as Record<string, unknown>;
181
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
182
+ throw new Error('expected a JSON object at the file root');
183
+ }
184
+ if (
185
+ payload.blocklist !== undefined &&
186
+ (!payload.blocklist || typeof payload.blocklist !== 'object' || Array.isArray(payload.blocklist))
187
+ ) {
188
+ throw new Error('expected blocklist to be a JSON object');
189
+ }
190
+ return { filePath, payload, blocklist: (payload.blocklist as Record<string, unknown>) || {} };
191
+ } catch (error) {
192
+ if (hardFailure) {throw error;}
193
+ warn(`Skipping malformed legacy agent blocklist ${path.resolve(filePath)}: ${(error as Error).message}`);
194
+ return null;
195
+ }
196
+ }
197
+
198
+ function sameValue(left: unknown, right: unknown): boolean {
199
+ return JSON.stringify(left) === JSON.stringify(right);
200
+ }
201
+
202
+ /**
203
+ * Merge agent blocklists from legacy sources into the destination file.
204
+ * Reports conflicts via `warn` when the same agent appears in multiple sources
205
+ * with different values. Existing destination entries take lowest precedence.
206
+ * @param options - Migration configuration (warn callback, destinationPath, sourcePaths)
207
+ * @returns Result with destination path, merged blocklist, and conflict details
208
+ */
209
+ function migrateAgentBlocklists(options: { warn?: (...args: unknown[]) => void; destinationPath?: string; sourcePaths?: string[] } = {}): { destinationPath: string; blocklist: Record<string, unknown>; conflicts: unknown[] } {
210
+ const opts = options;
211
+ const warn = opts.warn || (() => {});
212
+ const destinationPath = opts.destinationPath || storage.resolveAgentsLocalPath({ ensureDir: true });
213
+ const sources = (opts.sourcePaths || [])
214
+ .map(filePath => readBlocklistSource(filePath, warn))
215
+ .filter(Boolean) as BlocklistSource[];
216
+ const destination = readBlocklistSource(destinationPath, warn, true);
217
+ const selected: Record<string, unknown> = {};
218
+ const selectedFrom: Record<string, string> = {};
219
+ const conflicts: unknown[] = [];
220
+
221
+ for (const s of [...sources, ...(destination ? [destination] : [])]) {
222
+ if (!s) {continue;}
223
+ for (const [agent, value] of Object.entries(s.blocklist)) {
224
+ if (Object.prototype.hasOwnProperty.call(selected, agent) && !sameValue(selected[agent], value)) {
225
+ const conflict = {
226
+ agent,
227
+ previousSource: selectedFrom[agent],
228
+ previousValue: selected[agent],
229
+ selectedSource: s.filePath,
230
+ selectedValue: value
231
+ };
232
+ conflicts.push(conflict);
233
+ warn(
234
+ `Agent blocklist conflict for "${agent}": ${path.resolve(s.filePath)} takes precedence over ` +
235
+ `${path.resolve(selectedFrom[agent])}; selected=${JSON.stringify(value)} previous=${JSON.stringify(selected[agent])}`
236
+ );
237
+ }
238
+ selected[agent] = value;
239
+ selectedFrom[agent] = s.filePath;
240
+ }
241
+ }
242
+
243
+ const payload: Record<string, unknown> = destination ? { ...destination.payload } : {};
244
+ payload.blocklist = selected;
245
+ const content = `${JSON.stringify(payload, null, 2)}\n`;
246
+ const current = fs.existsSync(destinationPath) ? fs.readFileSync(destinationPath, 'utf8') : null;
247
+ if (current !== content) {storage.writeFileAtomic(destinationPath, content);}
248
+ return { destinationPath, blocklist: selected, conflicts };
249
+ }
250
+
251
+ export {
252
+ migrateStats,
253
+ migrateAgentBlocklists
254
+ };
255
+ export const _internals = { parseCsvLine, readStatsRows, serializeStatsRows };