@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.
- package/config/integration-pipelines.json +9 -0
- package/config/workflow.config.schema.json +12 -0
- package/docs/adr/0032-mission-refinement-state-and-usage-budget-signals.md +10 -8
- package/docs/adr/0036-mission-sizing-and-dependency-wave-heuristics.md +14 -12
- package/docs/adr/0041-integration-pipeline-gates.md +1 -1
- package/docs/adr/0047-per-mission-change-size-budget.md +161 -0
- package/docs/adr/index.md +1 -0
- package/docs/use-cases.md +44 -0
- package/lib/agents/agents.js +149 -100
- package/lib/agents/claude-telemetry.js +14 -10
- package/lib/agents/claude.js +32 -11
- package/lib/agents/codex-telemetry.js +32 -19
- package/lib/agents/codex.js +36 -11
- package/lib/agents/limit-hit.js +42 -25
- package/lib/agents/mistral-telemetry.js +1 -1
- package/lib/agents/mistral.js +13 -3
- package/lib/agents/opencode-export.js +11 -5
- package/lib/agents/opencode-telemetry.js +75 -90
- package/lib/agents/opencode.js +86 -29
- package/lib/agents/stage-telemetry.js +3 -7
- package/lib/commands/active.js +139 -94
- package/lib/commands/checkpoint.js +3 -1
- package/lib/commands/config.js +5 -3
- package/lib/commands/coverage-gate.js +17 -10
- package/lib/commands/diff.js +11 -5
- package/lib/commands/draft.js +95 -55
- package/lib/commands/handoff.js +253 -65
- package/lib/commands/integrate.js +288 -188
- package/lib/commands/mission-start.js +39 -34
- package/lib/commands/rebase.js +32 -21
- package/lib/commands/repair-handoff.js +21 -18
- package/lib/commands/resolve-conflict.js +5 -2
- package/lib/commands/review.js +1 -0
- package/lib/commands/setup-review.js +1 -0
- package/lib/commands/stats-backfill.js +50 -41
- package/lib/commands/stats.js +653 -204
- package/lib/commands/status.js +33 -28
- package/lib/core/fmt.js +58 -15
- package/lib/core/git.js +7 -5
- package/lib/core/gitignore.js +6 -4
- package/lib/core/mission-utils.js +161 -88
- package/lib/core/nels.js +199 -0
- package/lib/core/persistent-data-migration.js +55 -23
- package/lib/core/product-config.js +44 -23
- package/lib/core/runtime-matrix.js +15 -2
- package/lib/core/spawn-tee.js +40 -13
- package/lib/core/state-map.js +32 -13
- package/lib/core/storage.js +19 -5
- package/lib/core/subagent-limit.js +28 -0
- package/lib/core/verification.js +29 -12
- package/lib/review/rebase.js +11 -3
- package/lib/review/review-adapter.js +93 -6
- package/lib/review/review-artifacts.js +89 -23
- package/lib/review/review-commands.js +139 -45
- package/lib/review/review-events.js +103 -52
- package/lib/review/review-loop.js +117 -87
- package/lib/review/review-polling.js +25 -5
- package/lib/review/review-prompts.js +36 -5
- package/lib/review/review-state.js +39 -15
- package/lib/tools/backlog.js +162 -71
- package/lib/tools/forgejo.js +290 -129
- package/lib/tools/gatekeeper.js +15 -0
- package/lib/tools/redgreen.js +220 -0
- package/lib/tools/sessions.js +12 -5
- package/lib/tools/setup-review.js +170 -70
- package/package.json +11 -3
- package/prompts/draft.md +6 -0
- 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 {
|
|
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
|
-
|
|
144
|
-
|
|
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
|
|
173
|
-
|
|
174
|
-
|
|
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
|
|