@mandipadk7/kavi 0.1.7 → 1.0.1

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 CHANGED
@@ -9,16 +9,20 @@ Current capabilities:
9
9
  - `kavi init --no-commit`: skip the bootstrap commit and let `kavi open` or `kavi start` create the first base commit later.
10
10
  - `kavi doctor`: verify Node, Codex, Claude, git worktree support, and local readiness.
11
11
  - `kavi update`: check for and install a newer published Kavi package from npm.
12
- - `kavi start`: start a managed session without attaching the TUI.
13
- - `kavi open`: create a managed session with separate Codex and Claude worktrees and open the full-screen operator console, even from an empty folder or a repo with no `HEAD` yet.
12
+ - `kavi start`: start a managed session without attaching the TUI. Add `--approve-all` to run future Codex and Claude turns with full access and without Kavi approval prompts.
13
+ - `kavi open`: create a managed session with separate Codex and Claude worktrees and open the full-screen operator console, even from an empty folder or a repo with no `HEAD` yet. Add `--approve-all` to start in full-access mode.
14
14
  - `kavi resume`: reopen the operator console for the current repo session.
15
+ - `kavi summary`: show a cohesive session-level view of progress, current changes, recent activity, and landing readiness.
16
+ - `kavi result`: show the current or latest landed outcome as one cohesive result surface, including per-agent output and merged landing details.
15
17
  - `kavi status`: inspect session health, task counts, and configured routing ownership rules from any terminal.
18
+ - `kavi activity`: show the session as a linear activity stream instead of raw daemon events.
16
19
  - `kavi route`: preview how Kavi would route a prompt before enqueuing it.
17
20
  - `kavi routes`: inspect recent task routing decisions with strategy, confidence, and metadata.
18
21
  - `kavi paths`: inspect resolved repo-local, user-local, worktree, and runtime paths.
19
22
  - `kavi task`: enqueue a task for `codex`, `claude`, or `auto` routing.
20
- - `kavi recommend`: inspect integration, handoff, and ownership-configuration recommendations derived from live claims, reviews, and routing state.
23
+ - `kavi recommend`: inspect integration, handoff, and ownership-configuration recommendations derived from live claims, reviews, and routing state, with filters for kind, target agent, and active vs dismissed status.
21
24
  - `kavi recommend-apply`: turn an actionable handoff or integration recommendation into a queued managed task.
25
+ - `kavi recommend-dismiss` and `kavi recommend-restore`: manage the recommendation inbox without losing the underlying session context.
22
26
  - `kavi tasks`: inspect the session task list with summaries and artifact availability.
23
27
  - `kavi task-output`: inspect the normalized envelope and raw output for a completed task.
24
28
  - `kavi decisions`: inspect the persisted routing, approval, task, and integration decisions.
@@ -37,8 +41,10 @@ Runtime model:
37
41
  - User-local runtime overrides live in `~/.config/kavi/config.toml` and can point Kavi at custom `node`, `codex`, and `claude` binaries.
38
42
  - The operator surface talks to the daemon over a local control socket under the machine-local state root.
39
43
  - SQLite event mirroring is opt-in with `KAVI_ENABLE_SQLITE_HISTORY=1`; the default event log is JSONL under `.kavi/state/events.jsonl`.
40
- - The operator console exposes a task board, dual agent lanes, a live inspector pane, approval actions, inline task composition, worktree diff review with file and hunk navigation, and persisted operator review threads on files or hunks.
44
+ - Landed result reports are persisted under `.kavi/state/reports/`, so the latest merged outcome remains inspectable after the landing command exits.
45
+ - The operator console now opens on an activity-first view so the session reads like a linear progress log, while still exposing dedicated results, task board, dual agent lanes, approvals, diff review, and persisted operator review threads when you need to drill in.
41
46
  - Routing can now use explicit path ownership rules from `.kavi/config.toml` via `[routing].codex_paths` and `[routing].claude_paths`, so known parts of the tree can bypass looser keyword or AI routing.
47
+ - Validation is optional by default for new repos. Set `validation_command` in `.kavi/config.toml` once the project has a real test or build command you want Kavi to enforce during `land`.
42
48
  - Ownership routing now prefers the strongest matching rule, not just the side with the most raw glob hits, and route metadata records the winning rule when one exists.
