@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/dist/engine.js
CHANGED
|
@@ -2,15 +2,29 @@ import { spawn } from 'node:child_process';
|
|
|
2
2
|
import { createWriteStream, mkdtempSync, readFileSync, rmSync } from 'node:fs';
|
|
3
3
|
import { tmpdir } from 'node:os';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
|
+
import { usageText } from "./state.js";
|
|
5
6
|
// Tools a write step may use under `claude -p`. Anything else is denied,
|
|
6
7
|
// since nobody is there to approve it.
|
|
7
8
|
const CLAUDE_WRITE = ['Read', 'Glob', 'Grep', 'Edit', 'Write', 'Bash'];
|
|
8
9
|
const CLAUDE_READ = ['Read', 'Glob', 'Grep', 'Bash(git diff:*)', 'Bash(git log:*)', 'Bash(git show:*)', 'Bash(npm test:*)', 'Bash(npm run:*)', 'Bash(pnpm:*)', 'Bash(yarn:*)', 'Bash(npx:*)', 'Bash(node:*)', 'Bash(ls:*)', 'Bash(rg:*)'];
|
|
9
10
|
export function engineArgs(o, outFile) {
|
|
11
|
+
// Nobody is there to approve a prompt, and an allowlist that misses a tool
|
|
12
|
+
// breaks the run, so on Linux every step runs with permissions skipped.
|
|
13
|
+
const skip = (o.platform ?? process.platform) === 'linux';
|
|
14
|
+
if (o.engine === 'claude' && skip) {
|
|
15
|
+
const args = ['-p', '--output-format', 'json', '--dangerously-skip-permissions'];
|
|
16
|
+
if (o.vault)
|
|
17
|
+
args.push('--add-dir', o.vault);
|
|
18
|
+
if (o.model)
|
|
19
|
+
args.push('--model', o.model);
|
|
20
|
+
if (o.settingsFile)
|
|
21
|
+
args.push('--settings', o.settingsFile);
|
|
22
|
+
return { cmd: 'claude', args };
|
|
23
|
+
}
|
|
10
24
|
if (o.engine === 'claude') {
|
|
11
25
|
// Edit rules cover every file-writing tool; a Write(path) rule is ignored.
|
|
12
26
|
const vaultTools = o.vault ? [`Edit(/${o.vault}/**)`] : [];
|
|
13
|
-
const args = ['-p', '--output-format', '
|
|
27
|
+
const args = ['-p', '--output-format', 'json', '--allowedTools', [...(o.write ? CLAUDE_WRITE : CLAUDE_READ), ...vaultTools].join(',')];
|
|
14
28
|
if (o.vault)
|
|
15
29
|
args.push('--add-dir', o.vault);
|
|
16
30
|
if (o.write)
|
|
@@ -21,6 +35,13 @@ export function engineArgs(o, outFile) {
|
|
|
21
35
|
args.push('--settings', o.settingsFile);
|
|
22
36
|
return { cmd: 'claude', args };
|
|
23
37
|
}
|
|
38
|
+
if (skip) {
|
|
39
|
+
const args = ['exec', '-C', o.cwd, '--dangerously-bypass-approvals-and-sandbox', '-o', outFile];
|
|
40
|
+
if (o.model)
|
|
41
|
+
args.push('-m', o.model);
|
|
42
|
+
args.push('-');
|
|
43
|
+
return { cmd: 'codex', args };
|
|
44
|
+
}
|
|
24
45
|
const args = ['exec', '-C', o.cwd, '-s', o.write ? 'workspace-write' : 'read-only', '-o', outFile];
|
|
25
46
|
// A read-only sandbox cannot write anywhere, so only write steps get the vault.
|
|
26
47
|
if (o.vault && o.write)
|
|
@@ -49,8 +70,52 @@ export async function stopAgents() {
|
|
|
49
70
|
if (c.exitCode === null && c.signalCode === null)
|
|
50
71
|
c.kill('SIGKILL');
|
|
51
72
|
}
|
|
73
|
+
// Why a failed agent process failed: the last lines it wrote to stderr.
|
|
74
|
+
export function errorTail(stderr) {
|
|
75
|
+
const lines = stderr.split('\n').map((l) => l.trim()).filter(Boolean);
|
|
76
|
+
let out = '';
|
|
77
|
+
for (const line of lines.reverse()) {
|
|
78
|
+
const next = out ? `${line} ${out}` : line;
|
|
79
|
+
if (next.length > 300)
|
|
80
|
+
break;
|
|
81
|
+
out = next;
|
|
82
|
+
}
|
|
83
|
+
return out || lines[0]?.slice(-300) || '';
|
|
84
|
+
}
|
|
85
|
+
// Claude's JSON answer: its final text, and the tokens and cost it spent.
|
|
86
|
+
// Null when the output is not that JSON, so it is taken as plain text.
|
|
87
|
+
export function parseClaudeJson(stdout) {
|
|
88
|
+
let j;
|
|
89
|
+
try {
|
|
90
|
+
j = JSON.parse(stdout);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
if (!j || typeof j !== 'object' || typeof j.result !== 'string')
|
|
96
|
+
return null;
|
|
97
|
+
const num = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : 0);
|
|
98
|
+
const usage = {};
|
|
99
|
+
const models = j.modelUsage && typeof j.modelUsage === 'object' ? Object.entries(j.modelUsage) : [];
|
|
100
|
+
if (models.length) {
|
|
101
|
+
// Every model the step used, side models included.
|
|
102
|
+
usage.input = models.reduce((t, [, m]) => t + num(m?.inputTokens), 0);
|
|
103
|
+
usage.output = models.reduce((t, [, m]) => t + num(m?.outputTokens), 0);
|
|
104
|
+
usage.cached = models.reduce((t, [, m]) => t + num(m?.cacheReadInputTokens) + num(m?.cacheCreationInputTokens), 0);
|
|
105
|
+
usage.model = models.reduce((best, cur) => (num(cur[1]?.costUSD) > num(best[1]?.costUSD) ? cur : best))[0];
|
|
106
|
+
}
|
|
107
|
+
else if (j.usage && typeof j.usage === 'object') {
|
|
108
|
+
usage.input = num(j.usage.input_tokens);
|
|
109
|
+
usage.output = num(j.usage.output_tokens);
|
|
110
|
+
usage.cached = num(j.usage.cache_read_input_tokens) + num(j.usage.cache_creation_input_tokens);
|
|
111
|
+
}
|
|
112
|
+
if (typeof j.total_cost_usd === 'number')
|
|
113
|
+
usage.costUsd = j.total_cost_usd;
|
|
114
|
+
return { text: j.result, isError: j.is_error === true, usage };
|
|
115
|
+
}
|
|
52
116
|
// Runs one agent step with the prompt on stdin. Resolves with the agent's
|
|
53
|
-
// final text;
|
|
117
|
+
// final text; what it prints also goes to the log file, Claude's answer as
|
|
118
|
+
// its text and a usage line. A process that fails also says why in `error`.
|
|
54
119
|
export function runAgent(o) {
|
|
55
120
|
const dir = mkdtempSync(join(tmpdir(), 'voidshift-run-'));
|
|
56
121
|
const outFile = join(dir, 'last.txt');
|
|
@@ -63,24 +128,52 @@ export function runAgent(o) {
|
|
|
63
128
|
const env = { ...process.env, VOIDSHIFT_RECALL: 'off' };
|
|
64
129
|
const child = spawn(cmd, args, { cwd: o.cwd, env, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
65
130
|
track(child);
|
|
131
|
+
// Claude's JSON comes at exit, so it is held and logged as text then.
|
|
66
132
|
let stdout = '';
|
|
67
133
|
child.stdout.on('data', (d) => {
|
|
68
134
|
stdout += d;
|
|
135
|
+
if (o.engine !== 'claude')
|
|
136
|
+
log.write(d);
|
|
137
|
+
});
|
|
138
|
+
// Only the tail is kept; the log has all of it.
|
|
139
|
+
let stderr = '';
|
|
140
|
+
child.stderr.on('data', (d) => {
|
|
141
|
+
stderr = (stderr + d).split('\n').slice(-20).join('\n');
|
|
69
142
|
log.write(d);
|
|
70
143
|
});
|
|
71
|
-
|
|
72
|
-
|
|
144
|
+
const timeoutMs = o.timeoutMs ?? 45 * 60_000;
|
|
145
|
+
let timedOut = false;
|
|
146
|
+
const timer = setTimeout(() => {
|
|
147
|
+
timedOut = true;
|
|
148
|
+
child.kill('SIGTERM');
|
|
149
|
+
}, timeoutMs);
|
|
73
150
|
child.on('error', (err) => {
|
|
74
151
|
clearTimeout(timer);
|
|
75
152
|
children.delete(child);
|
|
76
153
|
log.end(`\n${err.message}\n`);
|
|
77
|
-
resolve({ ok: false, output: err.message });
|
|
154
|
+
resolve({ ok: false, output: err.message, error: err.message });
|
|
78
155
|
});
|
|
79
156
|
child.on('close', (code) => {
|
|
80
157
|
clearTimeout(timer);
|
|
81
158
|
children.delete(child);
|
|
82
|
-
log.end(`\nexit ${code}\n`);
|
|
83
159
|
let output = stdout;
|
|
160
|
+
let usage;
|
|
161
|
+
let failure = '';
|
|
162
|
+
if (o.engine === 'claude') {
|
|
163
|
+
const parsed = parseClaudeJson(stdout);
|
|
164
|
+
if (parsed) {
|
|
165
|
+
output = parsed.text;
|
|
166
|
+
if (Object.keys(parsed.usage).length)
|
|
167
|
+
usage = parsed.usage;
|
|
168
|
+
if (parsed.isError)
|
|
169
|
+
failure = parsed.text;
|
|
170
|
+
log.write(`${output}\n\nusage: ${[usageText(usage), usage?.model].filter(Boolean).join(', ') || 'none reported'}\n`);
|
|
171
|
+
}
|
|
172
|
+
else
|
|
173
|
+
log.write(stdout);
|
|
174
|
+
}
|
|
175
|
+
else if (o.model)
|
|
176
|
+
usage = { model: o.model };
|
|
84
177
|
if (o.engine === 'codex') {
|
|
85
178
|
try {
|
|
86
179
|
output = readFileSync(outFile, 'utf8');
|
|
@@ -90,8 +183,52 @@ export function runAgent(o) {
|
|
|
90
183
|
}
|
|
91
184
|
}
|
|
92
185
|
rmSync(dir, { recursive: true, force: true });
|
|
93
|
-
|
|
186
|
+
const minutes = Math.round(timeoutMs / 60_000);
|
|
187
|
+
const error = code === 0 ? undefined : timedOut ? `timed out after ${minutes} minute${minutes === 1 ? '' : 's'}` : errorTail(stderr) || errorTail(failure) || `exit ${code}`;
|
|
188
|
+
// Settled once the log is written, so the step's log is whole when read.
|
|
189
|
+
log.end(`\nexit ${code}\n`, () => resolve({ ok: code === 0, output, ...(error ? { error } : {}), ...(usage ? { usage } : {}) }));
|
|
94
190
|
});
|
|
95
191
|
child.stdin.end(o.prompt);
|
|
96
192
|
});
|
|
97
193
|
}
|
|
194
|
+
// Runs one command step in a shell in place of an agent. Resolves with what it
|
|
195
|
+
// printed on stdout, which is also logged along with stderr; a command that
|
|
196
|
+
// fails also says why in `error`.
|
|
197
|
+
export function runCommand(o) {
|
|
198
|
+
const log = createWriteStream(o.logFile, { flags: 'a' });
|
|
199
|
+
log.write(`$ ${o.command}\n\n`);
|
|
200
|
+
return new Promise((resolve) => {
|
|
201
|
+
const child = spawn('sh', ['-c', o.command], { cwd: o.cwd, env: { ...process.env, ...o.env }, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
202
|
+
track(child);
|
|
203
|
+
let stdout = '';
|
|
204
|
+
child.stdout.on('data', (d) => {
|
|
205
|
+
stdout += d;
|
|
206
|
+
log.write(d);
|
|
207
|
+
});
|
|
208
|
+
// Only the tail is kept; the log has all of it.
|
|
209
|
+
let stderr = '';
|
|
210
|
+
child.stderr.on('data', (d) => {
|
|
211
|
+
stderr = (stderr + d).split('\n').slice(-20).join('\n');
|
|
212
|
+
log.write(d);
|
|
213
|
+
});
|
|
214
|
+
const timeoutMs = o.timeoutMs ?? 45 * 60_000;
|
|
215
|
+
let timedOut = false;
|
|
216
|
+
const timer = setTimeout(() => {
|
|
217
|
+
timedOut = true;
|
|
218
|
+
child.kill('SIGTERM');
|
|
219
|
+
}, timeoutMs);
|
|
220
|
+
child.on('error', (err) => {
|
|
221
|
+
clearTimeout(timer);
|
|
222
|
+
children.delete(child);
|
|
223
|
+
log.end(`\n${err.message}\n`);
|
|
224
|
+
resolve({ ok: false, output: err.message, error: err.message });
|
|
225
|
+
});
|
|
226
|
+
child.on('close', (code) => {
|
|
227
|
+
clearTimeout(timer);
|
|
228
|
+
children.delete(child);
|
|
229
|
+
const minutes = Math.round(timeoutMs / 60_000);
|
|
230
|
+
const error = code === 0 ? undefined : timedOut ? `timed out after ${minutes} minute${minutes === 1 ? '' : 's'}` : errorTail(stderr) || `exit ${code}`;
|
|
231
|
+
log.end(`\nexit ${code}\n`, () => resolve({ ok: code === 0, output: stdout, ...(error ? { error } : {}) }));
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
}
|
package/dist/git.d.ts
CHANGED
|
@@ -34,3 +34,4 @@ export interface CommitPolicy {
|
|
|
34
34
|
}
|
|
35
35
|
export declare function applyPolicy(message: string, policy: CommitPolicy, signer: string | null, cwd: string): string;
|
|
36
36
|
export declare function enforceCommitPolicy(worktree: string, from: string, policy: CommitPolicy): number;
|
|
37
|
+
export declare function removeWorktree(repo: string, worktree: string, branch: string): void;
|
package/dist/git.js
CHANGED
|
@@ -194,3 +194,8 @@ export function enforceCommitPolicy(worktree, from, policy) {
|
|
|
194
194
|
must(sh('git', ['reset', '-q', '--soft', head], worktree), 'git reset');
|
|
195
195
|
return changed;
|
|
196
196
|
}
|
|
197
|
+
// Removes a worktree and its branch. Errors are ignored: either may be gone already.
|
|
198
|
+
export function removeWorktree(repo, worktree, branch) {
|
|
199
|
+
sh('git', ['worktree', 'remove', '--force', worktree], repo);
|
|
200
|
+
sh('git', ['branch', '-D', branch], repo);
|
|
201
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,7 @@ import { controlChecks, controlCommands, controlUsage } from "./control.js";
|
|
|
9
9
|
import { keyStore, looksLikeLinearKey, readLinearKey, readSecret, storeLinearKey, storeSecret } from "./secrets.js";
|
|
10
10
|
import { TRACKERS } from "./tracker.js";
|
|
11
11
|
import { createVoidTrack } from "./voidtrack.js";
|
|
12
|
+
import { addWebhook, dropWebhook, looksLikeWebhook, moveWebhook, removeWebhook, testWebhook, webhookFor } from "./notify.js";
|
|
12
13
|
import { parseCron } from "./cron.js";
|
|
13
14
|
import { trackersInUse } from "./control.js";
|
|
14
15
|
import { runnerService } from "./service.js";
|
|
@@ -379,6 +380,7 @@ const USAGE = [
|
|
|
379
380
|
['link DIR [--claude | --codex]', 'Link the agents into a config folder'],
|
|
380
381
|
['unlink DIR', 'Remove the agents from a config folder'],
|
|
381
382
|
['key [--voidtrack]', 'Store or replace the Linear or VoidTrack API key (reads stdin without a terminal)'],
|
|
383
|
+
['discord [--workflow W] [--test] [--rm]', 'Store, test or remove the Discord webhook for run pings, global or for one workflow; it is stored only after a test message goes through (reads stdin without a terminal)'],
|
|
382
384
|
['profile [init]', `Your profile: rules (${RULES_FILE}) and Claude settings (${CLAUDE_SETTINGS}) every agent run carries; init creates it`],
|
|
383
385
|
['validate [DIR]', `Check an agent folder (DIR/${AGENT_FILE} and its prompts), or the config and every folder under agents/defs`],
|
|
384
386
|
['help [COMMAND]', 'Show this list, or one command'],
|
|
@@ -662,8 +664,14 @@ const workflowSubs = {
|
|
|
662
664
|
if (!teams)
|
|
663
665
|
return 1;
|
|
664
666
|
const r = await promptWorkflow(ctx.ui, config, teams, { name, workflow: current }, true);
|
|
665
|
-
if (r.name !== name)
|
|
667
|
+
if (r.name !== name) {
|
|
668
|
+
const refused = moveWebhook(ctx.configDir, name, r.name);
|
|
669
|
+
if (refused) {
|
|
670
|
+
ctx.log(refused);
|
|
671
|
+
return 1;
|
|
672
|
+
}
|
|
666
673
|
config.workflows = renameKey(config.workflows, name, r.name);
|
|
674
|
+
}
|
|
667
675
|
config.workflows[r.name] = r.workflow;
|
|
668
676
|
return saveWorkflow(config, r.name, r.workflow, ctx);
|
|
669
677
|
}
|
|
@@ -716,6 +724,11 @@ const workflowSubs = {
|
|
|
716
724
|
ctx.log(`A workflow named ${next} already exists.`);
|
|
717
725
|
return 1;
|
|
718
726
|
}
|
|
727
|
+
const refused = moveWebhook(ctx.configDir, old, next);
|
|
728
|
+
if (refused) {
|
|
729
|
+
ctx.log(refused);
|
|
730
|
+
return 1;
|
|
731
|
+
}
|
|
719
732
|
config.workflows = renameKey(config.workflows, old, next);
|
|
720
733
|
save(ctx, config);
|
|
721
734
|
ctx.log(`Workflow ${old} is now ${next}.`);
|
|
@@ -742,6 +755,7 @@ const workflowSubs = {
|
|
|
742
755
|
}
|
|
743
756
|
delete config.workflows[name];
|
|
744
757
|
save(ctx, config);
|
|
758
|
+
dropWebhook(ctx.configDir, name);
|
|
745
759
|
ctx.log(`Deleted workflow ${name}.`);
|
|
746
760
|
return 0;
|
|
747
761
|
},
|
|
@@ -1220,6 +1234,11 @@ const agents = {
|
|
|
1220
1234
|
if (!(await setupKey(ui, ctx.configDir, 'voidtrack')))
|
|
1221
1235
|
ctx.log('VoidTrack is not connected yet. Run vsf agents key --voidtrack.');
|
|
1222
1236
|
}
|
|
1237
|
+
if (!webhookFor(ctx.configDir) && (await ui.confirm('Send run pings to Discord?', false))) {
|
|
1238
|
+
const url = await ui.secret('Paste the Discord webhook URL', (v) => (looksLikeWebhook(v.trim()) ? undefined : 'Not a Discord webhook URL'));
|
|
1239
|
+
if (!(await addWebhook(ctx.configDir, url.trim(), undefined, ui.info)))
|
|
1240
|
+
ctx.log('Discord is not connected. Run vsf agents discord to retry.');
|
|
1241
|
+
}
|
|
1223
1242
|
if (Object.keys(config.workflows).length && (await ui.confirm('Start the background runner now? Every minute it checks for cards and schedules that start your workflows.'))) {
|
|
1224
1243
|
const ok = runnerService().start(ctx.home, [...ctx.selfCommand, 'agents', 'tick'], join(agentsDir(ctx.configDir), 'runner.log'));
|
|
1225
1244
|
ctx.log(ok ? 'Runner started.' : 'Could not start the runner. Try vsf agents start.');
|
|
@@ -1258,6 +1277,8 @@ const agents = {
|
|
|
1258
1277
|
checks.push({ name, status: 'fail', detail: err.message, fix: `vsf agents key${kind === 'linear' ? '' : ` --${kind}`}` });
|
|
1259
1278
|
}
|
|
1260
1279
|
}
|
|
1280
|
+
if (webhookFor(ctx.configDir))
|
|
1281
|
+
checks.push({ name: 'discord', status: 'ok', detail: 'webhook set' });
|
|
1261
1282
|
const workflows = Object.entries(saved.workflows);
|
|
1262
1283
|
const broken = workflows.filter(([, c]) => !saved.projects[c.project] || !isRepo(saved.projects[c.project].path)).map(([n]) => n);
|
|
1263
1284
|
checks.push(!workflows.length
|
|
@@ -1389,6 +1410,38 @@ const agents = {
|
|
|
1389
1410
|
}
|
|
1390
1411
|
return (await verifyAndStore(ctx.configDir, key, ctx.log, ctx.log, kind)) ? 0 : 1;
|
|
1391
1412
|
},
|
|
1413
|
+
async discord(args, ctx) {
|
|
1414
|
+
const p = begin('discord', args, { workflow: { type: 'string' }, test: { type: 'boolean' }, rm: { type: 'boolean' } }, ctx);
|
|
1415
|
+
if (typeof p === 'number')
|
|
1416
|
+
return p;
|
|
1417
|
+
if (p.positionals.length) {
|
|
1418
|
+
ctx.log('Webhooks are not accepted on the command line. Pipe the URL in instead.');
|
|
1419
|
+
return 2;
|
|
1420
|
+
}
|
|
1421
|
+
if (p.values.test && p.values.rm) {
|
|
1422
|
+
ctx.log('Give --test or --rm, not both.');
|
|
1423
|
+
return 2;
|
|
1424
|
+
}
|
|
1425
|
+
let workflow;
|
|
1426
|
+
if (p.values.workflow) {
|
|
1427
|
+
const name = pickWorkflow(readAgentsConfig(ctx.configDir), p.values.workflow, ctx);
|
|
1428
|
+
if (!name)
|
|
1429
|
+
return 1;
|
|
1430
|
+
workflow = name;
|
|
1431
|
+
}
|
|
1432
|
+
if (p.values.test)
|
|
1433
|
+
return (await testWebhook(ctx.configDir, workflow, ctx.log)) ? 0 : 1;
|
|
1434
|
+
if (p.values.rm)
|
|
1435
|
+
return removeWebhook(ctx.configDir, workflow, ctx.log) ? 0 : 1;
|
|
1436
|
+
const url = interactive()
|
|
1437
|
+
? await ctx.ui.secret('Paste the Discord webhook URL', (v) => (looksLikeWebhook(v.trim()) ? undefined : 'Not a Discord webhook URL'))
|
|
1438
|
+
: readFileSync(0, 'utf8');
|
|
1439
|
+
if (!url.trim()) {
|
|
1440
|
+
ctx.log('No webhook on stdin.');
|
|
1441
|
+
return 2;
|
|
1442
|
+
}
|
|
1443
|
+
return (await addWebhook(ctx.configDir, url.trim(), workflow, ctx.log)) ? 0 : 1;
|
|
1444
|
+
},
|
|
1392
1445
|
async help(args, ctx) {
|
|
1393
1446
|
const want = args.join(' ').trim();
|
|
1394
1447
|
if (!want) {
|
package/dist/manage.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type AgentsConfig, type Step, type Trigger, type Workflow } from './config.ts';
|
|
2
|
-
export declare function validateStep(step: Step, config: Pick<AgentsConfig, 'enabled'
|
|
3
|
-
export declare function validateWorkflow(config: Pick<AgentsConfig, 'enabled' | 'projects'
|
|
2
|
+
export declare function validateStep(step: Step, config: Pick<AgentsConfig, 'enabled'> & Partial<Pick<AgentsConfig, 'workflows'>>, dir?: string): string[];
|
|
3
|
+
export declare function validateWorkflow(config: Pick<AgentsConfig, 'enabled' | 'projects'> & Partial<Pick<AgentsConfig, 'workflows'>>, workflow: Workflow, dir?: string): string[];
|
|
4
4
|
export declare function validateAgentFolder(dir: string, config: Pick<AgentsConfig, 'enabled' | 'projects' | 'workflows' | 'folders'>): string[];
|
|
5
5
|
export declare const TRIGGER_FORMS = "TEAM:LABEL (Linear), voidtrack:TEAM:LABEL, or cron:SCHEDULE:TASK";
|
|
6
6
|
export declare function parseTrigger(input: string): Trigger | null;
|
package/dist/manage.js
CHANGED
|
@@ -29,13 +29,25 @@ export function validateStep(step, config, dir) {
|
|
|
29
29
|
problems.push(`${name} prompt must be a file name.`);
|
|
30
30
|
if (step.skill !== undefined && typeof step.skill !== 'string')
|
|
31
31
|
problems.push(`${name} skill must be a name or a folder.`);
|
|
32
|
-
|
|
32
|
+
if (step.run !== undefined && (typeof step.run !== 'string' || !step.run.trim()))
|
|
33
|
+
problems.push(`${name} run must be a command.`);
|
|
34
|
+
if (step.handoff !== undefined) {
|
|
35
|
+
if (typeof step.handoff !== 'string' || !step.handoff.trim())
|
|
36
|
+
problems.push(`${name} handoff must be a workflow name.`);
|
|
37
|
+
else if (config.workflows && !Object.hasOwn(config.workflows, step.handoff))
|
|
38
|
+
problems.push(`${name} hands cards to ${step.handoff}, which is not a workflow.`);
|
|
39
|
+
}
|
|
40
|
+
const sources = [step.skill, step.prompt, step.run].filter((v) => v !== undefined).length;
|
|
33
41
|
if (step.output === 'publish') {
|
|
34
42
|
if (sources)
|
|
35
|
-
problems.push(`${name} only pushes the branch, so it takes no skill or
|
|
43
|
+
problems.push(`${name} only pushes the branch, so it takes no skill, prompt or run.`);
|
|
36
44
|
}
|
|
37
45
|
else if (sources !== 1)
|
|
38
|
-
problems.push(`${name} needs exactly one of skill or
|
|
46
|
+
problems.push(`${name} needs exactly one of skill, prompt or run.`);
|
|
47
|
+
else if (step.run !== undefined) {
|
|
48
|
+
if (step.output !== 'none')
|
|
49
|
+
problems.push(`${name} runs a command, so its output must be none.`);
|
|
50
|
+
}
|
|
39
51
|
else if (typeof (step.prompt ?? step.skill) === 'string') {
|
|
40
52
|
const file = stepFile(step, dir);
|
|
41
53
|
if (!file || !existsSync(file))
|
|
@@ -121,8 +133,8 @@ export function validateAgentFolder(dir, config) {
|
|
|
121
133
|
catch (err) {
|
|
122
134
|
return [`${AGENT_FILE}: ${err.message}`];
|
|
123
135
|
}
|
|
124
|
-
const problems = validateWorkflow(config, workflow, dir);
|
|
125
136
|
const name = basename(dir);
|
|
137
|
+
const problems = validateWorkflow({ ...config, workflows: { ...config.workflows, [name]: workflow } }, workflow, dir);
|
|
126
138
|
const from = config.folders?.[name];
|
|
127
139
|
if (config.workflows[name] && (!from || resolve(from) !== resolve(dir)))
|
|
128
140
|
problems.push(`config.json already has a workflow named ${name}, and it wins. Rename the folder.`);
|
|
@@ -228,6 +240,10 @@ export function validateConfig(value, saved) {
|
|
|
228
240
|
problems.push('reviewState must be a non-empty string.');
|
|
229
241
|
if (!isInt(value.timeoutMinutes, 1, 480))
|
|
230
242
|
problems.push('timeoutMinutes must be a whole number from 1 to 480.');
|
|
243
|
+
if (value.maxRuns !== undefined && !isInt(value.maxRuns, 0, 32))
|
|
244
|
+
problems.push('maxRuns must be a whole number from 0 to 32, 0 for no cap.');
|
|
245
|
+
if (value.pauseAt !== undefined && !isInt(value.pauseAt, 0, 100))
|
|
246
|
+
problems.push('pauseAt must be a whole number from 0 to 100, 0 to never hold.');
|
|
231
247
|
const projects = isObj(value.projects) ? value.projects : null;
|
|
232
248
|
if (!projects)
|
|
233
249
|
problems.push('projects must be an object.');
|
|
@@ -263,7 +279,7 @@ export function validateConfig(value, saved) {
|
|
|
263
279
|
if (!isObj(value.workflows))
|
|
264
280
|
problems.push('workflows must be an object.');
|
|
265
281
|
else {
|
|
266
|
-
const ctx = { enabled: Array.isArray(value.enabled) ? value.enabled : [], projects: (projects ?? {}) };
|
|
282
|
+
const ctx = { enabled: Array.isArray(value.enabled) ? value.enabled : [], projects: (projects ?? {}), workflows: value.workflows };
|
|
267
283
|
for (const [name, w] of Object.entries(value.workflows)) {
|
|
268
284
|
// Folder workflows are checked with their folder, by vsf agents validate.
|
|
269
285
|
if (isObj(value.folders) && value.folders[name])
|
package/dist/notify.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export declare const looksLikeWebhook: (url: string) => boolean;
|
|
2
|
+
export declare const slug: (name: string) => string;
|
|
3
|
+
export declare const webhookFor: (configDir: string, workflow?: string, platform?: NodeJS.Platform) => string | null;
|
|
4
|
+
export declare function postDiscord(url: string, content: string, fetcher?: typeof fetch, timeoutMs?: number): Promise<string | null>;
|
|
5
|
+
export type RunKind = 'ready' | 'passed' | 'parked' | 'failed';
|
|
6
|
+
export interface RunEvent {
|
|
7
|
+
kind: RunKind;
|
|
8
|
+
card: string;
|
|
9
|
+
title: string;
|
|
10
|
+
workflow: string;
|
|
11
|
+
url?: string;
|
|
12
|
+
prUrl?: string;
|
|
13
|
+
reason?: string;
|
|
14
|
+
}
|
|
15
|
+
export declare function runMessage(e: RunEvent): string;
|
|
16
|
+
export declare function notifyRun(configDir: string, e: RunEvent, log: (line: string) => void, fetcher?: typeof fetch, platform?: NodeJS.Platform): Promise<void>;
|
|
17
|
+
export declare function notifyRunner(configDir: string, paused: boolean, reason: string, log: (line: string) => void, fetcher?: typeof fetch, platform?: NodeJS.Platform): Promise<void>;
|
|
18
|
+
export interface NotifyState {
|
|
19
|
+
summaryDay?: string;
|
|
20
|
+
summaryAt?: string;
|
|
21
|
+
}
|
|
22
|
+
export declare function readNotifyState(configDir: string): NotifyState;
|
|
23
|
+
export declare function writeNotifyState(configDir: string, state: NotifyState): void;
|
|
24
|
+
export declare function summaryMessage(configDir: string, since: Date, paused: boolean): string | null;
|
|
25
|
+
export declare function sendSummary(configDir: string, paused: boolean, log: (line: string) => void, now?: Date, fetcher?: typeof fetch, platform?: NodeJS.Platform): Promise<void>;
|
|
26
|
+
export declare function addWebhook(configDir: string, url: string, workflow: string | undefined, log: (line: string) => void, fetcher?: typeof fetch, platform?: NodeJS.Platform): Promise<boolean>;
|
|
27
|
+
export declare function testWebhook(configDir: string, workflow: string | undefined, log: (line: string) => void, fetcher?: typeof fetch, platform?: NodeJS.Platform): Promise<boolean>;
|
|
28
|
+
export declare function removeWebhook(configDir: string, workflow: string | undefined, log: (line: string) => void, platform?: NodeJS.Platform): boolean;
|
|
29
|
+
export declare function moveWebhook(configDir: string, from: string, to: string, platform?: NodeJS.Platform): string | null;
|
|
30
|
+
export declare function dropWebhook(configDir: string, workflow: string, platform?: NodeJS.Platform): void;
|