capyai 0.4.0 → 0.5.0
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/AGENTS.md +9 -121
- package/README.md +3 -1
- package/bin/capy.ts +2 -0
- package/package.json +1 -1
- package/skills/capy/SKILL.md +235 -64
- package/src/api.ts +45 -18
- package/src/commands/_shared.ts +4 -0
- package/src/commands/diff-pr.ts +6 -1
- package/src/commands/monitoring.ts +16 -10
- package/src/commands/quality.ts +5 -6
- package/src/commands/setup.ts +46 -0
- package/src/commands/tasks.ts +4 -2
- package/src/commands/threads.ts +7 -3
- package/src/commands/triage.ts +271 -0
- package/src/config.ts +1 -3
- package/src/github.ts +1 -1
- package/src/mcp.ts +145 -22
- package/src/types.ts +0 -2
- package/src/watch.ts +4 -4
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { defineCommand } from "citty";
|
|
2
|
-
import { jsonArg } from "./_shared.js";
|
|
2
|
+
import { jsonArg, isThreadId } from "./_shared.js";
|
|
3
3
|
|
|
4
4
|
export const watch = defineCommand({
|
|
5
5
|
meta: { name: "watch", description: "Poll + notify on completion" },
|
|
@@ -14,7 +14,7 @@ export const watch = defineCommand({
|
|
|
14
14
|
const fmt = await import("../output.js");
|
|
15
15
|
|
|
16
16
|
const interval = Math.max(1, Math.min(parseInt(args.interval) || config.load().watchInterval, 30));
|
|
17
|
-
const type = (args.id
|
|
17
|
+
const type = isThreadId(args.id) ? "thread" : "task";
|
|
18
18
|
const added = w.add(args.id, type, interval);
|
|
19
19
|
|
|
20
20
|
if (args.json) { fmt.out({ id: args.id, type, interval, added }); return; }
|
|
@@ -70,7 +70,7 @@ export const wait = defineCommand({
|
|
|
70
70
|
|
|
71
71
|
const timeoutMs = Math.max(10, parseInt(args.timeout) || 300) * 1000;
|
|
72
72
|
const intervalMs = Math.max(5, Math.min(parseInt(args.interval) || 10, 60)) * 1000;
|
|
73
|
-
const isThread =
|
|
73
|
+
const isThread = isThreadId(args.id);
|
|
74
74
|
const terminalTask = new Set(["needs_review", "archived", "completed", "failed"]);
|
|
75
75
|
const terminalThread = new Set(["idle", "archived", "completed"]);
|
|
76
76
|
const terminal = isThread ? terminalThread : terminalTask;
|
|
@@ -78,19 +78,25 @@ export const wait = defineCommand({
|
|
|
78
78
|
const start = Date.now();
|
|
79
79
|
if (!args.json) process.stderr.write(`Waiting for ${args.id} (${isThread ? "thread" : "task"})...`);
|
|
80
80
|
|
|
81
|
+
let lastStatus = "unknown";
|
|
81
82
|
while (Date.now() - start < timeoutMs) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
83
|
+
try {
|
|
84
|
+
const data = isThread ? await api.getThread(args.id) : await api.getTask(args.id);
|
|
85
|
+
lastStatus = data.status;
|
|
86
|
+
if (terminal.has(data.status)) {
|
|
87
|
+
if (args.json) { fmt.out(data); return; }
|
|
88
|
+
console.log(`\n${isThread ? "Thread" : "Task"} ${data.status}.`);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
} catch {
|
|
92
|
+
// transient error during poll, keep trying
|
|
87
93
|
}
|
|
88
94
|
if (!args.json) process.stderr.write(".");
|
|
89
95
|
await new Promise(r => setTimeout(r, intervalMs));
|
|
90
96
|
}
|
|
91
97
|
|
|
92
|
-
if (args.json) { fmt.out({ error: { code: "timeout", message: `Timed out after ${args.timeout}s
|
|
93
|
-
console.error(`\ncapy: timed out after ${args.timeout}s`);
|
|
98
|
+
if (args.json) { fmt.out({ error: { code: "timeout", message: `Timed out after ${args.timeout}s`, lastStatus } }); process.exit(1); }
|
|
99
|
+
console.error(`\ncapy: timed out after ${args.timeout}s (last status: ${lastStatus})`);
|
|
94
100
|
process.exit(1);
|
|
95
101
|
},
|
|
96
102
|
});
|
package/src/commands/quality.ts
CHANGED
|
@@ -209,13 +209,12 @@ export const approve = defineCommand({
|
|
|
209
209
|
const approveCmd = cfg.approveCommand;
|
|
210
210
|
if (approveCmd) {
|
|
211
211
|
try {
|
|
212
|
-
const {
|
|
213
|
-
const
|
|
212
|
+
const { execSync } = await import("node:child_process");
|
|
213
|
+
const expanded = approveCmd
|
|
214
214
|
.replace("{task}", task.identifier || task.id)
|
|
215
|
-
.replace("{title}", task.title || "")
|
|
216
|
-
.replace("{pr}", String(task.pullRequest?.number || ""))
|
|
217
|
-
|
|
218
|
-
execFileSync(parts[0], parts.slice(1), { encoding: "utf8", timeout: 15000, stdio: "pipe" });
|
|
215
|
+
.replace("{title}", (task.title || "").replace(/'/g, "'\\''"))
|
|
216
|
+
.replace("{pr}", String(task.pullRequest?.number || ""));
|
|
217
|
+
execSync(expanded, { timeout: 15000, stdio: "pipe" });
|
|
219
218
|
log.info("Post-approve hook ran.");
|
|
220
219
|
} catch {}
|
|
221
220
|
}
|
package/src/commands/setup.ts
CHANGED
|
@@ -133,6 +133,50 @@ export const config = defineCommand({
|
|
|
133
133
|
},
|
|
134
134
|
});
|
|
135
135
|
|
|
136
|
+
const projectsList = defineCommand({
|
|
137
|
+
meta: { name: "list", description: "List projects" },
|
|
138
|
+
args: { ...jsonArg },
|
|
139
|
+
async run({ args }) {
|
|
140
|
+
const api = await import("../api.js");
|
|
141
|
+
const fmt = await import("../output.js");
|
|
142
|
+
|
|
143
|
+
const projects = await api.listProjectsAuth();
|
|
144
|
+
if (args.json) { fmt.out(projects); return; }
|
|
145
|
+
if (!projects.length) { console.log("No projects."); return; }
|
|
146
|
+
fmt.table(["ID", "NAME", "CODE", "REPOS"], projects.map(p => [
|
|
147
|
+
p.id.slice(0, 16), p.name, p.taskCode, String(p.repos.length),
|
|
148
|
+
]));
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const projectsGet = defineCommand({
|
|
153
|
+
meta: { name: "get", description: "Project details" },
|
|
154
|
+
args: {
|
|
155
|
+
id: { type: "positional", required: false, description: "Project ID (defaults to current)" },
|
|
156
|
+
...jsonArg,
|
|
157
|
+
},
|
|
158
|
+
async run({ args }) {
|
|
159
|
+
const api = await import("../api.js");
|
|
160
|
+
const fmt = await import("../output.js");
|
|
161
|
+
const { log } = await import("@clack/prompts");
|
|
162
|
+
|
|
163
|
+
const data = await api.getProject(args.id);
|
|
164
|
+
if (args.json) { fmt.out(data); return; }
|
|
165
|
+
log.info(`Project: ${data.name}\nID: ${data.id}\nCode: ${data.taskCode}`);
|
|
166
|
+
if (data.repos.length) {
|
|
167
|
+
console.log(`Repos:`);
|
|
168
|
+
data.repos.forEach(r => console.log(` ${r.repoFullName} (${r.branch})`));
|
|
169
|
+
}
|
|
170
|
+
if (data.createdAt) console.log(`Created: ${data.createdAt}`);
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
export const projects = defineCommand({
|
|
175
|
+
meta: { name: "projects", description: "List and manage projects" },
|
|
176
|
+
default: "list",
|
|
177
|
+
subCommands: { list: projectsList, get: projectsGet },
|
|
178
|
+
});
|
|
179
|
+
|
|
136
180
|
export const models = defineCommand({
|
|
137
181
|
meta: { name: "models", description: "List available models" },
|
|
138
182
|
args: { ...jsonArg },
|
|
@@ -162,6 +206,7 @@ export const tools = defineCommand({
|
|
|
162
206
|
captain: { args: "<prompt>", desc: "Start Captain thread" },
|
|
163
207
|
build: { args: "<prompt>", desc: "Start Build agent (isolated)" },
|
|
164
208
|
threads: { args: "[list|get|msg|stop]", desc: "Manage threads" },
|
|
209
|
+
triage: { args: "[id,...]", desc: "Actionable triage with diffs + recs" },
|
|
165
210
|
status: { args: "", desc: "Dashboard" },
|
|
166
211
|
list: { args: "[status]", desc: "List tasks" },
|
|
167
212
|
get: { args: "<id>", desc: "Task details" },
|
|
@@ -178,6 +223,7 @@ export const tools = defineCommand({
|
|
|
178
223
|
watch: { args: "<id>", desc: "Poll + notify on completion" },
|
|
179
224
|
unwatch: { args: "<id>", desc: "Stop watching" },
|
|
180
225
|
watches: { args: "", desc: "List watches" },
|
|
226
|
+
projects: { args: "[list|get]", desc: "List/get projects" },
|
|
181
227
|
pool: { args: "[status|set|test|...]", desc: "Manage warm pool VMs" },
|
|
182
228
|
models: { args: "", desc: "List models" },
|
|
183
229
|
tools: { args: "", desc: "This list" },
|
package/src/commands/tasks.ts
CHANGED
|
@@ -5,14 +5,16 @@ export const list = defineCommand({
|
|
|
5
5
|
meta: { name: "list", description: "List tasks", alias: "ls" },
|
|
6
6
|
args: {
|
|
7
7
|
status: { type: "positional", required: false, description: "Filter by status" },
|
|
8
|
+
limit: { type: "string", description: "Max results (default 30)" },
|
|
9
|
+
cursor: { type: "string", description: "Pagination cursor from previous response" },
|
|
8
10
|
...jsonArg,
|
|
9
11
|
},
|
|
10
12
|
async run({ args }) {
|
|
11
13
|
const api = await import("../api.js");
|
|
12
14
|
const fmt = await import("../output.js");
|
|
13
15
|
|
|
14
|
-
const data = await api.listTasks({ status: args.status });
|
|
15
|
-
if (args.json) { fmt.out(data.items || []); return; }
|
|
16
|
+
const data = await api.listTasks({ status: args.status, limit: args.limit ? parseInt(args.limit) : undefined, cursor: args.cursor });
|
|
17
|
+
if (args.json) { fmt.out({ items: data.items || [], nextCursor: data.nextCursor, hasMore: data.hasMore }); return; }
|
|
16
18
|
if (!data.items?.length) { console.log("No tasks."); return; }
|
|
17
19
|
fmt.table(["ID", "STATUS", "TITLE", "PR"], data.items.map(t => [
|
|
18
20
|
t.identifier,
|
package/src/commands/threads.ts
CHANGED
|
@@ -3,13 +3,17 @@ import { jsonArg } from "./_shared.js";
|
|
|
3
3
|
|
|
4
4
|
const list = defineCommand({
|
|
5
5
|
meta: { name: "list", description: "List threads" },
|
|
6
|
-
args: {
|
|
6
|
+
args: {
|
|
7
|
+
limit: { type: "string", description: "Max results (default 10)" },
|
|
8
|
+
cursor: { type: "string", description: "Pagination cursor from previous response" },
|
|
9
|
+
...jsonArg,
|
|
10
|
+
},
|
|
7
11
|
async run({ args }) {
|
|
8
12
|
const api = await import("../api.js");
|
|
9
13
|
const fmt = await import("../output.js");
|
|
10
14
|
|
|
11
|
-
const data = await api.listThreads();
|
|
12
|
-
if (args.json) { fmt.out(data.items || []); return; }
|
|
15
|
+
const data = await api.listThreads({ limit: args.limit ? parseInt(args.limit) : undefined, cursor: args.cursor });
|
|
16
|
+
if (args.json) { fmt.out({ items: data.items || [], nextCursor: data.nextCursor, hasMore: data.hasMore }); return; }
|
|
13
17
|
if (!data.items?.length) { console.log("No threads."); return; }
|
|
14
18
|
fmt.table(["ID", "STATUS", "TITLE"], data.items.map(t => [
|
|
15
19
|
t.id.slice(0, 16), t.status, (t.title || "(untitled)").slice(0, 40),
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { defineCommand } from "citty";
|
|
2
|
+
import { jsonArg } from "./_shared.js";
|
|
3
|
+
|
|
4
|
+
interface TriageTask {
|
|
5
|
+
identifier: string;
|
|
6
|
+
title: string;
|
|
7
|
+
status: string;
|
|
8
|
+
labels: string[];
|
|
9
|
+
category: "merged" | "ready" | "needs_pr" | "stuck" | "backlog" | "in_progress";
|
|
10
|
+
pr?: { number: number; state: string; url?: string };
|
|
11
|
+
diff?: { files: number; additions: number; deletions: number };
|
|
12
|
+
jam?: { model: string; status: string; credits: { llm: number; vm: number } };
|
|
13
|
+
createdAt?: string;
|
|
14
|
+
updatedAt?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface TriageResult {
|
|
18
|
+
summary: { total: number; merged: number; ready: number; needs_pr: number; stuck: number; backlog: number; in_progress: number };
|
|
19
|
+
tasks: TriageTask[];
|
|
20
|
+
recommendations: string[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const triage = defineCommand({
|
|
24
|
+
meta: { name: "triage", description: "Actionable status for all tasks with diffs, PR state, and recommendations" },
|
|
25
|
+
args: {
|
|
26
|
+
ids: { type: "positional", required: false, description: "Specific task IDs (comma-separated or space-separated)" },
|
|
27
|
+
brief: { type: "boolean", description: "Fast mode: skip diff fetching, just categorize by status + PR state", default: false },
|
|
28
|
+
...jsonArg,
|
|
29
|
+
},
|
|
30
|
+
async run({ args }) {
|
|
31
|
+
const api = await import("../api.js");
|
|
32
|
+
const config = await import("../config.js");
|
|
33
|
+
const github = await import("../github.js");
|
|
34
|
+
const fmt = await import("../output.js");
|
|
35
|
+
const { log, spinner } = await import("@clack/prompts");
|
|
36
|
+
|
|
37
|
+
const cfg = config.load();
|
|
38
|
+
|
|
39
|
+
// Get base task list
|
|
40
|
+
let tasks: any[];
|
|
41
|
+
if (args.ids) {
|
|
42
|
+
const ids = args.ids.split(/[,\s]+/).filter(Boolean);
|
|
43
|
+
tasks = await Promise.all(ids.map(id => api.getTask(id)));
|
|
44
|
+
} else {
|
|
45
|
+
const data = await api.listTasks({ limit: 100 });
|
|
46
|
+
tasks = data.items || [];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!tasks.length) {
|
|
50
|
+
if (args.json) { fmt.out({ summary: { total: 0 }, tasks: [], recommendations: [] }); return; }
|
|
51
|
+
console.log("No tasks.");
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Fetch detail + diff in parallel for all tasks
|
|
56
|
+
const brief = !!args.brief;
|
|
57
|
+
let results: Awaited<ReturnType<typeof enrichTasks>>;
|
|
58
|
+
if (!args.json) {
|
|
59
|
+
const s = spinner();
|
|
60
|
+
s.start(`Loading ${tasks.length} tasks${brief ? "" : " (details + diffs)"}...`);
|
|
61
|
+
results = await enrichTasks(api, tasks, cfg, brief);
|
|
62
|
+
s.stop(`${tasks.length} tasks loaded`);
|
|
63
|
+
} else {
|
|
64
|
+
results = await enrichTasks(api, tasks, cfg, brief);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Cross-ref PR state with GitHub
|
|
68
|
+
for (const r of results) {
|
|
69
|
+
if (r.pr && r.pr.state === "closed") {
|
|
70
|
+
const repo = r._raw?.pullRequest?.repoFullName || cfg.repos[0]?.repoFullName;
|
|
71
|
+
if (repo) {
|
|
72
|
+
const ghPR = github.getPR(repo, r.pr.number);
|
|
73
|
+
if (ghPR) r.pr.state = ghPR.state.toLowerCase();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Categorize
|
|
79
|
+
const triaged: TriageTask[] = results.map(r => {
|
|
80
|
+
let category: TriageTask["category"];
|
|
81
|
+
if (r.status === "backlog") {
|
|
82
|
+
category = "backlog";
|
|
83
|
+
} else if (r.status === "in_progress") {
|
|
84
|
+
category = "in_progress";
|
|
85
|
+
} else if (r.pr?.state === "merged") {
|
|
86
|
+
category = "merged";
|
|
87
|
+
} else if (r.pr && r.pr.state === "open") {
|
|
88
|
+
category = "ready";
|
|
89
|
+
} else if (r.diff && r.diff.files > 0 && !r.pr) {
|
|
90
|
+
category = "needs_pr";
|
|
91
|
+
} else if (r.status === "needs_review" && !r.pr && (!r.diff || r.diff.files === 0)) {
|
|
92
|
+
category = brief ? "needs_pr" : "stuck";
|
|
93
|
+
} else if (r.status === "needs_review" && r.pr) {
|
|
94
|
+
category = "ready";
|
|
95
|
+
} else if (r.status === "needs_review" && !r.pr) {
|
|
96
|
+
category = "needs_pr";
|
|
97
|
+
} else {
|
|
98
|
+
category = "stuck";
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
identifier: r.identifier,
|
|
103
|
+
title: r.title,
|
|
104
|
+
status: r.status,
|
|
105
|
+
labels: r.labels || [],
|
|
106
|
+
category,
|
|
107
|
+
...(r.pr ? { pr: { number: r.pr.number, state: r.pr.state, url: r.pr.url } } : {}),
|
|
108
|
+
...(r.diff ? { diff: r.diff } : {}),
|
|
109
|
+
...(r.jam ? { jam: r.jam } : {}),
|
|
110
|
+
createdAt: r.createdAt,
|
|
111
|
+
updatedAt: r.updatedAt,
|
|
112
|
+
};
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// Sort: in_progress first, then needs_pr, ready, stuck, backlog, merged
|
|
116
|
+
const order: Record<string, number> = { in_progress: 0, needs_pr: 1, ready: 2, stuck: 3, backlog: 4, merged: 5 };
|
|
117
|
+
triaged.sort((a, b) => (order[a.category] ?? 9) - (order[b.category] ?? 9));
|
|
118
|
+
|
|
119
|
+
// Build summary
|
|
120
|
+
const summary = {
|
|
121
|
+
total: triaged.length,
|
|
122
|
+
merged: triaged.filter(t => t.category === "merged").length,
|
|
123
|
+
ready: triaged.filter(t => t.category === "ready").length,
|
|
124
|
+
needs_pr: triaged.filter(t => t.category === "needs_pr").length,
|
|
125
|
+
stuck: triaged.filter(t => t.category === "stuck").length,
|
|
126
|
+
backlog: triaged.filter(t => t.category === "backlog").length,
|
|
127
|
+
in_progress: triaged.filter(t => t.category === "in_progress").length,
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// Build recommendations
|
|
131
|
+
const recs: string[] = [];
|
|
132
|
+
const needsPr = triaged.filter(t => t.category === "needs_pr");
|
|
133
|
+
const stuck = triaged.filter(t => t.category === "stuck");
|
|
134
|
+
const ready = triaged.filter(t => t.category === "ready");
|
|
135
|
+
const inProgress = triaged.filter(t => t.category === "in_progress");
|
|
136
|
+
|
|
137
|
+
if (needsPr.length) {
|
|
138
|
+
recs.push(`Create PRs: ${needsPr.map(t => t.identifier).join(", ")} (have diffs, no PR)`);
|
|
139
|
+
}
|
|
140
|
+
if (ready.length) {
|
|
141
|
+
recs.push(`Review + approve: ${ready.map(t => t.identifier).join(", ")}`);
|
|
142
|
+
}
|
|
143
|
+
if (stuck.length) {
|
|
144
|
+
// Detect duplicates by similar titles
|
|
145
|
+
const stuckTitles = stuck.map(t => t.title.replace(/^(Implement |PLW-\d+ (BLOCKER|MEDIUM|LOW): )/i, "").slice(0, 40));
|
|
146
|
+
const dupes = stuck.filter((t, i) => {
|
|
147
|
+
const norm = stuckTitles[i];
|
|
148
|
+
return triaged.some(other =>
|
|
149
|
+
other.identifier !== t.identifier &&
|
|
150
|
+
(other.category === "needs_pr" || other.category === "ready" || other.category === "merged") &&
|
|
151
|
+
other.title.replace(/^(Implement |PLW-\d+ (BLOCKER|MEDIUM|LOW): )/i, "").slice(0, 40) === norm
|
|
152
|
+
);
|
|
153
|
+
});
|
|
154
|
+
if (dupes.length) {
|
|
155
|
+
recs.push(`Stop duplicates: ${dupes.map(t => t.identifier).join(", ")} (no output, duplicates of working tasks)`);
|
|
156
|
+
}
|
|
157
|
+
const realStuck = stuck.filter(t => !dupes.includes(t));
|
|
158
|
+
if (realStuck.length) {
|
|
159
|
+
recs.push(`Retry or stop: ${realStuck.map(t => t.identifier).join(", ")} (no diff produced)`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const result: TriageResult = { summary, tasks: triaged, recommendations: recs };
|
|
164
|
+
|
|
165
|
+
if (args.json) { fmt.out(result); return; }
|
|
166
|
+
|
|
167
|
+
// Human output
|
|
168
|
+
const groups: Record<string, TriageTask[]> = {};
|
|
169
|
+
triaged.forEach(t => { (groups[t.category] = groups[t.category] || []).push(t); });
|
|
170
|
+
|
|
171
|
+
const sectionNames: Record<string, string> = {
|
|
172
|
+
in_progress: "IN PROGRESS",
|
|
173
|
+
needs_pr: "HAS CODE, NEEDS PR",
|
|
174
|
+
ready: "READY TO REVIEW",
|
|
175
|
+
stuck: "STUCK (no output)",
|
|
176
|
+
backlog: "BACKLOG",
|
|
177
|
+
merged: "MERGED",
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
for (const cat of ["in_progress", "needs_pr", "ready", "stuck", "backlog", "merged"]) {
|
|
181
|
+
const items = groups[cat];
|
|
182
|
+
if (!items?.length) continue;
|
|
183
|
+
|
|
184
|
+
fmt.section(`${sectionNames[cat]} (${items.length})`);
|
|
185
|
+
items.forEach(t => {
|
|
186
|
+
let line = ` ${fmt.pad(t.identifier, 8)}`;
|
|
187
|
+
|
|
188
|
+
if (t.pr) {
|
|
189
|
+
line += ` PR#${fmt.pad(String(t.pr.number), 4)} [${fmt.pad(t.pr.state, 6)}]`;
|
|
190
|
+
} else {
|
|
191
|
+
line += ` `;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (t.diff && t.diff.files > 0) {
|
|
195
|
+
line += ` +${fmt.pad(String(t.diff.additions), 5)} -${fmt.pad(String(t.diff.deletions), 5)} ${fmt.pad(t.diff.files + " files", 8)}`;
|
|
196
|
+
} else {
|
|
197
|
+
line += ` `;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (t.jam) {
|
|
201
|
+
line += ` ${fmt.pad(t.jam.model, 12)}`;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
line += ` ${(t.title || "").slice(0, 45)}`;
|
|
205
|
+
console.log(line);
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
console.log();
|
|
210
|
+
log.info(`Summary: ${summary.total} tasks — ${summary.in_progress} active, ${summary.needs_pr} need PR, ${summary.ready} to review, ${summary.stuck} stuck, ${summary.merged} merged`);
|
|
211
|
+
|
|
212
|
+
if (recs.length) {
|
|
213
|
+
console.log();
|
|
214
|
+
log.step("Recommendations");
|
|
215
|
+
recs.forEach((r, i) => console.log(` ${i + 1}. ${r}`));
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
async function enrichTasks(api: typeof import("../api.js"), tasks: any[], cfg: any, brief = false) {
|
|
221
|
+
const enriched = await Promise.all(tasks.map(async (task) => {
|
|
222
|
+
const id = task.identifier || task.id;
|
|
223
|
+
let detail: any = task;
|
|
224
|
+
let diff: any = null;
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
if (!task.jams) {
|
|
228
|
+
detail = await api.getTask(id);
|
|
229
|
+
}
|
|
230
|
+
} catch {}
|
|
231
|
+
|
|
232
|
+
if (!brief) {
|
|
233
|
+
try {
|
|
234
|
+
diff = await api.getDiff(id);
|
|
235
|
+
} catch {}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const lastJam = (detail.jams || []).at(-1);
|
|
239
|
+
const credits = lastJam?.credits;
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
identifier: detail.identifier || id,
|
|
243
|
+
title: detail.title || task.title || "",
|
|
244
|
+
status: detail.status || task.status,
|
|
245
|
+
labels: detail.labels || task.labels || [],
|
|
246
|
+
createdAt: detail.createdAt || task.createdAt,
|
|
247
|
+
updatedAt: detail.updatedAt || task.updatedAt,
|
|
248
|
+
pr: detail.pullRequest?.number ? {
|
|
249
|
+
number: detail.pullRequest.number,
|
|
250
|
+
state: detail.pullRequest.state || "?",
|
|
251
|
+
url: detail.pullRequest.url,
|
|
252
|
+
} : null,
|
|
253
|
+
diff: diff?.stats ? {
|
|
254
|
+
files: diff.stats.files || 0,
|
|
255
|
+
additions: diff.stats.additions || 0,
|
|
256
|
+
deletions: diff.stats.deletions || 0,
|
|
257
|
+
} : null,
|
|
258
|
+
jam: lastJam ? {
|
|
259
|
+
model: lastJam.model || "?",
|
|
260
|
+
status: lastJam.status || "?",
|
|
261
|
+
credits: {
|
|
262
|
+
llm: typeof credits === "object" ? (credits?.llm ?? 0) : (credits || 0),
|
|
263
|
+
vm: typeof credits === "object" ? (credits?.vm ?? 0) : 0,
|
|
264
|
+
},
|
|
265
|
+
} : null,
|
|
266
|
+
_raw: detail,
|
|
267
|
+
};
|
|
268
|
+
}));
|
|
269
|
+
|
|
270
|
+
return enriched;
|
|
271
|
+
}
|
package/src/config.ts
CHANGED
|
@@ -13,10 +13,8 @@ const DEFAULTS: CapyConfig = {
|
|
|
13
13
|
repos: [],
|
|
14
14
|
defaultModel: "gpt-5.4",
|
|
15
15
|
quality: {
|
|
16
|
-
minReviewScore: 4,
|
|
17
16
|
requireCI: true,
|
|
18
17
|
requireTests: true,
|
|
19
|
-
requireLinearLink: true,
|
|
20
18
|
reviewProvider: "greptile",
|
|
21
19
|
},
|
|
22
20
|
watchInterval: 3,
|
|
@@ -56,7 +54,7 @@ export function load(): CapyConfig {
|
|
|
56
54
|
|
|
57
55
|
export function save(cfg: CapyConfig): void {
|
|
58
56
|
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
59
|
-
fs.writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 2) + "\n");
|
|
57
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 2) + "\n", { mode: 0o600 });
|
|
60
58
|
}
|
|
61
59
|
|
|
62
60
|
export function get(key: string): unknown {
|
package/src/github.ts
CHANGED
|
@@ -42,7 +42,7 @@ export function getCIStatus(repo: string, number: number, prData?: PRData | null
|
|
|
42
42
|
const checks: StatusCheck[] = pr.statusCheckRollup || [];
|
|
43
43
|
const total = checks.length;
|
|
44
44
|
const passing = checks.filter(c =>
|
|
45
|
-
c.conclusion === "SUCCESS" || c.conclusion === "NEUTRAL"
|
|
45
|
+
c.conclusion === "SUCCESS" || c.conclusion === "NEUTRAL"
|
|
46
46
|
).length;
|
|
47
47
|
const failing = checks.filter(c =>
|
|
48
48
|
c.conclusion === "FAILURE" || c.conclusion === "ERROR" || c.conclusion === "TIMED_OUT"
|