@magnusekdahl/parallix 1.0.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 (123) hide show
  1. package/CHANGELOG.md +140 -0
  2. package/LICENSE +661 -0
  3. package/README.md +196 -0
  4. package/config/agents.json +25 -0
  5. package/config/agents.local.json.template +8 -0
  6. package/config/state-map.json +4 -0
  7. package/config/state-map.json.template +31 -0
  8. package/config/workflow.config.schema.json +98 -0
  9. package/data/.gitkeep +0 -0
  10. package/docs/adr/0031-ai-agent-instruction-boundary-and-command-floor.md +114 -0
  11. package/docs/adr/0032-mission-refinement-state-and-usage-budget-signals.md +135 -0
  12. package/docs/adr/0034-module-and-skill-invocation-model.md +202 -0
  13. package/docs/adr/0036-mission-sizing-and-dependency-wave-heuristics.md +79 -0
  14. package/docs/adr/0037-ai-workflow-coordination-architecture.md +162 -0
  15. package/docs/adr/0041-integration-pipeline-gates.md +165 -0
  16. package/docs/adr/0042-workflow-cli-color-rendering-approach.md +106 -0
  17. package/docs/adr/0043-git-target-resolution-strategy.md +185 -0
  18. package/docs/adr/0044-workflow-distribution-model.md +277 -0
  19. package/docs/adr/0045-parallax-branch-model.md +182 -0
  20. package/docs/adr/0046-npm-publish-process-and-security.md +138 -0
  21. package/docs/adr/index.md +20 -0
  22. package/docs/agents.md +212 -0
  23. package/docs/authority-reference.md +298 -0
  24. package/docs/forgejo-setup.md +31 -0
  25. package/docs/migration/extraction.md +61 -0
  26. package/docs/migration/task-classification.md +36 -0
  27. package/docs/operator-setup.md +76 -0
  28. package/docs/readme-rewrite-benchmark.md +188 -0
  29. package/docs/use-cases.md +105 -0
  30. package/examples/README.md +62 -0
  31. package/examples/run-enterprise-tarball-workflow-smoke.sh +257 -0
  32. package/examples/run-verify-env-smoke.sh +40 -0
  33. package/index.js +250 -0
  34. package/lib/README.md +13 -0
  35. package/lib/agents/agents.js +867 -0
  36. package/lib/agents/claude-telemetry.js +233 -0
  37. package/lib/agents/claude.js +139 -0
  38. package/lib/agents/codex-telemetry.js +202 -0
  39. package/lib/agents/codex.js +219 -0
  40. package/lib/agents/limit-hit.js +252 -0
  41. package/lib/agents/mistral-telemetry.js +44 -0
  42. package/lib/agents/mistral.js +68 -0
  43. package/lib/agents/opencode-export.js +110 -0
  44. package/lib/agents/opencode-telemetry.js +356 -0
  45. package/lib/agents/opencode.js +218 -0
  46. package/lib/agents/stage-telemetry.js +37 -0
  47. package/lib/commands/active.js +625 -0
  48. package/lib/commands/checkpoint.js +76 -0
  49. package/lib/commands/config.js +39 -0
  50. package/lib/commands/coverage-gate.js +358 -0
  51. package/lib/commands/diff.js +119 -0
  52. package/lib/commands/draft.js +854 -0
  53. package/lib/commands/handoff.js +501 -0
  54. package/lib/commands/integrate.js +1528 -0
  55. package/lib/commands/mission-start.js +246 -0
  56. package/lib/commands/rebase.js +597 -0
  57. package/lib/commands/repair-handoff.js +227 -0
  58. package/lib/commands/resolve-conflict.js +109 -0
  59. package/lib/commands/review.js +13 -0
  60. package/lib/commands/setup-review.js +13 -0
  61. package/lib/commands/setup.js +3 -0
  62. package/lib/commands/stats-backfill.js +395 -0
  63. package/lib/commands/stats.js +1601 -0
  64. package/lib/commands/status.js +183 -0
  65. package/lib/commands/verify.js +1 -0
  66. package/lib/core/fmt.js +202 -0
  67. package/lib/core/git.js +73 -0
  68. package/lib/core/gitignore.js +110 -0
  69. package/lib/core/mission-utils.js +1017 -0
  70. package/lib/core/persistent-data-migration.js +201 -0
  71. package/lib/core/product-config.js +508 -0
  72. package/lib/core/runtime-matrix.js +82 -0
  73. package/lib/core/spawn-tee.js +173 -0
  74. package/lib/core/state-map.js +89 -0
  75. package/lib/core/storage.js +165 -0
  76. package/lib/core/verification.js +149 -0
  77. package/lib/index.js +77 -0
  78. package/lib/review/rebase.js +163 -0
  79. package/lib/review/review-adapter.js +135 -0
  80. package/lib/review/review-artifacts.js +619 -0
  81. package/lib/review/review-commands.js +1375 -0
  82. package/lib/review/review-events.js +1007 -0
  83. package/lib/review/review-loop.js +1004 -0
  84. package/lib/review/review-polling.js +141 -0
  85. package/lib/review/review-prompts.js +212 -0
  86. package/lib/review/review-state.js +280 -0
  87. package/lib/review/review.js +96 -0
  88. package/lib/tools/backlog.js +680 -0
  89. package/lib/tools/forgejo.js +1585 -0
  90. package/lib/tools/gatekeeper.js +106 -0
  91. package/lib/tools/sessions.js +74 -0
  92. package/lib/tools/setup-review.js +1053 -0
  93. package/package.json +56 -0
  94. package/prompts/act-on-review-verbose.md +20 -0
  95. package/prompts/act-on-review.md +22 -0
  96. package/prompts/draft.md +20 -0
  97. package/prompts/execute.md +24 -0
  98. package/prompts/portfolio.md +30 -0
  99. package/prompts/review-verbose.md +20 -0
  100. package/prompts/review.md +17 -0
  101. package/px.js +236 -0
  102. package/templates/AGENTS-snippet.md +14 -0
  103. package/templates/AGENTS.md.template +34 -0
  104. package/templates/CLAUDE.md.template +27 -0
  105. package/templates/CODEX.md.template +38 -0
  106. package/templates/MISTRAL.md.template +24 -0
  107. package/templates/claude-commands/act-on-review.md +3 -0
  108. package/templates/claude-commands/area-review.md +3 -0
  109. package/templates/claude-commands/draft.md +6 -0
  110. package/templates/claude-commands/execute.md +6 -0
  111. package/templates/claude-commands/integrate.md +4 -0
  112. package/templates/claude-commands/portfolio.md +5 -0
  113. package/templates/claude-commands/review.md +4 -0
  114. package/templates/codex/config.toml +6 -0
  115. package/templates/mission-scaffold.md +39 -0
  116. package/templates/vibe/skills/act-on-review/SKILL.md +16 -0
  117. package/templates/vibe/skills/area-review/SKILL.md +16 -0
  118. package/templates/vibe/skills/draft/SKILL.md +16 -0
  119. package/templates/vibe/skills/execute/SKILL.md +16 -0
  120. package/templates/vibe/skills/integrate/SKILL.md +16 -0
  121. package/templates/vibe/skills/portfolio/SKILL.md +21 -0
  122. package/templates/vibe/skills/review/SKILL.md +16 -0
  123. package/tools/setup-forgejo-docker.sh +84 -0
