@voidlabs.gg/voidshift-agents 0.0.0-stage → 0.4.0
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/LICENSE +202 -0
- package/README.md +134 -2
- package/dist/config.d.ts +60 -0
- package/dist/config.js +98 -0
- package/dist/control.d.ts +25 -0
- package/dist/control.js +712 -0
- package/dist/engine.d.ts +22 -0
- package/dist/engine.js +94 -0
- package/dist/git.d.ts +28 -0
- package/dist/git.js +105 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1171 -0
- package/dist/keychain.d.ts +3 -0
- package/dist/keychain.js +19 -0
- package/dist/launchd.d.ts +7 -0
- package/dist/launchd.js +68 -0
- package/dist/linear.d.ts +33 -0
- package/dist/linear.js +122 -0
- package/dist/manage.d.ts +18 -0
- package/dist/manage.js +120 -0
- package/dist/memory.d.ts +2 -0
- package/dist/memory.js +37 -0
- package/dist/names.d.ts +8 -0
- package/dist/names.js +26 -0
- package/dist/repos.d.ts +1 -0
- package/dist/repos.js +45 -0
- package/dist/runner.d.ts +23 -0
- package/dist/runner.js +392 -0
- package/dist/state.d.ts +62 -0
- package/dist/state.js +107 -0
- package/dist/types.d.ts +51 -0
- package/dist/types.js +3 -0
- package/dist/workflow.d.ts +50 -0
- package/dist/workflow.js +93 -0
- package/pack/vs-dev/SKILL.md +51 -0
- package/pack/vs-plan/SKILL.md +140 -0
- package/pack/vs-plan-review/SKILL.md +63 -0
- package/pack/vs-publish/SKILL.md +45 -0
- package/pack/vs-qa/SKILL.md +72 -0
- package/package.json +50 -4
package/dist/engine.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type ChildProcess } from 'node:child_process';
|
|
2
|
+
export type Engine = 'claude' | 'codex';
|
|
3
|
+
export interface RunOptions {
|
|
4
|
+
engine: Engine;
|
|
5
|
+
model?: string;
|
|
6
|
+
cwd: string;
|
|
7
|
+
prompt: string;
|
|
8
|
+
write: boolean;
|
|
9
|
+
logFile: string;
|
|
10
|
+
timeoutMs?: number;
|
|
11
|
+
vault?: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function engineArgs(o: RunOptions, outFile: string): {
|
|
14
|
+
cmd: string;
|
|
15
|
+
args: string[];
|
|
16
|
+
};
|
|
17
|
+
export declare function track(child: ChildProcess): void;
|
|
18
|
+
export declare function stopAgents(): Promise<void>;
|
|
19
|
+
export declare function runAgent(o: RunOptions): Promise<{
|
|
20
|
+
ok: boolean;
|
|
21
|
+
output: string;
|
|
22
|
+
}>;
|
package/dist/engine.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { createWriteStream, mkdtempSync, readFileSync, rmSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
// Tools a write step may use under `claude -p`. Anything else is denied,
|
|
6
|
+
// since nobody is there to approve it.
|
|
7
|
+
const CLAUDE_WRITE = ['Read', 'Glob', 'Grep', 'Edit', 'Write', 'Bash'];
|
|
8
|
+
const CLAUDE_READ = ['Read', 'Glob', 'Grep', 'Bash(git diff:*)', 'Bash(git log:*)', 'Bash(git show:*)', 'Bash(npm test:*)', 'Bash(npm run:*)', 'Bash(pnpm:*)', 'Bash(yarn:*)', 'Bash(npx:*)', 'Bash(node:*)', 'Bash(ls:*)', 'Bash(rg:*)'];
|
|
9
|
+
export function engineArgs(o, outFile) {
|
|
10
|
+
if (o.engine === 'claude') {
|
|
11
|
+
const vaultTools = o.vault ? [`Edit(/${o.vault}/**)`, `Write(/${o.vault}/**)`] : [];
|
|
12
|
+
const args = ['-p', '--output-format', 'text', '--allowedTools', [...(o.write ? CLAUDE_WRITE : CLAUDE_READ), ...vaultTools].join(',')];
|
|
13
|
+
if (o.vault)
|
|
14
|
+
args.push('--add-dir', o.vault);
|
|
15
|
+
if (o.write)
|
|
16
|
+
args.push('--permission-mode', 'acceptEdits');
|
|
17
|
+
if (o.model)
|
|
18
|
+
args.push('--model', o.model);
|
|
19
|
+
return { cmd: 'claude', args };
|
|
20
|
+
}
|
|
21
|
+
const args = ['exec', '-C', o.cwd, '-s', o.write ? 'workspace-write' : 'read-only', '-o', outFile];
|
|
22
|
+
// A read-only sandbox cannot write anywhere, so only write steps get the vault.
|
|
23
|
+
if (o.vault && o.write)
|
|
24
|
+
args.push('--add-dir', o.vault);
|
|
25
|
+
if (o.model)
|
|
26
|
+
args.push('-m', o.model);
|
|
27
|
+
args.push('-');
|
|
28
|
+
return { cmd: 'codex', args };
|
|
29
|
+
}
|
|
30
|
+
// Agent processes running now, so a cancel can stop them.
|
|
31
|
+
const children = new Set();
|
|
32
|
+
// Registers a child process so stopAgents() can stop it.
|
|
33
|
+
export function track(child) {
|
|
34
|
+
children.add(child);
|
|
35
|
+
child.on('close', () => children.delete(child));
|
|
36
|
+
}
|
|
37
|
+
// Asks every running agent to stop and waits up to 2 seconds for them to go,
|
|
38
|
+
// then kills the rest. The caller exits right after, so nothing may linger.
|
|
39
|
+
export async function stopAgents() {
|
|
40
|
+
for (const c of children)
|
|
41
|
+
c.kill('SIGTERM');
|
|
42
|
+
const deadline = Date.now() + 2000;
|
|
43
|
+
while (children.size && Date.now() < deadline)
|
|
44
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
45
|
+
for (const c of children)
|
|
46
|
+
if (c.exitCode === null && c.signalCode === null)
|
|
47
|
+
c.kill('SIGKILL');
|
|
48
|
+
}
|
|
49
|
+
// Runs one agent step with the prompt on stdin. Resolves with the agent's
|
|
50
|
+
// final text; everything it prints also goes to the log file.
|
|
51
|
+
export function runAgent(o) {
|
|
52
|
+
const dir = mkdtempSync(join(tmpdir(), 'voidshift-run-'));
|
|
53
|
+
const outFile = join(dir, 'last.txt');
|
|
54
|
+
const { cmd, args } = engineArgs(o, outFile);
|
|
55
|
+
const log = createWriteStream(o.logFile, { flags: 'a' });
|
|
56
|
+
log.write(`$ ${cmd} ${args.join(' ')}\n\n`);
|
|
57
|
+
return new Promise((resolve) => {
|
|
58
|
+
// The runner recalls memory for the card itself; the recall hook would
|
|
59
|
+
// match on the whole prompt, skill text included.
|
|
60
|
+
const env = { ...process.env, VOIDSHIFT_RECALL: 'off' };
|
|
61
|
+
const child = spawn(cmd, args, { cwd: o.cwd, env, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
62
|
+
track(child);
|
|
63
|
+
let stdout = '';
|
|
64
|
+
child.stdout.on('data', (d) => {
|
|
65
|
+
stdout += d;
|
|
66
|
+
log.write(d);
|
|
67
|
+
});
|
|
68
|
+
child.stderr.on('data', (d) => log.write(d));
|
|
69
|
+
const timer = setTimeout(() => child.kill('SIGTERM'), o.timeoutMs ?? 45 * 60_000);
|
|
70
|
+
child.on('error', (err) => {
|
|
71
|
+
clearTimeout(timer);
|
|
72
|
+
children.delete(child);
|
|
73
|
+
log.end(`\n${err.message}\n`);
|
|
74
|
+
resolve({ ok: false, output: err.message });
|
|
75
|
+
});
|
|
76
|
+
child.on('close', (code) => {
|
|
77
|
+
clearTimeout(timer);
|
|
78
|
+
children.delete(child);
|
|
79
|
+
log.end(`\nexit ${code}\n`);
|
|
80
|
+
let output = stdout;
|
|
81
|
+
if (o.engine === 'codex') {
|
|
82
|
+
try {
|
|
83
|
+
output = readFileSync(outFile, 'utf8');
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// fall back to stdout
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
rmSync(dir, { recursive: true, force: true });
|
|
90
|
+
resolve({ ok: code === 0, output });
|
|
91
|
+
});
|
|
92
|
+
child.stdin.end(o.prompt);
|
|
93
|
+
});
|
|
94
|
+
}
|
package/dist/git.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export interface Out {
|
|
2
|
+
ok: boolean;
|
|
3
|
+
code: number | null;
|
|
4
|
+
stdout: string;
|
|
5
|
+
stderr: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function sh(cmd: string, args: string[], cwd: string, timeoutMs?: number): Out;
|
|
8
|
+
export declare function defaultBranch(repo: string): string;
|
|
9
|
+
export declare function ensureWorktree(repo: string, path: string, branch: string, base: string): void;
|
|
10
|
+
export declare function commitsAhead(worktree: string, base: string): number;
|
|
11
|
+
export declare function push(worktree: string, branch: string): void;
|
|
12
|
+
export declare function ensureDraftPr(worktree: string, branch: string, base: string, title: string, body: string): string;
|
|
13
|
+
export declare function markReady(worktree: string, branch: string): boolean;
|
|
14
|
+
export interface CheckSummary {
|
|
15
|
+
failed: {
|
|
16
|
+
name: string;
|
|
17
|
+
link: string;
|
|
18
|
+
}[];
|
|
19
|
+
pending: string[];
|
|
20
|
+
passed: number;
|
|
21
|
+
}
|
|
22
|
+
export declare function summarizeChecks(text: string): CheckSummary;
|
|
23
|
+
export declare function ciReport(s: CheckSummary): string;
|
|
24
|
+
export declare function waitForChecks(worktree: string, branch: string, timeoutMs?: number): Promise<{
|
|
25
|
+
verdict: 'SHIP' | 'FIX' | 'SKIP';
|
|
26
|
+
report: string;
|
|
27
|
+
}>;
|
|
28
|
+
export declare function resetWorktree(worktree: string): void;
|
package/dist/git.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { track } from "./engine.js";
|
|
4
|
+
export function sh(cmd, args, cwd, timeoutMs = 120_000) {
|
|
5
|
+
const r = spawnSync(cmd, args, { cwd, encoding: 'utf8', timeout: timeoutMs });
|
|
6
|
+
return { ok: r.status === 0, code: r.status, stdout: (r.stdout ?? '').trim(), stderr: (r.stderr ?? '').trim() };
|
|
7
|
+
}
|
|
8
|
+
function must(out, what) {
|
|
9
|
+
if (!out.ok)
|
|
10
|
+
throw new Error(`${what} failed: ${out.stderr || out.stdout}`);
|
|
11
|
+
return out.stdout;
|
|
12
|
+
}
|
|
13
|
+
export function defaultBranch(repo) {
|
|
14
|
+
const ref = sh('git', ['symbolic-ref', '--short', 'refs/remotes/origin/HEAD'], repo);
|
|
15
|
+
if (ref.ok)
|
|
16
|
+
return ref.stdout.replace(/^origin\//, '');
|
|
17
|
+
for (const b of ['main', 'master'])
|
|
18
|
+
if (sh('git', ['rev-parse', '--verify', `origin/${b}`], repo).ok)
|
|
19
|
+
return b;
|
|
20
|
+
throw new Error(`cannot tell the default branch of ${repo}`);
|
|
21
|
+
}
|
|
22
|
+
// A worktree per card on the card's branch, so the developer's checkout is
|
|
23
|
+
// never touched.
|
|
24
|
+
export function ensureWorktree(repo, path, branch, base) {
|
|
25
|
+
must(sh('git', ['fetch', 'origin'], repo), 'git fetch');
|
|
26
|
+
if (existsSync(path)) {
|
|
27
|
+
sh('git', ['pull', '--ff-only', 'origin', branch], path);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const remote = sh('git', ['rev-parse', '--verify', `origin/${branch}`], repo).ok;
|
|
31
|
+
const local = sh('git', ['rev-parse', '--verify', branch], repo).ok;
|
|
32
|
+
const args = remote
|
|
33
|
+
? ['worktree', 'add', '-B', branch, path, `origin/${branch}`]
|
|
34
|
+
: local
|
|
35
|
+
? ['worktree', 'add', path, branch]
|
|
36
|
+
: ['worktree', 'add', '-b', branch, path, `origin/${base}`];
|
|
37
|
+
must(sh('git', args, repo), 'git worktree add');
|
|
38
|
+
}
|
|
39
|
+
export function commitsAhead(worktree, base) {
|
|
40
|
+
const out = sh('git', ['rev-list', '--count', `origin/${base}..HEAD`], worktree);
|
|
41
|
+
return out.ok ? Number(out.stdout) : 0;
|
|
42
|
+
}
|
|
43
|
+
export function push(worktree, branch) {
|
|
44
|
+
must(sh('git', ['push', '-u', 'origin', branch], worktree), 'git push');
|
|
45
|
+
}
|
|
46
|
+
export function ensureDraftPr(worktree, branch, base, title, body) {
|
|
47
|
+
const view = sh('gh', ['pr', 'view', branch, '--json', 'url', '-q', '.url'], worktree);
|
|
48
|
+
if (view.ok && view.stdout)
|
|
49
|
+
return view.stdout;
|
|
50
|
+
return must(sh('gh', ['pr', 'create', '--draft', '--base', base, '--head', branch, '--title', title, '--body', body], worktree), 'gh pr create');
|
|
51
|
+
}
|
|
52
|
+
export function markReady(worktree, branch) {
|
|
53
|
+
return sh('gh', ['pr', 'ready', branch], worktree).ok;
|
|
54
|
+
}
|
|
55
|
+
// Parses plain `gh pr checks` output: name, state, duration, link per line,
|
|
56
|
+
// tab separated. Works on old gh versions that lack --json.
|
|
57
|
+
export function summarizeChecks(text) {
|
|
58
|
+
const out = { failed: [], pending: [], passed: 0 };
|
|
59
|
+
for (const line of text.split('\n')) {
|
|
60
|
+
const [name, state, , link] = line.split('\t');
|
|
61
|
+
if (!name || !state)
|
|
62
|
+
continue;
|
|
63
|
+
if (state === 'pass' || state === 'skipping')
|
|
64
|
+
out.passed++;
|
|
65
|
+
else if (state === 'pending')
|
|
66
|
+
out.pending.push(name.trim());
|
|
67
|
+
else
|
|
68
|
+
out.failed.push({ name: name.trim(), link: (link ?? '').trim() });
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
// One short line per failed check, for the card and for Dev.
|
|
73
|
+
export function ciReport(s) {
|
|
74
|
+
const lines = s.failed.map((f) => `- ${f.name}${f.link ? `: ${f.link}` : ''}`);
|
|
75
|
+
const pending = s.pending.length ? `\nStill running: ${s.pending.join(', ')}.` : '';
|
|
76
|
+
return `CI failed:\n${lines.join('\n')}${pending}`;
|
|
77
|
+
}
|
|
78
|
+
// Waits for CI on the PR, then reads the final state once. Repos without CI
|
|
79
|
+
// pass through as SKIP.
|
|
80
|
+
export function waitForChecks(worktree, branch, timeoutMs = 30 * 60_000) {
|
|
81
|
+
return new Promise((resolve) => {
|
|
82
|
+
const child = spawn('gh', ['pr', 'checks', branch, '--watch', '--interval', '30'], { cwd: worktree, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
83
|
+
track(child);
|
|
84
|
+
let text = '';
|
|
85
|
+
child.stdout.on('data', (d) => (text += d));
|
|
86
|
+
child.stderr.on('data', (d) => (text += d));
|
|
87
|
+
const timer = setTimeout(() => child.kill('SIGTERM'), timeoutMs);
|
|
88
|
+
child.on('close', () => {
|
|
89
|
+
clearTimeout(timer);
|
|
90
|
+
if (/no checks reported/i.test(text))
|
|
91
|
+
return resolve({ verdict: 'SKIP', report: 'No CI checks on this repo.' });
|
|
92
|
+
const summary = summarizeChecks(sh('gh', ['pr', 'checks', branch], worktree).stdout);
|
|
93
|
+
if (!summary.failed.length && !summary.pending.length)
|
|
94
|
+
return resolve({ verdict: 'SHIP', report: `CI passed (${summary.passed} checks).` });
|
|
95
|
+
if (!summary.failed.length)
|
|
96
|
+
return resolve({ verdict: 'FIX', report: `CI did not finish in time. Still running: ${summary.pending.join(', ')}.` });
|
|
97
|
+
resolve({ verdict: 'FIX', report: ciReport(summary) });
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
// QA runs with write access so it can start the app; nothing it changes stays.
|
|
102
|
+
export function resetWorktree(worktree) {
|
|
103
|
+
sh('git', ['reset', '--hard', 'HEAD'], worktree);
|
|
104
|
+
sh('git', ['clean', '-fd'], worktree);
|
|
105
|
+
}
|
package/dist/index.d.ts
ADDED