@voidlabs.gg/voidshift 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 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 label, runs
54
- agents in order in a project (a repo on your Mac), and sends work back to the
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
- The runner checks Linear every minute, works in its own copy of the repo so
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
@@ -63,8 +63,10 @@ export interface WorkflowItem {
63
63
  name: string;
64
64
  triggers: {
65
65
  source: string;
66
- team: string;
67
- label: string;
66
+ team?: string;
67
+ label?: string;
68
+ schedule?: string;
69
+ task?: string;
68
70
  }[];
69
71
  steps: string[];
70
72
  maxRounds: number;
@@ -8,7 +8,7 @@ import { useBackGuard, useEquivalent, useHints, useKeys, useScreen } from "../co
8
8
  import { theme } from "../theme.js";
9
9
  import { Panel, Select, TextInput } from "../widgets.js";
10
10
  import { tilde } from "../format.js";
11
- import { matchWarnings, stepLabel, stepName, triggerText, warningText, sameTriggerLike } from "./workflows.js";
11
+ import { matchWarnings, stepLabel, stepName, triggerSpec, triggerText, warningText, sameTriggerLike } from "./workflows.js";
12
12
  const h = React.createElement;
13
13
  export function formReducer(s, a) {
14
14
  switch (a.type) {
@@ -38,7 +38,7 @@ export function formReducer(s, a) {
38
38
  }
39
39
  export function initialForm(w, name, projects) {
40
40
  if (w)
41
- return { name, triggers: w.triggers.map((t) => ({ team: t.team, label: t.label })), steps: [...w.steps], project: w.project, rounds: w.maxRounds };
41
+ return { name, triggers: w.triggers.map((t) => ({ ...t })), steps: [...w.steps], project: w.project, rounds: w.maxRounds };
42
42
  return { name: '', triggers: [], steps: [], project: projects.length === 1 ? projects[0] : '', rounds: 3 };
43
43
  }
44
44
  const FALLBACK_PRODUCERS = ['vs-plan', 'vs-dev'];
@@ -95,7 +95,7 @@ export function saveArgs(s, isNew, catalog) {
95
95
  'workflow',
96
96
  isNew ? 'add' : 'edit',
97
97
  s.name.trim(),
98
- ...s.triggers.flatMap((t) => ['--trigger', `${t.team}:${t.label}`]),
98
+ ...s.triggers.flatMap((t) => ['--trigger', triggerSpec(t)]),
99
99
  '--steps',
100
100
  s.steps.map((id) => stepName(catalog, id)).join(','),
101
101
  '--project',
@@ -105,17 +105,19 @@ export function saveArgs(s, isNew, catalog) {
105
105
  ];
106
106
  }
107
107
  export const renameArgs = (from, to) => ['agents', 'workflow', 'rename', from, to];
108
- // A trigger from the two text fields, or a complaint.
108
+ // A trigger from the two text fields, or a complaint. A team written as
109
+ // voidtrack:ORG is a VoidTrack team; schedules are added with the CLI.
109
110
  export function makeTrigger(team, label) {
110
- const t = team.trim().toUpperCase();
111
+ const voidtrack = /^voidtrack:/i.test(team.trim());
112
+ const t = team.trim().replace(/^voidtrack:/i, '').toUpperCase();
111
113
  const l = label.trim();
112
114
  if (!t)
113
- return { error: 'Give the Linear team, like ENG.' };
115
+ return { error: 'Give the team, like ENG, or voidtrack:ORG for VoidTrack.' };
114
116
  if (t.includes(':') || /\s/.test(t))
115
- return { error: 'The team is a short key like ENG.' };
117
+ return { error: 'The team is a short key like ENG, or voidtrack:ORG.' };
116
118
  if (!l)
117
119
  return { error: 'Give the label, like vs-plan.' };
118
- return { trigger: { team: t, label: l } };
120
+ return { trigger: { source: voidtrack ? 'voidtrack' : 'linear', team: t, label: l } };
119
121
  }
120
122
  // Warnings for triggers this workflow shares with another. In place when
121
123
  // editing, last in order when new.
@@ -348,7 +350,7 @@ function WorkflowForm(props) {
348
350
  // Triggers
349
351
  h(Box, null, h(Label, { text: 'Starts when', focused: where === 'triggers' }), h(Box, { flexDirection: 'column', flexGrow: 1 }, form.triggers.length === 0 ? h(Text, { color: theme.dim }, ' no trigger yet') : null, ...form.triggers.map((t, i) => {
350
352
  const on = where === 'triggers' && i === tCursor && adding === null;
351
- return h(Text, { key: `${t.team}:${t.label}`, backgroundColor: on ? theme.selected : undefined, bold: on, color: on ? theme.bold : undefined }, ` ${triggerText(t)} `);
353
+ return h(Text, { key: triggerSpec(t), backgroundColor: on ? theme.selected : undefined, bold: on, color: on ? theme.bold : undefined }, ` ${triggerText(t)} `);
352
354
  }), adding !== null
353
355
  ? h(Box, { flexDirection: 'column' }, h(Box, null, h(Text, { color: theme.dim }, ' team '), h(TextInput, { value: newTeam, onChange: setNewTeam, focus: adding === 'team', placeholder: 'ENG', onSubmit: () => setAdding('label') }), h(Text, { color: theme.dim }, ' label '), h(TextInput, { value: newLabel, onChange: setNewLabel, focus: adding === 'label', placeholder: 'vs-plan', onSubmit: commitTrigger })), addError ? h(Text, { color: theme.red }, ` ${addError}`) : null)
354
356
  : null), h(Box, { flexShrink: 0, marginLeft: 2 }, h(Text, { color: theme.dim }, where === 'triggers' ? 'a add trigger · x remove' : ''))), h(Text, null, ' '),
@@ -4,10 +4,14 @@ import type { Screen } from '../screens.ts';
4
4
  export declare function stepLabel(catalog: StepsCatalog | null, id: string): string;
5
5
  export declare function stepName(catalog: StepsCatalog | null, id: string): string;
6
6
  export interface TriggerLike {
7
- team: string;
8
- label: string;
7
+ source?: string;
8
+ team?: string;
9
+ label?: string;
10
+ schedule?: string;
11
+ task?: string;
9
12
  }
10
13
  export declare const triggerText: (t: TriggerLike) => string;
14
+ export declare const triggerSpec: (t: TriggerLike) => string;
11
15
  export declare const triggersText: (triggers: TriggerLike[]) => string;
12
16
  export declare const sameTriggerLike: (a: TriggerLike, b: TriggerLike) => boolean;
13
17
  export declare const stepsText: (catalog: StepsCatalog | null, steps: string[]) => string;
@@ -28,9 +28,11 @@ export function stepLabel(catalog, id) {
28
28
  export function stepName(catalog, id) {
29
29
  return catalog?.steps.find((s) => s.id === id)?.name ?? id.replace(/^vs-/, '');
30
30
  }
31
- export const triggerText = (t) => `${t.team} + ${t.label}`;
31
+ export const triggerText = (t) => t.source === 'cron' ? `cron ${t.schedule}` : `${t.source === 'voidtrack' ? 'VoidTrack ' : ''}${t.team} + ${t.label}`;
32
+ // The form `--trigger` takes.
33
+ export const triggerSpec = (t) => t.source === 'cron' ? `cron:${t.schedule}:${t.task}` : t.source === 'voidtrack' ? `voidtrack:${t.team}:${t.label}` : `${t.team}:${t.label}`;
32
34
  export const triggersText = (triggers) => triggers.map(triggerText).join(', ');
33
- export const sameTriggerLike = (a, b) => a.team === b.team && a.label === b.label;
35
+ export const sameTriggerLike = (a, b) => triggerSpec(a) === triggerSpec(b);
34
36
  export const stepsText = (catalog, steps) => steps.map((s) => stepLabel(catalog, s)).join(' → ');
35
37
  // Two workflows that start on the same TEAM:LABEL: only the first in order
36
38
  // starts the card. One entry per (loser, winner) pair.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voidlabs.gg/voidshift",
3
- "version": "0.4.3",
3
+ "version": "0.4.5",
4
4
  "description": "One CLI for the Void dev tools: keep your Mac awake, run agents in the background, shared skills and memory for Claude Code and Codex.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {