@inteeka/task-cli 0.2.26 → 0.2.28
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 +504 -63
- package/dist/cli.js.map +1 -1
- package/package.json +3 -3
package/dist/cli.js
CHANGED
|
@@ -171,6 +171,17 @@ var CLI_ALLOWED_TOOLS = Object.freeze([
|
|
|
171
171
|
"Bash(pnpm typecheck*)",
|
|
172
172
|
"Bash(pnpm lint*)"
|
|
173
173
|
]);
|
|
174
|
+
var CLI_REVIEW_ALLOWED_TOOLS = Object.freeze([
|
|
175
|
+
"Read",
|
|
176
|
+
"Glob",
|
|
177
|
+
"Grep",
|
|
178
|
+
"Bash(git diff:*)",
|
|
179
|
+
"Bash(git log:*)",
|
|
180
|
+
"Bash(git show:*)",
|
|
181
|
+
"Bash(git status)",
|
|
182
|
+
"Bash(git branch:*)",
|
|
183
|
+
"Bash(gh pr view:*)"
|
|
184
|
+
]);
|
|
174
185
|
var CLI_DEVICE_POLL_INTERVAL_SECONDS = 5;
|
|
175
186
|
var CLI_DEVICE_POLL_SLOW_DOWN_INCREMENT_SECONDS = 5;
|
|
176
187
|
var CLI_ACCESS_TOKEN_TTL_SECONDS = 60 * 60;
|
|
@@ -193,6 +204,15 @@ var CLI_AUDIT_ACTIONS = Object.freeze([
|
|
|
193
204
|
"cli.run.pr_failed",
|
|
194
205
|
"cli.run.push_failed",
|
|
195
206
|
"cli.run.resumed",
|
|
207
|
+
"cli.run.auto_review_passed",
|
|
208
|
+
"cli.run.auto_review_failed",
|
|
209
|
+
"cli.run.auto_review_errored",
|
|
210
|
+
// Server-side audit actions for the auto-review verdict path. Emitted
|
|
211
|
+
// by /api/v1/cli/me/tickets/[id]/pull-requests/[prNumber]/auto-review-verdict.
|
|
212
|
+
"git.pr.auto_review_passed",
|
|
213
|
+
"git.pr.auto_review_failed",
|
|
214
|
+
"git.pr.auto_review_sql_held",
|
|
215
|
+
"git.pr.auto_review_sql_hold_dispatched",
|
|
196
216
|
"cli.work.pr_recovered",
|
|
197
217
|
"cli.schedule.created",
|
|
198
218
|
"cli.schedule.paused",
|
|
@@ -1523,6 +1543,10 @@ var ALLOWED_TOOLS = CLI_ALLOWED_TOOLS;
|
|
|
1523
1543
|
function allowedToolsFlag() {
|
|
1524
1544
|
return ALLOWED_TOOLS.join(",");
|
|
1525
1545
|
}
|
|
1546
|
+
var REVIEW_ALLOWED_TOOLS = CLI_REVIEW_ALLOWED_TOOLS;
|
|
1547
|
+
function reviewAllowedToolsFlag() {
|
|
1548
|
+
return REVIEW_ALLOWED_TOOLS.join(",");
|
|
1549
|
+
}
|
|
1526
1550
|
|
|
1527
1551
|
// src/agent/system-prompt.ts
|
|
1528
1552
|
function buildSystemPrompt(args) {
|
|
@@ -1581,7 +1605,7 @@ async function runAgent(args) {
|
|
|
1581
1605
|
logHandle = createWriteStream(outputLogPath, { flags: "a" });
|
|
1582
1606
|
}
|
|
1583
1607
|
let stderrBuffer = "";
|
|
1584
|
-
const
|
|
1608
|
+
const STDERR_KEEP2 = 4e3;
|
|
1585
1609
|
return new Promise((resolve2, reject) => {
|
|
1586
1610
|
const child = spawn(claude, cliArgs, {
|
|
1587
1611
|
cwd: args.cwd,
|
|
@@ -1605,7 +1629,7 @@ async function runAgent(args) {
|
|
|
1605
1629
|
} else {
|
|
1606
1630
|
process.stderr.write(chunk);
|
|
1607
1631
|
}
|
|
1608
|
-
stderrBuffer = (stderrBuffer + chunk.toString("utf8")).slice(-
|
|
1632
|
+
stderrBuffer = (stderrBuffer + chunk.toString("utf8")).slice(-STDERR_KEEP2);
|
|
1609
1633
|
});
|
|
1610
1634
|
child.on("close", (code) => {
|
|
1611
1635
|
logHandle?.end();
|
|
@@ -1615,6 +1639,297 @@ async function runAgent(args) {
|
|
|
1615
1639
|
});
|
|
1616
1640
|
}
|
|
1617
1641
|
|
|
1642
|
+
// src/agent/pr-review-service.ts
|
|
1643
|
+
import { spawn as spawn2 } from "child_process";
|
|
1644
|
+
import { mkdir as mkdir7, writeFile as writeFile8 } from "fs/promises";
|
|
1645
|
+
import { homedir as homedir5 } from "os";
|
|
1646
|
+
import { join as join8 } from "path";
|
|
1647
|
+
|
|
1648
|
+
// src/agent/pr-review-prompt.ts
|
|
1649
|
+
var QA_BLOCK = `# /qa lens \u2014 final quality gate
|
|
1650
|
+
|
|
1651
|
+
You are acting as the QA lead reviewer. Score the diff against these checks:
|
|
1652
|
+
|
|
1653
|
+
1. Contract integrity \u2014 does the change honour the public API / UI contract that callers depend on? Any breaking change without a migration is an automatic NO-GO.
|
|
1654
|
+
2. Multi-tenant safety \u2014 every query touching tenant data must filter by organisation_id. Any new query missing this filter is a NO-GO.
|
|
1655
|
+
3. Validation \u2014 every new mutation endpoint has a Zod schema with .strict() on updates, max-lengths on free text, UUID validation on path params. Missing \u2192 NO-GO.
|
|
1656
|
+
4. Soft delete \u2014 never hard-delete user-facing entities. Hard delete \u2192 NO-GO.
|
|
1657
|
+
5. UI accessibility \u2014 new interactive elements have semantic markup (button, role, aria-label as needed). Raw <div onClick> on a logical control \u2192 NO-GO.
|
|
1658
|
+
6. Test coverage \u2014 meaningful new logic should have a test in the same PR. Untested critical path \u2192 CONDITIONAL (not NO-GO unless the path is a security boundary).
|
|
1659
|
+
|
|
1660
|
+
QA verdict scale:
|
|
1661
|
+
GO \u2014 all checks pass, ready to merge
|
|
1662
|
+
CONDITIONAL \u2014 non-blocking gaps (e.g. missing test, small a11y nit). Merge is acceptable but findings should be filed.
|
|
1663
|
+
NO-GO \u2014 at least one blocking check failed
|
|
1664
|
+
`;
|
|
1665
|
+
var SECURITY_BLOCK = `# /security lens \u2014 Security Review Gate
|
|
1666
|
+
|
|
1667
|
+
You are acting as the security agent. Apply the 5 Non-Negotiables and the 5 Security Layers.
|
|
1668
|
+
|
|
1669
|
+
5 Non-Negotiables (any failure \u2192 FAIL):
|
|
1670
|
+
1. RLS / organisation_id \u2014 every tenant table query filters by organisation_id; every insert sets it. No bypass via service-role client unless the file is a documented background job.
|
|
1671
|
+
2. Zod validation \u2014 every mutation endpoint validates inputs; update schemas use .strict().
|
|
1672
|
+
3. Secrets \u2014 no secrets in code, no NEXT_PUBLIC_ leakage of server-only env vars, no console.log of tokens / cookies / session ids.
|
|
1673
|
+
4. Audit chain \u2014 mutations on audited resources call writeAuditLog; system_audit_log and ticket_activity_log are never UPDATEd or DELETEd.
|
|
1674
|
+
5. CSRF / auth \u2014 dashboard mutations require session auth; widget endpoints require API key; webhook endpoints verify HMAC signature.
|
|
1675
|
+
|
|
1676
|
+
5 Security Layers to review independently:
|
|
1677
|
+
- authentication (session vs api_key vs HMAC selected correctly?)
|
|
1678
|
+
- tenant_resolution (organisation_id resolved from membership / api_key \u2192 project, never from request body?)
|
|
1679
|
+
- authorisation (role check matches endpoint sensitivity?)
|
|
1680
|
+
- rls (queries filtered by organisation_id?)
|
|
1681
|
+
- audit (mutations recorded with writeAuditLog?)
|
|
1682
|
+
|
|
1683
|
+
Security verdict scale:
|
|
1684
|
+
PASS \u2014 all five layers pass, no findings above 'low'
|
|
1685
|
+
PASS_WITH_CONDITIONS \u2014 only 'low' findings; no critical/high/medium issues
|
|
1686
|
+
FAIL \u2014 at least one critical, high, or medium finding, OR any 'fail' in the five layers
|
|
1687
|
+
`;
|
|
1688
|
+
var OUTPUT_CONTRACT = `# Output contract \u2014 REQUIRED
|
|
1689
|
+
|
|
1690
|
+
After your analysis, end your reply with exactly ONE fenced \`\`\`json block matching this schema. No prose after the closing fence.
|
|
1691
|
+
|
|
1692
|
+
\`\`\`json
|
|
1693
|
+
{
|
|
1694
|
+
"overall": "pass" | "fail",
|
|
1695
|
+
"summary": "<one sentence \u2014 what the PR does and the headline verdict>",
|
|
1696
|
+
"qa": {
|
|
1697
|
+
"verdict": "GO" | "CONDITIONAL" | "NO-GO",
|
|
1698
|
+
"failed_checks": ["<short label>", ...]
|
|
1699
|
+
},
|
|
1700
|
+
"security": {
|
|
1701
|
+
"verdict": "PASS" | "PASS_WITH_CONDITIONS" | "FAIL",
|
|
1702
|
+
"layers_reviewed": {
|
|
1703
|
+
"authentication": "PASS" | "FAIL",
|
|
1704
|
+
"tenant_resolution": "PASS" | "FAIL",
|
|
1705
|
+
"authorisation": "PASS" | "FAIL",
|
|
1706
|
+
"rls": "PASS" | "FAIL",
|
|
1707
|
+
"audit": "PASS" | "FAIL"
|
|
1708
|
+
}
|
|
1709
|
+
},
|
|
1710
|
+
"findings": [
|
|
1711
|
+
{
|
|
1712
|
+
"lens": "qa" | "security",
|
|
1713
|
+
"severity": "critical" | "high" | "medium" | "low",
|
|
1714
|
+
"file": "<repo-relative path or null>",
|
|
1715
|
+
"description": "<what is wrong>",
|
|
1716
|
+
"recommendation": "<what to change>"
|
|
1717
|
+
}
|
|
1718
|
+
]
|
|
1719
|
+
}
|
|
1720
|
+
\`\`\`
|
|
1721
|
+
|
|
1722
|
+
Rules:
|
|
1723
|
+
- "overall": "pass" ONLY when qa.verdict === "GO" AND security.verdict === "PASS". Anything else \u2192 "fail".
|
|
1724
|
+
- "findings" is an array (may be empty on a clean pass).
|
|
1725
|
+
- Keep total response under 4000 tokens \u2014 be concise and actionable.
|
|
1726
|
+
`;
|
|
1727
|
+
function buildPrReviewSystemPrompt(_args) {
|
|
1728
|
+
return [
|
|
1729
|
+
"You are a paranoid senior reviewer auditing a pull request that was generated by an AI agent. Treat every line of the diff as if it were written by an attacker who knows your blind spots.",
|
|
1730
|
+
"",
|
|
1731
|
+
"You have read-only tools (git diff, git log, Read, Grep, Glob, gh pr view). Do NOT attempt to edit or write files; the toolset will refuse.",
|
|
1732
|
+
"",
|
|
1733
|
+
"Two lenses, applied independently then combined. Be evidence-driven \u2014 cite file paths in findings.",
|
|
1734
|
+
"",
|
|
1735
|
+
QA_BLOCK,
|
|
1736
|
+
"",
|
|
1737
|
+
SECURITY_BLOCK,
|
|
1738
|
+
"",
|
|
1739
|
+
OUTPUT_CONTRACT
|
|
1740
|
+
].join("\n");
|
|
1741
|
+
}
|
|
1742
|
+
function buildPrReviewUserPrompt(args) {
|
|
1743
|
+
return [
|
|
1744
|
+
`PR #${args.prNumber} (${args.prUrl})`,
|
|
1745
|
+
`Branch: ${args.branchName} \u2192 ${args.baseBranch}`,
|
|
1746
|
+
"",
|
|
1747
|
+
`Run \`git diff ${args.baseBranch}...${args.branchName}\` to see every changed line. If the diff is large, also run \`git diff ${args.baseBranch}...${args.branchName} --stat\` first to orient yourself, then drill into files that look risky.`,
|
|
1748
|
+
"",
|
|
1749
|
+
"Read referenced files in full if a finding depends on surrounding context (RLS policies, validation schemas, audit-log helpers).",
|
|
1750
|
+
"",
|
|
1751
|
+
"When ready, emit your verdict as the fenced JSON block specified in the system prompt. No prose after the closing fence."
|
|
1752
|
+
].join("\n");
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
// src/agent/pr-review-service.ts
|
|
1756
|
+
var PrReviewError = class extends Error {
|
|
1757
|
+
code;
|
|
1758
|
+
excerpt;
|
|
1759
|
+
constructor(code, message, excerpt = "") {
|
|
1760
|
+
super(message);
|
|
1761
|
+
this.name = "PrReviewError";
|
|
1762
|
+
this.code = code;
|
|
1763
|
+
this.excerpt = excerpt;
|
|
1764
|
+
}
|
|
1765
|
+
};
|
|
1766
|
+
var STDERR_KEEP = 4e3;
|
|
1767
|
+
async function runPrReview(args) {
|
|
1768
|
+
const systemPrompt = buildPrReviewSystemPrompt(args);
|
|
1769
|
+
const userPrompt = buildPrReviewUserPrompt(args);
|
|
1770
|
+
const claude = args.claudePath ?? "claude";
|
|
1771
|
+
const cliArgs = [
|
|
1772
|
+
"--allowedTools",
|
|
1773
|
+
reviewAllowedToolsFlag(),
|
|
1774
|
+
"--system-prompt",
|
|
1775
|
+
systemPrompt,
|
|
1776
|
+
...args.modelId ? ["--model", args.modelId] : [],
|
|
1777
|
+
userPrompt
|
|
1778
|
+
];
|
|
1779
|
+
const logDir = join8(homedir5(), ".cache", "task", "runs");
|
|
1780
|
+
await mkdir7(logDir, { recursive: true }).catch(() => {
|
|
1781
|
+
});
|
|
1782
|
+
const logPath = join8(logDir, `${args.runId}.review.log`);
|
|
1783
|
+
await writeFile8(logPath, "").catch(() => {
|
|
1784
|
+
});
|
|
1785
|
+
const { createWriteStream } = await import("fs");
|
|
1786
|
+
const logHandle = createWriteStream(logPath, { flags: "a" });
|
|
1787
|
+
let stdoutBuffer = "";
|
|
1788
|
+
let stderrTail = "";
|
|
1789
|
+
const exitCode = await new Promise((resolve2, reject) => {
|
|
1790
|
+
const child = spawn2(claude, cliArgs, {
|
|
1791
|
+
cwd: args.cwd,
|
|
1792
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1793
|
+
env: { ...process.env, FORCE_COLOR: args.silent ? "0" : "1" }
|
|
1794
|
+
});
|
|
1795
|
+
child.on("error", (err) => {
|
|
1796
|
+
logHandle.end();
|
|
1797
|
+
reject(new PrReviewError("spawn_failed", `Could not spawn claude: ${err.message}`));
|
|
1798
|
+
});
|
|
1799
|
+
child.stdout?.on("data", (chunk) => {
|
|
1800
|
+
stdoutBuffer += chunk.toString("utf8");
|
|
1801
|
+
if (args.silent) {
|
|
1802
|
+
logHandle.write(chunk);
|
|
1803
|
+
} else {
|
|
1804
|
+
process.stdout.write(chunk);
|
|
1805
|
+
logHandle.write(chunk);
|
|
1806
|
+
}
|
|
1807
|
+
});
|
|
1808
|
+
child.stderr?.on("data", (chunk) => {
|
|
1809
|
+
if (args.silent) {
|
|
1810
|
+
logHandle.write(chunk);
|
|
1811
|
+
} else {
|
|
1812
|
+
process.stderr.write(chunk);
|
|
1813
|
+
logHandle.write(chunk);
|
|
1814
|
+
}
|
|
1815
|
+
stderrTail = (stderrTail + chunk.toString("utf8")).slice(-STDERR_KEEP);
|
|
1816
|
+
});
|
|
1817
|
+
child.on("close", (code) => {
|
|
1818
|
+
logHandle.end();
|
|
1819
|
+
resolve2(code ?? 0);
|
|
1820
|
+
});
|
|
1821
|
+
});
|
|
1822
|
+
if (exitCode !== 0) {
|
|
1823
|
+
throw new PrReviewError(
|
|
1824
|
+
"nonzero_exit",
|
|
1825
|
+
`claude review subprocess exited with code ${exitCode}`,
|
|
1826
|
+
stderrTail
|
|
1827
|
+
);
|
|
1828
|
+
}
|
|
1829
|
+
return parseVerdict(stdoutBuffer, logPath);
|
|
1830
|
+
}
|
|
1831
|
+
function parseVerdict(stdout, logPath) {
|
|
1832
|
+
const blocks = extractJsonBlocks(stdout);
|
|
1833
|
+
if (blocks.length === 0) {
|
|
1834
|
+
throw new PrReviewError(
|
|
1835
|
+
"no_json_block",
|
|
1836
|
+
"Could not find a fenced ```json block in the review subprocess output",
|
|
1837
|
+
stdout.slice(-2e3)
|
|
1838
|
+
);
|
|
1839
|
+
}
|
|
1840
|
+
const rawJson = blocks[blocks.length - 1];
|
|
1841
|
+
let parsed;
|
|
1842
|
+
try {
|
|
1843
|
+
parsed = JSON.parse(rawJson);
|
|
1844
|
+
} catch (err) {
|
|
1845
|
+
throw new PrReviewError(
|
|
1846
|
+
"invalid_json",
|
|
1847
|
+
`JSON.parse failed: ${err.message}`,
|
|
1848
|
+
rawJson.slice(0, 2e3)
|
|
1849
|
+
);
|
|
1850
|
+
}
|
|
1851
|
+
const verdict = normaliseVerdict(parsed, rawJson, logPath);
|
|
1852
|
+
return verdict;
|
|
1853
|
+
}
|
|
1854
|
+
function extractJsonBlocks(text) {
|
|
1855
|
+
const re = /```(?:json|JSON|jsonc)\s*\n([\s\S]*?)\n```/g;
|
|
1856
|
+
const out = [];
|
|
1857
|
+
let match;
|
|
1858
|
+
while ((match = re.exec(text)) !== null) {
|
|
1859
|
+
if (match[1] !== void 0) out.push(match[1]);
|
|
1860
|
+
}
|
|
1861
|
+
return out;
|
|
1862
|
+
}
|
|
1863
|
+
var QA_VERDICTS = /* @__PURE__ */ new Set(["GO", "CONDITIONAL", "NO-GO"]);
|
|
1864
|
+
var SECURITY_VERDICTS = /* @__PURE__ */ new Set(["PASS", "PASS_WITH_CONDITIONS", "FAIL"]);
|
|
1865
|
+
var SECURITY_LAYER_VERDICTS = /* @__PURE__ */ new Set(["PASS", "FAIL"]);
|
|
1866
|
+
var SEVERITIES = /* @__PURE__ */ new Set(["critical", "high", "medium", "low"]);
|
|
1867
|
+
var LAYER_KEYS = [
|
|
1868
|
+
"authentication",
|
|
1869
|
+
"tenant_resolution",
|
|
1870
|
+
"authorisation",
|
|
1871
|
+
"rls",
|
|
1872
|
+
"audit"
|
|
1873
|
+
];
|
|
1874
|
+
function normaliseVerdict(raw, rawJson, logPath) {
|
|
1875
|
+
if (raw === null || typeof raw !== "object") {
|
|
1876
|
+
throw new PrReviewError(
|
|
1877
|
+
"schema_mismatch",
|
|
1878
|
+
"Verdict JSON is not an object",
|
|
1879
|
+
rawJson.slice(0, 1e3)
|
|
1880
|
+
);
|
|
1881
|
+
}
|
|
1882
|
+
const r = raw;
|
|
1883
|
+
const qaRaw = r["qa"] ?? {};
|
|
1884
|
+
const secRaw = r["security"] ?? {};
|
|
1885
|
+
const layersRaw = secRaw["layers_reviewed"] ?? {};
|
|
1886
|
+
const qaVerdict = String(qaRaw["verdict"] ?? "");
|
|
1887
|
+
if (!QA_VERDICTS.has(qaVerdict)) {
|
|
1888
|
+
throw new PrReviewError(
|
|
1889
|
+
"schema_mismatch",
|
|
1890
|
+
`qa.verdict is "${qaVerdict}", expected one of GO / CONDITIONAL / NO-GO`,
|
|
1891
|
+
rawJson.slice(0, 1e3)
|
|
1892
|
+
);
|
|
1893
|
+
}
|
|
1894
|
+
const securityVerdict = String(secRaw["verdict"] ?? "");
|
|
1895
|
+
if (!SECURITY_VERDICTS.has(securityVerdict)) {
|
|
1896
|
+
throw new PrReviewError(
|
|
1897
|
+
"schema_mismatch",
|
|
1898
|
+
`security.verdict is "${securityVerdict}", expected PASS / PASS_WITH_CONDITIONS / FAIL`,
|
|
1899
|
+
rawJson.slice(0, 1e3)
|
|
1900
|
+
);
|
|
1901
|
+
}
|
|
1902
|
+
const layers = {};
|
|
1903
|
+
for (const key of LAYER_KEYS) {
|
|
1904
|
+
const val = String(layersRaw[key] ?? "");
|
|
1905
|
+
layers[key] = SECURITY_LAYER_VERDICTS.has(val) ? val : "FAIL";
|
|
1906
|
+
}
|
|
1907
|
+
const findingsRaw = Array.isArray(r["findings"]) ? r["findings"] : [];
|
|
1908
|
+
const findings = findingsRaw.slice(0, 50).map((f) => {
|
|
1909
|
+
const fr = f ?? {};
|
|
1910
|
+
const severity = String(fr["severity"] ?? "low");
|
|
1911
|
+
const lensRaw = String(fr["lens"] ?? "qa");
|
|
1912
|
+
return {
|
|
1913
|
+
lens: lensRaw === "security" ? "security" : "qa",
|
|
1914
|
+
severity: SEVERITIES.has(severity) ? severity : "low",
|
|
1915
|
+
file: typeof fr["file"] === "string" ? fr["file"].slice(0, 500) : null,
|
|
1916
|
+
description: String(fr["description"] ?? "").slice(0, 2e3),
|
|
1917
|
+
recommendation: String(fr["recommendation"] ?? "").slice(0, 2e3)
|
|
1918
|
+
};
|
|
1919
|
+
});
|
|
1920
|
+
const failedChecks = Array.isArray(qaRaw["failed_checks"]) ? qaRaw["failed_checks"].map((s) => String(s).slice(0, 200)).slice(0, 20) : [];
|
|
1921
|
+
const overall = qaVerdict === "GO" && securityVerdict === "PASS" ? "pass" : "fail";
|
|
1922
|
+
return {
|
|
1923
|
+
overall,
|
|
1924
|
+
summary: String(r["summary"] ?? "").slice(0, 1e3) || (overall === "pass" ? "Review passed." : "Review failed."),
|
|
1925
|
+
qa: { verdict: qaVerdict, failed_checks: failedChecks },
|
|
1926
|
+
security: { verdict: securityVerdict, layers_reviewed: layers },
|
|
1927
|
+
findings,
|
|
1928
|
+
rawJson: rawJson.slice(0, 5e4),
|
|
1929
|
+
logPath
|
|
1930
|
+
};
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1618
1933
|
// src/guardrail/diff-check.ts
|
|
1619
1934
|
import { execFileSync as execFileSync4 } from "child_process";
|
|
1620
1935
|
|
|
@@ -1705,7 +2020,7 @@ function discardWorkingTreeChanges(cwd) {
|
|
|
1705
2020
|
}
|
|
1706
2021
|
|
|
1707
2022
|
// src/test-runner/run-tests.ts
|
|
1708
|
-
import { spawn as
|
|
2023
|
+
import { spawn as spawn3 } from "child_process";
|
|
1709
2024
|
var ALLOWED_EXECUTABLES = /* @__PURE__ */ new Set(["pnpm", "npm", "yarn", "bun", "node", "npx"]);
|
|
1710
2025
|
var DEFAULT_COMMAND = "pnpm typecheck";
|
|
1711
2026
|
var TIMEOUT_MS = 10 * 60 * 1e3;
|
|
@@ -1733,7 +2048,7 @@ async function runProjectTest(args) {
|
|
|
1733
2048
|
const [exe, ...rest] = argv;
|
|
1734
2049
|
const startedAt = Date.now();
|
|
1735
2050
|
return new Promise((resolve2) => {
|
|
1736
|
-
const child =
|
|
2051
|
+
const child = spawn3(exe, rest, {
|
|
1737
2052
|
cwd: args.cwd,
|
|
1738
2053
|
stdio: ["ignore", "pipe", "pipe"],
|
|
1739
2054
|
shell: false,
|
|
@@ -1778,16 +2093,16 @@ async function runProjectTest(args) {
|
|
|
1778
2093
|
}
|
|
1779
2094
|
|
|
1780
2095
|
// src/test-runner/auto-install.ts
|
|
1781
|
-
import { spawn as
|
|
2096
|
+
import { spawn as spawn4 } from "child_process";
|
|
1782
2097
|
import { stat as stat2 } from "fs/promises";
|
|
1783
|
-
import { dirname as dirname4, join as
|
|
2098
|
+
import { dirname as dirname4, join as join9 } from "path";
|
|
1784
2099
|
var INSTALL_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
1785
2100
|
var TAIL_BYTES2 = 4e3;
|
|
1786
2101
|
async function findPnpmWorkspaceRoot(start) {
|
|
1787
2102
|
let cur = start;
|
|
1788
2103
|
for (; ; ) {
|
|
1789
2104
|
try {
|
|
1790
|
-
await stat2(
|
|
2105
|
+
await stat2(join9(cur, "pnpm-lock.yaml"));
|
|
1791
2106
|
return cur;
|
|
1792
2107
|
} catch {
|
|
1793
2108
|
}
|
|
@@ -1817,8 +2132,8 @@ async function ensureWorkspaceInstalled(args) {
|
|
|
1817
2132
|
tail: ""
|
|
1818
2133
|
};
|
|
1819
2134
|
}
|
|
1820
|
-
const lockMtime = await mtimeMs(
|
|
1821
|
-
const markerMtime = await mtimeMs(
|
|
2135
|
+
const lockMtime = await mtimeMs(join9(workspaceRoot, "pnpm-lock.yaml"));
|
|
2136
|
+
const markerMtime = await mtimeMs(join9(workspaceRoot, "node_modules", ".modules.yaml"));
|
|
1822
2137
|
if (lockMtime !== null && markerMtime !== null && markerMtime >= lockMtime) {
|
|
1823
2138
|
return {
|
|
1824
2139
|
ok: true,
|
|
@@ -1832,7 +2147,7 @@ async function ensureWorkspaceInstalled(args) {
|
|
|
1832
2147
|
}
|
|
1833
2148
|
const reason = markerMtime === null ? "node_modules missing \u2014 cold install" : "pnpm-lock.yaml newer than install marker";
|
|
1834
2149
|
return new Promise((resolve2) => {
|
|
1835
|
-
const child =
|
|
2150
|
+
const child = spawn4("pnpm", ["install", "--frozen-lockfile"], {
|
|
1836
2151
|
cwd: workspaceRoot,
|
|
1837
2152
|
stdio: ["ignore", "pipe", "pipe"],
|
|
1838
2153
|
shell: false,
|
|
@@ -1878,10 +2193,10 @@ async function ensureWorkspaceInstalled(args) {
|
|
|
1878
2193
|
}
|
|
1879
2194
|
|
|
1880
2195
|
// src/util/progress.ts
|
|
1881
|
-
import { mkdir as
|
|
2196
|
+
import { mkdir as mkdir8, writeFile as writeFile9, rename as rename2, unlink as unlink3, readdir, stat as stat3 } from "fs/promises";
|
|
1882
2197
|
import { tmpdir } from "os";
|
|
1883
|
-
import { join as
|
|
1884
|
-
var PROGRESS_DIR =
|
|
2198
|
+
import { join as join10 } from "path";
|
|
2199
|
+
var PROGRESS_DIR = join10(tmpdir(), "task-progress");
|
|
1885
2200
|
var STALE_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
1886
2201
|
var ProgressWriter = class {
|
|
1887
2202
|
path;
|
|
@@ -1894,7 +2209,7 @@ var ProgressWriter = class {
|
|
|
1894
2209
|
this.ticketId = ticketId;
|
|
1895
2210
|
const deliveryId = process.env["TASK_DELIVERY_ID"]?.trim();
|
|
1896
2211
|
const fileBase = deliveryId && deliveryId.length > 0 ? deliveryId : `manual-${process.pid}`;
|
|
1897
|
-
this.path =
|
|
2212
|
+
this.path = join10(PROGRESS_DIR, `${fileBase}.json`);
|
|
1898
2213
|
}
|
|
1899
2214
|
/** Switch the in-flight ticket between phases (used by `task scan` which iterates). */
|
|
1900
2215
|
setTicketId(ticketId) {
|
|
@@ -1928,12 +2243,12 @@ var ProgressWriter = class {
|
|
|
1928
2243
|
});
|
|
1929
2244
|
}
|
|
1930
2245
|
async writeAtomic(payload) {
|
|
1931
|
-
await
|
|
2246
|
+
await mkdir8(PROGRESS_DIR, { recursive: true }).catch(() => {
|
|
1932
2247
|
});
|
|
1933
2248
|
const body = JSON.stringify(payload);
|
|
1934
2249
|
const tmp = `${this.path}.tmp`;
|
|
1935
2250
|
try {
|
|
1936
|
-
await
|
|
2251
|
+
await writeFile9(tmp, body, { encoding: "utf8", mode: 384 });
|
|
1937
2252
|
await rename2(tmp, this.path);
|
|
1938
2253
|
} catch {
|
|
1939
2254
|
await unlink3(tmp).catch(() => {
|
|
@@ -1950,7 +2265,7 @@ var ProgressWriter = class {
|
|
|
1950
2265
|
const cutoff = Date.now() - STALE_MAX_AGE_MS;
|
|
1951
2266
|
await Promise.all(
|
|
1952
2267
|
entries.filter((name) => name.endsWith(".json") || name.endsWith(".json.tmp")).map(async (name) => {
|
|
1953
|
-
const p =
|
|
2268
|
+
const p = join10(PROGRESS_DIR, name);
|
|
1954
2269
|
try {
|
|
1955
2270
|
const s = await stat3(p);
|
|
1956
2271
|
if (s.mtimeMs < cutoff) {
|
|
@@ -1992,6 +2307,9 @@ function registerWork(program2) {
|
|
|
1992
2307
|
).option(
|
|
1993
2308
|
"--confirm",
|
|
1994
2309
|
"Confirm --reset in non-TTY (silent / scheduled-task) contexts. Has no effect without --reset."
|
|
2310
|
+
).option(
|
|
2311
|
+
"--no-auto-review",
|
|
2312
|
+
"Skip the post-PR /qa + /security auto-review. The PR is left open for a human to review and merge."
|
|
1995
2313
|
).option("--schedule-id <id>", "Internal: schedule id when invoked from a scheduled task").action(async (ticketId, opts) => {
|
|
1996
2314
|
await runWork(ticketId, opts);
|
|
1997
2315
|
});
|
|
@@ -2515,6 +2833,122 @@ Claude session: ${runId}
|
|
|
2515
2833
|
if (err instanceof CliError) err.phase = "post_push";
|
|
2516
2834
|
throw err;
|
|
2517
2835
|
}
|
|
2836
|
+
const autoReviewEnabled = opts.autoReview !== false;
|
|
2837
|
+
if (autoReviewEnabled && prNumber !== void 0 && prUrl !== void 0 && !opts.dryRun) {
|
|
2838
|
+
await progress.setPhase("auto_reviewing");
|
|
2839
|
+
if (!silent) {
|
|
2840
|
+
process.stdout.write(c.dim(" Auto-reviewing PR \u2014 /qa + /security\u2026\n"));
|
|
2841
|
+
}
|
|
2842
|
+
try {
|
|
2843
|
+
const verdict = await runPrReview({
|
|
2844
|
+
cwd,
|
|
2845
|
+
runId,
|
|
2846
|
+
silent,
|
|
2847
|
+
baseBranch: ticketBaseBranch,
|
|
2848
|
+
branchName,
|
|
2849
|
+
prNumber,
|
|
2850
|
+
prUrl,
|
|
2851
|
+
...ctx.localCfg.claude_path ? { claudePath: ctx.localCfg.claude_path } : {}
|
|
2852
|
+
});
|
|
2853
|
+
const verdictResp = await apiCallOrThrow(
|
|
2854
|
+
"POST",
|
|
2855
|
+
`/api/v1/cli/me/tickets/${detail.id}/pull-requests/${prNumber}/auto-review-verdict`,
|
|
2856
|
+
{
|
|
2857
|
+
body: {
|
|
2858
|
+
overall: verdict.overall,
|
|
2859
|
+
summary: verdict.summary,
|
|
2860
|
+
qa: verdict.qa,
|
|
2861
|
+
security: verdict.security,
|
|
2862
|
+
findings: verdict.findings,
|
|
2863
|
+
raw_json: verdict.rawJson,
|
|
2864
|
+
claude_session_id: runId
|
|
2865
|
+
}
|
|
2866
|
+
}
|
|
2867
|
+
);
|
|
2868
|
+
await apiCall("POST", "/api/v1/cli/me/runs", {
|
|
2869
|
+
body: {
|
|
2870
|
+
ticket_id: detail.id,
|
|
2871
|
+
schedule_id: opts.scheduleId,
|
|
2872
|
+
event: verdict.overall === "pass" ? "auto_review_passed" : "auto_review_failed",
|
|
2873
|
+
claude_session_id: runId,
|
|
2874
|
+
output_excerpt: `qa=${verdict.qa.verdict} security=${verdict.security.verdict}` + (verdict.findings.length > 0 ? ` | ${verdict.findings.length} finding(s)` : "")
|
|
2875
|
+
}
|
|
2876
|
+
});
|
|
2877
|
+
if (!silent) {
|
|
2878
|
+
const icon = verdict.overall === "pass" ? c.ok("\u2713 Auto-review PASS") : c.warn("\u2717 Auto-review FAIL");
|
|
2879
|
+
process.stdout.write(
|
|
2880
|
+
`${icon} qa=${verdict.qa.verdict} security=${verdict.security.verdict}
|
|
2881
|
+
`
|
|
2882
|
+
);
|
|
2883
|
+
if (verdict.overall === "pass") {
|
|
2884
|
+
switch (verdictResp.status) {
|
|
2885
|
+
case "merged":
|
|
2886
|
+
case "already_merged":
|
|
2887
|
+
process.stdout.write(c.ok(" \u2713 Merged into ") + c.cyan(`${ticketBaseBranch}
|
|
2888
|
+
`));
|
|
2889
|
+
break;
|
|
2890
|
+
case "merging":
|
|
2891
|
+
process.stdout.write(
|
|
2892
|
+
c.dim(" PR approved \u2014 merge in progress on ") + c.cyan(`${ticketBaseBranch}
|
|
2893
|
+
`)
|
|
2894
|
+
);
|
|
2895
|
+
break;
|
|
2896
|
+
case "queued_for_merge":
|
|
2897
|
+
process.stdout.write(
|
|
2898
|
+
c.dim(" PR approved \u2014 queued for merge to ") + c.cyan(`${ticketBaseBranch}
|
|
2899
|
+
`) + c.dim(" GitHub still computing mergeability; the merge cron will retry.\n")
|
|
2900
|
+
);
|
|
2901
|
+
break;
|
|
2902
|
+
case "merge_failed":
|
|
2903
|
+
process.stdout.write(
|
|
2904
|
+
c.warn(" PR approved but merge failed") + c.dim(" \u2014 review on GitHub for the reason (conflicts, blocked, etc.).\n")
|
|
2905
|
+
);
|
|
2906
|
+
break;
|
|
2907
|
+
case "approved_not_queued":
|
|
2908
|
+
process.stdout.write(
|
|
2909
|
+
c.warn(" PR approved but not queued") + c.dim(" \u2014 server reported the merge queue was not unlocked.\n")
|
|
2910
|
+
);
|
|
2911
|
+
break;
|
|
2912
|
+
case "sql_held_for_review": {
|
|
2913
|
+
const count = verdictResp.sql_files_count ?? verdictResp.sql_files?.length ?? 0;
|
|
2914
|
+
process.stdout.write(
|
|
2915
|
+
c.warn(" \u26A0 Held for human review") + c.dim(
|
|
2916
|
+
` \u2014 PR touches ${count} SQL / migration file${count === 1 ? "" : "s"}. Findings posted on the PR.
|
|
2917
|
+
`
|
|
2918
|
+
)
|
|
2919
|
+
);
|
|
2920
|
+
break;
|
|
2921
|
+
}
|
|
2922
|
+
default:
|
|
2923
|
+
process.stdout.write(c.dim(` Verdict recorded. (status=${verdictResp.status})
|
|
2924
|
+
`));
|
|
2925
|
+
}
|
|
2926
|
+
} else {
|
|
2927
|
+
process.stdout.write(
|
|
2928
|
+
c.dim(` ${verdict.findings.length} finding(s) posted as PR comment; PR left open.
|
|
2929
|
+
`)
|
|
2930
|
+
);
|
|
2931
|
+
}
|
|
2932
|
+
}
|
|
2933
|
+
} catch (err) {
|
|
2934
|
+
const excerpt = err instanceof PrReviewError ? `${err.code}: ${err.message.slice(0, 1e3)}` : err.message.slice(0, 1e3);
|
|
2935
|
+
await apiCall("POST", "/api/v1/cli/me/runs", {
|
|
2936
|
+
body: {
|
|
2937
|
+
ticket_id: detail.id,
|
|
2938
|
+
schedule_id: opts.scheduleId,
|
|
2939
|
+
event: "auto_review_errored",
|
|
2940
|
+
claude_session_id: runId,
|
|
2941
|
+
output_excerpt: excerpt
|
|
2942
|
+
}
|
|
2943
|
+
});
|
|
2944
|
+
if (!silent) {
|
|
2945
|
+
process.stdout.write(
|
|
2946
|
+
`${c.warn("! Auto-review skipped")} ${c.dim(excerpt.slice(0, 200))}
|
|
2947
|
+
` + c.dim(" PR left open for manual review.\n")
|
|
2948
|
+
);
|
|
2949
|
+
}
|
|
2950
|
+
}
|
|
2951
|
+
}
|
|
2518
2952
|
try {
|
|
2519
2953
|
checkoutBranch(cwd, baseBranch);
|
|
2520
2954
|
} catch {
|
|
@@ -3431,10 +3865,10 @@ function autopilotExitCode(code, status) {
|
|
|
3431
3865
|
}
|
|
3432
3866
|
|
|
3433
3867
|
// src/scan/llm.ts
|
|
3434
|
-
import { spawn as
|
|
3435
|
-
import { mkdir as
|
|
3436
|
-
import { homedir as
|
|
3437
|
-
import { join as
|
|
3868
|
+
import { spawn as spawn5 } from "child_process";
|
|
3869
|
+
import { mkdir as mkdir9, writeFile as writeFile10 } from "fs/promises";
|
|
3870
|
+
import { homedir as homedir6 } from "os";
|
|
3871
|
+
import { join as join11 } from "path";
|
|
3438
3872
|
var FIX_PROMPT_JSON_SCHEMA = {
|
|
3439
3873
|
type: "object",
|
|
3440
3874
|
// Phase 3 — confidence_reason is REQUIRED unconditionally so the
|
|
@@ -3523,7 +3957,7 @@ async function generateFixPromptJson(args) {
|
|
|
3523
3957
|
return new Promise((resolve2, reject) => {
|
|
3524
3958
|
let child;
|
|
3525
3959
|
try {
|
|
3526
|
-
child =
|
|
3960
|
+
child = spawn5(claude, cliArgs, {
|
|
3527
3961
|
stdio: ["pipe", "pipe", "pipe"],
|
|
3528
3962
|
signal: args.signal
|
|
3529
3963
|
});
|
|
@@ -3649,10 +4083,10 @@ function readEnvelopeTokens(raw, userPrompt, innerText) {
|
|
|
3649
4083
|
async function maybeDumpDebug(ticketId, stdout, stderr) {
|
|
3650
4084
|
if (!DEBUG && stdout.length === 0 && stderr.length === 0) return null;
|
|
3651
4085
|
try {
|
|
3652
|
-
const dir =
|
|
3653
|
-
await
|
|
3654
|
-
const path =
|
|
3655
|
-
await
|
|
4086
|
+
const dir = join11(homedir6(), ".cache", "task", "scan-debug");
|
|
4087
|
+
await mkdir9(dir, { recursive: true });
|
|
4088
|
+
const path = join11(dir, `${ticketId}-${Date.now()}.log`);
|
|
4089
|
+
await writeFile10(
|
|
3656
4090
|
path,
|
|
3657
4091
|
["## ticket_id", ticketId, "", "## stdout", stdout, "", "## stderr", stderr].join("\n")
|
|
3658
4092
|
);
|
|
@@ -3998,7 +4432,7 @@ function clampInt(raw, min, max, fallback) {
|
|
|
3998
4432
|
}
|
|
3999
4433
|
|
|
4000
4434
|
// src/commands/slack-import.ts
|
|
4001
|
-
import { spawn as
|
|
4435
|
+
import { spawn as spawn6 } from "child_process";
|
|
4002
4436
|
import { request as request5 } from "undici";
|
|
4003
4437
|
var BULLET_TYPES = ["bug", "feature", "task", "question", "improvement"];
|
|
4004
4438
|
var BULLET_PRIORITIES = ["critical", "high", "medium", "low", "none"];
|
|
@@ -4135,7 +4569,7 @@ async function generateBullets(args) {
|
|
|
4135
4569
|
return new Promise((resolve2, reject) => {
|
|
4136
4570
|
let child;
|
|
4137
4571
|
try {
|
|
4138
|
-
child =
|
|
4572
|
+
child = spawn6(claude, cliArgs, { stdio: ["pipe", "pipe", "pipe"] });
|
|
4139
4573
|
} catch (err) {
|
|
4140
4574
|
reject(new Error(`Could not invoke claude: ${err.message}`));
|
|
4141
4575
|
return;
|
|
@@ -4239,7 +4673,10 @@ function registerFastTrack(program2) {
|
|
|
4239
4673
|
).option("--max <n>", "Process up to N tickets in this invocation", "1").option("--api-url <url>", "Override TASK_API_URL").option("--silent", "Suppress per-ticket progress chrome").option("--dry-run", "Run scan + approve + agent + tests but do not commit, push, or open a PR").option(
|
|
4240
4674
|
"--reset",
|
|
4241
4675
|
"DESTRUCTIVE: discard local working-tree changes before the first ticket. Requires --confirm in non-TTY contexts."
|
|
4242
|
-
).option("--confirm", "Confirm --reset in non-TTY (silent / scheduled-task) contexts").option(
|
|
4676
|
+
).option("--confirm", "Confirm --reset in non-TTY (silent / scheduled-task) contexts").option(
|
|
4677
|
+
"--no-auto-review",
|
|
4678
|
+
"Skip the post-PR /qa + /security auto-review. Each PR is left open for a human to review and merge."
|
|
4679
|
+
).option("--schedule-id <id>", "Internal: schedule id when invoked from a scheduled task").action(async (opts) => {
|
|
4243
4680
|
await runFastTrack(opts);
|
|
4244
4681
|
});
|
|
4245
4682
|
}
|
|
@@ -4280,7 +4717,11 @@ async function runFastTrack(opts) {
|
|
|
4280
4717
|
...opts.dryRun ? { dryRun: true } : {},
|
|
4281
4718
|
...opts.scheduleId ? { scheduleId: opts.scheduleId } : {},
|
|
4282
4719
|
...firstIteration && opts.reset ? { reset: true } : {},
|
|
4283
|
-
...firstIteration && opts.confirm ? { confirm: true } : {}
|
|
4720
|
+
...firstIteration && opts.confirm ? { confirm: true } : {},
|
|
4721
|
+
// Commander sets opts.autoReview to `false` only when --no-auto-review
|
|
4722
|
+
// is passed; otherwise it's `undefined` and processOneTicketImpl
|
|
4723
|
+
// treats `undefined` as "auto-review enabled" (opts.autoReview !== false).
|
|
4724
|
+
...opts.autoReview === false ? { autoReview: false } : {}
|
|
4284
4725
|
};
|
|
4285
4726
|
const outcome = await fastTrackOneTicket({
|
|
4286
4727
|
api,
|
|
@@ -4620,10 +5061,10 @@ import { randomUUID as randomUUID4 } from "crypto";
|
|
|
4620
5061
|
import { platform as platform2 } from "os";
|
|
4621
5062
|
|
|
4622
5063
|
// src/scheduler/launchd.ts
|
|
4623
|
-
import { mkdir as
|
|
4624
|
-
import { homedir as
|
|
4625
|
-
import { join as
|
|
4626
|
-
import { execFileSync as execFileSync9, spawn as
|
|
5064
|
+
import { mkdir as mkdir10, readFile as readFile5, writeFile as writeFile11, unlink as unlink4, readdir as readdir2 } from "fs/promises";
|
|
5065
|
+
import { homedir as homedir7 } from "os";
|
|
5066
|
+
import { join as join12 } from "path";
|
|
5067
|
+
import { execFileSync as execFileSync9, spawn as spawn7 } from "child_process";
|
|
4627
5068
|
|
|
4628
5069
|
// src/scheduler/cron-translate.ts
|
|
4629
5070
|
function translateToLaunchd(cron) {
|
|
@@ -4724,14 +5165,14 @@ function expandField(field, min, max) {
|
|
|
4724
5165
|
}
|
|
4725
5166
|
|
|
4726
5167
|
// src/scheduler/launchd.ts
|
|
4727
|
-
var PLIST_DIR =
|
|
5168
|
+
var PLIST_DIR = join12(homedir7(), "Library", "LaunchAgents");
|
|
4728
5169
|
var LABEL_PREFIX = "com.inteeka.task.cli.";
|
|
4729
5170
|
var SAFE_ID_RE = /^[0-9a-zA-Z._-]+$/;
|
|
4730
5171
|
function plistPath(id) {
|
|
4731
5172
|
if (!SAFE_ID_RE.test(id) || id.includes("..")) {
|
|
4732
5173
|
throw new Error(`Refusing to compute plist path for unsafe id: ${id}`);
|
|
4733
5174
|
}
|
|
4734
|
-
return
|
|
5175
|
+
return join12(PLIST_DIR, `${LABEL_PREFIX}${id}.plist`);
|
|
4735
5176
|
}
|
|
4736
5177
|
function buildPlist(entry) {
|
|
4737
5178
|
const calendars = translateToLaunchd(entry.cron);
|
|
@@ -4767,9 +5208,9 @@ ${fields}
|
|
|
4767
5208
|
` <string>/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin</string>`,
|
|
4768
5209
|
` </dict>`,
|
|
4769
5210
|
` <key>StandardOutPath</key>`,
|
|
4770
|
-
` <string>${escapeXml(
|
|
5211
|
+
` <string>${escapeXml(join12(homedir7(), ".cache", "task", "launchd-stdout.log"))}</string>`,
|
|
4771
5212
|
` <key>StandardErrorPath</key>`,
|
|
4772
|
-
` <string>${escapeXml(
|
|
5213
|
+
` <string>${escapeXml(join12(homedir7(), ".cache", "task", "launchd-stderr.log"))}</string>`,
|
|
4773
5214
|
!entry.enabled ? ` <key>Disabled</key>
|
|
4774
5215
|
<true/>` : "",
|
|
4775
5216
|
"</dict>",
|
|
@@ -4787,9 +5228,9 @@ function bootstrapDomain() {
|
|
|
4787
5228
|
}
|
|
4788
5229
|
var launchdAdapter = {
|
|
4789
5230
|
async upsert(entry) {
|
|
4790
|
-
await
|
|
5231
|
+
await mkdir10(PLIST_DIR, { recursive: true });
|
|
4791
5232
|
const path = plistPath(entry.id);
|
|
4792
|
-
await
|
|
5233
|
+
await writeFile11(path, buildPlist(entry));
|
|
4793
5234
|
try {
|
|
4794
5235
|
execFileSync9("launchctl", ["bootout", bootstrapDomain(), path], { stdio: "ignore" });
|
|
4795
5236
|
} catch {
|
|
@@ -4818,7 +5259,7 @@ var launchdAdapter = {
|
|
|
4818
5259
|
for (const file of ours) {
|
|
4819
5260
|
const id = file.slice(LABEL_PREFIX.length, -".plist".length);
|
|
4820
5261
|
try {
|
|
4821
|
-
const xml = await readFile5(
|
|
5262
|
+
const xml = await readFile5(join12(PLIST_DIR, file), "utf8");
|
|
4822
5263
|
const cron = xml.match(/<key>StartCalendarInterval<\/key>[\s\S]*?<\/array>/)?.[0] ?? "";
|
|
4823
5264
|
const command = xml.match(/<key>ProgramArguments<\/key>\s*<array>([\s\S]*?)<\/array>/)?.[1] ?? "";
|
|
4824
5265
|
const disabled = /<key>Disabled<\/key>\s*<true\/>/.test(xml);
|
|
@@ -4841,7 +5282,7 @@ var launchdAdapter = {
|
|
|
4841
5282
|
return new Promise((resolve2) => {
|
|
4842
5283
|
const args = entry.command.match(/(?:[^\s"]+|"[^"]*")+/g) ?? [entry.command];
|
|
4843
5284
|
const cmd = args.shift() ?? entry.command;
|
|
4844
|
-
const child =
|
|
5285
|
+
const child = spawn7(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
4845
5286
|
let stdoutTail = "";
|
|
4846
5287
|
let stderrTail = "";
|
|
4847
5288
|
child.stdout?.on("data", (chunk) => {
|
|
@@ -4864,7 +5305,7 @@ var launchdAdapter = {
|
|
|
4864
5305
|
}
|
|
4865
5306
|
if (enabled) {
|
|
4866
5307
|
xml = xml.replace(/\s*<key>Disabled<\/key>\s*<true\/>/, "");
|
|
4867
|
-
await
|
|
5308
|
+
await writeFile11(path, xml);
|
|
4868
5309
|
try {
|
|
4869
5310
|
execFileSync9("launchctl", ["bootout", bootstrapDomain(), path], { stdio: "ignore" });
|
|
4870
5311
|
} catch {
|
|
@@ -4876,7 +5317,7 @@ var launchdAdapter = {
|
|
|
4876
5317
|
"</dict>\n</plist>",
|
|
4877
5318
|
" <key>Disabled</key>\n <true/>\n</dict>\n</plist>"
|
|
4878
5319
|
);
|
|
4879
|
-
await
|
|
5320
|
+
await writeFile11(path, xml);
|
|
4880
5321
|
}
|
|
4881
5322
|
try {
|
|
4882
5323
|
execFileSync9("launchctl", ["bootout", bootstrapDomain(), path], { stdio: "ignore" });
|
|
@@ -4887,7 +5328,7 @@ var launchdAdapter = {
|
|
|
4887
5328
|
};
|
|
4888
5329
|
|
|
4889
5330
|
// src/scheduler/cron.ts
|
|
4890
|
-
import { execFileSync as execFileSync10, spawn as
|
|
5331
|
+
import { execFileSync as execFileSync10, spawn as spawn8 } from "child_process";
|
|
4891
5332
|
|
|
4892
5333
|
// src/scheduler/safe-command.ts
|
|
4893
5334
|
var FORBIDDEN = /[;&|`$()<>\\]/;
|
|
@@ -4948,7 +5389,7 @@ function readCrontab() {
|
|
|
4948
5389
|
}
|
|
4949
5390
|
}
|
|
4950
5391
|
function writeCrontab(text) {
|
|
4951
|
-
const child =
|
|
5392
|
+
const child = spawn8("crontab", ["-"], { stdio: ["pipe", "inherit", "inherit"] });
|
|
4952
5393
|
child.stdin.write(text);
|
|
4953
5394
|
child.stdin.end();
|
|
4954
5395
|
}
|
|
@@ -5029,7 +5470,7 @@ var cronAdapter = {
|
|
|
5029
5470
|
return Promise.resolve({ exitCode: 1, stdoutTail: "", stderrTail: `rejected: ${reason}` });
|
|
5030
5471
|
}
|
|
5031
5472
|
return new Promise((resolve2) => {
|
|
5032
|
-
const child =
|
|
5473
|
+
const child = spawn8(parsed.bin, parsed.args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
5033
5474
|
let stdoutTail = "";
|
|
5034
5475
|
let stderrTail = "";
|
|
5035
5476
|
child.stdout?.on(
|
|
@@ -5057,7 +5498,7 @@ var cronAdapter = {
|
|
|
5057
5498
|
};
|
|
5058
5499
|
|
|
5059
5500
|
// src/scheduler/windows.ts
|
|
5060
|
-
import { execFileSync as execFileSync11, spawn as
|
|
5501
|
+
import { execFileSync as execFileSync11, spawn as spawn9 } from "child_process";
|
|
5061
5502
|
var TASK_PREFIX = "TaskCLI_";
|
|
5062
5503
|
function taskName(id) {
|
|
5063
5504
|
return `${TASK_PREFIX}${id.replace(/[^A-Za-z0-9_-]/g, "_")}`;
|
|
@@ -5170,7 +5611,7 @@ var windowsAdapter = {
|
|
|
5170
5611
|
return Promise.resolve({ exitCode: 1, stdoutTail: "", stderrTail: `rejected: ${reason}` });
|
|
5171
5612
|
}
|
|
5172
5613
|
return new Promise((resolve2) => {
|
|
5173
|
-
const child =
|
|
5614
|
+
const child = spawn9(parsed.bin, parsed.args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
5174
5615
|
let stdoutTail = "";
|
|
5175
5616
|
let stderrTail = "";
|
|
5176
5617
|
child.stdout?.on(
|
|
@@ -5229,10 +5670,10 @@ var unsupportedAdapter = {
|
|
|
5229
5670
|
};
|
|
5230
5671
|
|
|
5231
5672
|
// src/scheduler/registry.ts
|
|
5232
|
-
import { mkdir as
|
|
5233
|
-
import { homedir as
|
|
5234
|
-
import { dirname as dirname5, join as
|
|
5235
|
-
var REGISTRY_PATH =
|
|
5673
|
+
import { mkdir as mkdir11, readFile as readFile6, writeFile as writeFile12 } from "fs/promises";
|
|
5674
|
+
import { homedir as homedir8 } from "os";
|
|
5675
|
+
import { dirname as dirname5, join as join13 } from "path";
|
|
5676
|
+
var REGISTRY_PATH = join13(homedir8(), ".config", "task", "schedules.json");
|
|
5236
5677
|
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
5237
5678
|
function looksLikeRegistryRow(value) {
|
|
5238
5679
|
if (!value || typeof value !== "object") return false;
|
|
@@ -5252,8 +5693,8 @@ async function readRegistry() {
|
|
|
5252
5693
|
}
|
|
5253
5694
|
}
|
|
5254
5695
|
async function writeRegistry(rows) {
|
|
5255
|
-
await
|
|
5256
|
-
await
|
|
5696
|
+
await mkdir11(dirname5(REGISTRY_PATH), { recursive: true });
|
|
5697
|
+
await writeFile12(REGISTRY_PATH, JSON.stringify(rows, null, 2));
|
|
5257
5698
|
}
|
|
5258
5699
|
async function upsertRegistry(row) {
|
|
5259
5700
|
if (!UUID_RE.test(row.id)) {
|
|
@@ -5493,8 +5934,8 @@ function stripAnsi(s) {
|
|
|
5493
5934
|
|
|
5494
5935
|
// src/commands/runs.ts
|
|
5495
5936
|
import { readFile as readFile7 } from "fs/promises";
|
|
5496
|
-
import { homedir as
|
|
5497
|
-
import { join as
|
|
5937
|
+
import { homedir as homedir9 } from "os";
|
|
5938
|
+
import { join as join14 } from "path";
|
|
5498
5939
|
function registerRuns(program2) {
|
|
5499
5940
|
const cmd = program2.command("runs").description("Inspect agentic CLI run history");
|
|
5500
5941
|
cmd.command("list").description("List recent runs").option("--limit <n>", "Max rows", "50").option("--ticket <id>", "Filter by ticket").option("--schedule <id>", "Filter by schedule").action(async (opts) => {
|
|
@@ -5523,7 +5964,7 @@ function registerRuns(program2) {
|
|
|
5523
5964
|
process.stdout.write(JSON.stringify(row, null, 2) + "\n");
|
|
5524
5965
|
});
|
|
5525
5966
|
cmd.command("logs <id>").description("Show captured agent output for a run, if available").action(async (id) => {
|
|
5526
|
-
const localPath =
|
|
5967
|
+
const localPath = join14(homedir9(), ".cache", "task", "runs", `${id}.log`);
|
|
5527
5968
|
try {
|
|
5528
5969
|
const text = await readFile7(localPath, "utf8");
|
|
5529
5970
|
process.stdout.write(text);
|
|
@@ -5596,8 +6037,8 @@ function registerConfig(program2) {
|
|
|
5596
6037
|
|
|
5597
6038
|
// src/commands/doctor.ts
|
|
5598
6039
|
import { execFileSync as execFileSync12 } from "child_process";
|
|
5599
|
-
import { readFile as readFile8, writeFile as
|
|
5600
|
-
import { join as
|
|
6040
|
+
import { readFile as readFile8, writeFile as writeFile13 } from "fs/promises";
|
|
6041
|
+
import { join as join15 } from "path";
|
|
5601
6042
|
import { request as request6 } from "undici";
|
|
5602
6043
|
var ALLOWED_TEST_EXECUTABLES = /* @__PURE__ */ new Set(["pnpm", "npm", "yarn", "bun", "node", "npx"]);
|
|
5603
6044
|
var DEFAULT_TEST_COMMAND = "pnpm typecheck";
|
|
@@ -5811,7 +6252,7 @@ async function checkPrePushTest(root, configuredCommand, fix) {
|
|
|
5811
6252
|
detail: `${command} (non-script executable, not statically verifiable)`
|
|
5812
6253
|
};
|
|
5813
6254
|
}
|
|
5814
|
-
const pkgPath =
|
|
6255
|
+
const pkgPath = join15(root, "package.json");
|
|
5815
6256
|
let pkgRaw;
|
|
5816
6257
|
try {
|
|
5817
6258
|
pkgRaw = await readFile8(pkgPath, "utf8");
|
|
@@ -5847,7 +6288,7 @@ async function checkPrePushTest(root, configuredCommand, fix) {
|
|
|
5847
6288
|
pkg.scripts = { ...scripts, typecheck: "tsc --noEmit" };
|
|
5848
6289
|
const indent = detectIndent(pkgRaw);
|
|
5849
6290
|
const trailingNewline = pkgRaw.endsWith("\n") ? "\n" : "";
|
|
5850
|
-
await
|
|
6291
|
+
await writeFile13(pkgPath, JSON.stringify(pkg, null, indent) + trailingNewline);
|
|
5851
6292
|
return {
|
|
5852
6293
|
name: "pre-push test",
|
|
5853
6294
|
ok: true,
|
|
@@ -5896,7 +6337,7 @@ function checkBinary(name, command) {
|
|
|
5896
6337
|
}
|
|
5897
6338
|
|
|
5898
6339
|
// src/commands/version.ts
|
|
5899
|
-
var CLI_VERSION = true ? "0.2.
|
|
6340
|
+
var CLI_VERSION = true ? "0.2.28" : "0.0.0-dev";
|
|
5900
6341
|
function registerVersion(program2) {
|
|
5901
6342
|
program2.command("version").description("Print the CLI version").action(() => {
|
|
5902
6343
|
process.stdout.write(CLI_VERSION + "\n");
|