@voidlabs.gg/voidshift-agents 0.5.0 → 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/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/runner.js CHANGED
@@ -1,6 +1,6 @@
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";
@@ -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]));
@@ -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.5.0",
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": {