43
49
  - Diff-based path claims now release older overlapping same-agent claims automatically, so the active claim set stays closer to each managed worktree's current unlanded surface.
44
50
 
@@ -46,27 +52,35 @@ Notes:
46
52
  - `kavi init` and `kavi open` now support the "empty folder to first managed session" path. If no git repo exists, Kavi initializes one; if git exists but no `HEAD` exists yet, Kavi creates the bootstrap commit it needs for worktrees.
47
53
  - Codex runs through `codex app-server` in managed mode, so Codex-side approvals now land in the same Kavi inbox as Claude hook approvals.
48
54
  - Claude still runs through the installed `claude` CLI with Kavi-managed hooks and approval decisions.
55
+ - `--approve-all` and the operator console's `!` toggle switch Kavi into a full-access mode for future turns. In that mode, Claude runs with `--dangerously-skip-permissions`, Codex runs with its no-approval danger-full-access equivalent, and Kavi stops prompting for approvals on those future turns.
49
56
  - `kavi doctor` now checks Claude auth readiness with `claude auth status`, and startup blocks if Claude is installed but not authenticated.
50
57
  - `kavi doctor` also validates ownership path rules for duplicates, repo escapes, and absolute-path mistakes before those rules affect routing.
51
58
  - `kavi doctor` now also flags overlapping cross-agent ownership rules that do not produce a clear specificity winner.
52
59
  - The dashboard and operator commands now use the daemon's local RPC socket instead of editing session files directly, and the TUI stays updated from pushed daemon snapshots rather than polling.
53
60
  - The socket is machine-local rather than repo-local to avoid Unix socket path-length issues in deep repos and temp directories.
54
- - The console is keyboard-driven: `1-7` switch views, `j/k` move selection, `[` and `]` cycle task detail sections, `,` and `.` cycle changed files, `{` and `}` cycle patch hunks, `A/C/Q/M` add review notes, `o/O` cycle existing threads, `T` reply, `E` edit, `R` resolve or reopen, `a` cycle thread assignee, `w` mark a thread as won't-fix, `x` mark it as accepted-risk, `F` queue a fix task, `H` queue a handoff task, `y/n` resolve approvals, and `c` opens the inline task composer with live route preview diagnostics.
61
+ - The console is keyboard-driven: `1-9` switch views, `j/k` move selection, `[` and `]` cycle task detail sections, `,` and `.` cycle changed files, `{` and `}` cycle patch hunks, `A/C/Q/M` add review notes, `o/O` cycle existing threads, `T` reply, `E` edit, `R` resolve or reopen, `a` cycle thread assignee, `w` mark a thread as won't-fix, `x` mark it as accepted-risk, `F` queue a fix task, `H` queue a handoff task, `y/n` resolve approvals, `!` toggles full-access mode, and `c` opens the inline task composer with live route preview diagnostics. Inside the composer, digits are treated as literal text and `Tab` cycles the route owner.
55
62
  - Review filters are available both in the CLI and the TUI: `kavi reviews --assignee operator --status open`, and inside the console use `u`, `v`, and `d` to cycle assignee, status, and disposition filters for the active diff context.
56
63
  - The claims inspector now shows active overlap hotspots and ownership-rule conflicts so routing pressure points are visible directly in the operator surface.
57
64
  - The claims and decision inspectors now also show recommendation-driven next actions, so hotspots, cross-agent review pressure, and ownership-config problems can be turned into follow-up tasks directly from the operator surface.
65
+ - Recommendations now have a persisted lifecycle of their own: they can be dismissed, restored, and tracked across follow-up tasks, and repeated applies are guarded when an open follow-up task already exists.
58
66
  - Review threads now carry explicit assignees and richer dispositions, including `accepted risk` and `won't fix`, instead of relying only on free-form note text.
59
67
  - Successful follow-up tasks now auto-resolve linked open review threads, landed follow-up work marks those resolved threads as landed, and replying to a resolved thread reopens it.
60
68
  - `kavi update --check` reports newer published builds without installing them, and `kavi update` can apply a chosen `latest`, `beta`, or exact version after confirmation.
