@voidlabs.gg/voidshift-agents 0.4.2 → 0.4.4

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/index.js CHANGED
@@ -4,12 +4,16 @@ import { homedir } from 'node:os';
4
4
  import { basename, dirname, join, resolve, sep } from 'node:path';
5
5
  import { parseArgs } from 'node:util';
6
6
  import { fileURLToPath } from 'node:url';
7
- import { AGENTS, STEPS, TEMPLATES, isProducer, agentsDir, describeSteps, describeTriggers, describeWorkflow, stepEnabled, visibleAgents, readAgentsConfig, writeAgentsConfig, DEFAULTS } from "./config.js";
7
+ import { AGENTS, STEPS, TEMPLATES, isProducer, agentsDir, describeSteps, describeTrigger, describeTriggers, describeWorkflow, stepEnabled, visibleAgents, readAgentsConfig, writeAgentsConfig, DEFAULTS } from "./config.js";
8
8
  import { controlChecks, controlCommands, controlUsage } from "./control.js";
9
- import { looksLikeLinearKey, readLinearKey, storeLinearKey } from "./keychain.js";
10
- import { startRunner, stopRunner } from "./launchd.js";
9
+ import { keyStore, looksLikeLinearKey, readLinearKey, readSecret, storeLinearKey, storeSecret } from "./secrets.js";
10
+ import { TRACKERS } from "./tracker.js";
11
+ import { createVoidTrack } from "./voidtrack.js";
12
+ import { parseCron } from "./cron.js";
13
+ import { trackersInUse } from "./control.js";
14
+ import { runnerService } from "./service.js";
11
15
  import { createLinear } from "./linear.js";
12
- import { duplicateTriggerWarnings, moveKey, parseTrigger, renameKey, sameTrigger, validateConfig, validateWorkflow, workflowsUsing } from "./manage.js";
16
+ import { TRIGGER_FORMS, duplicateTriggerWarnings, moveKey, parseTrigger, renameKey, sameTrigger, validateConfig, validateWorkflow, workflowsUsing } from "./manage.js";
13
17
  import { parseStep, resolveName, stepNames } from "./names.js";
14
18
  import { findRepos } from "./repos.js";
15
19
  import { defaultBranch, sh } from "./git.js";
@@ -81,30 +85,32 @@ async function chooseHomes(ui, configDir, saved) {
81
85
  return homes;
82
86
  }
83
87
  // Checks the key against Linear, and only then stores it.
