@inteeka/task-cli 0.2.4 → 0.2.6

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/dist/cli.js CHANGED
@@ -191,6 +191,9 @@ var CLI_AUDIT_ACTIONS = Object.freeze([
191
191
  "cli.run.tests_failed",
192
192
  "cli.run.pr_opened",
193
193
  "cli.run.pr_failed",
194
+ "cli.run.push_failed",
195
+ "cli.run.resumed",
196
+ "cli.work.pr_recovered",
194
197
  "cli.schedule.created",
195
198
  "cli.schedule.paused",
196
199
  "cli.schedule.resumed",
@@ -224,6 +227,13 @@ var c = {
224
227
  var CliError = class extends Error {
225
228
  code;
226
229
  hint;
230
+ /**
231
+ * Discriminator for the multi-work loop's between-iteration cleanup.
232
+ * `post_push` means the per-ticket branch is on origin (with a commit)
233
+ * and should be preserved for `task resume`. Pre-push failures (or
234
+ * unset / 'pre_push') let the loop purge the branch as usual.
235
+ */
236
+ phase;
227
237
  constructor(code, message, hint) {
228
238
  super(message);
229
239
  this.code = code;
@@ -988,9 +998,243 @@ function printTable(headers, rows) {
988
998
  }
989
999
 
990
1000
  // src/commands/status.ts
1001
+ import { execFileSync as execFileSync3 } from "child_process";
1002
+
1003
+ // src/git/branch.ts
1004
+ import { execFileSync as execFileSync2 } from "child_process";
1005
+
1006
+ // src/git/commit.ts
991
1007
  import { execFileSync } from "child_process";
1008
+ function commitOnly(args) {
1009
+ execFileSync("git", ["add", "-A"], { cwd: args.cwd });
1010
+ const statusRaw = execFileSync("git", ["status", "--porcelain"], {
1011
+ cwd: args.cwd,
1012
+ encoding: "utf8"
1013
+ });
1014
+ if (!statusRaw.trim()) {
1015
+ throw new Error("No changes to commit (empty diff)");
1016
+ }
1017
+ execFileSync("git", ["commit", "-m", args.message], { cwd: args.cwd });
1018
+ const sha = execFileSync("git", ["rev-parse", "HEAD"], {
1019
+ cwd: args.cwd,
1020
+ encoding: "utf8"
1021
+ }).trim();
1022
+ return { sha };
1023
+ }
1024
+ function currentBranch(cwd) {
1025
+ try {
1026
+ return execFileSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
1027
+ cwd,
1028
+ encoding: "utf8"
1029
+ }).trim();
1030
+ } catch {
1031
+ return "HEAD";
1032
+ }
1033
+ }
1034
+
1035
+ // src/git/branch.ts
1036
+ var VALID_BRANCH = /^[A-Za-z0-9._/-]{1,200}$/;
1037
+ var TICKET_BRANCH = /^task\/[a-z0-9-]{1,80}$/;
1038
+ function assertValidBranchName(branch) {
1039
+ if (!VALID_BRANCH.test(branch) || branch.includes("..") || branch.startsWith("/") || branch.endsWith("/")) {
1040
+ throw new CliError(
1041
+ CLI_EXIT_CODES.MISCONFIGURATION,
1042
+ `Invalid branch name: ${branch}`,
1043
+ 'Branch names must contain only [A-Za-z0-9._/-], no "..", and no leading/trailing slash.'
1044
+ );
1045
+ }
1046
+ }
1047
+ function isWorkingTreeClean(cwd) {
1048
+ const out = execFileSync2("git", ["status", "--porcelain"], {
1049
+ cwd,
1050
+ encoding: "utf8"
1051
+ });
1052
+ return out.trim().length === 0;
1053
+ }
1054
+ function assertBaseBranch(cwd, expected) {
1055
+ assertValidBranchName(expected);
1056
+ const current = currentBranch(cwd);
1057
+ if (current !== expected) {
1058
+ throw new CliError(
1059
+ CLI_EXIT_CODES.MISCONFIGURATION,
1060
+ `task work requires branch "${expected}" but you're on "${current}"`,
1061
+ `Run "git checkout ${expected}" first. The base branch is configured per project; ask an admin if it should be different.`
1062
+ );
1063
+ }
1064
+ if (!isWorkingTreeClean(cwd)) {
1065
+ throw new CliError(
1066
+ CLI_EXIT_CODES.MISCONFIGURATION,
1067
+ "Working tree is dirty",
1068
+ "Commit, stash, or discard your local changes before running task work."
1069
+ );
1070
+ }
1071
+ }
1072
+ function createTicketBranch(cwd, branchName, baseBranch) {
1073
+ assertValidBranchName(branchName);
1074
+ assertValidBranchName(baseBranch);
1075
+ if (!TICKET_BRANCH.test(branchName)) {
1076
+ throw new CliError(
1077
+ CLI_EXIT_CODES.MISCONFIGURATION,
1078
+ `Per-ticket branch must match ^task/[a-z0-9-]{1,80}$ \u2014 got "${branchName}"`
1079
+ );
1080
+ }
1081
+ try {
1082
+ execFileSync2("git", ["checkout", "-b", branchName, baseBranch], {
1083
+ cwd,
1084
+ stdio: ["ignore", "pipe", "pipe"]
1085
+ });
1086
+ } catch (err) {
1087
+ const stderr = err.stderr?.toString("utf8") ?? "";
1088
+ throw new CliError(
1089
+ CLI_EXIT_CODES.GENERIC_ERROR,
1090
+ `Could not create branch ${branchName}: ${stderr.slice(0, 400) || err.message}`
1091
+ );
1092
+ }
1093
+ }
1094
+ function deleteLocalBranch(cwd, branchName) {
1095
+ if (!VALID_BRANCH.test(branchName)) return;
1096
+ try {
1097
+ execFileSync2("git", ["branch", "-D", branchName], {
1098
+ cwd,
1099
+ stdio: ["ignore", "ignore", "ignore"]
1100
+ });
1101
+ } catch {
1102
+ }
1103
+ }
1104
+ function checkoutBranch(cwd, branchName) {
1105
+ assertValidBranchName(branchName);
1106
+ try {
1107
+ execFileSync2("git", ["checkout", branchName], {
1108
+ cwd,
1109
+ stdio: ["ignore", "pipe", "pipe"]
1110
+ });
1111
+ } catch (err) {
1112
+ const stderr = err.stderr?.toString("utf8") ?? "";
1113
+ throw new CliError(
1114
+ CLI_EXIT_CODES.GENERIC_ERROR,
1115
+ `Could not check out branch ${branchName}: ${stderr.slice(0, 400) || err.message}`
1116
+ );
1117
+ }
1118
+ }
1119
+ function pushBranch(cwd, branchName) {
1120
+ assertValidBranchName(branchName);
1121
+ try {
1122
+ execFileSync2("git", ["push", "-u", "origin", branchName], {
1123
+ cwd,
1124
+ stdio: ["ignore", "pipe", "pipe"]
1125
+ });
1126
+ return { remote: "origin" };
1127
+ } catch (err) {
1128
+ const stderr = err.stderr?.toString("utf8") ?? "";
1129
+ throw new CliError(
1130
+ CLI_EXIT_CODES.GENERIC_ERROR,
1131
+ `Push failed: ${stderr.slice(0, 600) || err.message}`
1132
+ );
1133
+ }
1134
+ }
1135
+ function enforceBaseBranchClean(cwd, baseBranch, opts = {}) {
1136
+ assertValidBranchName(baseBranch);
1137
+ try {
1138
+ execFileSync2("git", ["restore", "--staged", "--worktree", "."], {
1139
+ cwd,
1140
+ stdio: ["ignore", "pipe", "pipe"]
1141
+ });
1142
+ } catch {
1143
+ try {
1144
+ execFileSync2("git", ["reset", "--hard", "HEAD"], {
1145
+ cwd,
1146
+ stdio: ["ignore", "pipe", "pipe"]
1147
+ });
1148
+ } catch {
1149
+ }
1150
+ }
1151
+ try {
1152
+ execFileSync2("git", ["clean", "-fd"], {
1153
+ cwd,
1154
+ stdio: ["ignore", "pipe", "pipe"]
1155
+ });
1156
+ } catch {
1157
+ }
1158
+ checkoutBranch(cwd, baseBranch);
1159
+ const current = currentBranch(cwd);
1160
+ if (current !== baseBranch) {
1161
+ throw new CliError(
1162
+ CLI_EXIT_CODES.GENERIC_ERROR,
1163
+ `Branch enforcement failed: expected "${baseBranch}", on "${current}"`,
1164
+ `Run "git checkout ${baseBranch}" manually, then re-run task multi-work.`
1165
+ );
1166
+ }
1167
+ if (!isWorkingTreeClean(cwd)) {
1168
+ throw new CliError(
1169
+ CLI_EXIT_CODES.GENERIC_ERROR,
1170
+ "Branch enforcement failed: working tree is dirty after returning to base",
1171
+ 'Inspect with "git status" and clean up before re-running task multi-work.'
1172
+ );
1173
+ }
1174
+ if (opts.deleteBranch && opts.deleteBranch !== baseBranch) {
1175
+ deleteLocalBranch(cwd, opts.deleteBranch);
1176
+ }
1177
+ }
1178
+ function remoteBranchExists(cwd, branchName, opts = {}) {
1179
+ assertValidBranchName(branchName);
1180
+ const remote = opts.remote ?? "origin";
1181
+ if (!/^[A-Za-z0-9._-]{1,100}$/.test(remote)) {
1182
+ throw new CliError(CLI_EXIT_CODES.MISCONFIGURATION, `Invalid remote name: ${remote}`);
1183
+ }
1184
+ let stdout;
1185
+ try {
1186
+ stdout = execFileSync2("git", ["ls-remote", "--heads", remote, branchName], {
1187
+ cwd,
1188
+ encoding: "utf8",
1189
+ stdio: ["ignore", "pipe", "pipe"]
1190
+ });
1191
+ } catch (err) {
1192
+ const stderr = err.stderr?.toString("utf8") ?? "";
1193
+ throw new CliError(
1194
+ CLI_EXIT_CODES.NETWORK_UNREACHABLE,
1195
+ `Could not reach remote "${remote}": ${stderr.slice(0, 400) || err.message}`,
1196
+ "Check that the remote exists (`git remote -v`) and that you have credentials to talk to it."
1197
+ );
1198
+ }
1199
+ return stdout.trim().length > 0;
1200
+ }
1201
+ function listLocalTicketBranches(cwd) {
1202
+ let stdout;
1203
+ try {
1204
+ stdout = execFileSync2(
1205
+ "git",
1206
+ ["for-each-ref", "--format=%(refname:short) %(upstream:short)", "refs/heads/task/"],
1207
+ { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }
1208
+ );
1209
+ } catch {
1210
+ return [];
1211
+ }
1212
+ const out = [];
1213
+ for (const rawLine of stdout.split("\n")) {
1214
+ const line = rawLine.trim();
1215
+ if (line.length === 0) continue;
1216
+ const [name, upstream = ""] = line.split(" ");
1217
+ if (!name || !TICKET_BRANCH.test(name)) continue;
1218
+ const seqMatch = name.match(/^task\/(\d+)/);
1219
+ out.push({
1220
+ name,
1221
+ sequenceNumber: seqMatch && seqMatch[1] ? parseInt(seqMatch[1], 10) : null,
1222
+ hasUpstream: upstream.trim().length > 0
1223
+ });
1224
+ }
1225
+ return out;
1226
+ }
1227
+ function branchSlug(sequenceNumber, title) {
1228
+ const safeTitle = title.toLowerCase().normalize("NFKD").replace(/[̀-ͯ]/g, "").replace(/[^a-z0-9\s-]/g, "").trim().replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
1229
+ const slugBudget = 70;
1230
+ const truncatedSlug = safeTitle.slice(0, slugBudget);
1231
+ const tail = truncatedSlug.length > 0 ? `-${truncatedSlug}` : "";
1232
+ return `task/${sequenceNumber}${tail}`.replace(/-+$/, "");
1233
+ }
1234
+
1235
+ // src/commands/status.ts
992
1236
  function registerStatus(program2) {
993
- program2.command("status").description("Show CLI auth, link, and git state").option("--remote", "Also fetch /cli/access for live state").action(async (_opts) => {
1237
+ program2.command("status").alias("s").description("Show CLI auth, link, git state, in-flight tickets, and orphan branches").option("--remote", "Also fetch /cli/access for live state").action(async (_opts) => {
994
1238
  const creds = await readCredentials();
995
1239
  const root = findRepoRoot();
996
1240
  const project = await readProjectConfig(root);
@@ -1026,12 +1270,13 @@ ${c.bold("Project link")}
1026
1270
  process.stdout.write(`
1027
1271
  ${c.bold("Repo")}
1028
1272
  `);
1273
+ let inGitRepo = false;
1029
1274
  try {
1030
- const branch = execFileSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
1275
+ const branch = execFileSync3("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
1031
1276
  cwd: root,
1032
1277
  encoding: "utf8"
1033
1278
  }).trim();
1034
- const dirty = execFileSync("git", ["status", "--porcelain"], {
1279
+ const dirty = execFileSync3("git", ["status", "--porcelain"], {
1035
1280
  cwd: root,
1036
1281
  encoding: "utf8"
1037
1282
  });
@@ -1041,10 +1286,58 @@ ${c.bold("Repo")}
1041
1286
  ` ${dirty.trim().length > 0 ? c.warn("working tree dirty") : c.ok("clean")}
1042
1287
  `
1043
1288
  );
1289
+ inGitRepo = true;
1044
1290
  } catch {
1045
1291
  process.stdout.write(` ${c.warn("!")} not inside a git repo
1046
1292
  `);
1047
1293
  }
1294
+ if (creds && project && inGitRepo) {
1295
+ const inFlight = await apiCall("GET", "/api/v1/cli/me/tickets", {
1296
+ query: {
1297
+ project_id: project.project_id,
1298
+ ai_fix_status: "building",
1299
+ limit: 100
1300
+ }
1301
+ });
1302
+ const inFlightTickets = inFlight.ok && inFlight.data ? inFlight.data : [];
1303
+ const localBranches = listLocalTicketBranches(root);
1304
+ const inFlightBranchNames = new Set(
1305
+ inFlightTickets.map((t) => branchSlug(t.sequence_number, t.title))
1306
+ );
1307
+ process.stdout.write(`
1308
+ ${c.bold("In-flight tickets")}
1309
+ `);
1310
+ if (inFlightTickets.length === 0) {
1311
+ process.stdout.write(c.dim(" none\n"));
1312
+ } else {
1313
+ for (const t of inFlightTickets) {
1314
+ const expectedBranch = branchSlug(t.sequence_number, t.title);
1315
+ const branchPresent = localBranches.some((b) => b.name === expectedBranch);
1316
+ const tag = branchPresent ? c.ok("local branch present") : c.warn("local branch missing");
1317
+ process.stdout.write(
1318
+ ` ${c.dim("\xB7")} #${t.sequence_number} "${t.title}" \u2014 ${tag} \u2192 ${c.cyan(`task resume #${t.sequence_number}`)}
1319
+ `
1320
+ );
1321
+ }
1322
+ }
1323
+ const orphans = localBranches.filter((b) => !inFlightBranchNames.has(b.name));
1324
+ process.stdout.write(`
1325
+ ${c.bold("Orphan task/* branches")}
1326
+ `);
1327
+ if (orphans.length === 0) {
1328
+ process.stdout.write(c.dim(" none\n"));
1329
+ } else {
1330
+ for (const b of orphans) {
1331
+ const upstream = b.hasUpstream ? c.dim(" (has upstream)") : c.dim(" (no upstream)");
1332
+ process.stdout.write(` ${c.dim("\xB7")} ${b.name}${upstream}
1333
+ `);
1334
+ }
1335
+ process.stdout.write(
1336
+ ` ${c.dim("\u2192 run")} ${c.cyan("task reset")} ${c.dim("to clean up")}
1337
+ `
1338
+ );
1339
+ }
1340
+ }
1048
1341
  });
1049
1342
  }
1050
1343
 
@@ -1277,7 +1570,7 @@ async function runAgent(args) {
1277
1570
  }
1278
1571
 
1279
1572
  // src/guardrail/diff-check.ts
1280
- import { execFileSync as execFileSync2 } from "child_process";
1573
+ import { execFileSync as execFileSync4 } from "child_process";
1281
1574
 
1282
1575
  // src/guardrail/protected-paths.ts
1283
1576
  import picomatch from "picomatch";
@@ -1336,7 +1629,7 @@ function checkDiff(args) {
1336
1629
  }
1337
1630
  function safeGitOutput(args, cwd) {
1338
1631
  try {
1339
- return execFileSync2("git", args, { cwd, encoding: "utf8" });
1632
+ return execFileSync4("git", args, { cwd, encoding: "utf8" });
1340
1633
  } catch {
1341
1634
  return "";
1342
1635
  }
@@ -1345,186 +1638,8 @@ function splitLines(text) {
1345
1638
  return text.split(/\r?\n/).map((l) => l.trim()).filter((l) => l.length > 0);
1346
1639
  }
1347
1640
 
1348
- // src/git/commit.ts
1349
- import { execFileSync as execFileSync3 } from "child_process";
1350
- function commitOnly(args) {
1351
- execFileSync3("git", ["add", "-A"], { cwd: args.cwd });
1352
- const statusRaw = execFileSync3("git", ["status", "--porcelain"], {
1353
- cwd: args.cwd,
1354
- encoding: "utf8"
1355
- });
1356
- if (!statusRaw.trim()) {
1357
- throw new Error("No changes to commit (empty diff)");
1358
- }
1359
- execFileSync3("git", ["commit", "-m", args.message], { cwd: args.cwd });
1360
- const sha = execFileSync3("git", ["rev-parse", "HEAD"], {
1361
- cwd: args.cwd,
1362
- encoding: "utf8"
1363
- }).trim();
1364
- return { sha };
1365
- }
1366
- function currentBranch(cwd) {
1367
- try {
1368
- return execFileSync3("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
1369
- cwd,
1370
- encoding: "utf8"
1371
- }).trim();
1372
- } catch {
1373
- return "HEAD";
1374
- }
1375
- }
1376
-
1377
- // src/git/branch.ts
1378
- import { execFileSync as execFileSync4 } from "child_process";
1379
- var VALID_BRANCH = /^[A-Za-z0-9._/-]{1,200}$/;
1380
- var TICKET_BRANCH = /^task\/[a-z0-9-]{1,80}$/;
1381
- function assertValidBranchName(branch) {
1382
- if (!VALID_BRANCH.test(branch) || branch.includes("..") || branch.startsWith("/") || branch.endsWith("/")) {
1383
- throw new CliError(
1384
- CLI_EXIT_CODES.MISCONFIGURATION,
1385
- `Invalid branch name: ${branch}`,
1386
- 'Branch names must contain only [A-Za-z0-9._/-], no "..", and no leading/trailing slash.'
1387
- );
1388
- }
1389
- }
1390
- function isWorkingTreeClean(cwd) {
1391
- const out = execFileSync4("git", ["status", "--porcelain"], {
1392
- cwd,
1393
- encoding: "utf8"
1394
- });
1395
- return out.trim().length === 0;
1396
- }
1397
- function assertBaseBranch(cwd, expected) {
1398
- assertValidBranchName(expected);
1399
- const current = currentBranch(cwd);
1400
- if (current !== expected) {
1401
- throw new CliError(
1402
- CLI_EXIT_CODES.MISCONFIGURATION,
1403
- `task work requires branch "${expected}" but you're on "${current}"`,
1404
- `Run "git checkout ${expected}" first. The base branch is configured per project; ask an admin if it should be different.`
1405
- );
1406
- }
1407
- if (!isWorkingTreeClean(cwd)) {
1408
- throw new CliError(
1409
- CLI_EXIT_CODES.MISCONFIGURATION,
1410
- "Working tree is dirty",
1411
- "Commit, stash, or discard your local changes before running task work."
1412
- );
1413
- }
1414
- }
1415
- function createTicketBranch(cwd, branchName, baseBranch) {
1416
- assertValidBranchName(branchName);
1417
- assertValidBranchName(baseBranch);
1418
- if (!TICKET_BRANCH.test(branchName)) {
1419
- throw new CliError(
1420
- CLI_EXIT_CODES.MISCONFIGURATION,
1421
- `Per-ticket branch must match ^task/[a-z0-9-]{1,80}$ \u2014 got "${branchName}"`
1422
- );
1423
- }
1424
- try {
1425
- execFileSync4("git", ["checkout", "-b", branchName, baseBranch], {
1426
- cwd,
1427
- stdio: ["ignore", "pipe", "pipe"]
1428
- });
1429
- } catch (err) {
1430
- const stderr = err.stderr?.toString("utf8") ?? "";
1431
- throw new CliError(
1432
- CLI_EXIT_CODES.GENERIC_ERROR,
1433
- `Could not create branch ${branchName}: ${stderr.slice(0, 400) || err.message}`
1434
- );
1435
- }
1436
- }
1437
- function deleteLocalBranch(cwd, branchName) {
1438
- if (!VALID_BRANCH.test(branchName)) return;
1439
- try {
1440
- execFileSync4("git", ["branch", "-D", branchName], {
1441
- cwd,
1442
- stdio: ["ignore", "ignore", "ignore"]
1443
- });
1444
- } catch {
1445
- }
1446
- }
1447
- function checkoutBranch(cwd, branchName) {
1448
- assertValidBranchName(branchName);
1449
- try {
1450
- execFileSync4("git", ["checkout", branchName], {
1451
- cwd,
1452
- stdio: ["ignore", "pipe", "pipe"]
1453
- });
1454
- } catch (err) {
1455
- const stderr = err.stderr?.toString("utf8") ?? "";
1456
- throw new CliError(
1457
- CLI_EXIT_CODES.GENERIC_ERROR,
1458
- `Could not check out branch ${branchName}: ${stderr.slice(0, 400) || err.message}`
1459
- );
1460
- }
1461
- }
1462
- function pushBranch(cwd, branchName) {
1463
- assertValidBranchName(branchName);
1464
- try {
1465
- execFileSync4("git", ["push", "-u", "origin", branchName], {
1466
- cwd,
1467
- stdio: ["ignore", "pipe", "pipe"]
1468
- });
1469
- return { remote: "origin" };
1470
- } catch (err) {
1471
- const stderr = err.stderr?.toString("utf8") ?? "";
1472
- throw new CliError(
1473
- CLI_EXIT_CODES.GENERIC_ERROR,
1474
- `Push failed: ${stderr.slice(0, 600) || err.message}`
1475
- );
1476
- }
1477
- }
1478
- function enforceBaseBranchClean(cwd, baseBranch, opts = {}) {
1479
- assertValidBranchName(baseBranch);
1480
- try {
1481
- execFileSync4("git", ["restore", "--staged", "--worktree", "."], {
1482
- cwd,
1483
- stdio: ["ignore", "pipe", "pipe"]
1484
- });
1485
- } catch {
1486
- try {
1487
- execFileSync4("git", ["reset", "--hard", "HEAD"], {
1488
- cwd,
1489
- stdio: ["ignore", "pipe", "pipe"]
1490
- });
1491
- } catch {
1492
- }
1493
- }
1494
- try {
1495
- execFileSync4("git", ["clean", "-fd"], {
1496
- cwd,
1497
- stdio: ["ignore", "pipe", "pipe"]
1498
- });
1499
- } catch {
1500
- }
1501
- checkoutBranch(cwd, baseBranch);
1502
- const current = currentBranch(cwd);
1503
- if (current !== baseBranch) {
1504
- throw new CliError(
1505
- CLI_EXIT_CODES.GENERIC_ERROR,
1506
- `Branch enforcement failed: expected "${baseBranch}", on "${current}"`,
1507
- `Run "git checkout ${baseBranch}" manually, then re-run task multi-work.`
1508
- );
1509
- }
1510
- if (!isWorkingTreeClean(cwd)) {
1511
- throw new CliError(
1512
- CLI_EXIT_CODES.GENERIC_ERROR,
1513
- "Branch enforcement failed: working tree is dirty after returning to base",
1514
- 'Inspect with "git status" and clean up before re-running task multi-work.'
1515
- );
1516
- }
1517
- if (opts.deleteBranch && opts.deleteBranch !== baseBranch) {
1518
- deleteLocalBranch(cwd, opts.deleteBranch);
1519
- }
1520
- }
1521
- function branchSlug(sequenceNumber, title) {
1522
- const safeTitle = title.toLowerCase().normalize("NFKD").replace(/[̀-ͯ]/g, "").replace(/[^a-z0-9\s-]/g, "").trim().replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
1523
- const slugBudget = 70;
1524
- const truncatedSlug = safeTitle.slice(0, slugBudget);
1525
- const tail = truncatedSlug.length > 0 ? `-${truncatedSlug}` : "";
1526
- return `task/${sequenceNumber}${tail}`.replace(/-+$/, "");
1527
- }
1641
+ // src/commands/work.ts
1642
+ import { execFileSync as execFileSync6 } from "child_process";
1528
1643
 
1529
1644
  // src/git/restore.ts
1530
1645
  import { execFileSync as execFileSync5 } from "child_process";
@@ -1636,7 +1751,13 @@ async function buildWorkContext(opts) {
1636
1751
  };
1637
1752
  }
1638
1753
  function registerWork(program2) {
1639
- program2.command("work [ticketId]").description("Run the agent on a CLI-approved ticket \u2014 cuts a per-ticket branch and opens a PR").option("--auto", "Pick the next eligible ticket without prompting").option("--next", "Alias for --auto --max 1").option("--dry-run", "Run the agent + tests but do not commit, push, or open a PR").option("--no-push", "[deprecated in Phase 2 \u2014 task work always pushes via per-ticket branch]").option("--max <n>", "Process up to N tickets in this invocation", "1").option("--silent", "Suppress TTY output (used by scheduled tasks)").option("--schedule-id <id>", "Internal: schedule id when invoked from a scheduled task").action(async (ticketId, opts) => {
1754
+ program2.command("work [ticketId]").description("Run the agent on a CLI-approved ticket \u2014 cuts a per-ticket branch and opens a PR").option("--auto", "Pick the next eligible ticket without prompting").option("--next", "Alias for --auto --max 1").option("--dry-run", "Run the agent + tests but do not commit, push, or open a PR").option("--no-push", "[deprecated in Phase 2 \u2014 task work always pushes via per-ticket branch]").option("--max <n>", "Process up to N tickets in this invocation", "1").option("--silent", "Suppress TTY output (used by scheduled tasks)").option(
1755
+ "--reset",
1756
+ "DESTRUCTIVE: discard all local working-tree changes before running. Requires interactive y/n; pair with --confirm in non-TTY contexts."
1757
+ ).option(
1758
+ "--confirm",
1759
+ "Confirm --reset in non-TTY (silent / scheduled-task) contexts. Has no effect without --reset."
1760
+ ).option("--schedule-id <id>", "Internal: schedule id when invoked from a scheduled task").action(async (ticketId, opts) => {
1640
1761
  await runWork(ticketId, opts);
1641
1762
  });
1642
1763
  }
@@ -1668,6 +1789,9 @@ async function runWork(ticketId, opts) {
1668
1789
  }
1669
1790
  async function processOneTicket(ctx, opts, ticketIdHint) {
1670
1791
  const { cwd, baseBranch, silent } = ctx;
1792
+ if (opts.reset) {
1793
+ await purgeWorkingTreeWithConsent(ctx, opts);
1794
+ }
1671
1795
  try {
1672
1796
  assertBaseBranch(cwd, baseBranch);
1673
1797
  } catch (err) {
@@ -1695,15 +1819,36 @@ async function processOneTicket(ctx, opts, ticketIdHint) {
1695
1819
  }
1696
1820
  const branchName = branchSlug(detail.sequence_number, detail.title);
1697
1821
  const testCommand = detail.project_cli_test_command ?? null;
1822
+ const ticketBaseBranch = detail.project_cli_base_branch || baseBranch;
1698
1823
  if (!silent) {
1699
1824
  process.stdout.write(`
1700
1825
  ${c.bold(`#${detail.sequence_number}: ${detail.title}`)}
1701
1826
  `);
1702
- process.stdout.write(c.dim(` base branch: ${baseBranch}
1827
+ process.stdout.write(c.dim(` base branch: ${ticketBaseBranch}
1703
1828
  `));
1704
1829
  process.stdout.write(c.dim(` ticket branch: ${branchName}
1705
1830
  `));
1706
1831
  }
1832
+ try {
1833
+ if (!remoteBranchExists(cwd, ticketBaseBranch)) {
1834
+ await apiCall("POST", "/api/v1/cli/me/runs", {
1835
+ body: {
1836
+ ticket_id: detail.id,
1837
+ schedule_id: opts.scheduleId,
1838
+ event: "branch_check_failed",
1839
+ output_excerpt: `base branch "${ticketBaseBranch}" missing on origin`
1840
+ }
1841
+ });
1842
+ throw new CliError(
1843
+ CLI_EXIT_CODES.MISCONFIGURATION,
1844
+ `Base branch "${ticketBaseBranch}" does not exist on origin`,
1845
+ `Push it (\`git push origin ${ticketBaseBranch}\`) or update the project's cli_base_branch via the dashboard / PATCH /api/v1/projects/<id>.`
1846
+ );
1847
+ }
1848
+ } catch (err) {
1849
+ if (err instanceof CliError) throw err;
1850
+ throw err;
1851
+ }
1707
1852
  const runId = randomUUID();
1708
1853
  await apiCall("POST", "/api/v1/cli/me/runs", {
1709
1854
  body: {
@@ -1714,18 +1859,44 @@ ${c.bold(`#${detail.sequence_number}: ${detail.title}`)}
1714
1859
  }
1715
1860
  });
1716
1861
  try {
1717
- createTicketBranch(cwd, branchName, baseBranch);
1862
+ createTicketBranch(cwd, branchName, ticketBaseBranch);
1718
1863
  } catch (err) {
1719
- await apiCall("POST", "/api/v1/cli/me/runs", {
1720
- body: {
1721
- ticket_id: detail.id,
1722
- schedule_id: opts.scheduleId,
1723
- event: "branch_check_failed",
1724
- claude_session_id: runId,
1725
- output_excerpt: err.message.slice(0, 4e3)
1864
+ const stderr = err instanceof Error ? err.message : String(err);
1865
+ const looksLikeAlreadyExists = /already exists/i.test(stderr);
1866
+ if (looksLikeAlreadyExists) {
1867
+ const recovered = await tryAutoCleanOrphanBranch(
1868
+ ctx,
1869
+ detail,
1870
+ branchName,
1871
+ ticketBaseBranch,
1872
+ runId,
1873
+ opts.scheduleId
1874
+ );
1875
+ if (recovered === "recreated") {
1876
+ } else {
1877
+ await apiCall("POST", "/api/v1/cli/me/runs", {
1878
+ body: {
1879
+ ticket_id: detail.id,
1880
+ schedule_id: opts.scheduleId,
1881
+ event: "branch_check_failed",
1882
+ claude_session_id: runId,
1883
+ output_excerpt: stderr.slice(0, 4e3)
1884
+ }
1885
+ });
1886
+ throw err;
1726
1887
  }
1727
- });
1728
- throw err;
1888
+ } else {
1889
+ await apiCall("POST", "/api/v1/cli/me/runs", {
1890
+ body: {
1891
+ ticket_id: detail.id,
1892
+ schedule_id: opts.scheduleId,
1893
+ event: "branch_check_failed",
1894
+ claude_session_id: runId,
1895
+ output_excerpt: stderr.slice(0, 4e3)
1896
+ }
1897
+ });
1898
+ throw err;
1899
+ }
1729
1900
  }
1730
1901
  await apiCall("POST", "/api/v1/cli/me/runs", {
1731
1902
  body: {
@@ -1973,25 +2144,33 @@ Claude session: ${runId}
1973
2144
  body: {
1974
2145
  ticket_id: detail.id,
1975
2146
  schedule_id: opts.scheduleId,
1976
- event: "pr_failed",
2147
+ event: "push_failed",
1977
2148
  claude_session_id: runId,
1978
2149
  output_excerpt: err.message.slice(0, 4e3)
1979
2150
  }
1980
2151
  });
2152
+ if (err instanceof CliError) err.phase = "post_push";
1981
2153
  throw err;
1982
2154
  }
1983
2155
  if (!silent)
1984
2156
  process.stdout.write(`${c.ok("\u2713 Pushed")} ${branchName} (${commitSha.slice(0, 12)})
1985
2157
  `);
1986
2158
  const prTitle = `task #${detail.sequence_number}: ${detail.title}`.slice(0, 200);
1987
- const prBody = buildPrBody({ detail, runId, commitSha, branchName, baseBranch, testResult });
2159
+ const prBody = buildPrBody({
2160
+ detail,
2161
+ runId,
2162
+ commitSha,
2163
+ branchName,
2164
+ baseBranch: ticketBaseBranch,
2165
+ testResult
2166
+ });
1988
2167
  let prNumber;
1989
2168
  let prUrl;
1990
2169
  try {
1991
2170
  const prResp = await apiCallOrThrow("POST", `/api/v1/cli/me/tickets/${detail.id}/pull-requests`, {
1992
2171
  body: {
1993
2172
  source_branch: branchName,
1994
- base_branch: baseBranch,
2173
+ base_branch: ticketBaseBranch,
1995
2174
  title: prTitle,
1996
2175
  body: prBody
1997
2176
  }
@@ -2009,7 +2188,7 @@ Claude session: ${runId}
2009
2188
  });
2010
2189
  if (!silent) {
2011
2190
  process.stdout.write(
2012
- `${c.ok("\u2713 PR opened")} ${c.cyan(prResp.pr_url)} \u2192 ${baseBranch}
2191
+ `${c.ok("\u2713 PR opened")} ${c.cyan(prResp.pr_url)} \u2192 ${ticketBaseBranch}
2013
2192
  ` + (prResp.ticket_status_advanced ? c.dim(` Ticket status auto-advanced to 'git_review'.
2014
2193
  `) : "")
2015
2194
  );
@@ -2024,6 +2203,7 @@ Claude session: ${runId}
2024
2203
  output_excerpt: err.message.slice(0, 4e3)
2025
2204
  }
2026
2205
  });
2206
+ if (err instanceof CliError) err.phase = "post_push";
2027
2207
  throw err;
2028
2208
  }
2029
2209
  try {
@@ -2063,6 +2243,127 @@ function buildPrBody(args) {
2063
2243
  "Please review carefully \u2014 this is an AI-generated change."
2064
2244
  ].filter(Boolean).join("\n");
2065
2245
  }
2246
+ async function purgeWorkingTreeWithConsent(ctx, opts) {
2247
+ const { cwd, baseBranch, silent } = ctx;
2248
+ const isTty = !silent && process.stdin.isTTY === true;
2249
+ const status = (() => {
2250
+ try {
2251
+ return execFileSync6("git", ["status", "--short"], { cwd, encoding: "utf8" }).trim();
2252
+ } catch {
2253
+ return "";
2254
+ }
2255
+ })();
2256
+ const onBranch = (() => {
2257
+ try {
2258
+ return currentBranch(cwd);
2259
+ } catch {
2260
+ return "(unknown)";
2261
+ }
2262
+ })();
2263
+ const willSwitchBranch = onBranch !== baseBranch && onBranch !== "(unknown)";
2264
+ if (!isTty) {
2265
+ if (!opts.confirm) {
2266
+ throw new CliError(
2267
+ CLI_EXIT_CODES.MISCONFIGURATION,
2268
+ "--reset requires --confirm in non-interactive (silent / scheduled-task) contexts",
2269
+ "Re-run with both flags only after confirming the destructive purge is intentional."
2270
+ );
2271
+ }
2272
+ if (!silent) {
2273
+ process.stdout.write(c.dim(" --reset --confirm: discarding working-tree changes\n"));
2274
+ if (willSwitchBranch) {
2275
+ process.stdout.write(
2276
+ c.dim(` --reset --confirm: also switching from "${onBranch}" to "${baseBranch}"
2277
+ `)
2278
+ );
2279
+ }
2280
+ }
2281
+ } else {
2282
+ if (!silent) {
2283
+ process.stdout.write(`${c.bold("Current branch:")} ${c.cyan(onBranch)}
2284
+ `);
2285
+ process.stdout.write(`${c.bold("Working tree changes:")}
2286
+ `);
2287
+ process.stdout.write(status.length > 0 ? `${c.dim(status)}
2288
+ ` : c.dim(" (none)\n"));
2289
+ process.stdout.write(`
2290
+ ${c.bold("--reset will:")}
2291
+ `);
2292
+ process.stdout.write(
2293
+ c.dim(" \u2022 discard ALL working-tree changes above (git restore --staged --worktree .)\n")
2294
+ );
2295
+ process.stdout.write(c.dim(" \u2022 delete untracked files + directories (git clean -fd)\n"));
2296
+ if (willSwitchBranch) {
2297
+ process.stdout.write(
2298
+ c.dim(` \u2022 switch from ${c.cyan(onBranch)} ${c.dim("to")} ${c.cyan(baseBranch)}
2299
+ `)
2300
+ );
2301
+ } else {
2302
+ process.stdout.write(
2303
+ c.dim(` \u2022 stay on ${c.cyan(baseBranch)} ${c.dim("(already on base)")}
2304
+ `)
2305
+ );
2306
+ }
2307
+ process.stdout.write("\n");
2308
+ }
2309
+ const answer = await inquirer2.prompt([
2310
+ { type: "confirm", name: "confirm", message: "Proceed?", default: false }
2311
+ ]);
2312
+ if (!answer.confirm) {
2313
+ if (!silent) process.stdout.write(c.dim("Aborted \u2014 working tree untouched.\n"));
2314
+ process.exit(CLI_EXIT_CODES.SUCCESS);
2315
+ }
2316
+ }
2317
+ enforceBaseBranchClean(cwd, baseBranch);
2318
+ }
2319
+ async function tryAutoCleanOrphanBranch(ctx, detail, branchName, ticketBaseBranch, runId, scheduleId) {
2320
+ const inFlight = await apiCall(
2321
+ "GET",
2322
+ "/api/v1/cli/me/tickets",
2323
+ {
2324
+ query: {
2325
+ project_id: ctx.project.project_id,
2326
+ ai_fix_status: "building",
2327
+ limit: 100
2328
+ }
2329
+ }
2330
+ );
2331
+ const claimedByMe = inFlight.ok && inFlight.data ? inFlight.data.some((t) => t.sequence_number === detail.sequence_number) : false;
2332
+ if (claimedByMe) {
2333
+ if (!ctx.silent) {
2334
+ process.stderr.write(
2335
+ `${c.dim(" Local branch matches an in-flight ticket; refusing to auto-delete.")}
2336
+ ${c.dim(` Run \`task resume #${detail.sequence_number}\` to retry the PR for the existing branch.`)}
2337
+ `
2338
+ );
2339
+ }
2340
+ return "in_flight";
2341
+ }
2342
+ try {
2343
+ deleteLocalBranch(ctx.cwd, branchName);
2344
+ createTicketBranch(ctx.cwd, branchName, ticketBaseBranch);
2345
+ } catch {
2346
+ return "failed";
2347
+ }
2348
+ await apiCall("POST", "/api/v1/cli/me/runs", {
2349
+ body: {
2350
+ ticket_id: detail.id,
2351
+ schedule_id: scheduleId,
2352
+ event: "branch_check_failed",
2353
+ claude_session_id: runId,
2354
+ output_excerpt: "auto-cleaned-orphan-and-recreated"
2355
+ }
2356
+ });
2357
+ if (!ctx.silent) {
2358
+ process.stdout.write(
2359
+ c.dim(
2360
+ ` Auto-cleaned orphan local branch '${branchName}' and re-cut from ${ticketBaseBranch}.
2361
+ `
2362
+ )
2363
+ );
2364
+ }
2365
+ return "recreated";
2366
+ }
2066
2367
  async function pickNextEligible(projectId) {
2067
2368
  const result = await apiCall("GET", "/api/v1/cli/me/tickets", {
2068
2369
  query: { project_id: projectId, limit: 1 }
@@ -2109,20 +2410,26 @@ function registerMultiWork(program2) {
2109
2410
  ).option("--max <n>", "Process up to N tickets in this batch", "10").option("--dry-run", "Run the agent + tests but do not commit, push, or open PRs").option("--silent", "Suppress TTY output (used by scheduled tasks)").option(
2110
2411
  "--abort-on-failure",
2111
2412
  "Stop the batch on the first per-ticket failure (default: skip and continue)"
2112
- ).option("--schedule-id <id>", "Internal: schedule id when invoked from a scheduled task").action(async (opts) => {
2413
+ ).option(
2414
+ "--reset",
2415
+ "DESTRUCTIVE: discard local working-tree changes before the first ticket. Requires --confirm in non-TTY contexts."
2416
+ ).option("--confirm", "Confirm --reset in non-TTY (silent / scheduled-task) contexts.").option("--schedule-id <id>", "Internal: schedule id when invoked from a scheduled task").action(async (opts) => {
2113
2417
  await runMultiWork(opts);
2114
2418
  });
2115
2419
  }
2116
2420
  async function runMultiWork(opts) {
2117
2421
  const ctx = await buildWorkContext({ max: opts.max, silent: opts.silent });
2118
2422
  const max = Math.max(1, parseInt(opts.max, 10) || 10);
2423
+ let firstIteration = true;
2119
2424
  const innerOpts = {
2120
2425
  auto: true,
2121
2426
  next: false,
2122
2427
  dryRun: opts.dryRun,
2123
2428
  max: "1",
2124
2429
  silent: opts.silent,
2125
- scheduleId: opts.scheduleId
2430
+ scheduleId: opts.scheduleId,
2431
+ reset: opts.reset,
2432
+ confirm: opts.confirm
2126
2433
  };
2127
2434
  const results = [];
2128
2435
  let processed = 0;
@@ -2137,11 +2444,28 @@ async function runMultiWork(opts) {
2137
2444
  let caughtError = null;
2138
2445
  try {
2139
2446
  outcome = await processOneTicket(ctx, innerOpts, null);
2447
+ if (firstIteration) {
2448
+ firstIteration = false;
2449
+ innerOpts.reset = false;
2450
+ innerOpts.confirm = false;
2451
+ }
2140
2452
  } catch (err) {
2453
+ if (firstIteration) {
2454
+ firstIteration = false;
2455
+ innerOpts.reset = false;
2456
+ innerOpts.confirm = false;
2457
+ }
2141
2458
  caughtError = err instanceof Error ? err.message : String(err);
2459
+ const phaseTag = err instanceof CliError && err.phase === "post_push" ? "post_push" : "pre_push";
2142
2460
  if (!ctx.silent) {
2143
2461
  process.stderr.write(`${c.err("\u2717 Ticket failed")}: ${caughtError}
2144
2462
  `);
2463
+ if (phaseTag === "post_push") {
2464
+ process.stderr.write(
2465
+ `${c.dim(" Branch kept on disk + remote \u2014 run `task resume` to retry the PR.")}
2466
+ `
2467
+ );
2468
+ }
2145
2469
  }
2146
2470
  if (opts.abortOnFailure) {
2147
2471
  try {
@@ -2224,6 +2548,356 @@ ${c.bold("Batch summary")}
2224
2548
  );
2225
2549
  }
2226
2550
 
2551
+ // src/commands/resume.ts
2552
+ import { execFileSync as execFileSync7 } from "child_process";
2553
+ import inquirer3 from "inquirer";
2554
+ function registerResume(program2) {
2555
+ program2.command("resume [ticketRef]").description(
2556
+ "Resume a ticket whose previous `task work` run failed after the per-ticket branch was pushed (i.e. ai_fix_status='building'). Re-pushes the branch and re-attempts the PR \u2014 idempotent end-to-end."
2557
+ ).option("--silent", "Suppress TTY output").action(async (ticketRef, opts) => {
2558
+ await runResume(ticketRef, opts);
2559
+ });
2560
+ }
2561
+ async function runResume(ticketRef, opts) {
2562
+ const cwd = findRepoRoot();
2563
+ const project = await readProjectConfig(cwd);
2564
+ if (!project) {
2565
+ throw new CliError(
2566
+ CLI_EXIT_CODES.MISCONFIGURATION,
2567
+ "No project link in this repo",
2568
+ "Run 'task link' first."
2569
+ );
2570
+ }
2571
+ const baseBranch = project.cli_base_branch ?? "development";
2572
+ const silent = !!opts.silent;
2573
+ assertBaseBranch(cwd, baseBranch);
2574
+ const access2 = await apiCallOrThrow("GET", "/api/v1/cli/access");
2575
+ const ticketId = await resolveTicketId(project, ticketRef, silent);
2576
+ if (!ticketId) {
2577
+ if (!silent) {
2578
+ process.stdout.write(c.dim("No in-flight tickets to resume.\n"));
2579
+ }
2580
+ return;
2581
+ }
2582
+ const detail = await apiCallOrThrow(
2583
+ "GET",
2584
+ `/api/v1/cli/me/tickets/${ticketId}`
2585
+ );
2586
+ if (detail.project_id !== project.project_id) {
2587
+ throw new CliError(
2588
+ CLI_EXIT_CODES.MISCONFIGURATION,
2589
+ `Ticket #${detail.sequence_number} belongs to a different project than this repo's link`,
2590
+ "cd to the project repo this ticket belongs to (or re-link with `task link`) and retry."
2591
+ );
2592
+ }
2593
+ if (detail.ai_fix_status !== "building") {
2594
+ throw new CliError(
2595
+ CLI_EXIT_CODES.GENERIC_ERROR,
2596
+ `Ticket #${detail.sequence_number} is in ai_fix_status='${detail.ai_fix_status}', not 'building' \u2014 nothing to resume`,
2597
+ "A previous resume attempt may already have succeeded. Check the dashboard for the ticket's PR."
2598
+ );
2599
+ }
2600
+ if (detail.claimed_by_user_id && detail.claimed_by_user_id !== access2.user_id) {
2601
+ throw new CliError(
2602
+ CLI_EXIT_CODES.UNAUTHORISED,
2603
+ `Ticket #${detail.sequence_number} is claimed by another user`,
2604
+ 'Ask that user to resume it, or have an admin reset ai_fix_status to "approved" so the ticket can be re-claimed fresh.'
2605
+ );
2606
+ }
2607
+ const branchName = branchSlug(detail.sequence_number, detail.title);
2608
+ const ticketBaseBranch = detail.project_cli_base_branch || baseBranch;
2609
+ if (!localBranchExists(cwd, branchName)) {
2610
+ throw new CliError(
2611
+ CLI_EXIT_CODES.GENERIC_ERROR,
2612
+ `Local branch '${branchName}' is missing \u2014 cannot resume`,
2613
+ 'The branch was deleted (or never existed locally). Ask an admin to set ai_fix_status back to "approved" so `task work` can produce a fresh fix.'
2614
+ );
2615
+ }
2616
+ if (!isAncestor(cwd, ticketBaseBranch, branchName)) {
2617
+ throw new CliError(
2618
+ CLI_EXIT_CODES.GENERIC_ERROR,
2619
+ `Base branch '${ticketBaseBranch}' is no longer an ancestor of '${branchName}' \u2014 cannot resume`,
2620
+ 'The base branch has moved (rebase/force-push). Have an admin reset ai_fix_status to "approved" and re-run `task work` for a fresh branch.'
2621
+ );
2622
+ }
2623
+ if (!silent) {
2624
+ process.stdout.write(`
2625
+ ${c.bold(`Resuming #${detail.sequence_number}: ${detail.title}`)}
2626
+ `);
2627
+ process.stdout.write(c.dim(` branch: ${branchName} \u2192 ${ticketBaseBranch}
2628
+ `));
2629
+ }
2630
+ checkoutBranch(cwd, branchName);
2631
+ try {
2632
+ pushBranch(cwd, branchName);
2633
+ if (!silent) process.stdout.write(c.dim(" \u2713 pushed (idempotent)\n"));
2634
+ } catch (err) {
2635
+ try {
2636
+ checkoutBranch(cwd, baseBranch);
2637
+ } catch {
2638
+ }
2639
+ await apiCall("POST", "/api/v1/cli/me/runs", {
2640
+ body: {
2641
+ ticket_id: detail.id,
2642
+ event: "push_failed",
2643
+ output_excerpt: err.message.slice(0, 4e3)
2644
+ }
2645
+ });
2646
+ throw err;
2647
+ }
2648
+ const prTitle = `task #${detail.sequence_number}: ${detail.title}`.slice(0, 200);
2649
+ const prBody = buildResumePrBody(detail, branchName, ticketBaseBranch);
2650
+ let prResp;
2651
+ try {
2652
+ prResp = await apiCallOrThrow("POST", `/api/v1/cli/me/tickets/${detail.id}/pull-requests`, {
2653
+ body: {
2654
+ source_branch: branchName,
2655
+ base_branch: ticketBaseBranch,
2656
+ title: prTitle,
2657
+ body: prBody
2658
+ }
2659
+ });
2660
+ } catch (err) {
2661
+ try {
2662
+ checkoutBranch(cwd, baseBranch);
2663
+ } catch {
2664
+ }
2665
+ await apiCall("POST", "/api/v1/cli/me/runs", {
2666
+ body: {
2667
+ ticket_id: detail.id,
2668
+ event: "pr_failed",
2669
+ output_excerpt: err.message.slice(0, 4e3)
2670
+ }
2671
+ });
2672
+ throw err;
2673
+ }
2674
+ await apiCall("POST", "/api/v1/cli/me/runs", {
2675
+ body: {
2676
+ ticket_id: detail.id,
2677
+ event: "resumed",
2678
+ output_excerpt: `${prResp.recovered ? "recovered" : "opened"} PR #${prResp.pr_number}: ${prResp.pr_url}`
2679
+ }
2680
+ });
2681
+ if (!silent) {
2682
+ const tag = prResp.recovered ? c.ok("\u2713 Recovered PR") : c.ok("\u2713 PR opened");
2683
+ process.stdout.write(`${tag} ${c.cyan(prResp.pr_url)}
2684
+ `);
2685
+ }
2686
+ try {
2687
+ checkoutBranch(cwd, baseBranch);
2688
+ } catch {
2689
+ }
2690
+ }
2691
+ async function resolveTicketId(project, ticketRef, silent) {
2692
+ if (ticketRef && ticketRef.length > 0) {
2693
+ if (/^[0-9a-f-]{36}$/i.test(ticketRef)) return ticketRef;
2694
+ const seqMatch = ticketRef.match(/^#?(\d+)$/);
2695
+ if (seqMatch) {
2696
+ const result2 = await apiCall("GET", "/api/v1/cli/me/tickets", {
2697
+ query: { project_id: project.project_id, ai_fix_status: "building", limit: 100 }
2698
+ });
2699
+ if (!result2.ok || !result2.data) {
2700
+ throw new CliError(
2701
+ CLI_EXIT_CODES.GENERIC_ERROR,
2702
+ `Could not list in-flight tickets (HTTP ${result2.status})${result2.error?.message ? `: ${result2.error.message}` : ""}`
2703
+ );
2704
+ }
2705
+ const seq = parseInt(seqMatch[1] ?? "", 10);
2706
+ const match = result2.data.find((t) => t.sequence_number === seq);
2707
+ if (!match) {
2708
+ throw new CliError(
2709
+ CLI_EXIT_CODES.GENERIC_ERROR,
2710
+ `No in-flight ticket #${seq} claimed by you on this project`,
2711
+ "Run `task doctor` to list your in-flight tickets, or pass the UUID directly."
2712
+ );
2713
+ }
2714
+ return match.id;
2715
+ }
2716
+ throw new CliError(
2717
+ CLI_EXIT_CODES.MISCONFIGURATION,
2718
+ `Invalid ticket reference: ${ticketRef}`,
2719
+ "Pass either a UUID or a sequence number like #42."
2720
+ );
2721
+ }
2722
+ const result = await apiCall("GET", "/api/v1/cli/me/tickets", {
2723
+ query: { project_id: project.project_id, ai_fix_status: "building", limit: 100 }
2724
+ });
2725
+ if (!result.ok) {
2726
+ throw new CliError(
2727
+ CLI_EXIT_CODES.GENERIC_ERROR,
2728
+ `Could not list in-flight tickets (HTTP ${result.status})${result.error?.message ? `: ${result.error.message}` : ""}`
2729
+ );
2730
+ }
2731
+ if (!result.data || result.data.length === 0) return null;
2732
+ if (result.data.length === 1) {
2733
+ const only = result.data[0];
2734
+ return only ? only.id : null;
2735
+ }
2736
+ if (silent) {
2737
+ throw new CliError(
2738
+ CLI_EXIT_CODES.MISCONFIGURATION,
2739
+ `${result.data.length} in-flight tickets \u2014 pass a specific reference in silent mode`,
2740
+ "Run `task doctor` to see the list, then `task resume #N` for the one you want."
2741
+ );
2742
+ }
2743
+ const answer = await inquirer3.prompt([
2744
+ {
2745
+ type: "list",
2746
+ name: "ticketId",
2747
+ message: "Pick an in-flight ticket to resume:",
2748
+ choices: result.data.map((t) => ({
2749
+ name: `#${t.sequence_number} \u2014 ${t.title}`,
2750
+ value: t.id
2751
+ }))
2752
+ }
2753
+ ]);
2754
+ return answer.ticketId;
2755
+ }
2756
+ function localBranchExists(cwd, branchName) {
2757
+ try {
2758
+ execFileSync7("git", ["rev-parse", "--verify", `refs/heads/${branchName}`], {
2759
+ cwd,
2760
+ stdio: ["ignore", "ignore", "ignore"]
2761
+ });
2762
+ return true;
2763
+ } catch {
2764
+ return false;
2765
+ }
2766
+ }
2767
+ function isAncestor(cwd, ancestor, descendant) {
2768
+ try {
2769
+ execFileSync7("git", ["merge-base", "--is-ancestor", ancestor, descendant], {
2770
+ cwd,
2771
+ stdio: ["ignore", "ignore", "ignore"]
2772
+ });
2773
+ return true;
2774
+ } catch {
2775
+ return false;
2776
+ }
2777
+ }
2778
+ function buildResumePrBody(detail, branchName, baseBranch) {
2779
+ return [
2780
+ `Resolves ticket #${detail.sequence_number}: ${detail.title}`,
2781
+ "",
2782
+ detail.description ? `> ${detail.description.slice(0, 1500)}` : "",
2783
+ "",
2784
+ "---",
2785
+ "",
2786
+ `**Generated by:** \`task resume\` (recovery of a previous \`task work\` run)`,
2787
+ `**Branch:** \`${branchName}\` \u2190 \`${baseBranch}\``,
2788
+ "",
2789
+ "Please review carefully \u2014 this is an AI-generated change."
2790
+ ].filter(Boolean).join("\n");
2791
+ }
2792
+
2793
+ // src/commands/reset.ts
2794
+ import inquirer4 from "inquirer";
2795
+ function registerReset(program2) {
2796
+ program2.command("reset").alias("r").description(
2797
+ "Delete orphan local task/* branches \u2014 branches the CLI created locally but that are no longer in your in-flight (`ai_fix_status='building'`) list. Interactive y/n in TTY; pair with --confirm in non-TTY contexts."
2798
+ ).option("--silent", "Suppress TTY output").option("--confirm", "Confirm deletion in non-TTY (silent / scheduled-task) contexts").action(async (opts) => {
2799
+ await runReset(opts);
2800
+ });
2801
+ }
2802
+ async function runReset(opts) {
2803
+ const cwd = findRepoRoot();
2804
+ const project = await readProjectConfig(cwd);
2805
+ if (!project) {
2806
+ throw new CliError(
2807
+ CLI_EXIT_CODES.MISCONFIGURATION,
2808
+ "No project link in this repo",
2809
+ "Run 'task link' first."
2810
+ );
2811
+ }
2812
+ const silent = !!opts.silent;
2813
+ const localBranches = listLocalTicketBranches(cwd);
2814
+ if (localBranches.length === 0) {
2815
+ if (!silent) process.stdout.write(c.dim("No local task/* branches \u2014 nothing to clean up.\n"));
2816
+ return;
2817
+ }
2818
+ const inFlight = await apiCall("GET", "/api/v1/cli/me/tickets", {
2819
+ query: { project_id: project.project_id, ai_fix_status: "building", limit: 100 }
2820
+ });
2821
+ const inFlightBranchNames = /* @__PURE__ */ new Set();
2822
+ if (inFlight.ok && inFlight.data) {
2823
+ for (const t of inFlight.data) {
2824
+ inFlightBranchNames.add(branchSlug(t.sequence_number, t.title));
2825
+ }
2826
+ }
2827
+ const orphans = localBranches.filter((b) => !inFlightBranchNames.has(b.name));
2828
+ const kept = localBranches.filter((b) => inFlightBranchNames.has(b.name));
2829
+ if (orphans.length === 0) {
2830
+ if (!silent) {
2831
+ process.stdout.write(c.dim("No orphan task/* branches.\n"));
2832
+ if (kept.length > 0) {
2833
+ process.stdout.write(
2834
+ c.dim(
2835
+ ` ${kept.length} in-flight branch(es) preserved: ${kept.map((b) => b.name).join(", ")}
2836
+ `
2837
+ )
2838
+ );
2839
+ }
2840
+ }
2841
+ return;
2842
+ }
2843
+ if (!silent) {
2844
+ process.stdout.write(`${c.bold("Orphan task/* branches:")}
2845
+ `);
2846
+ for (const b of orphans) {
2847
+ const upstream = b.hasUpstream ? c.dim(" (has upstream)") : c.dim(" (no upstream)");
2848
+ process.stdout.write(` ${c.dim("\xB7")} ${b.name}${upstream}
2849
+ `);
2850
+ }
2851
+ if (kept.length > 0) {
2852
+ process.stdout.write(`
2853
+ ${c.dim("In-flight branch(es) preserved (use `task resume`):")}
2854
+ `);
2855
+ for (const b of kept) {
2856
+ process.stdout.write(` ${c.dim("\xB7")} ${b.name}
2857
+ `);
2858
+ }
2859
+ }
2860
+ process.stdout.write("\n");
2861
+ }
2862
+ const isTty = !silent && process.stdin.isTTY === true;
2863
+ if (!isTty) {
2864
+ if (!opts.confirm) {
2865
+ throw new CliError(
2866
+ CLI_EXIT_CODES.MISCONFIGURATION,
2867
+ "`task reset` requires --confirm in non-interactive (silent / scheduled-task) contexts",
2868
+ "Re-run with --confirm only after verifying the orphan list is what you expect."
2869
+ );
2870
+ }
2871
+ } else {
2872
+ const answer = await inquirer4.prompt([
2873
+ {
2874
+ type: "confirm",
2875
+ name: "confirm",
2876
+ message: `Delete ${orphans.length} orphan branch(es)?`,
2877
+ default: false
2878
+ }
2879
+ ]);
2880
+ if (!answer.confirm) {
2881
+ if (!silent) process.stdout.write(c.dim("Aborted \u2014 no branches deleted.\n"));
2882
+ process.exit(CLI_EXIT_CODES.SUCCESS);
2883
+ }
2884
+ }
2885
+ let deleted = 0;
2886
+ for (const b of orphans) {
2887
+ deleteLocalBranch(cwd, b.name);
2888
+ deleted += 1;
2889
+ if (!silent) process.stdout.write(`${c.ok("\u2713")} deleted ${b.name}
2890
+ `);
2891
+ }
2892
+ if (!silent) {
2893
+ process.stdout.write(
2894
+ `
2895
+ ${c.bold(`Cleaned up ${deleted} branch(es).`)} ${c.dim("Run `task work` to start fresh.")}
2896
+ `
2897
+ );
2898
+ }
2899
+ }
2900
+
2227
2901
  // src/commands/scan.ts
2228
2902
  import { randomUUID as randomUUID2 } from "crypto";
2229
2903
  import ora2 from "ora";
@@ -2931,7 +3605,7 @@ function clampInt(raw, min, max, fallback) {
2931
3605
  }
2932
3606
 
2933
3607
  // src/commands/pr-test.ts
2934
- import { execFileSync as execFileSync6 } from "child_process";
3608
+ import { execFileSync as execFileSync8 } from "child_process";
2935
3609
  function registerPrTest(program2) {
2936
3610
  program2.command("pr-test").description(
2937
3611
  "Dry-run the full PR pipeline \u2014 cuts a throwaway branch, opens a real PR via the dashboard, then cleans up. Use this to verify your git integration before running task work on real tickets."
@@ -2974,7 +3648,7 @@ async function runPrTest(opts) {
2974
3648
  try {
2975
3649
  if (!silent) process.stdout.write(`${c.dim("Step 3/6: empty commit\u2026")}
2976
3650
  `);
2977
- execFileSync6(
3651
+ execFileSync8(
2978
3652
  "git",
2979
3653
  ["commit", "--allow-empty", "-m", `task pr-test: connectivity probe ${timestamp}`],
2980
3654
  { cwd, stdio: ["ignore", "pipe", "pipe"] }
@@ -3077,7 +3751,7 @@ import { platform as platform2 } from "os";
3077
3751
  import { mkdir as mkdir7, readFile as readFile5, writeFile as writeFile8, unlink as unlink3, readdir } from "fs/promises";
3078
3752
  import { homedir as homedir6 } from "os";
3079
3753
  import { join as join8 } from "path";
3080
- import { execFileSync as execFileSync7, spawn as spawn4 } from "child_process";
3754
+ import { execFileSync as execFileSync9, spawn as spawn4 } from "child_process";
3081
3755
 
3082
3756
  // src/scheduler/cron-translate.ts
3083
3757
  function translateToLaunchd(cron) {
@@ -3245,17 +3919,17 @@ var launchdAdapter = {
3245
3919
  const path = plistPath(entry.id);
3246
3920
  await writeFile8(path, buildPlist(entry));
3247
3921
  try {
3248
- execFileSync7("launchctl", ["bootout", bootstrapDomain(), path], { stdio: "ignore" });
3922
+ execFileSync9("launchctl", ["bootout", bootstrapDomain(), path], { stdio: "ignore" });
3249
3923
  } catch {
3250
3924
  }
3251
3925
  if (entry.enabled) {
3252
- execFileSync7("launchctl", ["bootstrap", bootstrapDomain(), path]);
3926
+ execFileSync9("launchctl", ["bootstrap", bootstrapDomain(), path]);
3253
3927
  }
3254
3928
  },
3255
3929
  async remove(id) {
3256
3930
  const path = plistPath(id);
3257
3931
  try {
3258
- execFileSync7("launchctl", ["bootout", bootstrapDomain(), path], { stdio: "ignore" });
3932
+ execFileSync9("launchctl", ["bootout", bootstrapDomain(), path], { stdio: "ignore" });
3259
3933
  } catch {
3260
3934
  }
3261
3935
  try {
@@ -3320,10 +3994,10 @@ var launchdAdapter = {
3320
3994
  xml = xml.replace(/\s*<key>Disabled<\/key>\s*<true\/>/, "");
3321
3995
  await writeFile8(path, xml);
3322
3996
  try {
3323
- execFileSync7("launchctl", ["bootout", bootstrapDomain(), path], { stdio: "ignore" });
3997
+ execFileSync9("launchctl", ["bootout", bootstrapDomain(), path], { stdio: "ignore" });
3324
3998
  } catch {
3325
3999
  }
3326
- execFileSync7("launchctl", ["bootstrap", bootstrapDomain(), path]);
4000
+ execFileSync9("launchctl", ["bootstrap", bootstrapDomain(), path]);
3327
4001
  } else {
3328
4002
  if (!/<key>Disabled<\/key>/.test(xml)) {
3329
4003
  xml = xml.replace(
@@ -3333,7 +4007,7 @@ var launchdAdapter = {
3333
4007
  await writeFile8(path, xml);
3334
4008
  }
3335
4009
  try {
3336
- execFileSync7("launchctl", ["bootout", bootstrapDomain(), path], { stdio: "ignore" });
4010
+ execFileSync9("launchctl", ["bootout", bootstrapDomain(), path], { stdio: "ignore" });
3337
4011
  } catch {
3338
4012
  }
3339
4013
  }
@@ -3341,7 +4015,7 @@ var launchdAdapter = {
3341
4015
  };
3342
4016
 
3343
4017
  // src/scheduler/cron.ts
3344
- import { execFileSync as execFileSync8, spawn as spawn5 } from "child_process";
4018
+ import { execFileSync as execFileSync10, spawn as spawn5 } from "child_process";
3345
4019
 
3346
4020
  // src/scheduler/safe-command.ts
3347
4021
  var FORBIDDEN = /[;&|`$()<>\\]/;
@@ -3396,7 +4070,7 @@ var MARK_OPEN = (id) => `# task-cli:${id}:start`;
3396
4070
  var MARK_CLOSE = (id) => `# task-cli:${id}:end`;
3397
4071
  function readCrontab() {
3398
4072
  try {
3399
- return execFileSync8("crontab", ["-l"], { encoding: "utf8" });
4073
+ return execFileSync10("crontab", ["-l"], { encoding: "utf8" });
3400
4074
  } catch {
3401
4075
  return "";
3402
4076
  }
@@ -3511,7 +4185,7 @@ var cronAdapter = {
3511
4185
  };
3512
4186
 
3513
4187
  // src/scheduler/windows.ts
3514
- import { execFileSync as execFileSync9, spawn as spawn6 } from "child_process";
4188
+ import { execFileSync as execFileSync11, spawn as spawn6 } from "child_process";
3515
4189
  var TASK_PREFIX = "TaskCLI_";
3516
4190
  function taskName(id) {
3517
4191
  return `${TASK_PREFIX}${id.replace(/[^A-Za-z0-9_-]/g, "_")}`;
@@ -3576,22 +4250,22 @@ function pad(v) {
3576
4250
  var windowsAdapter = {
3577
4251
  async upsert(entry) {
3578
4252
  const args = buildSchtasksArgs(entry, entry.command);
3579
- execFileSync9("schtasks.exe", args, { stdio: "ignore" });
4253
+ execFileSync11("schtasks.exe", args, { stdio: "ignore" });
3580
4254
  if (!entry.enabled) {
3581
- execFileSync9("schtasks.exe", ["/Change", "/TN", taskName(entry.id), "/DISABLE"], {
4255
+ execFileSync11("schtasks.exe", ["/Change", "/TN", taskName(entry.id), "/DISABLE"], {
3582
4256
  stdio: "ignore"
3583
4257
  });
3584
4258
  }
3585
4259
  },
3586
4260
  async remove(id) {
3587
4261
  try {
3588
- execFileSync9("schtasks.exe", ["/Delete", "/TN", taskName(id), "/F"], { stdio: "ignore" });
4262
+ execFileSync11("schtasks.exe", ["/Delete", "/TN", taskName(id), "/F"], { stdio: "ignore" });
3589
4263
  } catch {
3590
4264
  }
3591
4265
  },
3592
4266
  async list() {
3593
4267
  try {
3594
- const csv = execFileSync9("schtasks.exe", ["/Query", "/FO", "CSV", "/V"], {
4268
+ const csv = execFileSync11("schtasks.exe", ["/Query", "/FO", "CSV", "/V"], {
3595
4269
  encoding: "utf8"
3596
4270
  });
3597
4271
  const lines = csv.split(/\r?\n/);
@@ -3641,7 +4315,7 @@ var windowsAdapter = {
3641
4315
  },
3642
4316
  async setEnabled(id, enabled) {
3643
4317
  try {
3644
- execFileSync9(
4318
+ execFileSync11(
3645
4319
  "schtasks.exe",
3646
4320
  ["/Change", "/TN", taskName(id), enabled ? "/ENABLE" : "/DISABLE"],
3647
4321
  { stdio: "ignore" }
@@ -4049,7 +4723,7 @@ function registerConfig(program2) {
4049
4723
  }
4050
4724
 
4051
4725
  // src/commands/doctor.ts
4052
- import { execFileSync as execFileSync10 } from "child_process";
4726
+ import { execFileSync as execFileSync12 } from "child_process";
4053
4727
  import { readFile as readFile8, writeFile as writeFile10 } from "fs/promises";
4054
4728
  import { join as join11 } from "path";
4055
4729
  import { request as request5 } from "undici";
@@ -4100,8 +4774,27 @@ function registerDoctor(program2) {
4100
4774
  detail: `${apiUrl}: ${err.message}`
4101
4775
  });
4102
4776
  }
4777
+ if (project) {
4778
+ const baseBranch = project.cli_base_branch ?? "development";
4779
+ try {
4780
+ const exists = remoteBranchExists(root, baseBranch);
4781
+ checks.push({
4782
+ name: "base branch on origin",
4783
+ ok: exists,
4784
+ detail: exists ? `origin/${baseBranch} reachable` : `origin has no branch "${baseBranch}"`,
4785
+ remediation: exists ? void 0 : `push it (\`git push origin ${baseBranch}\`) or update the project's cli_base_branch via PATCH /api/v1/projects/${project.project_id}`
4786
+ });
4787
+ } catch (err) {
4788
+ checks.push({
4789
+ name: "base branch on origin",
4790
+ ok: false,
4791
+ detail: err instanceof CliError ? err.message : `could not check origin: ${err.message}`,
4792
+ remediation: err instanceof CliError && err.hint ? err.hint : "Verify that `origin` is configured (`git remote -v`)."
4793
+ });
4794
+ }
4795
+ }
4103
4796
  try {
4104
- const dirty = execFileSync10("git", ["status", "--porcelain"], {
4797
+ const dirty = execFileSync12("git", ["status", "--porcelain"], {
4105
4798
  cwd: root,
4106
4799
  encoding: "utf8"
4107
4800
  }).trim();
@@ -4119,10 +4812,19 @@ function registerDoctor(program2) {
4119
4812
  opts.fix === true
4120
4813
  );
4121
4814
  checks.push(testCheck);
4815
+ let inFlight = [];
4816
+ if (creds && project) {
4817
+ inFlight = await listInFlightTickets(project.project_id, root);
4818
+ checks.push({
4819
+ name: "in-flight tickets",
4820
+ ok: true,
4821
+ detail: inFlight.length === 0 ? "none" : `${inFlight.length} ticket(s) waiting to be resumed`
4822
+ });
4823
+ }
4122
4824
  let allOk = true;
4123
4825
  for (const check of checks) {
4124
4826
  const sym = check.ok ? c.ok("\u2713") : c.err("\u2717");
4125
- process.stdout.write(`${sym} ${check.name.padEnd(16)} ${c.dim(check.detail)}
4827
+ process.stdout.write(`${sym} ${check.name.padEnd(20)} ${c.dim(check.detail)}
4126
4828
  `);
4127
4829
  if (!check.ok) {
4128
4830
  allOk = false;
@@ -4132,9 +4834,42 @@ function registerDoctor(program2) {
4132
4834
  }
4133
4835
  }
4134
4836
  }
4837
+ for (const t of inFlight) {
4838
+ const status = t.branchPresent ? c.ok("local branch present") : c.err("local branch missing");
4839
+ process.stdout.write(
4840
+ ` ${c.dim("\u2192")} #${t.sequenceNumber} "${t.title}" \u2014 ${status} \u2014 ${c.cyan(`task resume #${t.sequenceNumber}`)}
4841
+ `
4842
+ );
4843
+ }
4135
4844
  if (!allOk) process.exit(1);
4136
4845
  });
4137
4846
  }
4847
+ async function listInFlightTickets(projectId, cwd) {
4848
+ const result = await apiCall(
4849
+ "GET",
4850
+ "/api/v1/cli/me/tickets",
4851
+ {
4852
+ query: { project_id: projectId, ai_fix_status: "building", limit: 100 }
4853
+ }
4854
+ );
4855
+ if (!result.ok || !result.data) return [];
4856
+ return result.data.map((t) => ({
4857
+ sequenceNumber: t.sequence_number,
4858
+ title: t.title,
4859
+ branchPresent: localBranchExists2(cwd, branchSlug(t.sequence_number, t.title))
4860
+ }));
4861
+ }
4862
+ function localBranchExists2(cwd, branchName) {
4863
+ try {
4864
+ execFileSync12("git", ["rev-parse", "--verify", `refs/heads/${branchName}`], {
4865
+ cwd,
4866
+ stdio: ["ignore", "ignore", "ignore"]
4867
+ });
4868
+ return true;
4869
+ } catch {
4870
+ return false;
4871
+ }
4872
+ }
4138
4873
  async function checkPrePushTest(root, configuredCommand, fix) {
4139
4874
  const command = configuredCommand && configuredCommand.trim().length > 0 ? configuredCommand.trim() : DEFAULT_TEST_COMMAND;
4140
4875
  const argv = command.split(/\s+/).filter((s) => s.length > 0);
@@ -4209,11 +4944,12 @@ async function checkPrePushTest(root, configuredCommand, fix) {
4209
4944
  function resolveScriptName(argv) {
4210
4945
  const [exe, ...rest] = argv;
4211
4946
  if (!exe || rest.length === 0) return null;
4947
+ const hasAnyFlag = rest.some((tok) => tok.startsWith("-"));
4948
+ if (hasAnyFlag) return null;
4212
4949
  if (exe === "pnpm" || exe === "yarn" || exe === "bun") {
4213
4950
  const next = rest[0];
4214
4951
  if (!next) return null;
4215
4952
  if (next === "run") return rest[1] ?? null;
4216
- if (next.startsWith("-")) return null;
4217
4953
  return next;
4218
4954
  }
4219
4955
  if (exe === "npm") {
@@ -4231,7 +4967,7 @@ function detectIndent(raw) {
4231
4967
  }
4232
4968
  function checkBinary(name, command) {
4233
4969
  try {
4234
- const out = execFileSync10(command, ["--version"], { encoding: "utf8" }).trim();
4970
+ const out = execFileSync12(command, ["--version"], { encoding: "utf8" }).trim();
4235
4971
  return { name, ok: true, detail: out.split("\n")[0] ?? out };
4236
4972
  } catch {
4237
4973
  return { name, ok: false, detail: `'${command}' not found on PATH` };
@@ -4239,7 +4975,7 @@ function checkBinary(name, command) {
4239
4975
  }
4240
4976
 
4241
4977
  // src/commands/version.ts
4242
- var CLI_VERSION = true ? "0.2.4" : "0.0.0-dev";
4978
+ var CLI_VERSION = true ? "0.2.6" : "0.0.0-dev";
4243
4979
  function registerVersion(program2) {
4244
4980
  program2.command("version").description("Print the CLI version").action(() => {
4245
4981
  process.stdout.write(CLI_VERSION + "\n");
@@ -4263,6 +4999,8 @@ registerTickets(program);
4263
4999
  registerTicket(program);
4264
5000
  registerWork(program);
4265
5001
  registerMultiWork(program);
5002
+ registerResume(program);
5003
+ registerReset(program);
4266
5004
  registerScan(program);
4267
5005
  registerPrTest(program);
4268
5006
  registerScheduledTask(program);