@voidlabs.gg/voidshift-agents 0.4.5 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -127,9 +127,18 @@ vsf memory status # the vault and where recall is on
127
127
  vsf memory search WORDS # notes recall would bring up
128
128
  vsf memory try "a prompt" # exactly what recall would add to that prompt
129
129
  vsf memory check # notes missing from INDEX.md, broken links
130
+ vsf memory sync # pull and push the vault through its git remote
130
131
  vsf memory on | off # turn recall on or off
131
132
  ```
132
133
 
134
+ To share the vault between machines, make it a git repository with a remote
135
+ named `origin` (any host; a private repository). `vsf memory sync` commits new
136
+ notes, rebases onto the remote and pushes. It also runs in the background from
137
+ the recall hook when the last sync is over ten minutes old, and the agents
138
+ runner syncs before and after every run. When a note changed on both sides,
139
+ the remote's version stays and this machine's copy is saved beside it as
140
+ `note.conflict-<machine>-<time>.md`, so nothing is lost.
141
+
133
142
  Setup creates a vault at `~/memory` or uses one you already have. Codex asks you
134
143
  to trust the recall hook once: run `/hooks` in Codex.
135
144
 
@@ -180,8 +189,12 @@ pnpm test
180
189
  node packages/cli/src/main.ts
181
190
  ```
182
191
 
183
- Node 22.18 or newer runs the TypeScript sources directly. Releases are cut by
184
- pushing a `v*` tag; CI publishes every package at that version.
192
+ Node 22.18 or newer runs the TypeScript sources directly. Each package
193
+ releases on its own: bump its `package.json` and push `agents-v0.5.0` (or
194
+ `cli-v`, `memory-v`, `awake-v`), and CI publishes only that package. A plain
195
+ `v*` tag publishes all four at one version. Tools declare the CLI contract
196
+ they were built for; a CLI that is too old refuses to load them and says to
197
+ run `vsf update`.
185
198
 
186
199
  ## License
187
200
 
package/dist/control.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { type AgentsConfig, type Tracker as Kind } from './config.ts';
2
2
  import { type Current, type RunEntry } from './state.ts';
3
+ import { type Trackers } from './tracker.ts';
3
4
  import type { Check, ToolCommand, ToolContext } from './types.ts';
4
5
  export { parsePlistArgs } from './launchd.ts';
5
6
  export declare const trackersInUse: (config: Pick<AgentsConfig, "workflows">) => Kind[];
@@ -23,5 +24,12 @@ export interface CancelDeps {
23
24
  cancelCard: (card: string, label: string, source: Kind) => Promise<boolean>;
24
25
  }
25
26
  export declare function cancelCurrent(ctx: ToolContext, c: Current, deps: CancelDeps): Promise<void>;
27
+ export declare function trackerWarnings(kinds: Kind[], trackers: Trackers): Promise<{
28
+ kind: Kind;
29
+ message: string;
30
+ }[]>;
31
+ export declare function workingLine(current: (Current & {
32
+ elapsedMinutes: number;
33
+ }) | null): string;
26
34
  export declare const controlCommands: Record<string, ToolCommand>;
27
35
  export declare function controlChecks(ctx: ToolContext): Promise<Check[]>;
package/dist/control.js CHANGED
@@ -230,23 +230,30 @@ const realDeps = (ctx) => ({
230
230
  }
231
231
  },
232
232
  });
