@mandipadk7/kavi 0.1.2 → 0.1.4
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/README.md +11 -65
- package/dist/config.js +8 -2
- package/dist/daemon.js +357 -0
- package/dist/doctor.js +33 -0
- package/dist/git.js +101 -2
- package/dist/main.js +171 -23
- package/dist/reviews.js +188 -0
- package/dist/router.js +69 -0
- package/dist/rpc.js +45 -0
- package/dist/session.js +27 -0
- package/dist/task-artifacts.js +28 -1
- package/dist/tui.js +381 -8
- package/package.json +2 -1
package/dist/git.js
CHANGED
|
@@ -3,7 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import { ensureDir } from "./fs.js";
|
|
4
4
|
import { buildSessionId } from "./paths.js";
|
|
5
5
|
import { runCommand } from "./process.js";
|
|
6
|
-
export async function
|
|
6
|
+
export async function findRepoRoot(cwd) {
|
|
7
7
|
const result = await runCommand("git", [
|
|
8
8
|
"rev-parse",
|
|
9
9
|
"--show-toplevel"
|
|
@@ -11,10 +11,17 @@ export async function detectRepoRoot(cwd) {
|
|
|
11
11
|
cwd
|
|
12
12
|
});
|
|
13
13
|
if (result.code !== 0) {
|
|
14
|
-
|
|
14
|
+
return null;
|
|
15
15
|
}
|
|
16
16
|
return result.stdout.trim();
|
|
17
17
|
}
|
|
18
|
+
export async function detectRepoRoot(cwd) {
|
|
19
|
+
const repoRoot = await findRepoRoot(cwd);
|
|
20
|
+
if (!repoRoot) {
|
|
21
|
+
throw new Error("Not inside a git repository.");
|
|
22
|
+
}
|
|
23
|
+
return repoRoot;
|
|
24
|
+
}
|
|
18
25
|
export async function getHeadCommit(repoRoot) {
|
|
19
26
|
const result = await runCommand("git", [
|
|
20
27
|
"rev-parse",
|
|
@@ -27,6 +34,98 @@ export async function getHeadCommit(repoRoot) {
|
|
|
27
34
|
}
|
|
28
35
|
return result.stdout.trim();
|
|
29
36
|
}
|
|
37
|
+
async function initializeRepository(repoRoot) {
|
|
38
|
+
const preferred = await runCommand("git", [
|
|
39
|
+
"init",
|
|
40
|
+
"--initial-branch=main"
|
|
41
|
+
], {
|
|
42
|
+
cwd: repoRoot
|
|
43
|
+
});
|
|
44
|
+
if (preferred.code === 0) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const fallback = await runCommand("git", [
|
|
48
|
+
"init"
|
|
49
|
+
], {
|
|
50
|
+
cwd: repoRoot
|
|
51
|
+
});
|
|
52
|
+
if (fallback.code !== 0) {
|
|
53
|
+
throw new Error(fallback.stderr.trim() || preferred.stderr.trim() || `Unable to initialize git in ${repoRoot}.`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
export async function ensureGitRepository(cwd) {
|
|
57
|
+
const existing = await findRepoRoot(cwd);
|
|
58
|
+
if (existing) {
|
|
59
|
+
return {
|
|
60
|
+
repoRoot: existing,
|
|
61
|
+
createdRepository: false
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
await initializeRepository(cwd);
|
|
65
|
+
return {
|
|
66
|
+
repoRoot: await detectRepoRoot(cwd),
|
|
67
|
+
createdRepository: true
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
export async function hasHeadCommit(repoRoot) {
|
|
71
|
+
const result = await runCommand("git", [
|
|
72
|
+
"rev-parse",
|
|
73
|
+
"--verify",
|
|
74
|
+
"HEAD"
|
|
75
|
+
], {
|
|
76
|
+
cwd: repoRoot
|
|
77
|
+
});
|
|
78
|
+
return result.code === 0;
|
|
79
|
+
}
|
|
80
|
+
export async function ensureBootstrapCommit(repoRoot, message = "kavi: bootstrap project") {
|
|
81
|
+
if (await hasHeadCommit(repoRoot)) {
|
|
82
|
+
return {
|
|
83
|
+
createdCommit: false,
|
|
84
|
+
commit: await getHeadCommit(repoRoot),
|
|
85
|
+
stagedPaths: []
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
const add = await runCommand("git", [
|
|
89
|
+
"add",
|
|
90
|
+
"-A"
|
|
91
|
+
], {
|
|
92
|
+
cwd: repoRoot
|
|
93
|
+
});
|
|
94
|
+
if (add.code !== 0) {
|
|
95
|
+
throw new Error(add.stderr.trim() || `Unable to stage bootstrap files in ${repoRoot}.`);
|
|
96
|
+
}
|
|
97
|
+
const staged = await runCommand("git", [
|
|
98
|
+
"diff",
|
|
99
|
+
"--cached",
|
|
100
|
+
"--name-only",
|
|
101
|
+
"--diff-filter=ACMRTUXB"
|
|
102
|
+
], {
|
|
103
|
+
cwd: repoRoot
|
|
104
|
+
});
|
|
105
|
+
if (staged.code !== 0) {
|
|
106
|
+
throw new Error(staged.stderr.trim() || `Unable to inspect staged bootstrap files in ${repoRoot}.`);
|
|
107
|
+
}
|
|
108
|
+
const commit = await runCommand("git", [
|
|
109
|
+
"-c",
|
|
110
|
+
"user.name=Kavi",
|
|
111
|
+
"-c",
|
|
112
|
+
"user.email=kavi@local.invalid",
|
|
113
|
+
"commit",
|
|
114
|
+
"--allow-empty",
|
|
115
|
+
"-m",
|
|
116
|
+
message
|
|
117
|
+
], {
|
|
118
|
+
cwd: repoRoot
|
|
119
|
+
});
|
|
120
|
+
if (commit.code !== 0) {
|
|
121
|
+
throw new Error(commit.stderr.trim() || `Unable to create bootstrap commit in ${repoRoot}.`);
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
createdCommit: true,
|
|
125
|
+
commit: await getHeadCommit(repoRoot),
|
|
126
|
+
stagedPaths: parsePathList(staged.stdout)
|
|
127
|
+
};
|
|
128
|
+
}
|
|
30
129
|
export async function getCurrentBranch(repoRoot) {
|
|
31
130
|
const result = await runCommand("git", [
|
|
32
131
|
"branch",
|
package/dist/main.js
CHANGED
|
@@ -9,14 +9,15 @@ import { KaviDaemon } from "./daemon.js";
|
|
|
9
9
|
import { addDecisionRecord, upsertPathClaim } from "./decision-ledger.js";
|
|
10
10
|
import { runDoctor } from "./doctor.js";
|
|
11
11
|
import { writeJson } from "./fs.js";
|
|
12
|
-
import { createGitignoreEntries, detectRepoRoot, ensureWorktrees,
|
|
12
|
+
import { createGitignoreEntries, detectRepoRoot, ensureBootstrapCommit, ensureGitRepository, ensureWorktrees, findRepoRoot, findOverlappingWorktreePaths, landBranches, resolveTargetBranch } from "./git.js";
|
|
13
13
|
import { buildSessionId, resolveAppPaths } from "./paths.js";
|
|
14
14
|
import { isProcessAlive, spawnDetachedNode } from "./process.js";
|
|
15
15
|
import { pingRpc, readSnapshot, rpcEnqueueTask, rpcNotifyExternalUpdate, rpcKickoff, rpcRecentEvents, rpcResolveApproval, rpcShutdown, rpcTaskArtifact } from "./rpc.js";
|
|
16
|
+
import { markReviewNotesLandedForTasks } from "./reviews.js";
|
|
16
17
|
import { resolveSessionRuntime } from "./runtime.js";
|
|
17
18
|
import { buildAdHocTask, extractPromptPathHints, routeTask } from "./router.js";
|
|
18
|
-
import { createSessionRecord, loadSessionRecord, readRecentEvents, recordEvent, sessionExists, sessionHeartbeatAgeMs } from "./session.js";
|
|
19
|
-
import { listTaskArtifacts, loadTaskArtifact } from "./task-artifacts.js";
|
|
19
|
+
import { createSessionRecord, loadSessionRecord, readRecentEvents, recordEvent, saveSessionRecord, sessionExists, sessionHeartbeatAgeMs } from "./session.js";
|
|
20
|
+
import { listTaskArtifacts, loadTaskArtifact, saveTaskArtifact } from "./task-artifacts.js";
|
|
20
21
|
import { attachTui } from "./tui.js";
|
|
21
22
|
const HEARTBEAT_STALE_MS = 10_000;
|
|
22
23
|
const CLAUDE_AUTO_ALLOW_TOOLS = new Set([
|
|
@@ -59,7 +60,7 @@ async function readStdinText() {
|
|
|
59
60
|
function renderUsage() {
|
|
60
61
|
return [
|
|
61
62
|
"Usage:",
|
|
62
|
-
" kavi init [--home]",
|
|
63
|
+
" kavi init [--home] [--no-commit]",
|
|
63
64
|
" kavi doctor [--json]",
|
|
64
65
|
" kavi start [--goal \"...\"]",
|
|
65
66
|
" kavi open [--goal \"...\"]",
|
|
@@ -71,6 +72,7 @@ function renderUsage() {
|
|
|
71
72
|
" kavi task-output <task-id|latest> [--json]",
|
|
72
73
|
" kavi decisions [--json] [--limit N]",
|
|
73
74
|
" kavi claims [--json] [--all]",
|
|
75
|
+
" kavi reviews [--json] [--all]",
|
|
74
76
|
" kavi approvals [--json] [--all]",
|
|
75
77
|
" kavi approve <request-id|latest> [--remember]",
|
|
76
78
|
" kavi deny <request-id|latest> [--remember]",
|
|
@@ -111,22 +113,65 @@ async function waitForSession(paths, expectedState = "running") {
|
|
|
111
113
|
}
|
|
112
114
|
throw new Error(`Timed out waiting for session state ${expectedState} in ${paths.stateFile}.`);
|
|
113
115
|
}
|
|
114
|
-
async function
|
|
115
|
-
|
|
116
|
+
async function prepareProjectContext(cwd, options) {
|
|
117
|
+
let repoRoot;
|
|
118
|
+
let createdRepository = false;
|
|
119
|
+
let hasGitRepository = false;
|
|
120
|
+
if (options.createRepository) {
|
|
121
|
+
const repository = await ensureGitRepository(cwd);
|
|
122
|
+
repoRoot = repository.repoRoot;
|
|
123
|
+
createdRepository = repository.createdRepository;
|
|
124
|
+
hasGitRepository = true;
|
|
125
|
+
} else {
|
|
126
|
+
const existingRepoRoot = await findRepoRoot(cwd);
|
|
127
|
+
repoRoot = existingRepoRoot ?? cwd;
|
|
128
|
+
hasGitRepository = existingRepoRoot !== null;
|
|
129
|
+
}
|
|
116
130
|
const paths = resolveAppPaths(repoRoot);
|
|
117
131
|
await ensureProjectScaffold(paths);
|
|
118
|
-
|
|
119
|
-
|
|
132
|
+
if (hasGitRepository) {
|
|
133
|
+
await createGitignoreEntries(repoRoot);
|
|
134
|
+
}
|
|
135
|
+
if (options.ensureHomeConfig) {
|
|
120
136
|
await ensureHomeConfig(paths);
|
|
121
|
-
console.log(`Initialized user-local Kavi config in ${paths.homeConfigFile}`);
|
|
122
137
|
}
|
|
123
|
-
|
|
138
|
+
return {
|
|
139
|
+
repoRoot,
|
|
140
|
+
paths,
|
|
141
|
+
createdRepository,
|
|
142
|
+
bootstrapCommit: options.ensureHeadCommit ? await ensureBootstrapCommit(repoRoot) : null
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
async function commandInit(cwd, args) {
|
|
146
|
+
const skipCommit = args.includes("--no-commit");
|
|
147
|
+
const prepared = await prepareProjectContext(cwd, {
|
|
148
|
+
createRepository: true,
|
|
149
|
+
ensureHeadCommit: !skipCommit,
|
|
150
|
+
ensureHomeConfig: args.includes("--home")
|
|
151
|
+
});
|
|
152
|
+
if (args.includes("--home")) {
|
|
153
|
+
console.log(`Initialized user-local Kavi config in ${prepared.paths.homeConfigFile}`);
|
|
154
|
+
}
|
|
155
|
+
if (prepared.createdRepository) {
|
|
156
|
+
console.log(`Initialized git repository in ${prepared.repoRoot}`);
|
|
157
|
+
}
|
|
158
|
+
console.log(`Initialized Kavi project scaffold in ${prepared.paths.kaviDir}`);
|
|
159
|
+
if (skipCommit) {
|
|
160
|
+
console.log("Skipped bootstrap commit creation (--no-commit).");
|
|
161
|
+
console.log('Kavi will create the first base commit automatically on "kavi open" or "kavi start".');
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (prepared.bootstrapCommit?.createdCommit) {
|
|
165
|
+
console.log(`Created bootstrap commit ${prepared.bootstrapCommit.commit.slice(0, 12)} with ${prepared.bootstrapCommit.stagedPaths.length} tracked path${prepared.bootstrapCommit.stagedPaths.length === 1 ? "" : "s"}.`);
|
|
166
|
+
}
|
|
124
167
|
}
|
|
125
168
|
async function commandDoctor(cwd, args) {
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
169
|
+
const prepared = await prepareProjectContext(cwd, {
|
|
170
|
+
createRepository: false,
|
|
171
|
+
ensureHeadCommit: false,
|
|
172
|
+
ensureHomeConfig: false
|
|
173
|
+
});
|
|
174
|
+
const checks = await runDoctor(prepared.repoRoot, prepared.paths);
|
|
130
175
|
if (args.includes("--json")) {
|
|
131
176
|
console.log(JSON.stringify(checks, null, 2));
|
|
132
177
|
process.exitCode = checks.some((check)=>!check.ok) ? 1 : 0;
|
|
@@ -142,12 +187,12 @@ async function commandDoctor(cwd, args) {
|
|
|
142
187
|
process.exitCode = failed ? 1 : 0;
|
|
143
188
|
}
|
|
144
189
|
async function startOrAttachSession(cwd, goal) {
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
190
|
+
const prepared = await prepareProjectContext(cwd, {
|
|
191
|
+
createRepository: true,
|
|
192
|
+
ensureHeadCommit: false,
|
|
193
|
+
ensureHomeConfig: true
|
|
194
|
+
});
|
|
195
|
+
const { repoRoot, paths } = prepared;
|
|
151
196
|
if (await sessionExists(paths)) {
|
|
152
197
|
try {
|
|
153
198
|
const session = await loadSessionRecord(paths);
|
|
@@ -163,14 +208,27 @@ async function startOrAttachSession(cwd, goal) {
|
|
|
163
208
|
});
|
|
164
209
|
} catch {}
|
|
165
210
|
}
|
|
211
|
+
await ensureStartupReady(repoRoot, paths);
|
|
166
212
|
const config = await loadConfig(paths);
|
|
167
213
|
const runtime = await resolveSessionRuntime(paths);
|
|
168
|
-
const
|
|
214
|
+
const bootstrapCommit = await ensureBootstrapCommit(repoRoot);
|
|
215
|
+
const baseCommit = bootstrapCommit.commit;
|
|
169
216
|
const sessionId = buildSessionId();
|
|
170
217
|
const rpcEndpoint = paths.socketPath;
|
|
171
218
|
await fs.writeFile(paths.commandsFile, "", "utf8");
|
|
172
219
|
const worktrees = await ensureWorktrees(repoRoot, paths, sessionId, config, baseCommit);
|
|
173
220
|
await createSessionRecord(paths, config, runtime, sessionId, baseCommit, worktrees, goal, rpcEndpoint);
|
|
221
|
+
if (prepared.createdRepository) {
|
|
222
|
+
await recordEvent(paths, sessionId, "repo.initialized", {
|
|
223
|
+
repoRoot
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
if (bootstrapCommit.createdCommit) {
|
|
227
|
+
await recordEvent(paths, sessionId, "repo.bootstrap_committed", {
|
|
228
|
+
commit: bootstrapCommit.commit,
|
|
229
|
+
stagedPaths: bootstrapCommit.stagedPaths
|
|
230
|
+
});
|
|
231
|
+
}
|
|
174
232
|
const pid = spawnDetachedNode(runtime.nodeExecutable, [
|
|
175
233
|
fileURLToPath(import.meta.url),
|
|
176
234
|
"__daemon",
|
|
@@ -189,6 +247,7 @@ async function ensureStartupReady(repoRoot, paths) {
|
|
|
189
247
|
"node",
|
|
190
248
|
"codex",
|
|
191
249
|
"claude",
|
|
250
|
+
"claude-auth",
|
|
192
251
|
"git-worktree",
|
|
193
252
|
"codex-app-server",
|
|
194
253
|
"codex-auth-file"
|
|
@@ -239,8 +298,8 @@ async function commandResume(cwd) {
|
|
|
239
298
|
}
|
|
240
299
|
async function commandStart(cwd, args) {
|
|
241
300
|
const goal = getGoal(args);
|
|
242
|
-
const repoRoot = await detectRepoRoot(cwd);
|
|
243
301
|
const socketPath = await startOrAttachSession(cwd, goal);
|
|
302
|
+
const repoRoot = await detectRepoRoot(cwd);
|
|
244
303
|
const paths = resolveAppPaths(repoRoot);
|
|
245
304
|
const session = await loadSessionRecord(paths);
|
|
246
305
|
console.log(`Started Kavi session ${session.id}`);
|
|
@@ -283,6 +342,10 @@ async function commandStatus(cwd, args) {
|
|
|
283
342
|
decisionCounts: {
|
|
284
343
|
total: session.decisions.length
|
|
285
344
|
},
|
|
345
|
+
reviewCounts: {
|
|
346
|
+
open: session.reviewNotes.filter((note)=>note.status === "open").length,
|
|
347
|
+
total: session.reviewNotes.length
|
|
348
|
+
},
|
|
286
349
|
pathClaimCounts: {
|
|
287
350
|
active: session.pathClaims.filter((claim)=>claim.status === "active").length
|
|
288
351
|
},
|
|
@@ -302,6 +365,7 @@ async function commandStatus(cwd, args) {
|
|
|
302
365
|
console.log(`Runtime: node=${payload.runtime.nodeExecutable} codex=${payload.runtime.codexExecutable} claude=${payload.runtime.claudeExecutable}`);
|
|
303
366
|
console.log(`Tasks: total=${payload.taskCounts.total} pending=${payload.taskCounts.pending} running=${payload.taskCounts.running} blocked=${payload.taskCounts.blocked} completed=${payload.taskCounts.completed} failed=${payload.taskCounts.failed}`);
|
|
304
367
|
console.log(`Approvals: pending=${payload.approvalCounts.pending}`);
|
|
368
|
+
console.log(`Reviews: open=${payload.reviewCounts.open} total=${payload.reviewCounts.total}`);
|
|
305
369
|
console.log(`Decisions: total=${payload.decisionCounts.total}`);
|
|
306
370
|
console.log(`Path claims: active=${payload.pathClaimCounts.active}`);
|
|
307
371
|
for (const worktree of payload.worktrees){
|
|
@@ -309,7 +373,7 @@ async function commandStatus(cwd, args) {
|
|
|
309
373
|
}
|
|
310
374
|
}
|
|
311
375
|
async function commandPaths(cwd, args) {
|
|
312
|
-
const repoRoot = await
|
|
376
|
+
const repoRoot = await findRepoRoot(cwd) ?? cwd;
|
|
313
377
|
const paths = resolveAppPaths(repoRoot);
|
|
314
378
|
const runtime = await resolveSessionRuntime(paths);
|
|
315
379
|
const payload = {
|
|
@@ -474,6 +538,21 @@ async function commandTaskOutput(cwd, args) {
|
|
|
474
538
|
}
|
|
475
539
|
console.log("Envelope:");
|
|
476
540
|
console.log(JSON.stringify(artifact.envelope, null, 2));
|
|
541
|
+
console.log("Review Notes:");
|
|
542
|
+
if (artifact.reviewNotes.length === 0) {
|
|
543
|
+
console.log("-");
|
|
544
|
+
} else {
|
|
545
|
+
for (const note of artifact.reviewNotes){
|
|
546
|
+
console.log(`${note.createdAt} | ${note.disposition} | ${note.status} | ${note.filePath}${note.hunkIndex === null ? "" : ` | hunk ${note.hunkIndex + 1}`}`);
|
|
547
|
+
console.log(` assignee: ${note.assignee ?? "-"}`);
|
|
548
|
+
console.log(` comments: ${note.comments.length}`);
|
|
549
|
+
for (const [index, comment] of note.comments.entries()){
|
|
550
|
+
console.log(` ${index === 0 ? "root" : `reply-${index}`}: ${comment.body}`);
|
|
551
|
+
}
|
|
552
|
+
console.log(` landed: ${note.landedAt ?? "-"}`);
|
|
553
|
+
console.log(` follow-ups: ${note.followUpTaskIds.join(", ") || "-"}`);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
477
556
|
console.log("Raw Output:");
|
|
478
557
|
console.log(artifact.rawOutput ?? "");
|
|
479
558
|
}
|
|
@@ -520,6 +599,32 @@ async function commandClaims(cwd, args) {
|
|
|
520
599
|
console.log(` note: ${claim.note ?? "-"}`);
|
|
521
600
|
}
|
|
522
601
|
}
|
|
602
|
+
async function commandReviews(cwd, args) {
|
|
603
|
+
const { paths } = await requireSession(cwd);
|
|
604
|
+
const rpcSnapshot = await tryRpcSnapshot(paths);
|
|
605
|
+
const session = rpcSnapshot?.session ?? await loadSessionRecord(paths);
|
|
606
|
+
const notes = args.includes("--all") ? [
|
|
607
|
+
...session.reviewNotes
|
|
608
|
+
] : session.reviewNotes.filter((note)=>note.status === "open");
|
|
609
|
+
if (args.includes("--json")) {
|
|
610
|
+
console.log(JSON.stringify(notes, null, 2));
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
if (notes.length === 0) {
|
|
614
|
+
console.log("No review notes recorded.");
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
for (const note of notes){
|
|
618
|
+
console.log(`${note.id} | ${note.agent} | ${note.status} | ${note.disposition} | ${note.filePath}${note.hunkIndex === null ? "" : ` | hunk ${note.hunkIndex + 1}`}`);
|
|
619
|
+
console.log(` task: ${note.taskId ?? "-"}`);
|
|
620
|
+
console.log(` assignee: ${note.assignee ?? "-"}`);
|
|
621
|
+
console.log(` updated: ${note.updatedAt}`);
|
|
622
|
+
console.log(` comments: ${note.comments.length}`);
|
|
623
|
+
console.log(` landed: ${note.landedAt ?? "-"}`);
|
|
624
|
+
console.log(` follow-ups: ${note.followUpTaskIds.join(", ") || "-"}`);
|
|
625
|
+
console.log(` body: ${note.body}`);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
523
628
|
async function commandApprovals(cwd, args) {
|
|
524
629
|
const { paths } = await requireSession(cwd);
|
|
525
630
|
const rpcSnapshot = await tryRpcSnapshot(paths);
|
|
@@ -683,6 +788,46 @@ async function commandLand(cwd) {
|
|
|
683
788
|
snapshotCommits: result.snapshotCommits,
|
|
684
789
|
commands: result.commandsRun
|
|
685
790
|
});
|
|
791
|
+
const landedReviewNotes = markReviewNotesLandedForTasks(session, session.tasks.filter((task)=>task.status === "completed").map((task)=>task.id));
|
|
792
|
+
for (const note of landedReviewNotes){
|
|
793
|
+
addDecisionRecord(session, {
|
|
794
|
+
kind: "review",
|
|
795
|
+
agent: note.agent,
|
|
796
|
+
taskId: note.taskId,
|
|
797
|
+
summary: `Marked review note ${note.id} as landed`,
|
|
798
|
+
detail: `Follow-up work for ${note.filePath} is now part of ${targetBranch}.`,
|
|
799
|
+
metadata: {
|
|
800
|
+
reviewNoteId: note.id,
|
|
801
|
+
filePath: note.filePath,
|
|
802
|
+
landedAt: note.landedAt,
|
|
803
|
+
targetBranch
|
|
804
|
+
}
|
|
805
|
+
});
|
|
806
|
+
await recordEvent(paths, session.id, "review.note_landed", {
|
|
807
|
+
reviewNoteId: note.id,
|
|
808
|
+
taskId: note.taskId,
|
|
809
|
+
followUpTaskIds: note.followUpTaskIds,
|
|
810
|
+
agent: note.agent,
|
|
811
|
+
filePath: note.filePath,
|
|
812
|
+
landedAt: note.landedAt,
|
|
813
|
+
targetBranch
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
await saveSessionRecord(paths, session);
|
|
817
|
+
const artifactTaskIds = [
|
|
818
|
+
...new Set(landedReviewNotes.flatMap((note)=>note.taskId ? [
|
|
819
|
+
note.taskId
|
|
820
|
+
] : []))
|
|
821
|
+
];
|
|
822
|
+
for (const taskId of artifactTaskIds){
|
|
823
|
+
const artifact = await loadTaskArtifact(paths, taskId);
|
|
824
|
+
if (!artifact) {
|
|
825
|
+
continue;
|
|
826
|
+
}
|
|
827
|
+
artifact.reviewNotes = session.reviewNotes.filter((note)=>note.taskId === taskId);
|
|
828
|
+
await saveTaskArtifact(paths, artifact);
|
|
829
|
+
}
|
|
830
|
+
await notifyOperatorSurface(paths, "land.completed");
|
|
686
831
|
console.log(`Landed branches into ${targetBranch}`);
|
|
687
832
|
console.log(`Integration branch: ${result.integrationBranch}`);
|
|
688
833
|
console.log(`Integration worktree: ${result.integrationPath}`);
|
|
@@ -845,6 +990,9 @@ async function main() {
|
|
|
845
990
|
case "claims":
|
|
846
991
|
await commandClaims(cwd, args);
|
|
847
992
|
break;
|
|
993
|
+
case "reviews":
|
|
994
|
+
await commandReviews(cwd, args);
|
|
995
|
+
break;
|
|
848
996
|
case "approvals":
|
|
849
997
|
await commandApprovals(cwd, args);
|
|
850
998
|
break;
|
package/dist/reviews.js
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { nowIso } from "./paths.js";
|
|
3
|
+
const MAX_REVIEW_NOTES = 200;
|
|
4
|
+
function trimBody(value) {
|
|
5
|
+
return value.trim();
|
|
6
|
+
}
|
|
7
|
+
function summarizeReviewNote(disposition, filePath, hunkHeader, body) {
|
|
8
|
+
const label = reviewDispositionSummaryLabel(disposition);
|
|
9
|
+
const scope = hunkHeader ? `${filePath} ${hunkHeader}` : filePath;
|
|
10
|
+
const firstLine = trimBody(body).split("\n")[0]?.trim() ?? "";
|
|
11
|
+
return firstLine ? `${label} ${scope}: ${firstLine}` : `${label} ${scope}`;
|
|
12
|
+
}
|
|
13
|
+
function reviewDispositionSummaryLabel(disposition) {
|
|
14
|
+
switch(disposition){
|
|
15
|
+
case "accepted_risk":
|
|
16
|
+
return "Accepted Risk";
|
|
17
|
+
case "wont_fix":
|
|
18
|
+
return "Won't Fix";
|
|
19
|
+
default:
|
|
20
|
+
return disposition[0].toUpperCase() + disposition.slice(1);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function createReviewComment(body) {
|
|
24
|
+
const timestamp = nowIso();
|
|
25
|
+
return {
|
|
26
|
+
id: randomUUID(),
|
|
27
|
+
body: trimBody(body),
|
|
28
|
+
createdAt: timestamp,
|
|
29
|
+
updatedAt: timestamp
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export function addReviewNote(session, input) {
|
|
33
|
+
const timestamp = nowIso();
|
|
34
|
+
const note = {
|
|
35
|
+
id: randomUUID(),
|
|
36
|
+
agent: input.agent,
|
|
37
|
+
assignee: input.assignee ?? input.agent,
|
|
38
|
+
taskId: input.taskId ?? null,
|
|
39
|
+
filePath: input.filePath,
|
|
40
|
+
hunkIndex: input.hunkIndex ?? null,
|
|
41
|
+
hunkHeader: input.hunkHeader ?? null,
|
|
42
|
+
disposition: input.disposition,
|
|
43
|
+
status: "open",
|
|
44
|
+
summary: summarizeReviewNote(input.disposition, input.filePath, input.hunkHeader ?? null, input.body),
|
|
45
|
+
body: trimBody(input.body),
|
|
46
|
+
comments: [
|
|
47
|
+
createReviewComment(input.body)
|
|
48
|
+
],
|
|
49
|
+
resolvedAt: null,
|
|
50
|
+
landedAt: null,
|
|
51
|
+
followUpTaskIds: [],
|
|
52
|
+
createdAt: timestamp,
|
|
53
|
+
updatedAt: timestamp
|
|
54
|
+
};
|
|
55
|
+
session.reviewNotes = [
|
|
56
|
+
...session.reviewNotes,
|
|
57
|
+
note
|
|
58
|
+
].slice(-MAX_REVIEW_NOTES);
|
|
59
|
+
return note;
|
|
60
|
+
}
|
|
61
|
+
export function reviewNotesForTask(session, taskId) {
|
|
62
|
+
return session.reviewNotes.filter((note)=>note.taskId === taskId);
|
|
63
|
+
}
|
|
64
|
+
export function reviewNotesForPath(session, agent, filePath, hunkIndex) {
|
|
65
|
+
return session.reviewNotes.filter((note)=>{
|
|
66
|
+
if (note.agent !== agent || note.filePath !== filePath) {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
if (hunkIndex === undefined) {
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
return note.hunkIndex === hunkIndex;
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
export function updateReviewNote(session, noteId, input) {
|
|
76
|
+
const note = session.reviewNotes.find((item)=>item.id === noteId) ?? null;
|
|
77
|
+
if (!note) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
const nextBody = typeof input.body === "string" ? trimBody(input.body) : note.body;
|
|
81
|
+
const nextDisposition = input.disposition ?? note.disposition;
|
|
82
|
+
const nextAssignee = input.assignee === undefined ? note.assignee : input.assignee;
|
|
83
|
+
note.body = nextBody;
|
|
84
|
+
note.disposition = nextDisposition;
|
|
85
|
+
note.assignee = nextAssignee;
|
|
86
|
+
if (note.comments.length === 0) {
|
|
87
|
+
note.comments.push(createReviewComment(nextBody));
|
|
88
|
+
} else if (typeof input.body === "string") {
|
|
89
|
+
note.comments[0] = {
|
|
90
|
+
...note.comments[0],
|
|
91
|
+
body: nextBody,
|
|
92
|
+
updatedAt: nowIso()
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
note.summary = summarizeReviewNote(nextDisposition, note.filePath, note.hunkHeader, nextBody);
|
|
96
|
+
note.updatedAt = nowIso();
|
|
97
|
+
return note;
|
|
98
|
+
}
|
|
99
|
+
export function setReviewNoteStatus(session, noteId, status) {
|
|
100
|
+
const note = session.reviewNotes.find((item)=>item.id === noteId) ?? null;
|
|
101
|
+
if (!note) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
note.status = status;
|
|
105
|
+
note.resolvedAt = status === "resolved" ? nowIso() : null;
|
|
106
|
+
if (status === "open") {
|
|
107
|
+
note.landedAt = null;
|
|
108
|
+
}
|
|
109
|
+
note.updatedAt = nowIso();
|
|
110
|
+
return note;
|
|
111
|
+
}
|
|
112
|
+
export function linkReviewFollowUpTask(session, noteId, taskId, assignee) {
|
|
113
|
+
const note = session.reviewNotes.find((item)=>item.id === noteId) ?? null;
|
|
114
|
+
if (!note) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
note.followUpTaskIds = [
|
|
118
|
+
...new Set([
|
|
119
|
+
...note.followUpTaskIds,
|
|
120
|
+
taskId
|
|
121
|
+
])
|
|
122
|
+
];
|
|
123
|
+
if (assignee !== undefined) {
|
|
124
|
+
note.assignee = assignee;
|
|
125
|
+
}
|
|
126
|
+
note.updatedAt = nowIso();
|
|
127
|
+
return note;
|
|
128
|
+
}
|
|
129
|
+
export function addReviewReply(session, noteId, body) {
|
|
130
|
+
const note = session.reviewNotes.find((item)=>item.id === noteId) ?? null;
|
|
131
|
+
if (!note) {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
note.comments.push(createReviewComment(body));
|
|
135
|
+
note.status = "open";
|
|
136
|
+
note.resolvedAt = null;
|
|
137
|
+
note.landedAt = null;
|
|
138
|
+
note.updatedAt = nowIso();
|
|
139
|
+
return note;
|
|
140
|
+
}
|
|
141
|
+
export function cycleReviewAssignee(current, noteAgent) {
|
|
142
|
+
const sequence = [
|
|
143
|
+
noteAgent,
|
|
144
|
+
noteAgent === "codex" ? "claude" : "codex",
|
|
145
|
+
"operator",
|
|
146
|
+
null
|
|
147
|
+
];
|
|
148
|
+
const index = sequence.findIndex((item)=>item === current);
|
|
149
|
+
if (index === -1) {
|
|
150
|
+
return noteAgent;
|
|
151
|
+
}
|
|
152
|
+
return sequence[(index + 1) % sequence.length] ?? null;
|
|
153
|
+
}
|
|
154
|
+
export function autoResolveReviewNotesForCompletedTask(session, taskId) {
|
|
155
|
+
const resolved = [];
|
|
156
|
+
for (const note of session.reviewNotes){
|
|
157
|
+
if (note.status !== "open" || !note.followUpTaskIds.includes(taskId)) {
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
note.status = "resolved";
|
|
161
|
+
note.resolvedAt = nowIso();
|
|
162
|
+
note.updatedAt = nowIso();
|
|
163
|
+
resolved.push(note);
|
|
164
|
+
}
|
|
165
|
+
return resolved;
|
|
166
|
+
}
|
|
167
|
+
export function markReviewNotesLandedForTasks(session, taskIds) {
|
|
168
|
+
if (taskIds.length === 0) {
|
|
169
|
+
return [];
|
|
170
|
+
}
|
|
171
|
+
const landedTaskIds = new Set(taskIds);
|
|
172
|
+
const landed = [];
|
|
173
|
+
for (const note of session.reviewNotes){
|
|
174
|
+
if (note.status !== "resolved" || note.landedAt !== null) {
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
if (!note.followUpTaskIds.some((taskId)=>landedTaskIds.has(taskId))) {
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
note.landedAt = nowIso();
|
|
181
|
+
note.updatedAt = nowIso();
|
|
182
|
+
landed.push(note);
|
|
183
|
+
}
|
|
184
|
+
return landed;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
//# sourceURL=reviews.ts
|