69
+ - The operator console now includes a dedicated recommendations view. Use `4` to switch there, `Enter` to apply a recommendation, `P` to force an additional follow-up task when one is already open, `z` to dismiss, and `Z` to restore.
70
+ - `kavi land` now prints a clearer merged-result summary, including the pre-land change surface by agent, validation status, and how many review threads were landed as part of the merge.
71
+ - After `kavi land`, run `kavi result` to inspect the persisted merged-result report and the latest per-agent outcome in one place.
61
72
 
62
- Install commands for testers:
73
+ Install commands:
63
74
 
64
75
  ```bash
76
+ # stable
77
+ npm install -g @mandipadk7/kavi
78
+
65
79
  # beta channel
66
80
  npm install -g @mandipadk7/kavi@beta
67
81
 
68
82
  # one-off
69
- npx @mandipadk7/kavi@beta help
83
+ npx @mandipadk7/kavi help
70
84
  ```
71
85
 
72
86
  User-local config example:
@@ -135,9 +135,33 @@ export async function writeClaudeSettings(paths, session) {
135
135
  "--event",
136
136
  event
137
137
  ]);
138
+ const toolHooks = session.fullAccessMode ? {} : {
139
+ PreToolUse: [
140
+ {
141
+ matcher: "Bash|Edit|Write|MultiEdit",
142
+ hooks: [
143
+ {
144
+ type: "command",
145
+ command: buildHookCommand("PreToolUse")
146
+ }
147
+ ]
148
+ }
149
+ ],
150
+ PostToolUse: [
151
+ {
152
+ matcher: "Bash|Edit|Write|MultiEdit",
153
+ hooks: [
154
+ {
155
+ type: "command",
156
+ command: buildHookCommand("PostToolUse")
157
+ }
158
+ ]
159
+ }
160
+ ]
161
+ };
138
162
  const settings = {
139
163
  permissions: {
140
- defaultMode: "plan"
164
+ defaultMode: session.fullAccessMode ? "bypassPermissions" : "plan"
141
165
  },
142
166
  hooks: {
143
167
  SessionStart: [
@@ -151,28 +175,7 @@ export async function writeClaudeSettings(paths, session) {
151
175
  ]
152
176
  }
153
177
  ],
154
- PreToolUse: [
155
- {
156
- matcher: "Bash|Edit|Write|MultiEdit",
157
- hooks: [
158
- {
159
- type: "command",
160
- command: buildHookCommand("PreToolUse")
161
- }
162
- ]
163
- }
164
- ],
165
- PostToolUse: [
166
- {
167
- matcher: "Bash|Edit|Write|MultiEdit",
168
- hooks: [
169
- {
170
- type: "command",
171
- command: buildHookCommand("PostToolUse")
172
- }
173
- ]
174
- }
175
- ],
178
+ ...toolHooks,
176
179
  Notification: [
177
180
  {
178
181
  matcher: "permission_prompt|idle_prompt|auth_success",
@@ -202,35 +205,48 @@ export async function writeClaudeSettings(paths, session) {
202
205
  };
203
206
  await fs.writeFile(paths.claudeSettingsFile, `${JSON.stringify(settings, null, 2)}\n`, "utf8");
204
207
  }
205
- export async function runClaudeTask(session, task, paths) {
206
- const worktree = findWorktree(session, "claude");
207
- const claudeSessionId = session.agentStatus.claude.sessionId ?? `${session.id}-claude`;
208
- await writeClaudeSettings(paths, session);
209
- const repoPrompt = await loadAgentPrompt(paths, "claude");
210
- const prompt = [
211
- buildAgentInstructions("claude", worktree.path, repoPrompt),
212
- "",
213
- buildTaskPrompt(session, task, "claude")
214
- ].join("\n");
215
- const result = await runCommand(session.runtime.claudeExecutable, [
208
+ export function resolveClaudeSessionId(session) {
209
+ return session.agentStatus.claude.sessionId ?? session.id;
210
+ }
211
+ export function buildClaudeCommandArgs(session, claudeSessionId, prompt, settingsPath) {
212
+ const sessionArgs = session.agentStatus.claude.sessionId ? [
213
+ "--resume",
214
+ claudeSessionId
215
+ ] : [
216
+ "--session-id",
217
+ claudeSessionId
218
+ ];
219
+ return [
216
220
  "-p",
217
221
  "--output-format",
218
222
  "json",
219
223
  "--json-schema",
220
224
  CLAUDE_ENVELOPE_SCHEMA,
221
225
  "--settings",
222
- paths.claudeSettingsFile,
223
- "--permission-mode",
224
- "plan",
225
- ...session.agentStatus.claude.sessionId ? [
226
- "--resume",
227
- claudeSessionId
226
+ settingsPath,
227
+ ...session.fullAccessMode ? [
228
+ "--permission-mode",
229
+ "bypassPermissions",
230
+ "--dangerously-skip-permissions"
228
231
  ] : [
229
- "--session-id",
230
- claudeSessionId
232
+ "--permission-mode",
233
+ "plan"
231
234
  ],
235
+ ...sessionArgs,
232
236
  prompt
233
- ], {
237
+ ];
238
+ }
239
+ export async function runClaudeTask(session, task, paths) {
240
+ const worktree = findWorktree(session, "claude");
241
+ const claudeSessionId = resolveClaudeSessionId(session);
242
+ await writeClaudeSettings(paths, session);
243
+ const repoPrompt = await loadAgentPrompt(paths, "claude");
244
+ const prompt = [
245
+ buildAgentInstructions("claude", worktree.path, repoPrompt),
246
+ "",
247
+ buildTaskPrompt(session, task, "claude")
248
+ ].join("\n");
249
+ const result = await runCommand(session.runtime.claudeExecutable, buildClaudeCommandArgs(session, claudeSessionId, prompt, paths.claudeSettingsFile), {
234
250
  cwd: worktree.path
235
251
  });
236
252
  const rawOutput = result.code === 0 ? result.stdout : `${result.stdout}\n${result.stderr}`;
@@ -128,19 +128,41 @@ function buildApprovalResponse(method, params, approved, remember) {
128
128
  throw new Error(`Unsupported Codex approval request: ${method}`);
129
129
  }
130
130
  }
131
- function buildThreadParams(session, worktree, developerInstructions) {
131
+ export function buildThreadParams(session, worktree, developerInstructions) {
132
132
  const configuredModel = session.config.agents.codex.model.trim();
133
133
  return {
134
134
  cwd: worktree.path,
135
- approvalPolicy: "on-request",
136
- approvalsReviewer: "user",
137
- sandbox: "workspace-write",
135
+ approvalPolicy: session.fullAccessMode ? "never" : "on-request",
136
+ sandbox: session.fullAccessMode ? "danger-full-access" : "workspace-write",
138
137
  baseInstructions: "You are Codex inside Kavi. Operate inside the assigned worktree and keep work task-scoped.",
139
138
  developerInstructions,
140
139
  model: configuredModel || null,
141
140
  ephemeral: false,
142
141
  experimentalRawEvents: false,
143
- persistExtendedHistory: true
142
+ persistExtendedHistory: true,
143
+ ...session.fullAccessMode ? {} : {
144
+ approvalsReviewer: "user"
145
+ }
146
+ };
147
+ }
148
+ export function buildCodexTurnParams(session, worktree, threadId, inputText) {
149
+ return {
150
+ threadId,
151
+ cwd: worktree.path,
152
+ approvalPolicy: session.fullAccessMode ? "never" : "on-request",
153
+ sandbox: session.fullAccessMode ? "danger-full-access" : "workspace-write",
154
+ model: session.config.agents.codex.model.trim() || null,
155
+ outputSchema: ENVELOPE_OUTPUT_SCHEMA,
156
+ input: [
157
+ {
158
+ type: "text",
159
+ text: inputText,
160
+ text_elements: []
161
+ }
162
+ ],
163
+ ...session.fullAccessMode ? {} : {
164
+ approvalsReviewer: "user"
165
+ }
144
166
  };
145
167
  }
146
168
  async function ensureThread(client, session, paths, worktree, developerInstructions) {
@@ -163,6 +185,16 @@ async function ensureThread(client, session, paths, worktree, developerInstructi
163
185
  }
164
186
  async function handleCodexApproval(session, paths, request) {
165
187
  const descriptor = describeCodexApprovalRequest(request.method, request.params);
188
+ if (session.fullAccessMode) {
189
+ await recordEvent(paths, session.id, "approval.full_access_bypassed", {
190
+ agent: "codex",
191
+ requestId: request.id,
192
+ method: request.method,
193
+ toolName: descriptor.toolName,
194
+ summary: descriptor.summary
195
+ });
196
+ return buildApprovalResponse(request.method, request.params, true, true);
197
+ }
166
198
  const rule = await findApprovalRule(paths, {
167
199
  repoRoot: session.repoRoot,
168
200
  agent: "codex",
@@ -218,21 +250,7 @@ export async function runCodexTask(session, task, paths) {
218
250
  try {
219
251
  await client.initialize();
220
252
  const threadId = await ensureThread(client, session, paths, worktree, developerInstructions);
221
- const result = await client.runTurn({
222
- threadId,
223
- cwd: worktree.path,
224
- approvalPolicy: "on-request",
225
- approvalsReviewer: "user",
226
- model: session.config.agents.codex.model.trim() || null,
227
- outputSchema: ENVELOPE_OUTPUT_SCHEMA,
228
- input: [
229
- {
230
- type: "text",
231
- text: buildTaskPrompt(session, task, "codex"),
232
- text_elements: []
233
- }
234
- ]
235
- });
253
+ const result = await client.runTurn(buildCodexTurnParams(session, worktree, threadId, buildTaskPrompt(session, task, "codex")));
236
254
  const rawOutput = `${result.assistantMessage}${result.stderr ? `\n\n[stderr]\n${result.stderr}` : ""}`;
237
255
  const envelope = extractJsonObject(result.assistantMessage);
238
256
  return {
@@ -1,4 +1,11 @@
1
1
  import { spawn } from "node:child_process";
2
+ export function buildCodexAppServerArgs() {
3
+ return [
4
+ "app-server",
5
+ "--listen",
6
+ "stdio://"
7
+ ];
8
+ }
2
9
  function asObject(value) {
3
10
  return value && typeof value === "object" && !Array.isArray(value) ? value : {};
4
11
  }
@@ -29,13 +36,7 @@ export class CodexAppServerClient {
29
36
  closePromise = null;
30
37
  constructor(runtime, cwd, onRequest){
31
38
  this.onRequest = onRequest;
32
- this.child = spawn(runtime.codexExecutable, [
33
- "app-server",
34
- "--listen",
35
- "stdio://",
36
- "--session-source",
37
- "cli"
38
- ], {
39
+ this.child = spawn(runtime.codexExecutable, buildCodexAppServerArgs(), {
39
40
  cwd,
40
41
  stdio: [
41
42
  "pipe",
package/dist/config.js CHANGED
@@ -4,7 +4,7 @@ import { ensureDir, fileExists } from "./fs.js";
4
4
  import { parseToml } from "./toml.js";
5
5
  const DEFAULT_CONFIG = `version = 1
6
6
  base_branch = "main"
7
- validation_command = "npm test"
7
+ validation_command = ""
8
8
  message_limit = 6
9
9
 
10
10
  [routing]
@@ -57,7 +57,7 @@ export function defaultConfig() {
57
57
  return {
58
58
  version: 1,
59
59
  baseBranch: "main",
60
- validationCommand: "npm test",
60
+ validationCommand: "",
61
61
  messageLimit: 6,
62
62
  routing: {
63
63
  frontendKeywords: [
@@ -109,6 +109,7 @@ export async function ensureProjectScaffold(paths) {
109
109
  await ensureDir(paths.kaviDir);
110
110
  await ensureDir(paths.promptsDir);
111
111
  await ensureDir(paths.stateDir);
112
+ await ensureDir(paths.reportsDir);
112
113
  await ensureDir(paths.runtimeDir);
113
114
  await ensureDir(paths.runsDir);
114
115
  if (!await fileExists(paths.configFile)) {
@@ -140,7 +141,7 @@ export async function loadConfig(paths) {
140
141
  return {
141
142
  version: asNumber(parsed.version, 1),
142
143
  baseBranch: asString(parsed.base_branch, "main"),
143
- validationCommand: asString(parsed.validation_command, "npm test"),
144
+ validationCommand: asString(parsed.validation_command, ""),
144
145
  messageLimit: asNumber(parsed.message_limit, 6),
145
146
  routing: {
146
147
  frontendKeywords: asStringArray(routing.frontend_keywords, defaultConfig().routing.frontendKeywords),
package/dist/daemon.js CHANGED
@@ -9,6 +9,8 @@ import { listApprovalRequests, resolveApprovalRequest } from "./approvals.js";
9
9
  import { addDecisionRecord, releaseSupersededClaims, upsertPathClaim } from "./decision-ledger.js";
10
10
  import { getWorktreeDiffReview, listWorktreeChangedPaths } from "./git.js";
11
11
  import { nowIso } from "./paths.js";
12
+ import { dismissOperatorRecommendation, recordRecommendationApplied, restoreOperatorRecommendation } from "./recommendations.js";
13
+ import { loadLatestLandReport } from "./reports.js";
12
14
  import { addReviewReply, addReviewNote, autoResolveReviewNotesForCompletedTask, linkReviewFollowUpTask, reviewNotesForTask, setReviewNoteStatus, updateReviewNote } from "./reviews.js";
13
15
  import { buildAdHocTask, buildKickoffTasks } from "./router.js";
14
16
  import { loadSessionRecord, readRecentEvents, recordEvent, saveSessionRecord } from "./session.js";
@@ -171,6 +173,20 @@ export class KaviDaemon {
171
173
  ok: true
172
174
  }
173
175
  };
176
+ case "dismissRecommendation":
177
+ await this.dismissRecommendationFromRpc(params);
178
+ return {
179
+ result: {
180
+ ok: true
181
+ }
182
+ };
183
+ case "restoreRecommendation":
184
+ await this.restoreRecommendationFromRpc(params);
185
+ return {
186
+ result: {
187
+ ok: true
188
+ }
189
+ };
174
190
  case "shutdown":
175
191
  return {
176
192
  result: {
@@ -185,6 +201,13 @@ export class KaviDaemon {
185
201
  ok: true
186
202
  }
187
203
  };
204
+ case "setFullAccessMode":
205
+ await this.setFullAccessModeFromRpc(params);
206
+ return {
207
+ result: {
208
+ ok: true
209
+ }
210
+ };
188
211
  case "taskArtifact":
189
212
  return {
190
213
  result: await this.getTaskArtifactFromRpc(params)
@@ -253,11 +276,13 @@ export class KaviDaemon {
253
276
  agent: worktree.agent,
254
277
  paths: await listWorktreeChangedPaths(worktree.path, session.baseCommit).catch(()=>[])
255
278
  })));
279
+ const latestLandReport = await loadLatestLandReport(this.paths);
256
280
  return {
257
281
  session,
258
282
  events,
259
283
  approvals,
260
- worktreeDiffs
284
+ worktreeDiffs,
285
+ latestLandReport
261
286
  };
262
287
  }
263
288
  async runMutation(fn) {
@@ -310,14 +335,58 @@ export class KaviDaemon {
310
335
  paths: task.claimedPaths,
311
336
  note: task.routeReason
312
337
  });
338
+ if (typeof params.recommendationId === "string") {
339
+ recordRecommendationApplied(this.session, params.recommendationId, taskId);
340
+ }
313
341
  await saveSessionRecord(this.paths, this.session);
314
342
  await recordEvent(this.paths, this.session.id, "task.enqueued", {
315
343
  owner,
316
- via: "rpc"
344
+ via: "rpc",
345
+ recommendationId: typeof params.recommendationId === "string" ? params.recommendationId : null
317
346
  });
347
+ if (typeof params.recommendationId === "string" && (params.recommendationKind === "handoff" || params.recommendationKind === "integration" || params.recommendationKind === "ownership-config")) {
348
+ await recordEvent(this.paths, this.session.id, "recommendation.applied", {
349
+ recommendationId: params.recommendationId,
350
+ recommendationKind: params.recommendationKind,
351
+ owner,
352
+ taskId
353
+ });
354
+ }
318
355
  await this.publishSnapshot("task.enqueued");
319
356
  });
320
357
  }
358
+ async dismissRecommendationFromRpc(params) {
359
+ await this.runMutation(async ()=>{
360
+ const recommendationId = typeof params.recommendationId === "string" ? params.recommendationId : "";
361
+ if (!recommendationId) {
362
+ throw new Error("dismissRecommendation requires a recommendationId.");
363
+ }
364
+ const reason = typeof params.reason === "string" ? params.reason : null;
365
+ const recommendation = dismissOperatorRecommendation(this.session, recommendationId, reason);
366
+ await saveSessionRecord(this.paths, this.session);
367
+ await recordEvent(this.paths, this.session.id, "recommendation.dismissed", {
368
+ recommendationId,
369
+ kind: recommendation.kind,
370
+ reason
371
+ });
372
+ await this.publishSnapshot("recommendation.dismissed");
373
+ });
374
+ }
375
+ async restoreRecommendationFromRpc(params) {
376
+ await this.runMutation(async ()=>{
377
+ const recommendationId = typeof params.recommendationId === "string" ? params.recommendationId : "";
378
+ if (!recommendationId) {
379
+ throw new Error("restoreRecommendation requires a recommendationId.");
380
+ }
381
+ const recommendation = restoreOperatorRecommendation(this.session, recommendationId);
382
+ await saveSessionRecord(this.paths, this.session);
383
+ await recordEvent(this.paths, this.session.id, "recommendation.restored", {
384
+ recommendationId,
385
+ kind: recommendation.kind
386
+ });
387
+ await this.publishSnapshot("recommendation.restored");
388
+ });
389
+ }
321
390
  async kickoffFromRpc(params) {
322
391
  await this.runMutation(async ()=>{
323
392
  const prompt = typeof params.prompt === "string" ? params.prompt : "";
@@ -383,6 +452,29 @@ export class KaviDaemon {
383
452
  await this.publishSnapshot("approval.resolved");
384
453
  });
385
454
  }
455
+ async setFullAccessModeFromRpc(params) {
456
+ await this.runMutation(async ()=>{
457
+ const enabled = params.enabled === true;
458
+ if (this.session.fullAccessMode === enabled) {
459
+ return;
460
+ }
461
+ this.session.fullAccessMode = enabled;
462
+ addDecisionRecord(this.session, {
463
+ kind: "approval",
464
+ agent: null,
465
+ summary: `${enabled ? "Enabled" : "Disabled"} approve-all mode`,
466
+ detail: enabled ? "Future Claude and Codex turns will run with full access and without Kavi approval prompts." : "Future Claude and Codex turns will return to standard approval and sandbox behavior.",
467
+ metadata: {
468
+ enabled
469
+ }
470
+ });
471
+ await saveSessionRecord(this.paths, this.session);
472
+ await recordEvent(this.paths, this.session.id, "session.full_access_mode_changed", {
473
+ enabled
474
+ });
475
+ await this.publishSnapshot("session.full_access_mode_changed");
476
+ });
477
+ }
386
478
  async getTaskArtifactFromRpc(params) {
387
479
  const taskId = typeof params.taskId === "string" ? params.taskId : "";
388
480
  if (!taskId) {
@@ -823,10 +915,22 @@ export class KaviDaemon {
823
915
  paths: task.claimedPaths,
824
916
  note: task.routeReason
825
917
  });
918
+ if (typeof command.payload.recommendationId === "string") {
919
+ recordRecommendationApplied(this.session, command.payload.recommendationId, taskId);
920
+ }
826
921
  await saveSessionRecord(this.paths, this.session);
827
922
  await recordEvent(this.paths, this.session.id, "task.enqueued", {
828
- owner
923
+ owner,
924
+ recommendationId: typeof command.payload.recommendationId === "string" ? command.payload.recommendationId : null
829
925
  });
926
+ if (typeof command.payload.recommendationId === "string" && (command.payload.recommendationKind === "handoff" || command.payload.recommendationKind === "integration" || command.payload.recommendationKind === "ownership-config")) {
927
+ await recordEvent(this.paths, this.session.id, "recommendation.applied", {
928
+ recommendationId: command.payload.recommendationId,
929
+ recommendationKind: command.payload.recommendationKind,
930
+ owner,
931
+ taskId
932
+ });
933
+ }
830
934
  await this.publishSnapshot("task.enqueued");
831
935
  }
832
936
  }
package/dist/git.js CHANGED
@@ -342,6 +342,32 @@ export async function listWorktreeChangedPaths(worktreePath, baseCommit) {
342
342
  ...parsePathList(untracked.stdout)
343
343
  ]);
344
344
  }
345
+ export async function resolveValidationPlan(integrationPath, validationCommand) {
346
+ const trimmed = validationCommand.trim();
347
+ if (!trimmed) {
348
+ return {
349
+ command: "",
350
+ status: "not_configured",
351
+ detail: "No validation command was configured."
352
+ };
353
+ }
354
+ if (trimmed === "npm test") {
355
+ try {
356
+ await fs.access(path.join(integrationPath, "package.json"));
357
+ } catch {
358
+ return {
359
+ command: trimmed,
360
+ status: "skipped",
361
+ detail: 'Skipped default validation command "npm test" because package.json is not present yet.'
362
+ };
363
+ }
364
+ }
365
+ return {
366
+ command: trimmed,
367
+ status: "ran",
368
+ detail: `Validation ran with "${trimmed}".`
369
+ };
370
+ }
345
371
  export async function getWorktreeDiffReview(agent, worktreePath, baseCommit, filePath) {
346
372
  const changedPaths = await listWorktreeChangedPaths(worktreePath, baseCommit);
347
373
  const selectedPath = filePath && changedPaths.includes(filePath) ? filePath : changedPaths[0] ?? null;
@@ -461,16 +487,20 @@ export async function landBranches(repoRoot, targetBranch, worktrees, validation
461
487
  throw new Error(merge.stderr.trim() || `Unable to merge branch ${worktree.branch} into integration branch ${integrationBranch}.`);
462
488
  }
463
489
  }
464
- if (validationCommand.trim()) {
465
- const validation = await runCommand("zsh", [
490
+ const validation = await resolveValidationPlan(integrationPath, validationCommand);
491
+ if (validation.status === "skipped") {
492
+ commandsRun.push(`SKIP ${validation.command} (${validation.detail})`);
493
+ }
494
+ if (validation.status === "ran") {
495
+ const validationRun = await runCommand("zsh", [
466
496
  "-lc",
467
- validationCommand
497
+ validation.command
468
498
  ], {
469
499
  cwd: integrationPath
470
500
  });
471
- commandsRun.push(validationCommand);
472
- if (validation.code !== 0) {
473
- throw new Error(`Validation command failed.\n${validation.stdout}\n${validation.stderr}`.trim());
501
+ commandsRun.push(validation.command);
502
+ if (validationRun.code !== 0) {
503
+ throw new Error(`Validation command failed.\n${validationRun.stdout}\n${validationRun.stderr}`.trim());
474
504
  }
475
505
  }
476
506
  const currentBranch = await getCurrentBranch(repoRoot).catch(()=>"");
@@ -500,10 +530,35 @@ export async function landBranches(repoRoot, targetBranch, worktrees, validation
500
530
  throw new Error(updateRef.stderr.trim() || `Unable to advance ${targetBranch}; it changed while landing was in progress.`);
501
531
  }
502
532
  }
533
+ const landedHead = await getBranchCommit(repoRoot, targetBranch);
534
+ for (const worktree of worktrees){
535
+ const reset = await runCommand("git", [
536
+ "reset",
537
+ "--hard",
538
+ landedHead
539
+ ], {
540
+ cwd: worktree.path
541
+ });
542
+ commandsRun.push(`git -C ${worktree.path} reset --hard ${landedHead}`);
543
+ if (reset.code !== 0) {
544
+ throw new Error(reset.stderr.trim() || `Unable to reset managed worktree ${worktree.path} to landed head ${landedHead}.`);
545
+ }
546
+ const clean = await runCommand("git", [
547
+ "clean",
548
+ "-fd"
549
+ ], {
550
+ cwd: worktree.path
551
+ });
552
+ commandsRun.push(`git -C ${worktree.path} clean -fd`);
553
+ if (clean.code !== 0) {
554
+ throw new Error(clean.stderr.trim() || `Unable to clean managed worktree ${worktree.path}.`);
555
+ }
556
+ }
503
557
  return {
504
558
  commandsRun,
505
559
  integrationBranch,
506
560
  integrationPath,
561
+ validation,
507
562
  snapshotCommits
508
563
  };
509
564
  }