@caseyharalson/orrery 0.13.1 → 0.14.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/lib/cli/commands/resume.js +16 -11
- package/lib/cli/commands/stop.js +146 -0
- package/lib/cli/index.js +2 -0
- package/lib/orchestration/index.js +17 -2
- package/lib/utils/stop-signal.js +51 -0
- package/package.json +1 -1
|
@@ -93,6 +93,7 @@ module.exports = function registerResumeCommand(program) {
|
|
|
93
93
|
|
|
94
94
|
let planFile;
|
|
95
95
|
let plan;
|
|
96
|
+
let hasWorktree = false;
|
|
96
97
|
|
|
97
98
|
if (options.plan) {
|
|
98
99
|
// Resolve plan path (same pattern as status.js)
|
|
@@ -134,7 +135,7 @@ module.exports = function registerResumeCommand(program) {
|
|
|
134
135
|
".worktrees",
|
|
135
136
|
`plan-${planId}`
|
|
136
137
|
);
|
|
137
|
-
|
|
138
|
+
hasWorktree = fs.existsSync(worktreePath);
|
|
138
139
|
|
|
139
140
|
if (!hasWorktree) {
|
|
140
141
|
// Verify current branch matches plan's work_branch
|
|
@@ -258,17 +259,21 @@ module.exports = function registerResumeCommand(program) {
|
|
|
258
259
|
console.log(` ${colorize("pending", "yellow")} ${step.id}`);
|
|
259
260
|
}
|
|
260
261
|
|
|
261
|
-
// 7. Commit the plan file changes
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
262
|
+
// 7. Commit the plan file changes (only when not using a worktree,
|
|
263
|
+
// since the plan file lives in the main repo's .agent-work/, not
|
|
264
|
+
// inside the worktree's git tree)
|
|
265
|
+
if (!hasWorktree) {
|
|
266
|
+
const planName = planFileName.replace(/\.ya?ml$/, "");
|
|
267
|
+
const commitMessage = `chore: unblock steps in ${planName}`;
|
|
268
|
+
const commitHash = commit(commitMessage, [planFile], process.cwd());
|
|
265
269
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
270
|
+
if (commitHash) {
|
|
271
|
+
console.log(
|
|
272
|
+
`\nCommitted: ${commitMessage} (${commitHash.slice(0, 7)})`
|
|
273
|
+
);
|
|
274
|
+
} else {
|
|
275
|
+
console.log("\n(no changes to commit)");
|
|
276
|
+
}
|
|
272
277
|
}
|
|
273
278
|
|
|
274
279
|
// 8. Resume orchestration
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
const path = require("path");
|
|
2
|
+
|
|
3
|
+
const { derivePlanId } = require("../../utils/git");
|
|
4
|
+
const {
|
|
5
|
+
getLockStatus,
|
|
6
|
+
listPlanLocks,
|
|
7
|
+
readLock,
|
|
8
|
+
isOrreryProcess
|
|
9
|
+
} = require("../../utils/lock");
|
|
10
|
+
const { requestStop, clearStopSignal } = require("../../utils/stop-signal");
|
|
11
|
+
|
|
12
|
+
function supportsColor() {
|
|
13
|
+
return Boolean(process.stdout.isTTY) && !process.env.NO_COLOR;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function colorize(text, color) {
|
|
17
|
+
if (!supportsColor()) return text;
|
|
18
|
+
const colors = {
|
|
19
|
+
green: "\x1b[32m",
|
|
20
|
+
yellow: "\x1b[33m",
|
|
21
|
+
red: "\x1b[31m",
|
|
22
|
+
reset: "\x1b[0m"
|
|
23
|
+
};
|
|
24
|
+
return `${colors[color] || ""}${text}${colors.reset}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Send SIGTERM to a process after verifying it's an orrery process.
|
|
29
|
+
* @param {number} pid - Process ID
|
|
30
|
+
* @param {string} label - Label for log messages
|
|
31
|
+
* @returns {boolean} - Whether the signal was sent
|
|
32
|
+
*/
|
|
33
|
+
function killProcess(pid, label) {
|
|
34
|
+
if (!isOrreryProcess(pid)) {
|
|
35
|
+
console.log(
|
|
36
|
+
` ${colorize("skipped", "yellow")} ${label} — PID ${pid} is not an orrery process`
|
|
37
|
+
);
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
process.kill(pid, "SIGTERM");
|
|
43
|
+
console.log(
|
|
44
|
+
` ${colorize("stopped", "green")} ${label} (PID ${pid}, sent SIGTERM)`
|
|
45
|
+
);
|
|
46
|
+
return true;
|
|
47
|
+
} catch (err) {
|
|
48
|
+
if (err.code === "ESRCH") {
|
|
49
|
+
console.log(
|
|
50
|
+
` ${colorize("skipped", "yellow")} ${label} — PID ${pid} already exited`
|
|
51
|
+
);
|
|
52
|
+
} else {
|
|
53
|
+
console.log(` ${colorize("failed", "red")} ${label} — ${err.message}`);
|
|
54
|
+
}
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = function registerStopCommand(program) {
|
|
60
|
+
program
|
|
61
|
+
.command("stop")
|
|
62
|
+
.description("Stop running orchestrations")
|
|
63
|
+
.option("--plan <file>", "Stop a specific plan by name or file")
|
|
64
|
+
.option(
|
|
65
|
+
"--graceful",
|
|
66
|
+
"Finish current step(s) then stop (instead of immediate SIGTERM)"
|
|
67
|
+
)
|
|
68
|
+
.action((options) => {
|
|
69
|
+
const graceful = options.graceful || false;
|
|
70
|
+
|
|
71
|
+
if (options.plan) {
|
|
72
|
+
// Stop a specific plan
|
|
73
|
+
const planBasename = path.basename(options.plan);
|
|
74
|
+
const planId = derivePlanId(planBasename);
|
|
75
|
+
|
|
76
|
+
const lock = readLock(planId);
|
|
77
|
+
if (!lock) {
|
|
78
|
+
console.log(`No active execution found for plan "${planId}".`);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const status = getLockStatus(planId);
|
|
83
|
+
if (status.stale) {
|
|
84
|
+
console.log(
|
|
85
|
+
`Lock for plan "${planId}" is stale (PID ${lock.pid} no longer running).`
|
|
86
|
+
);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (!status.locked) {
|
|
91
|
+
console.log(`No active execution found for plan "${planId}".`);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (graceful) {
|
|
96
|
+
requestStop(planId);
|
|
97
|
+
console.log(
|
|
98
|
+
`Graceful stop requested for plan "${planId}" — will stop after current step(s) finish.`
|
|
99
|
+
);
|
|
100
|
+
} else {
|
|
101
|
+
killProcess(lock.pid, `plan "${planId}"`);
|
|
102
|
+
clearStopSignal(planId);
|
|
103
|
+
}
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Stop all running plans
|
|
108
|
+
const planLocks = listPlanLocks();
|
|
109
|
+
const activeLocks = planLocks.filter((l) => l.active);
|
|
110
|
+
|
|
111
|
+
// Also check global lock
|
|
112
|
+
const globalStatus = getLockStatus();
|
|
113
|
+
const globalLock = globalStatus.locked ? readLock() : null;
|
|
114
|
+
|
|
115
|
+
if (activeLocks.length === 0 && !globalLock) {
|
|
116
|
+
console.log("No active orchestrations found.");
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (graceful) {
|
|
121
|
+
// Write signal files for all active plans + global
|
|
122
|
+
if (globalLock) {
|
|
123
|
+
requestStop();
|
|
124
|
+
console.log(
|
|
125
|
+
"Graceful stop requested for global execution — will stop after current step(s) finish."
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
for (const pl of activeLocks) {
|
|
129
|
+
requestStop(pl.planId);
|
|
130
|
+
console.log(
|
|
131
|
+
`Graceful stop requested for plan "${pl.planId}" — will stop after current step(s) finish.`
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
} else {
|
|
135
|
+
// Immediate stop via SIGTERM
|
|
136
|
+
if (globalLock) {
|
|
137
|
+
killProcess(globalLock.pid, "global execution");
|
|
138
|
+
clearStopSignal();
|
|
139
|
+
}
|
|
140
|
+
for (const pl of activeLocks) {
|
|
141
|
+
killProcess(pl.pid, `plan "${pl.planId}"`);
|
|
142
|
+
clearStopSignal(pl.planId);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
};
|
package/lib/cli/index.js
CHANGED
|
@@ -7,6 +7,7 @@ const registerInstallDevcontainer = require("./commands/install-devcontainer");
|
|
|
7
7
|
const registerOrchestrate = require("./commands/orchestrate");
|
|
8
8
|
const registerStatus = require("./commands/status");
|
|
9
9
|
const registerResume = require("./commands/resume");
|
|
10
|
+
const registerStop = require("./commands/stop");
|
|
10
11
|
const registerValidatePlan = require("./commands/validate-plan");
|
|
11
12
|
const registerIngestPlan = require("./commands/ingest-plan");
|
|
12
13
|
const registerManual = require("./commands/manual");
|
|
@@ -34,6 +35,7 @@ function buildProgram() {
|
|
|
34
35
|
registerOrchestrate(program);
|
|
35
36
|
registerStatus(program);
|
|
36
37
|
registerResume(program);
|
|
38
|
+
registerStop(program);
|
|
37
39
|
registerValidatePlan(program);
|
|
38
40
|
registerIngestPlan(program);
|
|
39
41
|
registerManual(program);
|
|
@@ -77,6 +77,7 @@ const {
|
|
|
77
77
|
|
|
78
78
|
const { ProgressTracker } = require("./progress-tracker");
|
|
79
79
|
const { acquireLock, releaseLock } = require("../utils/lock");
|
|
80
|
+
const { isStopRequested, clearStopSignal } = require("../utils/stop-signal");
|
|
80
81
|
|
|
81
82
|
const REPO_ROOT = process.cwd();
|
|
82
83
|
|
|
@@ -333,8 +334,9 @@ async function orchestrate(options = {}) {
|
|
|
333
334
|
return;
|
|
334
335
|
}
|
|
335
336
|
|
|
336
|
-
// Clean up lock on signals
|
|
337
|
+
// Clean up lock and stop signals on signals
|
|
337
338
|
const cleanupLock = () => {
|
|
339
|
+
clearStopSignal();
|
|
338
340
|
releaseLock();
|
|
339
341
|
process.exit();
|
|
340
342
|
};
|
|
@@ -519,6 +521,8 @@ async function resumeInWorktree(
|
|
|
519
521
|
}
|
|
520
522
|
|
|
521
523
|
const cleanupLock = () => {
|
|
524
|
+
clearStopSignal(planId);
|
|
525
|
+
clearStopSignal();
|
|
522
526
|
releaseLock(planId);
|
|
523
527
|
process.exit();
|
|
524
528
|
};
|
|
@@ -921,8 +925,10 @@ async function processPlanInWorktree(normalizedOptions) {
|
|
|
921
925
|
return;
|
|
922
926
|
}
|
|
923
927
|
|
|
924
|
-
// Clean up per-plan lock on signals
|
|
928
|
+
// Clean up per-plan lock and stop signals on signals
|
|
925
929
|
const cleanupLock = () => {
|
|
930
|
+
clearStopSignal(planId);
|
|
931
|
+
clearStopSignal();
|
|
926
932
|
releaseLock(planId);
|
|
927
933
|
process.exit();
|
|
928
934
|
};
|
|
@@ -1341,7 +1347,16 @@ async function processPlan(
|
|
|
1341
1347
|
}
|
|
1342
1348
|
|
|
1343
1349
|
// Main execution loop
|
|
1350
|
+
const loopPlanId = derivePlanId(path.basename(planFile));
|
|
1344
1351
|
while (!plan.isComplete()) {
|
|
1352
|
+
// Check for graceful stop signal
|
|
1353
|
+
if (isStopRequested(loopPlanId) || isStopRequested()) {
|
|
1354
|
+
console.log("\nStop signal received — stopping after current step(s).");
|
|
1355
|
+
clearStopSignal(loopPlanId);
|
|
1356
|
+
clearStopSignal();
|
|
1357
|
+
break;
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1345
1360
|
// Get steps ready to execute
|
|
1346
1361
|
const readySteps = getReadySteps(plan);
|
|
1347
1362
|
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
|
|
4
|
+
const { getWorkDir } = require("./paths");
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Get the path to the stop signal file.
|
|
8
|
+
* @param {string} [planId] - Optional plan ID for per-plan signals
|
|
9
|
+
* @returns {string} - Path to the signal file
|
|
10
|
+
*/
|
|
11
|
+
function getStopSignalPath(planId) {
|
|
12
|
+
const fileName = planId ? `stop-${planId}.signal` : "stop.signal";
|
|
13
|
+
return path.join(getWorkDir(), fileName);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Write a stop signal file to request graceful stop.
|
|
18
|
+
* @param {string} [planId] - Optional plan ID for per-plan signals
|
|
19
|
+
*/
|
|
20
|
+
function requestStop(planId) {
|
|
21
|
+
const signalPath = getStopSignalPath(planId);
|
|
22
|
+
fs.writeFileSync(signalPath, new Date().toISOString() + "\n");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Check if a stop signal has been requested.
|
|
27
|
+
* @param {string} [planId] - Optional plan ID for per-plan signals
|
|
28
|
+
* @returns {boolean}
|
|
29
|
+
*/
|
|
30
|
+
function isStopRequested(planId) {
|
|
31
|
+
return fs.existsSync(getStopSignalPath(planId));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Remove the stop signal file.
|
|
36
|
+
* @param {string} [planId] - Optional plan ID for per-plan signals
|
|
37
|
+
*/
|
|
38
|
+
function clearStopSignal(planId) {
|
|
39
|
+
try {
|
|
40
|
+
fs.unlinkSync(getStopSignalPath(planId));
|
|
41
|
+
} catch {
|
|
42
|
+
// Ignore if file doesn't exist
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = {
|
|
47
|
+
getStopSignalPath,
|
|
48
|
+
requestStop,
|
|
49
|
+
isStopRequested,
|
|
50
|
+
clearStopSignal
|
|
51
|
+
};
|