@inteeka/task-cli 0.2.25 → 0.2.27
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 +700 -63
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
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,13 @@ 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",
|
|
196
214
|
"cli.work.pr_recovered",
|
|
197
215
|
"cli.schedule.created",
|
|
198
216
|
"cli.schedule.paused",
|
|
@@ -1523,6 +1541,10 @@ var ALLOWED_TOOLS = CLI_ALLOWED_TOOLS;
|
|
|
1523
1541
|
function allowedToolsFlag() {
|
|
1524
1542
|
return ALLOWED_TOOLS.join(",");
|
|
1525
1543
|
}
|
|
1544
|
+
var REVIEW_ALLOWED_TOOLS = CLI_REVIEW_ALLOWED_TOOLS;
|
|
1545
|
+
function reviewAllowedToolsFlag() {
|
|
1546
|
+
return REVIEW_ALLOWED_TOOLS.join(",");
|
|
1547
|
+
}
|
|
1526
1548
|
|
|
1527
1549
|
// src/agent/system-prompt.ts
|
|
1528
1550
|
function buildSystemPrompt(args) {
|
|
@@ -1581,7 +1603,7 @@ async function runAgent(args) {
|
|
|
1581
1603
|
logHandle = createWriteStream(outputLogPath, { flags: "a" });
|
|
1582
1604
|
}
|
|
1583
1605
|
let stderrBuffer = "";
|
|
1584
|
-
const
|
|
1606
|
+
const STDERR_KEEP2 = 4e3;
|
|
1585
1607
|
return new Promise((resolve2, reject) => {
|
|
1586
1608
|
const child = spawn(claude, cliArgs, {
|
|
1587
1609
|
cwd: args.cwd,
|
|
@@ -1605,7 +1627,7 @@ async function runAgent(args) {
|
|
|
1605
1627
|
} else {
|
|
1606
1628
|
process.stderr.write(chunk);
|
|
1607
1629
|
}
|
|
1608
|
-
stderrBuffer = (stderrBuffer + chunk.toString("utf8")).slice(-
|
|
1630
|
+
stderrBuffer = (stderrBuffer + chunk.toString("utf8")).slice(-STDERR_KEEP2);
|
|
1609
1631
|
});
|
|
1610
1632
|
child.on("close", (code) => {
|
|
1611
1633
|
logHandle?.end();
|
|
@@ -1615,6 +1637,297 @@ async function runAgent(args) {
|
|
|
1615
1637
|
});
|
|
1616
1638
|
}
|
|
1617
1639
|
|
|
1640
|
+
// src/agent/pr-review-service.ts
|
|
1641
|
+
import { spawn as spawn2 } from "child_process";
|
|
1642
|
+
import { mkdir as mkdir7, writeFile as writeFile8 } from "fs/promises";
|
|
1643
|
+
import { homedir as homedir5 } from "os";
|
|
1644
|
+
import { join as join8 } from "path";
|
|
1645
|
+
|
|
1646
|
+
// src/agent/pr-review-prompt.ts
|
|
1647
|
+
var QA_BLOCK = `# /qa lens \u2014 final quality gate
|
|
1648
|
+
|
|
1649
|
+
You are acting as the QA lead reviewer. Score the diff against these checks:
|
|
1650
|
+
|
|
1651
|
+
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.
|
|
1652
|
+
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.
|
|
1653
|
+
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.
|
|
1654
|
+
4. Soft delete \u2014 never hard-delete user-facing entities. Hard delete \u2192 NO-GO.
|
|
1655
|
+
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.
|
|
1656
|
+
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).
|
|
1657
|
+
|
|
1658
|
+
QA verdict scale:
|
|
1659
|
+
GO \u2014 all checks pass, ready to merge
|
|
1660
|
+
CONDITIONAL \u2014 non-blocking gaps (e.g. missing test, small a11y nit). Merge is acceptable but findings should be filed.
|
|
1661
|
+
NO-GO \u2014 at least one blocking check failed
|
|
1662
|
+
`;
|
|
1663
|
+
var SECURITY_BLOCK = `# /security lens \u2014 Security Review Gate
|
|
1664
|
+
|
|
1665
|
+
You are acting as the security agent. Apply the 5 Non-Negotiables and the 5 Security Layers.
|
|
1666
|
+
|
|
1667
|
+
5 Non-Negotiables (any failure \u2192 FAIL):
|
|
1668
|
+
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.
|
|
1669
|
+
2. Zod validation \u2014 every mutation endpoint validates inputs; update schemas use .strict().
|
|
1670
|
+
3. Secrets \u2014 no secrets in code, no NEXT_PUBLIC_ leakage of server-only env vars, no console.log of tokens / cookies / session ids.
|
|
1671
|
+
4. Audit chain \u2014 mutations on audited resources call writeAuditLog; system_audit_log and ticket_activity_log are never UPDATEd or DELETEd.
|
|
1672
|
+
5. CSRF / auth \u2014 dashboard mutations require session auth; widget endpoints require API key; webhook endpoints verify HMAC signature.
|
|
1673
|
+
|
|
1674
|
+
5 Security Layers to review independently:
|
|
1675
|
+
- authentication (session vs api_key vs HMAC selected correctly?)
|
|
1676
|
+
- tenant_resolution (organisation_id resolved from membership / api_key \u2192 project, never from request body?)
|
|
1677
|
+
- authorisation (role check matches endpoint sensitivity?)
|
|
1678
|
+
- rls (queries filtered by organisation_id?)
|
|
1679
|
+
- audit (mutations recorded with writeAuditLog?)
|
|
1680
|
+
|
|
1681
|
+
Security verdict scale:
|
|
1682
|
+
PASS \u2014 all five layers pass, no findings above 'low'
|
|
1683
|
+
PASS_WITH_CONDITIONS \u2014 only 'low' findings; no critical/high/medium issues
|
|
1684
|
+
FAIL \u2014 at least one critical, high, or medium finding, OR any 'fail' in the five layers
|
|
1685
|
+
`;
|
|
1686
|
+
var OUTPUT_CONTRACT = `# Output contract \u2014 REQUIRED
|
|
1687
|
+
|
|
1688
|
+
After your analysis, end your reply with exactly ONE fenced \`\`\`json block matching this schema. No prose after the closing fence.
|
|
1689
|
+
|
|
1690
|
+
\`\`\`json
|
|
1691
|
+
{
|
|
1692
|
+
"overall": "pass" | "fail",
|
|
1693
|
+
"summary": "<one sentence \u2014 what the PR does and the headline verdict>",
|
|
1694
|
+
"qa": {
|
|
1695
|
+
"verdict": "GO" | "CONDITIONAL" | "NO-GO",
|
|
1696
|
+
"failed_checks": ["<short label>", ...]
|
|
1697
|
+
},
|
|
1698
|
+
"security": {
|
|
1699
|
+
"verdict": "PASS" | "PASS_WITH_CONDITIONS" | "FAIL",
|
|
1700
|
+
"layers_reviewed": {
|
|
1701
|
+
"authentication": "PASS" | "FAIL",
|
|
1702
|
+
"tenant_resolution": "PASS" | "FAIL",
|
|
1703
|
+
"authorisation": "PASS" | "FAIL",
|
|
1704
|
+
"rls": "PASS" | "FAIL",
|
|
1705
|
+
"audit": "PASS" | "FAIL"
|
|
1706
|
+
}
|
|
1707
|
+
},
|
|
1708
|
+
"findings": [
|
|
1709
|
+
{
|
|
1710
|
+
"lens": "qa" | "security",
|
|
1711
|
+
"severity": "critical" | "high" | "medium" | "low",
|
|
1712
|
+
"file": "<repo-relative path or null>",
|
|
1713
|
+
"description": "<what is wrong>",
|
|
1714
|
+
"recommendation": "<what to change>"
|
|
1715
|
+
}
|
|
1716
|
+
]
|
|
1717
|
+
}
|
|
1718
|
+
\`\`\`
|
|
1719
|
+
|
|
1720
|
+
Rules:
|
|
1721
|
+
- "overall": "pass" ONLY when qa.verdict === "GO" AND security.verdict === "PASS". Anything else \u2192 "fail".
|
|
1722
|
+
- "findings" is an array (may be empty on a clean pass).
|
|
1723
|
+
- Keep total response under 4000 tokens \u2014 be concise and actionable.
|
|
1724
|
+
`;
|
|
1725
|
+
function buildPrReviewSystemPrompt(_args) {
|
|
1726
|
+
return [
|
|
1727
|
+
"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.",
|
|
1728
|
+
"",
|
|
1729
|
+
"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.",
|
|
1730
|
+
"",
|
|
1731
|
+
"Two lenses, applied independently then combined. Be evidence-driven \u2014 cite file paths in findings.",
|
|
1732
|
+
"",
|
|
1733
|
+
QA_BLOCK,
|
|
1734
|
+
"",
|
|
1735
|
+
SECURITY_BLOCK,
|
|
1736
|
+
"",
|
|
1737
|
+
OUTPUT_CONTRACT
|
|
1738
|
+
].join("\n");
|
|
1739
|
+
}
|
|
1740
|
+
function buildPrReviewUserPrompt(args) {
|
|
1741
|
+
return [
|
|
1742
|
+
`PR #${args.prNumber} (${args.prUrl})`,
|
|
1743
|
+
`Branch: ${args.branchName} \u2192 ${args.baseBranch}`,
|
|
1744
|
+
"",
|
|
1745
|
+
`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.`,
|
|
1746
|
+
"",
|
|
1747
|
+
"Read referenced files in full if a finding depends on surrounding context (RLS policies, validation schemas, audit-log helpers).",
|
|
1748
|
+
"",
|
|
1749
|
+
"When ready, emit your verdict as the fenced JSON block specified in the system prompt. No prose after the closing fence."
|
|
1750
|
+
].join("\n");
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
// src/agent/pr-review-service.ts
|
|
1754
|
+
var PrReviewError = class extends Error {
|
|
1755
|
+
code;
|
|
1756
|
+
excerpt;
|
|
1757
|
+
constructor(code, message, excerpt = "") {
|
|
1758
|
+
super(message);
|
|
1759
|
+
this.name = "PrReviewError";
|
|
1760
|
+
this.code = code;
|
|
1761
|
+
this.excerpt = excerpt;
|
|
1762
|
+
}
|
|
1763
|
+
};
|
|
1764
|
+
var STDERR_KEEP = 4e3;
|
|
1765
|
+
async function runPrReview(args) {
|
|
1766
|
+
const systemPrompt = buildPrReviewSystemPrompt(args);
|
|
1767
|
+
const userPrompt = buildPrReviewUserPrompt(args);
|
|
1768
|
+
const claude = args.claudePath ?? "claude";
|
|
1769
|
+
const cliArgs = [
|
|
1770
|
+
"--allowedTools",
|
|
1771
|
+
reviewAllowedToolsFlag(),
|
|
1772
|
+
"--system-prompt",
|
|
1773
|
+
systemPrompt,
|
|
1774
|
+
...args.modelId ? ["--model", args.modelId] : [],
|
|
1775
|
+
userPrompt
|
|
1776
|
+
];
|
|
1777
|
+
const logDir = join8(homedir5(), ".cache", "task", "runs");
|
|
1778
|
+
await mkdir7(logDir, { recursive: true }).catch(() => {
|
|
1779
|
+
});
|
|
1780
|
+
const logPath = join8(logDir, `${args.runId}.review.log`);
|
|
1781
|
+
await writeFile8(logPath, "").catch(() => {
|
|
1782
|
+
});
|
|
1783
|
+
const { createWriteStream } = await import("fs");
|
|
1784
|
+
const logHandle = createWriteStream(logPath, { flags: "a" });
|
|
1785
|
+
let stdoutBuffer = "";
|
|
1786
|
+
let stderrTail = "";
|
|
1787
|
+
const exitCode = await new Promise((resolve2, reject) => {
|
|
1788
|
+
const child = spawn2(claude, cliArgs, {
|
|
1789
|
+
cwd: args.cwd,
|
|
1790
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1791
|
+
env: { ...process.env, FORCE_COLOR: args.silent ? "0" : "1" }
|
|
1792
|
+
});
|
|
1793
|
+
child.on("error", (err) => {
|
|
1794
|
+
logHandle.end();
|
|
1795
|
+
reject(new PrReviewError("spawn_failed", `Could not spawn claude: ${err.message}`));
|
|
1796
|
+
});
|
|
1797
|
+
child.stdout?.on("data", (chunk) => {
|
|
1798
|
+
stdoutBuffer += chunk.toString("utf8");
|
|
1799
|
+
if (args.silent) {
|
|
1800
|
+
logHandle.write(chunk);
|
|
1801
|
+
} else {
|
|
1802
|
+
process.stdout.write(chunk);
|
|
1803
|
+
logHandle.write(chunk);
|
|
1804
|
+
}
|
|
1805
|
+
});
|
|
1806
|
+
child.stderr?.on("data", (chunk) => {
|
|
1807
|
+
if (args.silent) {
|
|
1808
|
+
logHandle.write(chunk);
|
|
1809
|
+
} else {
|
|
1810
|
+
process.stderr.write(chunk);
|
|
1811
|
+
logHandle.write(chunk);
|
|
1812
|
+
}
|
|
1813
|
+
stderrTail = (stderrTail + chunk.toString("utf8")).slice(-STDERR_KEEP);
|
|
1814
|
+
});
|
|
1815
|
+
child.on("close", (code) => {
|
|
1816
|
+
logHandle.end();
|
|
1817
|
+
resolve2(code ?? 0);
|
|
1818
|
+
});
|
|
1819
|
+
});
|
|
1820
|
+
if (exitCode !== 0) {
|
|
1821
|
+
throw new PrReviewError(
|
|
1822
|
+
"nonzero_exit",
|
|
1823
|
+
`claude review subprocess exited with code ${exitCode}`,
|
|
1824
|
+
stderrTail
|
|
1825
|
+
);
|
|
1826
|
+
}
|
|
1827
|
+
return parseVerdict(stdoutBuffer, logPath);
|
|
1828
|
+
}
|
|
1829
|
+
function parseVerdict(stdout, logPath) {
|
|
1830
|
+
const blocks = extractJsonBlocks(stdout);
|
|
1831
|
+
if (blocks.length === 0) {
|
|
1832
|
+
throw new PrReviewError(
|
|
1833
|
+
"no_json_block",
|
|
1834
|
+
"Could not find a fenced ```json block in the review subprocess output",
|
|
1835
|
+
stdout.slice(-2e3)
|
|
1836
|
+
);
|
|
1837
|
+
}
|
|
1838
|
+
const rawJson = blocks[blocks.length - 1];
|
|
1839
|
+
let parsed;
|
|
1840
|
+
try {
|
|
1841
|
+
parsed = JSON.parse(rawJson);
|
|
1842
|
+
} catch (err) {
|
|
1843
|
+
throw new PrReviewError(
|
|
1844
|
+
"invalid_json",
|
|
1845
|
+
`JSON.parse failed: ${err.message}`,
|
|
1846
|
+
rawJson.slice(0, 2e3)
|
|
1847
|
+
);
|
|
1848
|
+
}
|
|
1849
|
+
const verdict = normaliseVerdict(parsed, rawJson, logPath);
|
|
1850
|
+
return verdict;
|
|
1851
|
+
}
|
|
1852
|
+
function extractJsonBlocks(text) {
|
|
1853
|
+
const re = /```(?:json|JSON|jsonc)\s*\n([\s\S]*?)\n```/g;
|
|
1854
|
+
const out = [];
|
|
1855
|
+
let match;
|
|
1856
|
+
while ((match = re.exec(text)) !== null) {
|
|
1857
|
+
if (match[1] !== void 0) out.push(match[1]);
|
|
1858
|
+
}
|
|
1859
|
+
return out;
|
|
1860
|
+
}
|
|
1861
|
+
var QA_VERDICTS = /* @__PURE__ */ new Set(["GO", "CONDITIONAL", "NO-GO"]);
|
|
1862
|
+
var SECURITY_VERDICTS = /* @__PURE__ */ new Set(["PASS", "PASS_WITH_CONDITIONS", "FAIL"]);
|
|
1863
|
+
var SECURITY_LAYER_VERDICTS = /* @__PURE__ */ new Set(["PASS", "FAIL"]);
|
|
1864
|
+
var SEVERITIES = /* @__PURE__ */ new Set(["critical", "high", "medium", "low"]);
|
|
1865
|
+
var LAYER_KEYS = [
|
|
1866
|
+
"authentication",
|
|
1867
|
+
"tenant_resolution",
|
|
1868
|
+
"authorisation",
|
|
1869
|
+
"rls",
|
|
1870
|
+
"audit"
|
|
1871
|
+
];
|
|
1872
|
+
function normaliseVerdict(raw, rawJson, logPath) {
|
|
1873
|
+
if (raw === null || typeof raw !== "object") {
|
|
1874
|
+
throw new PrReviewError(
|
|
1875
|
+
"schema_mismatch",
|
|
1876
|
+
"Verdict JSON is not an object",
|
|
1877
|
+
rawJson.slice(0, 1e3)
|
|
1878
|
+
);
|
|
1879
|
+
}
|
|
1880
|
+
const r = raw;
|
|
1881
|
+
const qaRaw = r["qa"] ?? {};
|
|
1882
|
+
const secRaw = r["security"] ?? {};
|
|
1883
|
+
const layersRaw = secRaw["layers_reviewed"] ?? {};
|
|
1884
|
+
const qaVerdict = String(qaRaw["verdict"] ?? "");
|
|
1885
|
+
if (!QA_VERDICTS.has(qaVerdict)) {
|
|
1886
|
+
throw new PrReviewError(
|
|
1887
|
+
"schema_mismatch",
|
|
1888
|
+
`qa.verdict is "${qaVerdict}", expected one of GO / CONDITIONAL / NO-GO`,
|
|
1889
|
+
rawJson.slice(0, 1e3)
|
|
1890
|
+
);
|
|
1891
|
+
}
|
|
1892
|
+
const securityVerdict = String(secRaw["verdict"] ?? "");
|
|
1893
|
+
if (!SECURITY_VERDICTS.has(securityVerdict)) {
|
|
1894
|
+
throw new PrReviewError(
|
|
1895
|
+
"schema_mismatch",
|
|
1896
|
+
`security.verdict is "${securityVerdict}", expected PASS / PASS_WITH_CONDITIONS / FAIL`,
|
|
1897
|
+
rawJson.slice(0, 1e3)
|
|
1898
|
+
);
|
|
1899
|
+
}
|
|
1900
|
+
const layers = {};
|
|
1901
|
+
for (const key of LAYER_KEYS) {
|
|
1902
|
+
const val = String(layersRaw[key] ?? "");
|
|
1903
|
+
layers[key] = SECURITY_LAYER_VERDICTS.has(val) ? val : "FAIL";
|
|
1904
|
+
}
|
|
1905
|
+
const findingsRaw = Array.isArray(r["findings"]) ? r["findings"] : [];
|
|
1906
|
+
const findings = findingsRaw.slice(0, 50).map((f) => {
|
|
1907
|
+
const fr = f ?? {};
|
|
1908
|
+
const severity = String(fr["severity"] ?? "low");
|
|
1909
|
+
const lensRaw = String(fr["lens"] ?? "qa");
|
|
1910
|
+
return {
|
|
1911
|
+
lens: lensRaw === "security" ? "security" : "qa",
|
|
1912
|
+
severity: SEVERITIES.has(severity) ? severity : "low",
|
|
1913
|
+
file: typeof fr["file"] === "string" ? fr["file"].slice(0, 500) : null,
|
|
1914
|
+
description: String(fr["description"] ?? "").slice(0, 2e3),
|
|
1915
|
+
recommendation: String(fr["recommendation"] ?? "").slice(0, 2e3)
|
|
1916
|
+
};
|
|
1917
|
+
});
|
|
1918
|
+
const failedChecks = Array.isArray(qaRaw["failed_checks"]) ? qaRaw["failed_checks"].map((s) => String(s).slice(0, 200)).slice(0, 20) : [];
|
|
1919
|
+
const overall = qaVerdict === "GO" && securityVerdict === "PASS" ? "pass" : "fail";
|
|
1920
|
+
return {
|
|
1921
|
+
overall,
|
|
1922
|
+
summary: String(r["summary"] ?? "").slice(0, 1e3) || (overall === "pass" ? "Review passed." : "Review failed."),
|
|
1923
|
+
qa: { verdict: qaVerdict, failed_checks: failedChecks },
|
|
1924
|
+
security: { verdict: securityVerdict, layers_reviewed: layers },
|
|
1925
|
+
findings,
|
|
1926
|
+
rawJson: rawJson.slice(0, 5e4),
|
|
1927
|
+
logPath
|
|
1928
|
+
};
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1618
1931
|
// src/guardrail/diff-check.ts
|
|
1619
1932
|
import { execFileSync as execFileSync4 } from "child_process";
|
|
1620
1933
|
|
|
@@ -1705,7 +2018,7 @@ function discardWorkingTreeChanges(cwd) {
|
|
|
1705
2018
|
}
|
|
1706
2019
|
|
|
1707
2020
|
// src/test-runner/run-tests.ts
|
|
1708
|
-
import { spawn as
|
|
2021
|
+
import { spawn as spawn3 } from "child_process";
|
|
1709
2022
|
var ALLOWED_EXECUTABLES = /* @__PURE__ */ new Set(["pnpm", "npm", "yarn", "bun", "node", "npx"]);
|
|
1710
2023
|
var DEFAULT_COMMAND = "pnpm typecheck";
|
|
1711
2024
|
var TIMEOUT_MS = 10 * 60 * 1e3;
|
|
@@ -1733,7 +2046,7 @@ async function runProjectTest(args) {
|
|
|
1733
2046
|
const [exe, ...rest] = argv;
|
|
1734
2047
|
const startedAt = Date.now();
|
|
1735
2048
|
return new Promise((resolve2) => {
|
|
1736
|
-
const child =
|
|
2049
|
+
const child = spawn3(exe, rest, {
|
|
1737
2050
|
cwd: args.cwd,
|
|
1738
2051
|
stdio: ["ignore", "pipe", "pipe"],
|
|
1739
2052
|
shell: false,
|
|
@@ -1778,16 +2091,16 @@ async function runProjectTest(args) {
|
|
|
1778
2091
|
}
|
|
1779
2092
|
|
|
1780
2093
|
// src/test-runner/auto-install.ts
|
|
1781
|
-
import { spawn as
|
|
2094
|
+
import { spawn as spawn4 } from "child_process";
|
|
1782
2095
|
import { stat as stat2 } from "fs/promises";
|
|
1783
|
-
import { dirname as dirname4, join as
|
|
2096
|
+
import { dirname as dirname4, join as join9 } from "path";
|
|
1784
2097
|
var INSTALL_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
1785
2098
|
var TAIL_BYTES2 = 4e3;
|
|
1786
2099
|
async function findPnpmWorkspaceRoot(start) {
|
|
1787
2100
|
let cur = start;
|
|
1788
2101
|
for (; ; ) {
|
|
1789
2102
|
try {
|
|
1790
|
-
await stat2(
|
|
2103
|
+
await stat2(join9(cur, "pnpm-lock.yaml"));
|
|
1791
2104
|
return cur;
|
|
1792
2105
|
} catch {
|
|
1793
2106
|
}
|
|
@@ -1817,8 +2130,8 @@ async function ensureWorkspaceInstalled(args) {
|
|
|
1817
2130
|
tail: ""
|
|
1818
2131
|
};
|
|
1819
2132
|
}
|
|
1820
|
-
const lockMtime = await mtimeMs(
|
|
1821
|
-
const markerMtime = await mtimeMs(
|
|
2133
|
+
const lockMtime = await mtimeMs(join9(workspaceRoot, "pnpm-lock.yaml"));
|
|
2134
|
+
const markerMtime = await mtimeMs(join9(workspaceRoot, "node_modules", ".modules.yaml"));
|
|
1822
2135
|
if (lockMtime !== null && markerMtime !== null && markerMtime >= lockMtime) {
|
|
1823
2136
|
return {
|
|
1824
2137
|
ok: true,
|
|
@@ -1832,7 +2145,7 @@ async function ensureWorkspaceInstalled(args) {
|
|
|
1832
2145
|
}
|
|
1833
2146
|
const reason = markerMtime === null ? "node_modules missing \u2014 cold install" : "pnpm-lock.yaml newer than install marker";
|
|
1834
2147
|
return new Promise((resolve2) => {
|
|
1835
|
-
const child =
|
|
2148
|
+
const child = spawn4("pnpm", ["install", "--frozen-lockfile"], {
|
|
1836
2149
|
cwd: workspaceRoot,
|
|
1837
2150
|
stdio: ["ignore", "pipe", "pipe"],
|
|
1838
2151
|
shell: false,
|
|
@@ -1878,10 +2191,10 @@ async function ensureWorkspaceInstalled(args) {
|
|
|
1878
2191
|
}
|
|
1879
2192
|
|
|
1880
2193
|
// src/util/progress.ts
|
|
1881
|
-
import { mkdir as
|
|
2194
|
+
import { mkdir as mkdir8, writeFile as writeFile9, rename as rename2, unlink as unlink3, readdir, stat as stat3 } from "fs/promises";
|
|
1882
2195
|
import { tmpdir } from "os";
|
|
1883
|
-
import { join as
|
|
1884
|
-
var PROGRESS_DIR =
|
|
2196
|
+
import { join as join10 } from "path";
|
|
2197
|
+
var PROGRESS_DIR = join10(tmpdir(), "task-progress");
|
|
1885
2198
|
var STALE_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
1886
2199
|
var ProgressWriter = class {
|
|
1887
2200
|
path;
|
|
@@ -1894,7 +2207,7 @@ var ProgressWriter = class {
|
|
|
1894
2207
|
this.ticketId = ticketId;
|
|
1895
2208
|
const deliveryId = process.env["TASK_DELIVERY_ID"]?.trim();
|
|
1896
2209
|
const fileBase = deliveryId && deliveryId.length > 0 ? deliveryId : `manual-${process.pid}`;
|
|
1897
|
-
this.path =
|
|
2210
|
+
this.path = join10(PROGRESS_DIR, `${fileBase}.json`);
|
|
1898
2211
|
}
|
|
1899
2212
|
/** Switch the in-flight ticket between phases (used by `task scan` which iterates). */
|
|
1900
2213
|
setTicketId(ticketId) {
|
|
@@ -1928,12 +2241,12 @@ var ProgressWriter = class {
|
|
|
1928
2241
|
});
|
|
1929
2242
|
}
|
|
1930
2243
|
async writeAtomic(payload) {
|
|
1931
|
-
await
|
|
2244
|
+
await mkdir8(PROGRESS_DIR, { recursive: true }).catch(() => {
|
|
1932
2245
|
});
|
|
1933
2246
|
const body = JSON.stringify(payload);
|
|
1934
2247
|
const tmp = `${this.path}.tmp`;
|
|
1935
2248
|
try {
|
|
1936
|
-
await
|
|
2249
|
+
await writeFile9(tmp, body, { encoding: "utf8", mode: 384 });
|
|
1937
2250
|
await rename2(tmp, this.path);
|
|
1938
2251
|
} catch {
|
|
1939
2252
|
await unlink3(tmp).catch(() => {
|
|
@@ -1950,7 +2263,7 @@ var ProgressWriter = class {
|
|
|
1950
2263
|
const cutoff = Date.now() - STALE_MAX_AGE_MS;
|
|
1951
2264
|
await Promise.all(
|
|
1952
2265
|
entries.filter((name) => name.endsWith(".json") || name.endsWith(".json.tmp")).map(async (name) => {
|
|
1953
|
-
const p =
|
|
2266
|
+
const p = join10(PROGRESS_DIR, name);
|
|
1954
2267
|
try {
|
|
1955
2268
|
const s = await stat3(p);
|
|
1956
2269
|
if (s.mtimeMs < cutoff) {
|
|
@@ -1992,6 +2305,9 @@ function registerWork(program2) {
|
|
|
1992
2305
|
).option(
|
|
1993
2306
|
"--confirm",
|
|
1994
2307
|
"Confirm --reset in non-TTY (silent / scheduled-task) contexts. Has no effect without --reset."
|
|
2308
|
+
).option(
|
|
2309
|
+
"--no-auto-review",
|
|
2310
|
+
"Skip the post-PR /qa + /security auto-review. The PR is left open for a human to review and merge."
|
|
1995
2311
|
).option("--schedule-id <id>", "Internal: schedule id when invoked from a scheduled task").action(async (ticketId, opts) => {
|
|
1996
2312
|
await runWork(ticketId, opts);
|
|
1997
2313
|
});
|
|
@@ -2515,6 +2831,86 @@ Claude session: ${runId}
|
|
|
2515
2831
|
if (err instanceof CliError) err.phase = "post_push";
|
|
2516
2832
|
throw err;
|
|
2517
2833
|
}
|
|
2834
|
+
const autoReviewEnabled = opts.autoReview !== false;
|
|
2835
|
+
if (autoReviewEnabled && prNumber !== void 0 && prUrl !== void 0 && !opts.dryRun) {
|
|
2836
|
+
await progress.setPhase("auto_reviewing");
|
|
2837
|
+
if (!silent) {
|
|
2838
|
+
process.stdout.write(c.dim(" Auto-reviewing PR \u2014 /qa + /security\u2026\n"));
|
|
2839
|
+
}
|
|
2840
|
+
try {
|
|
2841
|
+
const verdict = await runPrReview({
|
|
2842
|
+
cwd,
|
|
2843
|
+
runId,
|
|
2844
|
+
silent,
|
|
2845
|
+
baseBranch: ticketBaseBranch,
|
|
2846
|
+
branchName,
|
|
2847
|
+
prNumber,
|
|
2848
|
+
prUrl,
|
|
2849
|
+
...ctx.localCfg.claude_path ? { claudePath: ctx.localCfg.claude_path } : {}
|
|
2850
|
+
});
|
|
2851
|
+
const verdictResp = await apiCallOrThrow(
|
|
2852
|
+
"POST",
|
|
2853
|
+
`/api/v1/cli/me/tickets/${detail.id}/pull-requests/${prNumber}/auto-review-verdict`,
|
|
2854
|
+
{
|
|
2855
|
+
body: {
|
|
2856
|
+
overall: verdict.overall,
|
|
2857
|
+
summary: verdict.summary,
|
|
2858
|
+
qa: verdict.qa,
|
|
2859
|
+
security: verdict.security,
|
|
2860
|
+
findings: verdict.findings,
|
|
2861
|
+
raw_json: verdict.rawJson,
|
|
2862
|
+
claude_session_id: runId
|
|
2863
|
+
}
|
|
2864
|
+
}
|
|
2865
|
+
);
|
|
2866
|
+
await apiCall("POST", "/api/v1/cli/me/runs", {
|
|
2867
|
+
body: {
|
|
2868
|
+
ticket_id: detail.id,
|
|
2869
|
+
schedule_id: opts.scheduleId,
|
|
2870
|
+
event: verdict.overall === "pass" ? "auto_review_passed" : "auto_review_failed",
|
|
2871
|
+
claude_session_id: runId,
|
|
2872
|
+
output_excerpt: `qa=${verdict.qa.verdict} security=${verdict.security.verdict}` + (verdict.findings.length > 0 ? ` | ${verdict.findings.length} finding(s)` : "")
|
|
2873
|
+
}
|
|
2874
|
+
});
|
|
2875
|
+
if (!silent) {
|
|
2876
|
+
const icon = verdict.overall === "pass" ? c.ok("\u2713 Auto-review PASS") : c.warn("\u2717 Auto-review FAIL");
|
|
2877
|
+
process.stdout.write(
|
|
2878
|
+
`${icon} qa=${verdict.qa.verdict} security=${verdict.security.verdict}
|
|
2879
|
+
`
|
|
2880
|
+
);
|
|
2881
|
+
if (verdict.overall === "pass") {
|
|
2882
|
+
process.stdout.write(
|
|
2883
|
+
c.dim(
|
|
2884
|
+
verdictResp.status === "queued_for_merge" ? " PR approved \u2014 queued for merge to " : " Verdict recorded; merge queue update: "
|
|
2885
|
+
) + c.cyan(`${ticketBaseBranch}
|
|
2886
|
+
`)
|
|
2887
|
+
);
|
|
2888
|
+
} else {
|
|
2889
|
+
process.stdout.write(
|
|
2890
|
+
c.dim(` ${verdict.findings.length} finding(s) posted as PR comment; PR left open.
|
|
2891
|
+
`)
|
|
2892
|
+
);
|
|
2893
|
+
}
|
|
2894
|
+
}
|
|
2895
|
+
} catch (err) {
|
|
2896
|
+
const excerpt = err instanceof PrReviewError ? `${err.code}: ${err.message.slice(0, 1e3)}` : err.message.slice(0, 1e3);
|
|
2897
|
+
await apiCall("POST", "/api/v1/cli/me/runs", {
|
|
2898
|
+
body: {
|
|
2899
|
+
ticket_id: detail.id,
|
|
2900
|
+
schedule_id: opts.scheduleId,
|
|
2901
|
+
event: "auto_review_errored",
|
|
2902
|
+
claude_session_id: runId,
|
|
2903
|
+
output_excerpt: excerpt
|
|
2904
|
+
}
|
|
2905
|
+
});
|
|
2906
|
+
if (!silent) {
|
|
2907
|
+
process.stdout.write(
|
|
2908
|
+
`${c.warn("! Auto-review skipped")} ${c.dim(excerpt.slice(0, 200))}
|
|
2909
|
+
` + c.dim(" PR left open for manual review.\n")
|
|
2910
|
+
);
|
|
2911
|
+
}
|
|
2912
|
+
}
|
|
2913
|
+
}
|
|
2518
2914
|
try {
|
|
2519
2915
|
checkoutBranch(cwd, baseBranch);
|
|
2520
2916
|
} catch {
|
|
@@ -3431,10 +3827,10 @@ function autopilotExitCode(code, status) {
|
|
|
3431
3827
|
}
|
|
3432
3828
|
|
|
3433
3829
|
// src/scan/llm.ts
|
|
3434
|
-
import { spawn as
|
|
3435
|
-
import { mkdir as
|
|
3436
|
-
import { homedir as
|
|
3437
|
-
import { join as
|
|
3830
|
+
import { spawn as spawn5 } from "child_process";
|
|
3831
|
+
import { mkdir as mkdir9, writeFile as writeFile10 } from "fs/promises";
|
|
3832
|
+
import { homedir as homedir6 } from "os";
|
|
3833
|
+
import { join as join11 } from "path";
|
|
3438
3834
|
var FIX_PROMPT_JSON_SCHEMA = {
|
|
3439
3835
|
type: "object",
|
|
3440
3836
|
// Phase 3 — confidence_reason is REQUIRED unconditionally so the
|
|
@@ -3523,7 +3919,7 @@ async function generateFixPromptJson(args) {
|
|
|
3523
3919
|
return new Promise((resolve2, reject) => {
|
|
3524
3920
|
let child;
|
|
3525
3921
|
try {
|
|
3526
|
-
child =
|
|
3922
|
+
child = spawn5(claude, cliArgs, {
|
|
3527
3923
|
stdio: ["pipe", "pipe", "pipe"],
|
|
3528
3924
|
signal: args.signal
|
|
3529
3925
|
});
|
|
@@ -3649,10 +4045,10 @@ function readEnvelopeTokens(raw, userPrompt, innerText) {
|
|
|
3649
4045
|
async function maybeDumpDebug(ticketId, stdout, stderr) {
|
|
3650
4046
|
if (!DEBUG && stdout.length === 0 && stderr.length === 0) return null;
|
|
3651
4047
|
try {
|
|
3652
|
-
const dir =
|
|
3653
|
-
await
|
|
3654
|
-
const path =
|
|
3655
|
-
await
|
|
4048
|
+
const dir = join11(homedir6(), ".cache", "task", "scan-debug");
|
|
4049
|
+
await mkdir9(dir, { recursive: true });
|
|
4050
|
+
const path = join11(dir, `${ticketId}-${Date.now()}.log`);
|
|
4051
|
+
await writeFile10(
|
|
3656
4052
|
path,
|
|
3657
4053
|
["## ticket_id", ticketId, "", "## stdout", stdout, "", "## stderr", stderr].join("\n")
|
|
3658
4054
|
);
|
|
@@ -3997,6 +4393,239 @@ function clampInt(raw, min, max, fallback) {
|
|
|
3997
4393
|
return Math.min(v, max);
|
|
3998
4394
|
}
|
|
3999
4395
|
|
|
4396
|
+
// src/commands/slack-import.ts
|
|
4397
|
+
import { spawn as spawn6 } from "child_process";
|
|
4398
|
+
import { request as request5 } from "undici";
|
|
4399
|
+
var BULLET_TYPES = ["bug", "feature", "task", "question", "improvement"];
|
|
4400
|
+
var BULLET_PRIORITIES = ["critical", "high", "medium", "low", "none"];
|
|
4401
|
+
var CLASSIFICATIONS = ["code", "physical"];
|
|
4402
|
+
var BULLETS_SCHEMA = {
|
|
4403
|
+
type: "object",
|
|
4404
|
+
required: ["bullets"],
|
|
4405
|
+
additionalProperties: false,
|
|
4406
|
+
properties: {
|
|
4407
|
+
bullets: {
|
|
4408
|
+
type: "array",
|
|
4409
|
+
maxItems: 200,
|
|
4410
|
+
items: {
|
|
4411
|
+
type: "object",
|
|
4412
|
+
required: ["title", "description", "type", "priority", "classification"],
|
|
4413
|
+
additionalProperties: false,
|
|
4414
|
+
properties: {
|
|
4415
|
+
title: { type: "string", minLength: 1, maxLength: 500 },
|
|
4416
|
+
description: { type: "string", minLength: 1, maxLength: 8e3 },
|
|
4417
|
+
type: { type: "string", enum: BULLET_TYPES },
|
|
4418
|
+
priority: { type: "string", enum: BULLET_PRIORITIES },
|
|
4419
|
+
classification: { type: "string", enum: CLASSIFICATIONS },
|
|
4420
|
+
reason: { type: "string", maxLength: 500 }
|
|
4421
|
+
}
|
|
4422
|
+
}
|
|
4423
|
+
}
|
|
4424
|
+
}
|
|
4425
|
+
};
|
|
4426
|
+
var SYSTEM_PROMPT = `You convert raw Slack call notes into a list of actionable tickets, one per concrete action item.
|
|
4427
|
+
|
|
4428
|
+
For each bullet decide:
|
|
4429
|
+
- classification = "code" if the item requires changes to the team's software (bug fixes, feature work, refactors, deploys, infra/config changes that a developer would do).
|
|
4430
|
+
- classification = "physical" if it is a human action that doesn't touch the codebase (sending an email, calling a vendor, booking a meeting, writing a non-code doc, scheduling someone's time, follow-ups, status updates).
|
|
4431
|
+
|
|
4432
|
+
When in doubt, prefer "physical". A false-positive "code" ticket wastes AI budget and pollutes the dev queue. A false-positive "physical" ticket still gets tracked and a human can re-classify.
|
|
4433
|
+
|
|
4434
|
+
For each ticket also choose:
|
|
4435
|
+
- type: bug | feature | task | question | improvement (task is the safe default)
|
|
4436
|
+
- priority: critical | high | medium | low | none (medium is the safe default)
|
|
4437
|
+
|
|
4438
|
+
The "reason" field is a brief 1-sentence explanation of WHY you classified the bullet this way \u2014 useful for the team reviewing the import. Keep it under 500 chars.
|
|
4439
|
+
|
|
4440
|
+
Drop greetings, social chatter, recaps that aren't actionable, and anything that is purely informational. Each bullet must be a concrete action someone has to do.
|
|
4441
|
+
|
|
4442
|
+
Return JSON only, matching the supplied schema. No prose, no markdown fences, no commentary.`;
|
|
4443
|
+
var SLACK_IMPORT_MODEL = "claude-sonnet-4-6";
|
|
4444
|
+
function sanitiseNotes(raw) {
|
|
4445
|
+
return raw.replace(/ignore\s+(all\s+)?previous\s+instructions/gi, "[REDACTED]").replace(/system\s*:\s*/gi, "[REDACTED]");
|
|
4446
|
+
}
|
|
4447
|
+
function registerSlackImport(program2) {
|
|
4448
|
+
program2.command("slack-import").description(
|
|
4449
|
+
"Internal: parse Slack call notes (from stdin) into classified bullets and PATCH them back to a slack_imports row. Spawned by the listener \u2014 not for direct human use."
|
|
4450
|
+
).requiredOption("--import-id <uuid>", "Slack-import row id (from the webhook payload)").requiredOption("--organisation-id <uuid>", "Organisation id of the import").requiredOption("--project-id <uuid>", "Project id of the import").requiredOption("--update-url <url>", "Absolute callback URL for PATCH status updates").option(
|
|
4451
|
+
"--notes-stdin",
|
|
4452
|
+
"Read raw notes from stdin (default; the listener passes them this way)"
|
|
4453
|
+
).option("--notes <text>", "Inline notes for local testing (mutually exclusive with stdin)").option("--claude-path <path>", "Override the claude binary path").action(async (opts) => {
|
|
4454
|
+
await runSlackImport(opts);
|
|
4455
|
+
});
|
|
4456
|
+
}
|
|
4457
|
+
async function runSlackImport(opts) {
|
|
4458
|
+
let creds = await readCredentials();
|
|
4459
|
+
if (!creds) {
|
|
4460
|
+
throw new CliError(
|
|
4461
|
+
CLI_EXIT_CODES.MISCONFIGURATION,
|
|
4462
|
+
"Not signed in",
|
|
4463
|
+
"Run 'task login' on this host so the listener can PATCH back."
|
|
4464
|
+
);
|
|
4465
|
+
}
|
|
4466
|
+
creds = await ensureFreshAccessToken(creds);
|
|
4467
|
+
await patchStatus(opts.updateUrl, creds.access_token, { status: "processing" }).catch((err) => {
|
|
4468
|
+
process.stderr.write(`[slack-import] processing PATCH failed: ${err.message}
|
|
4469
|
+
`);
|
|
4470
|
+
});
|
|
4471
|
+
const rawNotes = await readNotes(opts);
|
|
4472
|
+
if (!rawNotes.trim()) {
|
|
4473
|
+
await patchStatus(opts.updateUrl, creds.access_token, {
|
|
4474
|
+
status: "failed",
|
|
4475
|
+
error_message: "No notes provided on stdin"
|
|
4476
|
+
});
|
|
4477
|
+
throw new CliError(CLI_EXIT_CODES.GENERIC_ERROR, "No notes provided");
|
|
4478
|
+
}
|
|
4479
|
+
let bullets;
|
|
4480
|
+
try {
|
|
4481
|
+
bullets = await generateBullets({
|
|
4482
|
+
notes: sanitiseNotes(rawNotes),
|
|
4483
|
+
claudePath: opts.claudePath
|
|
4484
|
+
});
|
|
4485
|
+
} catch (err) {
|
|
4486
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
4487
|
+
await patchStatus(opts.updateUrl, creds.access_token, {
|
|
4488
|
+
status: "failed",
|
|
4489
|
+
error_message: msg.slice(0, 2e3)
|
|
4490
|
+
}).catch(() => void 0);
|
|
4491
|
+
throw new CliError(CLI_EXIT_CODES.GENERIC_ERROR, `Bullet generation failed: ${msg}`);
|
|
4492
|
+
}
|
|
4493
|
+
const result = await patchStatus(opts.updateUrl, creds.access_token, {
|
|
4494
|
+
status: "completed",
|
|
4495
|
+
bullets
|
|
4496
|
+
});
|
|
4497
|
+
process.stdout.write(
|
|
4498
|
+
`${c.ok("\u2713")} slack-import ${opts.importId.slice(0, 8)}\u2026 \u2014 ${bullets.length} bullets (${result.code_count ?? 0} code, ${result.physical_count ?? 0} physical)
|
|
4499
|
+
`
|
|
4500
|
+
);
|
|
4501
|
+
}
|
|
4502
|
+
async function readNotes(opts) {
|
|
4503
|
+
if (opts.notes !== void 0) return opts.notes;
|
|
4504
|
+
const chunks = [];
|
|
4505
|
+
let total = 0;
|
|
4506
|
+
for await (const chunk of process.stdin) {
|
|
4507
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
4508
|
+
total += buf.length;
|
|
4509
|
+
if (total > 6e4) {
|
|
4510
|
+
throw new CliError(CLI_EXIT_CODES.GENERIC_ERROR, "Notes exceed 60KB");
|
|
4511
|
+
}
|
|
4512
|
+
chunks.push(buf);
|
|
4513
|
+
}
|
|
4514
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
4515
|
+
}
|
|
4516
|
+
async function generateBullets(args) {
|
|
4517
|
+
const claude = args.claudePath ?? "claude";
|
|
4518
|
+
const cliArgs = [
|
|
4519
|
+
"--print",
|
|
4520
|
+
"--output-format",
|
|
4521
|
+
"json",
|
|
4522
|
+
"--tools",
|
|
4523
|
+
"",
|
|
4524
|
+
"--system-prompt",
|
|
4525
|
+
SYSTEM_PROMPT,
|
|
4526
|
+
"--model",
|
|
4527
|
+
SLACK_IMPORT_MODEL,
|
|
4528
|
+
"--json-schema",
|
|
4529
|
+
JSON.stringify(BULLETS_SCHEMA)
|
|
4530
|
+
];
|
|
4531
|
+
return new Promise((resolve2, reject) => {
|
|
4532
|
+
let child;
|
|
4533
|
+
try {
|
|
4534
|
+
child = spawn6(claude, cliArgs, { stdio: ["pipe", "pipe", "pipe"] });
|
|
4535
|
+
} catch (err) {
|
|
4536
|
+
reject(new Error(`Could not invoke claude: ${err.message}`));
|
|
4537
|
+
return;
|
|
4538
|
+
}
|
|
4539
|
+
let stdoutBuf = "";
|
|
4540
|
+
let stderrBuf = "";
|
|
4541
|
+
child.stdout?.on("data", (b) => stdoutBuf += b.toString("utf8"));
|
|
4542
|
+
child.stderr?.on("data", (b) => stderrBuf += b.toString("utf8"));
|
|
4543
|
+
child.on("error", (err) => reject(err));
|
|
4544
|
+
child.on("close", (code) => {
|
|
4545
|
+
if (code !== 0) {
|
|
4546
|
+
reject(
|
|
4547
|
+
new Error(
|
|
4548
|
+
`claude exited ${code}${stderrBuf.trim() ? ": " + stderrBuf.trim().slice(0, 500) : ""}`
|
|
4549
|
+
)
|
|
4550
|
+
);
|
|
4551
|
+
return;
|
|
4552
|
+
}
|
|
4553
|
+
const extracted = extractStructured(stdoutBuf);
|
|
4554
|
+
if (!extracted.ok) {
|
|
4555
|
+
reject(new Error(extracted.error));
|
|
4556
|
+
return;
|
|
4557
|
+
}
|
|
4558
|
+
const list = extracted.value.bullets;
|
|
4559
|
+
if (!Array.isArray(list)) {
|
|
4560
|
+
reject(new Error("claude returned no bullets array"));
|
|
4561
|
+
return;
|
|
4562
|
+
}
|
|
4563
|
+
const out = [];
|
|
4564
|
+
for (const b of list) {
|
|
4565
|
+
const cls = b["classification"];
|
|
4566
|
+
if (cls !== "code" && cls !== "physical") continue;
|
|
4567
|
+
const title = typeof b["title"] === "string" ? b["title"].slice(0, 500) : "";
|
|
4568
|
+
const description = typeof b["description"] === "string" ? b["description"].slice(0, 8e3) : "";
|
|
4569
|
+
if (!title || !description) continue;
|
|
4570
|
+
const type2 = BULLET_TYPES.includes(b["type"]) ? b["type"] : "task";
|
|
4571
|
+
const priority = BULLET_PRIORITIES.includes(b["priority"]) ? b["priority"] : "medium";
|
|
4572
|
+
const item = { title, description, type: type2, priority, classification: cls };
|
|
4573
|
+
if (typeof b["reason"] === "string") item.reason = b["reason"].slice(0, 500);
|
|
4574
|
+
out.push(item);
|
|
4575
|
+
}
|
|
4576
|
+
resolve2(out);
|
|
4577
|
+
});
|
|
4578
|
+
child.stdin?.write(args.notes);
|
|
4579
|
+
child.stdin?.end();
|
|
4580
|
+
});
|
|
4581
|
+
}
|
|
4582
|
+
function extractStructured(raw) {
|
|
4583
|
+
const trimmed = raw.trim();
|
|
4584
|
+
if (!trimmed) return { ok: false, error: "claude returned empty stdout" };
|
|
4585
|
+
try {
|
|
4586
|
+
const env = JSON.parse(trimmed);
|
|
4587
|
+
if (env.is_error === true) {
|
|
4588
|
+
const code = typeof env.error_code === "string" ? env.error_code : "unknown";
|
|
4589
|
+
const result = typeof env.result === "string" ? env.result.slice(0, 400) : "";
|
|
4590
|
+
return { ok: false, error: `claude is_error (${code}): ${result || "no detail"}` };
|
|
4591
|
+
}
|
|
4592
|
+
if (env.structured_output && typeof env.structured_output === "object") {
|
|
4593
|
+
return { ok: true, value: env.structured_output };
|
|
4594
|
+
}
|
|
4595
|
+
if (typeof env.result === "string") {
|
|
4596
|
+
const m = env.result.match(/\{[\s\S]*\}/);
|
|
4597
|
+
if (m) return { ok: true, value: JSON.parse(m[0]) };
|
|
4598
|
+
}
|
|
4599
|
+
} catch {
|
|
4600
|
+
}
|
|
4601
|
+
try {
|
|
4602
|
+
const m = trimmed.match(/\{[\s\S]*\}/);
|
|
4603
|
+
if (m) return { ok: true, value: JSON.parse(m[0]) };
|
|
4604
|
+
} catch {
|
|
4605
|
+
}
|
|
4606
|
+
return { ok: false, error: "claude returned no parseable JSON" };
|
|
4607
|
+
}
|
|
4608
|
+
async function patchStatus(url, bearer, body) {
|
|
4609
|
+
const res = await request5(url, {
|
|
4610
|
+
method: "PATCH",
|
|
4611
|
+
headers: {
|
|
4612
|
+
"Content-Type": "application/json",
|
|
4613
|
+
Authorization: `Bearer ${bearer}`
|
|
4614
|
+
},
|
|
4615
|
+
body: JSON.stringify(body)
|
|
4616
|
+
});
|
|
4617
|
+
const text = await res.body.text();
|
|
4618
|
+
if (res.statusCode >= 400) {
|
|
4619
|
+
throw new Error(`PATCH ${url} \u2192 ${res.statusCode}: ${text.slice(0, 400)}`);
|
|
4620
|
+
}
|
|
4621
|
+
try {
|
|
4622
|
+
const parsed = JSON.parse(text);
|
|
4623
|
+
return parsed.data ?? {};
|
|
4624
|
+
} catch {
|
|
4625
|
+
return {};
|
|
4626
|
+
}
|
|
4627
|
+
}
|
|
4628
|
+
|
|
4000
4629
|
// src/commands/fast-track.ts
|
|
4001
4630
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
4002
4631
|
import ora3 from "ora";
|
|
@@ -4006,7 +4635,10 @@ function registerFastTrack(program2) {
|
|
|
4006
4635
|
).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(
|
|
4007
4636
|
"--reset",
|
|
4008
4637
|
"DESTRUCTIVE: discard local working-tree changes before the first ticket. Requires --confirm in non-TTY contexts."
|
|
4009
|
-
).option("--confirm", "Confirm --reset in non-TTY (silent / scheduled-task) contexts").option(
|
|
4638
|
+
).option("--confirm", "Confirm --reset in non-TTY (silent / scheduled-task) contexts").option(
|
|
4639
|
+
"--no-auto-review",
|
|
4640
|
+
"Skip the post-PR /qa + /security auto-review. Each PR is left open for a human to review and merge."
|
|
4641
|
+
).option("--schedule-id <id>", "Internal: schedule id when invoked from a scheduled task").action(async (opts) => {
|
|
4010
4642
|
await runFastTrack(opts);
|
|
4011
4643
|
});
|
|
4012
4644
|
}
|
|
@@ -4047,7 +4679,11 @@ async function runFastTrack(opts) {
|
|
|
4047
4679
|
...opts.dryRun ? { dryRun: true } : {},
|
|
4048
4680
|
...opts.scheduleId ? { scheduleId: opts.scheduleId } : {},
|
|
4049
4681
|
...firstIteration && opts.reset ? { reset: true } : {},
|
|
4050
|
-
...firstIteration && opts.confirm ? { confirm: true } : {}
|
|
4682
|
+
...firstIteration && opts.confirm ? { confirm: true } : {},
|
|
4683
|
+
// Commander sets opts.autoReview to `false` only when --no-auto-review
|
|
4684
|
+
// is passed; otherwise it's `undefined` and processOneTicketImpl
|
|
4685
|
+
// treats `undefined` as "auto-review enabled" (opts.autoReview !== false).
|
|
4686
|
+
...opts.autoReview === false ? { autoReview: false } : {}
|
|
4051
4687
|
};
|
|
4052
4688
|
const outcome = await fastTrackOneTicket({
|
|
4053
4689
|
api,
|
|
@@ -4387,10 +5023,10 @@ import { randomUUID as randomUUID4 } from "crypto";
|
|
|
4387
5023
|
import { platform as platform2 } from "os";
|
|
4388
5024
|
|
|
4389
5025
|
// src/scheduler/launchd.ts
|
|
4390
|
-
import { mkdir as
|
|
4391
|
-
import { homedir as
|
|
4392
|
-
import { join as
|
|
4393
|
-
import { execFileSync as execFileSync9, spawn as
|
|
5026
|
+
import { mkdir as mkdir10, readFile as readFile5, writeFile as writeFile11, unlink as unlink4, readdir as readdir2 } from "fs/promises";
|
|
5027
|
+
import { homedir as homedir7 } from "os";
|
|
5028
|
+
import { join as join12 } from "path";
|
|
5029
|
+
import { execFileSync as execFileSync9, spawn as spawn7 } from "child_process";
|
|
4394
5030
|
|
|
4395
5031
|
// src/scheduler/cron-translate.ts
|
|
4396
5032
|
function translateToLaunchd(cron) {
|
|
@@ -4491,14 +5127,14 @@ function expandField(field, min, max) {
|
|
|
4491
5127
|
}
|
|
4492
5128
|
|
|
4493
5129
|
// src/scheduler/launchd.ts
|
|
4494
|
-
var PLIST_DIR =
|
|
5130
|
+
var PLIST_DIR = join12(homedir7(), "Library", "LaunchAgents");
|
|
4495
5131
|
var LABEL_PREFIX = "com.inteeka.task.cli.";
|
|
4496
5132
|
var SAFE_ID_RE = /^[0-9a-zA-Z._-]+$/;
|
|
4497
5133
|
function plistPath(id) {
|
|
4498
5134
|
if (!SAFE_ID_RE.test(id) || id.includes("..")) {
|
|
4499
5135
|
throw new Error(`Refusing to compute plist path for unsafe id: ${id}`);
|
|
4500
5136
|
}
|
|
4501
|
-
return
|
|
5137
|
+
return join12(PLIST_DIR, `${LABEL_PREFIX}${id}.plist`);
|
|
4502
5138
|
}
|
|
4503
5139
|
function buildPlist(entry) {
|
|
4504
5140
|
const calendars = translateToLaunchd(entry.cron);
|
|
@@ -4534,9 +5170,9 @@ ${fields}
|
|
|
4534
5170
|
` <string>/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin</string>`,
|
|
4535
5171
|
` </dict>`,
|
|
4536
5172
|
` <key>StandardOutPath</key>`,
|
|
4537
|
-
` <string>${escapeXml(
|
|
5173
|
+
` <string>${escapeXml(join12(homedir7(), ".cache", "task", "launchd-stdout.log"))}</string>`,
|
|
4538
5174
|
` <key>StandardErrorPath</key>`,
|
|
4539
|
-
` <string>${escapeXml(
|
|
5175
|
+
` <string>${escapeXml(join12(homedir7(), ".cache", "task", "launchd-stderr.log"))}</string>`,
|
|
4540
5176
|
!entry.enabled ? ` <key>Disabled</key>
|
|
4541
5177
|
<true/>` : "",
|
|
4542
5178
|
"</dict>",
|
|
@@ -4554,9 +5190,9 @@ function bootstrapDomain() {
|
|
|
4554
5190
|
}
|
|
4555
5191
|
var launchdAdapter = {
|
|
4556
5192
|
async upsert(entry) {
|
|
4557
|
-
await
|
|
5193
|
+
await mkdir10(PLIST_DIR, { recursive: true });
|
|
4558
5194
|
const path = plistPath(entry.id);
|
|
4559
|
-
await
|
|
5195
|
+
await writeFile11(path, buildPlist(entry));
|
|
4560
5196
|
try {
|
|
4561
5197
|
execFileSync9("launchctl", ["bootout", bootstrapDomain(), path], { stdio: "ignore" });
|
|
4562
5198
|
} catch {
|
|
@@ -4585,7 +5221,7 @@ var launchdAdapter = {
|
|
|
4585
5221
|
for (const file of ours) {
|
|
4586
5222
|
const id = file.slice(LABEL_PREFIX.length, -".plist".length);
|
|
4587
5223
|
try {
|
|
4588
|
-
const xml = await readFile5(
|
|
5224
|
+
const xml = await readFile5(join12(PLIST_DIR, file), "utf8");
|
|
4589
5225
|
const cron = xml.match(/<key>StartCalendarInterval<\/key>[\s\S]*?<\/array>/)?.[0] ?? "";
|
|
4590
5226
|
const command = xml.match(/<key>ProgramArguments<\/key>\s*<array>([\s\S]*?)<\/array>/)?.[1] ?? "";
|
|
4591
5227
|
const disabled = /<key>Disabled<\/key>\s*<true\/>/.test(xml);
|
|
@@ -4608,7 +5244,7 @@ var launchdAdapter = {
|
|
|
4608
5244
|
return new Promise((resolve2) => {
|
|
4609
5245
|
const args = entry.command.match(/(?:[^\s"]+|"[^"]*")+/g) ?? [entry.command];
|
|
4610
5246
|
const cmd = args.shift() ?? entry.command;
|
|
4611
|
-
const child =
|
|
5247
|
+
const child = spawn7(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
4612
5248
|
let stdoutTail = "";
|
|
4613
5249
|
let stderrTail = "";
|
|
4614
5250
|
child.stdout?.on("data", (chunk) => {
|
|
@@ -4631,7 +5267,7 @@ var launchdAdapter = {
|
|
|
4631
5267
|
}
|
|
4632
5268
|
if (enabled) {
|
|
4633
5269
|
xml = xml.replace(/\s*<key>Disabled<\/key>\s*<true\/>/, "");
|
|
4634
|
-
await
|
|
5270
|
+
await writeFile11(path, xml);
|
|
4635
5271
|
try {
|
|
4636
5272
|
execFileSync9("launchctl", ["bootout", bootstrapDomain(), path], { stdio: "ignore" });
|
|
4637
5273
|
} catch {
|
|
@@ -4643,7 +5279,7 @@ var launchdAdapter = {
|
|
|
4643
5279
|
"</dict>\n</plist>",
|
|
4644
5280
|
" <key>Disabled</key>\n <true/>\n</dict>\n</plist>"
|
|
4645
5281
|
);
|
|
4646
|
-
await
|
|
5282
|
+
await writeFile11(path, xml);
|
|
4647
5283
|
}
|
|
4648
5284
|
try {
|
|
4649
5285
|
execFileSync9("launchctl", ["bootout", bootstrapDomain(), path], { stdio: "ignore" });
|
|
@@ -4654,7 +5290,7 @@ var launchdAdapter = {
|
|
|
4654
5290
|
};
|
|
4655
5291
|
|
|
4656
5292
|
// src/scheduler/cron.ts
|
|
4657
|
-
import { execFileSync as execFileSync10, spawn as
|
|
5293
|
+
import { execFileSync as execFileSync10, spawn as spawn8 } from "child_process";
|
|
4658
5294
|
|
|
4659
5295
|
// src/scheduler/safe-command.ts
|
|
4660
5296
|
var FORBIDDEN = /[;&|`$()<>\\]/;
|
|
@@ -4715,7 +5351,7 @@ function readCrontab() {
|
|
|
4715
5351
|
}
|
|
4716
5352
|
}
|
|
4717
5353
|
function writeCrontab(text) {
|
|
4718
|
-
const child =
|
|
5354
|
+
const child = spawn8("crontab", ["-"], { stdio: ["pipe", "inherit", "inherit"] });
|
|
4719
5355
|
child.stdin.write(text);
|
|
4720
5356
|
child.stdin.end();
|
|
4721
5357
|
}
|
|
@@ -4796,7 +5432,7 @@ var cronAdapter = {
|
|
|
4796
5432
|
return Promise.resolve({ exitCode: 1, stdoutTail: "", stderrTail: `rejected: ${reason}` });
|
|
4797
5433
|
}
|
|
4798
5434
|
return new Promise((resolve2) => {
|
|
4799
|
-
const child =
|
|
5435
|
+
const child = spawn8(parsed.bin, parsed.args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
4800
5436
|
let stdoutTail = "";
|
|
4801
5437
|
let stderrTail = "";
|
|
4802
5438
|
child.stdout?.on(
|
|
@@ -4824,7 +5460,7 @@ var cronAdapter = {
|
|
|
4824
5460
|
};
|
|
4825
5461
|
|
|
4826
5462
|
// src/scheduler/windows.ts
|
|
4827
|
-
import { execFileSync as execFileSync11, spawn as
|
|
5463
|
+
import { execFileSync as execFileSync11, spawn as spawn9 } from "child_process";
|
|
4828
5464
|
var TASK_PREFIX = "TaskCLI_";
|
|
4829
5465
|
function taskName(id) {
|
|
4830
5466
|
return `${TASK_PREFIX}${id.replace(/[^A-Za-z0-9_-]/g, "_")}`;
|
|
@@ -4937,7 +5573,7 @@ var windowsAdapter = {
|
|
|
4937
5573
|
return Promise.resolve({ exitCode: 1, stdoutTail: "", stderrTail: `rejected: ${reason}` });
|
|
4938
5574
|
}
|
|
4939
5575
|
return new Promise((resolve2) => {
|
|
4940
|
-
const child =
|
|
5576
|
+
const child = spawn9(parsed.bin, parsed.args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
4941
5577
|
let stdoutTail = "";
|
|
4942
5578
|
let stderrTail = "";
|
|
4943
5579
|
child.stdout?.on(
|
|
@@ -4996,10 +5632,10 @@ var unsupportedAdapter = {
|
|
|
4996
5632
|
};
|
|
4997
5633
|
|
|
4998
5634
|
// src/scheduler/registry.ts
|
|
4999
|
-
import { mkdir as
|
|
5000
|
-
import { homedir as
|
|
5001
|
-
import { dirname as dirname5, join as
|
|
5002
|
-
var REGISTRY_PATH =
|
|
5635
|
+
import { mkdir as mkdir11, readFile as readFile6, writeFile as writeFile12 } from "fs/promises";
|
|
5636
|
+
import { homedir as homedir8 } from "os";
|
|
5637
|
+
import { dirname as dirname5, join as join13 } from "path";
|
|
5638
|
+
var REGISTRY_PATH = join13(homedir8(), ".config", "task", "schedules.json");
|
|
5003
5639
|
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
5004
5640
|
function looksLikeRegistryRow(value) {
|
|
5005
5641
|
if (!value || typeof value !== "object") return false;
|
|
@@ -5019,8 +5655,8 @@ async function readRegistry() {
|
|
|
5019
5655
|
}
|
|
5020
5656
|
}
|
|
5021
5657
|
async function writeRegistry(rows) {
|
|
5022
|
-
await
|
|
5023
|
-
await
|
|
5658
|
+
await mkdir11(dirname5(REGISTRY_PATH), { recursive: true });
|
|
5659
|
+
await writeFile12(REGISTRY_PATH, JSON.stringify(rows, null, 2));
|
|
5024
5660
|
}
|
|
5025
5661
|
async function upsertRegistry(row) {
|
|
5026
5662
|
if (!UUID_RE.test(row.id)) {
|
|
@@ -5260,8 +5896,8 @@ function stripAnsi(s) {
|
|
|
5260
5896
|
|
|
5261
5897
|
// src/commands/runs.ts
|
|
5262
5898
|
import { readFile as readFile7 } from "fs/promises";
|
|
5263
|
-
import { homedir as
|
|
5264
|
-
import { join as
|
|
5899
|
+
import { homedir as homedir9 } from "os";
|
|
5900
|
+
import { join as join14 } from "path";
|
|
5265
5901
|
function registerRuns(program2) {
|
|
5266
5902
|
const cmd = program2.command("runs").description("Inspect agentic CLI run history");
|
|
5267
5903
|
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) => {
|
|
@@ -5290,7 +5926,7 @@ function registerRuns(program2) {
|
|
|
5290
5926
|
process.stdout.write(JSON.stringify(row, null, 2) + "\n");
|
|
5291
5927
|
});
|
|
5292
5928
|
cmd.command("logs <id>").description("Show captured agent output for a run, if available").action(async (id) => {
|
|
5293
|
-
const localPath =
|
|
5929
|
+
const localPath = join14(homedir9(), ".cache", "task", "runs", `${id}.log`);
|
|
5294
5930
|
try {
|
|
5295
5931
|
const text = await readFile7(localPath, "utf8");
|
|
5296
5932
|
process.stdout.write(text);
|
|
@@ -5363,9 +5999,9 @@ function registerConfig(program2) {
|
|
|
5363
5999
|
|
|
5364
6000
|
// src/commands/doctor.ts
|
|
5365
6001
|
import { execFileSync as execFileSync12 } from "child_process";
|
|
5366
|
-
import { readFile as readFile8, writeFile as
|
|
5367
|
-
import { join as
|
|
5368
|
-
import { request as
|
|
6002
|
+
import { readFile as readFile8, writeFile as writeFile13 } from "fs/promises";
|
|
6003
|
+
import { join as join15 } from "path";
|
|
6004
|
+
import { request as request6 } from "undici";
|
|
5369
6005
|
var ALLOWED_TEST_EXECUTABLES = /* @__PURE__ */ new Set(["pnpm", "npm", "yarn", "bun", "node", "npx"]);
|
|
5370
6006
|
var DEFAULT_TEST_COMMAND = "pnpm typecheck";
|
|
5371
6007
|
function registerDoctor(program2) {
|
|
@@ -5406,7 +6042,7 @@ function registerDoctor(program2) {
|
|
|
5406
6042
|
});
|
|
5407
6043
|
const apiUrl = creds?.api_url ?? cfg.api_url;
|
|
5408
6044
|
try {
|
|
5409
|
-
const res = await
|
|
6045
|
+
const res = await request6(apiUrl, {
|
|
5410
6046
|
method: "GET",
|
|
5411
6047
|
headersTimeout: 5e3,
|
|
5412
6048
|
bodyTimeout: 5e3
|
|
@@ -5578,7 +6214,7 @@ async function checkPrePushTest(root, configuredCommand, fix) {
|
|
|
5578
6214
|
detail: `${command} (non-script executable, not statically verifiable)`
|
|
5579
6215
|
};
|
|
5580
6216
|
}
|
|
5581
|
-
const pkgPath =
|
|
6217
|
+
const pkgPath = join15(root, "package.json");
|
|
5582
6218
|
let pkgRaw;
|
|
5583
6219
|
try {
|
|
5584
6220
|
pkgRaw = await readFile8(pkgPath, "utf8");
|
|
@@ -5614,7 +6250,7 @@ async function checkPrePushTest(root, configuredCommand, fix) {
|
|
|
5614
6250
|
pkg.scripts = { ...scripts, typecheck: "tsc --noEmit" };
|
|
5615
6251
|
const indent = detectIndent(pkgRaw);
|
|
5616
6252
|
const trailingNewline = pkgRaw.endsWith("\n") ? "\n" : "";
|
|
5617
|
-
await
|
|
6253
|
+
await writeFile13(pkgPath, JSON.stringify(pkg, null, indent) + trailingNewline);
|
|
5618
6254
|
return {
|
|
5619
6255
|
name: "pre-push test",
|
|
5620
6256
|
ok: true,
|
|
@@ -5663,7 +6299,7 @@ function checkBinary(name, command) {
|
|
|
5663
6299
|
}
|
|
5664
6300
|
|
|
5665
6301
|
// src/commands/version.ts
|
|
5666
|
-
var CLI_VERSION = true ? "0.2.
|
|
6302
|
+
var CLI_VERSION = true ? "0.2.27" : "0.0.0-dev";
|
|
5667
6303
|
function registerVersion(program2) {
|
|
5668
6304
|
program2.command("version").description("Print the CLI version").action(() => {
|
|
5669
6305
|
process.stdout.write(CLI_VERSION + "\n");
|
|
@@ -5690,6 +6326,7 @@ registerMultiWork(program);
|
|
|
5690
6326
|
registerResume(program);
|
|
5691
6327
|
registerReset(program);
|
|
5692
6328
|
registerScan(program);
|
|
6329
|
+
registerSlackImport(program);
|
|
5693
6330
|
registerFastTrack(program);
|
|
5694
6331
|
registerPrTest(program);
|
|
5695
6332
|
registerScheduledTask(program);
|