@mandipadk7/kavi 0.1.0 → 0.1.2
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 +25 -7
- package/dist/adapters/claude.js +136 -13
- package/dist/adapters/codex.js +235 -30
- package/dist/adapters/shared.js +35 -0
- package/dist/approvals.js +72 -1
- package/dist/codex-app-server.js +310 -0
- package/dist/command-queue.js +1 -0
- package/dist/daemon.js +446 -5
- package/dist/decision-ledger.js +75 -0
- package/dist/git.js +171 -0
- package/dist/main.js +251 -36
- package/dist/paths.js +1 -1
- package/dist/prompts.js +13 -0
- package/dist/router.js +190 -5
- package/dist/rpc.js +226 -0
- package/dist/runtime.js +4 -1
- package/dist/session.js +10 -1
- package/dist/task-artifacts.js +10 -2
- package/dist/tui.js +1653 -72
- package/package.json +7 -12
package/dist/main.js
CHANGED
|
@@ -6,13 +6,15 @@ import { createApprovalRequest, describeToolUse, findApprovalRule, listApprovalR
|
|
|
6
6
|
import { appendCommand } from "./command-queue.js";
|
|
7
7
|
import { ensureHomeConfig, ensureProjectScaffold, loadConfig } from "./config.js";
|
|
8
8
|
import { KaviDaemon } from "./daemon.js";
|
|
9
|
+
import { addDecisionRecord, upsertPathClaim } from "./decision-ledger.js";
|
|
9
10
|
import { runDoctor } from "./doctor.js";
|
|
10
11
|
import { writeJson } from "./fs.js";
|
|
11
|
-
import { createGitignoreEntries, detectRepoRoot, ensureWorktrees, getHeadCommit, landBranches, resolveTargetBranch } from "./git.js";
|
|
12
|
+
import { createGitignoreEntries, detectRepoRoot, ensureWorktrees, findOverlappingWorktreePaths, getHeadCommit, landBranches, resolveTargetBranch } from "./git.js";
|
|
12
13
|
import { buildSessionId, resolveAppPaths } from "./paths.js";
|
|
13
14
|
import { isProcessAlive, spawnDetachedNode } from "./process.js";
|
|
15
|
+
import { pingRpc, readSnapshot, rpcEnqueueTask, rpcNotifyExternalUpdate, rpcKickoff, rpcRecentEvents, rpcResolveApproval, rpcShutdown, rpcTaskArtifact } from "./rpc.js";
|
|
14
16
|
import { resolveSessionRuntime } from "./runtime.js";
|
|
15
|
-
import {
|
|
17
|
+
import { buildAdHocTask, extractPromptPathHints, routeTask } from "./router.js";
|
|
16
18
|
import { createSessionRecord, loadSessionRecord, readRecentEvents, recordEvent, sessionExists, sessionHeartbeatAgeMs } from "./session.js";
|
|
17
19
|
import { listTaskArtifacts, loadTaskArtifact } from "./task-artifacts.js";
|
|
18
20
|
import { attachTui } from "./tui.js";
|
|
@@ -67,6 +69,8 @@ function renderUsage() {
|
|
|
67
69
|
" kavi task [--agent codex|claude|auto] <prompt>",
|
|
68
70
|
" kavi tasks [--json]",
|
|
69
71
|
" kavi task-output <task-id|latest> [--json]",
|
|
72
|
+
" kavi decisions [--json] [--limit N]",
|
|
73
|
+
" kavi claims [--json] [--all]",
|
|
70
74
|
" kavi approvals [--json] [--all]",
|
|
71
75
|
" kavi approve <request-id|latest> [--remember]",
|
|
72
76
|
" kavi deny <request-id|latest> [--remember]",
|
|
@@ -95,7 +99,7 @@ async function waitForSession(paths, expectedState = "running") {
|
|
|
95
99
|
try {
|
|
96
100
|
if (await sessionExists(paths)) {
|
|
97
101
|
const session = await loadSessionRecord(paths);
|
|
98
|
-
if (expectedState === "running" && isSessionLive(session)) {
|
|
102
|
+
if (expectedState === "running" && isSessionLive(session) && await pingRpc(paths)) {
|
|
99
103
|
return;
|
|
100
104
|
}
|
|
101
105
|
if (expectedState === "stopped" && session.status === "stopped") {
|
|
@@ -143,14 +147,13 @@ async function startOrAttachSession(cwd, goal) {
|
|
|
143
147
|
await ensureProjectScaffold(paths);
|
|
144
148
|
await createGitignoreEntries(repoRoot);
|
|
145
149
|
await ensureHomeConfig(paths);
|
|
150
|
+
await ensureStartupReady(repoRoot, paths);
|
|
146
151
|
if (await sessionExists(paths)) {
|
|
147
152
|
try {
|
|
148
153
|
const session = await loadSessionRecord(paths);
|
|
149
|
-
if (isSessionLive(session)) {
|
|
154
|
+
if (isSessionLive(session) && await pingRpc(paths)) {
|
|
150
155
|
if (goal) {
|
|
151
|
-
await
|
|
152
|
-
prompt: goal
|
|
153
|
-
});
|
|
156
|
+
await rpcKickoff(paths, goal);
|
|
154
157
|
}
|
|
155
158
|
return session.socketPath;
|
|
156
159
|
}
|
|
@@ -164,7 +167,7 @@ async function startOrAttachSession(cwd, goal) {
|
|
|
164
167
|
const runtime = await resolveSessionRuntime(paths);
|
|
165
168
|
const baseCommit = await getHeadCommit(repoRoot);
|
|
166
169
|
const sessionId = buildSessionId();
|
|
167
|
-
const rpcEndpoint =
|
|
170
|
+
const rpcEndpoint = paths.socketPath;
|
|
168
171
|
await fs.writeFile(paths.commandsFile, "", "utf8");
|
|
169
172
|
const worktrees = await ensureWorktrees(repoRoot, paths, sessionId, config, baseCommit);
|
|
170
173
|
await createSessionRecord(paths, config, runtime, sessionId, baseCommit, worktrees, goal, rpcEndpoint);
|
|
@@ -180,6 +183,23 @@ async function startOrAttachSession(cwd, goal) {
|
|
|
180
183
|
await waitForSession(paths);
|
|
181
184
|
return rpcEndpoint;
|
|
182
185
|
}
|
|
186
|
+
async function ensureStartupReady(repoRoot, paths) {
|
|
187
|
+
const checks = await runDoctor(repoRoot, paths);
|
|
188
|
+
const required = new Set([
|
|
189
|
+
"node",
|
|
190
|
+
"codex",
|
|
191
|
+
"claude",
|
|
192
|
+
"git-worktree",
|
|
193
|
+
"codex-app-server",
|
|
194
|
+
"codex-auth-file"
|
|
195
|
+
]);
|
|
196
|
+
const failures = checks.filter((check)=>required.has(check.name) && !check.ok);
|
|
197
|
+
if (failures.length === 0) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
const details = failures.map((check)=>`${check.name}: ${check.detail}`).join("\n");
|
|
201
|
+
throw new Error(`Kavi startup blocked by failing readiness checks.\n${details}\nRun "kavi doctor" for the full report.`);
|
|
202
|
+
}
|
|
183
203
|
async function requireSession(cwd) {
|
|
184
204
|
const repoRoot = await detectRepoRoot(cwd);
|
|
185
205
|
const paths = resolveAppPaths(repoRoot);
|
|
@@ -191,6 +211,20 @@ async function requireSession(cwd) {
|
|
|
191
211
|
paths
|
|
192
212
|
};
|
|
193
213
|
}
|
|
214
|
+
async function tryRpcSnapshot(paths) {
|
|
215
|
+
if (!await pingRpc(paths)) {
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
return await readSnapshot(paths);
|
|
219
|
+
}
|
|
220
|
+
async function notifyOperatorSurface(paths, reason) {
|
|
221
|
+
if (!await pingRpc(paths)) {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
try {
|
|
225
|
+
await rpcNotifyExternalUpdate(paths, reason);
|
|
226
|
+
} catch {}
|
|
227
|
+
}
|
|
194
228
|
async function commandOpen(cwd, args) {
|
|
195
229
|
const goal = getGoal(args);
|
|
196
230
|
await startOrAttachSession(cwd, goal);
|
|
@@ -219,17 +253,20 @@ async function commandStart(cwd, args) {
|
|
|
219
253
|
}
|
|
220
254
|
async function commandStatus(cwd, args) {
|
|
221
255
|
const { paths } = await requireSession(cwd);
|
|
222
|
-
const
|
|
223
|
-
const
|
|
256
|
+
const rpcSnapshot = await tryRpcSnapshot(paths);
|
|
257
|
+
const session = rpcSnapshot?.session ?? await loadSessionRecord(paths);
|
|
258
|
+
const pendingApprovals = rpcSnapshot?.approvals.filter((item)=>item.status === "pending") ?? await listApprovalRequests(paths);
|
|
224
259
|
const heartbeatAgeMs = sessionHeartbeatAgeMs(session);
|
|
225
260
|
const payload = {
|
|
226
261
|
id: session.id,
|
|
227
262
|
status: session.status,
|
|
228
263
|
repoRoot: session.repoRoot,
|
|
264
|
+
socketPath: session.socketPath,
|
|
229
265
|
goal: session.goal,
|
|
230
266
|
daemonPid: session.daemonPid,
|
|
231
267
|
daemonHeartbeatAt: session.daemonHeartbeatAt,
|
|
232
268
|
daemonHealthy: isSessionLive(session),
|
|
269
|
+
rpcConnected: rpcSnapshot !== null,
|
|
233
270
|
heartbeatAgeMs,
|
|
234
271
|
runtime: session.runtime,
|
|
235
272
|
taskCounts: {
|
|
@@ -243,6 +280,12 @@ async function commandStatus(cwd, args) {
|
|
|
243
280
|
approvalCounts: {
|
|
244
281
|
pending: pendingApprovals.length
|
|
245
282
|
},
|
|
283
|
+
decisionCounts: {
|
|
284
|
+
total: session.decisions.length
|
|
285
|
+
},
|
|
286
|
+
pathClaimCounts: {
|
|
287
|
+
active: session.pathClaims.filter((claim)=>claim.status === "active").length
|
|
288
|
+
},
|
|
246
289
|
worktrees: session.worktrees
|
|
247
290
|
};
|
|
248
291
|
if (args.includes("--json")) {
|
|
@@ -252,12 +295,15 @@ async function commandStatus(cwd, args) {
|
|
|
252
295
|
console.log(`Session: ${payload.id}`);
|
|
253
296
|
console.log(`Status: ${payload.status}${payload.daemonHealthy ? " (healthy)" : " (stale or stopped)"}`);
|
|
254
297
|
console.log(`Repo: ${payload.repoRoot}`);
|
|
298
|
+
console.log(`Control: ${payload.socketPath}${payload.rpcConnected ? " (connected)" : " (disconnected)"}`);
|
|
255
299
|
console.log(`Goal: ${payload.goal ?? "-"}`);
|
|
256
300
|
console.log(`Daemon PID: ${payload.daemonPid ?? "-"}`);
|
|
257
301
|
console.log(`Heartbeat: ${payload.daemonHeartbeatAt ?? "-"}${heartbeatAgeMs === null ? "" : ` (${heartbeatAgeMs} ms ago)`}`);
|
|
258
302
|
console.log(`Runtime: node=${payload.runtime.nodeExecutable} codex=${payload.runtime.codexExecutable} claude=${payload.runtime.claudeExecutable}`);
|
|
259
303
|
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}`);
|
|
260
304
|
console.log(`Approvals: pending=${payload.approvalCounts.pending}`);
|
|
305
|
+
console.log(`Decisions: total=${payload.decisionCounts.total}`);
|
|
306
|
+
console.log(`Path claims: active=${payload.pathClaimCounts.active}`);
|
|
261
307
|
for (const worktree of payload.worktrees){
|
|
262
308
|
console.log(`- ${worktree.agent}: ${worktree.path}`);
|
|
263
309
|
}
|
|
@@ -278,6 +324,7 @@ async function commandPaths(cwd, args) {
|
|
|
278
324
|
eventsFile: paths.eventsFile,
|
|
279
325
|
approvalsFile: paths.approvalsFile,
|
|
280
326
|
commandsFile: paths.commandsFile,
|
|
327
|
+
socketPath: paths.socketPath,
|
|
281
328
|
runsDir: paths.runsDir,
|
|
282
329
|
claudeSettingsFile: paths.claudeSettingsFile,
|
|
283
330
|
homeApprovalRulesFile: paths.homeApprovalRulesFile,
|
|
@@ -298,6 +345,7 @@ async function commandPaths(cwd, args) {
|
|
|
298
345
|
console.log(`Events file: ${payload.eventsFile}`);
|
|
299
346
|
console.log(`Approvals file: ${payload.approvalsFile}`);
|
|
300
347
|
console.log(`Command queue: ${payload.commandsFile}`);
|
|
348
|
+
console.log(`Control socket: ${payload.socketPath}`);
|
|
301
349
|
console.log(`Task artifacts: ${payload.runsDir}`);
|
|
302
350
|
console.log(`Claude settings: ${payload.claudeSettingsFile}`);
|
|
303
351
|
console.log(`Approval rules: ${payload.homeApprovalRulesFile}`);
|
|
@@ -305,26 +353,52 @@ async function commandPaths(cwd, args) {
|
|
|
305
353
|
}
|
|
306
354
|
async function commandTask(cwd, args) {
|
|
307
355
|
const { paths } = await requireSession(cwd);
|
|
308
|
-
const
|
|
356
|
+
const rpcSnapshot = await tryRpcSnapshot(paths);
|
|
357
|
+
const session = rpcSnapshot?.session ?? await loadSessionRecord(paths);
|
|
309
358
|
const requestedAgent = getFlag(args, "--agent");
|
|
310
359
|
const prompt = getGoal(args.filter((arg, index)=>arg !== "--agent" && args[index - 1] !== "--agent"));
|
|
311
360
|
if (!prompt) {
|
|
312
361
|
throw new Error("A task prompt is required. Example: kavi task --agent auto \"Build auth route\"");
|
|
313
362
|
}
|
|
314
|
-
const
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
363
|
+
const routeDecision = requestedAgent === "codex" || requestedAgent === "claude" ? {
|
|
364
|
+
owner: requestedAgent,
|
|
365
|
+
strategy: "manual",
|
|
366
|
+
confidence: 1,
|
|
367
|
+
reason: `User explicitly assigned the task to ${requestedAgent}.`,
|
|
368
|
+
claimedPaths: extractPromptPathHints(prompt)
|
|
369
|
+
} : await routeTask(prompt, session, paths);
|
|
370
|
+
if (rpcSnapshot) {
|
|
371
|
+
await rpcEnqueueTask(paths, {
|
|
372
|
+
owner: routeDecision.owner,
|
|
373
|
+
prompt,
|
|
374
|
+
routeReason: routeDecision.reason,
|
|
375
|
+
claimedPaths: routeDecision.claimedPaths,
|
|
376
|
+
routeStrategy: routeDecision.strategy,
|
|
377
|
+
routeConfidence: routeDecision.confidence
|
|
378
|
+
});
|
|
379
|
+
} else {
|
|
380
|
+
await appendCommand(paths, "enqueue", {
|
|
381
|
+
owner: routeDecision.owner,
|
|
382
|
+
prompt,
|
|
383
|
+
routeReason: routeDecision.reason,
|
|
384
|
+
claimedPaths: routeDecision.claimedPaths,
|
|
385
|
+
routeStrategy: routeDecision.strategy,
|
|
386
|
+
routeConfidence: routeDecision.confidence
|
|
387
|
+
});
|
|
388
|
+
}
|
|
319
389
|
await recordEvent(paths, session.id, "task.cli_enqueued", {
|
|
320
|
-
owner,
|
|
321
|
-
prompt
|
|
390
|
+
owner: routeDecision.owner,
|
|
391
|
+
prompt,
|
|
392
|
+
strategy: routeDecision.strategy,
|
|
393
|
+
confidence: routeDecision.confidence,
|
|
394
|
+
claimedPaths: routeDecision.claimedPaths
|
|
322
395
|
});
|
|
323
|
-
console.log(`Queued task for ${owner}: ${prompt}`);
|
|
396
|
+
console.log(`Queued task for ${routeDecision.owner}: ${prompt}\nRoute: ${routeDecision.strategy} (${routeDecision.confidence.toFixed(2)}) ${routeDecision.reason}`);
|
|
324
397
|
}
|
|
325
398
|
async function commandTasks(cwd, args) {
|
|
326
399
|
const { paths } = await requireSession(cwd);
|
|
327
|
-
const
|
|
400
|
+
const rpcSnapshot = await tryRpcSnapshot(paths);
|
|
401
|
+
const session = rpcSnapshot?.session ?? await loadSessionRecord(paths);
|
|
328
402
|
const artifacts = await listTaskArtifacts(paths);
|
|
329
403
|
const artifactMap = new Map(artifacts.map((artifact)=>[
|
|
330
404
|
artifact.taskId,
|
|
@@ -338,6 +412,8 @@ async function commandTasks(cwd, args) {
|
|
|
338
412
|
createdAt: task.createdAt,
|
|
339
413
|
updatedAt: task.updatedAt,
|
|
340
414
|
summary: task.summary,
|
|
415
|
+
routeReason: task.routeReason,
|
|
416
|
+
claimedPaths: task.claimedPaths,
|
|
341
417
|
hasArtifact: artifactMap.has(task.id)
|
|
342
418
|
}));
|
|
343
419
|
if (args.includes("--json")) {
|
|
@@ -348,6 +424,8 @@ async function commandTasks(cwd, args) {
|
|
|
348
424
|
console.log(`${task.id} | ${task.owner} | ${task.status} | artifact=${task.hasArtifact ? "yes" : "no"}`);
|
|
349
425
|
console.log(` title: ${task.title}`);
|
|
350
426
|
console.log(` updated: ${task.updatedAt}`);
|
|
427
|
+
console.log(` route: ${task.routeReason ?? "-"}`);
|
|
428
|
+
console.log(` paths: ${task.claimedPaths.join(", ") || "-"}`);
|
|
351
429
|
console.log(` summary: ${task.summary ?? "-"}`);
|
|
352
430
|
}
|
|
353
431
|
}
|
|
@@ -367,12 +445,13 @@ function resolveRequestedTaskId(args, knownTaskIds) {
|
|
|
367
445
|
}
|
|
368
446
|
async function commandTaskOutput(cwd, args) {
|
|
369
447
|
const { paths } = await requireSession(cwd);
|
|
370
|
-
const
|
|
448
|
+
const rpcSnapshot = await tryRpcSnapshot(paths);
|
|
449
|
+
const session = rpcSnapshot?.session ?? await loadSessionRecord(paths);
|
|
371
450
|
const sortedTasks = [
|
|
372
451
|
...session.tasks
|
|
373
452
|
].sort((left, right)=>left.updatedAt.localeCompare(right.updatedAt));
|
|
374
453
|
const taskId = resolveRequestedTaskId(args, sortedTasks.map((task)=>task.id));
|
|
375
|
-
const artifact = await loadTaskArtifact(paths, taskId);
|
|
454
|
+
const artifact = rpcSnapshot ? await rpcTaskArtifact(paths, taskId) : await loadTaskArtifact(paths, taskId);
|
|
376
455
|
if (!artifact) {
|
|
377
456
|
throw new Error(`No task artifact found for ${taskId}.`);
|
|
378
457
|
}
|
|
@@ -386,15 +465,65 @@ async function commandTaskOutput(cwd, args) {
|
|
|
386
465
|
console.log(`Started: ${artifact.startedAt}`);
|
|
387
466
|
console.log(`Finished: ${artifact.finishedAt}`);
|
|
388
467
|
console.log(`Summary: ${artifact.summary ?? "-"}`);
|
|
468
|
+
console.log(`Route: ${artifact.routeReason ?? "-"}`);
|
|
469
|
+
console.log(`Claimed paths: ${artifact.claimedPaths.join(", ") || "-"}`);
|
|
389
470
|
console.log(`Error: ${artifact.error ?? "-"}`);
|
|
471
|
+
console.log("Decision Replay:");
|
|
472
|
+
for (const line of artifact.decisionReplay){
|
|
473
|
+
console.log(line);
|
|
474
|
+
}
|
|
390
475
|
console.log("Envelope:");
|
|
391
476
|
console.log(JSON.stringify(artifact.envelope, null, 2));
|
|
392
477
|
console.log("Raw Output:");
|
|
393
478
|
console.log(artifact.rawOutput ?? "");
|
|
394
479
|
}
|
|
480
|
+
async function commandDecisions(cwd, args) {
|
|
481
|
+
const { paths } = await requireSession(cwd);
|
|
482
|
+
const rpcSnapshot = await tryRpcSnapshot(paths);
|
|
483
|
+
const session = rpcSnapshot?.session ?? await loadSessionRecord(paths);
|
|
484
|
+
const limitArg = getFlag(args, "--limit");
|
|
485
|
+
const limit = limitArg ? Number(limitArg) : 20;
|
|
486
|
+
const decisions = [
|
|
487
|
+
...session.decisions
|
|
488
|
+
].sort((left, right)=>left.createdAt.localeCompare(right.createdAt)).slice(-Math.max(1, Number.isFinite(limit) ? limit : 20));
|
|
489
|
+
if (args.includes("--json")) {
|
|
490
|
+
console.log(JSON.stringify(decisions, null, 2));
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
if (decisions.length === 0) {
|
|
494
|
+
console.log("No decisions recorded.");
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
for (const decision of decisions){
|
|
498
|
+
console.log(`${decision.createdAt} | ${decision.kind} | ${decision.agent ?? "-"} | ${decision.summary}`);
|
|
499
|
+
console.log(` task: ${decision.taskId ?? "-"}`);
|
|
500
|
+
console.log(` detail: ${decision.detail}`);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
async function commandClaims(cwd, args) {
|
|
504
|
+
const { paths } = await requireSession(cwd);
|
|
505
|
+
const rpcSnapshot = await tryRpcSnapshot(paths);
|
|
506
|
+
const session = rpcSnapshot?.session ?? await loadSessionRecord(paths);
|
|
507
|
+
const claims = args.includes("--all") ? session.pathClaims : session.pathClaims.filter((claim)=>claim.status === "active");
|
|
508
|
+
if (args.includes("--json")) {
|
|
509
|
+
console.log(JSON.stringify(claims, null, 2));
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
if (claims.length === 0) {
|
|
513
|
+
console.log("No path claims recorded.");
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
for (const claim of claims){
|
|
517
|
+
console.log(`${claim.id} | ${claim.agent} | ${claim.status} | ${claim.source} | ${claim.paths.join(", ") || "-"}`);
|
|
518
|
+
console.log(` task: ${claim.taskId}`);
|
|
519
|
+
console.log(` updated: ${claim.updatedAt}`);
|
|
520
|
+
console.log(` note: ${claim.note ?? "-"}`);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
395
523
|
async function commandApprovals(cwd, args) {
|
|
396
524
|
const { paths } = await requireSession(cwd);
|
|
397
|
-
const
|
|
525
|
+
const rpcSnapshot = await tryRpcSnapshot(paths);
|
|
526
|
+
const requests = rpcSnapshot ? rpcSnapshot.approvals.filter((request)=>args.includes("--all") || request.status === "pending") : await listApprovalRequests(paths, {
|
|
398
527
|
includeResolved: args.includes("--all")
|
|
399
528
|
});
|
|
400
529
|
if (args.includes("--json")) {
|
|
@@ -425,28 +554,54 @@ function resolveApprovalRequestId(requests, requested) {
|
|
|
425
554
|
}
|
|
426
555
|
async function commandResolveApproval(cwd, args, decision) {
|
|
427
556
|
const { paths } = await requireSession(cwd);
|
|
428
|
-
const
|
|
557
|
+
const rpcSnapshot = await tryRpcSnapshot(paths);
|
|
558
|
+
const requests = rpcSnapshot?.approvals ?? await listApprovalRequests(paths, {
|
|
429
559
|
includeResolved: true
|
|
430
560
|
});
|
|
431
561
|
const requestedId = args.find((arg)=>!arg.startsWith("--")) ?? "latest";
|
|
432
562
|
const requestId = resolveApprovalRequestId(requests, requestedId);
|
|
433
563
|
const remember = args.includes("--remember");
|
|
434
|
-
const request =
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
564
|
+
const request = requests.find((item)=>item.id === requestId);
|
|
565
|
+
if (!request) {
|
|
566
|
+
throw new Error(`Approval request ${requestId} not found.`);
|
|
567
|
+
}
|
|
568
|
+
if (rpcSnapshot) {
|
|
569
|
+
await rpcResolveApproval(paths, {
|
|
570
|
+
requestId,
|
|
571
|
+
decision,
|
|
572
|
+
remember
|
|
573
|
+
});
|
|
574
|
+
} else {
|
|
575
|
+
const resolved = await resolveApprovalRequest(paths, requestId, decision, remember);
|
|
576
|
+
const session = await loadSessionRecord(paths);
|
|
577
|
+
addDecisionRecord(session, {
|
|
578
|
+
kind: "approval",
|
|
579
|
+
agent: resolved.agent,
|
|
580
|
+
summary: `${decision === "allow" ? "Approved" : "Denied"} ${resolved.toolName}`,
|
|
581
|
+
detail: resolved.summary,
|
|
582
|
+
metadata: {
|
|
583
|
+
requestId: resolved.id,
|
|
584
|
+
remember,
|
|
585
|
+
toolName: resolved.toolName
|
|
586
|
+
}
|
|
587
|
+
});
|
|
588
|
+
await saveSessionRecord(paths, session);
|
|
589
|
+
await recordEvent(paths, session.id, "approval.resolved", {
|
|
590
|
+
requestId: resolved.id,
|
|
591
|
+
decision,
|
|
592
|
+
remember,
|
|
593
|
+
agent: resolved.agent,
|
|
594
|
+
toolName: resolved.toolName
|
|
595
|
+
});
|
|
596
|
+
}
|
|
443
597
|
console.log(`${decision === "allow" ? "Approved" : "Denied"} ${request.id}: ${request.summary}${remember ? " (remembered)" : ""}`);
|
|
444
598
|
}
|
|
445
599
|
async function commandEvents(cwd, args) {
|
|
446
600
|
const { paths } = await requireSession(cwd);
|
|
447
601
|
const limitArg = getFlag(args, "--limit");
|
|
448
602
|
const limit = limitArg ? Number(limitArg) : 20;
|
|
449
|
-
const
|
|
603
|
+
const rpcSnapshot = await tryRpcSnapshot(paths);
|
|
604
|
+
const events = rpcSnapshot ? await rpcRecentEvents(paths, Number.isFinite(limit) ? limit : 20) : await readRecentEvents(paths, Number.isFinite(limit) ? limit : 20);
|
|
450
605
|
for (const event of events){
|
|
451
606
|
console.log(`${event.timestamp} ${event.type} ${JSON.stringify(event.payload)}`);
|
|
452
607
|
}
|
|
@@ -461,8 +616,12 @@ async function commandStop(cwd) {
|
|
|
461
616
|
console.log(`Marked stale Kavi session ${session.id} as stopped`);
|
|
462
617
|
return;
|
|
463
618
|
}
|
|
464
|
-
await
|
|
465
|
-
|
|
619
|
+
if (await pingRpc(paths)) {
|
|
620
|
+
await rpcShutdown(paths);
|
|
621
|
+
} else {
|
|
622
|
+
await appendCommand(paths, "shutdown", {});
|
|
623
|
+
await recordEvent(paths, session.id, "daemon.stop_requested", {});
|
|
624
|
+
}
|
|
466
625
|
await waitForSession(paths, "stopped");
|
|
467
626
|
console.log(`Stopped Kavi session ${session.id}`);
|
|
468
627
|
}
|
|
@@ -471,6 +630,51 @@ async function commandLand(cwd) {
|
|
|
471
630
|
const paths = resolveAppPaths(repoRoot);
|
|
472
631
|
const session = await loadSessionRecord(paths);
|
|
473
632
|
const targetBranch = await resolveTargetBranch(repoRoot, session.config.baseBranch);
|
|
633
|
+
const overlappingPaths = await findOverlappingWorktreePaths(session.worktrees, session.baseCommit);
|
|
634
|
+
if (overlappingPaths.length > 0) {
|
|
635
|
+
const existing = session.tasks.find((task)=>task.status === "pending" && task.title === "Resolve integration overlap");
|
|
636
|
+
if (!existing) {
|
|
637
|
+
const taskId = `integration-${Date.now()}`;
|
|
638
|
+
session.tasks.push(buildAdHocTask("codex", [
|
|
639
|
+
"Resolve overlapping worktree changes before landing.",
|
|
640
|
+
`Target branch: ${targetBranch}`,
|
|
641
|
+
"Overlapping paths:",
|
|
642
|
+
...overlappingPaths.map((item)=>`- ${item}`)
|
|
643
|
+
].join("\n"), taskId, {
|
|
644
|
+
title: "Resolve integration overlap",
|
|
645
|
+
routeReason: "Created by kavi land because multiple agents changed the same paths.",
|
|
646
|
+
claimedPaths: overlappingPaths
|
|
647
|
+
}));
|
|
648
|
+
upsertPathClaim(session, {
|
|
649
|
+
taskId,
|
|
650
|
+
agent: "codex",
|
|
651
|
+
source: "integration",
|
|
652
|
+
paths: overlappingPaths,
|
|
653
|
+
note: "Integration overlap detected during landing."
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
addDecisionRecord(session, {
|
|
657
|
+
kind: "integration",
|
|
658
|
+
agent: "codex",
|
|
659
|
+
summary: "Landing blocked by overlapping worktree paths",
|
|
660
|
+
detail: overlappingPaths.join(", "),
|
|
661
|
+
metadata: {
|
|
662
|
+
targetBranch,
|
|
663
|
+
overlappingPaths
|
|
664
|
+
}
|
|
665
|
+
});
|
|
666
|
+
await saveSessionRecord(paths, session);
|
|
667
|
+
await recordEvent(paths, session.id, "land.overlap_detected", {
|
|
668
|
+
targetBranch,
|
|
669
|
+
overlappingPaths
|
|
670
|
+
});
|
|
671
|
+
console.log("Landing blocked because both agent worktrees changed overlapping paths.");
|
|
672
|
+
console.log("Queued integration task for codex:");
|
|
673
|
+
for (const filePath of overlappingPaths){
|
|
674
|
+
console.log(`- ${filePath}`);
|
|
675
|
+
}
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
474
678
|
const result = await landBranches(repoRoot, targetBranch, session.worktrees, session.config.validationCommand, session.id, paths.integrationRoot);
|
|
475
679
|
await recordEvent(paths, session.id, "land.completed", {
|
|
476
680
|
targetBranch,
|
|
@@ -517,6 +721,7 @@ async function commandHook(args) {
|
|
|
517
721
|
toolName: descriptor.toolName,
|
|
518
722
|
summary: descriptor.summary
|
|
519
723
|
});
|
|
724
|
+
await notifyOperatorSurface(paths, "approval.auto_allowed");
|
|
520
725
|
console.log(JSON.stringify({
|
|
521
726
|
continue: true,
|
|
522
727
|
suppressOutput: true,
|
|
@@ -541,6 +746,7 @@ async function commandHook(args) {
|
|
|
541
746
|
decision: rule.decision,
|
|
542
747
|
summary: descriptor.summary
|
|
543
748
|
});
|
|
749
|
+
await notifyOperatorSurface(paths, "approval.auto_decided");
|
|
544
750
|
console.log(JSON.stringify({
|
|
545
751
|
continue: true,
|
|
546
752
|
suppressOutput: true,
|
|
@@ -565,6 +771,7 @@ async function commandHook(args) {
|
|
|
565
771
|
toolName: request.toolName,
|
|
566
772
|
summary: request.summary
|
|
567
773
|
});
|
|
774
|
+
await notifyOperatorSurface(paths, "approval.requested");
|
|
568
775
|
const resolved = await waitForApprovalDecision(paths, request.id);
|
|
569
776
|
const approved = resolved?.status === "approved";
|
|
570
777
|
const denied = resolved?.status === "denied";
|
|
@@ -573,6 +780,7 @@ async function commandHook(args) {
|
|
|
573
780
|
requestId: request.id,
|
|
574
781
|
outcome: approved ? "approved" : denied ? "denied" : "expired"
|
|
575
782
|
});
|
|
783
|
+
await notifyOperatorSurface(paths, "approval.completed");
|
|
576
784
|
console.log(JSON.stringify({
|
|
577
785
|
continue: true,
|
|
578
786
|
suppressOutput: true,
|
|
@@ -586,6 +794,7 @@ async function commandHook(args) {
|
|
|
586
794
|
}
|
|
587
795
|
if (session) {
|
|
588
796
|
await recordEvent(paths, session.id, "claude.hook", hookPayload);
|
|
797
|
+
await notifyOperatorSurface(paths, "claude.hook");
|
|
589
798
|
}
|
|
590
799
|
console.log(JSON.stringify({
|
|
591
800
|
continue: true
|
|
@@ -630,6 +839,12 @@ async function main() {
|
|
|
630
839
|
case "task-output":
|
|
631
840
|
await commandTaskOutput(cwd, args);
|
|
632
841
|
break;
|
|
842
|
+
case "decisions":
|
|
843
|
+
await commandDecisions(cwd, args);
|
|
844
|
+
break;
|
|
845
|
+
case "claims":
|
|
846
|
+
await commandClaims(cwd, args);
|
|
847
|
+
break;
|
|
633
848
|
case "approvals":
|
|
634
849
|
await commandApprovals(cwd, args);
|
|
635
850
|
break;
|
package/dist/paths.js
CHANGED
|
@@ -23,7 +23,7 @@ export function resolveAppPaths(repoRoot) {
|
|
|
23
23
|
approvalsFile: path.join(stateDir, "approvals.json"),
|
|
24
24
|
commandsFile: path.join(runtimeDir, "commands.jsonl"),
|
|
25
25
|
claudeSettingsFile: path.join(runtimeDir, "claude.settings.json"),
|
|
26
|
-
socketPath: path.join(
|
|
26
|
+
socketPath: path.join(homeStateDir, "sockets", `${safeRepoId}.sock`),
|
|
27
27
|
homeConfigDir,
|
|
28
28
|
homeConfigFile: path.join(homeConfigDir, "config.toml"),
|
|
29
29
|
homeApprovalRulesFile: path.join(homeConfigDir, "approval-rules.json"),
|
package/dist/prompts.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileExists } from "./fs.js";
|
|
4
|
+
export async function loadAgentPrompt(paths, agent) {
|
|
5
|
+
const promptPath = path.join(paths.promptsDir, `${agent}.md`);
|
|
6
|
+
if (!await fileExists(promptPath)) {
|
|
7
|
+
return "";
|
|
8
|
+
}
|
|
9
|
+
return (await fs.readFile(promptPath, "utf8")).trim();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
//# sourceURL=prompts.ts
|