td-barrage 0.1.4 → 0.1.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 +22 -16
- package/dist/client.d.ts +10 -10
- package/dist/client.js +63 -80
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/orchestrator.d.ts +2 -2
- package/package.json +7 -6
- package/dist/server.d.ts +0 -18
- package/dist/server.js +0 -64
package/README.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# td-barrage
|
|
2
2
|
|
|
3
|
-
A
|
|
3
|
+
A persistent, dependency-aware task queue for the pi coding agent. It loads a JSON task file, runs ready tasks through pi, saves state after every transition, and resumes interrupted work on the next invocation.
|
|
4
4
|
|
|
5
5
|
## Requirements
|
|
6
6
|
|
|
7
|
-
- Node.js
|
|
7
|
+
- Node.js 22.19 or newer
|
|
8
8
|
- npm
|
|
9
|
-
-
|
|
9
|
+
- pi authenticated with a model provider (`pi`, then `/login`), or a supported provider API key in the environment
|
|
10
10
|
|
|
11
11
|
## Install
|
|
12
12
|
|
|
@@ -62,7 +62,7 @@ Task files use a top-level `tasks` array. A bare array is also accepted when loa
|
|
|
62
62
|
Supported task fields:
|
|
63
63
|
|
|
64
64
|
- `id`: unique task id
|
|
65
|
-
- `prompt`: prompt sent to
|
|
65
|
+
- `prompt`: prompt sent to pi
|
|
66
66
|
- `dependsOn`: dependency ids
|
|
67
67
|
- `status`: `pending`, `running`, `done`, `failed`, or `blocked`
|
|
68
68
|
- `attempts`: persisted attempt counter
|
|
@@ -98,6 +98,13 @@ Exit codes:
|
|
|
98
98
|
- `2`: CLI usage or missing task-file error
|
|
99
99
|
- `75`: the run paused on a provider quota / rate-limit wall (see below)
|
|
100
100
|
|
|
101
|
+
## Pi sessions
|
|
102
|
+
|
|
103
|
+
Each task runs in its own in-memory pi agent session scoped to the task working
|
|
104
|
+
directory. Barrage uses pi's existing authentication and model settings. Terminal
|
|
105
|
+
provider errors from pi's final assistant state feed into the pause/resume flow
|
|
106
|
+
below. Sessions are disposed after their task prompt settles.
|
|
107
|
+
|
|
101
108
|
## Pausing on quota / rate limits
|
|
102
109
|
|
|
103
110
|
Running out of provider tokens is treated as a property of the run, not of any
|
|
@@ -111,12 +118,13 @@ task. When a prompt fails with a quota, rate-limit, or overload signal (HTTP
|
|
|
111
118
|
same wall, and
|
|
112
119
|
- exits `75` (`EX_TEMPFAIL`) with the reason on stderr.
|
|
113
120
|
|
|
114
|
-
Because state
|
|
115
|
-
re-running the same command once tokens are back
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
121
|
+
Because task state and workspace changes are persisted, resuming is just
|
|
122
|
+
re-running the same command once tokens are back. The pending task starts a fresh
|
|
123
|
+
in-memory pi session and can inspect the work already on disk; conversation
|
|
124
|
+
history is not restored. No failed or blocked task is recorded for the pause.
|
|
125
|
+
The distinct exit code lets a wrapper or scheduler tell "try again later" apart
|
|
126
|
+
from `1` ("something is broken"). A `Retry-After` header, when present, is
|
|
127
|
+
surfaced as a suggested wait in the pause message.
|
|
120
128
|
|
|
121
129
|
## Telemetry & output
|
|
122
130
|
|
|
@@ -128,7 +136,7 @@ are always reported, along with a closing summary and total elapsed time.
|
|
|
128
136
|
|
|
129
137
|
Verbosity flags:
|
|
130
138
|
|
|
131
|
-
- `--verbose, -v`: add per-step traces — the
|
|
139
|
+
- `--verbose, -v`: add per-step traces — the agent session id and the
|
|
132
140
|
success-check result for every task
|
|
133
141
|
- `--quiet, -q`: show only failures, blocks, and the final summary
|
|
134
142
|
- `--timestamps`: prefix each line with a wall-clock time
|
|
@@ -176,16 +184,14 @@ Integration tests are excluded by default. Run them with:
|
|
|
176
184
|
RUN_INTEGRATION=1 npm run test:integration
|
|
177
185
|
```
|
|
178
186
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
- `OPENCODE_BIN`: OpenCode executable, default `opencode`
|
|
182
|
-
- `OPENCODE_PORT`: server port, default `4096`
|
|
187
|
+
The integration test runs real pi sessions using your configured authentication
|
|
188
|
+
and model, so it may consume provider credits.
|
|
183
189
|
|
|
184
190
|
## Architecture
|
|
185
191
|
|
|
186
192
|
The orchestrator accepts its dependencies as arguments:
|
|
187
193
|
|
|
188
|
-
- `client`:
|
|
194
|
+
- `client`: pi-compatible agent client seam
|
|
189
195
|
- `fs`: filesystem seam
|
|
190
196
|
- `clock`: timer seam for timeout tests
|
|
191
197
|
- `log`: human or JSON event logger
|
package/dist/client.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
timeout?: number;
|
|
7
|
-
config?: unknown;
|
|
8
|
-
}
|
|
9
|
-
/** Imports an SDK module by specifier. Injectable so the export-selection logic is testable. */
|
|
1
|
+
import type { CreateAgentSessionOptions } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { AgentClient } from "./orchestrator.js";
|
|
3
|
+
/** Pi session settings Barrage allows callers to override. */
|
|
4
|
+
export type PiClientOptions = Pick<CreateAgentSessionOptions, "model" | "tools" | "excludeTools" | "noTools" | "thinkingLevel">;
|
|
5
|
+
/** Imports an SDK module by specifier. Injectable so the lazy wiring is testable. */
|
|
10
6
|
export type SdkImport = (specifier: string) => Promise<unknown>;
|
|
11
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Builds an orchestrator client backed by pi (`@earendil-works/pi-coding-agent`).
|
|
9
|
+
* Each task gets a one-shot in-memory session scoped to its working directory.
|
|
10
|
+
*/
|
|
11
|
+
export declare function createPiClient(options?: PiClientOptions, importSdk?: SdkImport): Promise<AgentClient>;
|
package/dist/client.js
CHANGED
|
@@ -1,98 +1,81 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
if (
|
|
10
|
-
|
|
1
|
+
const defaultImport = (specifier) => import(specifier);
|
|
2
|
+
const PI_SPECIFIER = "@earendil-works/pi-coding-agent";
|
|
3
|
+
/**
|
|
4
|
+
* Builds an orchestrator client backed by pi (`@earendil-works/pi-coding-agent`).
|
|
5
|
+
* Each task gets a one-shot in-memory session scoped to its working directory.
|
|
6
|
+
*/
|
|
7
|
+
export async function createPiClient(options = {}, importSdk = defaultImport) {
|
|
8
|
+
const sdk = (await importSdk(PI_SPECIFIER));
|
|
9
|
+
if (typeof sdk.createAgentSession !== "function") {
|
|
10
|
+
throw new Error("Unable to locate the pi createAgentSession export.");
|
|
11
11
|
}
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const created = (await sdk.createOpencode(options));
|
|
15
|
-
if (created.client) {
|
|
16
|
-
registerServerShutdown(created.server);
|
|
17
|
-
return wrapSdkClient(created.client);
|
|
18
|
-
}
|
|
12
|
+
if (typeof sdk.SessionManager?.inMemory !== "function") {
|
|
13
|
+
throw new Error("Unable to locate the pi SessionManager.inMemory export.");
|
|
19
14
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const Client = (sdk.OpenCode ?? sdk.Opencode ?? sdk.Client ?? sdk.default);
|
|
24
|
-
if (!Client) {
|
|
25
|
-
throw new Error("Unable to locate an OpenCode SDK client export.");
|
|
26
|
-
}
|
|
27
|
-
return new Client(options);
|
|
28
|
-
}
|
|
29
|
-
function registerServerShutdown(server) {
|
|
30
|
-
if (typeof server?.close !== "function")
|
|
31
|
-
return;
|
|
32
|
-
let closed = false;
|
|
33
|
-
const close = () => {
|
|
34
|
-
if (closed)
|
|
35
|
-
return;
|
|
36
|
-
closed = true;
|
|
37
|
-
server.close?.();
|
|
38
|
-
};
|
|
39
|
-
process.once("exit", close);
|
|
40
|
-
process.once("SIGINT", () => {
|
|
41
|
-
close();
|
|
42
|
-
process.exit(130);
|
|
43
|
-
});
|
|
44
|
-
process.once("SIGTERM", () => {
|
|
45
|
-
close();
|
|
46
|
-
process.exit(143);
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
function wrapSdkClient(client) {
|
|
50
|
-
const sessionDirs = new Map();
|
|
51
|
-
const raw = client;
|
|
15
|
+
const createAgentSession = sdk.createAgentSession;
|
|
16
|
+
const SessionManager = sdk.SessionManager;
|
|
17
|
+
const sessions = new Map();
|
|
52
18
|
return {
|
|
53
19
|
session: {
|
|
54
|
-
create: async (
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
|
|
20
|
+
create: async ({ cwd }) => {
|
|
21
|
+
const sessionOptions = {
|
|
22
|
+
...options,
|
|
23
|
+
cwd,
|
|
24
|
+
sessionManager: SessionManager.inMemory(cwd),
|
|
25
|
+
};
|
|
26
|
+
const { session } = await createAgentSession(sessionOptions);
|
|
27
|
+
sessions.set(session.sessionId, session);
|
|
28
|
+
return { id: session.sessionId };
|
|
62
29
|
},
|
|
63
30
|
prompt: async (sessionId, input) => {
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
};
|
|
68
|
-
if (input.signal?.aborted)
|
|
69
|
-
await abort();
|
|
70
|
-
input.signal?.addEventListener("abort", abort, { once: true });
|
|
31
|
+
const session = sessions.get(sessionId);
|
|
32
|
+
if (!session)
|
|
33
|
+
throw new Error(`Unknown pi session: ${sessionId}`);
|
|
71
34
|
try {
|
|
72
|
-
return await
|
|
73
|
-
sessionID: sessionId,
|
|
74
|
-
directory,
|
|
75
|
-
parts: [{ type: "text", text: input.prompt }],
|
|
76
|
-
}, { signal: input.signal });
|
|
35
|
+
return await runPrompt(session, input.prompt, input.signal);
|
|
77
36
|
}
|
|
78
37
|
finally {
|
|
79
|
-
|
|
38
|
+
sessions.delete(sessionId);
|
|
39
|
+
// Pi documents dispose() as best-effort cleanup, and its implementation
|
|
40
|
+
// deliberately does not throw. Keep that guarantee at the adapter seam.
|
|
41
|
+
try {
|
|
42
|
+
session.dispose();
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// Cleanup must not replace the prompt result or provider error.
|
|
46
|
+
}
|
|
80
47
|
}
|
|
81
48
|
},
|
|
82
49
|
},
|
|
83
50
|
};
|
|
84
51
|
}
|
|
85
|
-
function
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
52
|
+
async function runPrompt(session, prompt, signal) {
|
|
53
|
+
// Aborting an idle Pi session is a no-op, so never start work that its caller
|
|
54
|
+
// has already cancelled.
|
|
55
|
+
if (signal?.aborted)
|
|
56
|
+
throw abortError(signal);
|
|
57
|
+
const abort = () => {
|
|
58
|
+
void session.abort().catch(() => {
|
|
59
|
+
// The timeout/cancellation error belongs to the caller. An abort cleanup
|
|
60
|
+
// failure must not become an unhandled rejection or mask that error.
|
|
61
|
+
});
|
|
62
|
+
};
|
|
63
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
92
64
|
try {
|
|
93
|
-
|
|
65
|
+
await session.prompt(prompt);
|
|
66
|
+
}
|
|
67
|
+
finally {
|
|
68
|
+
signal?.removeEventListener("abort", abort);
|
|
94
69
|
}
|
|
95
|
-
|
|
96
|
-
|
|
70
|
+
// Pi encodes provider/runtime failures in the terminal assistant message and
|
|
71
|
+
// resolves prompt(). Its state exposes only the final error, after Pi's own
|
|
72
|
+
// retry loop has settled, so successful retries are not misclassified.
|
|
73
|
+
if (session.state.errorMessage) {
|
|
74
|
+
throw new Error(session.state.errorMessage);
|
|
97
75
|
}
|
|
98
76
|
}
|
|
77
|
+
function abortError(signal) {
|
|
78
|
+
if (signal.reason instanceof Error)
|
|
79
|
+
return signal.reason;
|
|
80
|
+
return new DOMException("The operation was aborted.", "AbortError");
|
|
81
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import { type GitCommitter } from "./git.js";
|
|
4
|
-
import { type
|
|
4
|
+
import { type AgentClient } from "./orchestrator.js";
|
|
5
5
|
export interface MainDeps {
|
|
6
6
|
fs?: typeof fs;
|
|
7
|
-
client?:
|
|
7
|
+
client?: AgentClient;
|
|
8
8
|
git?: GitCommitter;
|
|
9
9
|
stdout?: Pick<NodeJS.WritableStream, "write">;
|
|
10
10
|
stderr?: Pick<NodeJS.WritableStream, "write">;
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import { createRequire } from "node:module";
|
|
|
5
5
|
import process from "node:process";
|
|
6
6
|
import { pathToFileURL } from "node:url";
|
|
7
7
|
import { parseArgs } from "node:util";
|
|
8
|
-
import {
|
|
8
|
+
import { createPiClient } from "./client.js";
|
|
9
9
|
import { topologicalOrder } from "./deps.js";
|
|
10
10
|
import { createGitCommitter } from "./git.js";
|
|
11
11
|
import { createLogger } from "./log.js";
|
|
@@ -61,7 +61,7 @@ export async function main(argv = process.argv.slice(2), deps = {}) {
|
|
|
61
61
|
logger.event({ type: "dry_run", taskIds: ordered.map((task) => task.id) });
|
|
62
62
|
return 0;
|
|
63
63
|
}
|
|
64
|
-
const client = deps.client ?? (await
|
|
64
|
+
const client = deps.client ?? (await createPiClient());
|
|
65
65
|
const git = parsed.values["no-commit"] ? undefined : deps.git ?? createGitCommitter();
|
|
66
66
|
const summary = await runOrchestrator({
|
|
67
67
|
tasksPath,
|
package/dist/orchestrator.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { type TaskFs } from "./tasks.js";
|
|
|
4
4
|
import type { Logger } from "./log.js";
|
|
5
5
|
export declare const DEFAULT_TIMEOUT_MS: number;
|
|
6
6
|
export declare const DEFAULT_MAX_ATTEMPTS = 2;
|
|
7
|
-
export interface
|
|
7
|
+
export interface AgentClient {
|
|
8
8
|
session: {
|
|
9
9
|
create(input: {
|
|
10
10
|
cwd: string;
|
|
@@ -26,7 +26,7 @@ export interface OrchestratorFs extends TaskFs, SuccessFs {
|
|
|
26
26
|
}
|
|
27
27
|
export interface OrchestratorOptions {
|
|
28
28
|
tasksPath: string;
|
|
29
|
-
client:
|
|
29
|
+
client: AgentClient;
|
|
30
30
|
fs?: OrchestratorFs;
|
|
31
31
|
clock?: Clock;
|
|
32
32
|
log?: Logger;
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "td-barrage",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.5",
|
|
4
|
+
"description": "A persistent, dependency-aware task queue for the pi coding agent.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
7
|
-
"node": ">=
|
|
7
|
+
"node": ">=22.19"
|
|
8
8
|
},
|
|
9
9
|
"bin": {
|
|
10
10
|
"td-barrage": "dist/index.js"
|
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
"access": "public"
|
|
25
25
|
},
|
|
26
26
|
"scripts": {
|
|
27
|
-
"
|
|
27
|
+
"clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
|
|
28
|
+
"build": "npm run clean && tsc -p tsconfig.json",
|
|
28
29
|
"start": "node dist/index.js",
|
|
29
30
|
"dev": "tsx src/index.ts",
|
|
30
31
|
"test": "vitest run",
|
|
@@ -35,11 +36,11 @@
|
|
|
35
36
|
},
|
|
36
37
|
"dependencies": {
|
|
37
38
|
"@clack/prompts": "^1.4.0",
|
|
38
|
-
"@
|
|
39
|
+
"@earendil-works/pi-coding-agent": "^0.85.1",
|
|
39
40
|
"zod": "^3.25.0"
|
|
40
41
|
},
|
|
41
42
|
"devDependencies": {
|
|
42
|
-
"@types/node": "^
|
|
43
|
+
"@types/node": "^22.19.0",
|
|
43
44
|
"tsx": "^4.19.0",
|
|
44
45
|
"typescript": "^5.8.0",
|
|
45
46
|
"vitest": "^3.1.0"
|
package/dist/server.d.ts
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { spawn, type ChildProcess } from "node:child_process";
|
|
2
|
-
export interface StartServerOptions {
|
|
3
|
-
command?: string;
|
|
4
|
-
args?: string[];
|
|
5
|
-
port: number;
|
|
6
|
-
host?: string;
|
|
7
|
-
timeoutMs?: number;
|
|
8
|
-
pollMs?: number;
|
|
9
|
-
spawnProcess?: typeof spawn;
|
|
10
|
-
portOpen?: (host: string, port: number) => Promise<boolean>;
|
|
11
|
-
sleep?: (ms: number) => Promise<void>;
|
|
12
|
-
now?: () => number;
|
|
13
|
-
}
|
|
14
|
-
export interface StopServerOptions {
|
|
15
|
-
graceMs?: number;
|
|
16
|
-
}
|
|
17
|
-
export declare function startServer(options: StartServerOptions): Promise<ChildProcess>;
|
|
18
|
-
export declare function stopServer(child: Pick<ChildProcess, "kill" | "killed" | "exitCode" | "once">, options?: StopServerOptions): Promise<void>;
|
package/dist/server.js
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
// Integration-test harness for standing up a real `opencode serve` at a known
|
|
2
|
-
// port. Production does not use this: createOpenCodeClient (client.ts) lets the
|
|
3
|
-
// SDK's createOpencode spawn its own ephemeral server. These helpers exist so the
|
|
4
|
-
// integration test can connect a client to an explicit baseUrl.
|
|
5
|
-
import { spawn } from "node:child_process";
|
|
6
|
-
import net from "node:net";
|
|
7
|
-
export async function startServer(options) {
|
|
8
|
-
const command = options.command ?? "opencode";
|
|
9
|
-
const args = options.args ?? ["serve", "--port", String(options.port)];
|
|
10
|
-
const host = options.host ?? "127.0.0.1";
|
|
11
|
-
const timeoutMs = options.timeoutMs ?? 30_000;
|
|
12
|
-
const pollMs = options.pollMs ?? 100;
|
|
13
|
-
const portOpen = options.portOpen ?? isPortOpen;
|
|
14
|
-
const wait = options.sleep ?? sleep;
|
|
15
|
-
const now = options.now ?? Date.now;
|
|
16
|
-
const child = (options.spawnProcess ?? spawn)(command, args, {
|
|
17
|
-
stdio: "ignore",
|
|
18
|
-
detached: false,
|
|
19
|
-
});
|
|
20
|
-
const startedAt = now();
|
|
21
|
-
while (now() - startedAt < timeoutMs) {
|
|
22
|
-
if (await portOpen(host, options.port))
|
|
23
|
-
return child;
|
|
24
|
-
if (child.exitCode !== null)
|
|
25
|
-
throw new Error(`Server exited before port ${options.port} opened.`);
|
|
26
|
-
await wait(pollMs);
|
|
27
|
-
}
|
|
28
|
-
child.kill("SIGTERM");
|
|
29
|
-
throw new Error(`Timed out waiting for ${host}:${options.port}.`);
|
|
30
|
-
}
|
|
31
|
-
export async function stopServer(child, options = {}) {
|
|
32
|
-
const graceMs = options.graceMs ?? 5_000;
|
|
33
|
-
if (child.exitCode !== null)
|
|
34
|
-
return;
|
|
35
|
-
child.kill("SIGTERM");
|
|
36
|
-
await new Promise((resolve) => {
|
|
37
|
-
const timeout = setTimeout(() => {
|
|
38
|
-
if (child.exitCode === null)
|
|
39
|
-
child.kill("SIGKILL");
|
|
40
|
-
resolve();
|
|
41
|
-
}, graceMs);
|
|
42
|
-
child.once("exit", () => {
|
|
43
|
-
clearTimeout(timeout);
|
|
44
|
-
resolve();
|
|
45
|
-
});
|
|
46
|
-
});
|
|
47
|
-
}
|
|
48
|
-
async function isPortOpen(host, port) {
|
|
49
|
-
return new Promise((resolve) => {
|
|
50
|
-
const socket = net.createConnection({ host, port });
|
|
51
|
-
socket.once("connect", () => {
|
|
52
|
-
socket.destroy();
|
|
53
|
-
resolve(true);
|
|
54
|
-
});
|
|
55
|
-
socket.once("error", () => resolve(false));
|
|
56
|
-
socket.setTimeout(250, () => {
|
|
57
|
-
socket.destroy();
|
|
58
|
-
resolve(false);
|
|
59
|
-
});
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
function sleep(ms) {
|
|
63
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
64
|
-
}
|