@magnusekdahl/parallix 1.1.0 → 1.2.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 (68) hide show
  1. package/config/integration-pipelines.json +9 -0
  2. package/config/workflow.config.schema.json +12 -0
  3. package/docs/adr/0032-mission-refinement-state-and-usage-budget-signals.md +10 -8
  4. package/docs/adr/0036-mission-sizing-and-dependency-wave-heuristics.md +14 -12
  5. package/docs/adr/0041-integration-pipeline-gates.md +1 -1
  6. package/docs/adr/0047-per-mission-change-size-budget.md +161 -0
  7. package/docs/adr/index.md +1 -0
  8. package/docs/use-cases.md +44 -0
  9. package/lib/agents/agents.js +149 -100
  10. package/lib/agents/claude-telemetry.js +14 -10
  11. package/lib/agents/claude.js +32 -11
  12. package/lib/agents/codex-telemetry.js +32 -19
  13. package/lib/agents/codex.js +36 -11
  14. package/lib/agents/limit-hit.js +42 -25
  15. package/lib/agents/mistral-telemetry.js +1 -1
  16. package/lib/agents/mistral.js +13 -3
  17. package/lib/agents/opencode-export.js +11 -5
  18. package/lib/agents/opencode-telemetry.js +75 -90
  19. package/lib/agents/opencode.js +86 -29
  20. package/lib/agents/stage-telemetry.js +3 -7
  21. package/lib/commands/active.js +139 -94
  22. package/lib/commands/checkpoint.js +3 -1
  23. package/lib/commands/config.js +5 -3
  24. package/lib/commands/coverage-gate.js +17 -10
  25. package/lib/commands/diff.js +11 -5
  26. package/lib/commands/draft.js +95 -55
  27. package/lib/commands/handoff.js +253 -65
  28. package/lib/commands/integrate.js +288 -188
  29. package/lib/commands/mission-start.js +39 -34
  30. package/lib/commands/rebase.js +32 -21
  31. package/lib/commands/repair-handoff.js +21 -18
  32. package/lib/commands/resolve-conflict.js +5 -2
  33. package/lib/commands/review.js +1 -0
  34. package/lib/commands/setup-review.js +1 -0
  35. package/lib/commands/stats-backfill.js +50 -41
  36. package/lib/commands/stats.js +653 -204
  37. package/lib/commands/status.js +33 -28
  38. package/lib/core/fmt.js +58 -15
  39. package/lib/core/git.js +7 -5
  40. package/lib/core/gitignore.js +6 -4
  41. package/lib/core/mission-utils.js +161 -88
  42. package/lib/core/nels.js +199 -0
  43. package/lib/core/persistent-data-migration.js +55 -23
  44. package/lib/core/product-config.js +44 -23
  45. package/lib/core/runtime-matrix.js +15 -2
  46. package/lib/core/spawn-tee.js +40 -13
  47. package/lib/core/state-map.js +32 -13
  48. package/lib/core/storage.js +19 -5
  49. package/lib/core/subagent-limit.js +28 -0
  50. package/lib/core/verification.js +29 -12
  51. package/lib/review/rebase.js +11 -3
  52. package/lib/review/review-adapter.js +93 -6
  53. package/lib/review/review-artifacts.js +89 -23
  54. package/lib/review/review-commands.js +139 -45
  55. package/lib/review/review-events.js +103 -52
  56. package/lib/review/review-loop.js +117 -87
  57. package/lib/review/review-polling.js +25 -5
  58. package/lib/review/review-prompts.js +36 -5
  59. package/lib/review/review-state.js +39 -15
  60. package/lib/tools/backlog.js +162 -71
  61. package/lib/tools/forgejo.js +290 -129
  62. package/lib/tools/gatekeeper.js +15 -0
  63. package/lib/tools/redgreen.js +220 -0
  64. package/lib/tools/sessions.js +12 -5
  65. package/lib/tools/setup-review.js +170 -70
  66. package/package.json +11 -3
  67. package/prompts/draft.md +6 -0
  68. package/templates/mission-scaffold.md +1 -1
