@magnusekdahl/parallix 1.2.1 → 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.
- package/index.js +190 -185
- package/lib/commands/active.js +502 -579
- package/lib/commands/active.ts +665 -0
- package/lib/commands/checkpoint.js +98 -74
- package/lib/commands/checkpoint.ts +79 -0
- package/lib/commands/config.js +61 -31
- package/lib/commands/config.ts +47 -0
- package/lib/commands/coverage-gate.js +271 -246
- package/lib/commands/coverage-gate.ts +362 -0
- package/lib/commands/diff.js +140 -119
- package/lib/commands/diff.ts +125 -0
- package/lib/commands/draft.js +770 -923
- package/lib/commands/draft.ts +1021 -0
- package/lib/commands/handoff.js +588 -608
- package/lib/commands/handoff.ts +694 -0
- package/lib/commands/integrate.js +1331 -1468
- package/lib/commands/integrate.ts +1688 -0
- package/lib/commands/mission-start.js +279 -237
- package/lib/commands/mission-start.ts +263 -0
- package/lib/commands/rebase.js +606 -582
- package/lib/commands/rebase.ts +635 -0
- package/lib/commands/repair-handoff.js +181 -179
- package/lib/commands/repair-handoff.ts +228 -0
- package/lib/commands/resolve-conflict.js +121 -102
- package/lib/commands/resolve-conflict.ts +112 -0
- package/lib/commands/review.js +3 -4
- package/lib/commands/review.ts +14 -0
- package/lib/commands/setup-review.js +4 -5
- package/lib/commands/setup-review.ts +14 -0
- package/lib/commands/setup.js +3 -3
- package/lib/commands/setup.ts +2 -0
- package/lib/commands/stats-backfill.js +394 -359
- package/lib/commands/stats-backfill.ts +418 -0
- package/lib/commands/stats.js +1424 -1665
- package/lib/commands/stats.ts +2186 -0
- package/lib/commands/status.js +219 -200
- package/lib/commands/status.ts +219 -0
- package/lib/commands/verify.js +3 -1
- package/lib/commands/verify.ts +2 -0
- package/lib/core/nels.js +112 -127
- package/lib/core/nels.ts +189 -0
- package/lib/core/subagent-limit.js +15 -16
- package/lib/core/subagent-limit.ts +25 -0
- package/lib/index.js +142 -59
- package/lib/index.ts +134 -0
- package/lib/review/rebase.js +142 -145
- package/lib/review/rebase.ts +188 -0
- package/lib/review/review-adapter.js +99 -87
- package/lib/review/review-adapter.ts +198 -0
- package/lib/review/review-artifacts.js +486 -629
- package/lib/review/review-artifacts.ts +622 -0
- package/lib/review/review-commands.js +1220 -1373
- package/lib/review/review-commands.ts +1579 -0
- package/lib/review/review-events.js +544 -886
- package/lib/review/review-events.ts +954 -0
- package/lib/review/review-loop.js +945 -1020
- package/lib/review/review-loop.ts +1174 -0
- package/lib/review/review-polling.js +86 -115
- package/lib/review/review-polling.ts +194 -0
- package/lib/review/review-prompts.js +154 -189
- package/lib/review/review-prompts.ts +182 -0
- package/lib/review/review-state.js +246 -225
- package/lib/review/review-state.ts +353 -0
- package/lib/review/review.js +63 -84
- package/lib/review/review.ts +138 -0
- package/lib/tools/backlog.js +694 -680
- package/lib/tools/backlog.ts +835 -0
- package/lib/tools/forgejo.js +1270 -1405
- package/lib/tools/forgejo.ts +1793 -0
- package/lib/tools/gatekeeper.js +106 -96
- package/lib/tools/gatekeeper.ts +124 -0
- package/lib/tools/redgreen.js +200 -181
- package/lib/tools/redgreen.ts +221 -0
- package/lib/tools/sessions.js +58 -52
- package/lib/tools/sessions.ts +81 -0
- package/lib/tools/setup-review.js +902 -975
- package/lib/tools/setup-review.ts +1152 -0
- package/package.json +4 -4
- package/px.js +226 -208
package/lib/commands/active.js
CHANGED
|
@@ -1,159 +1,168 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
// @ts-nocheck
|
|
36
|
+
const git_js_1 = require("../core/git.js");
|
|
37
|
+
const fs = __importStar(require("node:fs"));
|
|
38
|
+
const path = __importStar(require("node:path"));
|
|
39
|
+
const fmt = __importStar(require("../core/fmt.js"));
|
|
40
|
+
const missionStart = __importStar(require("./mission-start.js"));
|
|
41
|
+
const agents = __importStar(require("../agents/agents.js"));
|
|
42
|
+
const mission_utils_js_1 = require("../core/mission-utils.js");
|
|
43
|
+
const handoff = __importStar(require("./handoff.js"));
|
|
44
|
+
const backlog_js_1 = require("../tools/backlog.js");
|
|
45
|
+
const product_config_js_1 = require("../core/product-config.js");
|
|
46
|
+
const review = __importStar(require("../review/review.js"));
|
|
47
|
+
const repairHandoff = __importStar(require("./repair-handoff.js"));
|
|
48
|
+
const stats = __importStar(require("./stats.js"));
|
|
49
|
+
const stage_telemetry_js_1 = require("../agents/stage-telemetry.js");
|
|
16
50
|
const EXECUTE_PROMPT_PATH = path.join(__dirname, '..', '..', 'prompts', 'execute.md');
|
|
17
|
-
|
|
18
51
|
/**
|
|
19
52
|
* @param {string[]} args
|
|
20
53
|
* @param {{inferSlugFn?: Function, missionStartFn?: Function, resolveWorktreeFn?: Function, readAgentConfigOrExitFn?: Function, resolveTaskFileFn?: Function, buildCheckpointContextFn?: Function, buildExecutePromptFn?: Function, selectLaunchAndRecordFn?: Function, enforceExecuteCommitSafetyFn?: Function, runHandoffAndReviewFn?: Function, exitFn?: Function, logFn?: Function, errorFn?: Function}} [options]
|
|
21
54
|
*/
|
|
22
55
|
async function active(args, options = {}) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
56
|
+
const { inferSlugFn = mission_utils_js_1.inferSlug, missionStartFn = missionStart, resolveWorktreeFn = mission_utils_js_1.resolveWorktree, readAgentConfigOrExitFn = agents.readAgentConfigOrExit, resolveTaskFileFn = backlog_js_1.resolveTaskFile, buildCheckpointContextFn = buildCheckpointContext, buildExecutePromptFn = buildExecutePrompt, selectLaunchAndRecordFn = selectLaunchAndRecord, enforceExecuteCommitSafetyFn = enforceExecuteCommitSafety, runHandoffAndReviewFn = runHandoffAndReview, exitFn = process.exit, logFn = fmt.log.info, errorFn = fmt.log.fail } = options;
|
|
57
|
+
const explicitSlug = args[0];
|
|
58
|
+
const slug = inferSlugFn(explicitSlug);
|
|
59
|
+
if (!slug) {
|
|
60
|
+
errorFn('Usage: px active [<slug>] [--implementer <family>]');
|
|
61
|
+
exitFn(1);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const normalizedSlug = slug.toLowerCase();
|
|
65
|
+
// Allow operators to pin the implementer agent family via CLI flag instead of WORKFLOW_AGENT env var.
|
|
66
|
+
/** @param {string[]} arr @param {string} flag @param {string} name */
|
|
67
|
+
function flagValue(arr, flag, name) {
|
|
68
|
+
const i = arr.indexOf(flag);
|
|
69
|
+
if (i === -1) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
const v = arr[i + 1];
|
|
73
|
+
if (!v || v.startsWith('--')) {
|
|
74
|
+
errorFn(fmt.status('FAIL', `Missing value for --${name}. Usage: px active <slug> --${name} <family>`));
|
|
75
|
+
exitFn(1);
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
return v;
|
|
79
|
+
}
|
|
80
|
+
const preselectedImplementer = flagValue(args, '--implementer', 'implementer');
|
|
81
|
+
logFn('Running execute preflight...');
|
|
82
|
+
const preflight = missionStartFn([normalizedSlug], { returnResult: true });
|
|
83
|
+
if (!preflight.pass) {
|
|
84
|
+
errorFn('Preflight failed. Fix blockers above before launching the execute agent.');
|
|
85
|
+
exitFn(1);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const worktree = resolveWorktreeFn(normalizedSlug);
|
|
89
|
+
if (!worktree) {
|
|
90
|
+
errorFn(`Could not locate dedicated worktree for mission/${fmt.slug(normalizedSlug)}. ` +
|
|
91
|
+
`Run "px draft ${normalizedSlug}" first or create the worktree manually.`);
|
|
92
|
+
exitFn(1);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const agentConfig = readAgentConfigOrExitFn();
|
|
96
|
+
const taskResolution = resolveTaskFileFn(normalizedSlug, worktree);
|
|
97
|
+
const checkpointContext = buildCheckpointContextFn(normalizedSlug);
|
|
98
|
+
const prompt = buildExecutePromptFn(normalizedSlug, checkpointContext, { rootDir: worktree });
|
|
99
|
+
logFn('Launching execute agent...');
|
|
100
|
+
let launchResult;
|
|
101
|
+
try {
|
|
102
|
+
launchResult = await selectLaunchAndRecordFn({
|
|
103
|
+
slug: normalizedSlug,
|
|
104
|
+
worktree,
|
|
105
|
+
preselectedAgent: preselectedImplementer,
|
|
106
|
+
agentConfig,
|
|
107
|
+
taskResolution,
|
|
108
|
+
prompt
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
errorFn(`Could not launch execute agent: ${ /** @type{Error} */(err).message}`);
|
|
113
|
+
exitFn(1);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const { agent, result } = launchResult;
|
|
117
|
+
if (result.error) {
|
|
118
|
+
errorFn(`Could not start execute agent (${fmt.agent(agent)}): ${result.error.message}`);
|
|
119
|
+
exitFn(1);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (typeof result.status === 'number' && result.status !== 0) {
|
|
123
|
+
errorFn(`Execute agent (${fmt.agent(agent)}) exited with status ${result.status}.`);
|
|
124
|
+
exitFn(result.status || 1);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
try {
|
|
128
|
+
enforceExecuteCommitSafetyFn({ slug: normalizedSlug, worktree });
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
errorFn(fmt.status('FAIL', /** @type{Error} */ (error).message));
|
|
132
|
+
exitFn(1);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
// Automation: Post-active handoff
|
|
136
|
+
logFn(`\nExecute agent (${fmt.agent(agent)}) completed successfully. Recording execute-phase stats...`);
|
|
137
|
+
try {
|
|
138
|
+
const result = launchResult.result;
|
|
139
|
+
const sinceMs = result && result.startedAt ? Date.parse(result.startedAt) : 0;
|
|
140
|
+
stats.recordActiveStats(
|
|
141
|
+
/** @type{Parameters<typeof stats.recordActiveStats>[0]} */ ({
|
|
142
|
+
slug: normalizedSlug,
|
|
143
|
+
rootDir: worktree,
|
|
144
|
+
implementer: agent,
|
|
145
|
+
model: (0, product_config_js_1.resolveAgentModel)(agent, worktree),
|
|
146
|
+
// Use the windowed Codex rollout (bounded by sinceMs) when present, else
|
|
147
|
+
// the launcher-attached telemetry — same resolution as the review hook,
|
|
148
|
+
// so the execute phase records this stage's delta, not cumulative usage.
|
|
149
|
+
telemetry: (0, stage_telemetry_js_1.resolveStageTelemetry)({ worktree, result, sinceMs }),
|
|
150
|
+
durationMinutes: result && result.startedAt && result.endedAt
|
|
151
|
+
? (Date.parse(result.endedAt) - Date.parse(result.startedAt)) / 60000
|
|
152
|
+
: 0,
|
|
153
|
+
}));
|
|
154
|
+
}
|
|
155
|
+
catch (err) {
|
|
156
|
+
logFn(fmt.status('WARN', `Could not record execute stats for ${fmt.slug(normalizedSlug)}: ${ /** @type{Error} */(err).message}`));
|
|
157
|
+
}
|
|
158
|
+
logFn(`\nExecute agent (${fmt.agent(agent)}) completed successfully. Starting automated handoff...`);
|
|
159
|
+
const ok = await runHandoffAndReviewFn(normalizedSlug, worktree, agent, {
|
|
160
|
+
taskFile: taskResolution.ok ? taskResolution.taskFile : null
|
|
96
161
|
});
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const { agent, result } = launchResult;
|
|
104
|
-
|
|
105
|
-
if (result.error) {
|
|
106
|
-
errorFn(`Could not start execute agent (${fmt.agent(agent)}): ${result.error.message}`);
|
|
107
|
-
exitFn(1);
|
|
108
|
-
return;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
if (typeof result.status === 'number' && result.status !== 0) {
|
|
112
|
-
errorFn(`Execute agent (${fmt.agent(agent)}) exited with status ${result.status}.`);
|
|
113
|
-
exitFn(result.status || 1);
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
try {
|
|
118
|
-
enforceExecuteCommitSafetyFn({ slug: normalizedSlug, worktree });
|
|
119
|
-
} catch (error) {
|
|
120
|
-
errorFn(fmt.status('FAIL', /** @type{Error} */(error).message));
|
|
121
|
-
exitFn(1);
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Automation: Post-active handoff
|
|
126
|
-
logFn(`\nExecute agent (${fmt.agent(agent)}) completed successfully. Recording execute-phase stats...`);
|
|
127
|
-
try {
|
|
128
|
-
const result = launchResult.result;
|
|
129
|
-
const sinceMs = result && result.startedAt ? Date.parse(result.startedAt) : 0;
|
|
130
|
-
stats.recordActiveStats(
|
|
131
|
-
/** @type{Parameters<typeof stats.recordActiveStats>[0]} */({
|
|
132
|
-
slug: normalizedSlug,
|
|
133
|
-
rootDir: worktree,
|
|
134
|
-
implementer: agent,
|
|
135
|
-
model: resolveAgentModel(agent, worktree),
|
|
136
|
-
// Use the windowed Codex rollout (bounded by sinceMs) when present, else
|
|
137
|
-
// the launcher-attached telemetry — same resolution as the review hook,
|
|
138
|
-
// so the execute phase records this stage's delta, not cumulative usage.
|
|
139
|
-
telemetry: resolveStageTelemetry({ worktree, result, sinceMs }),
|
|
140
|
-
durationMinutes: result && result.startedAt && result.endedAt
|
|
141
|
-
? (Date.parse(result.endedAt) - Date.parse(result.startedAt)) / 60000
|
|
142
|
-
: 0,
|
|
143
|
-
})
|
|
144
|
-
);
|
|
145
|
-
} catch (err) {
|
|
146
|
-
logFn(fmt.status('WARN', `Could not record execute stats for ${fmt.slug(normalizedSlug)}: ${/** @type{Error} */(err).message}`));
|
|
147
|
-
}
|
|
148
|
-
logFn(`\nExecute agent (${fmt.agent(agent)}) completed successfully. Starting automated handoff...`);
|
|
149
|
-
const ok = await runHandoffAndReviewFn(normalizedSlug, worktree, agent, {
|
|
150
|
-
taskFile: taskResolution.ok ? taskResolution.taskFile : null
|
|
151
|
-
});
|
|
152
|
-
if (!ok) {
|
|
153
|
-
exitFn(1);
|
|
154
|
-
}
|
|
162
|
+
if (!ok) {
|
|
163
|
+
exitFn(1);
|
|
164
|
+
}
|
|
155
165
|
}
|
|
156
|
-
|
|
157
166
|
// Select an active-step agent, launch it, and record the actual implementer in
|
|
158
167
|
// Backlog only after a successful launch. Accepts injectable dependencies so the
|
|
159
168
|
// selection/launch/record sequence can be verified in tests without process.exit.
|
|
@@ -168,127 +177,100 @@ async function active(args, options = {}) {
|
|
|
168
177
|
* @param {{slug: string, worktree: string, preselectedAgent?: string | null, agentConfig: object, taskResolution: object, prompt: string, startAgentFn?: Function, transitionTaskFn?: Function, getTaskStatusFn?: Function, getTaskImplementerFn?: Function, selectAgentFn?: Function, log?: Function}} opts
|
|
169
178
|
*/
|
|
170
179
|
async function selectLaunchAndRecord(opts) {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
let launchedAgent = null;
|
|
195
|
-
const rollbackIfNeeded = ({ throwOnFailure = true } = {}) => {
|
|
196
|
-
if (!launchRecorded || !priorStatus) {
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
log(fmt.status('WARN', `Execute launch for ${fmt.slug(slug)} did not complete cleanly; rolling task state back to ${priorStatus} / ${priorImplementer || 'none'}.`));
|
|
201
|
-
/** @type{{rootDir: string, log: Function, implementer?: string, clearAssignee?: boolean}} */
|
|
202
|
-
const rollbackOpts = { rootDir: worktree, log };
|
|
203
|
-
if (priorImplementer) {
|
|
204
|
-
rollbackOpts.implementer = priorImplementer;
|
|
205
|
-
} else {
|
|
206
|
-
rollbackOpts.clearAssignee = true;
|
|
207
|
-
}
|
|
208
|
-
if (!transitionTaskFn(slug, priorStatus, rollbackOpts)) {
|
|
209
|
-
const msg = `Failed to roll back task ${fmt.slug(slug)} to ${priorStatus} after execute launch failure.`;
|
|
210
|
-
if (throwOnFailure) {throw new Error(msg);}
|
|
211
|
-
log(fmt.status('WARN', msg));
|
|
212
|
-
}
|
|
213
|
-
launchRecorded = false;
|
|
214
|
-
};
|
|
215
|
-
|
|
216
|
-
let actual;
|
|
217
|
-
let result;
|
|
218
|
-
try {
|
|
219
|
-
({ agent: actual, result } = await startAgentFn('active', {
|
|
220
|
-
prompt,
|
|
221
|
-
worktree,
|
|
222
|
-
agent: preselected,
|
|
223
|
-
slug: slug,
|
|
224
|
-
role: 'implementer',
|
|
225
|
-
onLaunch: (/** @type{{agent: string}} */ { agent }) => {
|
|
226
|
-
launchedAgent = agent;
|
|
227
|
-
if (!(taskResolutionTyped && taskResolutionTyped.ok)) {
|
|
228
|
-
return;
|
|
180
|
+
const { slug, worktree, preselectedAgent = null, agentConfig, taskResolution, prompt, startAgentFn = (/** @type{string} */ step, /** @type{any} */ opts) => agents.startAgent(step, opts), transitionTaskFn = backlog_js_1.transitionTask, getTaskStatusFn = backlog_js_1.getTaskStatus, getTaskImplementerFn = backlog_js_1.getTaskImplementer, selectAgentFn = agents.selectAgent, log = fmt.log.plain } = opts;
|
|
181
|
+
const preselected = preselectedAgent || selectAgentFn('active', { config: agentConfig });
|
|
182
|
+
const taskResolutionTyped = /** @type{{ok: boolean, taskFile?: string} | undefined} */ (taskResolution);
|
|
183
|
+
const taskFile = taskResolutionTyped && taskResolutionTyped.ok && taskResolutionTyped.taskFile
|
|
184
|
+
? taskResolutionTyped.taskFile
|
|
185
|
+
: null;
|
|
186
|
+
const priorStatus = taskFile ? getTaskStatusFn(taskFile) : null;
|
|
187
|
+
const priorImplementer = taskFile ? getTaskImplementerFn(taskFile) : null;
|
|
188
|
+
let launchRecorded = false;
|
|
189
|
+
let launchTransitionFailed = false;
|
|
190
|
+
let launchedAgent = null;
|
|
191
|
+
const rollbackIfNeeded = ({ throwOnFailure = true } = {}) => {
|
|
192
|
+
if (!launchRecorded || !priorStatus) {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
log(fmt.status('WARN', `Execute launch for ${fmt.slug(slug)} did not complete cleanly; rolling task state back to ${priorStatus} / ${priorImplementer || 'none'}.`));
|
|
196
|
+
/** @type{{rootDir: string, log: Function, implementer?: string, clearAssignee?: boolean}} */
|
|
197
|
+
const rollbackOpts = { rootDir: worktree, log };
|
|
198
|
+
if (priorImplementer) {
|
|
199
|
+
rollbackOpts.implementer = priorImplementer;
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
rollbackOpts.clearAssignee = true;
|
|
229
203
|
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
204
|
+
if (!transitionTaskFn(slug, priorStatus, rollbackOpts)) {
|
|
205
|
+
const msg = `Failed to roll back task ${fmt.slug(slug)} to ${priorStatus} after execute launch failure.`;
|
|
206
|
+
if (throwOnFailure) {
|
|
207
|
+
throw new Error(msg);
|
|
208
|
+
}
|
|
209
|
+
log(fmt.status('WARN', msg));
|
|
235
210
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
211
|
+
launchRecorded = false;
|
|
212
|
+
};
|
|
213
|
+
let actual;
|
|
214
|
+
let result;
|
|
215
|
+
try {
|
|
216
|
+
({ agent: actual, result } = await startAgentFn('active', {
|
|
217
|
+
prompt,
|
|
218
|
+
worktree,
|
|
219
|
+
agent: preselected,
|
|
220
|
+
slug: slug,
|
|
221
|
+
role: 'implementer',
|
|
222
|
+
onLaunch: (/** @type{{agent: string}} */ { agent }) => {
|
|
223
|
+
launchedAgent = agent;
|
|
224
|
+
if (!(taskResolutionTyped && taskResolutionTyped.ok)) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
log(`Recording implementer ${fmt.agent(agent)} and status=active for ${fmt.slug(slug)}...`);
|
|
228
|
+
if (!transitionTaskFn(slug, 'active', { implementer: agent, rootDir: worktree, log })) {
|
|
229
|
+
launchTransitionFailed = true;
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
launchRecorded = true;
|
|
233
|
+
},
|
|
234
|
+
onLimitHit: () => {
|
|
235
|
+
// Roll back the intermediate active write so the retry's onLaunch starts
|
|
236
|
+
// from a clean state, preventing a spurious committed implementer entry.
|
|
237
|
+
rollbackIfNeeded({ throwOnFailure: false });
|
|
238
|
+
}
|
|
239
|
+
}));
|
|
240
|
+
}
|
|
241
|
+
catch (err) {
|
|
242
|
+
rollbackIfNeeded();
|
|
243
|
+
throw err;
|
|
244
|
+
}
|
|
245
|
+
// `actual` is the family that actually ran (may differ from `preselected` after
|
|
246
|
+
// a limit-hit fallback inside startAgent). Use it as the canonical implementer.
|
|
247
|
+
const agent = actual || launchedAgent || preselected;
|
|
248
|
+
const launchSucceeded = !result.error && (typeof result.status !== 'number' || result.status === 0);
|
|
249
|
+
if (!launchSucceeded) {
|
|
250
|
+
rollbackIfNeeded();
|
|
251
|
+
}
|
|
252
|
+
if (launchTransitionFailed) {
|
|
253
|
+
throw new Error(`Failed to record task ${fmt.slug(slug)} as active after execute launch.`);
|
|
254
|
+
}
|
|
255
|
+
return { preselected, agent, result };
|
|
264
256
|
}
|
|
265
|
-
|
|
266
257
|
// When startAgent falls back to a different family after a limit hit, the
|
|
267
258
|
// implementer that actually ran is `actual`, not the originally `preselected`
|
|
268
259
|
// one. Re-record the resolved agent in the Backlog task so the post-active
|
|
269
260
|
// handoff and the autonomous review loop poll the correct Forgejo identity.
|
|
270
261
|
/** @param {{slug: string, preselected: string, actual: string, taskResolution: object, worktree: string, log?: Function, transitionTaskFn?: Function}} opts */
|
|
271
262
|
function applyExecuteFallback(opts) {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
taskResolution
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
return preselected;
|
|
283
|
-
}
|
|
284
|
-
const taskResolutionTyped2 = /** @type{{ok: boolean, taskFile?: string} | undefined} */(taskResolution);
|
|
285
|
-
if (taskResolutionTyped2 && taskResolutionTyped2.ok) {
|
|
286
|
-
log(fmt.status('INFO', `Execute agent fell back from ${fmt.agent(preselected)} to ${fmt.agent(actual)}; enforcing backlog assignee.`));
|
|
287
|
-
transitionTaskFn(slug, 'active', { implementer: actual, rootDir: worktree, log });
|
|
288
|
-
}
|
|
289
|
-
return actual;
|
|
263
|
+
const { slug, preselected, actual, taskResolution, worktree, log = fmt.log.plain, transitionTaskFn = backlog_js_1.transitionTask } = opts;
|
|
264
|
+
if (!actual || actual === preselected) {
|
|
265
|
+
return preselected;
|
|
266
|
+
}
|
|
267
|
+
const taskResolutionTyped2 = /** @type{{ok: boolean, taskFile?: string} | undefined} */ (taskResolution);
|
|
268
|
+
if (taskResolutionTyped2 && taskResolutionTyped2.ok) {
|
|
269
|
+
log(fmt.status('INFO', `Execute agent fell back from ${fmt.agent(preselected)} to ${fmt.agent(actual)}; enforcing backlog assignee.`));
|
|
270
|
+
transitionTaskFn(slug, 'active', { implementer: actual, rootDir: worktree, log });
|
|
271
|
+
}
|
|
272
|
+
return actual;
|
|
290
273
|
}
|
|
291
|
-
|
|
292
274
|
/**
|
|
293
275
|
* Attempt to relaunch an agent to fix a repairable handoff error.
|
|
294
276
|
* For resume-capable agents, the startAgent function handles resume flags internally.
|
|
@@ -308,115 +290,89 @@ function applyExecuteFallback(opts) {
|
|
|
308
290
|
* @param {{isRelaunchableErrorFn?: Function, buildRelaunchPromptFn?: Function, workflowLauncherStatusFn?: Function, startAgentFn?: Function, log?: Function, error?: Function}} [options]
|
|
309
291
|
*/
|
|
310
292
|
async function attemptAgentRelaunch(slug, worktree, errorMsg, agent, options = {}) {
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
log(`
|
|
348
|
-
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
if (typeof result.result.status === 'number' && result.result.status !== 0) {
|
|
357
|
-
error(`Relaunch agent exited with status ${result.result.status}`);
|
|
358
|
-
return { relaunched: false, error: `Agent exited with status ${result.result.status}` };
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
log(`Relaunch successful. ${fmt.agent(agent)} is now running to fix the handoff error.`);
|
|
362
|
-
return { relaunched: true };
|
|
363
|
-
} catch (err) {
|
|
364
|
-
error(`Relaunch failed with exception: ${/** @type{Error} */(err).message}`);
|
|
365
|
-
return { relaunched: false, error: /** @type{Error} */(err).message };
|
|
366
|
-
}
|
|
293
|
+
const { isRelaunchableErrorFn = repairHandoff.isRelaunchableError, buildRelaunchPromptFn = repairHandoff.buildRelaunchPrompt, workflowLauncherStatusFn = agents.workflowLauncherStatus, startAgentFn = agents.startAgent, log = fmt.log.plain, error = fmt.log.plainError } = options;
|
|
294
|
+
// Check if this is a relaunchable error
|
|
295
|
+
if (!isRelaunchableErrorFn(errorMsg)) {
|
|
296
|
+
log(`Error is not relaunchable: ${errorMsg}`);
|
|
297
|
+
return { relaunched: false, error: 'Error is not relaunchable for agent relaunch' };
|
|
298
|
+
}
|
|
299
|
+
// Check if the agent launcher is available
|
|
300
|
+
const status = workflowLauncherStatusFn(agent);
|
|
301
|
+
if (!status.supported) {
|
|
302
|
+
error(`Agent ${fmt.agent(agent)} is not available for relaunch: ${status.detail || status.reason || 'unknown'}`);
|
|
303
|
+
return { relaunched: false, error: `Agent ${agent} launcher is not available` };
|
|
304
|
+
}
|
|
305
|
+
// Build the relaunch prompt
|
|
306
|
+
const prompt = buildRelaunchPromptFn(errorMsg, slug, worktree);
|
|
307
|
+
log(`Attempting to relaunch ${fmt.agent(agent)} to fix repairable handoff error...`);
|
|
308
|
+
// startAgent handles resume flags internally for resume-capable agents (codex, claude, gemini, custom)
|
|
309
|
+
try {
|
|
310
|
+
const result = await startAgentFn('active', {
|
|
311
|
+
prompt,
|
|
312
|
+
worktree,
|
|
313
|
+
agent,
|
|
314
|
+
slug,
|
|
315
|
+
role: 'implementer',
|
|
316
|
+
// startAgent will use RESUME_CAPABLE set and session markers to decide resume
|
|
317
|
+
onLaunch: (/** @type{{agent: string}} */ { agent: launchedAgent }) => {
|
|
318
|
+
log(`Relaunched ${fmt.agent(launchedAgent)} for repair. Session persistence will be used if available.`);
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
if (result.error) {
|
|
322
|
+
error(`Relaunch failed: ${result.error.message || String(result.error)}`);
|
|
323
|
+
return { relaunched: false, error: result.error.message || String(result.error) };
|
|
324
|
+
}
|
|
325
|
+
if (typeof result.result.status === 'number' && result.result.status !== 0) {
|
|
326
|
+
error(`Relaunch agent exited with status ${result.result.status}`);
|
|
327
|
+
return { relaunched: false, error: `Agent exited with status ${result.result.status}` };
|
|
328
|
+
}
|
|
329
|
+
log(`Relaunch successful. ${fmt.agent(agent)} is now running to fix the handoff error.`);
|
|
330
|
+
return { relaunched: true };
|
|
331
|
+
}
|
|
332
|
+
catch (err) {
|
|
333
|
+
error(`Relaunch failed with exception: ${ /** @type{Error} */(err).message}`);
|
|
334
|
+
return { relaunched: false, error: /** @type{Error} */ (err).message };
|
|
335
|
+
}
|
|
367
336
|
}
|
|
368
|
-
|
|
369
337
|
/** @param {string} slug @param {string} worktree @param {{findMissionDirFn?: Function, findCheckpointsFn?: Function, runFn?: Function, log?: Function, error?: Function}} [options] */
|
|
370
338
|
function validateCheckpointsBeforeHandoff(slug, worktree, options = {}) {
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
.
|
|
406
|
-
|
|
407
|
-
if (dirtyCheckpointLines.length > 0) {
|
|
408
|
-
const dirtyPaths = dirtyCheckpointLines
|
|
409
|
-
.map((/** @type{string} */ line) => line.slice(3).trim())
|
|
410
|
-
.filter(Boolean);
|
|
411
|
-
const msg = `Checkpoint documents must be committed before handoff. Uncommitted checkpoint files: ${dirtyPaths.map((/** @type{string} */ p) => fmt.path(p)).join(', ')}. Commit the checkpoint update and re-run the handoff.`;
|
|
412
|
-
error(msg);
|
|
413
|
-
return { ok: false, error: msg };
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
log(fmt.status('PASS', `Found ${checkpoints.length} checkpoint document(s) in ${fmt.path(missionDir)}.`));
|
|
417
|
-
return { ok: true };
|
|
339
|
+
const { findMissionDirFn = mission_utils_js_1.findMissionDir, findCheckpointsFn = mission_utils_js_1.findCheckpoints, runFn = git_js_1.git, log = fmt.log.plain, error = fmt.log.plainError } = options;
|
|
340
|
+
const rootDir = worktree || process.cwd();
|
|
341
|
+
const missionDir = findMissionDirFn(slug, rootDir);
|
|
342
|
+
if (!missionDir) {
|
|
343
|
+
const msg = `Mission directory not found for slug: ${fmt.slug(slug)}. Cannot validate checkpoints.`;
|
|
344
|
+
error(msg);
|
|
345
|
+
return { ok: false, error: msg };
|
|
346
|
+
}
|
|
347
|
+
const checkpoints = findCheckpointsFn(missionDir);
|
|
348
|
+
if (checkpoints.length === 0) {
|
|
349
|
+
const msg = `No checkpoint documents found in ${fmt.path(missionDir)}. The execute agent must create checkpoint documents (CP-N.md) with a Goal Check table before handoff. Create at least CP-1 documenting your implementation, including a Goal Check table with real evidence (file:line, test names).`;
|
|
350
|
+
error(msg);
|
|
351
|
+
return { ok: false, error: msg };
|
|
352
|
+
}
|
|
353
|
+
const checkpointArgs = checkpoints.map((/** @type{string} */ checkpoint) => path.relative(rootDir, checkpoint) || checkpoint);
|
|
354
|
+
const statusResult = runFn(['status', '--porcelain', '--', ...checkpointArgs], { cwd: rootDir });
|
|
355
|
+
if (statusResult.status !== 0) {
|
|
356
|
+
const statusError = (statusResult.stderr || statusResult.stdout || '').trim() || 'git status failed';
|
|
357
|
+
const msg = `Could not verify checkpoint commit state for ${fmt.slug(slug)}: ${statusError}`;
|
|
358
|
+
error(msg);
|
|
359
|
+
return { ok: false, error: msg };
|
|
360
|
+
}
|
|
361
|
+
const dirtyCheckpointLines = (statusResult.stdout || '')
|
|
362
|
+
.split('\n')
|
|
363
|
+
.map((/** @type{string} */ line) => line.trimEnd())
|
|
364
|
+
.filter(Boolean);
|
|
365
|
+
if (dirtyCheckpointLines.length > 0) {
|
|
366
|
+
const dirtyPaths = dirtyCheckpointLines
|
|
367
|
+
.map((/** @type{string} */ line) => line.slice(3).trim())
|
|
368
|
+
.filter(Boolean);
|
|
369
|
+
const msg = `Checkpoint documents must be committed before handoff. Uncommitted checkpoint files: ${dirtyPaths.map((/** @type{string} */ p) => fmt.path(p)).join(', ')}. Commit the checkpoint update and re-run the handoff.`;
|
|
370
|
+
error(msg);
|
|
371
|
+
return { ok: false, error: msg };
|
|
372
|
+
}
|
|
373
|
+
log(fmt.status('PASS', `Found ${checkpoints.length} checkpoint document(s) in ${fmt.path(missionDir)}.`));
|
|
374
|
+
return { ok: true };
|
|
418
375
|
}
|
|
419
|
-
|
|
420
376
|
/**
|
|
421
377
|
* @param {string} slug
|
|
422
378
|
* @param {string} worktree
|
|
@@ -424,123 +380,108 @@ function validateCheckpointsBeforeHandoff(slug, worktree, options = {}) {
|
|
|
424
380
|
* @param {{taskFile?: string | null, validateCheckpointsBeforeHandoffFn?: Function, performHandoff?: Function, startReviewLoop?: Function, repairHandoffFn?: {isRelaunchableError: Function, buildRelaunchPrompt: Function}, attemptAgentRelaunchFn?: Function, log?: Function, error?: Function}} [options]
|
|
425
381
|
*/
|
|
426
382
|
async function runHandoffAndReview(slug, worktree, agent, options = {}) {
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
log(`\nAutomated handoff failed: ${handoffResult.error}`);
|
|
453
|
-
log(`Attempting post-execute repair...`);
|
|
454
|
-
const { repaired, blocker } = await /** @type{Function} */(repairHandoffFn)(slug, worktree, /** @type{string} */(handoffResult.error), { taskFile, log, error });
|
|
455
|
-
if (repaired) {
|
|
456
|
-
log(`Repair successful. Retrying automated handoff...`);
|
|
457
|
-
handoffResult = await _performHandoff(slug, { forgejoUser: agent, worktree, force: true });
|
|
458
|
-
} else if (blocker) {
|
|
459
|
-
// If repair failed but provided a specific blocker (e.g. rebase failure),
|
|
460
|
-
// report that blocker as the final error instead of the original handoff error.
|
|
461
|
-
handoffResult.error = blocker;
|
|
462
|
-
} else if (!repaired && repairHandoff.isRelaunchableError(handoffResult.error)) {
|
|
463
|
-
// Attempt agent relaunch for repairable content errors (missing goal-check table)
|
|
464
|
-
log(`Content error detected. Attempting agent relaunch to fix...`);
|
|
465
|
-
const { relaunched, error: relaunchError } = await attemptAgentRelaunchFn(
|
|
466
|
-
slug, worktree, /** @type{string} */(handoffResult.error), agent, { log, error }
|
|
467
|
-
);
|
|
468
|
-
if (relaunched) {
|
|
469
|
-
// Agent was relaunched successfully; re-invoke performHandoff to verify
|
|
470
|
-
// the handoff-to-review transition actually completed, matching the
|
|
471
|
-
// contract of the repair-success path above.
|
|
472
|
-
log(`Agent relaunched. It will fix the checkpoint and retry handoff.`);
|
|
473
|
-
handoffResult = await _performHandoff(slug, { forgejoUser: agent, worktree, force: true });
|
|
474
|
-
if (!handoffResult.ok) {
|
|
475
|
-
handoffResult.error = `Post-relaunch handoff failed: ${handoffResult.error || 'unknown'}`;
|
|
383
|
+
const { taskFile = null, validateCheckpointsBeforeHandoffFn = validateCheckpointsBeforeHandoff, performHandoff: _performHandoff = (/** @type{string} */ s, /** @type{object} */ o) => handoff.performHandoff(s, o), startReviewLoop: _startReviewLoop = (/** @type{string} */ s, /** @type{object} */ o) => review.startReviewLoop(s, o), repairHandoffFn = /** @type{(s: string, w: string, e: string, o: object) => Promise<{repaired: boolean, blocker?: string}>} */ (repairHandoff), attemptAgentRelaunchFn = attemptAgentRelaunch, log = fmt.log.plain, error = fmt.log.plainError } = options;
|
|
384
|
+
// Pre-handoff checkpoint enforcement: validate checkpoints exist before calling performHandoff()
|
|
385
|
+
// This catches missing checkpoints immediately after the execute agent exits,
|
|
386
|
+
// before the repair flow runs, and provides an explicit instruction to create them.
|
|
387
|
+
const validation = validateCheckpointsBeforeHandoffFn(slug, worktree, { log, error });
|
|
388
|
+
if (!validation.ok) {
|
|
389
|
+
// Don't attempt repair for missing checkpoints — instruct the agent to create them
|
|
390
|
+
error(` Create a checkpoint document (CP-N.md) in ${fmt.path((0, mission_utils_js_1.missionDirForSlug)(worktree, slug))} with a Goal Check table.`);
|
|
391
|
+
error(` Then re-run: ${fmt.command(`px review ${slug} --submit`)}`);
|
|
392
|
+
return false;
|
|
393
|
+
}
|
|
394
|
+
let handoffResult = await _performHandoff(slug, { forgejoUser: agent, worktree });
|
|
395
|
+
if (!handoffResult.ok) {
|
|
396
|
+
// Attempt single repair for routine hygiene issues (dirty artifacts, rebase needed)
|
|
397
|
+
log(`\nAutomated handoff failed: ${handoffResult.error}`);
|
|
398
|
+
log(`Attempting post-execute repair...`);
|
|
399
|
+
const { repaired, blocker } = await /** @type{Function} */ (repairHandoffFn)(slug, worktree, /** @type{string} */ (handoffResult.error), { taskFile, log, error });
|
|
400
|
+
if (repaired) {
|
|
401
|
+
log(`Repair successful. Retrying automated handoff...`);
|
|
402
|
+
handoffResult = await _performHandoff(slug, { forgejoUser: agent, worktree, force: true });
|
|
403
|
+
}
|
|
404
|
+
else if (blocker) {
|
|
405
|
+
// If repair failed but provided a specific blocker (e.g. rebase failure),
|
|
406
|
+
// report that blocker as the final error instead of the original handoff error.
|
|
407
|
+
handoffResult.error = blocker;
|
|
476
408
|
}
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
409
|
+
else if (!repaired && repairHandoff.isRelaunchableError(handoffResult.error)) {
|
|
410
|
+
// Attempt agent relaunch for repairable content errors (missing goal-check table)
|
|
411
|
+
log(`Content error detected. Attempting agent relaunch to fix...`);
|
|
412
|
+
const { relaunched, error: relaunchError } = await attemptAgentRelaunchFn(slug, worktree, /** @type{string} */ (handoffResult.error), agent, { log, error });
|
|
413
|
+
if (relaunched) {
|
|
414
|
+
// Agent was relaunched successfully; re-invoke performHandoff to verify
|
|
415
|
+
// the handoff-to-review transition actually completed, matching the
|
|
416
|
+
// contract of the repair-success path above.
|
|
417
|
+
log(`Agent relaunched. It will fix the checkpoint and retry handoff.`);
|
|
418
|
+
handoffResult = await _performHandoff(slug, { forgejoUser: agent, worktree, force: true });
|
|
419
|
+
if (!handoffResult.ok) {
|
|
420
|
+
handoffResult.error = `Post-relaunch handoff failed: ${handoffResult.error || 'unknown'}`;
|
|
421
|
+
}
|
|
422
|
+
// Fall through to gatekeeper pushback / review loop / failure handling below.
|
|
423
|
+
}
|
|
424
|
+
else {
|
|
425
|
+
// Relaunch failed or was not possible
|
|
426
|
+
log(`Agent relaunch failed: ${relaunchError || 'unknown error'}`);
|
|
427
|
+
// Fall through to manual handoff message
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
if (!handoffResult.ok) {
|
|
432
|
+
error(`Automated handoff failed: ${handoffResult.error}`);
|
|
433
|
+
error(' You may need to complete the handoff manually:');
|
|
434
|
+
error(` ${fmt.command(`px review ${slug} --submit`)}`);
|
|
435
|
+
return false;
|
|
436
|
+
}
|
|
437
|
+
/** @type{{ok: boolean, error?: string, gatekeeperPushedBack?: boolean}} */
|
|
438
|
+
const hr = handoffResult;
|
|
439
|
+
if (hr.gatekeeperPushedBack) {
|
|
440
|
+
log(`\nGatekeeper posted pushback for ${fmt.slug(slug)}; skipping autonomous review loop until artifacts are fixed.`);
|
|
441
|
+
return true;
|
|
442
|
+
}
|
|
443
|
+
log(`\nStarting autonomous review loop (implementer: ${fmt.agent(agent)})...`);
|
|
444
|
+
_startReviewLoop(slug, { implementer: agent, worktree, recordStageStatsSafeFn: review.recordStageStatsSafe });
|
|
497
445
|
return true;
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
log(`\nStarting autonomous review loop (implementer: ${fmt.agent(agent)})...`);
|
|
501
|
-
_startReviewLoop(slug, { implementer: agent, worktree, recordStageStatsSafeFn: review.recordStageStatsSafe });
|
|
502
|
-
return true;
|
|
503
446
|
}
|
|
504
|
-
|
|
505
447
|
/** @param {string} slug */
|
|
506
448
|
function buildCheckpointContext(slug) {
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
449
|
+
const missionDir = (0, mission_utils_js_1.findMissionDir)(slug);
|
|
450
|
+
if (!missionDir) {
|
|
451
|
+
return 'No checkpoint documents found. Start from CP-1.';
|
|
452
|
+
}
|
|
453
|
+
const checkpoints = (0, mission_utils_js_1.findCheckpoints)(missionDir);
|
|
454
|
+
if (checkpoints.length === 0) {
|
|
455
|
+
return 'No checkpoint documents found. Start from CP-1.';
|
|
456
|
+
}
|
|
457
|
+
const latest = checkpoints[checkpoints.length - 1];
|
|
458
|
+
const firstLine = (0, mission_utils_js_1.getFirstLine)(latest);
|
|
459
|
+
return `Most recent checkpoint: ${path.basename(latest)} — ${firstLine}\nResume from there, or start the next checkpoint if that one is complete.`;
|
|
516
460
|
}
|
|
517
|
-
|
|
518
461
|
/** @param {string} slug @param {string} rootDir */
|
|
519
462
|
function resolveExecuteTaskPath(slug, rootDir) {
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
463
|
+
const resolution = (0, backlog_js_1.resolveTaskFile)(slug, rootDir);
|
|
464
|
+
if (resolution && resolution.ok && resolution.taskFile) {
|
|
465
|
+
return resolution.taskFile;
|
|
466
|
+
}
|
|
467
|
+
return path.join(rootDir, 'backlog', 'tasks', `<${slug}>.md`);
|
|
525
468
|
}
|
|
526
|
-
|
|
527
469
|
/** @param {string} slug @param {string} checkpointContext @param {{rootDir?: string}} [options] */
|
|
528
470
|
function buildExecutePrompt(slug, checkpointContext, options = {}) {
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
471
|
+
const { rootDir = process.cwd() } = options;
|
|
472
|
+
const template = fs.readFileSync(EXECUTE_PROMPT_PATH, 'utf8');
|
|
473
|
+
const year = (0, mission_utils_js_1.getMissionYear)(slug, rootDir) || String(new Date().getFullYear());
|
|
474
|
+
const missionPath = path.join((0, mission_utils_js_1.missionDirForSlug)(rootDir, slug), 'MISSION.md');
|
|
475
|
+
const missionDir = path.dirname(missionPath);
|
|
476
|
+
const taskPath = resolveExecuteTaskPath(slug, rootDir);
|
|
477
|
+
return template
|
|
478
|
+
.replaceAll('{{slug}}', slug)
|
|
479
|
+
.replaceAll('YYYY', year)
|
|
480
|
+
.replaceAll('{{missionPath}}', missionPath)
|
|
481
|
+
.replaceAll('{{missionDir}}', missionDir)
|
|
482
|
+
.replaceAll('{{taskPath}}', taskPath)
|
|
483
|
+
.replaceAll('{{checkpoint_context}}', checkpointContext || '');
|
|
542
484
|
}
|
|
543
|
-
|
|
544
485
|
// git status --porcelain wraps any path containing a space or other unusual
|
|
545
486
|
// byte in double quotes and C-escapes the contents (\\, \", \t, \n, \r, and
|
|
546
487
|
// \NNN octal for bytes >= 0x80 under the default core.quotePath). Those quotes
|
|
@@ -549,123 +490,105 @@ function buildExecutePrompt(slug, checkpointContext, options = {}) {
|
|
|
549
490
|
// quote-wrapped string as a pathspec that matches no file and aborts staging.
|
|
550
491
|
/** @param {string} rawPath */
|
|
551
492
|
function unquoteGitStatusPath(rawPath) {
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
493
|
+
if (rawPath.length < 2 || rawPath[0] !== '"' || rawPath[rawPath.length - 1] !== '"') {
|
|
494
|
+
return rawPath;
|
|
495
|
+
}
|
|
496
|
+
const inner = rawPath.slice(1, -1);
|
|
497
|
+
const simple = { a: 0x07, b: 0x08, t: 0x09, n: 0x0a, v: 0x0b, f: 0x0c, r: 0x0d, '"': 0x22, '\\': 0x5c };
|
|
498
|
+
const chunks = [];
|
|
499
|
+
let literal = '';
|
|
500
|
+
const flush = () => {
|
|
501
|
+
if (literal) {
|
|
502
|
+
chunks.push(Buffer.from(literal, 'utf8'));
|
|
503
|
+
literal = '';
|
|
504
|
+
}
|
|
505
|
+
};
|
|
506
|
+
for (let i = 0; i < inner.length; i++) {
|
|
507
|
+
const ch = inner[i];
|
|
508
|
+
if (ch !== '\\') {
|
|
509
|
+
literal += ch;
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
512
|
+
const next = inner[i + 1];
|
|
513
|
+
if (next >= '0' && next <= '7') {
|
|
514
|
+
let octal = '';
|
|
515
|
+
let j = i + 1;
|
|
516
|
+
while (j < inner.length && octal.length < 3 && inner[j] >= '0' && inner[j] <= '7') {
|
|
517
|
+
octal += inner[j];
|
|
518
|
+
j++;
|
|
519
|
+
}
|
|
520
|
+
flush();
|
|
521
|
+
chunks.push(Buffer.from([parseInt(octal, 8) & 0xff]));
|
|
522
|
+
i = j - 1;
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
525
|
+
if (Object.prototype.hasOwnProperty.call(simple, next)) {
|
|
526
|
+
flush();
|
|
527
|
+
chunks.push(Buffer.from([/** @type{number} */ (simple[ /** @type{keyof typeof simple} */(next)])]));
|
|
528
|
+
i++;
|
|
529
|
+
}
|
|
530
|
+
else {
|
|
531
|
+
// Unknown or trailing escape: keep the following character literally.
|
|
532
|
+
literal += next === undefined ? '\\' : next;
|
|
533
|
+
if (next !== undefined) {
|
|
534
|
+
i++;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
flush();
|
|
539
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
596
540
|
}
|
|
597
|
-
|
|
598
541
|
/** @param {string} entry */
|
|
599
542
|
function parseDirtyEntry(entry) {
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
543
|
+
const match = entry.match(/^(.{1,2})\s+(.*)$/);
|
|
544
|
+
const status = (match ? match[1] : entry.slice(0, 2)).padEnd(2, ' ');
|
|
545
|
+
const rawPath = (match ? match[2] : entry.slice(2)).trim();
|
|
546
|
+
// For renames git reports `<old> -> <new>`; each side is independently quoted
|
|
547
|
+
// and the ` -> ` separator is always literal, so split before unquoting and
|
|
548
|
+
// keep staging the destination path.
|
|
549
|
+
const renamed = rawPath.includes(' -> ') ? (rawPath.split(' -> ').pop() || rawPath) : rawPath;
|
|
550
|
+
const filePath = unquoteGitStatusPath(renamed.trim());
|
|
551
|
+
return { status, filePath };
|
|
609
552
|
}
|
|
610
|
-
|
|
611
553
|
/** @param {string} filePath */
|
|
612
554
|
function isExecuteIgnoredPath(filePath) {
|
|
613
|
-
|
|
555
|
+
return (0, mission_utils_js_1.isWorkflowGeneratedArtifact)(filePath);
|
|
614
556
|
}
|
|
615
|
-
|
|
616
557
|
/** @param {{slug: string, worktree: string, dirtyEntries?: Array<{status: string, filePath: string}>, gitImpl?: Function}} opts */
|
|
617
558
|
function enforceExecuteCommitSafety(opts) {
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
'-m',
|
|
651
|
-
'Safety harness: capture implementation changes left uncommitted by the execute agent.'
|
|
652
|
-
]);
|
|
653
|
-
|
|
654
|
-
if (commitResult.status !== 0) {
|
|
655
|
-
throw new Error('Execute safety harness could not create fallback commit.');
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
fmt.log.pass(`Execute safety harness committed remaining changes with "${commitMessage}".`);
|
|
659
|
-
return true;
|
|
559
|
+
const { slug, worktree, dirtyEntries = (0, git_js_1.getWorktreeStatus)(worktree), gitImpl = git_js_1.git } = opts;
|
|
560
|
+
const parsedEntries = /** @type{Array<{status: string, filePath: string}>} */ ( /** @type{Array<string>} */(dirtyEntries).map(parseDirtyEntry));
|
|
561
|
+
const relevantEntries = parsedEntries.filter(entry => !isExecuteIgnoredPath(entry.filePath));
|
|
562
|
+
const conflictEntries = relevantEntries.filter(entry => ['DD', 'AU', 'UD', 'UA', 'DU', 'AA', 'UU'].includes(entry.status.trim()));
|
|
563
|
+
if (conflictEntries.length > 0) {
|
|
564
|
+
throw new Error(`Execute safety harness found unresolved conflicts: ${conflictEntries.map(entry => entry.filePath).join(', ')}`);
|
|
565
|
+
}
|
|
566
|
+
if (relevantEntries.length === 0) {
|
|
567
|
+
fmt.log.pass('Execute safety harness: no uncommitted mission changes left behind.');
|
|
568
|
+
return false;
|
|
569
|
+
}
|
|
570
|
+
fmt.log.warn('Execute safety harness: execute agent left uncommitted changes. Creating fallback commit.');
|
|
571
|
+
for (const entry of relevantEntries) {
|
|
572
|
+
fmt.log.plain(` ${entry.status} ${entry.filePath}`);
|
|
573
|
+
}
|
|
574
|
+
const stagePaths = relevantEntries.map(entry => entry.filePath);
|
|
575
|
+
gitImpl(['-C', worktree, 'add', '--', ...stagePaths]);
|
|
576
|
+
const commitMessage = `execute(${slug}): capture agent output`;
|
|
577
|
+
const commitResult = gitImpl([
|
|
578
|
+
'-C',
|
|
579
|
+
worktree,
|
|
580
|
+
'commit',
|
|
581
|
+
'-m',
|
|
582
|
+
commitMessage,
|
|
583
|
+
'-m',
|
|
584
|
+
'Safety harness: capture implementation changes left uncommitted by the execute agent.'
|
|
585
|
+
]);
|
|
586
|
+
if (commitResult.status !== 0) {
|
|
587
|
+
throw new Error('Execute safety harness could not create fallback commit.');
|
|
588
|
+
}
|
|
589
|
+
fmt.log.pass(`Execute safety harness committed remaining changes with "${commitMessage}".`);
|
|
590
|
+
return true;
|
|
660
591
|
}
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
module.exports
|
|
664
|
-
module.exports.buildCheckpointContext = buildCheckpointContext;
|
|
665
|
-
module.exports.runHandoffAndReview = runHandoffAndReview;
|
|
666
|
-
module.exports.applyExecuteFallback = applyExecuteFallback;
|
|
667
|
-
module.exports.selectLaunchAndRecord = selectLaunchAndRecord;
|
|
668
|
-
module.exports.validateCheckpointsBeforeHandoff = validateCheckpointsBeforeHandoff;
|
|
669
|
-
module.exports.attemptAgentRelaunch = attemptAgentRelaunch;
|
|
670
|
-
module.exports.enforceExecuteCommitSafety = enforceExecuteCommitSafety;
|
|
671
|
-
module.exports.unquoteGitStatusPath = unquoteGitStatusPath;
|
|
592
|
+
/** @type {typeof active & {buildExecutePrompt: typeof buildExecutePrompt, buildCheckpointContext: typeof buildCheckpointContext, runHandoffAndReview: typeof runHandoffAndReview, applyExecuteFallback: typeof applyExecuteFallback, selectLaunchAndRecord: typeof selectLaunchAndRecord, validateCheckpointsBeforeHandoff: typeof validateCheckpointsBeforeHandoff, attemptAgentRelaunch: typeof attemptAgentRelaunch, enforceExecuteCommitSafety: typeof enforceExecuteCommitSafety, unquoteGitStatusPath: typeof unquoteGitStatusPath}} */
|
|
593
|
+
const _activeExport = Object.assign(active, { buildExecutePrompt, buildCheckpointContext, runHandoffAndReview, applyExecuteFallback, selectLaunchAndRecord, validateCheckpointsBeforeHandoff, attemptAgentRelaunch, enforceExecuteCommitSafety, unquoteGitStatusPath });
|
|
594
|
+
module.exports = _activeExport;
|