td-barrage 0.1.2 → 0.1.3
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 +9 -0
- package/dist/git.d.ts +28 -0
- package/dist/git.js +47 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4 -0
- package/dist/log.d.ts +11 -0
- package/dist/log.js +18 -0
- package/dist/orchestrator.d.ts +5 -0
- package/dist/orchestrator.js +32 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -90,6 +90,7 @@ Options:
|
|
|
90
90
|
- `--cwd <path>`: working directory for tasks that do not set their own
|
|
91
91
|
- `--results-dir <path>`: where result files are expected, default `.queue/results`
|
|
92
92
|
- `--dry-run`: print the topological plan (one id per line) and exit without running
|
|
93
|
+
- `--no-commit`: skip the git commit stage that otherwise runs after each successful task
|
|
93
94
|
- `--json`: emit newline-delimited JSON events instead of human output
|
|
94
95
|
|
|
95
96
|
Exit codes:
|
|
@@ -128,6 +129,14 @@ stays easy to pipe.
|
|
|
128
129
|
|
|
129
130
|
`result_json` succeeds when the configured JSON file parses and contains `"status": "ok"`.
|
|
130
131
|
|
|
132
|
+
## Commits
|
|
133
|
+
|
|
134
|
+
After a task passes its success check, the orchestrator stages everything with
|
|
135
|
+
`git add -A` and commits it in the task's cwd with the message `feat: <id>`. A
|
|
136
|
+
clean working tree (nothing to commit) is a quiet no-op, and a commit that fails
|
|
137
|
+
outright is reported as a warning but never flips the task back to failed — the
|
|
138
|
+
task already succeeded. Pass `--no-commit` to skip this stage entirely.
|
|
139
|
+
|
|
131
140
|
## Recovery
|
|
132
141
|
|
|
133
142
|
On startup, any task persisted as `running` is reset to `pending`, annotated in `recoveryNotes`, and retried. The `attempts` counter is preserved, so a task that crashed mid-run still counts that attempt.
|
package/dist/git.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export interface GitExecResult {
|
|
2
|
+
code: number;
|
|
3
|
+
stdout: string;
|
|
4
|
+
stderr: string;
|
|
5
|
+
}
|
|
6
|
+
/** Runs `git <args>` in `cwd` and resolves with its exit code and output (never rejects). */
|
|
7
|
+
export type GitExec = (args: string[], cwd: string) => Promise<GitExecResult>;
|
|
8
|
+
export interface CommitInput {
|
|
9
|
+
cwd: string;
|
|
10
|
+
message: string;
|
|
11
|
+
}
|
|
12
|
+
export interface CommitResult {
|
|
13
|
+
/** "committed" — a new commit was made; "nothing" — clean tree, nothing staged; "error" — git failed. */
|
|
14
|
+
status: "committed" | "nothing" | "error";
|
|
15
|
+
/** Short SHA of the new commit, when status is "committed". */
|
|
16
|
+
commit?: string;
|
|
17
|
+
/** Human-readable detail, primarily for the "error" status. */
|
|
18
|
+
detail?: string;
|
|
19
|
+
}
|
|
20
|
+
export interface GitCommitter {
|
|
21
|
+
commit(input: CommitInput): Promise<CommitResult>;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Stages every change with `git add -A` and commits it. A clean working tree
|
|
25
|
+
* (nothing to commit) is reported as "nothing" rather than an error, since a
|
|
26
|
+
* task can legitimately succeed without touching tracked files.
|
|
27
|
+
*/
|
|
28
|
+
export declare function createGitCommitter(exec?: GitExec): GitCommitter;
|
package/dist/git.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
/**
|
|
3
|
+
* Stages every change with `git add -A` and commits it. A clean working tree
|
|
4
|
+
* (nothing to commit) is reported as "nothing" rather than an error, since a
|
|
5
|
+
* task can legitimately succeed without touching tracked files.
|
|
6
|
+
*/
|
|
7
|
+
export function createGitCommitter(exec = defaultExec) {
|
|
8
|
+
return {
|
|
9
|
+
async commit({ cwd, message }) {
|
|
10
|
+
const add = await exec(["add", "-A"], cwd);
|
|
11
|
+
if (add.code !== 0) {
|
|
12
|
+
return { status: "error", detail: detailOf(add) || "git add failed" };
|
|
13
|
+
}
|
|
14
|
+
const commit = await exec(["commit", "-m", message], cwd);
|
|
15
|
+
if (commit.code === 0) {
|
|
16
|
+
const head = await exec(["rev-parse", "--short", "HEAD"], cwd);
|
|
17
|
+
return { status: "committed", commit: head.code === 0 ? head.stdout.trim() : undefined };
|
|
18
|
+
}
|
|
19
|
+
if (isNothingToCommit(commit)) {
|
|
20
|
+
return { status: "nothing" };
|
|
21
|
+
}
|
|
22
|
+
return { status: "error", detail: detailOf(commit) || `git commit exited with code ${commit.code}` };
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
// `git commit` exits non-zero with this phrasing when the index has no staged
|
|
27
|
+
// changes — a no-op we treat as success, not failure.
|
|
28
|
+
function isNothingToCommit(result) {
|
|
29
|
+
const text = `${result.stdout}\n${result.stderr}`;
|
|
30
|
+
return /nothing to commit|no changes added to commit|nothing added to commit/i.test(text);
|
|
31
|
+
}
|
|
32
|
+
function detailOf(result) {
|
|
33
|
+
return (result.stderr.trim() || result.stdout.trim()).split("\n")[0] ?? "";
|
|
34
|
+
}
|
|
35
|
+
const defaultExec = (args, cwd) => new Promise((resolve) => {
|
|
36
|
+
execFile("git", args, { cwd }, (error, stdout, stderr) => {
|
|
37
|
+
if (!error) {
|
|
38
|
+
resolve({ code: 0, stdout: String(stdout), stderr: String(stderr) });
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
// A process that exits non-zero surfaces a numeric `code`; a spawn
|
|
42
|
+
// failure (e.g. git not installed) surfaces a string code like "ENOENT",
|
|
43
|
+
// in which case we fall back to 1 and lean on the error message.
|
|
44
|
+
const code = typeof error.code === "number" ? error.code : 1;
|
|
45
|
+
resolve({ code, stdout: String(stdout), stderr: String(stderr) || error.message });
|
|
46
|
+
});
|
|
47
|
+
});
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
|
+
import { type GitCommitter } from "./git.js";
|
|
3
4
|
import { type OpenCodeClient } from "./orchestrator.js";
|
|
4
5
|
export interface MainDeps {
|
|
5
6
|
fs?: typeof fs;
|
|
6
7
|
client?: OpenCodeClient;
|
|
8
|
+
git?: GitCommitter;
|
|
7
9
|
stdout?: Pick<NodeJS.WritableStream, "write">;
|
|
8
10
|
stderr?: Pick<NodeJS.WritableStream, "write">;
|
|
9
11
|
}
|
package/dist/index.js
CHANGED
|
@@ -7,6 +7,7 @@ import { pathToFileURL } from "node:url";
|
|
|
7
7
|
import { parseArgs } from "node:util";
|
|
8
8
|
import { createOpenCodeClient } from "./client.js";
|
|
9
9
|
import { topologicalOrder } from "./deps.js";
|
|
10
|
+
import { createGitCommitter } from "./git.js";
|
|
10
11
|
import { createLogger } from "./log.js";
|
|
11
12
|
import { defaultResultsDir, runOrchestrator } from "./orchestrator.js";
|
|
12
13
|
import { loadTasks } from "./tasks.js";
|
|
@@ -23,6 +24,7 @@ export async function main(argv = process.argv.slice(2), deps = {}) {
|
|
|
23
24
|
"results-dir": { type: "string" },
|
|
24
25
|
json: { type: "boolean", default: false },
|
|
25
26
|
"dry-run": { type: "boolean", default: false },
|
|
27
|
+
"no-commit": { type: "boolean", default: false },
|
|
26
28
|
verbose: { type: "boolean", short: "v", default: false },
|
|
27
29
|
quiet: { type: "boolean", short: "q", default: false },
|
|
28
30
|
color: { type: "boolean", default: false },
|
|
@@ -62,6 +64,7 @@ export async function main(argv = process.argv.slice(2), deps = {}) {
|
|
|
62
64
|
return 0;
|
|
63
65
|
}
|
|
64
66
|
const client = deps.client ?? (await createOpenCodeClient());
|
|
67
|
+
const git = parsed.values["no-commit"] ? undefined : deps.git ?? createGitCommitter();
|
|
65
68
|
const summary = await runOrchestrator({
|
|
66
69
|
tasksPath,
|
|
67
70
|
client,
|
|
@@ -70,6 +73,7 @@ export async function main(argv = process.argv.slice(2), deps = {}) {
|
|
|
70
73
|
cwd,
|
|
71
74
|
resultsDir,
|
|
72
75
|
maxAttempts,
|
|
76
|
+
git,
|
|
73
77
|
version: readVersion(),
|
|
74
78
|
});
|
|
75
79
|
return summary.failed > 0 || summary.blocked > 0 ? 1 : 0;
|
package/dist/log.d.ts
CHANGED
|
@@ -36,6 +36,17 @@ export type LogEvent = {
|
|
|
36
36
|
durationMs: number;
|
|
37
37
|
done: number;
|
|
38
38
|
total: number;
|
|
39
|
+
} | {
|
|
40
|
+
type: "task_committed";
|
|
41
|
+
taskId: string;
|
|
42
|
+
message: string;
|
|
43
|
+
status: "committed" | "nothing";
|
|
44
|
+
commit?: string;
|
|
45
|
+
} | {
|
|
46
|
+
type: "task_commit_failed";
|
|
47
|
+
taskId: string;
|
|
48
|
+
message: string;
|
|
49
|
+
detail: string;
|
|
39
50
|
} | {
|
|
40
51
|
type: "task_failed";
|
|
41
52
|
taskId: string;
|
package/dist/log.js
CHANGED
|
@@ -8,6 +8,8 @@ const EVENT_LEVEL = {
|
|
|
8
8
|
task_session: 2,
|
|
9
9
|
task_check: 2,
|
|
10
10
|
task_done: 1,
|
|
11
|
+
task_committed: 1,
|
|
12
|
+
task_commit_failed: 0,
|
|
11
13
|
task_failed: 0,
|
|
12
14
|
task_blocked: 0,
|
|
13
15
|
summary: 0,
|
|
@@ -105,6 +107,16 @@ function createPrettyRenderer(output, s) {
|
|
|
105
107
|
clack.success(msg, opts);
|
|
106
108
|
return;
|
|
107
109
|
}
|
|
110
|
+
case "task_committed": {
|
|
111
|
+
const detail = event.status === "nothing"
|
|
112
|
+
? s.dim("no changes to commit")
|
|
113
|
+
: `${s.dim("committed")} ${event.commit ? s.cyan(event.commit) : ""} ${s.dim(event.message)}`;
|
|
114
|
+
clack.message(detail, opts);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
case "task_commit_failed":
|
|
118
|
+
clack.warn(`${s.bold(event.taskId)} ${s.yellow("commit failed")} ${s.dim(event.detail)}`, opts);
|
|
119
|
+
return;
|
|
108
120
|
case "task_failed": {
|
|
109
121
|
const tail = event.willRetry
|
|
110
122
|
? s.yellow(`will retry (attempt ${event.attempt}/${event.maxAttempts})`)
|
|
@@ -181,6 +193,12 @@ function formatHuman(event, s) {
|
|
|
181
193
|
return s.dim(` ↳ check ${event.strategy} → ${event.ok ? "ok" : "fail"}`);
|
|
182
194
|
case "task_done":
|
|
183
195
|
return `${s.green("✔")} ${s.bold(event.taskId)} ${s.green("done")} ${s.dim(`in ${fmtDuration(event.durationMs)} · ${event.done}/${event.total} complete`)}`;
|
|
196
|
+
case "task_committed":
|
|
197
|
+
return event.status === "nothing"
|
|
198
|
+
? s.dim(" ↳ no changes to commit")
|
|
199
|
+
: s.dim(` ↳ committed ${event.commit ? `${event.commit} ` : ""}${event.message}`);
|
|
200
|
+
case "task_commit_failed":
|
|
201
|
+
return `${s.yellow("⚠")} ${s.bold(event.taskId)} ${s.yellow("commit failed")} ${s.dim(event.detail)}`;
|
|
184
202
|
case "task_failed": {
|
|
185
203
|
const tail = event.willRetry
|
|
186
204
|
? s.yellow(`will retry (attempt ${event.attempt}/${event.maxAttempts})`)
|
package/dist/orchestrator.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { GitCommitter } from "./git.js";
|
|
1
2
|
import { type SuccessFs } from "./success.js";
|
|
2
3
|
import { type TaskFs } from "./tasks.js";
|
|
3
4
|
import type { Logger } from "./log.js";
|
|
@@ -34,6 +35,10 @@ export interface OrchestratorOptions {
|
|
|
34
35
|
maxAttempts?: number;
|
|
35
36
|
timeoutMs?: number;
|
|
36
37
|
version?: string;
|
|
38
|
+
/** Commit the working tree after each task succeeds. Defaults to true. */
|
|
39
|
+
commit?: boolean;
|
|
40
|
+
/** Commits the working tree after each successful task. Omit to skip committing entirely. */
|
|
41
|
+
git?: GitCommitter;
|
|
37
42
|
}
|
|
38
43
|
export interface Summary {
|
|
39
44
|
total: number;
|
package/dist/orchestrator.js
CHANGED
|
@@ -128,6 +128,38 @@ async function runTask(task, tasks, options, queueFs) {
|
|
|
128
128
|
});
|
|
129
129
|
}
|
|
130
130
|
await saveTasks(options.tasksPath, { tasks }, queueFs);
|
|
131
|
+
// Commit the work the task produced. This runs after the save above so the
|
|
132
|
+
// commit captures the task file in its final "done" state. A failed commit
|
|
133
|
+
// is reported but never flips the task back to failed — the task itself
|
|
134
|
+
// already passed its success check. Skipped when no committer is wired in.
|
|
135
|
+
if (task.status === "done" && options.commit !== false && options.git) {
|
|
136
|
+
await commitTask(task, options.git, options.log, cwd);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
async function commitTask(task, committer, log, cwd) {
|
|
140
|
+
const message = `feat: ${task.id}`;
|
|
141
|
+
try {
|
|
142
|
+
const result = await committer.commit({ cwd, message });
|
|
143
|
+
if (result.status === "error") {
|
|
144
|
+
log?.event({ type: "task_commit_failed", taskId: task.id, message, detail: result.detail ?? "" });
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
log?.event({
|
|
148
|
+
type: "task_committed",
|
|
149
|
+
taskId: task.id,
|
|
150
|
+
message,
|
|
151
|
+
status: result.status,
|
|
152
|
+
commit: result.commit,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
log?.event({
|
|
157
|
+
type: "task_commit_failed",
|
|
158
|
+
taskId: task.id,
|
|
159
|
+
message,
|
|
160
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
161
|
+
});
|
|
162
|
+
}
|
|
131
163
|
}
|
|
132
164
|
function countDone(tasks) {
|
|
133
165
|
return tasks.reduce((total, task) => (task.status === "done" ? total + 1 : total), 0);
|
package/package.json
CHANGED