@caseyharalson/orrery 0.10.0 → 0.12.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/HELP.md +429 -0
- package/README.md +10 -7
- package/agent/skills/discovery/SKILL.md +13 -5
- package/agent/skills/orrery-execute/SKILL.md +1 -1
- package/agent/skills/refine-plan/SKILL.md +3 -2
- package/agent/skills/simulate-plan/SKILL.md +3 -2
- package/lib/cli/commands/manual.js +22 -0
- package/lib/cli/commands/orchestrate.js +52 -0
- package/lib/cli/commands/plans-dir.js +10 -0
- package/lib/cli/commands/resume.js +96 -33
- package/lib/cli/commands/status.js +13 -0
- package/lib/cli/index.js +4 -0
- package/lib/orchestration/index.js +220 -144
- package/lib/utils/git.js +9 -2
- package/lib/utils/lock.js +170 -0
- package/lib/utils/paths.js +29 -2
- package/package.json +4 -2
package/lib/utils/git.js
CHANGED
|
@@ -22,7 +22,9 @@ function git(command, cwd) {
|
|
|
22
22
|
} catch (error) {
|
|
23
23
|
// Return stderr if available, otherwise throw
|
|
24
24
|
if (error.stderr) {
|
|
25
|
-
throw new Error(`git ${command} failed: ${error.stderr.trim()}
|
|
25
|
+
throw new Error(`git ${command} failed: ${error.stderr.trim()}`, {
|
|
26
|
+
cause: error
|
|
27
|
+
});
|
|
26
28
|
}
|
|
27
29
|
throw error;
|
|
28
30
|
}
|
|
@@ -34,7 +36,12 @@ function git(command, cwd) {
|
|
|
34
36
|
* @returns {string} - Current branch name
|
|
35
37
|
*/
|
|
36
38
|
function getCurrentBranch(cwd) {
|
|
37
|
-
|
|
39
|
+
try {
|
|
40
|
+
return git("rev-parse --abbrev-ref HEAD", cwd);
|
|
41
|
+
} catch {
|
|
42
|
+
// Unborn branch (no commits yet) — symbolic-ref still works
|
|
43
|
+
return git("symbolic-ref --short HEAD", cwd);
|
|
44
|
+
}
|
|
38
45
|
}
|
|
39
46
|
|
|
40
47
|
/**
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
const { execSync } = require("child_process");
|
|
2
|
+
const fs = require("fs");
|
|
3
|
+
const path = require("path");
|
|
4
|
+
|
|
5
|
+
const { getWorkDir } = require("./paths");
|
|
6
|
+
|
|
7
|
+
const LOCK_FILE = "exec.lock";
|
|
8
|
+
|
|
9
|
+
function getLockPath() {
|
|
10
|
+
return path.join(getWorkDir(), LOCK_FILE);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Check if a process with the given PID is running.
|
|
15
|
+
* @param {number} pid - Process ID
|
|
16
|
+
* @returns {boolean}
|
|
17
|
+
*/
|
|
18
|
+
function isProcessRunning(pid) {
|
|
19
|
+
try {
|
|
20
|
+
process.kill(pid, 0);
|
|
21
|
+
return true;
|
|
22
|
+
} catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Check if a PID belongs to an orrery process.
|
|
29
|
+
* @param {number} pid - Process ID
|
|
30
|
+
* @returns {boolean}
|
|
31
|
+
*/
|
|
32
|
+
function isOrreryProcess(pid) {
|
|
33
|
+
try {
|
|
34
|
+
// Linux: read /proc/<pid>/cmdline (null-separated args)
|
|
35
|
+
const cmdlinePath = `/proc/${pid}/cmdline`;
|
|
36
|
+
if (fs.existsSync(cmdlinePath)) {
|
|
37
|
+
const raw = fs.readFileSync(cmdlinePath, "utf8");
|
|
38
|
+
const args = raw.split("\0").filter(Boolean);
|
|
39
|
+
// Check if any argument ends with the orrery binary (bin/orrery.js or bin/orrery)
|
|
40
|
+
return args.some(
|
|
41
|
+
(arg) => arg.endsWith("bin/orrery.js") || arg.endsWith("bin/orrery")
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// macOS/other: use ps
|
|
46
|
+
const args = execSync(`ps -p ${pid} -o args=`, {
|
|
47
|
+
encoding: "utf8",
|
|
48
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
49
|
+
}).trim();
|
|
50
|
+
return args.includes("bin/orrery.js") || args.includes("bin/orrery ");
|
|
51
|
+
} catch {
|
|
52
|
+
// Cannot determine — treat as stale (safe default)
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Read and parse the lock file.
|
|
59
|
+
* @returns {{pid: number, startedAt: string, command: string}|null}
|
|
60
|
+
*/
|
|
61
|
+
function readLock() {
|
|
62
|
+
const lockPath = getLockPath();
|
|
63
|
+
try {
|
|
64
|
+
const content = fs.readFileSync(lockPath, "utf8");
|
|
65
|
+
return JSON.parse(content);
|
|
66
|
+
} catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Attempt to acquire the execution lock.
|
|
73
|
+
* @returns {{acquired: boolean, reason?: string, pid?: number}}
|
|
74
|
+
*/
|
|
75
|
+
function acquireLock() {
|
|
76
|
+
const lockPath = getLockPath();
|
|
77
|
+
const lockData = {
|
|
78
|
+
pid: process.pid,
|
|
79
|
+
startedAt: new Date().toISOString(),
|
|
80
|
+
command: process.argv.slice(2).join(" ")
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// Check for existing lock
|
|
84
|
+
const existing = readLock();
|
|
85
|
+
if (existing) {
|
|
86
|
+
const running = isProcessRunning(existing.pid);
|
|
87
|
+
if (running && isOrreryProcess(existing.pid)) {
|
|
88
|
+
return {
|
|
89
|
+
acquired: false,
|
|
90
|
+
reason: `Another orrery process is running (PID ${existing.pid}, started ${existing.startedAt})`,
|
|
91
|
+
pid: existing.pid
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Stale lock — remove it
|
|
96
|
+
try {
|
|
97
|
+
fs.unlinkSync(lockPath);
|
|
98
|
+
} catch {
|
|
99
|
+
// Ignore cleanup errors
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Atomic create
|
|
104
|
+
try {
|
|
105
|
+
fs.writeFileSync(lockPath, JSON.stringify(lockData, null, 2) + "\n", {
|
|
106
|
+
flag: "wx"
|
|
107
|
+
});
|
|
108
|
+
return { acquired: true };
|
|
109
|
+
} catch (err) {
|
|
110
|
+
if (err.code === "EEXIST") {
|
|
111
|
+
// Race condition — another process acquired between check and write
|
|
112
|
+
const raceWinner = readLock();
|
|
113
|
+
return {
|
|
114
|
+
acquired: false,
|
|
115
|
+
reason: `Another orrery process just started (PID ${raceWinner?.pid || "unknown"})`,
|
|
116
|
+
pid: raceWinner?.pid
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
acquired: false,
|
|
121
|
+
reason: `Failed to create lock file: ${err.message}`
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Release the execution lock (only if owned by current process).
|
|
128
|
+
*/
|
|
129
|
+
function releaseLock() {
|
|
130
|
+
const lockPath = getLockPath();
|
|
131
|
+
const existing = readLock();
|
|
132
|
+
|
|
133
|
+
if (existing && existing.pid === process.pid) {
|
|
134
|
+
try {
|
|
135
|
+
fs.unlinkSync(lockPath);
|
|
136
|
+
} catch {
|
|
137
|
+
// Ignore cleanup errors
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Get the current lock status (read-only).
|
|
144
|
+
* @returns {{locked: boolean, pid?: number, startedAt?: string, stale: boolean}}
|
|
145
|
+
*/
|
|
146
|
+
function getLockStatus() {
|
|
147
|
+
const existing = readLock();
|
|
148
|
+
|
|
149
|
+
if (!existing) {
|
|
150
|
+
return { locked: false, stale: false };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const running = isProcessRunning(existing.pid);
|
|
154
|
+
const isOrrery = running && isOrreryProcess(existing.pid);
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
locked: isOrrery,
|
|
158
|
+
pid: existing.pid,
|
|
159
|
+
startedAt: existing.startedAt,
|
|
160
|
+
stale: !isOrrery
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
module.exports = {
|
|
165
|
+
acquireLock,
|
|
166
|
+
releaseLock,
|
|
167
|
+
getLockStatus,
|
|
168
|
+
isProcessRunning,
|
|
169
|
+
isOrreryProcess
|
|
170
|
+
};
|
package/lib/utils/paths.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const crypto = require("crypto");
|
|
1
2
|
const fs = require("fs");
|
|
2
3
|
const path = require("path");
|
|
3
4
|
|
|
@@ -10,10 +11,28 @@ function ensureDir(dirPath) {
|
|
|
10
11
|
return dirPath;
|
|
11
12
|
}
|
|
12
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Generate a deterministic project identifier from the current working directory.
|
|
16
|
+
* Format: <sanitized-basename>-<hash8>
|
|
17
|
+
* @returns {string} - Project identifier
|
|
18
|
+
*/
|
|
19
|
+
function getProjectId() {
|
|
20
|
+
const cwd = path.resolve(process.cwd());
|
|
21
|
+
const basename = path.basename(cwd) || "root";
|
|
22
|
+
const sanitized = basename.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
23
|
+
const hash = crypto
|
|
24
|
+
.createHash("sha256")
|
|
25
|
+
.update(cwd)
|
|
26
|
+
.digest("hex")
|
|
27
|
+
.slice(0, 8);
|
|
28
|
+
return `${sanitized}-${hash}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
13
31
|
function getWorkDir() {
|
|
14
32
|
const envDir = process.env[WORK_DIR_ENV];
|
|
15
33
|
if (envDir && envDir.trim()) {
|
|
16
|
-
|
|
34
|
+
const projectScoped = path.join(envDir.trim(), getProjectId());
|
|
35
|
+
return ensureDir(projectScoped);
|
|
17
36
|
}
|
|
18
37
|
return ensureDir(path.join(process.cwd(), ".agent-work"));
|
|
19
38
|
}
|
|
@@ -34,10 +53,18 @@ function getTempDir() {
|
|
|
34
53
|
return ensureDir(path.join(getWorkDir(), "temp"));
|
|
35
54
|
}
|
|
36
55
|
|
|
56
|
+
function isWorkDirExternal() {
|
|
57
|
+
const workDir = path.resolve(getWorkDir());
|
|
58
|
+
const cwd = path.resolve(process.cwd());
|
|
59
|
+
return !workDir.startsWith(cwd + path.sep) && workDir !== cwd;
|
|
60
|
+
}
|
|
61
|
+
|
|
37
62
|
module.exports = {
|
|
38
63
|
getWorkDir,
|
|
39
64
|
getPlansDir,
|
|
40
65
|
getCompletedDir,
|
|
41
66
|
getReportsDir,
|
|
42
|
-
getTempDir
|
|
67
|
+
getTempDir,
|
|
68
|
+
getProjectId,
|
|
69
|
+
isWorkDirExternal
|
|
43
70
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@caseyharalson/orrery",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "Workflow planning and orchestration CLI for AI agents",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Casey Haralson",
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
".devcontainer.example",
|
|
42
42
|
"agent",
|
|
43
43
|
"bin",
|
|
44
|
+
"HELP.md",
|
|
44
45
|
"lib",
|
|
45
46
|
"LICENSE",
|
|
46
47
|
"README.md",
|
|
@@ -51,7 +52,8 @@
|
|
|
51
52
|
"yaml": "^2.8.2"
|
|
52
53
|
},
|
|
53
54
|
"devDependencies": {
|
|
54
|
-
"eslint": "^
|
|
55
|
+
"@eslint/js": "^10.0.1",
|
|
56
|
+
"eslint": "^10.0.1",
|
|
55
57
|
"eslint-config-prettier": "^10.1.8",
|
|
56
58
|
"prettier": "^3.8.1"
|
|
57
59
|
}
|