@gh-symphony/cli 0.0.11 → 0.0.12
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/dist/commands/logs.js +71 -21
- package/dist/commands/project.js +1 -1
- package/dist/commands/recover.js +2 -3
- package/dist/commands/run.js +27 -4
- package/dist/commands/start.d.ts +6 -0
- package/dist/commands/start.js +121 -57
- package/dist/commands/status.js +2 -3
- package/dist/index.js +6 -0
- package/dist/orchestrator-runtime.d.ts +0 -4
- package/dist/orchestrator-runtime.js +1 -23
- package/dist/skills/templates/gh-symphony.js +1 -1
- package/dist/workflow/generate-reference-workflow.js +1 -1
- package/dist/workflow/generate-workflow-md.js +1 -1
- package/package.json +5 -5
package/dist/commands/logs.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFile, readdir } from "node:fs/promises";
|
|
1
|
+
import { readFile, readdir, stat } from "node:fs/promises";
|
|
2
2
|
import { join, resolve } from "node:path";
|
|
3
3
|
import { createReadStream } from "node:fs";
|
|
4
4
|
import { createInterface } from "node:readline";
|
|
@@ -32,9 +32,17 @@ function parseLogsArgs(args) {
|
|
|
32
32
|
}
|
|
33
33
|
const handler = async (args, options) => {
|
|
34
34
|
const parsed = parseLogsArgs(args);
|
|
35
|
+
const runtimeRoot = resolve(options.configDir);
|
|
35
36
|
// If --run is specified, read that run's events
|
|
36
37
|
if (parsed.run) {
|
|
37
|
-
const eventsPath =
|
|
38
|
+
const eventsPath = parsed.projectId
|
|
39
|
+
? join(runtimeRoot, "projects", parsed.projectId, "runs", parsed.run, "events.ndjson")
|
|
40
|
+
: await resolveRunEventsPath(runtimeRoot, parsed.run);
|
|
41
|
+
if (!eventsPath) {
|
|
42
|
+
process.stderr.write(`No events found for run: ${parsed.run}\n`);
|
|
43
|
+
process.exitCode = 1;
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
38
46
|
try {
|
|
39
47
|
const content = await readFile(eventsPath, "utf8");
|
|
40
48
|
const lines = content.trim().split("\n").filter(Boolean);
|
|
@@ -96,31 +104,43 @@ const handler = async (args, options) => {
|
|
|
96
104
|
return;
|
|
97
105
|
}
|
|
98
106
|
// Scan all run events
|
|
99
|
-
const
|
|
107
|
+
const runRoots = parsed.projectId
|
|
108
|
+
? [join(runtimeRoot, "projects", parsed.projectId, "runs")]
|
|
109
|
+
: await listProjectRunRoots(runtimeRoot);
|
|
110
|
+
let foundRuns = false;
|
|
100
111
|
try {
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
const content = await readFile(eventsPath, "utf8");
|
|
106
|
-
const lines = content.trim().split("\n").filter(Boolean);
|
|
107
|
-
for (const line of lines) {
|
|
108
|
-
const event = JSON.parse(line);
|
|
109
|
-
if (parsed.projectId && event.projectId !== parsed.projectId)
|
|
110
|
-
continue;
|
|
111
|
-
if (parsed.level && event.level !== parsed.level)
|
|
112
|
-
continue;
|
|
113
|
-
if (parsed.issue && event.issueIdentifier !== parsed.issue)
|
|
114
|
-
continue;
|
|
115
|
-
process.stdout.write(formatEvent(event) + "\n");
|
|
116
|
-
}
|
|
112
|
+
for (const runsDir of runRoots) {
|
|
113
|
+
const entries = await safeReadDir(runsDir);
|
|
114
|
+
if (entries.length === 0) {
|
|
115
|
+
continue;
|
|
117
116
|
}
|
|
118
|
-
|
|
119
|
-
|
|
117
|
+
foundRuns = true;
|
|
118
|
+
for (const entry of entries) {
|
|
119
|
+
const eventsPath = join(runsDir, entry, "events.ndjson");
|
|
120
|
+
try {
|
|
121
|
+
const content = await readFile(eventsPath, "utf8");
|
|
122
|
+
const lines = content.trim().split("\n").filter(Boolean);
|
|
123
|
+
for (const line of lines) {
|
|
124
|
+
const event = JSON.parse(line);
|
|
125
|
+
if (parsed.projectId && event.projectId !== parsed.projectId)
|
|
126
|
+
continue;
|
|
127
|
+
if (parsed.level && event.level !== parsed.level)
|
|
128
|
+
continue;
|
|
129
|
+
if (parsed.issue && event.issueIdentifier !== parsed.issue)
|
|
130
|
+
continue;
|
|
131
|
+
process.stdout.write(formatEvent(event) + "\n");
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
// Skip runs without events
|
|
136
|
+
}
|
|
120
137
|
}
|
|
121
138
|
}
|
|
122
139
|
}
|
|
123
140
|
catch {
|
|
141
|
+
// fall through to friendly error below
|
|
142
|
+
}
|
|
143
|
+
if (!foundRuns) {
|
|
124
144
|
process.stderr.write("No runs found. Start the orchestrator first.\n");
|
|
125
145
|
}
|
|
126
146
|
};
|
|
@@ -132,3 +152,33 @@ function formatEvent(event) {
|
|
|
132
152
|
const extra = event.error ? ` error=${event.error}` : "";
|
|
133
153
|
return `[${at}] ${eventType} ${issue}${extra}`;
|
|
134
154
|
}
|
|
155
|
+
async function listProjectRunRoots(runtimeRoot) {
|
|
156
|
+
try {
|
|
157
|
+
const projectIds = await readdir(join(runtimeRoot, "projects"));
|
|
158
|
+
return projectIds.map((projectId) => join(runtimeRoot, "projects", projectId, "runs"));
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
return [];
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
async function resolveRunEventsPath(runtimeRoot, runId) {
|
|
165
|
+
for (const runsDir of await listProjectRunRoots(runtimeRoot)) {
|
|
166
|
+
const eventsPath = join(runsDir, runId, "events.ndjson");
|
|
167
|
+
try {
|
|
168
|
+
await stat(eventsPath);
|
|
169
|
+
return eventsPath;
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
// Continue searching.
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
async function safeReadDir(path) {
|
|
178
|
+
try {
|
|
179
|
+
return await readdir(path);
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
return [];
|
|
183
|
+
}
|
|
184
|
+
}
|
package/dist/commands/project.js
CHANGED
|
@@ -174,7 +174,7 @@ function isProcessRunning(pid) {
|
|
|
174
174
|
async function readPersistedSnapshot(configDir, projectId) {
|
|
175
175
|
try {
|
|
176
176
|
const runtimeRoot = resolveRuntimeRoot(configDir);
|
|
177
|
-
const content = await readFile(join(runtimeRoot, "
|
|
177
|
+
const content = await readFile(join(runtimeRoot, "projects", projectId, "status.json"), "utf8");
|
|
178
178
|
return JSON.parse(content);
|
|
179
179
|
}
|
|
180
180
|
catch {
|
package/dist/commands/recover.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFile, readdir } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { runCli as orchestratorRunCli } from "@gh-symphony/orchestrator";
|
|
4
|
-
import { resolveRuntimeRoot,
|
|
4
|
+
import { resolveRuntimeRoot, } from "../orchestrator-runtime.js";
|
|
5
5
|
import { handleMissingManagedProjectConfig, resolveManagedProjectConfig, } from "../project-selection.js";
|
|
6
6
|
function parseRecoverArgs(args) {
|
|
7
7
|
const parsed = { dryRun: false };
|
|
@@ -29,7 +29,6 @@ const handler = async (args, options) => {
|
|
|
29
29
|
}
|
|
30
30
|
const runtimeRoot = resolveRuntimeRoot(options.configDir);
|
|
31
31
|
const projectId = projectConfig.projectId;
|
|
32
|
-
await syncProjectToRuntime(options.configDir, projectConfig);
|
|
33
32
|
if (parsed.dryRun) {
|
|
34
33
|
process.stdout.write("Dry run — scanning for stalled runs...\n");
|
|
35
34
|
const candidates = await listRecoverCandidates(runtimeRoot, projectId);
|
|
@@ -57,7 +56,7 @@ const handler = async (args, options) => {
|
|
|
57
56
|
};
|
|
58
57
|
export default handler;
|
|
59
58
|
async function listRecoverCandidates(runtimeRoot, projectId) {
|
|
60
|
-
const runsDir = join(runtimeRoot, "
|
|
59
|
+
const runsDir = join(runtimeRoot, "projects", projectId, "runs");
|
|
61
60
|
const candidates = [];
|
|
62
61
|
let entries = [];
|
|
63
62
|
try {
|
package/dist/commands/run.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { runCli as orchestratorRunCli } from "@gh-symphony/orchestrator";
|
|
2
|
-
import { resolveRuntimeRoot,
|
|
2
|
+
import { resolveRuntimeRoot, } from "../orchestrator-runtime.js";
|
|
3
3
|
import { handleMissingManagedProjectConfig, resolveManagedProjectConfig, } from "../project-selection.js";
|
|
4
4
|
function parseRunArgs(args) {
|
|
5
5
|
const parsed = {
|
|
@@ -11,18 +11,41 @@ function parseRunArgs(args) {
|
|
|
11
11
|
parsed.watch = true;
|
|
12
12
|
}
|
|
13
13
|
else if (arg === "--project" || arg === "--project-id") {
|
|
14
|
-
|
|
14
|
+
const value = args[i + 1];
|
|
15
|
+
if (!value || value.startsWith("-")) {
|
|
16
|
+
parsed.error = `Option '${arg}' argument missing`;
|
|
17
|
+
return parsed;
|
|
18
|
+
}
|
|
19
|
+
parsed.projectId = value;
|
|
15
20
|
i += 1;
|
|
16
21
|
}
|
|
17
|
-
else if (
|
|
22
|
+
else if (arg === "--log-level") {
|
|
23
|
+
const value = args[i + 1];
|
|
24
|
+
if (!value || value.startsWith("-")) {
|
|
25
|
+
parsed.error = `Option '${arg}' argument missing`;
|
|
26
|
+
return parsed;
|
|
27
|
+
}
|
|
28
|
+
parsed.logLevel = value;
|
|
29
|
+
i += 1;
|
|
30
|
+
}
|
|
31
|
+
else if (!arg?.startsWith("-")) {
|
|
18
32
|
// Positional arg = issue identifier
|
|
19
33
|
parsed.issue = arg;
|
|
20
34
|
}
|
|
35
|
+
else {
|
|
36
|
+
parsed.error = `Unknown option '${arg}'`;
|
|
37
|
+
return parsed;
|
|
38
|
+
}
|
|
21
39
|
}
|
|
22
40
|
return parsed;
|
|
23
41
|
}
|
|
24
42
|
const handler = async (args, options) => {
|
|
25
43
|
const parsed = parseRunArgs(args);
|
|
44
|
+
if (parsed.error) {
|
|
45
|
+
process.stderr.write(`${parsed.error}\n`);
|
|
46
|
+
process.exitCode = 2;
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
26
49
|
if (!parsed.issue) {
|
|
27
50
|
process.stderr.write("Usage: gh-symphony run <owner/repo#number>\n");
|
|
28
51
|
process.exitCode = 2;
|
|
@@ -38,7 +61,6 @@ const handler = async (args, options) => {
|
|
|
38
61
|
}
|
|
39
62
|
const runtimeRoot = resolveRuntimeRoot(options.configDir);
|
|
40
63
|
const projectId = projectConfig.projectId;
|
|
41
|
-
await syncProjectToRuntime(options.configDir, projectConfig);
|
|
42
64
|
// Validate the issue identifier belongs to a configured repo
|
|
43
65
|
const [repoSpec] = parsed.issue.split("#");
|
|
44
66
|
if (repoSpec &&
|
|
@@ -57,6 +79,7 @@ const handler = async (args, options) => {
|
|
|
57
79
|
projectId,
|
|
58
80
|
"--issue",
|
|
59
81
|
parsed.issue,
|
|
82
|
+
...(parsed.logLevel ? ["--log-level", parsed.logLevel] : []),
|
|
60
83
|
]);
|
|
61
84
|
if (parsed.watch) {
|
|
62
85
|
process.stdout.write("\nWatching for status changes...\n");
|
package/dist/commands/start.d.ts
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
import { rm } from "node:fs/promises";
|
|
2
2
|
import type { GlobalOptions } from "../index.js";
|
|
3
|
+
import { releaseProjectLock, type ProjectLockHandle } from "@gh-symphony/orchestrator";
|
|
3
4
|
type ForegroundShutdownOptions = {
|
|
4
5
|
configDir: string;
|
|
5
6
|
projectId: string;
|
|
6
7
|
statusServer: {
|
|
7
8
|
close(): void;
|
|
8
9
|
};
|
|
10
|
+
projectLock?: ProjectLockHandle | null;
|
|
11
|
+
service?: {
|
|
12
|
+
shutdown(): Promise<void>;
|
|
13
|
+
};
|
|
9
14
|
exit?: (code?: number) => never;
|
|
10
15
|
removePortFile?: typeof rm;
|
|
16
|
+
releaseLock?: typeof releaseProjectLock;
|
|
11
17
|
};
|
|
12
18
|
declare const handler: (args: string[], options: GlobalOptions) => Promise<void>;
|
|
13
19
|
export declare function shutdownForegroundOrchestrator(input: ForegroundShutdownOptions): Promise<never>;
|
package/dist/commands/start.js
CHANGED
|
@@ -3,8 +3,8 @@ import { dirname, join } from "node:path";
|
|
|
3
3
|
import { spawn } from "node:child_process";
|
|
4
4
|
import { once } from "node:events";
|
|
5
5
|
import { daemonPidPath, orchestratorLogPath, orchestratorPortPath, } from "../config.js";
|
|
6
|
-
import { OrchestratorService, createStore, startOrchestratorStatusServer, } from "@gh-symphony/orchestrator";
|
|
7
|
-
import { resolveRuntimeRoot,
|
|
6
|
+
import { OrchestratorService, acquireProjectLock, createStore, releaseProjectLock, resolveOrchestratorLogLevel, startOrchestratorStatusServer, } from "@gh-symphony/orchestrator";
|
|
7
|
+
import { resolveRuntimeRoot, } from "../orchestrator-runtime.js";
|
|
8
8
|
import { handleMissingManagedProjectConfig, resolveManagedProjectConfig, } from "../project-selection.js";
|
|
9
9
|
import { bold, dim, green, red, yellow, cyan, setNoColor } from "../ansi.js";
|
|
10
10
|
import { getGhToken } from "../github/gh-auth.js";
|
|
@@ -39,6 +39,16 @@ function parseStartArgs(args) {
|
|
|
39
39
|
i += 1;
|
|
40
40
|
continue;
|
|
41
41
|
}
|
|
42
|
+
if (arg === "--log-level") {
|
|
43
|
+
const value = args[i + 1];
|
|
44
|
+
if (!value || value.startsWith("-")) {
|
|
45
|
+
parsed.error = `Option '${arg}' argument missing`;
|
|
46
|
+
return parsed;
|
|
47
|
+
}
|
|
48
|
+
parsed.logLevel = value;
|
|
49
|
+
i += 1;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
42
52
|
if (arg?.startsWith("-")) {
|
|
43
53
|
parsed.error = `Unknown option '${arg}'`;
|
|
44
54
|
return parsed;
|
|
@@ -133,9 +143,17 @@ const handler = async (args, options) => {
|
|
|
133
143
|
}
|
|
134
144
|
const runtimeRoot = resolveRuntimeRoot(options.configDir);
|
|
135
145
|
const projectId = projectConfig.projectId;
|
|
136
|
-
|
|
146
|
+
let logLevel;
|
|
147
|
+
try {
|
|
148
|
+
logLevel = resolveOrchestratorLogLevel(parsed.logLevel ?? process.env.SYMPHONY_LOG_LEVEL);
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
process.stderr.write(`${error instanceof Error ? error.message : "Unsupported log level"}\n`);
|
|
152
|
+
process.exitCode = 2;
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
137
155
|
if (parsed.daemon) {
|
|
138
|
-
await startDaemon(options, projectId);
|
|
156
|
+
await startDaemon(options, projectId, parsed.logLevel);
|
|
139
157
|
return;
|
|
140
158
|
}
|
|
141
159
|
// ── 5.1: Foreground mode with live logging ────────────────────────────────
|
|
@@ -148,65 +166,99 @@ const handler = async (args, options) => {
|
|
|
148
166
|
// Workers will fail if token is needed but not available
|
|
149
167
|
}
|
|
150
168
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
port: 0,
|
|
156
|
-
getProjectStatus: () => service.status(),
|
|
157
|
-
onRefresh: async () => {
|
|
158
|
-
await service.runOnce();
|
|
159
|
-
},
|
|
160
|
-
});
|
|
161
|
-
await persistStatusServerPort(options.configDir, projectId, statusServer);
|
|
162
|
-
logLine(green("\u25B2"), `Starting orchestrator for project: ${bold(projectId)}`);
|
|
163
|
-
logLine(dim("\u00B7"), dim("Press Ctrl+C to stop"));
|
|
164
|
-
let running = true;
|
|
165
|
-
let shuttingDown = false;
|
|
166
|
-
const shutdown = async () => {
|
|
167
|
-
if (shuttingDown) {
|
|
168
|
-
return;
|
|
169
|
-
}
|
|
170
|
-
shuttingDown = true;
|
|
171
|
-
running = false;
|
|
172
|
-
await shutdownForegroundOrchestrator({
|
|
173
|
-
configDir: options.configDir,
|
|
169
|
+
let projectLock = null;
|
|
170
|
+
try {
|
|
171
|
+
projectLock = await acquireProjectLock({
|
|
172
|
+
runtimeRoot,
|
|
174
173
|
projectId,
|
|
175
|
-
statusServer,
|
|
176
174
|
});
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
175
|
+
const store = createStore(runtimeRoot);
|
|
176
|
+
const service = new OrchestratorService(store, projectConfig, {
|
|
177
|
+
logLevel,
|
|
178
|
+
});
|
|
179
|
+
const statusServer = startOrchestratorStatusServer({
|
|
180
|
+
host: "127.0.0.1",
|
|
181
|
+
port: 0,
|
|
182
|
+
getProjectStatus: () => service.status(),
|
|
183
|
+
onRefresh: async () => {
|
|
184
|
+
await service.runOnce();
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
await persistStatusServerPort(options.configDir, projectId, statusServer);
|
|
188
|
+
logLine(green("\u25B2"), `Starting orchestrator for project: ${bold(projectId)}`);
|
|
189
|
+
logLine(dim("\u00B7"), dim("Press Ctrl+C to stop"));
|
|
190
|
+
let running = true;
|
|
191
|
+
let shuttingDown = false;
|
|
192
|
+
let shutdownPromise = null;
|
|
193
|
+
const shutdown = async () => {
|
|
194
|
+
if (shuttingDown) {
|
|
195
|
+
return shutdownPromise;
|
|
196
|
+
}
|
|
197
|
+
shuttingDown = true;
|
|
198
|
+
running = false;
|
|
199
|
+
const heldLock = projectLock;
|
|
200
|
+
projectLock = null;
|
|
201
|
+
shutdownPromise = shutdownForegroundOrchestrator({
|
|
202
|
+
configDir: options.configDir,
|
|
203
|
+
projectId,
|
|
204
|
+
statusServer,
|
|
205
|
+
projectLock: heldLock,
|
|
206
|
+
service,
|
|
207
|
+
});
|
|
208
|
+
return shutdownPromise;
|
|
209
|
+
};
|
|
210
|
+
process.on("SIGINT", () => {
|
|
211
|
+
void shutdown();
|
|
212
|
+
});
|
|
213
|
+
process.on("SIGTERM", () => {
|
|
214
|
+
void shutdown();
|
|
215
|
+
});
|
|
216
|
+
let prevSnapshot = null;
|
|
217
|
+
let isFirst = true;
|
|
218
|
+
while (running) {
|
|
219
|
+
try {
|
|
220
|
+
const snapshot = await service.runOnce();
|
|
221
|
+
logTickResult(snapshot, prevSnapshot, isFirst);
|
|
222
|
+
if (!isFirst) {
|
|
223
|
+
const currentRunIds = new Set(snapshot.activeRuns.map((run) => run.runId));
|
|
224
|
+
for (const prevRun of prevSnapshot?.activeRuns ?? []) {
|
|
225
|
+
if (!currentRunIds.has(prevRun.runId)) {
|
|
226
|
+
await tailWorkerLog(runtimeRoot, projectId, prevRun.runId, prevRun.issueIdentifier);
|
|
227
|
+
}
|
|
195
228
|
}
|
|
196
229
|
}
|
|
230
|
+
prevSnapshot = snapshot;
|
|
231
|
+
isFirst = false;
|
|
197
232
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
233
|
+
catch (error) {
|
|
234
|
+
logLine(red("\u2717"), red(`Tick error: ${error instanceof Error ? error.message : "Unknown error"}`));
|
|
235
|
+
}
|
|
236
|
+
if (!running) {
|
|
237
|
+
if (shutdownPromise) {
|
|
238
|
+
await shutdownPromise;
|
|
239
|
+
}
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
242
|
+
// Poll interval: default 30s
|
|
243
|
+
await new Promise((r) => setTimeout(r, 30_000));
|
|
203
244
|
}
|
|
204
|
-
|
|
205
|
-
|
|
245
|
+
}
|
|
246
|
+
finally {
|
|
247
|
+
await releaseProjectLock(projectLock);
|
|
206
248
|
}
|
|
207
249
|
};
|
|
208
250
|
export async function shutdownForegroundOrchestrator(input) {
|
|
209
251
|
logLine(yellow("\u25BC"), "Shutting down...");
|
|
252
|
+
// Drain active workers before tearing down infrastructure so that child
|
|
253
|
+
// processes receive SIGTERM/SIGKILL and do not become orphans.
|
|
254
|
+
if (input.service) {
|
|
255
|
+
try {
|
|
256
|
+
await input.service.shutdown();
|
|
257
|
+
}
|
|
258
|
+
catch (error) {
|
|
259
|
+
logLine(red("\u2717"), red(`Failed to shut down workers: ${error instanceof Error ? error.message : "Unknown error"}`));
|
|
260
|
+
}
|
|
261
|
+
}
|
|
210
262
|
try {
|
|
211
263
|
input.statusServer.close();
|
|
212
264
|
}
|
|
@@ -221,11 +273,17 @@ export async function shutdownForegroundOrchestrator(input) {
|
|
|
221
273
|
catch (error) {
|
|
222
274
|
logLine(yellow("\u26A0"), `Failed to remove persisted status port: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
223
275
|
}
|
|
276
|
+
try {
|
|
277
|
+
await (input.releaseLock ?? releaseProjectLock)(input.projectLock);
|
|
278
|
+
}
|
|
279
|
+
catch (error) {
|
|
280
|
+
logLine(yellow("\u26A0"), `Failed to release project lock: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
281
|
+
}
|
|
224
282
|
return (input.exit ?? process.exit)(0);
|
|
225
283
|
}
|
|
226
|
-
async function tailWorkerLog(runtimeRoot, runId, issueIdentifier) {
|
|
284
|
+
async function tailWorkerLog(runtimeRoot, projectId, runId, issueIdentifier) {
|
|
227
285
|
try {
|
|
228
|
-
const logPath = join(runtimeRoot, "
|
|
286
|
+
const logPath = join(runtimeRoot, "projects", projectId, "runs", runId, "worker.log");
|
|
229
287
|
const content = await readFile(logPath, "utf8");
|
|
230
288
|
const lines = content.split("\n").filter((l) => l.trim());
|
|
231
289
|
if (lines.length === 0)
|
|
@@ -242,12 +300,18 @@ async function tailWorkerLog(runtimeRoot, runId, issueIdentifier) {
|
|
|
242
300
|
}
|
|
243
301
|
export default handler;
|
|
244
302
|
// ── 5.2: Daemon mode ─────────────────────────────────────────────────────────
|
|
245
|
-
async function startDaemon(options, projectId) {
|
|
303
|
+
async function startDaemon(options, projectId, logLevel) {
|
|
246
304
|
const logPath = orchestratorLogPath(options.configDir, projectId);
|
|
247
305
|
await mkdir(dirname(logPath), { recursive: true });
|
|
248
306
|
const { openSync } = await import("node:fs");
|
|
249
307
|
const logFd = openSync(logPath, "a");
|
|
250
|
-
const child = spawn(process.execPath, [
|
|
308
|
+
const child = spawn(process.execPath, [
|
|
309
|
+
process.argv[1],
|
|
310
|
+
"start",
|
|
311
|
+
"--project",
|
|
312
|
+
projectId,
|
|
313
|
+
...(logLevel ? ["--log-level", logLevel] : []),
|
|
314
|
+
], {
|
|
251
315
|
cwd: process.cwd(),
|
|
252
316
|
env: {
|
|
253
317
|
...process.env,
|
package/dist/commands/status.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { resolveRuntimeRoot,
|
|
3
|
+
import { resolveRuntimeRoot, } from "../orchestrator-runtime.js";
|
|
4
4
|
import { handleMissingManagedProjectConfig, resolveManagedProjectConfig, } from "../project-selection.js";
|
|
5
5
|
import { bold, dim, green, red, yellow, cyan, stripAnsi } from "../ansi.js";
|
|
6
6
|
import { clearScreen, showCursor, hideCursor } from "../ansi.js";
|
|
@@ -137,7 +137,7 @@ function parseStatusArgs(args) {
|
|
|
137
137
|
}
|
|
138
138
|
async function readStatusSnapshot(runtimeRoot, projectId) {
|
|
139
139
|
try {
|
|
140
|
-
const statusPath = join(runtimeRoot, "
|
|
140
|
+
const statusPath = join(runtimeRoot, "projects", projectId, "status.json");
|
|
141
141
|
const content = await readFile(statusPath, "utf-8");
|
|
142
142
|
return JSON.parse(content);
|
|
143
143
|
}
|
|
@@ -163,7 +163,6 @@ const handler = async (args, options) => {
|
|
|
163
163
|
}
|
|
164
164
|
const runtimeRoot = resolveRuntimeRoot(options.configDir);
|
|
165
165
|
const projectId = projectConfig.projectId;
|
|
166
|
-
await syncProjectToRuntime(options.configDir, projectConfig);
|
|
167
166
|
if (parsed.watch) {
|
|
168
167
|
const isTTY = process.stdout.isTTY === true;
|
|
169
168
|
let terminalWidth = process.stdout.columns ?? 115;
|
package/dist/index.js
CHANGED
|
@@ -103,6 +103,7 @@ function createProgram() {
|
|
|
103
103
|
.command("start")
|
|
104
104
|
.description("Start the orchestrator")
|
|
105
105
|
.option("-d, --daemon", "Start in daemon mode")
|
|
106
|
+
.option("--log-level <level>", "Orchestrator lifecycle log level")
|
|
106
107
|
.option("--project-id <projectId>", "Project identifier")
|
|
107
108
|
.addOption(new Option("--project <projectId>").hideHelp())
|
|
108
109
|
.allowExcessArguments(false)).action(async function () {
|
|
@@ -111,6 +112,7 @@ function createProgram() {
|
|
|
111
112
|
const args = [];
|
|
112
113
|
pushOption(args, "--project-id", resolveProjectId(values));
|
|
113
114
|
pushOption(args, "--daemon", values.daemon);
|
|
115
|
+
pushOption(args, "--log-level", values.logLevel);
|
|
114
116
|
await invokeHandler("start", args, values);
|
|
115
117
|
});
|
|
116
118
|
addGlobalOptions(program
|
|
@@ -145,6 +147,7 @@ function createProgram() {
|
|
|
145
147
|
.command("run")
|
|
146
148
|
.description("Dispatch a single issue")
|
|
147
149
|
.argument("<issue>", "Issue identifier")
|
|
150
|
+
.option("--log-level <level>", "Orchestrator lifecycle log level")
|
|
148
151
|
.option("-w, --watch", "Watch status after dispatch")
|
|
149
152
|
.option("--project-id <projectId>", "Project identifier")
|
|
150
153
|
.addOption(new Option("--project <projectId>").hideHelp())
|
|
@@ -153,6 +156,7 @@ function createProgram() {
|
|
|
153
156
|
const values = this.optsWithGlobals();
|
|
154
157
|
const args = [issue];
|
|
155
158
|
pushOption(args, "--project-id", resolveProjectId(values));
|
|
159
|
+
pushOption(args, "--log-level", values.logLevel);
|
|
156
160
|
pushOption(args, "--watch", values.watch);
|
|
157
161
|
await invokeHandler("run", args, values);
|
|
158
162
|
});
|
|
@@ -229,6 +233,7 @@ function createProgram() {
|
|
|
229
233
|
.command("start")
|
|
230
234
|
.description("Start a specific project")
|
|
231
235
|
.option("-d, --daemon", "Start in daemon mode")
|
|
236
|
+
.option("--log-level <level>", "Orchestrator lifecycle log level")
|
|
232
237
|
.option("--project-id <projectId>", "Project identifier")
|
|
233
238
|
.addOption(new Option("--project <projectId>").hideHelp())
|
|
234
239
|
.allowExcessArguments(false)).action(async function () {
|
|
@@ -237,6 +242,7 @@ function createProgram() {
|
|
|
237
242
|
const args = ["start"];
|
|
238
243
|
pushOption(args, "--project-id", resolveProjectId(values));
|
|
239
244
|
pushOption(args, "--daemon", values.daemon);
|
|
245
|
+
pushOption(args, "--log-level", values.logLevel);
|
|
240
246
|
await invokeHandler("project", args, values);
|
|
241
247
|
});
|
|
242
248
|
addGlobalOptions(project
|
|
@@ -1,5 +1 @@
|
|
|
1
|
-
import { type CliProjectConfig } from "./config.js";
|
|
2
1
|
export declare function resolveRuntimeRoot(configDir: string): string;
|
|
3
|
-
export declare function resolveProjectConfig(configDir: string, requestedProjectId?: string): Promise<CliProjectConfig | null>;
|
|
4
|
-
export declare function orchestratorProjectConfigPath(runtimeRoot: string, projectId: string): string;
|
|
5
|
-
export declare function syncProjectToRuntime(configDir: string, projectConfig: CliProjectConfig): Promise<string>;
|
|
@@ -1,26 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { dirname, join, resolve } from "node:path";
|
|
3
|
-
import { loadGlobalConfig, loadProjectConfig, } from "./config.js";
|
|
1
|
+
import { resolve } from "node:path";
|
|
4
2
|
export function resolveRuntimeRoot(configDir) {
|
|
5
3
|
return resolve(configDir);
|
|
6
4
|
}
|
|
7
|
-
export async function resolveProjectConfig(configDir, requestedProjectId) {
|
|
8
|
-
if (requestedProjectId) {
|
|
9
|
-
return loadProjectConfig(configDir, requestedProjectId);
|
|
10
|
-
}
|
|
11
|
-
const global = await loadGlobalConfig(configDir);
|
|
12
|
-
if (!global?.activeProject) {
|
|
13
|
-
return null;
|
|
14
|
-
}
|
|
15
|
-
return loadProjectConfig(configDir, global.activeProject);
|
|
16
|
-
}
|
|
17
|
-
export function orchestratorProjectConfigPath(runtimeRoot, projectId) {
|
|
18
|
-
return join(runtimeRoot, "orchestrator", "projects", projectId, "config.json");
|
|
19
|
-
}
|
|
20
|
-
export async function syncProjectToRuntime(configDir, projectConfig) {
|
|
21
|
-
const runtimeRoot = resolveRuntimeRoot(configDir);
|
|
22
|
-
const configPath = orchestratorProjectConfigPath(runtimeRoot, projectConfig.projectId);
|
|
23
|
-
await mkdir(dirname(configPath), { recursive: true });
|
|
24
|
-
await writeFile(configPath, JSON.stringify(projectConfig, null, 2) + "\n");
|
|
25
|
-
return runtimeRoot;
|
|
26
|
-
}
|
|
@@ -83,7 +83,7 @@ export function generateGhSymphonySkill(ctx) {
|
|
|
83
83
|
lines.push("agent:");
|
|
84
84
|
lines.push(" max_concurrent_agents: 10");
|
|
85
85
|
lines.push(" max_retry_backoff_ms: 30000");
|
|
86
|
-
lines.push(" retry_base_delay_ms:
|
|
86
|
+
lines.push(" retry_base_delay_ms: 10000");
|
|
87
87
|
lines.push(" max_turns: 20");
|
|
88
88
|
lines.push("codex:");
|
|
89
89
|
lines.push(" command: codex app-server");
|
|
@@ -63,7 +63,7 @@ export function generateReferenceWorkflow(input) {
|
|
|
63
63
|
lines.push("agent:");
|
|
64
64
|
lines.push(" max_concurrent_agents: 10");
|
|
65
65
|
lines.push(" max_retry_backoff_ms: 30000");
|
|
66
|
-
lines.push(" retry_base_delay_ms:
|
|
66
|
+
lines.push(" retry_base_delay_ms: 10000");
|
|
67
67
|
lines.push(" max_turns: 20");
|
|
68
68
|
lines.push("");
|
|
69
69
|
lines.push("codex:");
|
|
@@ -37,7 +37,7 @@ function buildFrontMatter(input) {
|
|
|
37
37
|
lines.push("agent:");
|
|
38
38
|
lines.push(" max_concurrent_agents: 10");
|
|
39
39
|
lines.push(" max_retry_backoff_ms: 30000");
|
|
40
|
-
lines.push(" retry_base_delay_ms:
|
|
40
|
+
lines.push(" retry_base_delay_ms: 10000");
|
|
41
41
|
lines.push("codex:");
|
|
42
42
|
lines.push(` command: ${agentCommand}`);
|
|
43
43
|
lines.push(" read_timeout_ms: 5000");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gh-symphony/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.12",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "hojinzs",
|
|
6
6
|
"description": "Interactive CLI for GitHub Symphony orchestration",
|
|
@@ -37,10 +37,10 @@
|
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@clack/prompts": "^0.9.1",
|
|
39
39
|
"commander": "^14.0.1",
|
|
40
|
-
"@gh-symphony/
|
|
41
|
-
"@gh-symphony/
|
|
42
|
-
"@gh-symphony/
|
|
43
|
-
"@gh-symphony/
|
|
40
|
+
"@gh-symphony/core": "0.0.12",
|
|
41
|
+
"@gh-symphony/tracker-github": "0.0.12",
|
|
42
|
+
"@gh-symphony/worker": "0.0.12",
|
|
43
|
+
"@gh-symphony/orchestrator": "0.0.12"
|
|
44
44
|
},
|
|
45
45
|
"scripts": {
|
|
46
46
|
"build": "tsc -p tsconfig.json",
|