233
- async function startCmd(ctx) {
234
- if (!Object.keys(readAgentsConfig(ctx.configDir).workflows).length) {
235
- ctx.log('No workflows yet. Create one first: vsf agents workflow add.');
236
- return 1;
237
- }
238
- const trackers = trackersFor(ctx.configDir);
239
- for (const kind of trackersInUse(readAgentsConfig(ctx.configDir))) {
233
+ // One message per tracker whose key is missing or refused. The tick skips a
234
+ // tracker it cannot open, so these warn rather than stop the runner.
235
+ export async function trackerWarnings(kinds, trackers) {
236
+ const out = [];
237
+ for (const kind of kinds) {
240
238
  try {
241
239
  await trackers(kind).viewer();
242
240
  }
243
241
  catch (err) {
244
242
  const msg = err.message;
245
243
  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;
244
+ out.push({ kind, message: /^No .* API key/.test(msg) ? msg : `${TRACKERS[kind].name} did not accept the key: ${msg}. Run vsf agents key${flag}.` });
248
245
  }
249
246
  }
247
+ return out;
248
+ }
249
+ async function startCmd(ctx) {
250
+ if (!Object.keys(readAgentsConfig(ctx.configDir).workflows).length) {
251
+ ctx.log('No workflows yet. Create one first: vsf agents workflow add.');
252
+ return 1;
253
+ }
254
+ for (const w of await trackerWarnings(trackersInUse(readAgentsConfig(ctx.configDir)), trackersFor(ctx.configDir))) {
255
+ ctx.log(`Warning: ${w.message.replace(/\.?$/, '.')} Workflows on ${TRACKERS[w.kind].name} are skipped until then.`);
256
+ }
250
257
  setPaused(ctx.configDir, false);
251
258
  if (!runnerService().start(ctx.home, program(ctx), logPath(ctx.configDir))) {
252
259
  ctx.log('Could not start the runner. Check vsf agents logs.');
@@ -263,6 +270,14 @@ async function stopCmd(ctx) {
263
270
  ctx.log('Runner stopped.');
264
271
  return 0;
265
272
  }
273
+ // The lock holds the round as the card's vs-vN label shows it; 0 before any round.
274
+ export function workingLine(current) {
275
+ if (!current)
276
+ return '';
277
+ if (!current.card)
278
+ return ' Checking now.';
279
+ return ` Working on ${current.card}${current.title ? ` "${current.title}"` : ''} (${[current.workflow, current.step, current.round ? `round ${current.round}` : '', `${current.elapsedMinutes}m`].filter(Boolean).join(', ')}).`;
280
+ }
266
281
  const status = guard(async (args, ctx) => {
267
282
  const p = parse('status', args, { json: { type: 'boolean' } }, ctx);
268
283
  if (typeof p === 'number')
@@ -291,11 +306,7 @@ const status = guard(async (args, ctx) => {
291
306
  ctx.log(JSON.stringify({ engine: config.engine, runner, lastTick, current, parked, interrupted, workflows, projects }, null, 2));
292
307
  return 0;
293
308
  }
294
- const working = current && !current.card
295
- ? ' Checking now.'
296
- : current
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(', ')}).`
298
- : '';
309
+ const working = workingLine(current);
299
310
  if (runner === 'running')
300
311
  ctx.log(`Runner: running.${working}`);
301
312
  else if (runner === 'paused')
package/dist/index.js CHANGED
@@ -1042,6 +1042,7 @@ const homeCommands = {
1042
1042
  const sameModels = (a, b) => a.dev === b.dev && a.review === b.review;
1043
1043
  const agents = {
1044
1044
  name: 'agents',
1045
+ contract: 1,
1045
1046
  summary: 'Agents for Claude Code and Codex that plan, build and QA Linear cards',
1046
1047
  config: {
1047
1048
  read: (dir) => readAgentsConfig(dir),
package/dist/memory.d.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  export declare function vaultPath(configDir: string): string | undefined;
2
2
  export declare function recall(selfCommand: string[], configDir: string, text: string): string;
3
+ export declare function syncMemory(selfCommand: string[], configDir: string): string;
package/dist/memory.js CHANGED
@@ -35,3 +35,15 @@ export function recall(selfCommand, configDir, text) {
35
35
  return '';
36
36
  }
37
37
  }
38
+ // Brings the vault up to date through its git remote, or shares the notes
39
+ // agents wrote. Never fails a run: returns a line worth logging, or ''.
40
+ export function syncMemory(selfCommand, configDir) {
41
+ if (!vaultPath(configDir) || !selfCommand.length)
42
+ return '';
43
+ const [cmd, ...args] = selfCommand;
44
+ const r = spawnSync(cmd, [...args, 'memory', 'sync', '--quiet'], { encoding: 'utf8', timeout: 90_000 });
45
+ const out = `${r.stdout ?? ''}${r.stderr ?? ''}`.trim();
46
+ if (r.error)
47
+ return `memory sync did not run (${r.error.message})`;
48
+ return r.status === 0 ? out : out || 'memory sync failed';
49
+ }
package/dist/runner.js CHANGED
@@ -1,12 +1,12 @@
1
1
  import { existsSync, mkdirSync } from 'node:fs';
2
2
  import { basename, join } from 'node:path';
3
- import { LABELS, PLAN_HEADING, buildPrompt, findingsFor, fixTarget, isStatusLabel, latestPlan, parseDevResult, parseVerdict, planBody, qaPassed, stageLabels, verdictBlock, } from "./workflow.js";
3
+ import { LABELS, PLAN_HEADING, buildPrompt, findingsFor, fixTarget, isStatusLabel, latestPlan, needsPlan, parseDevResult, parseVerdict, planBody, qaPassed, stageLabels, verdictBlock, } from "./workflow.js";
4
4
  import { agentsDir, hasWork, isCardTrigger, resolveStep, stepId, stepIdLabel } from "./config.js";
5
5
  import { lastDue, parseCron } from "./cron.js";
6
6
  import { runAgent, stopAgents } from "./engine.js";
7
7
  import { commitsAhead, enforceCommitPolicy, defaultBranch, ensureDraftPr, ensureWorktree, headSha, markReady, push, resetWorktree, waitForChecks, waitForPrHead } from "./git.js";
8
8
  import { noTracker } from "./tracker.js";
9
- import { recall, vaultPath } from "./memory.js";
9
+ import { recall, syncMemory, vaultPath } from "./memory.js";
10
10
  import { claudeSettingsFile, readRules, rulesSection } from "./profile.js";
11
11
  import { bootTime, clearCurrent, isPaused, ours, readCurrent, readPending, writeCurrent, writePending, writeRun, readCronState, writeCronState } from "./state.js";
12
12
  const stamp = () => new Date().toISOString().replace(/[:.]/g, '-');
@@ -138,10 +138,7 @@ export async function runCard(card, trigger, workflowName, workflow, project, rc
138
138
  throw new Park(`Workflow ${workflowName} has no ${stepIdLabel(opts.from)} step.`);
139
139
  // A scheduled run has no card to plan on, so its task is the plan unless a Plan step writes one.
140
140
  let plan = isCardTrigger(trigger) ? latestPlan(await linear.comments(card.identifier)) : card.description ?? null;
141
- const firstPlan = steps.findIndex((d, n) => n >= from && d.output === 'plan');
142
- // A check before any commits is a check on the plan, like Plan review.
143
- const needsPlan = steps.findIndex((d, n) => n >= from && (d.output === 'commits' || (d.output === 'verdict' && d.builtin !== 'vs-qa')));
144
- if (!plan && needsPlan >= 0 && (firstPlan < 0 || firstPlan > needsPlan)) {
141
+ if (!plan && needsPlan(steps, from)) {
145
142
  throw new Park('No plan comment (starting with "## Plan") was found. Publish a plan first, or start the workflow with Plan.');
146
143
  }
147
144
  await stage(stageOf(steps[from]));
@@ -156,6 +153,10 @@ export async function runCard(card, trigger, workflowName, workflow, project, rc
156
153
  throw new Park(`Your agent profile's Claude settings are broken: ${err.message}`);
157
154
  }
158
155
  const rules = rulesSection(readRules(rc.configDir));
156
+ // Latest notes from every machine before recall reads them.
157
+ const pulled = syncMemory(rc.selfCommand, rc.configDir);
158
+ if (pulled)
159
+ say(pulled);
159
160
  const known = [recall(rc.selfCommand, rc.configDir, `${workflow.project} ${card.title}\n${card.description ?? ''}`), vault ? memoryNote(vault) : '', rules].filter(Boolean).join('\n\n');
160
161
  const worktree = join(agentsDir(rc.configDir), 'worktrees', card.identifier);
161
162
  ensureWorktree(project.path, worktree, card.branchName, base);
@@ -336,6 +337,10 @@ export async function runCard(card, trigger, workflowName, workflow, project, rc
336
337
  }
337
338
  finally {
338
339
  active = null;
340
+ // Share whatever notes the agents wrote, whatever the outcome.
341
+ const shared = syncMemory(rc.selfCommand, rc.configDir);
342
+ if (shared)
343
+ say(shared);
339
344
  }
340
345
  }
341
346
  // A scheduled run's stand-in for a card: the task, on a branch of its own.
package/dist/types.d.ts CHANGED
@@ -40,6 +40,7 @@ export type ToolCommand = (args: string[], ctx: ToolContext) => Promise<number>;
40
40
  export interface Tool {
41
41
  name: string;
42
42
  summary: string;
43
+ contract?: number;
43
44
  install(ctx: ToolContext): Promise<void>;
44
45
  uninstall(ctx: ToolContext): Promise<void>;
45
46
  checks(ctx: ToolContext): Promise<Check[]>;
@@ -49,4 +49,8 @@ export interface PromptInput {
49
49
  export declare function buildPrompt(p: PromptInput): string;
50
50
  export declare function verdictBlock<T>(steps: T[], i: number, isVerdict: (s: T) => boolean): [number, number];
51
51
  export declare function fixTarget<T>(steps: T[], blockStart: number, isProducer: (s: T) => boolean): number;
52
+ export declare function needsPlan(steps: {
53
+ output: string;
54
+ builtin?: string;
55
+ }[], from: number): boolean;
52
56
  export declare function planBody(output: string): string;
package/dist/workflow.js CHANGED
@@ -105,6 +105,21 @@ export function fixTarget(steps, blockStart, isProducer) {
105
105
  return j;
106
106
  return -1;
107
107
  }
108
+ // Whether a run starting at `from` needs a plan comment: some step before any
109
+ // Plan step writes commits or checks the plan. A check counts as a plan check
110
+ // only when a Plan step comes before it, so a review-only workflow runs bare.
111
+ export function needsPlan(steps, from) {
112
+ for (let n = from; n < steps.length; n++) {
113
+ const s = steps[n];
114
+ if (s.output === 'plan')
115
+ return false;
116
+ if (s.output === 'commits')
117
+ return true;
118
+ if (s.output === 'verdict' && s.builtin !== 'vs-qa' && (s.builtin === 'vs-plan-review' || steps.slice(0, n).some((d) => d.output === 'plan')))
119
+ return true;
120
+ }
121
+ return false;
122
+ }
108
123
  // Plan output minus the trailing RESULT line, ready to post.
109
124
  export function planBody(output) {
110
125
  return output.replace(/^\s*RESULT:.*$/gim, '').trim();
@@ -94,7 +94,9 @@ The plan file holds the detail. In the chat, keep it short and plain:
94
94
  Start straight at Understanding. Do not repeat the card id or title; the card
95
95
  already shows them.
96
96
 
97
- Length follows the change: under 350 words for a small one, never padded.
97
+ Outcome first. Under 350 words for a small change, never over 700 for any
98
+ change. File and line detail only in Surfaces; no code blocks over 5 lines;
99
+ Six angles lists only angles that apply.
98
100
 
99
101
  **Understanding.** Two to four sentences: what the card asks, what it got
100
102
  wrong, which reading you chose.
@@ -123,6 +125,7 @@ anything a human must click.
123
125
  - No ticket ids in code comments.
124
126
  - No em dashes or en dashes.
125
127
  - No narration of process. State findings.
128
+ - Do not quote code the dev agent can read.
126
129
 
127
130
  ## When you run inside a workflow
128
131
 
@@ -41,7 +41,8 @@ main points.
41
41
  2. The findings that change what gets built, most important first, at most 5,
42
42
  one short plain line each: what is wrong and what to do about it.
43
43
  3. Everything smaller in one line: `Minor: ...`.
44
- 4. A short `Details` section: for each finding, the file and line that proves it.
44
+ 4. A short `Details` section: one line per finding, the file and line that
45
+ proves it.
45
46
  5. Last line, exactly one of:
46
47
 
47
48
  ```
@@ -56,6 +57,7 @@ fixes to the plan file.
56
57
 
57
58
  - No em dashes or en dashes.
58
59
  - No narration of process. State findings.
60
+ - Whole review under 300 words.
59
61
 
60
62
  ## When you run inside a workflow
61
63
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voidlabs.gg/voidshift-agents",
3
- "version": "0.4.5",
3
+ "version": "0.5.1",
4
4
  "description": "Agents for Claude Code and Codex that plan, build and QA Linear cards, chained and run in the background.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -40,7 +40,7 @@
40
40
  "scripts": {
41
41
  "build": "rm -rf dist && tsc -p tsconfig.json",
42
42
  "typecheck": "tsc -p tsconfig.json --noEmit",
43
- "test": "node --test test/*.test.ts",
43
+ "test": "node --test --test-force-exit --test-timeout=60000 test/*.test.ts",
44
44
  "prepublishOnly": "rm -rf dist && tsc -p tsconfig.json"
45
45
  },
46
46
  "devDependencies": {