@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 +21 -7
- package/dist/adapters/claude.js +59 -43
- package/dist/adapters/codex.js +38 -20
- package/dist/codex-app-server.js +8 -7
- package/dist/config.js +4 -3
- package/dist/daemon.js +107 -3
- package/dist/git.js +61 -6
- package/dist/main.js +378 -71
- package/dist/paths.js +2 -0
- package/dist/recommendations.js +205 -19
- package/dist/reports.js +118 -0
- package/dist/rpc.js +25 -1
- package/dist/session.js +14 -1
- package/dist/tui.js +385 -56
- package/dist/workflow.js +450 -0
- package/package.json +1 -1
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
|
-
-
|
|
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-
|
|
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
|
|
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
|
|
83
|
+
npx @mandipadk7/kavi help
|
|
70
84
|
```
|
|
71
85
|
|
|
72
86
|
User-local config example:
|
package/dist/adapters/claude.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
"--
|
|
227
|
-
claudeSessionId
|
|
226
|
+
settingsPath,
|
|
227
|
+
...session.fullAccessMode ? [
|
|
228
|
+
"--permission-mode",
|
|
229
|
+
"bypassPermissions",
|
|
230
|
+
"--dangerously-skip-permissions"
|
|
228
231
|
] : [
|
|
229
|
-
"--
|
|
230
|
-
|
|
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}`;
|
package/dist/adapters/codex.js
CHANGED
|
@@ -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
|
-
|
|
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 {
|
package/dist/codex-app-server.js
CHANGED
|
@@ -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 = "
|
|
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: "
|
|
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, "
|
|
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
|
-
|
|
465
|
-
|
|
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
|
-
|
|
497
|
+
validation.command
|
|
468
498
|
], {
|
|
469
499
|
cwd: integrationPath
|
|
470
500
|
});
|
|
471
|
-
commandsRun.push(
|
|
472
|
-
if (
|
|
473
|
-
throw new Error(`Validation command failed.\n${
|
|
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
|
}
|