@voidlabs.gg/voidshift-agents 0.4.3 → 0.4.5
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 +52 -4
- package/dist/config.d.ts +65 -6
- package/dist/config.js +114 -16
- package/dist/control.d.ts +3 -1
- package/dist/control.js +69 -39
- package/dist/cron.d.ts +9 -0
- package/dist/cron.js +66 -0
- package/dist/engine.d.ts +1 -0
- package/dist/engine.js +2 -0
- package/dist/git.d.ts +6 -0
- package/dist/git.js +60 -0
- package/dist/index.js +151 -35
- package/dist/linear.d.ts +1 -1
- package/dist/linear.js +3 -3
- package/dist/manage.d.ts +5 -2
- package/dist/manage.js +138 -21
- package/dist/names.d.ts +1 -0
- package/dist/names.js +4 -1
- package/dist/profile.d.ts +10 -0
- package/dist/profile.js +70 -0
- package/dist/runner.d.ts +15 -8
- package/dist/runner.js +219 -98
- package/dist/state.d.ts +7 -2
- package/dist/state.js +15 -0
- package/dist/tracker.d.ts +24 -0
- package/dist/tracker.js +36 -0
- package/dist/voidtrack.d.ts +18 -0
- package/dist/voidtrack.js +89 -0
- package/dist/workflow.d.ts +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -50,15 +50,24 @@ Five agents, installed into Claude Code (and Codex if you use it):
|
|
|
50
50
|
| QA | CI, then code review, then a usability check in a headless browser |
|
|
51
51
|
|
|
52
52
|
Use them yourself (`/vs-plan ENG-123` in Claude Code), or let a **workflow**
|
|
53
|
-
run them. A workflow starts when one of your Linear cards gets a
|
|
54
|
-
agents in order in a project (a repo on your
|
|
55
|
-
step that made it when a check fails:
|
|
53
|
+
run them. A workflow starts when one of your Linear or VoidTrack cards gets a
|
|
54
|
+
label, or on a schedule, runs agents in order in a project (a repo on your
|
|
55
|
+
machine), and sends work back to the step that made it when a check fails:
|
|
56
56
|
|
|
57
57
|
```
|
|
58
58
|
when ENG + vs-plan: Dev → QA in tether-relay
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
-
|
|
61
|
+
Triggers, as `--trigger` takes them:
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
ENG:vs-plan # a Linear card in ENG assigned to you gets vs-plan
|
|
65
|
+
voidtrack:ORG:vs-start # the same for VoidTrack (vsf agents key --voidtrack)
|
|
66
|
+
"cron:0 5 * * 1:Update dependencies" # every Monday 05:00, no card, on its own branch
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
A schedule fires once when due (a missed one is made up within a day) and
|
|
70
|
+
`vsf agents run --workflow NAME` fires it now. The runner checks every minute, works in its own copy of the repo so
|
|
62
71
|
your checkout is never touched, and keeps two labels on the card as it goes: the
|
|
63
72
|
stage (`vs-planning`, `vs-plan-review`, `vs-dev`, `vs-qa`, then `vs-qa-verified`
|
|
64
73
|
or `vs-parked`) and the round (`vs-v1`, `vs-v2`). On Linear it posts the plan and
|
|
@@ -77,8 +86,33 @@ vsf agents workflow add|edit|show|rename|move|rm
|
|
|
77
86
|
vsf agents projects | project add|edit|show|rename|rm
|
|
78
87
|
vsf agents homes | link DIR # where the agents are installed
|
|
79
88
|
vsf agents clean # old worktrees and run logs
|
|
89
|
+
vsf agents validate [DIR] # check the config and agent folders
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
A workflow can also be a folder you write by hand,
|
|
93
|
+
`~/.config/voidshift/agents/defs/NAME/agent.json`, with its prompt files next to
|
|
94
|
+
it. Steps are the built-in ids above or your own:
|
|
95
|
+
|
|
96
|
+
```json
|
|
97
|
+
{
|
|
98
|
+
"triggers": [{ "source": "cron", "schedule": "0 7 * * 1-5", "task": "Morning brief" }],
|
|
99
|
+
"steps": [{ "id": "brief", "prompt": "brief.md", "output": "none", "model": "haiku", "timeoutMinutes": 10 }],
|
|
100
|
+
"maxRounds": 1,
|
|
101
|
+
"project": "tether-relay",
|
|
102
|
+
"writeBack": { "labels": true, "state": null, "comment": true }
|
|
103
|
+
}
|
|
80
104
|
```
|
|
81
105
|
|
|
106
|
+
A step's `output` is `plan`, `verdict`, `commits`, `publish` or `none`; a failed
|
|
107
|
+
`verdict` sends the work back to the nearest `plan` or `commits` step. `engine`,
|
|
108
|
+
`model` and `timeoutMinutes` fall back to the config, and `write` to true only
|
|
109
|
+
for `commits`. A `publish` step takes no prompt; the others take one `prompt`
|
|
110
|
+
file or `skill`, which must ask for a `RESULT:` line (`plan`, `commits`) or a
|
|
111
|
+
`VERDICT:` line (`verdict`). `writeBack` applies to card runs: `state` moves a
|
|
112
|
+
verified card there (`null` never moves it), `labels: false` only takes the
|
|
113
|
+
trigger label off, and `comment: false` skips the closing comment. Edit the file,
|
|
114
|
+
then `vsf agents validate`.
|
|
115
|
+
|
|
82
116
|
### memory
|
|
83
117
|
|
|
84
118
|
A folder of markdown notes, one topic per note, with an `INDEX.md` holding one
|
|
@@ -99,6 +133,20 @@ vsf memory on | off # turn recall on or off
|
|
|
99
133
|
Setup creates a vault at `~/memory` or uses one you already have. Codex asks you
|
|
100
134
|
to trust the recall hook once: run `/hooks` in Codex.
|
|
101
135
|
|
|
136
|
+
### Your profile
|
|
137
|
+
|
|
138
|
+
Every agent run carries your profile, on any machine, without touching your
|
|
139
|
+
Claude or Codex folder. `vsf agents profile init` creates
|
|
140
|
+
`~/.config/voidshift/agents/profile/`:
|
|
141
|
+
|
|
142
|
+
- `RULES.md` is added to every step's prompt (style, commit rules, tone).
|
|
143
|
+
- `claude-settings.json` is passed to Claude runs with `--settings`, so hooks
|
|
144
|
+
come along. `{{profile}}` stands for the folder, so hook scripts can live in it.
|
|
145
|
+
|
|
146
|
+
Setup also asks what the runner does to agent commits before it pushes them:
|
|
147
|
+
remove AI co-author lines, and add `Signed-off-by` for repos that ask for a
|
|
148
|
+
DCO. Only the new commits of that step are rewritten, never pushed ones.
|
|
149
|
+
|
|
102
150
|
## What to know
|
|
103
151
|
|
|
104
152
|
- **Your own logins.** Agents run the official `claude` and `codex` CLIs you
|
package/dist/config.d.ts
CHANGED
|
@@ -1,32 +1,69 @@
|
|
|
1
1
|
import type { Engine } from './engine.ts';
|
|
2
2
|
export type StepId = 'vs-plan' | 'vs-plan-review' | 'vs-publish' | 'vs-dev' | 'vs-qa';
|
|
3
|
-
export type Output = 'plan' | 'verdict' | 'commits' | 'publish';
|
|
3
|
+
export type Output = 'plan' | 'verdict' | 'commits' | 'publish' | 'none';
|
|
4
|
+
export declare const OUTPUTS: Output[];
|
|
4
5
|
export declare const STEPS: Record<StepId, {
|
|
5
6
|
label: string;
|
|
6
7
|
output: Output;
|
|
7
8
|
hint: string;
|
|
9
|
+
write: boolean;
|
|
10
|
+
role: 'dev' | 'review';
|
|
8
11
|
}>;
|
|
9
|
-
export
|
|
12
|
+
export interface StepDef {
|
|
13
|
+
id: string;
|
|
14
|
+
label?: string;
|
|
15
|
+
skill?: string;
|
|
16
|
+
prompt?: string;
|
|
17
|
+
output: Output;
|
|
18
|
+
engine?: Engine;
|
|
19
|
+
model?: string;
|
|
20
|
+
timeoutMinutes?: number;
|
|
21
|
+
write?: boolean;
|
|
22
|
+
}
|
|
23
|
+
export type Step = StepId | StepDef;
|
|
24
|
+
export declare const STEP_ID: RegExp;
|
|
25
|
+
export declare const isBuiltin: (id: string) => id is StepId;
|
|
26
|
+
export declare const outputOf: (s: Step) => Output;
|
|
27
|
+
export declare const stepId: (s: Step) => string;
|
|
28
|
+
export declare const stepLabel: (s: Step) => string;
|
|
29
|
+
export declare const stepIdLabel: (id: string) => string;
|
|
30
|
+
export declare const isProducer: (s: Step) => boolean;
|
|
10
31
|
export declare const AGENTS: Record<string, {
|
|
11
32
|
label: string;
|
|
12
33
|
hint: string;
|
|
13
34
|
}>;
|
|
14
35
|
export declare const visibleAgents: () => string[];
|
|
15
36
|
export declare const stepEnabled: (step: StepId, enabled: string[]) => boolean;
|
|
16
|
-
export
|
|
17
|
-
|
|
37
|
+
export type Tracker = 'linear' | 'voidtrack';
|
|
38
|
+
export interface CardTrigger {
|
|
39
|
+
source: Tracker;
|
|
18
40
|
team: string;
|
|
19
41
|
label: string;
|
|
20
42
|
}
|
|
43
|
+
export interface CronTrigger {
|
|
44
|
+
source: 'cron';
|
|
45
|
+
schedule: string;
|
|
46
|
+
task: string;
|
|
47
|
+
}
|
|
48
|
+
export type Trigger = CardTrigger | CronTrigger;
|
|
49
|
+
export declare const isCardTrigger: (t: Trigger) => t is CardTrigger;
|
|
50
|
+
export declare function triggerSpec(t: Trigger): string;
|
|
51
|
+
export declare function describeTrigger(t: Trigger): string;
|
|
21
52
|
export interface Project {
|
|
22
53
|
path: string;
|
|
23
54
|
base?: string;
|
|
24
55
|
}
|
|
56
|
+
export interface WriteBack {
|
|
57
|
+
labels?: boolean;
|
|
58
|
+
state?: string | null;
|
|
59
|
+
comment?: boolean;
|
|
60
|
+
}
|
|
25
61
|
export interface Workflow {
|
|
26
62
|
triggers: Trigger[];
|
|
27
|
-
steps:
|
|
63
|
+
steps: Step[];
|
|
28
64
|
maxRounds: number;
|
|
29
65
|
project: string;
|
|
66
|
+
writeBack?: WriteBack;
|
|
30
67
|
}
|
|
31
68
|
export interface AgentsConfig {
|
|
32
69
|
engine: Engine;
|
|
@@ -43,6 +80,11 @@ export interface AgentsConfig {
|
|
|
43
80
|
dir: string;
|
|
44
81
|
}[];
|
|
45
82
|
enabled: string[];
|
|
83
|
+
commits?: {
|
|
84
|
+
signOff?: boolean;
|
|
85
|
+
dropAiTrailers?: boolean;
|
|
86
|
+
};
|
|
87
|
+
folders?: Record<string, string>;
|
|
46
88
|
}
|
|
47
89
|
export declare const TEMPLATES: Record<string, {
|
|
48
90
|
steps: StepId[];
|
|
@@ -52,9 +94,26 @@ export declare const DEFAULTS: AgentsConfig;
|
|
|
52
94
|
export declare const agentsDir: (configDir: string) => string;
|
|
53
95
|
export declare const hasWork: (workflow: Pick<Workflow, "steps">) => boolean;
|
|
54
96
|
export declare const hasProducer: (workflow: Pick<Workflow, "steps">) => boolean;
|
|
55
|
-
export declare const describeSteps: (steps:
|
|
97
|
+
export declare const describeSteps: (steps: Step[]) => string;
|
|
56
98
|
export declare const describeTriggers: (triggers: Trigger[]) => string;
|
|
57
99
|
export declare const describeWorkflow: (c: Workflow) => string;
|
|
58
100
|
export declare function migrate(saved: Record<string, any>): Partial<AgentsConfig>;
|
|
101
|
+
export declare const defsDir: (configDir: string) => string;
|
|
102
|
+
export declare const AGENT_FILE = "agent.json";
|
|
103
|
+
export declare function agentFolders(configDir: string): string[];
|
|
104
|
+
export declare function readAgentFolder(dir: string): Workflow;
|
|
59
105
|
export declare function readAgentsConfig(configDir: string): AgentsConfig;
|
|
60
106
|
export declare function writeAgentsConfig(configDir: string, config: AgentsConfig): void;
|
|
107
|
+
export interface ResolvedStep {
|
|
108
|
+
id: string;
|
|
109
|
+
label: string;
|
|
110
|
+
output: Output;
|
|
111
|
+
engine: Engine;
|
|
112
|
+
model?: string;
|
|
113
|
+
timeoutMs: number;
|
|
114
|
+
write: boolean;
|
|
115
|
+
prompt: () => string;
|
|
116
|
+
builtin?: StepId;
|
|
117
|
+
}
|
|
118
|
+
export declare function stepFile(step: StepDef, dir?: string): string | null;
|
|
119
|
+
export declare function resolveStep(step: Step, config: Pick<AgentsConfig, 'engine' | 'models' | 'timeoutMinutes'>, dir?: string): ResolvedStep;
|
package/dist/config.js
CHANGED
|
@@ -1,14 +1,25 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
-
import { basename, join } from 'node:path';
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { basename, join, resolve } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
export const OUTPUTS = ['plan', 'verdict', 'commits', 'publish', 'none'];
|
|
5
|
+
// The built-in steps, the vs-* template. `write` is whether the agent may
|
|
6
|
+
// edit files; `role` picks the model from config.models.
|
|
3
7
|
export const STEPS = {
|
|
4
|
-
'vs-plan': { label: 'Plan', output: 'plan', hint: 'writes a plan and posts it to the card' },
|
|
5
|
-
'vs-plan-review': { label: 'Plan review', output: 'verdict', hint: 'checks the plan; on FIX the workflow goes back to Plan' },
|
|
6
|
-
'vs-publish': { label: 'Publish', output: 'publish', hint: 'creates and pushes the branch for the card' },
|
|
7
|
-
'vs-dev': { label: 'Dev', output: 'commits', hint: 'implements the plan, commits, opens a draft PR' },
|
|
8
|
-
'vs-qa': { label: 'QA', output: 'verdict', hint: 'CI, then code review, then a usability check; on FIX back to Dev' },
|
|
8
|
+
'vs-plan': { label: 'Plan', output: 'plan', hint: 'writes a plan and posts it to the card', write: false, role: 'dev' },
|
|
9
|
+
'vs-plan-review': { label: 'Plan review', output: 'verdict', hint: 'checks the plan; on FIX the workflow goes back to Plan', write: false, role: 'review' },
|
|
10
|
+
'vs-publish': { label: 'Publish', output: 'publish', hint: 'creates and pushes the branch for the card', write: false, role: 'dev' },
|
|
11
|
+
'vs-dev': { label: 'Dev', output: 'commits', hint: 'implements the plan, commits, opens a draft PR', write: true, role: 'dev' },
|
|
12
|
+
'vs-qa': { label: 'QA', output: 'verdict', hint: 'CI, then code review, then a usability check; on FIX back to Dev', write: true, role: 'review' },
|
|
9
13
|
};
|
|
14
|
+
// What a custom step id may look like.
|
|
15
|
+
export const STEP_ID = /^[a-z0-9][a-z0-9-]*$/;
|
|
16
|
+
export const isBuiltin = (id) => Object.hasOwn(STEPS, id);
|
|
17
|
+
export const outputOf = (s) => (typeof s === 'string' ? STEPS[s].output : s.output);
|
|
18
|
+
export const stepId = (s) => (typeof s === 'string' ? s : s.id);
|
|
19
|
+
export const stepLabel = (s) => (typeof s === 'string' ? STEPS[s]?.label ?? s : s.label ?? s.id);
|
|
20
|
+
export const stepIdLabel = (id) => (isBuiltin(id) ? STEPS[id].label : id);
|
|
10
21
|
// Steps a failed verdict can send work back to.
|
|
11
|
-
export const isProducer = (s) =>
|
|
22
|
+
export const isProducer = (s) => outputOf(s) === 'plan' || outputOf(s) === 'commits';
|
|
12
23
|
// The agents in the pack, as setup describes them.
|
|
13
24
|
export const AGENTS = {
|
|
14
25
|
'vs-plan': { label: 'Plan', hint: 'plans a card against the code' },
|
|
@@ -19,6 +30,19 @@ export const AGENTS = {
|
|
|
19
30
|
};
|
|
20
31
|
export const visibleAgents = () => Object.keys(AGENTS);
|
|
21
32
|
export const stepEnabled = (step, enabled) => enabled.includes(step);
|
|
33
|
+
export const isCardTrigger = (t) => t.source !== 'cron';
|
|
34
|
+
// The form `--trigger` takes: "ENG:vs-plan" (Linear), "voidtrack:ORG:vs-start",
|
|
35
|
+
// or "cron:0 5 * * 1:Update the dependencies".
|
|
36
|
+
export function triggerSpec(t) {
|
|
37
|
+
if (t.source === 'cron')
|
|
38
|
+
return `cron:${t.schedule}:${t.task}`;
|
|
39
|
+
return t.source === 'voidtrack' ? `voidtrack:${t.team}:${t.label}` : `${t.team}:${t.label}`;
|
|
40
|
+
}
|
|
41
|
+
export function describeTrigger(t) {
|
|
42
|
+
if (t.source === 'cron')
|
|
43
|
+
return `cron ${t.schedule}`;
|
|
44
|
+
return `${t.source === 'voidtrack' ? 'VoidTrack ' : ''}${t.team} + ${t.label}`;
|
|
45
|
+
}
|
|
22
46
|
// Step lists to start a workflow from.
|
|
23
47
|
export const TEMPLATES = {
|
|
24
48
|
build: { steps: ['vs-dev', 'vs-qa'], maxRounds: 3 },
|
|
@@ -37,10 +61,10 @@ export const DEFAULTS = {
|
|
|
37
61
|
};
|
|
38
62
|
export const agentsDir = (configDir) => join(configDir, 'agents');
|
|
39
63
|
const configPath = (configDir) => join(agentsDir(configDir), 'config.json');
|
|
40
|
-
export const hasWork = (workflow) => workflow.steps.some((s) =>
|
|
64
|
+
export const hasWork = (workflow) => workflow.steps.some((s) => outputOf(s) === 'commits');
|
|
41
65
|
export const hasProducer = (workflow) => workflow.steps.some(isProducer);
|
|
42
|
-
export const describeSteps = (steps) => steps.map(
|
|
43
|
-
export const describeTriggers = (triggers) => triggers.map(
|
|
66
|
+
export const describeSteps = (steps) => steps.map(stepLabel).join(' → ');
|
|
67
|
+
export const describeTriggers = (triggers) => triggers.map(describeTrigger).join(', ');
|
|
44
68
|
export const describeWorkflow = (c) => `when ${describeTriggers(c.triggers)}: ${describeSteps(c.steps)} in ${c.project}`;
|
|
45
69
|
// Step ids that were folded into QA.
|
|
46
70
|
const RENAMED = { 'vs-qa-review': 'vs-qa', ci: null, 'vs-qa-ui': null };
|
|
@@ -85,14 +109,88 @@ export function migrate(saved) {
|
|
|
85
109
|
const enabled = [...new Set((saved.enabled ?? []).map((a) => (a in RENAMED ? RENAMED[a] : a)).filter(Boolean))];
|
|
86
110
|
return { ...rest, ...(models ? { models } : {}), projects, workflows, enabled };
|
|
87
111
|
}
|
|
112
|
+
// Agent folders: defs/<name>/agent.json, with its prompt files next to it.
|
|
113
|
+
export const defsDir = (configDir) => join(agentsDir(configDir), 'defs');
|
|
114
|
+
export const AGENT_FILE = 'agent.json';
|
|
115
|
+
export function agentFolders(configDir) {
|
|
116
|
+
try {
|
|
117
|
+
return readdirSync(defsDir(configDir), { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => join(defsDir(configDir), e.name));
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return [];
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// The workflow in an agent folder. Throws when agent.json is missing or not JSON.
|
|
124
|
+
export function readAgentFolder(dir) {
|
|
125
|
+
const value = JSON.parse(readFileSync(join(dir, AGENT_FILE), 'utf8'));
|
|
126
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
127
|
+
throw new Error(`${AGENT_FILE} must be an object`);
|
|
128
|
+
const { triggers, steps, maxRounds, project, writeBack } = value;
|
|
129
|
+
return { triggers, steps, maxRounds, project, ...(writeBack !== undefined ? { writeBack } : {}) };
|
|
130
|
+
}
|
|
88
131
|
export function readAgentsConfig(configDir) {
|
|
89
132
|
const path = configPath(configDir);
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
133
|
+
let config = structuredClone(DEFAULTS);
|
|
134
|
+
if (existsSync(path)) {
|
|
135
|
+
const saved = migrate(JSON.parse(readFileSync(path, 'utf8')));
|
|
136
|
+
config = { ...config, ...saved, models: { ...DEFAULTS.models, ...saved.models } };
|
|
137
|
+
}
|
|
138
|
+
// A name config.json already uses wins, and a broken folder is skipped:
|
|
139
|
+
// `vsf agents validate` reports both.
|
|
140
|
+
for (const dir of agentFolders(configDir)) {
|
|
141
|
+
const name = basename(dir);
|
|
142
|
+
if (config.workflows[name])
|
|
143
|
+
continue;
|
|
144
|
+
try {
|
|
145
|
+
config.workflows[name] = readAgentFolder(dir);
|
|
146
|
+
config.folders = { ...config.folders, [name]: dir };
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
// skipped
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return config;
|
|
94
153
|
}
|
|
95
154
|
export function writeAgentsConfig(configDir, config) {
|
|
96
155
|
mkdirSync(agentsDir(configDir), { recursive: true });
|
|
97
|
-
|
|
156
|
+
const { folders, ...rest } = config;
|
|
157
|
+
const workflows = Object.fromEntries(Object.entries(config.workflows).filter(([name]) => !folders?.[name]));
|
|
158
|
+
writeFileSync(configPath(configDir), JSON.stringify({ ...rest, workflows }, null, 2) + '\n');
|
|
159
|
+
}
|
|
160
|
+
const packDir = fileURLToPath(new URL('../pack/', import.meta.url));
|
|
161
|
+
const packSkill = (name) => join(packDir, name, 'SKILL.md');
|
|
162
|
+
// The file a custom step's prompt comes from: its prompt file, a pack skill,
|
|
163
|
+
// or a skill folder next to agent.json. Null for a step with neither.
|
|
164
|
+
export function stepFile(step, dir = '.') {
|
|
165
|
+
if (step.prompt)
|
|
166
|
+
return resolve(dir, step.prompt);
|
|
167
|
+
if (!step.skill)
|
|
168
|
+
return null;
|
|
169
|
+
if (STEP_ID.test(step.skill) && existsSync(packSkill(step.skill)))
|
|
170
|
+
return packSkill(step.skill);
|
|
171
|
+
return resolve(dir, step.skill, 'SKILL.md');
|
|
172
|
+
}
|
|
173
|
+
export function resolveStep(step, config, dir) {
|
|
174
|
+
if (typeof step === 'string') {
|
|
175
|
+
const s = STEPS[step];
|
|
176
|
+
return {
|
|
177
|
+
id: step, label: s.label, output: s.output, engine: config.engine, model: config.models[s.role],
|
|
178
|
+
timeoutMs: config.timeoutMinutes * 60_000, write: s.write, prompt: () => readFileSync(packSkill(step), 'utf8'), builtin: step,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
id: step.id,
|
|
183
|
+
label: step.label ?? step.id,
|
|
184
|
+
output: step.output,
|
|
185
|
+
engine: step.engine ?? config.engine,
|
|
186
|
+
model: step.model ?? config.models[step.output === 'verdict' ? 'review' : 'dev'],
|
|
187
|
+
timeoutMs: (step.timeoutMinutes ?? config.timeoutMinutes) * 60_000,
|
|
188
|
+
write: step.write ?? step.output === 'commits',
|
|
189
|
+
prompt: () => {
|
|
190
|
+
const file = stepFile(step, dir);
|
|
191
|
+
if (!file)
|
|
192
|
+
throw new Error(`Step ${step.id} has no prompt or skill.`);
|
|
193
|
+
return readFileSync(file, 'utf8');
|
|
194
|
+
},
|
|
195
|
+
};
|
|
98
196
|
}
|
package/dist/control.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { type AgentsConfig, type Tracker as Kind } from './config.ts';
|
|
1
2
|
import { type Current, type RunEntry } from './state.ts';
|
|
2
3
|
import type { Check, ToolCommand, ToolContext } from './types.ts';
|
|
3
4
|
export { parsePlistArgs } from './launchd.ts';
|
|
5
|
+
export declare const trackersInUse: (config: Pick<AgentsConfig, "workflows">) => Kind[];
|
|
4
6
|
export declare function parseAge(input: string): number | null;
|
|
5
7
|
export declare function formatDuration(ms: number): string;
|
|
6
8
|
export declare function stampDate(stamp: string): Date;
|
|
@@ -18,7 +20,7 @@ export interface CancelDeps {
|
|
|
18
20
|
ours: (pid: number) => boolean;
|
|
19
21
|
kill: (pid: number, signal: 'SIGTERM' | 'SIGKILL') => void;
|
|
20
22
|
sleep: (ms: number) => Promise<void>;
|
|
21
|
-
cancelCard: (card: string, label: string) => Promise<boolean>;
|
|
23
|
+
cancelCard: (card: string, label: string, source: Kind) => Promise<boolean>;
|
|
22
24
|
}
|
|
23
25
|
export declare function cancelCurrent(ctx: ToolContext, c: Current, deps: CancelDeps): Promise<void>;
|
|
24
26
|
export declare const controlCommands: Record<string, ToolCommand>;
|
package/dist/control.js
CHANGED
|
@@ -2,12 +2,11 @@ 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, readAgentsConfig } from "./config.js";
|
|
6
|
-
import { readLinearKey } from "./secrets.js";
|
|
5
|
+
import { agentsDir, isCardTrigger, readAgentsConfig } from "./config.js";
|
|
7
6
|
import { isPaused, listRuns, ours, clearCurrent, liveCurrent, readCurrent, setPaused, writeRun } from "./state.js";
|
|
8
7
|
import { runnerService } from "./service.js";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
8
|
+
import { TRACKERS, trackersFor } from "./tracker.js";
|
|
9
|
+
import { parseFrom, resolveName, stepNames } from "./names.js";
|
|
11
10
|
import { isStatusLabel } from "./workflow.js";
|
|
12
11
|
import { cancelCard, tick } from "./runner.js";
|
|
13
12
|
const HOUR = 3_600_000;
|
|
@@ -15,11 +14,27 @@ const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', '
|
|
|
15
14
|
const CARD_ID = /^[A-Za-z][A-Za-z0-9]*-\d+$/;
|
|
16
15
|
const ISO_LINE = /^(\d{4}-\d\d-\d\dT[\d:.]+Z)\s*(.*)$/;
|
|
17
16
|
export { parsePlistArgs } from "./launchd.js";
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
17
|
+
// Trackers the workflows' card triggers use.
|
|
18
|
+
export const trackersInUse = (config) => [
|
|
19
|
+
...new Set(Object.values(config.workflows).flatMap((w) => w.triggers.filter(isCardTrigger).map((t) => t.source))),
|
|
20
|
+
];
|
|
21
|
+
// A card by id, from the tracker of a workflow that starts on its team.
|
|
22
|
+
async function findCard(config, trackers, id) {
|
|
23
|
+
const team = id.split('-')[0].toUpperCase();
|
|
24
|
+
const sources = [...new Set(Object.values(config.workflows).flatMap((w) => w.triggers.filter(isCardTrigger).filter((t) => t.team === team).map((t) => t.source)))];
|
|
25
|
+
if (!sources.length)
|
|
26
|
+
throw new Error(`No workflow starts on team ${team}.`);
|
|
27
|
+
let last = null;
|
|
28
|
+
for (const source of sources) {
|
|
29
|
+
try {
|
|
30
|
+
const tracker = trackers(source);
|
|
31
|
+
return { card: await tracker.card(id), tracker, source };
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
last = err;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
throw last;
|
|
23
38
|
}
|
|
24
39
|
const logPath = (configDir) => join(agentsDir(configDir), 'runner.log');
|
|
25
40
|
const program = (ctx) => [...ctx.selfCommand, 'agents', 'tick'];
|
|
@@ -179,7 +194,7 @@ export async function cancelCurrent(ctx, c, deps) {
|
|
|
179
194
|
// gone just now
|
|
180
195
|
}
|
|
181
196
|
}
|
|
182
|
-
ctx.log(c.card ? `Cancelled ${c.card}.` : 'Stopped a check
|
|
197
|
+
ctx.log(c.card ? `Cancelled ${c.card}.` : 'Stopped a check.');
|
|
183
198
|
return;
|
|
184
199
|
}
|
|
185
200
|
// Dead or a reused pid: no signal, just tidy up what it left behind.
|
|
@@ -193,33 +208,44 @@ export async function cancelCurrent(ctx, c, deps) {
|
|
|
193
208
|
// the record is only a nicety
|
|
194
209
|
}
|
|
195
210
|
}
|
|
196
|
-
if (c.card) {
|
|
197
|
-
const
|
|
198
|
-
await deps.cancelCard(c.card, label);
|
|
211
|
+
if (c.card && c.source !== 'cron') {
|
|
212
|
+
const first = readAgentsConfig(ctx.configDir).workflows[c.workflow ?? '']?.triggers.find(isCardTrigger);
|
|
213
|
+
await deps.cancelCard(c.card, first?.label ?? "the workflow's label", c.source ?? 'linear');
|
|
199
214
|
ctx.log(`Cleared a stale lock and cancelled ${c.card}.`);
|
|
200
215
|
}
|
|
201
216
|
else
|
|
202
217
|
ctx.log('Cleared a stale lock.');
|
|
203
218
|
clearCurrent(ctx.configDir);
|
|
204
219
|
}
|
|
205
|
-
const realDeps = (ctx
|
|
220
|
+
const realDeps = (ctx) => ({
|
|
206
221
|
ours,
|
|
207
222
|
kill: (pid, signal) => process.kill(pid, signal),
|
|
208
223
|
sleep,
|
|
209
|
-
cancelCard: (card, label) =>
|
|
224
|
+
cancelCard: async (card, label, source) => {
|
|
225
|
+
try {
|
|
226
|
+
return await cancelCard(trackersFor(ctx.configDir)(source), ctx.configDir, card, label, source);
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
},
|
|
210
232
|
});
|
|
211
233
|
async function startCmd(ctx) {
|
|
212
234
|
if (!Object.keys(readAgentsConfig(ctx.configDir).workflows).length) {
|
|
213
235
|
ctx.log('No workflows yet. Create one first: vsf agents workflow add.');
|
|
214
236
|
return 1;
|
|
215
237
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
238
|
+
const trackers = trackersFor(ctx.configDir);
|
|
239
|
+
for (const kind of trackersInUse(readAgentsConfig(ctx.configDir))) {
|
|
240
|
+
try {
|
|
241
|
+
await trackers(kind).viewer();
|
|
242
|
+
}
|
|
243
|
+
catch (err) {
|
|
244
|
+
const msg = err.message;
|
|
245
|
+
const flag = kind === 'linear' ? '' : ` --${kind}`;
|
|
246
|
+
ctx.log(/^No .* API key/.test(msg) ? msg : `${TRACKERS[kind].name} did not accept the key: ${msg}. Run vsf agents key${flag}.`);
|
|
247
|
+
return 1;
|
|
248
|
+
}
|
|
223
249
|
}
|
|
224
250
|
setPaused(ctx.configDir, false);
|
|
225
251
|
if (!runnerService().start(ctx.home, program(ctx), logPath(ctx.configDir))) {
|
|
@@ -233,7 +259,7 @@ async function stopCmd(ctx) {
|
|
|
233
259
|
// Stopping the runner may have stopped the pass; finish or tidy up whatever the lock still names.
|
|
234
260
|
const c = readCurrent(ctx.configDir);
|
|
235
261
|
if (c)
|
|
236
|
-
await cancelCurrent(ctx, c, realDeps(ctx
|
|
262
|
+
await cancelCurrent(ctx, c, realDeps(ctx));
|
|
237
263
|
ctx.log('Runner stopped.');
|
|
238
264
|
return 0;
|
|
239
265
|
}
|
|
@@ -266,7 +292,7 @@ const status = guard(async (args, ctx) => {
|
|
|
266
292
|
return 0;
|
|
267
293
|
}
|
|
268
294
|
const working = current && !current.card
|
|
269
|
-
? ' Checking
|
|
295
|
+
? ' Checking now.'
|
|
270
296
|
: current
|
|
271
297
|
? ` Working on ${current.card ?? 'a card'}${current.title ? ` "${current.title}"` : ''} (${[current.workflow, current.step, current.round !== undefined ? `round ${current.round + 1}` : '', `${current.elapsedMinutes}m`].filter(Boolean).join(', ')}).`
|
|
272
298
|
: '';
|
|
@@ -291,7 +317,7 @@ const start = guard(async (args, ctx) => {
|
|
|
291
317
|
return p;
|
|
292
318
|
const code = await startCmd(ctx);
|
|
293
319
|
if (!code)
|
|
294
|
-
ctx.log('Runner started.
|
|
320
|
+
ctx.log('Runner started. Every minute it checks for cards and schedules that start your workflows.');
|
|
295
321
|
return code;
|
|
296
322
|
});
|
|
297
323
|
const stop = guard(async (args, ctx) => {
|
|
@@ -347,13 +373,13 @@ const run = guard(async (args, ctx) => {
|
|
|
347
373
|
}
|
|
348
374
|
let from;
|
|
349
375
|
if (p.values.from) {
|
|
350
|
-
from =
|
|
376
|
+
from = parseFrom(p.values.from);
|
|
351
377
|
if (!from) {
|
|
352
378
|
ctx.log(`Unknown step ${p.values.from}. Steps: ${stepNames()}.`);
|
|
353
379
|
return 2;
|
|
354
380
|
}
|
|
355
381
|
}
|
|
356
|
-
const code = await tick({ configDir: ctx.configDir, config,
|
|
382
|
+
const code = await tick({ configDir: ctx.configDir, config, trackers: trackersFor(ctx.configDir), log: ctx.log, selfCommand: ctx.selfCommand }, card, { workflow, from });
|
|
357
383
|
if (!card && code === 0) {
|
|
358
384
|
if (isPaused(ctx.configDir))
|
|
359
385
|
ctx.log('The runner is paused, so nothing was picked up. vsf agents resume, or name a card.');
|
|
@@ -378,7 +404,7 @@ const cancel = guard(async (args, ctx) => {
|
|
|
378
404
|
ctx.log(`${c.card ?? 'Another card'} is running, not ${card}.`);
|
|
379
405
|
return 1;
|
|
380
406
|
}
|
|
381
|
-
await cancelCurrent(ctx, c, realDeps(ctx
|
|
407
|
+
await cancelCurrent(ctx, c, realDeps(ctx));
|
|
382
408
|
return 0;
|
|
383
409
|
});
|
|
384
410
|
const retry = guard(async (args, ctx) => {
|
|
@@ -394,9 +420,17 @@ const retry = guard(async (args, ctx) => {
|
|
|
394
420
|
}
|
|
395
421
|
const config = readAgentsConfig(ctx.configDir);
|
|
396
422
|
const names = Object.keys(config.workflows);
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
423
|
+
let found;
|
|
424
|
+
try {
|
|
425
|
+
found = await findCard(config, trackersFor(ctx.configDir), id);
|
|
426
|
+
}
|
|
427
|
+
catch (err) {
|
|
428
|
+
ctx.log(err.message);
|
|
429
|
+
return 1;
|
|
430
|
+
}
|
|
431
|
+
const { card, source } = found;
|
|
432
|
+
const l = found.tracker;
|
|
433
|
+
const startsOn = (n) => config.workflows[n].triggers.some((t) => isCardTrigger(t) && t.source === source && t.team === card.teamKey);
|
|
400
434
|
let chosen;
|
|
401
435
|
if (p.values.workflow) {
|
|
402
436
|
const r = resolveName(names, p.values.workflow, 'workflow');
|
|
@@ -423,7 +457,7 @@ const retry = guard(async (args, ctx) => {
|
|
|
423
457
|
}
|
|
424
458
|
chosen = candidates[0];
|
|
425
459
|
}
|
|
426
|
-
const label = config.workflows[chosen].triggers.find((t) => t.team === card.teamKey).label;
|
|
460
|
+
const label = config.workflows[chosen].triggers.filter(isCardTrigger).find((t) => t.source === source && t.team === card.teamKey).label;
|
|
427
461
|
const next = card.labels.filter((n) => !isStatusLabel(n));
|
|
428
462
|
if (!next.includes(label))
|
|
429
463
|
next.push(label);
|
|
@@ -474,7 +508,7 @@ const show = guard(async (args, ctx) => {
|
|
|
474
508
|
}
|
|
475
509
|
let labels = null;
|
|
476
510
|
try {
|
|
477
|
-
labels = (await
|
|
511
|
+
labels = (await findCard(readAgentsConfig(ctx.configDir), trackersFor(ctx.configDir), id)).card.labels;
|
|
478
512
|
}
|
|
479
513
|
catch {
|
|
480
514
|
// live labels are optional
|
|
@@ -646,11 +680,7 @@ const tickCmd = async (_args, ctx) => {
|
|
|
646
680
|
const stamped = (l) => ctx.log(`${new Date().toISOString()} ${l}`);
|
|
647
681
|
try {
|
|
648
682
|
const config = readAgentsConfig(ctx.configDir);
|
|
649
|
-
|
|
650
|
-
stamped('No Linear API key. Run vsf agents key.');
|
|
651
|
-
return 1;
|
|
652
|
-
}
|
|
653
|
-
return await tick({ configDir: ctx.configDir, config, linear: linear(ctx.configDir), log: stamped, selfCommand: ctx.selfCommand });
|
|
683
|
+
return await tick({ configDir: ctx.configDir, config, trackers: trackersFor(ctx.configDir), log: stamped, selfCommand: ctx.selfCommand });
|
|
654
684
|
}
|
|
655
685
|
catch (err) {
|
|
656
686
|
stamped(`Could not run the check: ${err.message}`);
|
|
@@ -681,7 +711,7 @@ export async function controlChecks(ctx) {
|
|
|
681
711
|
const checks = [];
|
|
682
712
|
const state = runnerState(ctx);
|
|
683
713
|
checks.push(state === 'running'
|
|
684
|
-
? { name: 'agent runner', status: 'ok', detail: '
|
|
714
|
+
? { name: 'agent runner', status: 'ok', detail: 'checking every minute' }
|
|
685
715
|
: state === 'paused'
|
|
686
716
|
? { name: 'agent runner', status: 'warn', detail: 'paused', fix: 'vsf agents resume' }
|
|
687
717
|
: { name: 'agent runner', status: 'warn', detail: 'not running', fix: 'vsf agents start' });
|
package/dist/cron.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface Schedule {
|
|
2
|
+
sets: Set<number>[];
|
|
3
|
+
anyDom: boolean;
|
|
4
|
+
anyDow: boolean;
|
|
5
|
+
}
|
|
6
|
+
export declare function parseCron(schedule: string): Schedule | null;
|
|
7
|
+
export declare function matches(s: Schedule, d: Date): boolean;
|
|
8
|
+
export declare const CATCH_UP_MS: number;
|
|
9
|
+
export declare function lastDue(s: Schedule, since: Date, now: Date): Date | null;
|