@@ -0,0 +1,395 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ const fmt = require('../core/fmt');
5
+ const stats = require('./stats');
6
+ const gitLib = require('../core/git');
7
+ const {
8
+ getTaskFrontmatterValue,
9
+ getTaskStatus,
10
+ resolveTaskFile,
11
+ } = require('../tools/backlog');
12
+ const { findMissionDir } = require('../core/mission-utils');
13
+
14
+ function hasCheckpointFiles(missionDir) {
15
+ if (!missionDir || !fs.existsSync(missionDir)) return false;
16
+ return fs.readdirSync(missionDir).some(file => /^CP-\d+\.md$/i.test(file) || /^CHECKPOINT_FINAL\.md$/i.test(file));
17
+ }
18
+
19
+ function listHistoricalMissionSlugs(rootDir = process.cwd()) {
20
+ const missionsRoot = path.join(rootDir, 'docs', 'missions');
21
+ if (!fs.existsSync(missionsRoot)) return [];
22
+
23
+ const slugs = [];
24
+ for (const year of fs.readdirSync(missionsRoot).sort()) {
25
+ const yearDir = path.join(missionsRoot, year);
26
+ if (!/^\d{4}$/.test(year) || !fs.statSync(yearDir).isDirectory()) continue;
27
+ for (const slug of fs.readdirSync(yearDir).sort()) {
28
+ if (!/^task-\d+/i.test(slug)) continue;
29
+ const missionDir = path.join(yearDir, slug);
30
+ if (!fs.statSync(missionDir).isDirectory()) continue;
31
+ if (!hasCheckpointFiles(missionDir)) continue;
32
+ slugs.push(slug);
33
+ }
34
+ }
35
+ return slugs;
36
+ }
37
+
38
+ function extractDateOnly(value) {
39
+ const match = String(value || '').match(/^(\d{4}-\d{2}-\d{2})/);
40
+ return match ? match[1] : null;
41
+ }
42
+
43
+ function normalizeHistoricalImplementer(value) {
44
+ const normalized = String(value || '').trim().toLowerCase();
45
+ if (!normalized) return null;
46
+ if (/(^|[^a-z])codex([^a-z]|$)/.test(normalized)) return 'codex';
47
+ if (/(^|[^a-z])claude([^a-z]|$)/.test(normalized)) return 'claude';
48
+ if (/(^|[^a-z])gemini([^a-z]|$)/.test(normalized)) return 'gemini';
49
+ if (/(^|[^a-z])qwen([^a-z]|$)/.test(normalized)) return 'qwen';
50
+ if (/(^|[^a-z])mistral([^a-z]|$)/.test(normalized)) return 'mistral';
51
+ if (/(^|[^a-z])magnus([^a-z]|$)/.test(normalized)) return 'magnus';
52
+ if (/(^|[^a-z])human([^a-z]|$)/.test(normalized)) return 'human';
53
+ return null;
54
+ }
55
+
56
+ function deriveImplementerFromGitHistory(slug, taskFile, rootDir = process.cwd()) {
57
+ const missionDir = findMissionDir(slug, rootDir);
58
+ const targets = [missionDir, taskFile].filter(Boolean);
59
+ if (targets.length === 0) return null;
60
+
61
+ const result = gitLib.git(['-C', rootDir, 'log', '--all', '--format=%an|%ae', '--', ...targets]);
62
+ if (result.status !== 0) return null;
63
+
64
+ const authors = result.stdout
65
+ .split('\n')
66
+ .map(line => line.trim())
67
+ .filter(Boolean)
68
+ .map(line => {
69
+ const [name = '', email = ''] = line.split('|');
70
+ return normalizeHistoricalImplementer(email) || normalizeHistoricalImplementer(name);
71
+ })
72
+ .filter(Boolean);
73
+
74
+ if (authors.length === 0) return null;
75
+ const uniqueAuthors = [...new Set(authors)];
76
+ return uniqueAuthors.length === 1 ? uniqueAuthors[0] : uniqueAuthors[0];
77
+ }
78
+
79
+ function deriveDateFromGitHistory(slug, taskFile, rootDir = process.cwd()) {
80
+ const missionDir = findMissionDir(slug, rootDir);
81
+ const targets = [missionDir, taskFile].filter(Boolean);
82
+ if (targets.length === 0) return null;
83
+
84
+ const result = gitLib.git(['-C', rootDir, 'log', '--all', '-1', '--format=%cs', '--', ...targets]);
85
+ if (result.status !== 0) return null;
86
+ return extractDateOnly(result.stdout.trim());
87
+ }
88
+
89
+ function inferHistoricalClassificationFromMissionDoc(slug, rootDir = process.cwd()) {
90
+ const missionDir = findMissionDir(slug, rootDir);
91
+ if (!missionDir) return null;
92
+
93
+ const missionFile = path.join(missionDir, 'MISSION.md');
94
+ if (!fs.existsSync(missionFile)) return null;
95
+ const text = fs.readFileSync(missionFile, 'utf8').toLowerCase();
96
+
97
+ const productSurfacePatterns = [
98
+ /web-client\//g,
99
+ /\bios\b/g,
100
+ /\bandroid\b/g,
101
+ /\bwearos\b/g,
102
+ /\bauth-server\b/g,
103
+ /\bserver\/src\b/g,
104
+ /\bgrocer(?:y|ies)\b/g,
105
+ /\bboard\b/g,
106
+ /\bdelta\b/g,
107
+ /\bsync\b/g,
108
+ /\bprice\b/g,
109
+ /\bstore\b/g,
110
+ /\bshopping\b/g,
111
+ /\bui\b/g,
112
+ /\bclient\b/g,
113
+ /\bcheapest\b/g,
114
+ ];
115
+ const workflowPatterns = [
116
+ /workflow\//g,
117
+ /node parallix\/index\.js/g,
118
+ /\bworkflow\b/g,
119
+ /\bagent\b/g,
120
+ /\bbacklog\b/g,
121
+ /\bforgejo\b/g,
122
+ /\breview loop\b/g,
123
+ /\breview\b/g,
124
+ /\bdraft\b/g,
125
+ /\bintegrate\b/g,
126
+ /\brebase\b/g,
127
+ /\bcheckpoint\b/g,
128
+ /\bprompt\b/g,
129
+ /\bstats\b/g,
130
+ /\bworktree\b/g,
131
+ /\bcli\b/g,
132
+ /\bgit\b/g,
133
+ /\bcoverage\b/g,
134
+ /\bverification\b/g,
135
+ ];
136
+
137
+ const score = patterns => patterns.reduce((sum, pattern) => sum + ((text.match(pattern) || []).length), 0);
138
+ const productScore = score(productSurfacePatterns);
139
+ const workflowScore = score(workflowPatterns);
140
+
141
+ if (productScore === 0 && workflowScore === 0) return null;
142
+ if (workflowScore >= productScore + 2) return 'ai_sdlc';
143
+ if (productScore >= workflowScore + 2) return 'user_value';
144
+
145
+ const titleLine = text.split('\n')[0] || '';
146
+ if (/\bworkflow|agent|forgejo|backlog|review|rebase|checkpoint|stats|cli\b/.test(titleLine)) {
147
+ return 'ai_sdlc';
148
+ }
149
+ if (/\bweb client|web-client|ios|android|grocer|board|delta|sync|store|price\b/.test(titleLine)) {
150
+ return 'user_value';
151
+ }
152
+
153
+ return null;
154
+ }
155
+
156
+ function resolveHistoricalClassification(slug, taskFile, rootDir = process.cwd()) {
157
+ try {
158
+ return {
159
+ value: stats.resolveMissionClassification(slug, rootDir).classification,
160
+ source: 'backlog-label',
161
+ };
162
+ } catch (_) {
163
+ const legacy = stats._internals.normalizeClassification(getTaskFrontmatterValue(taskFile, 'classification'));
164
+ if (legacy) {
165
+ return { value: legacy, source: 'backlog-classification' };
166
+ }
167
+ const inferred = inferHistoricalClassificationFromMissionDoc(slug, rootDir);
168
+ if (inferred) {
169
+ return { value: inferred, source: 'mission-doc-heuristic' };
170
+ }
171
+ return { value: null, source: null };
172
+ }
173
+ }
174
+
175
+ function collectHistoricalStatsBackfill(rootDir = process.cwd(), filePath = null) {
176
+ filePath = filePath || stats.resolveStatsPath();
177
+ const repoName = stats.resolveStatsRepoName(rootDir);
178
+ const existingMissions = new Set(
179
+ stats.loadStatsCsv(filePath, { rootDir }).rows
180
+ .filter(row => String(row.repo || '').trim() === repoName)
181
+ .map(row => row.mission)
182
+ );
183
+ const rows = [];
184
+ const unresolved = [];
185
+ const skipped = [];
186
+
187
+ for (const slug of listHistoricalMissionSlugs(rootDir)) {
188
+ if (existingMissions.has(slug)) continue;
189
+
190
+ const taskResolution = resolveTaskFile(slug, rootDir);
191
+ if (!taskResolution.ok) {
192
+ unresolved.push({
193
+ slug,
194
+ reason: 'task-resolution',
195
+ detail: taskResolution.reason,
196
+ });
197
+ continue;
198
+ }
199
+
200
+ const taskFile = taskResolution.taskFile;
201
+ const status = getTaskStatus(taskFile);
202
+ if (status !== 'done') {
203
+ skipped.push({ slug, reason: `status=${status || 'unknown'}` });
204
+ continue;
205
+ }
206
+
207
+ const date = extractDateOnly(getTaskFrontmatterValue(taskFile, 'updated_date')) || deriveDateFromGitHistory(slug, taskFile, rootDir);
208
+ const classification = resolveHistoricalClassification(slug, taskFile, rootDir);
209
+
210
+ let implementerInfo = null;
211
+ let implementerError = null;
212
+ try {
213
+ implementerInfo = stats.deriveImplementerAndFixRounds(slug, rootDir);
214
+ } catch (error) {
215
+ implementerError = error.message;
216
+ }
217
+
218
+ if (!implementerInfo?.implementer) {
219
+ const gitHistoryImplementer = deriveImplementerFromGitHistory(slug, taskFile, rootDir);
220
+ if (gitHistoryImplementer) {
221
+ implementerInfo = {
222
+ implementer: gitHistoryImplementer,
223
+ prFixRounds: 0,
224
+ source: 'git-history-author',
225
+ };
226
+ }
227
+ }
228
+
229
+ if (!date || !classification.value || !implementerInfo?.implementer) {
230
+ unresolved.push({
231
+ slug,
232
+ reason: 'missing-fields',
233
+ date: date || null,
234
+ classification: classification.value || null,
235
+ implementer: implementerInfo?.implementer || null,
236
+ prFixRounds: implementerInfo?.prFixRounds ?? null,
237
+ sources: {
238
+ classification: classification.source,
239
+ implementer: implementerInfo?.source || null,
240
+ },
241
+ missing: [
242
+ ...(!date ? ['date'] : []),
243
+ ...(!classification.value ? ['classification'] : []),
244
+ ...(!implementerInfo?.implementer ? ['implementer'] : []),
245
+ ],
246
+ detail: implementerError,
247
+ });
248
+ continue;
249
+ }
250
+
251
+ rows.push({
252
+ date,
253
+ repo: repoName,
254
+ mission: slug,
255
+ classification: classification.value,
256
+ implementer: implementerInfo.implementer,
257
+ pr_fix_rounds: String(implementerInfo.prFixRounds),
258
+ sources: {
259
+ date: 'backlog-updated_date',
260
+ classification: classification.source,
261
+ implementer: implementerInfo.source,
262
+ },
263
+ });
264
+ }
265
+
266
+ rows.sort((a, b) => a.date.localeCompare(b.date) || a.mission.localeCompare(b.mission));
267
+ unresolved.sort((a, b) => a.slug.localeCompare(b.slug));
268
+ skipped.sort((a, b) => a.slug.localeCompare(b.slug));
269
+
270
+ return { rows, unresolved, skipped };
271
+ }
272
+
273
+ function renderBackfillSummary(report) {
274
+ const lines = [];
275
+ lines.push(`Resolved rows: ${report.rows.length}`);
276
+ lines.push(`Unresolved missions: ${report.unresolved.length}`);
277
+ lines.push(`Skipped missions: ${report.skipped.length}`);
278
+ lines.push('');
279
+
280
+ if (report.rows.length > 0) {
281
+ lines.push('Resolved:');
282
+ for (const row of report.rows) {
283
+ lines.push(`- ${row.date} ${row.mission} ${row.classification} ${row.implementer} ${row.pr_fix_rounds} [${row.sources.classification}/${row.sources.implementer}]`);
284
+ }
285
+ lines.push('');
286
+ }
287
+
288
+ if (report.unresolved.length > 0) {
289
+ lines.push('Unresolved:');
290
+ for (const item of report.unresolved) {
291
+ const missing = Array.isArray(item.missing) && item.missing.length > 0 ? ` missing=${item.missing.join(',')}` : '';
292
+ const detail = item.detail ? ` detail=${item.detail}` : '';
293
+ lines.push(`- ${item.slug} reason=${item.reason}${missing}${detail}`);
294
+ }
295
+ lines.push('');
296
+ }
297
+
298
+ if (report.skipped.length > 0) {
299
+ lines.push('Skipped:');
300
+ for (const item of report.skipped) {
301
+ lines.push(`- ${item.slug} ${item.reason}`);
302
+ }
303
+ }
304
+
305
+ return lines.join('\n');
306
+ }
307
+
308
+ function applyBackfillRows(rows, filePath = null, rootDir = process.cwd()) {
309
+ filePath = filePath || stats.resolveStatsPath({ ensureDir: true });
310
+ let changed = 0;
311
+ for (const row of rows) {
312
+ const result = stats.upsertStatsRow(row, { filePath, rootDir });
313
+ if (result.changed) changed += 1;
314
+ }
315
+ return changed;
316
+ }
317
+
318
+ function printUsage(log = fmt.log.plain) {
319
+ log(`Usage: px stats-backfill [--apply] [--json] [--csv-file <path>]
320
+
321
+ Examples:
322
+ px stats-backfill
323
+ px stats-backfill --json
324
+ px stats-backfill --apply
325
+
326
+ Notes:
327
+ - This command is for historical stats recovery only.
328
+ - With no --csv-file, reads and writes <PARALLIX_HOME>/stats.csv.
329
+ - It uses strict workflow stats derivation for implementer/fix rounds and historical fallbacks for classification.
330
+ - Non-done missions are skipped and unresolved missions are reported without being written.`);
331
+ }
332
+
333
+ function statsBackfill(args, options = {}) {
334
+ const log = options.log || fmt.log.plain;
335
+ const error = options.error || fmt.log.plainError;
336
+ const exit = options.exit || process.exit;
337
+ const rootDir = options.rootDir || process.cwd();
338
+
339
+ if (args.includes('--help') || args.includes('-h')) {
340
+ printUsage(log);
341
+ return;
342
+ }
343
+
344
+ let apply = false;
345
+ let json = false;
346
+ let filePath = null;
347
+
348
+ for (let i = 0; i < args.length; i += 1) {
349
+ const arg = args[i];
350
+ if (arg === '--apply') {
351
+ apply = true;
352
+ continue;
353
+ }
354
+ if (arg === '--json') {
355
+ json = true;
356
+ continue;
357
+ }
358
+ if (arg === '--csv-file' && i + 1 < args.length) {
359
+ filePath = args[i + 1];
360
+ i += 1;
361
+ }
362
+ }
363
+ filePath = filePath || stats.resolveStatsPath({ ensureDir: apply });
364
+
365
+ const report = collectHistoricalStatsBackfill(rootDir, filePath);
366
+ const changed = apply ? applyBackfillRows(report.rows, filePath, rootDir) : 0;
367
+ const payload = {
368
+ resolved: report.rows.length,
369
+ unresolved: report.unresolved.length,
370
+ skipped: report.skipped.length,
371
+ changed,
372
+ rows: report.rows,
373
+ unresolvedItems: report.unresolved,
374
+ skippedItems: report.skipped,
375
+ };
376
+
377
+ if (json) {
378
+ log(JSON.stringify(payload, null, 2));
379
+ } else {
380
+ log(renderBackfillSummary(report));
381
+ if (apply) {
382
+ log('');
383
+ log(fmt.status('PASS', `Applied ${changed} stats rows to ${filePath}`));
384
+ }
385
+ }
386
+
387
+ if (apply && report.unresolved.length > 0) {
388
+ error(fmt.status('INFO', `${report.unresolved.length} missions remain unresolved and were not written.`));
389
+ }
390
+ }
391
+
392
+ module.exports = statsBackfill;
393
+ module.exports.collectHistoricalStatsBackfill = collectHistoricalStatsBackfill;
394
+ module.exports.inferHistoricalClassificationFromMissionDoc = inferHistoricalClassificationFromMissionDoc;
395
+ module.exports.extractDateOnly = extractDateOnly;