@voidlabs.gg/voidshift-agents 0.6.0 → 0.8.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/README.md +31 -0
- package/dist/config.d.ts +6 -0
- package/dist/config.js +11 -0
- package/dist/control.d.ts +20 -0
- package/dist/control.js +103 -23
- package/dist/engine.d.ts +22 -0
- package/dist/engine.js +144 -7
- package/dist/git.d.ts +1 -0
- package/dist/git.js +5 -0
- package/dist/index.js +54 -1
- package/dist/manage.d.ts +2 -2
- package/dist/manage.js +21 -5
- package/dist/notify.d.ts +30 -0
- package/dist/notify.js +233 -0
- package/dist/runner.d.ts +8 -1
- package/dist/runner.js +245 -46
- package/dist/secrets.d.ts +1 -0
- package/dist/secrets.js +12 -1
- package/dist/service.js +1 -1
- package/dist/state.d.ts +40 -1
- package/dist/state.js +116 -0
- package/dist/systemd.d.ts +2 -0
- package/dist/systemd.js +14 -0
- package/dist/usage.d.ts +18 -0
- package/dist/usage.js +102 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -103,6 +103,15 @@ vsf agents project edit erp --workspace solvia --match DS06
|
|
|
103
103
|
Atleurope ERP"). A card of that project runs in the matching project, in any
|
|
104
104
|
workspace, before the workflow's own project is considered.
|
|
105
105
|
|
|
106
|
+
Two settings keep runs inside your Claude subscription. `vsf config set
|
|
107
|
+
agents.maxRuns 2` caps how many cards run at once over every workspace (0, the
|
|
108
|
+
default, is no cap). Every 5 minutes the runner reads your 5 hour window with
|
|
109
|
+
Claude Code's login, read only, and once it reaches `agents.pauseAt` percent
|
|
110
|
+
(90 by default, 0 turns it off) no new run starts until the window resets;
|
|
111
|
+
running cards finish. `vsf agents status` shows the window, and each run's
|
|
112
|
+
`run.json` keeps the tokens and estimated cost of every step. Naming a card,
|
|
113
|
+
`vsf agents run CARD`, starts it anyway.
|
|
114
|
+
|
|
106
115
|
A workflow can also be a folder you write by hand,
|
|
107
116
|
`~/.config/voidshift/agents/defs/NAME/agent.json`, with its prompt files next to
|
|
108
117
|
it. Steps are the built-in ids above or your own:
|
|
@@ -127,6 +136,20 @@ verified card there (`null` never moves it), `labels: false` only takes the
|
|
|
127
136
|
trigger label off, and `comment: false` skips the closing comment. Edit the file,
|
|
128
137
|
then `vsf agents validate`.
|
|
129
138
|
|
|
139
|
+
A step can run a shell command instead of an agent: `"run": "node sweep.mjs"`
|
|
140
|
+
with `output: "none"`. Exit 0 is done, anything else fails the run, and the last
|
|
141
|
+
line it prints is the step's summary. A run made only of command steps works in
|
|
142
|
+
the project folder, with no worktree or branch. A step with `"handoff": "WORKFLOW"`
|
|
143
|
+
hands cards on: every `HANDOFF: ENG-12` line in its output queues that card for
|
|
144
|
+
the named workflow, which runs it as a run of its own, label or not. The card
|
|
145
|
+
leaves the queue once its run has started.
|
|
146
|
+
|
|
147
|
+
Scheduled runs are jobs: `vsf agents status` shows one line per job, the Runs
|
|
148
|
+
list keeps to card runs (`vsf agents runs --jobs` lists the scheduled ones), and
|
|
149
|
+
a scheduled run that opened no pull request removes its worktree and branch. A
|
|
150
|
+
run whose process ends without an outcome is closed as failed on the runner's
|
|
151
|
+
next pass.
|
|
152
|
+
|
|
130
153
|
### memory
|
|
131
154
|
|
|
132
155
|
A folder of markdown notes, one topic per note, with an `INDEX.md` holding one
|
|
@@ -182,6 +205,14 @@ DCO. Only the new commits of that step are rewritten, never pushed ones.
|
|
|
182
205
|
- **Your config** lives in `~/.config/voidshift`.
|
|
183
206
|
- **Linux.** The runner is a systemd user timer. Run
|
|
184
207
|
`sudo loginctl enable-linger $USER` so it keeps going after you log out.
|
|
208
|
+
Agents there run with permissions skipped (`--dangerously-skip-permissions`
|
|
209
|
+
for Claude, `--dangerously-bypass-approvals-and-sandbox` for Codex), so no
|
|
210
|
+
step ever waits on a prompt and the runner's user account is the boundary.
|
|
211
|
+
Run it as a dedicated user with no sudo that holds only the keys it needs,
|
|
212
|
+
never as root, and enable lingering for it from an admin account
|
|
213
|
+
(`sudo loginctl enable-linger <user>`). `vsf doctor` warns when the runner
|
|
214
|
+
user has sudo. A step that fails shows its error in the run log, on the card
|
|
215
|
+
and in Discord.
|
|
185
216
|
|
|
186
217
|
```sh
|
|
187
218
|
vsf doctor # check everything is set up, and whether an update is out
|
package/dist/config.d.ts
CHANGED
|
@@ -14,6 +14,8 @@ export interface StepDef {
|
|
|
14
14
|
label?: string;
|
|
15
15
|
skill?: string;
|
|
16
16
|
prompt?: string;
|
|
17
|
+
run?: string;
|
|
18
|
+
handoff?: string;
|
|
17
19
|
output: Output;
|
|
18
20
|
engine?: Engine;
|
|
19
21
|
model?: string;
|
|
@@ -94,6 +96,8 @@ export interface AgentsConfig {
|
|
|
94
96
|
signOff?: boolean;
|
|
95
97
|
dropAiTrailers?: boolean;
|
|
96
98
|
};
|
|
99
|
+
maxRuns?: number;
|
|
100
|
+
pauseAt?: number;
|
|
97
101
|
folders?: Record<string, string>;
|
|
98
102
|
}
|
|
99
103
|
export declare const TEMPLATES: Record<string, {
|
|
@@ -133,6 +137,8 @@ export interface ResolvedStep {
|
|
|
133
137
|
write: boolean;
|
|
134
138
|
prompt: () => string;
|
|
135
139
|
builtin?: StepId;
|
|
140
|
+
command?: string;
|
|
141
|
+
handoff?: string;
|
|
136
142
|
}
|
|
137
143
|
export declare function stepFile(step: StepDef, dir?: string): string | null;
|
|
138
144
|
export declare function resolveStep(step: Step, config: Pick<AgentsConfig, 'engine' | 'models' | 'timeoutMinutes'>, dir?: string): ResolvedStep;
|
package/dist/config.js
CHANGED
|
@@ -62,6 +62,8 @@ export const DEFAULTS = {
|
|
|
62
62
|
timeoutMinutes: 45,
|
|
63
63
|
homes: [],
|
|
64
64
|
enabled: [],
|
|
65
|
+
maxRuns: 0,
|
|
66
|
+
pauseAt: 90,
|
|
65
67
|
};
|
|
66
68
|
export const agentsDir = (configDir) => join(configDir, 'agents');
|
|
67
69
|
const configPath = (configDir) => join(agentsDir(configDir), 'config.json');
|
|
@@ -221,6 +223,14 @@ export function resolveStep(step, config, dir) {
|
|
|
221
223
|
timeoutMs: config.timeoutMinutes * 60_000, write: s.write, prompt: () => readFileSync(packSkill(step), 'utf8'), builtin: step,
|
|
222
224
|
};
|
|
223
225
|
}
|
|
226
|
+
// A command step runs a shell command, so it has no prompt.
|
|
227
|
+
if (step.run) {
|
|
228
|
+
return {
|
|
229
|
+
id: step.id, label: step.label ?? step.id, output: step.output, engine: step.engine ?? config.engine,
|
|
230
|
+
model: step.model ?? config.models.dev, timeoutMs: (step.timeoutMinutes ?? config.timeoutMinutes) * 60_000,
|
|
231
|
+
write: step.write ?? false, command: step.run, prompt: () => '', handoff: step.handoff,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
224
234
|
return {
|
|
225
235
|
id: step.id,
|
|
226
236
|
label: step.label ?? step.id,
|
|
@@ -235,5 +245,6 @@ export function resolveStep(step, config, dir) {
|
|
|
235
245
|
throw new Error(`Step ${step.id} has no prompt or skill.`);
|
|
236
246
|
return readFileSync(file, 'utf8');
|
|
237
247
|
},
|
|
248
|
+
handoff: step.handoff,
|
|
238
249
|
};
|
|
239
250
|
}
|
package/dist/control.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export declare function countRounds(steps: {
|
|
|
11
11
|
result?: string;
|
|
12
12
|
}[]): number;
|
|
13
13
|
export declare const isInterrupted: (e: RunEntry, live: Current[]) => boolean;
|
|
14
|
+
export declare const shownOutcome: (e: RunEntry, live: Current[]) => string;
|
|
14
15
|
export declare function formatRunLine(e: RunEntry, now?: number, live?: Current[]): string;
|
|
15
16
|
export declare function scanRunnerErrors(text: string, now: number): {
|
|
16
17
|
count: number;
|
|
@@ -28,12 +29,31 @@ export declare function trackerWarnings(kinds: Kind[], trackers: Trackers): Prom
|
|
|
28
29
|
kind: Kind;
|
|
29
30
|
message: string;
|
|
30
31
|
}[]>;
|
|
32
|
+
export declare function changePaused(ctx: Pick<ToolContext, 'configDir' | 'log'>, paused: boolean, reason: string, fetcher?: typeof fetch, platform?: NodeJS.Platform): Promise<void>;
|
|
31
33
|
export declare function workingLine(current: (Current & {
|
|
32
34
|
elapsedMinutes: number;
|
|
33
35
|
}) | null): string;
|
|
34
36
|
export declare function workspaceLines(workspaces: string[], running: (Current & {
|
|
35
37
|
elapsedMinutes: number;
|
|
36
38
|
})[]): string[];
|
|
39
|
+
interface JobSummary {
|
|
40
|
+
workflow: string;
|
|
41
|
+
schedule: string;
|
|
42
|
+
task: string;
|
|
43
|
+
last: {
|
|
44
|
+
at: string;
|
|
45
|
+
outcome: string;
|
|
46
|
+
reason?: string;
|
|
47
|
+
summary?: string;
|
|
48
|
+
} | null;
|
|
49
|
+
lastOkAt: string | null;
|
|
50
|
+
today: {
|
|
51
|
+
runs: number;
|
|
52
|
+
failed: number;
|
|
53
|
+
handed: number;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
export declare function jobSummaries(config: Pick<AgentsConfig, 'workflows'>, all: RunEntry[], live: Current[], now?: number): JobSummary[];
|
|
37
57
|
export declare function pickCancel(locks: Current[], dispatcher: Current | null, card?: string): Current | string;
|
|
38
58
|
export declare const controlCommands: Record<string, ToolCommand>;
|
|
39
59
|
export declare function controlChecks(ctx: ToolContext): Promise<Check[]>;
|
package/dist/control.js
CHANGED
|
@@ -2,13 +2,15 @@ import { spawnSync } from 'node:child_process';
|
|
|
2
2
|
import { closeSync, existsSync, openSync, readFileSync, readSync, readdirSync, rmSync, statSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { parseArgs } from 'node:util';
|
|
5
|
-
import { agentsDir, isCardTrigger, readAgentsConfig, workspaceOf, workspacesInUse } from "./config.js";
|
|
6
|
-
import { isPaused, listRuns, ours, clearLock, liveCurrent, liveRuns, readCurrent, setPaused, workspaceLocks, writeRun } from "./state.js";
|
|
5
|
+
import { DEFAULTS, agentsDir, isCardTrigger, readAgentsConfig, workspaceOf, workspacesInUse } from "./config.js";
|
|
6
|
+
import { STOPPED_REASON, isPaused, lastSeen, listRuns, ours, clearLock, liveCurrent, liveRuns, readCurrent, readWindow, setPaused, usageText, workspaceLocks, writeRun } from "./state.js";
|
|
7
|
+
import { windowHold, windowLine } from "./usage.js";
|
|
7
8
|
import { runnerService } from "./service.js";
|
|
8
9
|
import { TRACKERS, trackersFor } from "./tracker.js";
|
|
9
10
|
import { parseFrom, resolveName, stepNames } from "./names.js";
|
|
10
11
|
import { isStatusLabel } from "./workflow.js";
|
|
11
|
-
import { cancelCard, dispatch, tick } from "./runner.js";
|
|
12
|
+
import { cancelCard, dispatch, gateReason, tick } from "./runner.js";
|
|
13
|
+
import { notifyRun, notifyRunner, sendSummary } from "./notify.js";
|
|
12
14
|
const HOUR = 3_600_000;
|
|
13
15
|
const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
14
16
|
const CARD_ID = /^[A-Za-z][A-Za-z0-9]*-\d+$/;
|
|
@@ -80,13 +82,15 @@ export function countRounds(steps) {
|
|
|
80
82
|
}
|
|
81
83
|
// A run that says 'running' but is none of the live runs was cut off.
|
|
82
84
|
export const isInterrupted = (e, live) => e.record?.outcome === 'running' && !live.some((l) => (l.runDir ? l.runDir === e.dir : l.card === e.card));
|
|
85
|
+
// The record's outcome, except a run cut off without a live process, which failed.
|
|
86
|
+
export const shownOutcome = (e, live) => (isInterrupted(e, live) ? 'failed' : e.record.outcome);
|
|
83
87
|
export function formatRunLine(e, now = Date.now(), live = []) {
|
|
84
88
|
const r = e.record;
|
|
85
89
|
if (!r)
|
|
86
90
|
return `${e.card} ${when(stampDate(e.stamp))} older run, logs only (${plural(e.logs.length, 'log')})`;
|
|
87
91
|
const rounds = countRounds(r.steps);
|
|
88
|
-
const end = r.endedAt ? Date.parse(r.endedAt) : now;
|
|
89
|
-
const parts = [e.card, when(new Date(r.startedAt)), r.workflow, r.steps.map((s) => s.label).join(' → '),
|
|
92
|
+
const end = r.endedAt ? Date.parse(r.endedAt) : isInterrupted(e, live) ? Date.parse(lastSeen(r)) : now;
|
|
93
|
+
const parts = [e.card, when(new Date(r.startedAt)), r.workflow, r.steps.map((s) => s.label).join(' → '), shownOutcome(e, live)];
|
|
90
94
|
if (rounds)
|
|
91
95
|
parts.push(plural(rounds, 'round'));
|
|
92
96
|
parts.push(formatDuration(end - Date.parse(r.startedAt)));
|
|
@@ -121,16 +125,16 @@ const readLog = (configDir) => {
|
|
|
121
125
|
}
|
|
122
126
|
};
|
|
123
127
|
const USAGE = {
|
|
124
|
-
status: 'status [--json] Runner, the card each workspace is on, parked cards and workflows',
|
|
128
|
+
status: 'status [--json] Runner, the card each workspace is on, failed and parked cards, scheduled jobs and workflows',
|
|
125
129
|
start: 'start Start the background runner',
|
|
126
130
|
stop: 'stop Stop the runner and cancel the cards it is working on',
|
|
127
131
|
restart: 'restart Stop and start the runner',
|
|
128
132
|
pause: 'pause Pick up no new cards; a card in progress carries on',
|
|
129
133
|
resume: 'resume Pick up new cards again',
|
|
130
|
-
run: 'run [CARD] [--workflow W] [--from STEP] Run now instead of waiting for the next minute',
|
|
134
|
+
run: 'run [CARD] [--workflow W] [--from STEP] Run now instead of waiting for the next minute; a named card skips the pause, the cap and the window hold',
|
|
131
135
|
cancel: 'cancel [CARD] Stop a card being worked on and clear its labels',
|
|
132
136
|
retry: 'retry CARD [--workflow W] Queue a parked or cancelled card again',
|
|
133
|
-
runs: 'runs [CARD] [--limit N] [--json] Past runs, newest first',
|
|
137
|
+
runs: 'runs [CARD] [--limit N] [--jobs] [--json] Past runs, newest first; --jobs lists scheduled runs only',
|
|
134
138
|
show: 'show CARD [--json] Steps, outcome and labels of the latest run',
|
|
135
139
|
logs: 'logs [CARD] [--step N] [-f] [--lines N] Runner log, or a card\'s step logs',
|
|
136
140
|
clean: 'clean [--older 14d] [--yes] Remove old worktrees and run folders',
|
|
@@ -246,6 +250,13 @@ export async function trackerWarnings(kinds, trackers) {
|
|
|
246
250
|
}
|
|
247
251
|
return out;
|
|
248
252
|
}
|
|
253
|
+
// Pauses or resumes the runner, and pings Discord only when that changed something.
|
|
254
|
+
export async function changePaused(ctx, paused, reason, fetcher = fetch, platform = process.platform) {
|
|
255
|
+
const was = isPaused(ctx.configDir);
|
|
256
|
+
setPaused(ctx.configDir, paused);
|
|
257
|
+
if (was !== paused)
|
|
258
|
+
await notifyRunner(ctx.configDir, paused, reason, ctx.log, fetcher, platform);
|
|
259
|
+
}
|
|
249
260
|
async function startCmd(ctx) {
|
|
250
261
|
if (!Object.keys(readAgentsConfig(ctx.configDir).workflows).length) {
|
|
251
262
|
ctx.log('No workflows yet. Create one first: vsf agents workflow add.');
|
|
@@ -254,7 +265,7 @@ async function startCmd(ctx) {
|
|
|
254
265
|
for (const w of await trackerWarnings(trackersInUse(readAgentsConfig(ctx.configDir)), trackersFor(ctx.configDir))) {
|
|
255
266
|
ctx.log(`Warning: ${w.message.replace(/\.?$/, '.')} Workflows on ${TRACKERS[w.kind].name} are skipped until then.`);
|
|
256
267
|
}
|
|
257
|
-
|
|
268
|
+
await changePaused(ctx, false, 'by vsf agents start');
|
|
258
269
|
if (!runnerService().start(ctx.home, program(ctx), logPath(ctx.configDir))) {
|
|
259
270
|
ctx.log('Could not start the runner. Check vsf agents logs.');
|
|
260
271
|
return 1;
|
|
@@ -286,6 +297,43 @@ export function workspaceLines(workspaces, running) {
|
|
|
286
297
|
const names = [...new Set([...workspaces, ...running.map((r) => workspaceOf(r))])];
|
|
287
298
|
return names.map((ws) => ` ${ws}:${workingLine(running.find((r) => workspaceOf(r) === ws) ?? null) || ' idle.'}`);
|
|
288
299
|
}
|
|
300
|
+
// One entry per cron trigger: its newest run, last success and today's counts.
|
|
301
|
+
export function jobSummaries(config, all, live, now = Date.now()) {
|
|
302
|
+
const day = new Date(now).toDateString();
|
|
303
|
+
const jobs = [];
|
|
304
|
+
for (const [workflow, w] of Object.entries(config.workflows)) {
|
|
305
|
+
for (const t of w.triggers) {
|
|
306
|
+
if (t.source !== 'cron')
|
|
307
|
+
continue;
|
|
308
|
+
const mine = all.filter((e) => e.record?.source === 'cron' && e.record.workflow === workflow && e.record.trigger === t.schedule);
|
|
309
|
+
const newest = mine[0];
|
|
310
|
+
const r = newest?.record;
|
|
311
|
+
const ok = mine.find((e) => e.record.outcome === 'done' || e.record.outcome === 'verified');
|
|
312
|
+
const todays = mine.filter((e) => new Date(e.record.startedAt).toDateString() === day);
|
|
313
|
+
jobs.push({
|
|
314
|
+
workflow,
|
|
315
|
+
schedule: t.schedule,
|
|
316
|
+
task: t.task,
|
|
317
|
+
last: r ? { at: r.endedAt ?? r.startedAt, outcome: shownOutcome(newest, live), reason: r.reason, summary: r.steps.at(-1)?.summary } : null,
|
|
318
|
+
lastOkAt: ok?.record.startedAt ?? null,
|
|
319
|
+
today: {
|
|
320
|
+
runs: todays.length,
|
|
321
|
+
failed: todays.filter((e) => ['failed', 'parked'].includes(shownOutcome(e, live))).length,
|
|
322
|
+
handed: todays.reduce((n, e) => n + e.record.steps.reduce((m, s) => m + (s.handoffs?.length ?? 0), 0), 0),
|
|
323
|
+
},
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return jobs;
|
|
328
|
+
}
|
|
329
|
+
// The status line for one job.
|
|
330
|
+
function jobLine(j) {
|
|
331
|
+
const bad = j.last?.outcome === 'failed' || j.last?.outcome === 'parked';
|
|
332
|
+
const head = bad
|
|
333
|
+
? `last failed ${clock(new Date(j.last.at))}${j.last.reason ? ` (${j.last.reason.replace(/\.$/, '')})` : ''}`
|
|
334
|
+
: `last ok ${j.lastOkAt ? clock(new Date(j.lastOkAt)) : 'never'}`;
|
|
335
|
+
return `Job ${j.workflow} (cron ${j.schedule}): ${head}, ${plural(j.today.runs, 'run')} today, ${j.today.failed} failed, ${j.today.handed} handed.`;
|
|
336
|
+
}
|
|
289
337
|
const status = guard(async (args, ctx) => {
|
|
290
338
|
const p = parse('status', args, { json: { type: 'boolean' } }, ctx);
|
|
291
339
|
if (typeof p === 'number')
|
|
@@ -301,20 +349,29 @@ const status = guard(async (args, ctx) => {
|
|
|
301
349
|
const current = running.length ? { ...running[0], checking: false } : dispatcher ? { ...dispatcher, checking: true, elapsedMinutes: elapsed(dispatcher) } : null;
|
|
302
350
|
const seen = new Set();
|
|
303
351
|
const parked = [];
|
|
352
|
+
const failed = [];
|
|
304
353
|
const interrupted = [];
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
if (seen.has(e.card))
|
|
354
|
+
const all = listRuns(ctx.configDir);
|
|
355
|
+
for (const e of all) {
|
|
356
|
+
if (e.record?.source === 'cron' || seen.has(e.card))
|
|
309
357
|
continue;
|
|
310
358
|
seen.add(e.card);
|
|
311
359
|
if (e.record?.outcome === 'parked')
|
|
312
360
|
parked.push({ card: e.card, reason: e.record.reason });
|
|
361
|
+
else if (e.record?.outcome === 'failed')
|
|
362
|
+
failed.push({ card: e.card, reason: e.record.reason });
|
|
363
|
+
else if (isInterrupted(e, live))
|
|
364
|
+
failed.push({ card: e.card, reason: e.record?.reason ?? STOPPED_REASON });
|
|
313
365
|
}
|
|
366
|
+
const jobs = jobSummaries(config, all, live);
|
|
314
367
|
const workflows = Object.keys(config.workflows).length;
|
|
315
368
|
const projects = Object.keys(config.projects).length;
|
|
369
|
+
const pauseAt = config.pauseAt ?? DEFAULTS.pauseAt;
|
|
370
|
+
const maxRuns = config.maxRuns ?? 0;
|
|
371
|
+
const w = readWindow(ctx.configDir);
|
|
316
372
|
if (p.values.json) {
|
|
317
|
-
|
|
373
|
+
const window = w && { ...w, heldUntil: windowHold(w, pauseAt) };
|
|
374
|
+
ctx.log(JSON.stringify({ engine: config.engine, runner, lastTick, current, running, parked, failed, interrupted, jobs, workflows, projects, window, maxRuns }, null, 2));
|
|
318
375
|
return 0;
|
|
319
376
|
}
|
|
320
377
|
// One workspace keeps the single line; several get a line each.
|
|
@@ -330,12 +387,19 @@ const status = guard(async (args, ctx) => {
|
|
|
330
387
|
if (perWorkspace)
|
|
331
388
|
for (const line of workspaceLines(workspaces, running))
|
|
332
389
|
ctx.log(line);
|
|
390
|
+
const windowText = windowLine(w, pauseAt);
|
|
391
|
+
if (windowText)
|
|
392
|
+
ctx.log(windowText);
|
|
393
|
+
if (maxRuns)
|
|
394
|
+
ctx.log(`Cap: at most ${plural(maxRuns, 'run')} at once.`);
|
|
333
395
|
if (lastTick)
|
|
334
396
|
ctx.log(`Last check: ${clock(new Date(lastTick.at))}, ${lastTick.line.replace(/\.$/, '') || 'no details'}.`);
|
|
335
|
-
for (const
|
|
336
|
-
ctx.log(`
|
|
397
|
+
for (const k of failed)
|
|
398
|
+
ctx.log(`Failed: ${k.card}${k.reason ? ` (${k.reason.replace(/\.$/, '')})` : ''}. vsf agents retry ${k.card} after fixing the cause.`);
|
|
337
399
|
for (const k of parked)
|
|
338
400
|
ctx.log(`Parked: ${k.card}${k.reason ? ` (${k.reason.replace(/\.$/, '')})` : ''}. vsf agents retry ${k.card} after fixing the cause.`);
|
|
401
|
+
for (const j of jobs)
|
|
402
|
+
ctx.log(jobLine(j));
|
|
339
403
|
ctx.log(`${plural(workflows, 'workflow')}, ${plural(projects, 'project')}. vsf agents workflows lists them.`);
|
|
340
404
|
return 0;
|
|
341
405
|
});
|
|
@@ -366,7 +430,7 @@ const pause = guard(async (args, ctx) => {
|
|
|
366
430
|
const p = parse('pause', args, {}, ctx);
|
|
367
431
|
if (typeof p === 'number')
|
|
368
432
|
return p;
|
|
369
|
-
|
|
433
|
+
await changePaused(ctx, true, 'by vsf agents pause');
|
|
370
434
|
ctx.log('Paused. The runner picks up no new cards; a card in progress carries on. vsf agents resume to continue.');
|
|
371
435
|
return 0;
|
|
372
436
|
});
|
|
@@ -374,7 +438,7 @@ const resume = guard(async (args, ctx) => {
|
|
|
374
438
|
const p = parse('resume', args, {}, ctx);
|
|
375
439
|
if (typeof p === 'number')
|
|
376
440
|
return p;
|
|
377
|
-
|
|
441
|
+
await changePaused(ctx, false, 'by vsf agents resume');
|
|
378
442
|
ctx.log('Resumed.');
|
|
379
443
|
return 0;
|
|
380
444
|
});
|
|
@@ -411,6 +475,8 @@ const run = guard(async (args, ctx) => {
|
|
|
411
475
|
if (!card && code === 0) {
|
|
412
476
|
if (isPaused(ctx.configDir))
|
|
413
477
|
ctx.log('The runner is paused, so nothing was picked up. vsf agents resume, or name a card.');
|
|
478
|
+
else if (gateReason({ configDir: ctx.configDir, config }))
|
|
479
|
+
ctx.log('Name a card to run it anyway.');
|
|
414
480
|
else
|
|
415
481
|
ctx.log('Nothing to do, or the card was verified.');
|
|
416
482
|
}
|
|
@@ -456,6 +522,11 @@ const retry = guard(async (args, ctx) => {
|
|
|
456
522
|
ctx.log(`${id} is running now. vsf agents cancel ${id} first.`);
|
|
457
523
|
return 1;
|
|
458
524
|
}
|
|
525
|
+
const latest = listRuns(ctx.configDir, id)[0]?.record;
|
|
526
|
+
if (latest?.source === 'cron') {
|
|
527
|
+
ctx.log(`${id} is a scheduled run. vsf agents run --workflow ${latest.workflow} fires it now.`);
|
|
528
|
+
return 1;
|
|
529
|
+
}
|
|
459
530
|
const config = readAgentsConfig(ctx.configDir);
|
|
460
531
|
const names = Object.keys(config.workflows);
|
|
461
532
|
let found;
|
|
@@ -506,7 +577,7 @@ const retry = guard(async (args, ctx) => {
|
|
|
506
577
|
return 0;
|
|
507
578
|
});
|
|
508
579
|
const runs = guard(async (args, ctx) => {
|
|
509
|
-
const p = parse('runs', args, { limit: { type: 'string' }, json: { type: 'boolean' } }, ctx);
|
|
580
|
+
const p = parse('runs', args, { limit: { type: 'string' }, jobs: { type: 'boolean' }, json: { type: 'boolean' } }, ctx);
|
|
510
581
|
if (typeof p === 'number')
|
|
511
582
|
return p;
|
|
512
583
|
const card = p.positionals[0]?.toUpperCase();
|
|
@@ -515,7 +586,8 @@ const runs = guard(async (args, ctx) => {
|
|
|
515
586
|
const limit = p.values.limit === undefined ? 20 : Number(p.values.limit);
|
|
516
587
|
if (!Number.isInteger(limit) || limit < 1)
|
|
517
588
|
return usageError('runs', ctx, '--limit needs a whole number above 0.');
|
|
518
|
-
|
|
589
|
+
// A named card lists all its runs; otherwise card runs, or with --jobs scheduled ones.
|
|
590
|
+
const entries = listRuns(ctx.configDir, card).filter((e) => !!card || (e.record?.source === 'cron') === !!p.values.jobs).slice(0, limit);
|
|
519
591
|
if (p.values.json) {
|
|
520
592
|
ctx.log(JSON.stringify(entries.map((e) => ({ card: e.card, stamp: e.stamp, dir: e.dir, logs: e.logs, record: e.record })), null, 2));
|
|
521
593
|
return 0;
|
|
@@ -556,13 +628,17 @@ const show = guard(async (args, ctx) => {
|
|
|
556
628
|
return 0;
|
|
557
629
|
}
|
|
558
630
|
ctx.log(`${latest.card} ${latest.title}`);
|
|
559
|
-
ctx.log(`Workflow ${latest.workflow}, ${
|
|
631
|
+
ctx.log(`Workflow ${latest.workflow}, ${shownOutcome(all[0], liveRuns(ctx.configDir))}${latest.prUrl ? `, PR ${latest.prUrl}` : ''}`);
|
|
560
632
|
if (labels)
|
|
561
633
|
ctx.log(`Labels: ${labels.join(', ') || 'none'}`);
|
|
562
634
|
for (const s of latest.steps) {
|
|
563
635
|
const took = s.endedAt ? formatDuration(Date.parse(s.endedAt) - Date.parse(s.startedAt)) : 'running';
|
|
564
|
-
|
|
636
|
+
const used = usageText(s.usage);
|
|
637
|
+
ctx.log(` ${s.label.padEnd(12)} ${(s.result ?? 'running').toLowerCase().padEnd(8)} ${took.padEnd(6)} ${used ? `${used} ` : ''}${oneLine(s.summary ?? '')}`.trimEnd());
|
|
565
638
|
}
|
|
639
|
+
const total = usageText(latest.usage);
|
|
640
|
+
if (total || latest.usage?.models?.length)
|
|
641
|
+
ctx.log(`Usage: ${[total, latest.usage?.models?.join(', ')].filter(Boolean).join(', ')}`);
|
|
566
642
|
if (latest.outcome === 'parked' && latest.reason)
|
|
567
643
|
ctx.log(`Parked: ${oneLine(latest.reason, 300)}`);
|
|
568
644
|
if (all.length > 1)
|
|
@@ -720,7 +796,11 @@ const tickCmd = async (args, ctx) => {
|
|
|
720
796
|
try {
|
|
721
797
|
const { values } = parseArgs({ args, options: { card: { type: 'string' }, workflow: { type: 'string' }, schedule: { type: 'string' } }, strict: true });
|
|
722
798
|
const config = readAgentsConfig(ctx.configDir);
|
|
723
|
-
const rc = {
|
|
799
|
+
const rc = {
|
|
800
|
+
configDir: ctx.configDir, config, trackers: trackersFor(ctx.configDir), log: stamped, selfCommand: ctx.selfCommand,
|
|
801
|
+
notify: (e) => notifyRun(ctx.configDir, e, stamped),
|
|
802
|
+
summary: (paused) => sendSummary(ctx.configDir, paused, stamped),
|
|
803
|
+
};
|
|
724
804
|
if (values.card || values.workflow)
|
|
725
805
|
return await tick(rc, values.card?.toUpperCase(), { workflow: values.workflow, schedule: values.schedule, child: true });
|
|
726
806
|
return await dispatch(rc, { reload: () => readAgentsConfig(ctx.configDir) });
|
package/dist/engine.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type ChildProcess } from 'node:child_process';
|
|
2
|
+
import { type Usage } from './state.ts';
|
|
2
3
|
export type Engine = 'claude' | 'codex';
|
|
3
4
|
export interface RunOptions {
|
|
4
5
|
engine: Engine;
|
|
@@ -10,6 +11,7 @@ export interface RunOptions {
|
|
|
10
11
|
timeoutMs?: number;
|
|
11
12
|
vault?: string;
|
|
12
13
|
settingsFile?: string;
|
|
14
|
+
platform?: NodeJS.Platform;
|
|
13
15
|
}
|
|
14
16
|
export declare function engineArgs(o: RunOptions, outFile: string): {
|
|
15
17
|
cmd: string;
|
|
@@ -17,7 +19,27 @@ export declare function engineArgs(o: RunOptions, outFile: string): {
|
|
|
17
19
|
};
|
|
18
20
|
export declare function track(child: ChildProcess): void;
|
|
19
21
|
export declare function stopAgents(): Promise<void>;
|
|
22
|
+
export declare function errorTail(stderr: string): string;
|
|
23
|
+
export interface ClaudeResult {
|
|
24
|
+
text: string;
|
|
25
|
+
isError: boolean;
|
|
26
|
+
usage: Usage;
|
|
27
|
+
}
|
|
28
|
+
export declare function parseClaudeJson(stdout: string): ClaudeResult | null;
|
|
20
29
|
export declare function runAgent(o: RunOptions): Promise<{
|
|
21
30
|
ok: boolean;
|
|
22
31
|
output: string;
|
|
32
|
+
error?: string;
|
|
33
|
+
usage?: Usage;
|
|
34
|
+
}>;
|
|
35
|
+
export declare function runCommand(o: {
|
|
36
|
+
command: string;
|
|
37
|
+
cwd: string;
|
|
38
|
+
logFile: string;
|
|
39
|
+
timeoutMs?: number;
|
|
40
|
+
env?: Record<string, string>;
|
|
41
|
+
}): Promise<{
|
|
42
|
+
ok: boolean;
|
|
43
|
+
output: string;
|
|
44
|
+
error?: string;
|
|
23
45
|
}>;
|