84
- async function verifyAndStore(key, info, warn) {
88
+ async function verifyAndStore(configDir, key, info, warn, kind = 'linear') {
89
+ const name = TRACKERS[kind].name;
85
90
  try {
86
- const me = await createLinear(key).viewer();
87
- if (!storeLinearKey(key)) {
88
- warn('Linear accepted the key but the Keychain did not store it.');
91
+ const me = await (kind === 'voidtrack' ? createVoidTrack(key) : createLinear(key)).viewer();
92
+ if (!(kind === 'voidtrack' ? storeSecret(configDir, 'voidtrack', key) : storeLinearKey(configDir, key))) {
93
+ warn(`${name} accepted the key but it could not be stored in ${keyStore()}.`);
89
94
  return null;
90
95
  }
91
- info(`Linear connected as ${me.name}. Key stored in your macOS Keychain.`);
96
+ info(`${name} connected as ${me.name}. Key stored in ${keyStore()}.`);
92
97
  return me.name;
93
98
  }
94
99
  catch (err) {
95
- warn(`Linear did not accept that key: ${err.message}. Nothing was stored.`);
100
+ warn(`${name} did not accept that key: ${err.message}. Nothing was stored.`);
96
101
  return null;
97
102
  }
98
103
  }
99
104
  // Asks for the key, checks it against Linear, and only then stores it.
100
- async function setupKey(ui) {
101
- ui.info('Agents need a Linear personal API key (Linear > Settings > Security & access > API keys).');
102
- const key = await ui.secret('Paste your Linear API key', (v) => (looksLikeLinearKey(v.trim()) ? undefined : 'Linear keys start with lin_api_'));
103
- return verifyAndStore(key.trim(), ui.info, ui.warn);
105
+ async function setupKey(ui, configDir, kind = 'linear') {
106
+ const { name, keyHint } = TRACKERS[kind];
107
+ ui.info(`Agents need a ${name} API key (${keyHint}).`);
108
+ const key = await ui.secret(`Paste your ${name} API key`, (v) => kind === 'linear' ? (looksLikeLinearKey(v.trim()) ? undefined : 'Linear keys start with lin_api_') : v.trim() ? undefined : 'Required');
109
+ return verifyAndStore(configDir, key.trim(), ui.info, ui.warn, kind);
104
110
  }
105
- async function currentLinearUser() {
111
+ async function currentLinearUser(configDir) {
106
112
  try {
107
- return readLinearKey() ? (await linear().viewer()).name : null;
113
+ return readLinearKey(configDir) ? (await linear(configDir).viewer()).name : null;
108
114
  }
109
115
  catch {
110
116
  return null;
@@ -181,8 +187,8 @@ function syncLinks(chosen, all, enabled, log) {
181
187
  log(`Agents linked in ${homeLabel(home)}: ${linked} linked${skipped ? `, ${skipped} skipped (something else is already there)` : ''}`);
182
188
  }
183
189
  }
184
- function linear() {
185
- const key = readLinearKey();
190
+ function linear(configDir) {
191
+ const key = readLinearKey(configDir);
186
192
  if (!key)
187
193
  throw new Error('No Linear API key. Run vsf agents key');
188
194
  return createLinear(key);
@@ -277,11 +283,22 @@ async function promptWorkflow(ui, config, teams, current = {}, edit = false) {
277
283
  if (!triggers) {
278
284
  triggers = [];
279
285
  do {
280
- const team = teams.length
281
- ? await ui.select(triggers.length ? 'Also start for cards in which Linear team?' : 'Starts when a card in which Linear team...', teams.map((t) => ({ value: t.key, label: `${t.key} ${t.name}`, hint: t.mine ? 'your team' : undefined })), myTeams.length === 1 ? myTeams[0].key : undefined)
282
- : (await ui.text('Starts when a card in which Linear team key...', { validate: (v) => (v.trim() ? undefined : 'Required') })).trim().toUpperCase();
286
+ const source = await ui.select(triggers.length ? 'Also start it when...' : 'Starts when...', [
287
+ { value: 'linear', label: 'A Linear card gets a label' },
288
+ { value: 'voidtrack', label: 'A VoidTrack card gets a label' },
289
+ { value: 'cron', label: 'On a schedule', hint: 'runs on a standing task, no card' },
290
+ ], 'linear');
291
+ if (source === 'cron') {
292
+ const schedule = (await ui.text('Cron schedule (minute hour day month weekday)', { initial: '0 5 * * 1', validate: (v) => (parseCron(v) ? undefined : 'Five fields, like 0 5 * * 1') })).trim();
293
+ const task = (await ui.text('What should each run work on?', { placeholder: 'Update the dependencies and fix what breaks', validate: (v) => (v.trim() ? undefined : 'Required') })).trim();
294
+ triggers.push({ source: 'cron', schedule, task });
295
+ continue;
296
+ }
297
+ const team = source === 'linear' && teams.length
298
+ ? await ui.select('A card in which Linear team...', teams.map((t) => ({ value: t.key, label: `${t.key} ${t.name}`, hint: t.mine ? 'your team' : undefined })), myTeams.length === 1 ? myTeams[0].key : undefined)
299
+ : (await ui.text(`A card in which ${TRACKERS[source].name} team key...`, { validate: (v) => (v.trim() ? undefined : 'Required') })).trim().toUpperCase();
283
300
  const label = (await ui.text('...gets which label?', { initial: 'vs-plan', validate: (v) => (v.trim() ? undefined : 'Required') })).trim();
284
- triggers.push({ source: 'linear', team, label });
301
+ triggers.push({ source, team, label });
285
302
  } while (await ui.confirm('Add another way to start this workflow?', false));
286
303
  }
287
304
  let steps = w.steps?.length && (await keep('steps', describeSteps(w.steps))) ? w.steps : undefined;
@@ -341,7 +358,7 @@ async function setupWorkflows(ui, config, teams, log) {
341
358
  // One entry per command, in the order help lists them.
342
359
  const USAGE = [
343
360
  ['workflows [--json]', 'List workflows in the order they match'],
344
- ['workflow add [NAME] [--trigger TEAM:LABEL]... [--steps dev,qa | --template build] [--project P] [--rounds N]', 'Create a workflow; prompts for what is missing'],
361
+ ['workflow add [NAME] [--trigger TEAM:LABEL]... [--steps dev,qa | --template build] [--project P] [--rounds N]', 'Create a workflow; prompts for what is missing. A trigger is TEAM:LABEL (Linear), voidtrack:TEAM:LABEL or "cron:SCHEDULE:TASK"'],
345
362
  ['workflow show NAME [--json]', 'Show one workflow'],
346
363
  ['workflow edit NAME [--trigger T:L]... [--add-trigger T:L] [--remove-trigger T:L] [--steps S | --template T] [--project P] [--rounds N]', 'Change a workflow; no flags asks'],
347
364
  ['workflow rename OLD NEW', 'Rename a workflow, keeping its place'],
@@ -356,7 +373,7 @@ const USAGE = [
356
373
  ['homes [--json]', 'Show where the agents are linked'],
357
374
  ['link DIR [--claude | --codex]', 'Link the agents into a config folder'],
358
375
  ['unlink DIR', 'Remove the agents from a config folder'],
359
- ['key', 'Store or replace the Linear API key (reads stdin without a terminal)'],
376
+ ['key [--voidtrack]', 'Store or replace the Linear or VoidTrack API key (reads stdin without a terminal)'],
360
377
  ['help [COMMAND]', 'Show this list, or one command'],
361
378
  ];
362
379
  const usageOf = (name) => USAGE.filter(([c]) => c === name || c.startsWith(`${name} `))
@@ -441,7 +458,7 @@ function readWorkflowFlags(v, config) {
441
458
  for (const t of v.trigger) {
442
459
  const parsed = parseTrigger(t);
443
460
  if (!parsed)
444
- return { error: `Bad trigger ${t}. Use TEAM:LABEL, like ENG:vs-plan.`, code: 2 };
461
+ return { error: `Bad trigger ${t}. Use ${TRIGGER_FORMS}.`, code: 2 };
445
462
  flags.triggers.push(parsed);
446
463
  }
447
464
  }
@@ -489,7 +506,7 @@ function pickWorkflow(config, input, ctx) {
489
506
  }
490
507
  async function teamsOrNull(ctx) {
491
508
  try {
492
- return await linear().teams();
509
+ return await linear(ctx.configDir).teams();
493
510
  }
494
511
  catch (err) {
495
512
  ctx.log(`Could not list Linear teams: ${err.message}`);
@@ -590,7 +607,7 @@ const workflowSubs = {
590
607
  else {
591
608
  ctx.log(`Workflow ${name}: ${describeWorkflow(w)}`);
592
609
  for (const t of w.triggers)
593
- ctx.log(`Trigger: ${t.team} + ${t.label}`);
610
+ ctx.log(`Trigger: ${describeTrigger(t)}${t.source === 'cron' ? `, task: ${t.task}` : ''}`);
594
611
  ctx.log(`Steps: ${describeSteps(w.steps)}`);
595
612
  ctx.log(`Project: ${w.project}`);
596
613
  ctx.log(`Rounds: ${w.maxRounds}`);
@@ -637,7 +654,7 @@ const workflowSubs = {
637
654
  for (const t of (v['add-trigger'] ?? [])) {
638
655
  const parsed = parseTrigger(t);
639
656
  if (!parsed) {
640
- ctx.log(`Bad trigger ${t}. Use TEAM:LABEL, like ENG:vs-plan.`);
657
+ ctx.log(`Bad trigger ${t}. Use ${TRIGGER_FORMS}.`);
641
658
  return 2;
642
659
  }
643
660
  if (!next.triggers.some((x) => sameTrigger(x, parsed)))
@@ -1045,22 +1062,26 @@ const agents = {
1045
1062
  config.models = { ...DEFAULTS.models };
1046
1063
  }
1047
1064
  config.engine = engine;
1048
- const name = await currentLinearUser();
1065
+ const name = await currentLinearUser(ctx.configDir);
1049
1066
  if (!name || !(await ui.confirm(`Keep using Linear as ${name}?`))) {
1050
- if (!(await setupKey(ui)))
1067
+ if (!(await setupKey(ui, ctx.configDir)))
1051
1068
  throw new Error('Linear is not connected. Run vsf setup agents to retry.');
1052
1069
  }
1053
1070
  await setupProjects(ui, config, ctx.log);
1054
- await setupWorkflows(ui, config, await linear().teams(), ctx.log);
1071
+ await setupWorkflows(ui, config, await linear(ctx.configDir).teams(), ctx.log);
1055
1072
  writeAgentsConfig(ctx.configDir, config);
1056
- if (Object.keys(config.workflows).length && (await ui.confirm('Start the background runner now? It checks Linear every minute for cards that start your workflows.'))) {
1057
- const ok = startRunner(ctx.home, [...ctx.selfCommand, 'agents', 'tick'], join(agentsDir(ctx.configDir), 'runner.log'));
1073
+ if (trackersInUse(config).includes('voidtrack') && !readSecret(ctx.configDir, 'voidtrack')) {
1074
+ if (!(await setupKey(ui, ctx.configDir, 'voidtrack')))
1075
+ ctx.log('VoidTrack is not connected yet. Run vsf agents key --voidtrack.');
1076
+ }
1077
+ 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.'))) {
1078
+ const ok = runnerService().start(ctx.home, [...ctx.selfCommand, 'agents', 'tick'], join(agentsDir(ctx.configDir), 'runner.log'));
1058
1079
  ctx.log(ok ? 'Runner started.' : 'Could not start the runner. Try vsf agents start.');
1059
1080
  }
1060
1081
  ctx.log('In Claude Code or Codex, connect Linear too, then use /vs-plan, /vs-plan-review and /vs-publish on a card.');
1061
1082
  },
1062
1083
  async uninstall(ctx) {
1063
- stopRunner(ctx.home);
1084
+ runnerService().stop(ctx.home);
1064
1085
  syncLinks([], candidateHomes(ctx.configDir, readAgentsConfig(ctx.configDir).homes), [], ctx.log);
1065
1086
  ctx.log('Agents unlinked and the runner stopped. Config and the Linear key were kept.');
1066
1087
  },
@@ -1077,12 +1098,19 @@ const agents = {
1077
1098
  ? { name: `agents in ${homeLabel(h)}`, status: 'ok', detail: `${wanted.length} linked` }
1078
1099
  : { name: `agents in ${homeLabel(h)}`, status: 'warn', detail: `missing ${missing.join(', ')}`, fix: `vsf agents link ${tilde(h.dir)}` });
1079
1100
  }
1080
- try {
1081
- const me = await linear().viewer();
1082
- checks.push({ name: 'Linear', status: 'ok', detail: `connected as ${me.name}` });
1083
- }
1084
- catch (err) {
1085
- checks.push({ name: 'Linear', status: 'fail', detail: err.message, fix: 'vsf agents key' });
1101
+ const inUse = trackersInUse(saved);
1102
+ for (const kind of inUse.length ? inUse : ['linear']) {
1103
+ const { name } = TRACKERS[kind];
1104
+ try {
1105
+ const key = readSecret(ctx.configDir, kind);
1106
+ if (!key)
1107
+ throw new Error(`No ${name} API key`);
1108
+ const me = await (kind === 'voidtrack' ? createVoidTrack(key) : createLinear(key)).viewer();
1109
+ checks.push({ name, status: 'ok', detail: `connected as ${me.name}` });
1110
+ }
1111
+ catch (err) {
1112
+ checks.push({ name, status: 'fail', detail: err.message, fix: `vsf agents key${kind === 'linear' ? '' : ` --${kind}`}` });
1113
+ }
1086
1114
  }
1087
1115
  const workflows = Object.entries(saved.workflows);
1088
1116
  const broken = workflows.filter(([, c]) => !saved.projects[c.project] || !isRepo(saved.projects[c.project].path)).map(([n]) => n);
@@ -1134,21 +1162,26 @@ const agents = {
1134
1162
  link: homeCommands.link,
1135
1163
  unlink: homeCommands.unlink,
1136
1164
  async key(args, ctx) {
1137
- const p = begin('key', args, {}, ctx);
1165
+ const p = begin('key', args, { voidtrack: { type: 'boolean' } }, ctx);
1138
1166
  if (typeof p === 'number')
1139
1167
  return p;
1140
1168
  if (p.positionals.length) {
1141
1169
  ctx.log('Keys are not accepted on the command line. Pipe the key in instead.');
1142
1170
  return 2;
1143
1171
  }
1172
+ const kind = p.values.voidtrack ? 'voidtrack' : 'linear';
1144
1173
  if (interactive())
1145
- return (await setupKey(ctx.ui)) ? 0 : 1;
1174
+ return (await setupKey(ctx.ui, ctx.configDir, kind)) ? 0 : 1;
1146
1175
  const key = readFileSync(0, 'utf8').trim();
1147
- if (!looksLikeLinearKey(key)) {
1176
+ if (kind === 'linear' && !looksLikeLinearKey(key)) {
1148
1177
  ctx.log('Linear keys start with lin_api_');
1149
1178
  return 2;
1150
1179
  }
1151
- return (await verifyAndStore(key, ctx.log, ctx.log)) ? 0 : 1;
1180
+ if (!key) {
1181
+ ctx.log('No key on stdin.');
1182
+ return 2;
1183
+ }
1184
+ return (await verifyAndStore(ctx.configDir, key, ctx.log, ctx.log, kind)) ? 0 : 1;
1152
1185
  },
1153
1186
  async help(args, ctx) {
1154
1187
  const want = args.join(' ').trim();
package/dist/launchd.d.ts CHANGED
@@ -5,3 +5,4 @@ export declare function runnerEnv(): Record<string, string>;
5
5
  export declare const runnerLoaded: () => boolean;
6
6
  export declare function startRunner(home: string, program: string[], logPath: string): boolean;
7
7
  export declare function stopRunner(home: string): void;
8
+ export declare function parsePlistArgs(plist: string): string[];
package/dist/launchd.js CHANGED
@@ -66,3 +66,9 @@ export function stopRunner(home) {
66
66
  launchctl(['bootout', `${domain()}/${LABEL}`]);
67
67
  rmSync(plistPath(home), { force: true });
68
68
  }
69
+ // The <string> values of a launchd plist's ProgramArguments.
70
+ export function parsePlistArgs(plist) {
71
+ const block = /<key>ProgramArguments<\/key>\s*<array>([\s\S]*?)<\/array>/.exec(plist)?.[1] ?? '';
72
+ const unxml = (s) => s.replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&amp;/g, '&');
73
+ return [...block.matchAll(/<string>([\s\S]*?)<\/string>/g)].map((m) => unxml(m[1]));
74
+ }
package/dist/linear.d.ts CHANGED
@@ -20,7 +20,7 @@ export declare function createLinear(apiKey: string): {
20
20
  name: string;
21
21
  mine: boolean;
22
22
  }[]>;
23
- myCardsWithLabel(label: string): Promise<Card[]>;
23
+ myCardsWithLabel(label: string, team: string): Promise<Card[]>;
24
24
  card(identifier: string): Promise<Card>;
25
25
  comments(identifier: string): Promise<{
26
26
  body: string;
package/dist/linear.js CHANGED
@@ -79,14 +79,14 @@ export function createLinear(apiKey) {
79
79
  const all = d?.teams?.nodes ?? [];
80
80
  return all.map((t) => ({ ...t, mine: mine.has(t.key) })).sort((a, b) => Number(b.mine) - Number(a.mine));
81
81
  },
82
- // My cards carrying the trigger label.
83
- async myCardsWithLabel(label) {
82
+ // My cards in the team carrying the trigger label.
83
+ async myCardsWithLabel(label, team) {
84
84
  const d = await gql('myCards', `query MyCards($label: String!) {
85
85
  issues(first: 20, orderBy: createdAt, filter: { assignee: { isMe: { eq: true } }, labels: { name: { eq: $label } } }) {
86
86
  nodes { ${CARD_FIELDS} }
87
87
  }
88
88
  }`, { label });
89
- return (d?.issues?.nodes ?? []).map(mapCard);
89
+ return (d?.issues?.nodes ?? []).map(mapCard).filter((c) => c.teamKey === team);
90
90
  },
91
91
  async card(identifier) {
92
92
  const d = await gql('card', `query Card($id: String!) { issue(id: $id) { ${CARD_FIELDS} } }`, { id: identifier });
package/dist/manage.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { type AgentsConfig, type Trigger, type Workflow } from './config.ts';
2
2
  export declare function validateWorkflow(config: Pick<AgentsConfig, 'enabled' | 'projects'>, workflow: Workflow): string[];
3
+ export declare const TRIGGER_FORMS = "TEAM:LABEL (Linear), voidtrack:TEAM:LABEL, or cron:SCHEDULE:TASK";
3
4
  export declare function parseTrigger(input: string): Trigger | null;
4
5
  export declare const sameTrigger: (a: Trigger, b: Trigger) => boolean;
5
6
  export declare function duplicateTriggerWarnings(workflows: Record<string, Workflow>, name: string, workflow: Workflow): string[];
package/dist/manage.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
- import { AGENTS, STEPS } from "./config.js";
3
+ import { AGENTS, STEPS, describeTrigger, triggerSpec } from "./config.js";
4
+ import { parseCron } from "./cron.js";
4
5
  const isInt = (v, min, max) => typeof v === 'number' && Number.isInteger(v) && v >= min && v <= max;
5
6
  // Everything wrong with one workflow, in plain words. Empty means it is fine.
6
7
  export function validateWorkflow(config, workflow) {
@@ -8,10 +9,20 @@ export function validateWorkflow(config, workflow) {
8
9
  if (!workflow.triggers?.length)
9
10
  problems.push('Needs at least one trigger.');
10
11
  for (const t of workflow.triggers ?? []) {
11
- if (!t.team?.trim())
12
- problems.push('A trigger has no team.');
13
- if (!t.label?.trim())
14
- problems.push('A trigger has no label.');
12
+ if (t.source === 'cron') {
13
+ if (!parseCron(t.schedule ?? ''))
14
+ problems.push(`Schedule ${t.schedule} is not a five field cron schedule, like 0 5 * * 1.`);
15
+ if (!t.task?.trim())
16
+ problems.push('A schedule has no task.');
17
+ }
18
+ else if (t.source !== 'linear' && t.source !== 'voidtrack')
19
+ problems.push(`Unknown trigger source ${t.source}.`);
20
+ else {
21
+ if (!t.team?.trim())
22
+ problems.push('A trigger has no team.');
23
+ if (!t.label?.trim())
24
+ problems.push('A trigger has no label.');
25
+ }
15
26
  }
16
27
  if (!workflow.steps?.length)
17
28
  problems.push('Needs at least one step.');
@@ -27,16 +38,27 @@ export function validateWorkflow(config, workflow) {
27
38
  problems.push('Rounds must be a whole number from 1 to 9.');
28
39
  return problems;
29
40
  }
30
- // "ENG:vs-plan" into a trigger; the team is upper-cased.
41
+ export const TRIGGER_FORMS = 'TEAM:LABEL (Linear), voidtrack:TEAM:LABEL, or cron:SCHEDULE:TASK';
42
+ // "ENG:vs-plan" (Linear, also "linear:ENG:vs-plan"), "voidtrack:ORG:vs-start"
43
+ // or "cron:0 5 * * 1:Update the dependencies" into a trigger. Teams are
44
+ // upper-cased; a schedule must parse.
31
45
  export function parseTrigger(input) {
32
- const i = input.indexOf(':');
33
- if (i < 0)
46
+ const text = input.trim();
47
+ // A source prefix decides the form; a bare TEAM:LABEL is Linear.
48
+ if (text.startsWith('cron:')) {
49
+ const cron = /^cron:([^:]+):([\s\S]+)$/.exec(text);
50
+ const schedule = cron?.[1].trim().replace(/\s+/g, ' ') ?? '';
51
+ const task = cron?.[2].trim() ?? '';
52
+ return parseCron(schedule) && task ? { source: 'cron', schedule, task } : null;
53
+ }
54
+ const m = /^(linear|voidtrack):/.test(text) ? /^(linear|voidtrack):([^:]+):(.+)$/.exec(text) : /^()([^:]+):(.+)$/.exec(text);
55
+ if (!m)
34
56
  return null;
35
- const team = input.slice(0, i).trim().toUpperCase();
36
- const label = input.slice(i + 1).trim();
37
- return team && label ? { source: 'linear', team, label } : null;
57
+ const team = m[2].trim().toUpperCase();
58
+ const label = m[3].trim();
59
+ return team && label ? { source: (m[1] || 'linear'), team, label } : null;
38
60
  }
39
- export const sameTrigger = (a, b) => a.team === b.team && a.label === b.label;
61
+ export const sameTrigger = (a, b) => triggerSpec(a) === triggerSpec(b);
40
62
  // Warning lines for triggers this workflow shares with another one. Only the
41
63
  // first workflow in order starts a card, so the rest never get it.
42
64
  export function duplicateTriggerWarnings(workflows, name, workflow) {
@@ -47,7 +69,7 @@ export function duplicateTriggerWarnings(workflows, name, workflow) {
47
69
  for (const other of owners) {
48
70
  if (other === name)
49
71
  continue;
50
- lines.push(`Warning: ${other} also starts on ${t.team} + ${t.label}, and ${first} wins. vsf agents workflow move changes the order.`);
72
+ lines.push(`Warning: ${other} also starts on ${describeTrigger(t)}, and ${first} wins. vsf agents workflow move changes the order.`);
51
73
  }
52
74
  }
53
75
  return lines;
package/dist/runner.d.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  import { type AgentsConfig, type Workflow, type Project, type StepId, type Trigger } from './config.ts';
2
- import type { Card, Linear } from './linear.ts';
2
+ import type { Card } from './linear.ts';
3
+ import { type Tracker, type Trackers } from './tracker.ts';
3
4
  export interface RunContext {
4
5
  configDir: string;
5
6
  config: AgentsConfig;
6
- linear: Linear;
7
+ trackers: Trackers;
7
8
  log: (line: string) => void;
8
9
  selfCommand: string[];
9
10
  }
@@ -11,13 +12,19 @@ export declare function summarize(output: string): string;
11
12
  export declare function runCard(card: Card, trigger: Trigger, workflowName: string, workflow: Workflow, project: Project, rc: RunContext, opts?: {
12
13
  from?: StepId;
13
14
  }): Promise<'verified' | 'changes' | 'parked'>;
14
- export declare function nextJob(rc: RunContext, only?: string, workflowName?: string): Promise<{
15
+ type Job = {
15
16
  card: Card;
16
17
  trigger: Trigger;
17
18
  workflow: string;
18
- } | null>;
19
- export declare function cancelCard(linear: Linear, configDir: string, cardId: string, label: string): Promise<boolean>;
19
+ };
20
+ export declare function cronCard(workflow: string, task: string, at: Date): Card;
21
+ export declare function nextJob(rc: RunContext, only?: string, workflowName?: string, opts?: {
22
+ force?: boolean;
23
+ now?: Date;
24
+ }): Promise<Job | null>;
25
+ export declare function cancelCard(tracker: Tracker, configDir: string, cardId: string, label: string, source?: 'linear' | 'voidtrack'): Promise<boolean>;
20
26
  export declare function tick(rc: RunContext, only?: string, opts?: {
21
27
  workflow?: string;
22
28
  from?: StepId;
23
29
  }): Promise<number>;
30
+ export {};