@inteeka/task-cli 0.2.29 → 0.2.31
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 +403 -50
- package/dist/cli.js.map +1 -1
- package/package.json +3 -3
package/dist/cli.js
CHANGED
|
@@ -3,6 +3,17 @@
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { Command } from "commander";
|
|
5
5
|
|
|
6
|
+
// ../../packages/constants/src/tickets.ts
|
|
7
|
+
var TERMINAL_STATUSES = ["done", "duplicate_archive", "not_required_archive"];
|
|
8
|
+
var RESOLVED_TICKET_STATUSES = [
|
|
9
|
+
"complete",
|
|
10
|
+
"done",
|
|
11
|
+
"duplicate_archive",
|
|
12
|
+
"not_required_archive"
|
|
13
|
+
];
|
|
14
|
+
var RESOLVED_TICKET_STATUSES_FILTER = `(${RESOLVED_TICKET_STATUSES.join(",")})`;
|
|
15
|
+
var TERMINAL_STATUSES_FILTER = `(${TERMINAL_STATUSES.join(",")})`;
|
|
16
|
+
|
|
6
17
|
// ../../packages/constants/src/plans.ts
|
|
7
18
|
var PLAN_LIMITS = {
|
|
8
19
|
free: {
|
|
@@ -949,7 +960,7 @@ function registerLink(program2) {
|
|
|
949
960
|
project_slug: chosen.slug,
|
|
950
961
|
project_name: chosen.name,
|
|
951
962
|
cli_protected_paths: chosen.cli_protected_paths,
|
|
952
|
-
cli_base_branch: chosen.cli_base_branch
|
|
963
|
+
...chosen.cli_base_branch ? { cli_base_branch: chosen.cli_base_branch } : {},
|
|
953
964
|
cli_test_command: chosen.cli_test_command ?? null
|
|
954
965
|
},
|
|
955
966
|
repoRoot
|
|
@@ -1099,10 +1110,14 @@ function currentBranch(cwd) {
|
|
|
1099
1110
|
}
|
|
1100
1111
|
|
|
1101
1112
|
// src/git/branch.ts
|
|
1102
|
-
var
|
|
1113
|
+
var BRANCH_NAME_REGEX = /^[A-Za-z0-9._/-]{1,200}$/;
|
|
1114
|
+
var VALID_BRANCH = BRANCH_NAME_REGEX;
|
|
1103
1115
|
var TICKET_BRANCH = /^task\/[a-z0-9-]{1,80}$/;
|
|
1116
|
+
function isValidBranchName(branch) {
|
|
1117
|
+
return BRANCH_NAME_REGEX.test(branch) && !branch.includes("..") && !branch.startsWith("/") && !branch.endsWith("/");
|
|
1118
|
+
}
|
|
1104
1119
|
function assertValidBranchName(branch) {
|
|
1105
|
-
if (!
|
|
1120
|
+
if (!isValidBranchName(branch)) {
|
|
1106
1121
|
throw new CliError(
|
|
1107
1122
|
CLI_EXIT_CODES.MISCONFIGURATION,
|
|
1108
1123
|
`Invalid branch name: ${branch}`,
|
|
@@ -1264,6 +1279,32 @@ function remoteBranchExists(cwd, branchName, opts = {}) {
|
|
|
1264
1279
|
}
|
|
1265
1280
|
return stdout.trim().length > 0;
|
|
1266
1281
|
}
|
|
1282
|
+
function detectDefaultBranch(cwd) {
|
|
1283
|
+
try {
|
|
1284
|
+
const ref = execFileSync2("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], {
|
|
1285
|
+
cwd,
|
|
1286
|
+
encoding: "utf8",
|
|
1287
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1288
|
+
}).trim();
|
|
1289
|
+
const prefix = "refs/remotes/origin/";
|
|
1290
|
+
if (ref.startsWith(prefix)) {
|
|
1291
|
+
const name = ref.slice(prefix.length);
|
|
1292
|
+
if (isValidBranchName(name)) return name;
|
|
1293
|
+
}
|
|
1294
|
+
} catch {
|
|
1295
|
+
}
|
|
1296
|
+
for (const candidate of ["main", "development", "master"]) {
|
|
1297
|
+
try {
|
|
1298
|
+
execFileSync2("git", ["rev-parse", "--verify", `origin/${candidate}`], {
|
|
1299
|
+
cwd,
|
|
1300
|
+
stdio: ["ignore", "ignore", "ignore"]
|
|
1301
|
+
});
|
|
1302
|
+
return candidate;
|
|
1303
|
+
} catch {
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
return null;
|
|
1307
|
+
}
|
|
1267
1308
|
function listLocalTicketBranches(cwd) {
|
|
1268
1309
|
let stdout;
|
|
1269
1310
|
try {
|
|
@@ -1582,11 +1623,74 @@ ${args.repoOverviewBlock}
|
|
|
1582
1623
|
${args.ticketSystemPrompt}${overview}`;
|
|
1583
1624
|
}
|
|
1584
1625
|
|
|
1626
|
+
// src/agent/stream-render.ts
|
|
1627
|
+
function truncate(value, max) {
|
|
1628
|
+
const oneLine = value.replace(/\s+/g, " ").trim();
|
|
1629
|
+
return oneLine.length > max ? `${oneLine.slice(0, max - 1)}\u2026` : oneLine;
|
|
1630
|
+
}
|
|
1631
|
+
function formatToolUse(block) {
|
|
1632
|
+
const name = typeof block["name"] === "string" ? block["name"] : "tool";
|
|
1633
|
+
const input = typeof block["input"] === "object" && block["input"] !== null ? block["input"] : {};
|
|
1634
|
+
const filePath = typeof input["file_path"] === "string" ? input["file_path"] : null;
|
|
1635
|
+
const command = typeof input["command"] === "string" ? input["command"] : null;
|
|
1636
|
+
const pattern = typeof input["pattern"] === "string" ? input["pattern"] : null;
|
|
1637
|
+
if (name === "Bash" && command) return `Bash: ${truncate(command, 120)}`;
|
|
1638
|
+
if (filePath && (name === "Edit" || name === "Write" || name === "Read" || name === "MultiEdit")) {
|
|
1639
|
+
return `${name} ${filePath}`;
|
|
1640
|
+
}
|
|
1641
|
+
if (pattern && (name === "Grep" || name === "Glob")) return `${name} ${truncate(pattern, 80)}`;
|
|
1642
|
+
return name;
|
|
1643
|
+
}
|
|
1644
|
+
function summariseStreamLine(line) {
|
|
1645
|
+
const trimmed = line.trim();
|
|
1646
|
+
if (trimmed.length === 0) return { display: null, resultError: null };
|
|
1647
|
+
let evt;
|
|
1648
|
+
try {
|
|
1649
|
+
evt = JSON.parse(trimmed);
|
|
1650
|
+
} catch {
|
|
1651
|
+
return { display: null, resultError: null };
|
|
1652
|
+
}
|
|
1653
|
+
if (typeof evt !== "object" || evt === null) return { display: null, resultError: null };
|
|
1654
|
+
const e = evt;
|
|
1655
|
+
if (e["type"] === "assistant") {
|
|
1656
|
+
const message = typeof e["message"] === "object" && e["message"] !== null ? e["message"] : {};
|
|
1657
|
+
const content = message["content"];
|
|
1658
|
+
if (!Array.isArray(content)) return { display: null, resultError: null };
|
|
1659
|
+
const parts = [];
|
|
1660
|
+
for (const block of content) {
|
|
1661
|
+
if (typeof block !== "object" || block === null) continue;
|
|
1662
|
+
const b = block;
|
|
1663
|
+
if (b["type"] === "text" && typeof b["text"] === "string") {
|
|
1664
|
+
const text = b["text"].trim();
|
|
1665
|
+
if (text.length > 0) parts.push(text);
|
|
1666
|
+
} else if (b["type"] === "tool_use") {
|
|
1667
|
+
parts.push(c.cyan(` \u2192 ${formatToolUse(b)}`));
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
return { display: parts.length > 0 ? parts.join("\n") : null, resultError: null };
|
|
1671
|
+
}
|
|
1672
|
+
if (e["type"] === "result") {
|
|
1673
|
+
if (e["is_error"] === true) {
|
|
1674
|
+
const detail = typeof e["result"] === "string" && e["result"].trim().length > 0 ? e["result"].trim() : typeof e["subtype"] === "string" ? e["subtype"] : "unknown error";
|
|
1675
|
+
return {
|
|
1676
|
+
display: c.err(` \u2717 agent run errored: ${truncate(detail, 240)}`),
|
|
1677
|
+
resultError: detail
|
|
1678
|
+
};
|
|
1679
|
+
}
|
|
1680
|
+
return { display: c.dim(" agent run complete"), resultError: null };
|
|
1681
|
+
}
|
|
1682
|
+
return { display: null, resultError: null };
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1585
1685
|
// src/agent/agent-service.ts
|
|
1586
1686
|
async function runAgent(args) {
|
|
1587
1687
|
const systemPrompt = buildSystemPrompt(args);
|
|
1588
1688
|
const claude = args.claudePath ?? "claude";
|
|
1589
1689
|
const cliArgs = [
|
|
1690
|
+
"--print",
|
|
1691
|
+
"--verbose",
|
|
1692
|
+
"--output-format",
|
|
1693
|
+
"stream-json",
|
|
1590
1694
|
"--allowedTools",
|
|
1591
1695
|
allowedToolsFlag(),
|
|
1592
1696
|
"--system-prompt",
|
|
@@ -1616,11 +1720,27 @@ async function runAgent(args) {
|
|
|
1616
1720
|
logHandle?.end();
|
|
1617
1721
|
reject(err);
|
|
1618
1722
|
});
|
|
1723
|
+
let stdoutBuffer = "";
|
|
1724
|
+
const renderLine = (line) => {
|
|
1725
|
+
const summary = summariseStreamLine(line);
|
|
1726
|
+
if (summary.display !== null) process.stdout.write(`${summary.display}
|
|
1727
|
+
`);
|
|
1728
|
+
if (summary.resultError !== null) {
|
|
1729
|
+
stderrBuffer = (stderrBuffer + summary.resultError).slice(-STDERR_KEEP2);
|
|
1730
|
+
}
|
|
1731
|
+
};
|
|
1619
1732
|
child.stdout?.on("data", (chunk) => {
|
|
1620
1733
|
if (args.silent && logHandle) {
|
|
1621
1734
|
logHandle.write(chunk);
|
|
1622
|
-
|
|
1623
|
-
|
|
1735
|
+
return;
|
|
1736
|
+
}
|
|
1737
|
+
stdoutBuffer += chunk.toString("utf8");
|
|
1738
|
+
let nl = stdoutBuffer.indexOf("\n");
|
|
1739
|
+
while (nl !== -1) {
|
|
1740
|
+
const line = stdoutBuffer.slice(0, nl);
|
|
1741
|
+
stdoutBuffer = stdoutBuffer.slice(nl + 1);
|
|
1742
|
+
renderLine(line);
|
|
1743
|
+
nl = stdoutBuffer.indexOf("\n");
|
|
1624
1744
|
}
|
|
1625
1745
|
});
|
|
1626
1746
|
child.stderr?.on("data", (chunk) => {
|
|
@@ -1632,6 +1752,10 @@ async function runAgent(args) {
|
|
|
1632
1752
|
stderrBuffer = (stderrBuffer + chunk.toString("utf8")).slice(-STDERR_KEEP2);
|
|
1633
1753
|
});
|
|
1634
1754
|
child.on("close", (code) => {
|
|
1755
|
+
if (!args.silent && stdoutBuffer.trim().length > 0) {
|
|
1756
|
+
renderLine(stdoutBuffer);
|
|
1757
|
+
stdoutBuffer = "";
|
|
1758
|
+
}
|
|
1635
1759
|
logHandle?.end();
|
|
1636
1760
|
const exitCode = code ?? 0;
|
|
1637
1761
|
resolve2({ exitCode, ok: exitCode === 0, outputLogPath, stderrTail: stderrBuffer });
|
|
@@ -1639,6 +1763,42 @@ async function runAgent(args) {
|
|
|
1639
1763
|
});
|
|
1640
1764
|
}
|
|
1641
1765
|
|
|
1766
|
+
// src/agent/lint-fix.ts
|
|
1767
|
+
var LINT_FIX_GUARDRAIL = [
|
|
1768
|
+
"You are fixing a FAILED pre-push check (lint, type-check, or build) that ran",
|
|
1769
|
+
"after a code change in this repository. The failing command and its output",
|
|
1770
|
+
"are provided in the block below as DATA.",
|
|
1771
|
+
"",
|
|
1772
|
+
"Make the MINIMAL edits required to fix ONLY the reported errors. Do not change",
|
|
1773
|
+
"unrelated code, behaviour, or formatting. Do NOT disable lint rules, add ignore",
|
|
1774
|
+
"or suppression comments, or weaken types to silence the check \u2014 fix the",
|
|
1775
|
+
"underlying cause. When the reported errors are addressed, stop."
|
|
1776
|
+
].join("\n");
|
|
1777
|
+
async function runLintFix(args) {
|
|
1778
|
+
const ticketBlock = [
|
|
1779
|
+
`# Pre-push check failed \u2014 fix attempt ${args.attempt} of ${args.maxAttempts}`,
|
|
1780
|
+
"",
|
|
1781
|
+
`Command: \`${args.command}\``,
|
|
1782
|
+
"",
|
|
1783
|
+
"## Check output",
|
|
1784
|
+
"",
|
|
1785
|
+
"```",
|
|
1786
|
+
args.output.trim().length > 0 ? args.output.trim() : "(no output captured)",
|
|
1787
|
+
"```",
|
|
1788
|
+
"",
|
|
1789
|
+
"Fix the errors reported above."
|
|
1790
|
+
].join("\n");
|
|
1791
|
+
return runAgent({
|
|
1792
|
+
ticketSystemPrompt: LINT_FIX_GUARDRAIL,
|
|
1793
|
+
projectProtectedPaths: args.projectProtectedPaths,
|
|
1794
|
+
ticketBlock,
|
|
1795
|
+
cwd: args.cwd,
|
|
1796
|
+
silent: args.silent,
|
|
1797
|
+
runId: args.runId,
|
|
1798
|
+
...args.claudePath ? { claudePath: args.claudePath } : {}
|
|
1799
|
+
});
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1642
1802
|
// src/agent/pr-review-service.ts
|
|
1643
1803
|
import { spawn as spawn2 } from "child_process";
|
|
1644
1804
|
import { mkdir as mkdir7, writeFile as writeFile8 } from "fs/promises";
|
|
@@ -2025,6 +2185,7 @@ var ALLOWED_EXECUTABLES = /* @__PURE__ */ new Set(["pnpm", "npm", "yarn", "bun",
|
|
|
2025
2185
|
var DEFAULT_COMMAND = "pnpm typecheck";
|
|
2026
2186
|
var TIMEOUT_MS = 10 * 60 * 1e3;
|
|
2027
2187
|
var TAIL_BYTES = 4e3;
|
|
2188
|
+
var FIX_CONTEXT_BYTES = 16e3;
|
|
2028
2189
|
function parseArgv(command) {
|
|
2029
2190
|
return command.trim().split(/\s+/).filter((s) => s.length > 0);
|
|
2030
2191
|
}
|
|
@@ -2058,25 +2219,31 @@ async function runProjectTest(args) {
|
|
|
2058
2219
|
let buf = "";
|
|
2059
2220
|
const append = (chunk) => {
|
|
2060
2221
|
buf += chunk.toString("utf8");
|
|
2061
|
-
if (buf.length >
|
|
2062
|
-
buf = buf.slice(-
|
|
2222
|
+
if (buf.length > FIX_CONTEXT_BYTES * 2) {
|
|
2223
|
+
buf = buf.slice(-FIX_CONTEXT_BYTES);
|
|
2063
2224
|
}
|
|
2064
2225
|
};
|
|
2065
|
-
child.stdout?.on("data",
|
|
2066
|
-
|
|
2226
|
+
child.stdout?.on("data", (chunk) => {
|
|
2227
|
+
append(chunk);
|
|
2228
|
+
if (!args.silent) process.stdout.write(chunk);
|
|
2229
|
+
});
|
|
2230
|
+
child.stderr?.on("data", (chunk) => {
|
|
2231
|
+
append(chunk);
|
|
2232
|
+
if (!args.silent) process.stderr.write(chunk);
|
|
2233
|
+
});
|
|
2067
2234
|
const timeoutHandle = setTimeout(() => {
|
|
2068
2235
|
child.kill("SIGKILL");
|
|
2069
2236
|
}, TIMEOUT_MS);
|
|
2070
2237
|
child.on("close", (code) => {
|
|
2071
2238
|
clearTimeout(timeoutHandle);
|
|
2072
2239
|
const durationMs = Date.now() - startedAt;
|
|
2073
|
-
const tail = buf.slice(-TAIL_BYTES);
|
|
2074
2240
|
resolve2({
|
|
2075
2241
|
ok: code === 0,
|
|
2076
2242
|
exitCode: code,
|
|
2077
2243
|
durationMs,
|
|
2078
2244
|
command,
|
|
2079
|
-
tail
|
|
2245
|
+
tail: buf.slice(-TAIL_BYTES),
|
|
2246
|
+
fixContext: buf.slice(-FIX_CONTEXT_BYTES)
|
|
2080
2247
|
});
|
|
2081
2248
|
});
|
|
2082
2249
|
child.on("error", () => {
|
|
@@ -2086,7 +2253,8 @@ async function runProjectTest(args) {
|
|
|
2086
2253
|
exitCode: null,
|
|
2087
2254
|
durationMs: Date.now() - startedAt,
|
|
2088
2255
|
command,
|
|
2089
|
-
tail: buf.slice(-TAIL_BYTES)
|
|
2256
|
+
tail: buf.slice(-TAIL_BYTES),
|
|
2257
|
+
fixContext: buf.slice(-FIX_CONTEXT_BYTES)
|
|
2090
2258
|
});
|
|
2091
2259
|
});
|
|
2092
2260
|
});
|
|
@@ -2292,11 +2460,20 @@ async function buildWorkContext(opts) {
|
|
|
2292
2460
|
);
|
|
2293
2461
|
}
|
|
2294
2462
|
const localCfg = await readLocalConfig();
|
|
2463
|
+
const cwd = findRepoRoot();
|
|
2464
|
+
if (opts.baseBranch !== void 0 && !isValidBranchName(opts.baseBranch)) {
|
|
2465
|
+
throw new CliError(
|
|
2466
|
+
CLI_EXIT_CODES.MISCONFIGURATION,
|
|
2467
|
+
`Invalid --base-branch value: ${opts.baseBranch}`,
|
|
2468
|
+
'Branch names must contain only [A-Za-z0-9._/-], no "..", and no leading/trailing slash.'
|
|
2469
|
+
);
|
|
2470
|
+
}
|
|
2471
|
+
const baseBranch = opts.baseBranch ?? project.cli_base_branch ?? detectDefaultBranch(cwd) ?? "main";
|
|
2295
2472
|
return {
|
|
2296
2473
|
project,
|
|
2297
2474
|
localCfg,
|
|
2298
|
-
cwd
|
|
2299
|
-
baseBranch
|
|
2475
|
+
cwd,
|
|
2476
|
+
baseBranch,
|
|
2300
2477
|
silent: !!opts.silent || localCfg.silent
|
|
2301
2478
|
};
|
|
2302
2479
|
}
|
|
@@ -2310,6 +2487,12 @@ function registerWork(program2) {
|
|
|
2310
2487
|
).option(
|
|
2311
2488
|
"--no-auto-review",
|
|
2312
2489
|
"Skip the post-PR /qa + /security auto-review. The PR is left open for a human to review and merge."
|
|
2490
|
+
).option(
|
|
2491
|
+
"--no-fix-lint",
|
|
2492
|
+
"Disable the auto-fix loop for the pre-push check \u2014 fail immediately on lint/type errors (legacy behaviour)."
|
|
2493
|
+
).option(
|
|
2494
|
+
"--base-branch <name>",
|
|
2495
|
+
"Override the base branch for this run. Wins over the project's configured cli_base_branch and the git auto-detect fallback. Typically supplied by the dashboard listener on each spawn."
|
|
2313
2496
|
).option("--schedule-id <id>", "Internal: schedule id when invoked from a scheduled task").action(async (ticketId, opts) => {
|
|
2314
2497
|
await runWork(ticketId, opts);
|
|
2315
2498
|
});
|
|
@@ -2683,10 +2866,77 @@ ${installResult.tail}`.slice(
|
|
|
2683
2866
|
)
|
|
2684
2867
|
);
|
|
2685
2868
|
}
|
|
2869
|
+
const MAX_FIX_ATTEMPTS = 2;
|
|
2870
|
+
const fixEnabled = opts.fixLint !== false;
|
|
2686
2871
|
if (!silent)
|
|
2687
|
-
process.stdout.write(c.dim(` running pre-push
|
|
2872
|
+
process.stdout.write(c.dim(` running pre-push check: ${testCommand ?? "pnpm typecheck"}
|
|
2688
2873
|
`));
|
|
2689
|
-
|
|
2874
|
+
let testResult = await runProjectTest({ cwd, command: testCommand, silent });
|
|
2875
|
+
let fixAttempts = 0;
|
|
2876
|
+
while (!testResult.ok && fixEnabled && fixAttempts < MAX_FIX_ATTEMPTS) {
|
|
2877
|
+
fixAttempts += 1;
|
|
2878
|
+
if (!silent)
|
|
2879
|
+
process.stdout.write(
|
|
2880
|
+
c.warn(
|
|
2881
|
+
` \u2717 pre-push check failed (exit ${testResult.exitCode}) \u2014 attempting fix ${fixAttempts}/${MAX_FIX_ATTEMPTS}
|
|
2882
|
+
`
|
|
2883
|
+
)
|
|
2884
|
+
);
|
|
2885
|
+
const fixResult = await runLintFix({
|
|
2886
|
+
command: testResult.command,
|
|
2887
|
+
output: testResult.fixContext,
|
|
2888
|
+
attempt: fixAttempts,
|
|
2889
|
+
maxAttempts: MAX_FIX_ATTEMPTS,
|
|
2890
|
+
projectProtectedPaths: detail.project_protected_paths,
|
|
2891
|
+
cwd,
|
|
2892
|
+
silent,
|
|
2893
|
+
runId,
|
|
2894
|
+
...ctx.localCfg.claude_path ? { claudePath: ctx.localCfg.claude_path } : {}
|
|
2895
|
+
}).catch((err) => {
|
|
2896
|
+
if (!silent) process.stdout.write(c.dim(` lint-fix agent could not run: ${err.message}
|
|
2897
|
+
`));
|
|
2898
|
+
return null;
|
|
2899
|
+
});
|
|
2900
|
+
if (!fixResult || !fixResult.ok) break;
|
|
2901
|
+
const fixGuardrail = checkDiff({
|
|
2902
|
+
cwd,
|
|
2903
|
+
projectProtectedPaths: detail.project_protected_paths
|
|
2904
|
+
});
|
|
2905
|
+
if (fixGuardrail.violation) {
|
|
2906
|
+
discardWorkingTreeChanges(cwd);
|
|
2907
|
+
try {
|
|
2908
|
+
checkoutBranch(cwd, baseBranch);
|
|
2909
|
+
} catch {
|
|
2910
|
+
}
|
|
2911
|
+
deleteLocalBranch(cwd, branchName);
|
|
2912
|
+
if (!silent) {
|
|
2913
|
+
process.stdout.write(
|
|
2914
|
+
`${c.err("\u2717 Guardrail blocked")} \u2014 lint-fix agent attempted to modify protected files:
|
|
2915
|
+
`
|
|
2916
|
+
);
|
|
2917
|
+
for (const p of fixGuardrail.offendingPaths) {
|
|
2918
|
+
process.stdout.write(` - ${p}
|
|
2919
|
+
`);
|
|
2920
|
+
}
|
|
2921
|
+
process.stdout.write(c.dim(" Working tree restored. Branch deleted.\n"));
|
|
2922
|
+
}
|
|
2923
|
+
await apiCall("POST", "/api/v1/cli/me/runs", {
|
|
2924
|
+
body: {
|
|
2925
|
+
ticket_id: detail.id,
|
|
2926
|
+
schedule_id: opts.scheduleId,
|
|
2927
|
+
event: "guardrail_blocked",
|
|
2928
|
+
claude_session_id: runId,
|
|
2929
|
+
offending_paths: fixGuardrail.offendingPaths
|
|
2930
|
+
}
|
|
2931
|
+
});
|
|
2932
|
+
throw new CliError(
|
|
2933
|
+
CLI_EXIT_CODES.GUARDRAIL_BLOCKED,
|
|
2934
|
+
`Lint-fix agent attempted to modify ${fixGuardrail.offendingPaths.length} protected file(s)`
|
|
2935
|
+
);
|
|
2936
|
+
}
|
|
2937
|
+
if (!silent) process.stdout.write(c.dim(" re-running pre-push check\u2026\n"));
|
|
2938
|
+
testResult = await runProjectTest({ cwd, command: testCommand, silent });
|
|
2939
|
+
}
|
|
2690
2940
|
if (!testResult.ok) {
|
|
2691
2941
|
discardWorkingTreeChanges(cwd);
|
|
2692
2942
|
try {
|
|
@@ -2706,16 +2956,21 @@ ${installResult.tail}`.slice(
|
|
|
2706
2956
|
});
|
|
2707
2957
|
if (!silent) {
|
|
2708
2958
|
process.stdout.write(
|
|
2709
|
-
`${c.err("\u2717 Pre-push
|
|
2959
|
+
`${c.err("\u2717 Pre-push check failed")} (exit ${testResult.exitCode})` + (fixAttempts > 0 ? ` after ${fixAttempts} fix attempt${fixAttempts === 1 ? "" : "s"}` : "") + ` \u2014 branch deleted, no push.
|
|
2710
2960
|
`
|
|
2711
2961
|
);
|
|
2712
|
-
if (testResult.tail.trim().length > 0) {
|
|
2713
|
-
process.stdout.write(c.dim(testResult.tail.slice(-1e3) + "\n"));
|
|
2714
|
-
}
|
|
2715
2962
|
}
|
|
2716
2963
|
throw new CliError(
|
|
2717
2964
|
CLI_EXIT_CODES.GENERIC_ERROR,
|
|
2718
|
-
`Pre-push
|
|
2965
|
+
`Pre-push check failed: ${testResult.command} (exit ${testResult.exitCode})`
|
|
2966
|
+
);
|
|
2967
|
+
}
|
|
2968
|
+
if (fixAttempts > 0 && !silent) {
|
|
2969
|
+
process.stdout.write(
|
|
2970
|
+
c.dim(
|
|
2971
|
+
` pre-push check passed after ${fixAttempts} fix attempt${fixAttempts === 1 ? "" : "s"}
|
|
2972
|
+
`
|
|
2973
|
+
)
|
|
2719
2974
|
);
|
|
2720
2975
|
}
|
|
2721
2976
|
await apiCall("POST", "/api/v1/cli/me/runs", {
|
|
@@ -3159,12 +3414,22 @@ function registerMultiWork(program2) {
|
|
|
3159
3414
|
).option(
|
|
3160
3415
|
"--reset",
|
|
3161
3416
|
"DESTRUCTIVE: discard local working-tree changes before the first ticket. Requires --confirm in non-TTY contexts."
|
|
3162
|
-
).option("--confirm", "Confirm --reset in non-TTY (silent / scheduled-task) contexts.").option(
|
|
3417
|
+
).option("--confirm", "Confirm --reset in non-TTY (silent / scheduled-task) contexts.").option(
|
|
3418
|
+
"--base-branch <name>",
|
|
3419
|
+
"Override the base branch for the whole batch. Wins over the project's configured cli_base_branch and the git auto-detect fallback."
|
|
3420
|
+
).option(
|
|
3421
|
+
"--no-fix-lint",
|
|
3422
|
+
"Disable the pre-push check auto-fix loop \u2014 fail immediately on lint/type errors (legacy behaviour)."
|
|
3423
|
+
).option("--schedule-id <id>", "Internal: schedule id when invoked from a scheduled task").action(async (opts) => {
|
|
3163
3424
|
await runMultiWork(opts);
|
|
3164
3425
|
});
|
|
3165
3426
|
}
|
|
3166
3427
|
async function runMultiWork(opts) {
|
|
3167
|
-
const ctx = await buildWorkContext({
|
|
3428
|
+
const ctx = await buildWorkContext({
|
|
3429
|
+
max: opts.max,
|
|
3430
|
+
silent: opts.silent,
|
|
3431
|
+
...opts.baseBranch ? { baseBranch: opts.baseBranch } : {}
|
|
3432
|
+
});
|
|
3168
3433
|
const max = Math.max(1, parseInt(opts.max, 10) || 10);
|
|
3169
3434
|
let firstIteration = true;
|
|
3170
3435
|
const innerOpts = {
|
|
@@ -3175,7 +3440,9 @@ async function runMultiWork(opts) {
|
|
|
3175
3440
|
silent: opts.silent,
|
|
3176
3441
|
scheduleId: opts.scheduleId,
|
|
3177
3442
|
reset: opts.reset,
|
|
3178
|
-
confirm: opts.confirm
|
|
3443
|
+
confirm: opts.confirm,
|
|
3444
|
+
fixLint: opts.fixLint,
|
|
3445
|
+
...opts.baseBranch ? { baseBranch: opts.baseBranch } : {}
|
|
3179
3446
|
};
|
|
3180
3447
|
const results = [];
|
|
3181
3448
|
let processed = 0;
|
|
@@ -3314,7 +3581,7 @@ async function runResume(ticketRef, opts) {
|
|
|
3314
3581
|
"Run 'task link' first."
|
|
3315
3582
|
);
|
|
3316
3583
|
}
|
|
3317
|
-
const baseBranch = project.cli_base_branch ?? "
|
|
3584
|
+
const baseBranch = project.cli_base_branch ?? detectDefaultBranch(cwd) ?? "main";
|
|
3318
3585
|
const silent = !!opts.silent;
|
|
3319
3586
|
assertBaseBranch(cwd, baseBranch);
|
|
3320
3587
|
const access2 = await apiCallOrThrow("GET", "/api/v1/cli/access");
|
|
@@ -4676,6 +4943,12 @@ function registerFastTrack(program2) {
|
|
|
4676
4943
|
).option("--confirm", "Confirm --reset in non-TTY (silent / scheduled-task) contexts").option(
|
|
4677
4944
|
"--no-auto-review",
|
|
4678
4945
|
"Skip the post-PR /qa + /security auto-review. Each PR is left open for a human to review and merge."
|
|
4946
|
+
).option(
|
|
4947
|
+
"--no-fix-lint",
|
|
4948
|
+
"Disable the pre-push check auto-fix loop \u2014 fail immediately on lint/type errors (legacy behaviour)."
|
|
4949
|
+
).option(
|
|
4950
|
+
"--base-branch <name>",
|
|
4951
|
+
"Override the base branch for this run. Wins over the project's configured cli_base_branch and the git auto-detect fallback."
|
|
4679
4952
|
).option("--schedule-id <id>", "Internal: schedule id when invoked from a scheduled task").action(async (opts) => {
|
|
4680
4953
|
await runFastTrack(opts);
|
|
4681
4954
|
});
|
|
@@ -4702,7 +4975,11 @@ async function runFastTrack(opts) {
|
|
|
4702
4975
|
const apiUrl = (opts.apiUrl ?? process.env["TASK_API_URL"] ?? creds.api_url ?? localCfg.api_url ?? linkedProject.api_url ?? "http://localhost:3400").replace(/\/$/, "");
|
|
4703
4976
|
const max = Math.max(1, parseInt(opts.max, 10) || 1);
|
|
4704
4977
|
const silent = !!opts.silent || localCfg.silent;
|
|
4705
|
-
const ctx = await buildWorkContext({
|
|
4978
|
+
const ctx = await buildWorkContext({
|
|
4979
|
+
max: "1",
|
|
4980
|
+
silent: opts.silent,
|
|
4981
|
+
...opts.baseBranch ? { baseBranch: opts.baseBranch } : {}
|
|
4982
|
+
});
|
|
4706
4983
|
const api = new AutopilotApi({ apiUrl, creds });
|
|
4707
4984
|
const claudePath = localCfg.claude_path ?? void 0;
|
|
4708
4985
|
let firstIteration = true;
|
|
@@ -4721,7 +4998,9 @@ async function runFastTrack(opts) {
|
|
|
4721
4998
|
// Commander sets opts.autoReview to `false` only when --no-auto-review
|
|
4722
4999
|
// is passed; otherwise it's `undefined` and processOneTicketImpl
|
|
4723
5000
|
// treats `undefined` as "auto-review enabled" (opts.autoReview !== false).
|
|
4724
|
-
...opts.autoReview === false ? { autoReview: false } : {}
|
|
5001
|
+
...opts.autoReview === false ? { autoReview: false } : {},
|
|
5002
|
+
...opts.fixLint === false ? { fixLint: false } : {},
|
|
5003
|
+
...opts.baseBranch ? { baseBranch: opts.baseBranch } : {}
|
|
4725
5004
|
};
|
|
4726
5005
|
const outcome = await fastTrackOneTicket({
|
|
4727
5006
|
api,
|
|
@@ -4939,7 +5218,7 @@ async function runPrTest(opts) {
|
|
|
4939
5218
|
"Run 'task link' first."
|
|
4940
5219
|
);
|
|
4941
5220
|
}
|
|
4942
|
-
const baseBranch = project.cli_base_branch ?? "
|
|
5221
|
+
const baseBranch = project.cli_base_branch ?? detectDefaultBranch(cwd) ?? "main";
|
|
4943
5222
|
const silent = !!opts.silent;
|
|
4944
5223
|
if (!silent) process.stdout.write(`${c.dim(`Step 1/6: assertBaseBranch (${baseBranch})\u2026`)}
|
|
4945
5224
|
`);
|
|
@@ -6037,15 +6316,21 @@ function registerConfig(program2) {
|
|
|
6037
6316
|
|
|
6038
6317
|
// src/commands/doctor.ts
|
|
6039
6318
|
import { execFileSync as execFileSync12 } from "child_process";
|
|
6319
|
+
import { existsSync } from "fs";
|
|
6040
6320
|
import { readFile as readFile8, writeFile as writeFile13 } from "fs/promises";
|
|
6041
|
-
import { join as join15 } from "path";
|
|
6321
|
+
import { isAbsolute, join as join15 } from "path";
|
|
6042
6322
|
import { request as request6 } from "undici";
|
|
6043
6323
|
var ALLOWED_TEST_EXECUTABLES = /* @__PURE__ */ new Set(["pnpm", "npm", "yarn", "bun", "node", "npx"]);
|
|
6324
|
+
var PACKAGE_MANAGERS = /* @__PURE__ */ new Set(["pnpm", "npm", "yarn", "bun"]);
|
|
6044
6325
|
var DEFAULT_TEST_COMMAND = "pnpm typecheck";
|
|
6326
|
+
var INSTALL_TIMEOUT_MS2 = 10 * 60 * 1e3;
|
|
6045
6327
|
function registerDoctor(program2) {
|
|
6046
6328
|
program2.command("doctor").description(
|
|
6047
6329
|
"Diagnose your CLI setup \u2014 Identity (who you are signed in as) first, then Setup checks"
|
|
6048
|
-
).option(
|
|
6330
|
+
).option(
|
|
6331
|
+
"--fix",
|
|
6332
|
+
"attempt to auto-remediate fixable problems (install missing dependencies, add a typecheck script)"
|
|
6333
|
+
).action(async (opts) => {
|
|
6049
6334
|
const checks = [];
|
|
6050
6335
|
const creds = await readCredentials();
|
|
6051
6336
|
let accessLite = null;
|
|
@@ -6101,7 +6386,15 @@ function registerDoctor(program2) {
|
|
|
6101
6386
|
});
|
|
6102
6387
|
}
|
|
6103
6388
|
if (project) {
|
|
6104
|
-
const
|
|
6389
|
+
const explicitlyConfigured = typeof project.cli_base_branch === "string";
|
|
6390
|
+
checks.push({
|
|
6391
|
+
group: "setup",
|
|
6392
|
+
name: "cli base branch configured",
|
|
6393
|
+
ok: explicitlyConfigured,
|
|
6394
|
+
detail: explicitlyConfigured ? `set to "${project.cli_base_branch}"` : "no value set on the project \u2014 using auto-detect fallback",
|
|
6395
|
+
remediation: explicitlyConfigured ? void 0 : `Open the dashboard \u2192 Project settings \u2192 Agentic CLI and set the base branch (typically "main" or "development"), then re-run \`task link\`.`
|
|
6396
|
+
});
|
|
6397
|
+
const baseBranch = project.cli_base_branch ?? detectDefaultBranch(root) ?? "main";
|
|
6105
6398
|
try {
|
|
6106
6399
|
const exists = remoteBranchExists(root, baseBranch);
|
|
6107
6400
|
checks.push({
|
|
@@ -6109,7 +6402,7 @@ function registerDoctor(program2) {
|
|
|
6109
6402
|
name: "base branch on origin",
|
|
6110
6403
|
ok: exists,
|
|
6111
6404
|
detail: exists ? `origin/${baseBranch} reachable` : `origin has no branch "${baseBranch}"`,
|
|
6112
|
-
remediation: exists ? void 0 : `push it (\`git push origin ${baseBranch}\`) or
|
|
6405
|
+
remediation: exists ? void 0 : `push it (\`git push origin ${baseBranch}\`) or set the project's base branch in the dashboard (Project settings \u2192 Agentic CLI), or pass --base-branch <name> on the next \`task work\``
|
|
6113
6406
|
});
|
|
6114
6407
|
} catch (err) {
|
|
6115
6408
|
checks.push({
|
|
@@ -6244,15 +6537,47 @@ async function checkPrePushTest(root, configuredCommand, fix) {
|
|
|
6244
6537
|
remediation: "update projects.cli_test_command via the dashboard"
|
|
6245
6538
|
};
|
|
6246
6539
|
}
|
|
6247
|
-
const scriptName =
|
|
6540
|
+
const { scriptName, subdir } = resolveTestTarget(argv);
|
|
6541
|
+
const targetDir = subdir ? join15(root, subdir) : root;
|
|
6542
|
+
const where = subdir ? `${subdir}/` : "repo root";
|
|
6543
|
+
const inSubdir = subdir ? ` in ${subdir}/` : "";
|
|
6544
|
+
if (PACKAGE_MANAGERS.has(exe)) {
|
|
6545
|
+
const nodeModules = join15(targetDir, "node_modules");
|
|
6546
|
+
if (!existsSync(nodeModules)) {
|
|
6547
|
+
if (fix) {
|
|
6548
|
+
try {
|
|
6549
|
+
execFileSync12(exe, ["install"], {
|
|
6550
|
+
cwd: targetDir,
|
|
6551
|
+
stdio: "inherit",
|
|
6552
|
+
timeout: INSTALL_TIMEOUT_MS2
|
|
6553
|
+
});
|
|
6554
|
+
} catch (err) {
|
|
6555
|
+
return {
|
|
6556
|
+
name: "pre-push test",
|
|
6557
|
+
ok: false,
|
|
6558
|
+
detail: `dependencies missing (${where}) \u2014 \`${exe} install\` failed: ${err.message}`,
|
|
6559
|
+
remediation: `run \`${exe} install\`${inSubdir} manually, then re-run \`task doctor\``
|
|
6560
|
+
};
|
|
6561
|
+
}
|
|
6562
|
+
}
|
|
6563
|
+
if (!existsSync(nodeModules)) {
|
|
6564
|
+
return {
|
|
6565
|
+
name: "pre-push test",
|
|
6566
|
+
ok: false,
|
|
6567
|
+
detail: `dependencies not installed \u2014 ${where} has no node_modules, so "${command}" will fail`,
|
|
6568
|
+
remediation: `run \`${exe} install\`${inSubdir}, or re-run \`task doctor --fix\` to install automatically`
|
|
6569
|
+
};
|
|
6570
|
+
}
|
|
6571
|
+
}
|
|
6572
|
+
}
|
|
6248
6573
|
if (!scriptName) {
|
|
6249
6574
|
return {
|
|
6250
6575
|
name: "pre-push test",
|
|
6251
6576
|
ok: true,
|
|
6252
|
-
detail: `${command} (non-script executable, not statically verifiable)`
|
|
6577
|
+
detail: PACKAGE_MANAGERS.has(exe) ? `${command} (dependencies present; script not statically verifiable)` : `${command} (non-script executable, not statically verifiable)`
|
|
6253
6578
|
};
|
|
6254
6579
|
}
|
|
6255
|
-
const pkgPath = join15(
|
|
6580
|
+
const pkgPath = join15(targetDir, "package.json");
|
|
6256
6581
|
let pkgRaw;
|
|
6257
6582
|
try {
|
|
6258
6583
|
pkgRaw = await readFile8(pkgPath, "utf8");
|
|
@@ -6260,7 +6585,7 @@ async function checkPrePushTest(root, configuredCommand, fix) {
|
|
|
6260
6585
|
return {
|
|
6261
6586
|
name: "pre-push test",
|
|
6262
6587
|
ok: false,
|
|
6263
|
-
detail: `no package.json
|
|
6588
|
+
detail: `no package.json in ${where} for "${command}"`,
|
|
6264
6589
|
remediation: `add a package.json with a "${scriptName}" script, or set projects.cli_test_command in the dashboard`
|
|
6265
6590
|
};
|
|
6266
6591
|
}
|
|
@@ -6271,7 +6596,7 @@ async function checkPrePushTest(root, configuredCommand, fix) {
|
|
|
6271
6596
|
return {
|
|
6272
6597
|
name: "pre-push test",
|
|
6273
6598
|
ok: false,
|
|
6274
|
-
detail: `package.json is invalid JSON: ${err.message}`
|
|
6599
|
+
detail: `package.json in ${where} is invalid JSON: ${err.message}`
|
|
6275
6600
|
};
|
|
6276
6601
|
}
|
|
6277
6602
|
const scripts = pkg.scripts ?? {};
|
|
@@ -6295,29 +6620,57 @@ async function checkPrePushTest(root, configuredCommand, fix) {
|
|
|
6295
6620
|
detail: `added "typecheck": "tsc --noEmit" to ${pkgPath}`
|
|
6296
6621
|
};
|
|
6297
6622
|
}
|
|
6298
|
-
const remediation = isDefaultTypecheck ? hasTypeScript ? 're-run with --fix to add "typecheck": "tsc --noEmit" to package.json' : 'add a "typecheck" script to package.json, or set a different cli_test_command in the dashboard' : `add a "${scriptName}" script to package.json, or update cli_test_command in the dashboard`;
|
|
6623
|
+
const remediation = isDefaultTypecheck ? hasTypeScript ? 're-run with --fix to add "typecheck": "tsc --noEmit" to package.json' : 'add a "typecheck" script to package.json, or set a different cli_test_command in the dashboard' : `add a "${scriptName}" script to ${where} package.json, or update cli_test_command in the dashboard`;
|
|
6299
6624
|
return {
|
|
6300
6625
|
name: "pre-push test",
|
|
6301
6626
|
ok: false,
|
|
6302
|
-
detail: `"${scriptName}" script missing \u2014 "${command}" will
|
|
6627
|
+
detail: `"${scriptName}" script missing from ${where} package.json \u2014 "${command}" will fail`,
|
|
6303
6628
|
remediation
|
|
6304
6629
|
};
|
|
6305
6630
|
}
|
|
6306
|
-
function
|
|
6631
|
+
function resolveTestTarget(argv) {
|
|
6307
6632
|
const [exe, ...rest] = argv;
|
|
6308
|
-
|
|
6309
|
-
const
|
|
6310
|
-
|
|
6311
|
-
|
|
6312
|
-
const
|
|
6313
|
-
if (
|
|
6314
|
-
|
|
6315
|
-
|
|
6633
|
+
let subdir = null;
|
|
6634
|
+
const positional = [];
|
|
6635
|
+
let opaque = false;
|
|
6636
|
+
for (let i = 0; i < rest.length; i++) {
|
|
6637
|
+
const tok = rest[i];
|
|
6638
|
+
if (tok === "--prefix" || tok === "-C" || tok === "--dir") {
|
|
6639
|
+
const val = rest[i + 1];
|
|
6640
|
+
if (val !== void 0) {
|
|
6641
|
+
subdir = val;
|
|
6642
|
+
i++;
|
|
6643
|
+
}
|
|
6644
|
+
continue;
|
|
6645
|
+
}
|
|
6646
|
+
const eq = tok.match(/^(?:--prefix|--dir)=(.+)$/);
|
|
6647
|
+
if (eq) {
|
|
6648
|
+
subdir = eq[1];
|
|
6649
|
+
continue;
|
|
6650
|
+
}
|
|
6651
|
+
if (tok.startsWith("-")) {
|
|
6652
|
+
opaque = true;
|
|
6653
|
+
continue;
|
|
6654
|
+
}
|
|
6655
|
+
positional.push(tok);
|
|
6316
6656
|
}
|
|
6657
|
+
if (subdir !== null && (isAbsolute(subdir) || subdir.split(/[\\/]/).includes(".."))) {
|
|
6658
|
+
subdir = null;
|
|
6659
|
+
opaque = true;
|
|
6660
|
+
}
|
|
6661
|
+
const scriptName = opaque ? null : resolveScriptName(exe, positional);
|
|
6662
|
+
return { scriptName, subdir, opaque };
|
|
6663
|
+
}
|
|
6664
|
+
function resolveScriptName(exe, positional) {
|
|
6665
|
+
if (!exe || positional.length === 0) return null;
|
|
6317
6666
|
if (exe === "npm") {
|
|
6318
|
-
if (
|
|
6667
|
+
if (positional[0] === "run" || positional[0] === "run-script") return positional[1] ?? null;
|
|
6319
6668
|
return null;
|
|
6320
6669
|
}
|
|
6670
|
+
if (exe === "pnpm" || exe === "yarn" || exe === "bun") {
|
|
6671
|
+
if (positional[0] === "run") return positional[1] ?? null;
|
|
6672
|
+
return positional[0] ?? null;
|
|
6673
|
+
}
|
|
6321
6674
|
return null;
|
|
6322
6675
|
}
|
|
6323
6676
|
function detectIndent(raw) {
|
|
@@ -6337,7 +6690,7 @@ function checkBinary(name, command) {
|
|
|
6337
6690
|
}
|
|
6338
6691
|
|
|
6339
6692
|
// src/commands/version.ts
|
|
6340
|
-
var CLI_VERSION = true ? "0.2.
|
|
6693
|
+
var CLI_VERSION = true ? "0.2.31" : "0.0.0-dev";
|
|
6341
6694
|
function registerVersion(program2) {
|
|
6342
6695
|
program2.command("version").description("Print the CLI version").action(() => {
|
|
6343
6696
|
process.stdout.write(CLI_VERSION + "\n");
|