@bridge_gpt/mcp-server 0.2.0 → 0.2.1
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/README.md +56 -54
- package/build/agent-launchers/claude.js +25 -17
- package/build/agent-launchers/cursor.js +65 -0
- package/build/agent-launchers/index.js +23 -8
- package/build/agent-registry.js +68 -0
- package/build/command-catalog.js +376 -0
- package/build/commands.generated.js +8 -5
- package/build/index.js +316 -116
- package/build/mcp-provisioning.js +94 -1
- package/build/pipeline-utils.js +0 -33
- package/build/pipelines.generated.js +2 -31
- package/build/schedule-run.js +436 -88
- package/build/schedule-store.js +41 -1
- package/build/scheduled-prompt.js +109 -0
- package/build/scheduler-backends/at-fallback.js +5 -10
- package/build/scheduler-backends/escaping.js +40 -10
- package/build/scheduler-backends/launchd.js +23 -14
- package/build/scheduler-backends/systemd-user.js +32 -19
- package/build/scheduler-backends/task-scheduler.js +8 -13
- package/build/start-tickets.js +459 -30
- package/build/version.generated.js +1 -1
- package/package.json +3 -2
- package/pipelines/implement-ticket.json +2 -28
- package/smoke-test/SMOKE-TEST.md +61 -18
package/build/schedule-run.js
CHANGED
|
@@ -16,11 +16,13 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { execFile } from "node:child_process";
|
|
18
18
|
import { promises as fs } from "node:fs";
|
|
19
|
-
import { randomUUID } from "node:crypto";
|
|
19
|
+
import { randomUUID, createHash } from "node:crypto";
|
|
20
20
|
import { pathApiForPlatform, selectSchedulerBackend, getSchedulerBackendByName, getSchedulerBackendsForPlatform, unsupportedSchedulerPlatformMessage, } from "./scheduler-backends/index.js";
|
|
21
|
-
import { ensureScheduleDirectories, getSchedulePaths, writeScheduleMetadata, readScheduleMetadata, listScheduleMetadata, deleteScheduleMetadata, } from "./schedule-store.js";
|
|
21
|
+
import { ensureScheduleDirectories, getSchedulePaths, writeScheduleMetadata, readScheduleMetadata, listScheduleMetadata, deleteScheduleMetadata, appendScheduleRunEvent, } from "./schedule-store.js";
|
|
22
22
|
import { getAgentLauncher, formatValidAgentLauncherNames } from "./agent-launchers/index.js";
|
|
23
23
|
import { resolveCommandOnPath } from "./agent-launchers/claude.js";
|
|
24
|
+
import { discoverCommandCatalog, resolveSchedulableCommand, validateCommandArgv, } from "./command-catalog.js";
|
|
25
|
+
import { buildTargetCommandLine } from "./scheduled-prompt.js";
|
|
24
26
|
/**
|
|
25
27
|
* Default deps backed by real subprocesses / process state. Subprocess execution
|
|
26
28
|
* uses `execFile` (list-based, never `shell: true`) and supports
|
|
@@ -55,12 +57,30 @@ export function createDefaultScheduleRunDeps() {
|
|
|
55
57
|
execPath: process.execPath,
|
|
56
58
|
homeDir: process.env.HOME ?? process.env.USERPROFILE ?? "",
|
|
57
59
|
now: () => Date.now(),
|
|
60
|
+
// The packaged CLI entry is argv[1]; the trigger invocation re-enters it as
|
|
61
|
+
// `node <cliEntryPath> schedule-run _execute <id>`.
|
|
62
|
+
cliEntryPath: process.argv[1],
|
|
63
|
+
// Best-effort: a Bridge credential is considered resolvable when BAPI_API_KEY
|
|
64
|
+
// is present in the environment. Never reads or returns the value itself.
|
|
65
|
+
bridgeCredentialResolved: () => Boolean(process.env.BAPI_API_KEY),
|
|
58
66
|
};
|
|
59
67
|
}
|
|
60
68
|
/** Return true only for a zero exit code. */
|
|
61
69
|
export function commandSucceeded(result) {
|
|
62
70
|
return result.exitCode === 0;
|
|
63
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* Detect a CLI entry path that lives inside a transient npx cache directory
|
|
74
|
+
* (`~/.npm/_npx/<hash>/…` on POSIX, `…\npm-cache\_npx\<hash>\…` on Windows).
|
|
75
|
+
* Such an entry can be garbage-collected before a future schedule fires, which
|
|
76
|
+
* would make the trigger shim `node <gone-path>` fail with ENOENT. Matched on the
|
|
77
|
+
* `_npx` path segment so it is independent of the surrounding cache root.
|
|
78
|
+
*/
|
|
79
|
+
export function isTransientNpxEntryPath(entryPath) {
|
|
80
|
+
if (!entryPath)
|
|
81
|
+
return false;
|
|
82
|
+
return /[\\/]_npx[\\/]/.test(entryPath);
|
|
83
|
+
}
|
|
64
84
|
const VALID_BACKEND_NAMES = [
|
|
65
85
|
"launchd",
|
|
66
86
|
"task-scheduler",
|
|
@@ -78,23 +98,28 @@ export function getScheduleRunUsage() {
|
|
|
78
98
|
"Usage:",
|
|
79
99
|
" npx -y @bridge_gpt/mcp-server schedule-run <create|list|cancel|doctor> [flags]",
|
|
80
100
|
"",
|
|
81
|
-
"Schedules a local, one-shot
|
|
82
|
-
"
|
|
83
|
-
"
|
|
101
|
+
"Schedules a local, one-shot run of ANY Bridge slash-command automation (from",
|
|
102
|
+
"the repo's .claude/commands/*.md catalog) at a chosen time using an OS-native",
|
|
103
|
+
"scheduler (launchd / Task Scheduler / systemd-user / at). Schedules are stored",
|
|
104
|
+
"locally under ~/.bridge-gpt/schedules/ and are never sent to a server.",
|
|
84
105
|
"",
|
|
85
106
|
"create:",
|
|
86
|
-
|
|
87
|
-
|
|
107
|
+
" schedule-run create --in 2h --command review-ticket -- BAPI-999",
|
|
108
|
+
' schedule-run create --at "<datetime>" --command start-tickets -- BAPI-1 BAPI-2',
|
|
109
|
+
" legacy: schedule-run create --in 2h --idea-file <path>",
|
|
110
|
+
"",
|
|
111
|
+
"All tokens after a bare -- are the structured argv passed to the command.",
|
|
88
112
|
"",
|
|
89
113
|
"create flags:",
|
|
90
114
|
' --at "<datetime>" ISO-8601 time to run (mutually exclusive with --in)',
|
|
91
115
|
" --in <duration> Run after a duration, e.g. 30m, 4h, 1d (mutually exclusive with --at)",
|
|
92
|
-
" --
|
|
93
|
-
" --
|
|
94
|
-
" --
|
|
95
|
-
" --
|
|
96
|
-
" --
|
|
97
|
-
" --
|
|
116
|
+
" --command <name> Discovered .claude/commands command to schedule",
|
|
117
|
+
" --idea-file <path> Legacy: schedule /full-automation with this idea file",
|
|
118
|
+
" --agent <name> Agent to launch (claude [default] or cursor-agent)",
|
|
119
|
+
" --repo-path <path> Working directory + command catalog root (default: cwd)",
|
|
120
|
+
" --auto Run hands-off; gate proceeds even when firing late",
|
|
121
|
+
" --no-auto Late runs halt without executing (headless-safe default)",
|
|
122
|
+
" --dry-run Print the generated unit + prompt + invocation without creating anything",
|
|
98
123
|
" --id <id> Override the auto-generated schedule id (mainly for tests)",
|
|
99
124
|
"",
|
|
100
125
|
"list flags:",
|
|
@@ -132,13 +157,24 @@ function takeValue(argv, index, arg, flag) {
|
|
|
132
157
|
function matchesFlag(arg, flag) {
|
|
133
158
|
return arg === flag || arg.startsWith(`${flag}=`);
|
|
134
159
|
}
|
|
135
|
-
/**
|
|
160
|
+
/**
|
|
161
|
+
* Parse `create` args. Everything after the FIRST bare `--` is the structured
|
|
162
|
+
* command argv (passed through verbatim, never re-tokenized); scheduler flags are
|
|
163
|
+
* parsed only before that delimiter. `--idea-file` without `--command` implies
|
|
164
|
+
* the legacy `full-automation` command.
|
|
165
|
+
*/
|
|
136
166
|
export function parseScheduleCreateArgs(argv) {
|
|
137
|
-
|
|
167
|
+
// Split on the first bare "--". Help is honored only from the scheduler-flag
|
|
168
|
+
// side; tokens after "--" belong to the delegated command verbatim.
|
|
169
|
+
const delimIndex = argv.indexOf("--");
|
|
170
|
+
const schedulerArgs = delimIndex === -1 ? argv : argv.slice(0, delimIndex);
|
|
171
|
+
const commandArgs = delimIndex === -1 ? [] : argv.slice(delimIndex + 1);
|
|
172
|
+
if (schedulerArgs.includes("-h") || schedulerArgs.includes("--help")) {
|
|
138
173
|
return { status: "help", usage: getScheduleRunUsage() };
|
|
139
174
|
}
|
|
140
175
|
let at;
|
|
141
176
|
let inDuration;
|
|
177
|
+
let commandName;
|
|
142
178
|
let ideaFile;
|
|
143
179
|
let agent = "claude";
|
|
144
180
|
let repoPath;
|
|
@@ -146,10 +182,10 @@ export function parseScheduleCreateArgs(argv) {
|
|
|
146
182
|
let sawAutoApprove = false;
|
|
147
183
|
let sawNoAutoApprove = false;
|
|
148
184
|
let dryRun = false;
|
|
149
|
-
for (let i = 0; i <
|
|
150
|
-
const arg =
|
|
185
|
+
for (let i = 0; i < schedulerArgs.length; i++) {
|
|
186
|
+
const arg = schedulerArgs[i];
|
|
151
187
|
if (matchesFlag(arg, "--at")) {
|
|
152
|
-
const r = takeValue(
|
|
188
|
+
const r = takeValue(schedulerArgs, i, arg, "--at");
|
|
153
189
|
if ("error" in r)
|
|
154
190
|
return { status: "error", message: r.error };
|
|
155
191
|
at = r.value;
|
|
@@ -157,15 +193,23 @@ export function parseScheduleCreateArgs(argv) {
|
|
|
157
193
|
continue;
|
|
158
194
|
}
|
|
159
195
|
if (matchesFlag(arg, "--in")) {
|
|
160
|
-
const r = takeValue(
|
|
196
|
+
const r = takeValue(schedulerArgs, i, arg, "--in");
|
|
161
197
|
if ("error" in r)
|
|
162
198
|
return { status: "error", message: r.error };
|
|
163
199
|
inDuration = r.value;
|
|
164
200
|
i = r.nextIndex;
|
|
165
201
|
continue;
|
|
166
202
|
}
|
|
203
|
+
if (matchesFlag(arg, "--command")) {
|
|
204
|
+
const r = takeValue(schedulerArgs, i, arg, "--command");
|
|
205
|
+
if ("error" in r)
|
|
206
|
+
return { status: "error", message: r.error };
|
|
207
|
+
commandName = r.value;
|
|
208
|
+
i = r.nextIndex;
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
167
211
|
if (matchesFlag(arg, "--idea-file")) {
|
|
168
|
-
const r = takeValue(
|
|
212
|
+
const r = takeValue(schedulerArgs, i, arg, "--idea-file");
|
|
169
213
|
if ("error" in r)
|
|
170
214
|
return { status: "error", message: r.error };
|
|
171
215
|
ideaFile = r.value;
|
|
@@ -173,13 +217,13 @@ export function parseScheduleCreateArgs(argv) {
|
|
|
173
217
|
continue;
|
|
174
218
|
}
|
|
175
219
|
if (matchesFlag(arg, "--agent")) {
|
|
176
|
-
const r = takeValue(
|
|
220
|
+
const r = takeValue(schedulerArgs, i, arg, "--agent");
|
|
177
221
|
if ("error" in r)
|
|
178
222
|
return { status: "error", message: r.error };
|
|
179
223
|
if (!getAgentLauncher(r.value)) {
|
|
180
224
|
return {
|
|
181
225
|
status: "error",
|
|
182
|
-
message: `Invalid --agent value: '${r.value}'.
|
|
226
|
+
message: `Invalid --agent value: '${r.value}'. Valid: ${formatValidAgentLauncherNames()}.`,
|
|
183
227
|
};
|
|
184
228
|
}
|
|
185
229
|
agent = r.value;
|
|
@@ -187,7 +231,7 @@ export function parseScheduleCreateArgs(argv) {
|
|
|
187
231
|
continue;
|
|
188
232
|
}
|
|
189
233
|
if (matchesFlag(arg, "--repo-path")) {
|
|
190
|
-
const r = takeValue(
|
|
234
|
+
const r = takeValue(schedulerArgs, i, arg, "--repo-path");
|
|
191
235
|
if ("error" in r)
|
|
192
236
|
return { status: "error", message: r.error };
|
|
193
237
|
repoPath = r.value;
|
|
@@ -195,7 +239,7 @@ export function parseScheduleCreateArgs(argv) {
|
|
|
195
239
|
continue;
|
|
196
240
|
}
|
|
197
241
|
if (matchesFlag(arg, "--id")) {
|
|
198
|
-
const r = takeValue(
|
|
242
|
+
const r = takeValue(schedulerArgs, i, arg, "--id");
|
|
199
243
|
if ("error" in r)
|
|
200
244
|
return { status: "error", message: r.error };
|
|
201
245
|
id = r.value;
|
|
@@ -219,7 +263,8 @@ export function parseScheduleCreateArgs(argv) {
|
|
|
219
263
|
}
|
|
220
264
|
return {
|
|
221
265
|
status: "error",
|
|
222
|
-
message: `Unexpected positional argument: '${arg}'. create takes
|
|
266
|
+
message: `Unexpected positional argument: '${arg}'. create takes flags before '--'; ` +
|
|
267
|
+
"put command arguments after '--'.",
|
|
223
268
|
};
|
|
224
269
|
}
|
|
225
270
|
if (Boolean(at) === Boolean(inDuration)) {
|
|
@@ -228,9 +273,6 @@ export function parseScheduleCreateArgs(argv) {
|
|
|
228
273
|
message: "create requires exactly one of --at or --in.",
|
|
229
274
|
};
|
|
230
275
|
}
|
|
231
|
-
if (!ideaFile) {
|
|
232
|
-
return { status: "error", message: "create requires --idea-file <path>." };
|
|
233
|
-
}
|
|
234
276
|
if (sawAutoApprove && sawNoAutoApprove) {
|
|
235
277
|
return {
|
|
236
278
|
status: "error",
|
|
@@ -244,21 +286,60 @@ export function parseScheduleCreateArgs(argv) {
|
|
|
244
286
|
"(start alphanumeric; letters, digits, '_' and '-'; max 64 chars).",
|
|
245
287
|
};
|
|
246
288
|
}
|
|
289
|
+
// Legacy compatibility: --idea-file without --command implies full-automation.
|
|
290
|
+
// --idea-file with any OTHER command is rejected (it only applies to that path).
|
|
291
|
+
if (ideaFile !== undefined) {
|
|
292
|
+
if (commandName === undefined) {
|
|
293
|
+
commandName = "full-automation";
|
|
294
|
+
}
|
|
295
|
+
else if (commandName !== "full-automation") {
|
|
296
|
+
return {
|
|
297
|
+
status: "error",
|
|
298
|
+
message: `--idea-file is only valid for legacy full-automation scheduling, not --command '${commandName}'. ` +
|
|
299
|
+
"Pass command arguments after '--' instead.",
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
if (commandName === undefined) {
|
|
304
|
+
return {
|
|
305
|
+
status: "error",
|
|
306
|
+
message: "create requires --command <name> (or legacy --idea-file <path>).",
|
|
307
|
+
};
|
|
308
|
+
}
|
|
247
309
|
return {
|
|
248
310
|
status: "ok",
|
|
249
311
|
subcommand: "create",
|
|
250
312
|
options: {
|
|
251
313
|
at,
|
|
252
314
|
in: inDuration,
|
|
315
|
+
commandName,
|
|
316
|
+
commandArgs,
|
|
253
317
|
ideaFile,
|
|
254
318
|
agent,
|
|
255
319
|
repoPath,
|
|
256
320
|
autoApprove: !sawNoAutoApprove,
|
|
321
|
+
autoApproveExplicit: sawAutoApprove || sawNoAutoApprove,
|
|
257
322
|
dryRun,
|
|
258
323
|
id,
|
|
259
324
|
},
|
|
260
325
|
};
|
|
261
326
|
}
|
|
327
|
+
/** Parse the hidden `_execute <id>` shim args (exactly one valid schedule id). */
|
|
328
|
+
export function parseScheduleExecuteArgs(argv) {
|
|
329
|
+
const positionals = argv.filter((a) => !a.startsWith("-"));
|
|
330
|
+
const flags = argv.filter((a) => a.startsWith("-"));
|
|
331
|
+
if (flags.length > 0) {
|
|
332
|
+
return { status: "error", message: `_execute takes no flags (got ${flags.join(", ")}).` };
|
|
333
|
+
}
|
|
334
|
+
if (positionals.length !== 1) {
|
|
335
|
+
return { status: "error", message: "_execute requires exactly one schedule id." };
|
|
336
|
+
}
|
|
337
|
+
const id = positionals[0];
|
|
338
|
+
if (!SCHEDULE_ID_PATTERN.test(id)) {
|
|
339
|
+
return { status: "error", message: `Invalid schedule id: '${id}'.` };
|
|
340
|
+
}
|
|
341
|
+
return { status: "ok", subcommand: "_execute", options: { id } };
|
|
342
|
+
}
|
|
262
343
|
/** Parse `list` args. */
|
|
263
344
|
export function parseScheduleListArgs(argv) {
|
|
264
345
|
if (argv.includes("-h") || argv.includes("--help")) {
|
|
@@ -427,6 +508,9 @@ export function parseScheduleRunArgs(argv) {
|
|
|
427
508
|
return parseScheduleCancelArgs(rest);
|
|
428
509
|
case "doctor":
|
|
429
510
|
return parseScheduleDoctorArgs(rest);
|
|
511
|
+
// Hidden internal shim the OS unit invokes; intentionally absent from usage.
|
|
512
|
+
case "_execute":
|
|
513
|
+
return parseScheduleExecuteArgs(rest);
|
|
430
514
|
default:
|
|
431
515
|
return {
|
|
432
516
|
status: "error",
|
|
@@ -464,24 +548,42 @@ function resolveAbsolute(p, deps) {
|
|
|
464
548
|
return pathApi.isAbsolute(p) ? pathApi.normalize(p) : pathApi.resolve(deps.cwd, p);
|
|
465
549
|
}
|
|
466
550
|
/**
|
|
467
|
-
*
|
|
468
|
-
*
|
|
469
|
-
*
|
|
470
|
-
|
|
471
|
-
|
|
551
|
+
* Collect read-only prerequisite/auth readiness for the selected create input.
|
|
552
|
+
* Returns presence/readiness booleans only — it never reads or returns secret
|
|
553
|
+
* values (CURSOR_API_KEY / Bridge credential material).
|
|
554
|
+
*/
|
|
555
|
+
export function collectScheduleRunPrereqStatus(input) {
|
|
556
|
+
return {
|
|
557
|
+
agentBinaryResolved: input.agentBinaryResolved,
|
|
558
|
+
repoPathExists: input.repoPathExists,
|
|
559
|
+
cursorApiKeyPresent: Boolean(input.deps.env.CURSOR_API_KEY),
|
|
560
|
+
bridgeCredentialResolved: input.deps.bridgeCredentialResolved?.() ?? Boolean(input.deps.env.BAPI_API_KEY),
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* Resolve and validate generic create inputs. Resolves the repo path first and
|
|
565
|
+
* validates it is a directory; discovers the `.claude/commands` catalog under it;
|
|
566
|
+
* resolves and validates the requested command (rejecting unknown /
|
|
567
|
+
* `schedulable: false` / malformed); validates structured argv against the
|
|
568
|
+
* command's schema; resolves the agent binary on the baked PATH; builds the agent
|
|
569
|
+
* invocation via the launcher and the trigger invocation for the `_execute` shim.
|
|
570
|
+
* The legacy `--idea-file` path is converted to structured `["--idea-file", abs]`
|
|
571
|
+
* args for `full-automation`. Installs nothing — orchestration owns side effects.
|
|
472
572
|
*/
|
|
473
573
|
export async function resolveScheduleCreateInput(options, runAtIso, deps) {
|
|
474
|
-
const
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
}
|
|
574
|
+
const launcher = getAgentLauncher(options.agent);
|
|
575
|
+
if (!launcher) {
|
|
576
|
+
return {
|
|
577
|
+
ok: false,
|
|
578
|
+
error: `Unsupported agent: '${options.agent}'. Valid: ${formatValidAgentLauncherNames()}.`,
|
|
579
|
+
};
|
|
481
580
|
}
|
|
482
|
-
|
|
483
|
-
|
|
581
|
+
const commandName = options.commandName;
|
|
582
|
+
if (!commandName) {
|
|
583
|
+
return { ok: false, error: "create requires a command (--command or legacy --idea-file)." };
|
|
484
584
|
}
|
|
585
|
+
// Resolve + validate the repo path (command-catalog root) BEFORE discovery.
|
|
586
|
+
const repoPath = resolveAbsolute(options.repoPath ?? deps.cwd, deps);
|
|
485
587
|
try {
|
|
486
588
|
const repoStat = await fs.stat(repoPath);
|
|
487
589
|
if (!repoStat.isDirectory()) {
|
|
@@ -491,30 +593,75 @@ export async function resolveScheduleCreateInput(options, runAtIso, deps) {
|
|
|
491
593
|
catch {
|
|
492
594
|
return { ok: false, error: `--repo-path does not exist: ${repoPath}` };
|
|
493
595
|
}
|
|
596
|
+
// Build the structured command argv. Legacy --idea-file is validated and turned
|
|
597
|
+
// into ["--idea-file", <abs>] (full-automation only; enforced by the parser).
|
|
598
|
+
let commandArgs = options.commandArgs;
|
|
599
|
+
let legacyIdeaFile;
|
|
600
|
+
if (options.ideaFile !== undefined) {
|
|
601
|
+
const ideaFile = resolveAbsolute(options.ideaFile, deps);
|
|
602
|
+
try {
|
|
603
|
+
const ideaStat = await fs.stat(ideaFile);
|
|
604
|
+
if (!ideaStat.isFile())
|
|
605
|
+
return { ok: false, error: `--idea-file is not a file: ${ideaFile}` };
|
|
606
|
+
}
|
|
607
|
+
catch {
|
|
608
|
+
return { ok: false, error: `--idea-file does not exist: ${ideaFile}` };
|
|
609
|
+
}
|
|
610
|
+
legacyIdeaFile = ideaFile;
|
|
611
|
+
commandArgs = ["--idea-file", ideaFile];
|
|
612
|
+
}
|
|
613
|
+
// Discover the command catalog under the resolved repo and resolve the command.
|
|
614
|
+
const catalog = await discoverCommandCatalog(repoPath, deps.platform, deps.commandCatalogFsDeps);
|
|
615
|
+
const resolvedCommand = resolveSchedulableCommand(catalog, commandName);
|
|
616
|
+
if (!resolvedCommand.ok)
|
|
617
|
+
return { ok: false, error: resolvedCommand.error };
|
|
618
|
+
const command = resolvedCommand.command;
|
|
619
|
+
// Validate the structured argv against the command's schema (or passthrough).
|
|
620
|
+
const validation = validateCommandArgv(commandArgs, command.argumentSchema);
|
|
621
|
+
if (!validation.ok) {
|
|
622
|
+
return { ok: false, error: `Invalid arguments for '${commandName}': ${validation.error}` };
|
|
623
|
+
}
|
|
624
|
+
const normalizedArgv = validation.normalizedArgv;
|
|
494
625
|
const envPath = deps.env.PATH ?? deps.env.Path ?? "";
|
|
495
626
|
const nodePath = deps.execPath;
|
|
496
|
-
const
|
|
497
|
-
if (!
|
|
498
|
-
return {
|
|
499
|
-
ok: false,
|
|
500
|
-
error: `Unsupported agent: '${options.agent}'. Valid: ${formatValidAgentLauncherNames()}.`,
|
|
501
|
-
};
|
|
502
|
-
}
|
|
503
|
-
const claudePath = await launcher.resolveBinary(envPath, deps);
|
|
504
|
-
if (!claudePath) {
|
|
627
|
+
const agentPath = await launcher.resolveBinary(envPath, deps);
|
|
628
|
+
if (!agentPath) {
|
|
505
629
|
return {
|
|
506
630
|
ok: false,
|
|
507
631
|
error: `Could not resolve the '${launcher.capability.command}' binary on the baked PATH.`,
|
|
508
632
|
};
|
|
509
633
|
}
|
|
510
|
-
// npx is resolved best-effort
|
|
634
|
+
// npx is resolved best-effort; empty when absent.
|
|
511
635
|
const npxPath = (await resolveCommandOnPath("npx", envPath, deps)) ?? "";
|
|
512
|
-
const
|
|
636
|
+
const id = options.id ?? randomUUID();
|
|
637
|
+
const agentInvocation = launcher.buildInvocation(agentPath, {
|
|
638
|
+
scheduleId: id,
|
|
513
639
|
runAtIso,
|
|
514
|
-
|
|
640
|
+
commandName,
|
|
641
|
+
args: normalizedArgv,
|
|
515
642
|
autoApprove: options.autoApprove,
|
|
643
|
+
repoPath,
|
|
644
|
+
commandFilePath: command.filePath,
|
|
645
|
+
commandBody: command.body,
|
|
646
|
+
schema: command.argumentSchema,
|
|
516
647
|
});
|
|
517
|
-
const
|
|
648
|
+
const cliEntryPath = deps.cliEntryPath ?? process.argv[1] ?? "";
|
|
649
|
+
const triggerInvocation = {
|
|
650
|
+
exe: nodePath,
|
|
651
|
+
args: [cliEntryPath, "schedule-run", "_execute", id],
|
|
652
|
+
};
|
|
653
|
+
const commandBodyHash = createHash("sha256").update(command.body, "utf-8").digest("hex");
|
|
654
|
+
const warnings = [];
|
|
655
|
+
if (command.interactive && !options.autoApproveExplicit) {
|
|
656
|
+
warnings.push(`Command '${commandName}' has an interactive step but --auto/--no-auto was not specified; ` +
|
|
657
|
+
"a headless scheduled run may hang waiting for confirmation. Pass --auto to run hands-off.");
|
|
658
|
+
}
|
|
659
|
+
if (isTransientNpxEntryPath(cliEntryPath)) {
|
|
660
|
+
warnings.push(`The scheduled run re-enters this CLI at '${cliEntryPath}', which is inside an npx cache ` +
|
|
661
|
+
"directory. npx caches can be pruned before a future schedule fires, which would make the " +
|
|
662
|
+
"run fail with ENOENT. Install the CLI persistently (e.g. `npm i -g @bridge_gpt/mcp-server`) " +
|
|
663
|
+
"and schedule with the installed binary so the trigger path stays stable.");
|
|
664
|
+
}
|
|
518
665
|
const paths = getSchedulePaths(id, deps.homeDir, deps.platform);
|
|
519
666
|
return {
|
|
520
667
|
ok: true,
|
|
@@ -522,40 +669,71 @@ export async function resolveScheduleCreateInput(options, runAtIso, deps) {
|
|
|
522
669
|
id,
|
|
523
670
|
runAtIso,
|
|
524
671
|
agent: options.agent,
|
|
525
|
-
|
|
672
|
+
command: commandName,
|
|
673
|
+
args: normalizedArgv,
|
|
674
|
+
commandFilePath: command.filePath,
|
|
675
|
+
commandBodyHash,
|
|
676
|
+
autoApprove: options.autoApprove,
|
|
526
677
|
repoPath,
|
|
678
|
+
legacyIdeaFile,
|
|
527
679
|
envPath,
|
|
528
680
|
nodePath,
|
|
529
681
|
npxPath,
|
|
530
|
-
|
|
531
|
-
|
|
682
|
+
agentPath,
|
|
683
|
+
agentInvocation,
|
|
684
|
+
triggerInvocation,
|
|
685
|
+
renderedPrompt: agentInvocation.prompt,
|
|
686
|
+
schema: command.argumentSchema,
|
|
532
687
|
paths: {
|
|
533
688
|
schedulesDir: paths.schedulesDir,
|
|
534
689
|
logsDir: paths.logsDir,
|
|
535
690
|
stdoutPath: paths.stdoutPath,
|
|
536
691
|
stderrPath: paths.stderrPath,
|
|
537
692
|
},
|
|
693
|
+
warnings,
|
|
694
|
+
prereq: collectScheduleRunPrereqStatus({
|
|
695
|
+
agent: options.agent,
|
|
696
|
+
agentBinaryResolved: Boolean(agentPath),
|
|
697
|
+
repoPathExists: true,
|
|
698
|
+
deps,
|
|
699
|
+
}),
|
|
538
700
|
},
|
|
539
701
|
};
|
|
540
702
|
}
|
|
541
|
-
/**
|
|
703
|
+
/**
|
|
704
|
+
* Build the local-only schedule metadata from resolved inputs + backend result.
|
|
705
|
+
* Writes the generalized fields plus legacy compatibility aliases (`invocation`,
|
|
706
|
+
* `claude_path`, `idea_file`) so old readers and full-automation records keep
|
|
707
|
+
* working. Run history is initialized with a `created` event.
|
|
708
|
+
*/
|
|
542
709
|
export function buildScheduleMetadata(resolved, createResult, createdAtIso) {
|
|
543
710
|
return {
|
|
544
711
|
id: resolved.id,
|
|
545
712
|
run_at_iso: resolved.runAtIso,
|
|
713
|
+
scheduled_at: resolved.runAtIso,
|
|
546
714
|
backend: createResult.backend,
|
|
547
715
|
unit_path: createResult.unitPath,
|
|
548
716
|
unit_paths: createResult.unitPaths,
|
|
549
717
|
backend_job_id: createResult.backendJobId,
|
|
550
718
|
agent: resolved.agent,
|
|
551
|
-
|
|
719
|
+
agent_path: resolved.agentPath,
|
|
720
|
+
command: resolved.command,
|
|
721
|
+
args: resolved.args,
|
|
722
|
+
command_file: resolved.commandFilePath,
|
|
723
|
+
command_body_hash: resolved.commandBodyHash,
|
|
724
|
+
auto_approve: resolved.autoApprove,
|
|
552
725
|
repo_path: resolved.repoPath,
|
|
553
726
|
created_at: createdAtIso,
|
|
554
727
|
env_path: resolved.envPath,
|
|
555
728
|
node_path: resolved.nodePath,
|
|
556
729
|
npx_path: resolved.npxPath,
|
|
557
|
-
|
|
558
|
-
|
|
730
|
+
// Legacy alias retained for full-automation/claude records.
|
|
731
|
+
claude_path: resolved.agent === "claude" ? resolved.agentPath : undefined,
|
|
732
|
+
idea_file: resolved.legacyIdeaFile,
|
|
733
|
+
invocation: resolved.agentInvocation,
|
|
734
|
+
agent_invocation: resolved.agentInvocation,
|
|
735
|
+
trigger_invocation: resolved.triggerInvocation,
|
|
736
|
+
run_history: [{ status: "created", at: createdAtIso }],
|
|
559
737
|
logs: { stdout: resolved.paths.stdoutPath, stderr: resolved.paths.stderrPath },
|
|
560
738
|
};
|
|
561
739
|
}
|
|
@@ -621,13 +799,17 @@ export async function orchestrateScheduleCreate(options, deps) {
|
|
|
621
799
|
deps,
|
|
622
800
|
id: resolved.id,
|
|
623
801
|
runAtIso: resolved.runAtIso,
|
|
624
|
-
|
|
802
|
+
triggerInvocation: resolved.triggerInvocation,
|
|
803
|
+
agentInvocation: resolved.agentInvocation,
|
|
804
|
+
command: resolved.command,
|
|
805
|
+
args: resolved.args,
|
|
806
|
+
agent: resolved.agent,
|
|
625
807
|
repoPath: resolved.repoPath,
|
|
626
|
-
ideaFile: resolved.ideaFile,
|
|
627
808
|
envPath: resolved.envPath,
|
|
628
809
|
nodePath: resolved.nodePath,
|
|
629
810
|
npxPath: resolved.npxPath,
|
|
630
|
-
|
|
811
|
+
agentPath: resolved.agentPath,
|
|
812
|
+
legacyIdeaFile: resolved.legacyIdeaFile,
|
|
631
813
|
paths: resolved.paths,
|
|
632
814
|
dryRun: options.dryRun,
|
|
633
815
|
};
|
|
@@ -637,7 +819,7 @@ export async function orchestrateScheduleCreate(options, deps) {
|
|
|
637
819
|
return { ok: false, error: createResult.error ?? "Backend dry-run failed." };
|
|
638
820
|
}
|
|
639
821
|
const metadata = buildScheduleMetadata(resolved, createResult, createdAtIso);
|
|
640
|
-
return { ok: true, dryRun: true, metadata, metadataPath, createResult };
|
|
822
|
+
return { ok: true, dryRun: true, metadata, metadataPath, createResult, resolved };
|
|
641
823
|
}
|
|
642
824
|
await ensureScheduleDirectories(deps.homeDir, deps.platform);
|
|
643
825
|
const createResult = await backend.create(createInput);
|
|
@@ -645,6 +827,9 @@ export async function orchestrateScheduleCreate(options, deps) {
|
|
|
645
827
|
return { ok: false, error: createResult.error ?? "Scheduler backend create failed." };
|
|
646
828
|
}
|
|
647
829
|
const metadata = buildScheduleMetadata(resolved, createResult, createdAtIso);
|
|
830
|
+
// Append the `scheduled` event only after the backend created a real OS job.
|
|
831
|
+
const scheduledAtIso = new Date(deps.now ? deps.now() : Date.now()).toISOString();
|
|
832
|
+
metadata.run_history = [...(metadata.run_history ?? []), { status: "scheduled", at: scheduledAtIso }];
|
|
648
833
|
try {
|
|
649
834
|
await writeScheduleMetadata(metadata, deps.homeDir, deps.platform);
|
|
650
835
|
}
|
|
@@ -657,33 +842,73 @@ export async function orchestrateScheduleCreate(options, deps) {
|
|
|
657
842
|
`(${error instanceof Error ? error.message : String(error)}); attempted to cancel the native unit.`,
|
|
658
843
|
};
|
|
659
844
|
}
|
|
660
|
-
return { ok: true, dryRun: false, metadata, metadataPath, createResult };
|
|
845
|
+
return { ok: true, dryRun: false, metadata, metadataPath, createResult, resolved };
|
|
846
|
+
}
|
|
847
|
+
/** Format a prereq status block (presence/readiness only, never secret values). */
|
|
848
|
+
function formatPrereqStatus(agent, prereq) {
|
|
849
|
+
const lines = [
|
|
850
|
+
` agent binary: ${prereq.agentBinaryResolved ? "resolved" : "MISSING"}`,
|
|
851
|
+
` repo path: ${prereq.repoPathExists ? "exists" : "MISSING"}`,
|
|
852
|
+
];
|
|
853
|
+
if (agent === "cursor-agent") {
|
|
854
|
+
lines.push(` CURSOR_API_KEY set: ${prereq.cursorApiKeyPresent ? "yes" : "no"}`);
|
|
855
|
+
}
|
|
856
|
+
else {
|
|
857
|
+
lines.push(` Bridge credential: ${prereq.bridgeCredentialResolved ? "resolved" : "not resolved"}`);
|
|
858
|
+
}
|
|
859
|
+
return lines;
|
|
661
860
|
}
|
|
662
|
-
/**
|
|
861
|
+
/**
|
|
862
|
+
* Format a create result for display. Real create and dry-run both show the
|
|
863
|
+
* delegated command, normalized args, schedule time, agent, repo path, metadata
|
|
864
|
+
* path, agent binary, trigger invocation, agent invocation argv, and the rendered
|
|
865
|
+
* target command line. Dry-run additionally prints the full concatenated prompt,
|
|
866
|
+
* the prerequisite/auth status (secret-free), and the backend artifacts.
|
|
867
|
+
*/
|
|
663
868
|
export function formatScheduleCreateResult(result) {
|
|
664
869
|
if (!result.ok)
|
|
665
870
|
return `Error: ${result.error}`;
|
|
666
871
|
const m = result.metadata;
|
|
872
|
+
const r = result.resolved;
|
|
873
|
+
const targetCommandLine = buildTargetCommandLine({
|
|
874
|
+
commandName: r.command,
|
|
875
|
+
args: r.args,
|
|
876
|
+
scheduledAt: r.runAtIso,
|
|
877
|
+
autoApprove: r.autoApprove,
|
|
878
|
+
schema: r.schema,
|
|
879
|
+
});
|
|
667
880
|
const lines = [];
|
|
668
881
|
lines.push(result.dryRun ? "[dry-run] schedule-run create preview" : "Schedule created.");
|
|
669
|
-
lines.push(` id:
|
|
670
|
-
lines.push(` backend:
|
|
671
|
-
lines.push(`
|
|
672
|
-
lines.push(`
|
|
673
|
-
lines.push(`
|
|
674
|
-
lines.push(`
|
|
675
|
-
lines.push(`
|
|
676
|
-
lines.push(`
|
|
677
|
-
lines.push(`
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
lines.push(`
|
|
681
|
-
lines.push(`
|
|
682
|
-
lines.push(`
|
|
882
|
+
lines.push(` id: ${m.id}`);
|
|
883
|
+
lines.push(` backend: ${m.backend}`);
|
|
884
|
+
lines.push(` command: ${r.command}`);
|
|
885
|
+
lines.push(` args: ${r.args.length > 0 ? r.args.join(" ") : "(none)"}`);
|
|
886
|
+
lines.push(` run at: ${m.run_at_iso}`);
|
|
887
|
+
lines.push(` agent: ${m.agent}`);
|
|
888
|
+
lines.push(` auto-approve: ${m.auto_approve ? "yes" : "no"}`);
|
|
889
|
+
lines.push(` metadata: ${result.metadataPath}${result.dryRun ? " (not written in dry-run)" : ""}`);
|
|
890
|
+
lines.push(` repo path: ${m.repo_path}`);
|
|
891
|
+
if (m.idea_file)
|
|
892
|
+
lines.push(` idea file: ${m.idea_file}`);
|
|
893
|
+
lines.push(` stdout log: ${m.logs.stdout}`);
|
|
894
|
+
lines.push(` stderr log: ${m.logs.stderr}`);
|
|
895
|
+
lines.push(` agent binary: ${r.agentPath}`);
|
|
896
|
+
lines.push(` target command: ${targetCommandLine}`);
|
|
897
|
+
lines.push(` trigger invocation: ${[r.triggerInvocation.exe, ...r.triggerInvocation.args].join(" ")}`);
|
|
898
|
+
lines.push(` agent invocation: ${[r.agentInvocation.exe, ...r.agentInvocation.args].join(" ")}`);
|
|
683
899
|
if (m.unit_paths.length > 0) {
|
|
684
|
-
lines.push(` unit paths:
|
|
900
|
+
lines.push(` unit paths: ${m.unit_paths.join(", ")}`);
|
|
901
|
+
}
|
|
902
|
+
for (const warning of r.warnings) {
|
|
903
|
+
lines.push(` warning: ${warning}`);
|
|
685
904
|
}
|
|
686
905
|
if (result.dryRun) {
|
|
906
|
+
lines.push("");
|
|
907
|
+
lines.push("prerequisites (secret-free):");
|
|
908
|
+
lines.push(...formatPrereqStatus(r.agent, r.prereq));
|
|
909
|
+
lines.push("");
|
|
910
|
+
lines.push("----- rendered prompt -----");
|
|
911
|
+
lines.push(r.renderedPrompt);
|
|
687
912
|
for (const artifact of result.createResult.artifacts) {
|
|
688
913
|
lines.push("");
|
|
689
914
|
lines.push(`----- ${artifact.kind}: ${artifact.path} -----`);
|
|
@@ -745,6 +970,19 @@ export async function orchestrateScheduleList(options, deps) {
|
|
|
745
970
|
entries.sort((a, b) => a.metadata.id.localeCompare(b.metadata.id));
|
|
746
971
|
return { entries, warnings };
|
|
747
972
|
}
|
|
973
|
+
/** Derive the displayed command (legacy rows show `full-automation`). */
|
|
974
|
+
export function scheduleCommandLabel(m) {
|
|
975
|
+
if (m.command)
|
|
976
|
+
return m.command;
|
|
977
|
+
if (m.idea_file)
|
|
978
|
+
return "full-automation";
|
|
979
|
+
return "(unknown)";
|
|
980
|
+
}
|
|
981
|
+
/** Latest run-history status, or empty string when no history is recorded. */
|
|
982
|
+
export function latestRunStatus(m) {
|
|
983
|
+
const h = m.run_history;
|
|
984
|
+
return h && h.length > 0 ? h[h.length - 1].status : "";
|
|
985
|
+
}
|
|
748
986
|
/** Format a list report as a table (or JSON when requested). */
|
|
749
987
|
export function formatScheduleListResult(report, json) {
|
|
750
988
|
if (json) {
|
|
@@ -753,9 +991,15 @@ export function formatScheduleListResult(report, json) {
|
|
|
753
991
|
return JSON.stringify({
|
|
754
992
|
entries: report.entries.map((e) => ({
|
|
755
993
|
id: e.metadata.id,
|
|
994
|
+
command: scheduleCommandLabel(e.metadata),
|
|
995
|
+
args: e.metadata.args ?? [],
|
|
996
|
+
scheduled_at: e.metadata.scheduled_at ?? e.metadata.run_at_iso,
|
|
756
997
|
run_at: e.metadata.run_at_iso,
|
|
757
998
|
backend: e.metadata.backend,
|
|
758
999
|
agent: e.metadata.agent,
|
|
1000
|
+
native_status: e.status,
|
|
1001
|
+
latest_run_status: latestRunStatus(e.metadata),
|
|
1002
|
+
run_history: e.metadata.run_history ?? [],
|
|
759
1003
|
status: e.status,
|
|
760
1004
|
unit_path: e.metadata.unit_path,
|
|
761
1005
|
})),
|
|
@@ -769,14 +1013,16 @@ export function formatScheduleListResult(report, json) {
|
|
|
769
1013
|
lines.push("No schedules found.");
|
|
770
1014
|
return lines.join("\n");
|
|
771
1015
|
}
|
|
772
|
-
lines.push(["ID", "RUN_AT", "BACKEND", "AGENT", "
|
|
1016
|
+
lines.push(["ID", "COMMAND", "RUN_AT", "BACKEND", "AGENT", "NATIVE", "LATEST", "UNIT_PATH"].join(" "));
|
|
773
1017
|
for (const e of report.entries) {
|
|
774
1018
|
lines.push([
|
|
775
1019
|
e.metadata.id,
|
|
1020
|
+
scheduleCommandLabel(e.metadata),
|
|
776
1021
|
e.metadata.run_at_iso,
|
|
777
1022
|
e.metadata.backend,
|
|
778
1023
|
e.metadata.agent,
|
|
779
1024
|
e.status,
|
|
1025
|
+
latestRunStatus(e.metadata) || "-",
|
|
780
1026
|
e.metadata.unit_path ?? "-",
|
|
781
1027
|
].join(" "));
|
|
782
1028
|
}
|
|
@@ -810,6 +1056,11 @@ export async function orchestrateScheduleCancel(options, deps) {
|
|
|
810
1056
|
if (!cancelResult.ok) {
|
|
811
1057
|
return { ok: false, error: cancelResult.error ?? "Backend cancel failed." };
|
|
812
1058
|
}
|
|
1059
|
+
// Best-effort: record a `canceled` event before deleting metadata. This is
|
|
1060
|
+
// primarily useful if deletion later fails, or if cancellation is refactored
|
|
1061
|
+
// to retain records; a failure here must not mask a successful native cancel.
|
|
1062
|
+
const canceledAtIso = new Date(deps.now ? deps.now() : Date.now()).toISOString();
|
|
1063
|
+
await appendScheduleRunEvent(options.id, { status: "canceled", at: canceledAtIso }, deps.homeDir, deps.platform).catch(() => undefined);
|
|
813
1064
|
// Stale or removed both delete local metadata; logs are preserved by the store.
|
|
814
1065
|
await deleteScheduleMetadata(options.id, deps.homeDir, deps.platform);
|
|
815
1066
|
return {
|
|
@@ -833,12 +1084,20 @@ export function formatScheduleCancelResult(result) {
|
|
|
833
1084
|
" logs: preserved",
|
|
834
1085
|
].join("\n");
|
|
835
1086
|
}
|
|
836
|
-
/**
|
|
1087
|
+
/**
|
|
1088
|
+
* Read-only diagnostics: platform support, backend order/availability, both agent
|
|
1089
|
+
* binaries, and secret-free auth readiness for each agent. Stays zero-argument
|
|
1090
|
+
* and environment-oriented — command-specific prompt rendering belongs to
|
|
1091
|
+
* `create --dry-run`, never here.
|
|
1092
|
+
*/
|
|
837
1093
|
export async function orchestrateScheduleDoctor(deps) {
|
|
838
1094
|
const platformResult = getSchedulerBackendsForPlatform(deps.platform);
|
|
839
1095
|
const envPath = deps.env.PATH ?? deps.env.Path ?? "";
|
|
840
1096
|
const claudeResolved = Boolean(await resolveCommandOnPath("claude", envPath, deps));
|
|
1097
|
+
const cursorResolved = Boolean(await resolveCommandOnPath("cursor-agent", envPath, deps));
|
|
841
1098
|
const npxResolved = Boolean(await resolveCommandOnPath("npx", envPath, deps));
|
|
1099
|
+
const cursorApiKeyPresent = Boolean(deps.env.CURSOR_API_KEY);
|
|
1100
|
+
const bridgeCredentialResolved = deps.bridgeCredentialResolved?.() ?? Boolean(deps.env.BAPI_API_KEY);
|
|
842
1101
|
if (!platformResult.ok) {
|
|
843
1102
|
return {
|
|
844
1103
|
platform: deps.platform,
|
|
@@ -846,7 +1105,10 @@ export async function orchestrateScheduleDoctor(deps) {
|
|
|
846
1105
|
candidateBackends: [],
|
|
847
1106
|
backendAvailability: [],
|
|
848
1107
|
claudeResolved,
|
|
1108
|
+
cursorResolved,
|
|
849
1109
|
npxResolved,
|
|
1110
|
+
cursorApiKeyPresent,
|
|
1111
|
+
bridgeCredentialResolved,
|
|
850
1112
|
unsupportedMessage: platformResult.error,
|
|
851
1113
|
};
|
|
852
1114
|
}
|
|
@@ -861,10 +1123,13 @@ export async function orchestrateScheduleDoctor(deps) {
|
|
|
861
1123
|
candidateBackends,
|
|
862
1124
|
backendAvailability,
|
|
863
1125
|
claudeResolved,
|
|
1126
|
+
cursorResolved,
|
|
864
1127
|
npxResolved,
|
|
1128
|
+
cursorApiKeyPresent,
|
|
1129
|
+
bridgeCredentialResolved,
|
|
865
1130
|
};
|
|
866
1131
|
}
|
|
867
|
-
/** Format the doctor report (table or JSON). */
|
|
1132
|
+
/** Format the doctor report (table or JSON). Never prints secret values. */
|
|
868
1133
|
export function formatScheduleDoctorReport(report, json) {
|
|
869
1134
|
if (json)
|
|
870
1135
|
return JSON.stringify(report, null, 2);
|
|
@@ -881,10 +1146,82 @@ export function formatScheduleDoctorReport(report, json) {
|
|
|
881
1146
|
lines.push(` ${a.available ? "AVAILABLE " : "UNAVAILABLE"} ${a.backend}`);
|
|
882
1147
|
}
|
|
883
1148
|
}
|
|
884
|
-
lines.push(`claude on PATH:
|
|
885
|
-
lines.push(`
|
|
1149
|
+
lines.push(`claude on PATH: ${report.claudeResolved ? "yes" : "no"}`);
|
|
1150
|
+
lines.push(`cursor-agent on PATH: ${report.cursorResolved ? "yes" : "no"}`);
|
|
1151
|
+
lines.push(`npx on PATH: ${report.npxResolved ? "yes" : "no"}`);
|
|
1152
|
+
lines.push(`CURSOR_API_KEY set: ${report.cursorApiKeyPresent ? "yes" : "no"}`);
|
|
1153
|
+
lines.push(`Bridge credential: ${report.bridgeCredentialResolved ? "resolved" : "not resolved"}`);
|
|
886
1154
|
return lines.join("\n");
|
|
887
1155
|
}
|
|
1156
|
+
/** Current time as ISO from the injectable clock. */
|
|
1157
|
+
function nowIso(deps) {
|
|
1158
|
+
return new Date(deps.now ? deps.now() : Date.now()).toISOString();
|
|
1159
|
+
}
|
|
1160
|
+
/**
|
|
1161
|
+
* The hidden `_execute <id>` shim the OS unit runs at fire time (BAPI-351). It
|
|
1162
|
+
* reads the local schedule metadata, validates the stored agent invocation,
|
|
1163
|
+
* records a `started` event, spawns the agent via the list-based `runCommand`
|
|
1164
|
+
* boundary (NEVER a shell string) with the schedule's cwd + baked env, forwards
|
|
1165
|
+
* the agent's stdout/stderr to its own streams (so existing OS log redirection
|
|
1166
|
+
* captures them), then records `completed`/`failed` and returns the agent's exit
|
|
1167
|
+
* code. Run-history writes are best-effort and never mask the agent's exit code;
|
|
1168
|
+
* env values (including any secrets) are never printed.
|
|
1169
|
+
*/
|
|
1170
|
+
export async function orchestrateScheduleExecute(options, deps, io) {
|
|
1171
|
+
const metadata = await readScheduleMetadata(options.id, deps.homeDir, deps.platform);
|
|
1172
|
+
if (!metadata) {
|
|
1173
|
+
return { ok: false, exitCode: 1, error: `No schedule found with id '${options.id}'.` };
|
|
1174
|
+
}
|
|
1175
|
+
const agentInvocation = metadata.agent_invocation ?? metadata.invocation;
|
|
1176
|
+
if (!agentInvocation || !agentInvocation.exe) {
|
|
1177
|
+
await appendScheduleRunEvent(options.id, { status: "failed", at: nowIso(deps), message: "missing agent_invocation" }, deps.homeDir, deps.platform).catch(() => undefined);
|
|
1178
|
+
return { ok: false, exitCode: 1, error: `Schedule '${options.id}' has no agent invocation to run.` };
|
|
1179
|
+
}
|
|
1180
|
+
await appendScheduleRunEvent(options.id, { status: "started", at: nowIso(deps) }, deps.homeDir, deps.platform).catch(() => undefined);
|
|
1181
|
+
// Child env: base env + baked PATH + schedule-context variables. No secrets are
|
|
1182
|
+
// synthesized here; only values already present in metadata/env are forwarded.
|
|
1183
|
+
const env = { ...deps.env };
|
|
1184
|
+
if (metadata.env_path) {
|
|
1185
|
+
env.PATH = metadata.env_path;
|
|
1186
|
+
if (deps.platform === "win32")
|
|
1187
|
+
env.Path = metadata.env_path;
|
|
1188
|
+
}
|
|
1189
|
+
env.BRIDGE_GPT_SCHEDULE_ID = metadata.id;
|
|
1190
|
+
if (metadata.command)
|
|
1191
|
+
env.BRIDGE_GPT_COMMAND = metadata.command;
|
|
1192
|
+
if (metadata.args)
|
|
1193
|
+
env.BRIDGE_GPT_COMMAND_ARGS_JSON = JSON.stringify(metadata.args);
|
|
1194
|
+
if (metadata.repo_path)
|
|
1195
|
+
env.BRIDGE_GPT_REPO_PATH = metadata.repo_path;
|
|
1196
|
+
if (metadata.agent)
|
|
1197
|
+
env.BRIDGE_GPT_AGENT = metadata.agent;
|
|
1198
|
+
if (metadata.agent_path)
|
|
1199
|
+
env.BRIDGE_GPT_AGENT_PATH = metadata.agent_path;
|
|
1200
|
+
if (metadata.idea_file)
|
|
1201
|
+
env.BRIDGE_GPT_IDEA_FILE = metadata.idea_file;
|
|
1202
|
+
let result;
|
|
1203
|
+
try {
|
|
1204
|
+
result = await deps.runCommand(agentInvocation.exe, agentInvocation.args, {
|
|
1205
|
+
cwd: metadata.repo_path,
|
|
1206
|
+
env,
|
|
1207
|
+
});
|
|
1208
|
+
}
|
|
1209
|
+
catch (error) {
|
|
1210
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
1211
|
+
await appendScheduleRunEvent(options.id, { status: "failed", at: nowIso(deps), message: `agent launch failed: ${msg}` }, deps.homeDir, deps.platform).catch(() => undefined);
|
|
1212
|
+
return { ok: false, exitCode: 1, error: `Failed to launch agent: ${msg}` };
|
|
1213
|
+
}
|
|
1214
|
+
if (result.stdout)
|
|
1215
|
+
io.writeStdout(result.stdout);
|
|
1216
|
+
if (result.stderr)
|
|
1217
|
+
io.writeStderr(result.stderr);
|
|
1218
|
+
if (result.exitCode === 0) {
|
|
1219
|
+
await appendScheduleRunEvent(options.id, { status: "completed", at: nowIso(deps), exit_code: 0 }, deps.homeDir, deps.platform).catch(() => undefined);
|
|
1220
|
+
return { ok: true, exitCode: 0 };
|
|
1221
|
+
}
|
|
1222
|
+
await appendScheduleRunEvent(options.id, { status: "failed", at: nowIso(deps), exit_code: result.exitCode }, deps.homeDir, deps.platform).catch(() => undefined);
|
|
1223
|
+
return { ok: false, exitCode: result.exitCode };
|
|
1224
|
+
}
|
|
888
1225
|
/**
|
|
889
1226
|
* CLI entry for `schedule-run`. Returns a numeric process exit code. Help → 0;
|
|
890
1227
|
* parser/validation errors → 1 (printed with usage to stderr); create/list/
|
|
@@ -936,6 +1273,17 @@ export async function runScheduleRunCli(argv, overrides = {}) {
|
|
|
936
1273
|
log(formatScheduleDoctorReport(report, parsed.options.json));
|
|
937
1274
|
return report.platformSupported ? 0 : 1;
|
|
938
1275
|
}
|
|
1276
|
+
case "_execute": {
|
|
1277
|
+
const io = {
|
|
1278
|
+
writeStdout: overrides.writeStdout ?? ((chunk) => process.stdout.write(chunk)),
|
|
1279
|
+
writeStderr: overrides.writeStderr ?? ((chunk) => process.stderr.write(chunk)),
|
|
1280
|
+
};
|
|
1281
|
+
const result = await orchestrateScheduleExecute(parsed.options, deps, io);
|
|
1282
|
+
if (!result.ok && result.error) {
|
|
1283
|
+
errorLog(`Error: ${result.error}`);
|
|
1284
|
+
}
|
|
1285
|
+
return result.exitCode;
|
|
1286
|
+
}
|
|
939
1287
|
}
|
|
940
1288
|
}
|
|
941
1289
|
catch (error) {
|