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 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})`)
@@ -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;
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "td-barrage",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "TypeScript queue runner that loads a JSON task file, resolves dependencies, runs tasks through the OpenCode SDK, and resumes interrupted work.",
5
5
  "type": "module",
6
6
  "engines": {