@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/dist/manage.js
CHANGED
|
@@ -1,42 +1,154 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
|
-
import { AGENTS,
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { basename, join, resolve } from 'node:path';
|
|
3
|
+
import { AGENTS, AGENT_FILE, OUTPUTS, STEP_ID, isBuiltin, readAgentFolder, stepFile, 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;
|
|
6
|
+
// The line a step's prompt must ask for, so the runner can read the result.
|
|
7
|
+
const RESULT_LINE = { plan: 'RESULT:', commits: 'RESULT:', verdict: 'VERDICT:' };
|
|
8
|
+
// Everything wrong with one step. `dir` is the agent folder its files are in.
|
|
9
|
+
export function validateStep(step, config, dir) {
|
|
10
|
+
if (typeof step === 'string') {
|
|
11
|
+
if (!isBuiltin(step))
|
|
12
|
+
return [`Unknown step ${step}.`];
|
|
13
|
+
return config.enabled.length && !config.enabled.includes(step) ? [`Step ${step} is not enabled.`] : [];
|
|
14
|
+
}
|
|
15
|
+
if (!isObj(step))
|
|
16
|
+
return ['A step must be a step id or an object.'];
|
|
17
|
+
const problems = [];
|
|
18
|
+
const id = typeof step.id === 'string' && STEP_ID.test(step.id) ? step.id : null;
|
|
19
|
+
if (!id)
|
|
20
|
+
problems.push(`Step id ${String(step.id)} must be lowercase letters, digits and dashes.`);
|
|
21
|
+
else if (isBuiltin(id))
|
|
22
|
+
problems.push(`Step id ${id} is a built-in step; use "${id}" on its own, or another id.`);
|
|
23
|
+
const name = id ?? 'A step';
|
|
24
|
+
if (!OUTPUTS.includes(step.output))
|
|
25
|
+
problems.push(`${name} output must be one of: ${OUTPUTS.join(', ')}.`);
|
|
26
|
+
if (step.label !== undefined && typeof step.label !== 'string')
|
|
27
|
+
problems.push(`${name} label must be text.`);
|
|
28
|
+
if (step.prompt !== undefined && typeof step.prompt !== 'string')
|
|
29
|
+
problems.push(`${name} prompt must be a file name.`);
|
|
30
|
+
if (step.skill !== undefined && typeof step.skill !== 'string')
|
|
31
|
+
problems.push(`${name} skill must be a name or a folder.`);
|
|
32
|
+
const sources = [step.skill, step.prompt].filter((v) => v !== undefined).length;
|
|
33
|
+
if (step.output === 'publish') {
|
|
34
|
+
if (sources)
|
|
35
|
+
problems.push(`${name} only pushes the branch, so it takes no skill or prompt.`);
|
|
36
|
+
}
|
|
37
|
+
else if (sources !== 1)
|
|
38
|
+
problems.push(`${name} needs exactly one of skill or prompt.`);
|
|
39
|
+
else if (typeof (step.prompt ?? step.skill) === 'string') {
|
|
40
|
+
const file = stepFile(step, dir);
|
|
41
|
+
if (!file || !existsSync(file))
|
|
42
|
+
problems.push(`${name} ${step.prompt ? 'prompt' : 'skill'} ${file ?? ''} does not exist.`);
|
|
43
|
+
else {
|
|
44
|
+
const want = RESULT_LINE[step.output];
|
|
45
|
+
if (want && !readFileSync(file, 'utf8').includes(want))
|
|
46
|
+
problems.push(`${name} prompt never asks for a ${want} line.`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (step.engine !== undefined && step.engine !== 'claude' && step.engine !== 'codex')
|
|
50
|
+
problems.push(`${name} engine must be claude or codex.`);
|
|
51
|
+
if (step.model !== undefined && typeof step.model !== 'string')
|
|
52
|
+
problems.push(`${name} model must be text.`);
|
|
53
|
+
if (step.timeoutMinutes !== undefined && !isInt(step.timeoutMinutes, 1, 480))
|
|
54
|
+
problems.push(`${name} timeoutMinutes must be a whole number from 1 to 480.`);
|
|
55
|
+
if (step.write !== undefined && typeof step.write !== 'boolean')
|
|
56
|
+
problems.push(`${name} write must be true or false.`);
|
|
57
|
+
else if (step.write === false && step.output === 'commits')
|
|
58
|
+
problems.push(`${name} makes commits, so write cannot be false.`);
|
|
59
|
+
return problems;
|
|
60
|
+
}
|
|
5
61
|
// Everything wrong with one workflow, in plain words. Empty means it is fine.
|
|
6
|
-
|
|
62
|
+
// `dir` is the agent folder a folder workflow comes from.
|
|
63
|
+
export function validateWorkflow(config, workflow, dir) {
|
|
7
64
|
const problems = [];
|
|
8
65
|
if (!workflow.triggers?.length)
|
|
9
66
|
problems.push('Needs at least one trigger.');
|
|
10
67
|
for (const t of workflow.triggers ?? []) {
|
|
11
|
-
if (
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
68
|
+
if (t.source === 'cron') {
|
|
69
|
+
if (!parseCron(t.schedule ?? ''))
|
|
70
|
+
problems.push(`Schedule ${t.schedule} is not a five field cron schedule, like 0 5 * * 1.`);
|
|
71
|
+
if (!t.task?.trim())
|
|
72
|
+
problems.push('A schedule has no task.');
|
|
73
|
+
}
|
|
74
|
+
else if (t.source !== 'linear' && t.source !== 'voidtrack')
|
|
75
|
+
problems.push(`Unknown trigger source ${t.source}.`);
|
|
76
|
+
else {
|
|
77
|
+
if (!t.team?.trim())
|
|
78
|
+
problems.push('A trigger has no team.');
|
|
79
|
+
if (!t.label?.trim())
|
|
80
|
+
problems.push('A trigger has no label.');
|
|
81
|
+
}
|
|
15
82
|
}
|
|
16
83
|
if (!workflow.steps?.length)
|
|
17
84
|
problems.push('Needs at least one step.');
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
85
|
+
const ids = new Set();
|
|
86
|
+
for (const s of Array.isArray(workflow.steps) ? workflow.steps : []) {
|
|
87
|
+
problems.push(...validateStep(s, config, dir));
|
|
88
|
+
const id = typeof s === 'string' ? s : isObj(s) ? s.id : undefined;
|
|
89
|
+
if (typeof id === 'string' && !isBuiltin(id)) {
|
|
90
|
+
if (ids.has(id))
|
|
91
|
+
problems.push(`Step id ${id} is used twice.`);
|
|
92
|
+
ids.add(id);
|
|
93
|
+
}
|
|
23
94
|
}
|
|
24
95
|
if (!config.projects[workflow.project])
|
|
25
96
|
problems.push(`Project ${workflow.project} does not exist.`);
|
|
26
97
|
if (!isInt(workflow.maxRounds, 1, 9))
|
|
27
98
|
problems.push('Rounds must be a whole number from 1 to 9.');
|
|
99
|
+
const wb = workflow.writeBack;
|
|
100
|
+
if (wb !== undefined) {
|
|
101
|
+
if (!isObj(wb))
|
|
102
|
+
problems.push('writeBack must be an object.');
|
|
103
|
+
else {
|
|
104
|
+
if (wb.labels !== undefined && typeof wb.labels !== 'boolean')
|
|
105
|
+
problems.push('writeBack.labels must be true or false.');
|
|
106
|
+
if (wb.comment !== undefined && typeof wb.comment !== 'boolean')
|
|
107
|
+
problems.push('writeBack.comment must be true or false.');
|
|
108
|
+
if (wb.state !== undefined && wb.state !== null && (typeof wb.state !== 'string' || !wb.state.trim()))
|
|
109
|
+
problems.push('writeBack.state must be a state name, or null to never move the card.');
|
|
110
|
+
}
|
|
111
|
+
}
|
|
28
112
|
return problems;
|
|
29
113
|
}
|
|
30
|
-
//
|
|
114
|
+
// Everything wrong with an agent folder: its agent.json, its workflow and
|
|
115
|
+
// prompt files, and a name config.json already uses.
|
|
116
|
+
export function validateAgentFolder(dir, config) {
|
|
117
|
+
let workflow;
|
|
118
|
+
try {
|
|
119
|
+
workflow = readAgentFolder(dir);
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
return [`${AGENT_FILE}: ${err.message}`];
|
|
123
|
+
}
|
|
124
|
+
const problems = validateWorkflow(config, workflow, dir);
|
|
125
|
+
const name = basename(dir);
|
|
126
|
+
const from = config.folders?.[name];
|
|
127
|
+
if (config.workflows[name] && (!from || resolve(from) !== resolve(dir)))
|
|
128
|
+
problems.push(`config.json already has a workflow named ${name}, and it wins. Rename the folder.`);
|
|
129
|
+
return problems;
|
|
130
|
+
}
|
|
131
|
+
export const TRIGGER_FORMS = 'TEAM:LABEL (Linear), voidtrack:TEAM:LABEL, or cron:SCHEDULE:TASK';
|
|
132
|
+
// "ENG:vs-plan" (Linear, also "linear:ENG:vs-plan"), "voidtrack:ORG:vs-start"
|
|
133
|
+
// or "cron:0 5 * * 1:Update the dependencies" into a trigger. Teams are
|
|
134
|
+
// upper-cased; a schedule must parse.
|
|
31
135
|
export function parseTrigger(input) {
|
|
32
|
-
const
|
|
33
|
-
|
|
136
|
+
const text = input.trim();
|
|
137
|
+
// A source prefix decides the form; a bare TEAM:LABEL is Linear.
|
|
138
|
+
if (text.startsWith('cron:')) {
|
|
139
|
+
const cron = /^cron:([^:]+):([\s\S]+)$/.exec(text);
|
|
140
|
+
const schedule = cron?.[1].trim().replace(/\s+/g, ' ') ?? '';
|
|
141
|
+
const task = cron?.[2].trim() ?? '';
|
|
142
|
+
return parseCron(schedule) && task ? { source: 'cron', schedule, task } : null;
|
|
143
|
+
}
|
|
144
|
+
const m = /^(linear|voidtrack):/.test(text) ? /^(linear|voidtrack):([^:]+):(.+)$/.exec(text) : /^()([^:]+):(.+)$/.exec(text);
|
|
145
|
+
if (!m)
|
|
34
146
|
return null;
|
|
35
|
-
const team =
|
|
36
|
-
const label =
|
|
37
|
-
return team && label ? { source: 'linear', team, label } : null;
|
|
147
|
+
const team = m[2].trim().toUpperCase();
|
|
148
|
+
const label = m[3].trim();
|
|
149
|
+
return team && label ? { source: (m[1] || 'linear'), team, label } : null;
|
|
38
150
|
}
|
|
39
|
-
export const sameTrigger = (a, b) => a
|
|
151
|
+
export const sameTrigger = (a, b) => triggerSpec(a) === triggerSpec(b);
|
|
40
152
|
// Warning lines for triggers this workflow shares with another one. Only the
|
|
41
153
|
// first workflow in order starts a card, so the rest never get it.
|
|
42
154
|
export function duplicateTriggerWarnings(workflows, name, workflow) {
|
|
@@ -47,7 +159,7 @@ export function duplicateTriggerWarnings(workflows, name, workflow) {
|
|
|
47
159
|
for (const other of owners) {
|
|
48
160
|
if (other === name)
|
|
49
161
|
continue;
|
|
50
|
-
lines.push(`Warning: ${other} also starts on ${t
|
|
162
|
+
lines.push(`Warning: ${other} also starts on ${describeTrigger(t)}, and ${first} wins. vsf agents workflow move changes the order.`);
|
|
51
163
|
}
|
|
52
164
|
}
|
|
53
165
|
return lines;
|
|
@@ -99,6 +211,8 @@ export function validateConfig(value, saved) {
|
|
|
99
211
|
}
|
|
100
212
|
if (!Array.isArray(value.enabled) || value.enabled.some((a) => typeof a !== 'string' || !(a in AGENTS)))
|
|
101
213
|
problems.push(`enabled must list agents from: ${Object.keys(AGENTS).join(', ')}.`);
|
|
214
|
+
if (value.commits !== undefined && (!isObj(value.commits) || Object.entries(value.commits).some(([k, v]) => !['signOff', 'dropAiTrailers'].includes(k) || typeof v !== 'boolean')))
|
|
215
|
+
problems.push('commits may only set signOff and dropAiTrailers, each true or false.');
|
|
102
216
|
if (!Array.isArray(value.homes) || value.homes.some((h) => !isObj(h) || (h.kind !== 'claude' && h.kind !== 'codex') || typeof h.dir !== 'string'))
|
|
103
217
|
problems.push('homes must be a list of { kind: claude or codex, dir }.');
|
|
104
218
|
if (!isObj(value.workflows))
|
|
@@ -106,6 +220,9 @@ export function validateConfig(value, saved) {
|
|
|
106
220
|
else {
|
|
107
221
|
const ctx = { enabled: Array.isArray(value.enabled) ? value.enabled : [], projects: (projects ?? {}) };
|
|
108
222
|
for (const [name, w] of Object.entries(value.workflows)) {
|
|
223
|
+
// Folder workflows are checked with their folder, by vsf agents validate.
|
|
224
|
+
if (isObj(value.folders) && value.folders[name])
|
|
225
|
+
continue;
|
|
109
226
|
if (!isObj(w))
|
|
110
227
|
problems.push(`Workflow ${name} must be an object.`);
|
|
111
228
|
else {
|
package/dist/names.d.ts
CHANGED
|
@@ -5,4 +5,5 @@ export declare function resolveName(names: string[], input: string, kind: string
|
|
|
5
5
|
error: string;
|
|
6
6
|
};
|
|
7
7
|
export declare function parseStep(input: string): StepId | null;
|
|
8
|
+
export declare const parseFrom: (input: string) => string | null;
|
|
8
9
|
export declare const stepNames: () => string;
|
package/dist/names.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { STEPS } from "./config.js";
|
|
1
|
+
import { STEPS, STEP_ID } from "./config.js";
|
|
2
2
|
// A workflow or project named in a command: the exact name, or the start of
|
|
3
3
|
// exactly one name, e.g. "tether" for tether-relay-build.
|
|
4
4
|
export function resolveName(names, input, kind) {
|
|
@@ -23,4 +23,7 @@ export function parseStep(input) {
|
|
|
23
23
|
}
|
|
24
24
|
return null;
|
|
25
25
|
}
|
|
26
|
+
// A step for --from: a built-in step by any of its names, or any id a custom
|
|
27
|
+
// step could have. Whether the workflow has it is checked once it is known.
|
|
28
|
+
export const parseFrom = (input) => parseStep(input) ?? (STEP_ID.test(input) ? input : null);
|
|
26
29
|
export const stepNames = () => Object.keys(STEPS).map((id) => id.replace(/^vs-/, '')).join(', ');
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Check } from './types.ts';
|
|
2
|
+
export declare const profileDir: (configDir: string) => string;
|
|
3
|
+
export declare const RULES_FILE = "RULES.md";
|
|
4
|
+
export declare const CLAUDE_SETTINGS = "claude-settings.json";
|
|
5
|
+
export declare function readRules(configDir: string): string;
|
|
6
|
+
export declare function renderClaudeSettings(configDir: string): string | null;
|
|
7
|
+
export declare function claudeSettingsFile(configDir: string, runDir: string): string | undefined;
|
|
8
|
+
export declare const rulesSection: (rules: string) => string;
|
|
9
|
+
export declare function initProfile(configDir: string): string[];
|
|
10
|
+
export declare function profileChecks(configDir: string): Check[];
|
package/dist/profile.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { agentsDir } from "./config.js";
|
|
4
|
+
// Your profile: what every agent run carries, on any machine, without touching
|
|
5
|
+
// a Claude or Codex folder. RULES.md is added to every step's prompt, and
|
|
6
|
+
// claude-settings.json is passed to Claude runs, with {{profile}} standing for
|
|
7
|
+
// this folder so hooks can live in it.
|
|
8
|
+
export const profileDir = (configDir) => join(agentsDir(configDir), 'profile');
|
|
9
|
+
export const RULES_FILE = 'RULES.md';
|
|
10
|
+
export const CLAUDE_SETTINGS = 'claude-settings.json';
|
|
11
|
+
export function readRules(configDir) {
|
|
12
|
+
const path = join(profileDir(configDir), RULES_FILE);
|
|
13
|
+
return existsSync(path) ? readFileSync(path, 'utf8').trim() : '';
|
|
14
|
+
}
|
|
15
|
+
// The Claude settings with {{profile}} filled in, as text, or null when there
|
|
16
|
+
// are none. Throws when the file is not a JSON object.
|
|
17
|
+
export function renderClaudeSettings(configDir) {
|
|
18
|
+
const dir = profileDir(configDir);
|
|
19
|
+
const path = join(dir, CLAUDE_SETTINGS);
|
|
20
|
+
if (!existsSync(path))
|
|
21
|
+
return null;
|
|
22
|
+
const text = readFileSync(path, 'utf8').replaceAll('{{profile}}', dir);
|
|
23
|
+
const value = JSON.parse(text);
|
|
24
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
25
|
+
throw new Error(`${CLAUDE_SETTINGS} must be a JSON object`);
|
|
26
|
+
return text;
|
|
27
|
+
}
|
|
28
|
+
// Writes the rendered settings into a run folder and returns the path, or
|
|
29
|
+
// undefined when the profile has none.
|
|
30
|
+
export function claudeSettingsFile(configDir, runDir) {
|
|
31
|
+
const text = renderClaudeSettings(configDir);
|
|
32
|
+
if (text === null)
|
|
33
|
+
return undefined;
|
|
34
|
+
const out = join(runDir, CLAUDE_SETTINGS);
|
|
35
|
+
writeFileSync(out, text);
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
// The prompt section for your rules, empty when there are none.
|
|
39
|
+
export const rulesSection = (rules) => (rules ? `# Your rules\n\nFollow these in everything you write, including commits and comments.\n\n${rules}` : '');
|
|
40
|
+
const STARTER_RULES = `<!-- Rules every agent follows on every run, on every machine.
|
|
41
|
+
Write them as you would tell a teammate. Delete this comment. -->
|
|
42
|
+
`;
|
|
43
|
+
// Creates the folder with a starter RULES.md. Existing files are kept.
|
|
44
|
+
export function initProfile(configDir) {
|
|
45
|
+
const dir = profileDir(configDir);
|
|
46
|
+
mkdirSync(dir, { recursive: true });
|
|
47
|
+
const made = [];
|
|
48
|
+
const rules = join(dir, RULES_FILE);
|
|
49
|
+
if (!existsSync(rules)) {
|
|
50
|
+
writeFileSync(rules, STARTER_RULES);
|
|
51
|
+
made.push(RULES_FILE);
|
|
52
|
+
}
|
|
53
|
+
return made;
|
|
54
|
+
}
|
|
55
|
+
export function profileChecks(configDir) {
|
|
56
|
+
const dir = profileDir(configDir);
|
|
57
|
+
if (!existsSync(dir))
|
|
58
|
+
return [];
|
|
59
|
+
const parts = [];
|
|
60
|
+
if (readRules(configDir).replace(/<!--[\s\S]*?-->/g, '').trim())
|
|
61
|
+
parts.push('rules');
|
|
62
|
+
try {
|
|
63
|
+
if (renderClaudeSettings(configDir) !== null)
|
|
64
|
+
parts.push('Claude settings');
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
return [{ name: 'agent profile', status: 'fail', detail: `${CLAUDE_SETTINGS}: ${err.message}`, fix: `fix ${join(dir, CLAUDE_SETTINGS)}` }];
|
|
68
|
+
}
|
|
69
|
+
return [parts.length ? { name: 'agent profile', status: 'ok', detail: parts.join(', ') } : { name: 'agent profile', status: 'warn', detail: 'empty', fix: `add rules to ${join(dir, RULES_FILE)}` }];
|
|
70
|
+
}
|
package/dist/runner.d.ts
CHANGED
|
@@ -1,23 +1,30 @@
|
|
|
1
|
-
import { type AgentsConfig, type Workflow, type Project, type
|
|
2
|
-
import type { Card
|
|
1
|
+
import { type AgentsConfig, type Workflow, type Project, type Trigger } from './config.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
|
-
|
|
7
|
+
trackers: Trackers;
|
|
7
8
|
log: (line: string) => void;
|
|
8
9
|
selfCommand: string[];
|
|
9
10
|
}
|
|
10
11
|
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
|
-
from?:
|
|
13
|
+
from?: string;
|
|
13
14
|
}): Promise<'verified' | 'changes' | 'parked'>;
|
|
14
|
-
|
|
15
|
+
type Job = {
|
|
15
16
|
card: Card;
|
|
16
17
|
trigger: Trigger;
|
|
17
18
|
workflow: string;
|
|
18
|
-
}
|
|
19
|
-
export declare function
|
|
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
|
-
from?:
|
|
28
|
+
from?: string;
|
|
23
29
|
}): Promise<number>;
|
|
30
|
+
export {};
|