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