@@ -21,6 +21,12 @@ const ACT_ON_REVIEW_VERBOSE_PROMPT_PATH = path.join(__dirname, '..', '..', 'prom
21
21
  * Slash-command / entrypoint used to invoke the review skill for each agent family.
22
22
  * Moved from scripts/autonomous-review.sh prompt_entrypoint_for() into Node.
23
23
  */
24
+ /**
25
+ * @typedef {{review: string, actOnReview: string}} PromptEntry
26
+ * @typedef {{codex: PromptEntry, claude: PromptEntry, mistral: PromptEntry, custom: PromptEntry, autonomous: PromptEntry}} PromptEntrypoints
27
+ */
28
+
29
+ /** @type {PromptEntrypoints} */
24
30
  const PROMPT_ENTRYPOINTS = {
25
31
  codex: { review: '$review all', actOnReview: '$act-on-review' },
26
32
  claude: { review: '/review all', actOnReview: '/act-on-review' },
@@ -29,15 +35,17 @@ const PROMPT_ENTRYPOINTS = {
29
35
  autonomous: { review: '$review all', actOnReview: '$act-on-review' }
30
36
  };
31
37
 
38
+ /** @param {string} agent */
32
39
  function reviewEntrypoint(agent) {
33
- const entry = PROMPT_ENTRYPOINTS[agent];
34
- if (!entry) throw new Error(`Unknown agent family for review entrypoint: ${agent}`);
40
+ const entry = /** @type {PromptEntry} */ (/** @type {{[key: string]: PromptEntry}} */ (PROMPT_ENTRYPOINTS)[agent]);
41
+ if (!entry) {throw new Error(`Unknown agent family for review entrypoint: ${agent}`);}
35
42
  return entry.review;
36
43
  }
37
44
 
45
+ /** @param {string} agent */
38
46
  function actOnReviewEntrypoint(agent) {
39
- const entry = PROMPT_ENTRYPOINTS[agent];
40
- if (!entry) throw new Error(`Unknown agent family for act-on-review entrypoint: ${agent}`);
47
+ const entry = /** @type {PromptEntry} */ (/** @type {{[key: string]: PromptEntry}} */ (PROMPT_ENTRYPOINTS)[agent]);
48
+ if (!entry) {throw new Error(`Unknown agent family for act-on-review entrypoint: ${agent}`);}
41
49
  return entry.actOnReview;
42
50
  }
43
51
 
@@ -53,11 +61,17 @@ function actOnReviewEntrypoint(agent) {
53
61
  * @param {string} [opts.repoRoot] - Absolute repo root path
54
62
  * @returns {string}
55
63
  */
64
+ /**
65
+ * @param {string} slug
66
+ * @param {string} [repoRoot]
67
+ * @param {string} [missionPathOverride]
68
+ * @returns {string}
69
+ */
56
70
  function resolveMissionPath(slug, repoRoot, missionPathOverride) {
57
71
  // `--mission <path>` override: when the caller points the review loop at a
58
72
  // mission contract in a non-standard location, agents must read that file
59
73
  // instead of the slug-derived standard path (task-1272 SC7).
60
- if (missionPathOverride) return missionPathOverride;
74
+ if (missionPathOverride) {return missionPathOverride;}
61
75
  const root = repoRoot || process.cwd();
62
76
  try {
63
77
  return missionPathForSlug(root, slug);
@@ -67,6 +81,7 @@ function resolveMissionPath(slug, repoRoot, missionPathOverride) {
67
81
  }
68
82
  }
69
83
 
84
+ /** @param {string} [repoRoot] */
70
85
  function resolvePrimaryBranch(repoRoot) {
71
86
  try {
72
87
  return getPrimaryBranch(repoRoot || process.cwd());
@@ -75,6 +90,10 @@ function resolvePrimaryBranch(repoRoot) {
75
90
  }
76
91
  }
77
92
 
93
+ /**
94
+ * @param {{reviewer: string, branch: string, implementer: string, focus?: string, attempt: number, repoRoot?: string, missionPath?: string}} opts
95
+ * @returns {string}
96
+ */
78
97
  function buildReviewPrompt({ reviewer, branch, implementer, focus = 'all', attempt, repoRoot = '', missionPath: missionPathOverride }) {
79
98
  const entrypoint = reviewEntrypoint(reviewer);
80
99
  const repoLine = repoRoot ? `\noperate from repo root: ${repoRoot}` : '';
@@ -109,6 +128,10 @@ function buildReviewPrompt({ reviewer, branch, implementer, focus = 'all', attem
109
128
  * @param {string} [opts.repoRoot] - Absolute repo root path
110
129
  * @returns {string}
111
130
  */
131
+ /**
132
+ * @param {{implementer: string, branch: string, attempt: number, repoRoot?: string, missionPath?: string}} opts
133
+ * @returns {string}
134
+ */
112
135
  function buildActOnReviewPrompt({ implementer, branch, attempt, repoRoot = '', missionPath: missionPathOverride }) {
113
136
  const entrypoint = actOnReviewEntrypoint(implementer);
114
137
  const repoLine = repoRoot ? `\noperate from repo root: ${repoRoot}` : '';
@@ -146,6 +169,10 @@ function buildActOnReviewPrompt({ implementer, branch, attempt, repoRoot = '', m
146
169
  * @param {string} [opts.actualReviewer] - The actual agent family launched (overrides reviewer for identity text, TASK-1051)
147
170
  * @returns {string}
148
171
  */
172
+ /**
173
+ * @param {{reviewer: string, branch: string, implementer: string, focus?: string, attempt: number, actualReviewer?: string, repoRoot?: string, missionPath?: string}} opts
174
+ * @returns {string}
175
+ */
149
176
  function buildCompactReviewPrompt({ reviewer, branch, implementer, focus = 'all', attempt, actualReviewer, repoRoot = '', missionPath: missionPathOverride }) {
150
177
  const slug = branch.replace(/^mission\//, '');
151
178
  const finalReviewer = actualReviewer || reviewer;
@@ -180,6 +207,10 @@ function buildCompactReviewPrompt({ reviewer, branch, implementer, focus = 'all'
180
207
  * @param {string} [opts.actualImplementer] - The actual agent family launched (overrides implementer for identity text, TASK-1051)
181
208
  * @returns {string}
182
209
  */
210
+ /**
211
+ * @param {{implementer: string, branch: string, attempt: number, reviewOutcome?: string, actualImplementer?: string, repoRoot?: string, missionPath?: string}} opts
212
+ * @returns {string}
213
+ */
183
214
  function buildCompactActOnReviewPrompt({ implementer, branch, attempt, reviewOutcome = '?', actualImplementer, repoRoot = '', missionPath: missionPathOverride }) {
184
215
  const slug = branch.replace(/^mission\//, '');
185
216
  const finalImplementer = actualImplementer || implementer;
@@ -23,7 +23,7 @@ const fmt = require('../core/fmt');
23
23
  */
24
24
  function reviewStateFile(slug, rootDir = process.cwd()) {
25
25
  const missionDir = findMissionDir(slug, rootDir);
26
- if (!missionDir) return null;
26
+ if (!missionDir) {return null;}
27
27
  return path.join(missionDir, 'review-state.json');
28
28
  }
29
29
 
@@ -36,7 +36,7 @@ function reviewStateFile(slug, rootDir = process.cwd()) {
36
36
  */
37
37
  function readReviewState(slug, rootDir = process.cwd()) {
38
38
  const statePath = reviewStateFile(slug, rootDir);
39
- if (!statePath || !fs.existsSync(statePath)) return null;
39
+ if (!statePath || !fs.existsSync(statePath)) {return null;}
40
40
 
41
41
  try {
42
42
  const raw = fs.readFileSync(statePath, 'utf8');
@@ -58,10 +58,10 @@ function readReviewState(slug, rootDir = process.cwd()) {
58
58
  *
59
59
  * @param {string} slug
60
60
  * @param {string} [rootDir]
61
- * @param {object} [options]
61
+ * @param {{readReviewStateFn?: Function}} [options]
62
62
  * @returns {{ identityUser: string|null, commentIdentityUser: string|null, forgejoUser: string|null, commentForgejoUser: string|null, reviewState: ReviewState|null, source: 'review-state'|null }}
63
63
  */
64
- function resolveReviewIdentity(slug, rootDir = process.cwd(), options = {}) {
64
+ function resolveReviewIdentity(slug, rootDir = process.cwd(), /** @type {{readReviewStateFn?: Function}} */ options = {}) {
65
65
  const readReviewStateFn = options.readReviewStateFn || readReviewState;
66
66
  const reviewState = readReviewStateFn(slug, rootDir);
67
67
  const reviewerUser = reviewState ? (reviewState.reviewer || reviewState.implementer || null) : null;
@@ -99,6 +99,7 @@ const PHASE_TRANSITIONS = {
99
99
  'approved': []
100
100
  };
101
101
 
102
+ /** @param {string} disposition */
102
103
  function inferPhaseFromDisposition(disposition) {
103
104
  switch (String(disposition || '').trim().toUpperCase()) {
104
105
  case 'APPROVED':
@@ -116,6 +117,7 @@ function inferPhaseFromDisposition(disposition) {
116
117
  }
117
118
  }
118
119
 
120
+ /** @param {string} phase @param {string} disposition */
119
121
  function normalizeReviewPhase(phase, disposition) {
120
122
  if (!phase) {
121
123
  return { phase: 'reviewing', original: null, normalized: false };
@@ -139,9 +141,17 @@ function normalizeReviewPhase(phase, disposition) {
139
141
  };
140
142
  }
141
143
 
144
+ /**
145
+ * @typedef {{reviewer?: string, implementer?: string, round?: number, startedAt?: string, phase?: string, disposition?: string | null, reviewerRetryCount?: number, implementerRetryCount?: number, metadata?: {[key: string]: any}}} ReviewStateData
146
+ */
147
+
142
148
  class ReviewState {
143
- constructor(slug, data = {}) {
144
- const phaseInfo = normalizeReviewPhase(data.phase, data.disposition);
149
+ /**
150
+ * @param {string} slug
151
+ * @param {ReviewStateData} [data]
152
+ */
153
+ constructor(slug, /** @type {ReviewStateData} */ data = {}) {
154
+ const phaseInfo = normalizeReviewPhase(/** @type {string} */ (data.phase || ''), /** @type {string} */ (data.disposition || ''));
145
155
  this.slug = slug;
146
156
  this.reviewer = data.reviewer;
147
157
  this.implementer = data.implementer;
@@ -155,6 +165,11 @@ class ReviewState {
155
165
  this.phaseOriginal = phaseInfo.normalized ? phaseInfo.original : null;
156
166
  }
157
167
 
168
+ /**
169
+ * @param {string} slug
170
+ * @param {ReviewState|ReviewStateData} dataOrInstance
171
+ * @returns {ReviewState}
172
+ */
158
173
  static from(slug, dataOrInstance) {
159
174
  if (dataOrInstance instanceof ReviewState) {
160
175
  if (dataOrInstance.slug !== slug) {
@@ -165,13 +180,18 @@ class ReviewState {
165
180
  return new ReviewState(slug, dataOrInstance || {});
166
181
  }
167
182
 
183
+ /**
184
+ * @param {string} nextPhase
185
+ * @returns {ReviewState}
186
+ */
168
187
  transitionTo(nextPhase) {
169
188
  if (!VALID_PHASES.includes(nextPhase)) {
170
189
  throw new Error(`Invalid phase: "${nextPhase}". Valid: ${VALID_PHASES.join(', ')}`);
171
190
  }
172
- const allowed = PHASE_TRANSITIONS[this.phase];
173
- if (!allowed || !allowed.includes(nextPhase)) {
174
- throw new Error(`Cannot transition from "${this.phase}" to "${nextPhase}". Allowed: ${(allowed || []).join(', ') || 'none'}`);
191
+ const transitions = /** @type {{[key: string]: string[]}} */ (PHASE_TRANSITIONS);
192
+ const allowedArr = transitions[this.phase];
193
+ if (!allowedArr || !allowedArr.includes(nextPhase)) {
194
+ throw new Error(`Cannot transition from "${this.phase}" to "${nextPhase}". Allowed: ${(allowedArr || []).join(', ') || 'none'}`);
175
195
  }
176
196
  this.phase = nextPhase;
177
197
  return this;
@@ -187,26 +207,30 @@ class ReviewState {
187
207
  return this;
188
208
  }
189
209
 
210
+ /**
211
+ * @returns {{reviewer?: string, implementer?: string, round?: number, startedAt?: string, phase?: string, disposition?: string, reviewerRetryCount?: number, implementerRetryCount?: number, metadata?: {[key: string]: any}}}
212
+ */
190
213
  toJSON() {
214
+ /** @type {{reviewer?: string, implementer?: string, round?: number, startedAt?: string, phase?: string, disposition?: string, reviewerRetryCount?: number, implementerRetryCount?: number, metadata?: {[key: string]: any}}} */
191
215
  const payload = {
192
216
  reviewer: this.reviewer,
193
217
  implementer: this.implementer,
194
218
  round: this.round,
195
219
  startedAt: this.startedAt,
196
220
  phase: this.phase,
197
- disposition: this.disposition
221
+ disposition: this.disposition || undefined
198
222
  };
199
223
 
200
- if (this.reviewerRetryCount > 0) payload.reviewerRetryCount = this.reviewerRetryCount;
201
- if (this.implementerRetryCount > 0) payload.implementerRetryCount = this.implementerRetryCount;
202
- if (Object.keys(this.metadata).length > 0) payload.metadata = this.metadata;
224
+ if (this.reviewerRetryCount > 0) {payload.reviewerRetryCount = this.reviewerRetryCount;}
225
+ if (this.implementerRetryCount > 0) {payload.implementerRetryCount = this.implementerRetryCount;}
226
+ if (Object.keys(this.metadata || {}).length > 0) {payload.metadata = this.metadata;}
203
227
 
204
228
  return payload;
205
229
  }
206
230
 
207
231
  save(worktree = resolveWorktree(this.slug) || process.cwd(), gitFn = git) {
208
232
  const statePath = reviewStateFile(this.slug, worktree);
209
- if (!statePath) return false;
233
+ if (!statePath) {return false;}
210
234
 
211
235
  const payload = this.toJSON();
212
236
  fs.writeFileSync(statePath, JSON.stringify(payload, null, 2) + '\n', 'utf8');
@@ -250,7 +274,7 @@ function writeReviewState(slug, state, worktree = resolveWorktree(slug) || proce
250
274
  */
251
275
  function resetReviewState(slug, worktree = resolveWorktree(slug) || process.cwd(), gitFn = git) {
252
276
  const statePath = reviewStateFile(slug, worktree);
253
- if (!statePath || !fs.existsSync(statePath)) return false;
277
+ if (!statePath || !fs.existsSync(statePath)) {return false;}
254
278
 
255
279
  fs.unlinkSync(statePath);
256
280