Skip to content

feat(agent-manager): allow sessions to move their worktree between sections or ungroup - #12815

Merged
marius-kilocode merged 4 commits into
mainfrom
allow-agent-manager-to-reassign-session-sections
Aug 3, 2026
Merged

feat(agent-manager): allow sessions to move their worktree between sections or ungroup#12815
marius-kilocode merged 4 commits into
mainfrom
allow-agent-manager-to-reassign-session-sections

Conversation

@marius-kilocode

Copy link
Copy Markdown
Collaborator

The Agent Manager tool now supports a move action so a session can reassign its own worktree to another section or ungroup it by passing sectionID: null. The move operation validates the target section and the session's worktree, then pushes refreshed state to the panel.

The model-facing contract is now explicit: action: "list" is required first, its result is the source of truth for section and session IDs, and direct edits to .kilo/agent-manager.json are rejected by edit, write, and apply_patch with an explicit pointer to the agent_manager tool.

Changes

  • New MoveRequest/MoveResult in the Agent Manager protocol
  • New move domain function with section/session validation
  • New restoreAgentManagerNulls OpenAPI hook so sectionID stays nullable in the generated SDK
  • Tool schema descriptions now spell out the list-first workflow and the no-direct-edit rule
  • List output now includes an instruction block and is pretty-printed
  • Optional nullable Task fields tolerate null from models
  • Focused CLI tests, bridge unit test, and protection test

Self-test

Agent Manager session moves its own worktree out of a section using the tool

…ctions or ungroup

Add a new  to the  tool so a session can
reassign its own worktree to another section or ungroup it by passing
. The move operation validates the target section and
the session's worktree, then pushes refreshed state to the panel.

Also tighten the model-facing contract so the list-to-move workflow is
unambiguous:  is required first, its result is the
source of truth for section and session IDs, and direct edits to
 are rejected by , , and
 with an explicit pointer to the  tool.

- New / in the Agent Manager protocol
- New  domain function with section/session validation
- New  OpenAPI hook so  stays
  nullable in the generated SDK
- Tool schema descriptions now spell out the list-first workflow and
  the no-direct-edit rule
- List output now includes an instruction block and is pretty-printed
- Optional nullable Task fields tolerate  from models
- Focused CLI tests, bridge unit test, and protection test
@marius-kilocode
marius-kilocode enabled auto-merge (squash) August 3, 2026 13:53
import * as path from "path"

export function assertMutablePath(filepath: string) {
const parts = filepath.split(path.sep)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Splitting on path.sep without normalizing lets the guard be bypassed

edit.ts and write.ts pass params.filePath through verbatim when it is absolute (path.isAbsolute(params.filePath) ? params.filePath : path.join(...)), so assertMutablePath can receive an unnormalized path:

  • On Windows, path.sep is \, but a model commonly emits forward slashes. C:/proj/.kilo/agent-manager.json splits into a single element, file becomes the whole string, and the guard returns early — the write goes through.
  • On POSIX, /proj/.kilo/./agent-manager.json yields dir === ".", and /proj/.kilo//agent-manager.json yields dir === "". Both bypass the check.

(apply_patch.ts is fine because it resolves the path first.) Normalizing fixes all three cases:

Suggested change
const parts = filepath.split(path.sep)
const parts = path.normalize(filepath).split(path.sep)

Reply with @kilocode-bot fix it to have Kilo Code address this issue.

const parts = filepath.split(path.sep)
const file = parts.at(-1)
const dir = parts.at(-2)
if (file !== "agent-manager.json" || ![".kilo", ".kilocode"].includes(dir ?? "")) return

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: This guard is global and unconditional, with no escape hatch

Two things worth confirming are intended:

  1. The block applies to every Kilo surface, including the TUI and kilo run where Agent Manager does not exist, and a user cannot opt out. .kilo/agent-manager.json is documented as recoverable state (packages/kilo-docs/pages/automate/agent-manager.md), so an explicit request like "the JSON in .kilo/agent-manager.json is corrupted, fix it" now hard-fails with a message pointing at action=move, which cannot repair malformed JSON. Consider routing this through a permission ask instead of an unconditional throw.
  2. edit/write/apply_patch are covered, but bash is not — echo ... > .kilo/agent-manager.json still succeeds. Fine for a nudge-level guardrail, but it means this should not be treated as an actual invariant.

Reply with @kilocode-bot fix it to have Kilo Code address this issue.

const bus = yield* Bus.Service
const host = yield* AgentManager.Service
const provider = yield* Provider.Service
const wire = ToolJsonSchema.fromSchema(WireParams)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: This mutates the schema object cached inside ToolJsonSchema.fromSchema

fromSchema memoizes results in a module-level WeakMap<Schema.Top, JSONSchema7> and returns the same object reference on every call (packages/opencode/src/tool/json-schema.ts:6-21). Since WireParams is a module-level constant, assigning to wire.properties.sectionID writes into the shared cache entry, so any future fromSchema(WireParams) caller sees the patched schema.

It is harmless today (the patch is idempotent and nothing else reads WireParams), but it is fragile. Copying before patching keeps the cache clean:

const base = ToolJsonSchema.fromSchema(WireParams)
const wire = { ...base, properties: { ...base.properties, sectionID: { anyOf: [...], description: "..." } } }

A short comment on why the patch is needed would also help: normalize strips the { type: "null" } arm from any property absent from required, which is exactly what makes Schema.optional(Schema.NullOr(...)) lose its null here. Without that note the block reads as arbitrary.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

sessionID: SessionID.annotate({
description: "Session ID returned by action=list. Do not use a worktree name, branch, or section name.",
}),
sectionID: Schema.NullOr(Schema.String).annotate({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: sectionID bounds disagree with the wire schema and the protocol

Three places describe the same field differently:

  • Here: Schema.NullOr(Schema.String) — accepts "" and arbitrarily long strings.
  • The hand-patched JSON Schema (line 288): advertises minLength: 1.
  • protocol.ts: MoveRequest.sectionID is Schema.NullOr(ID) where ID = Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(200)).

So sectionID: "" passes tool validation but violates both the advertised schema and the protocol contract, surfacing only later. Mirroring the protocol bound keeps all three aligned and turns it into a clean parameter error:

Suggested change
sectionID: Schema.NullOr(Schema.String).annotate({
sectionID: Schema.NullOr(Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(200))).annotate({

Separately: MoveParams.sectionID is required while WireParams.sectionID is Schema.optional(...), so the advertised schema lets a model call action: "move" with no sectionID and then fail the whole Params union with a confusing multi-arm error. Making it Schema.optional(Schema.NullOr(...)) (treating omitted as ungroup, same as null) would be more forgiving and consistent with the rest of this PR's null tolerance.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

...result.overview,
},
null,
2,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: Pretty-printing the overview meaningfully inflates tool output

The overview can hold up to 100 sections × 100 worktrees plus 100 local sessions (see the isMaxLength checks in protocol.ts). Switching from compact JSON.stringify to 2-space indentation roughly doubles the byte count for the same data, and tool output is auto-truncated — so on a large workspace this makes it more likely the tail (ungrouped, local.sessions) gets cut off, which is exactly the data action=move needs.

Since the instructions block already tells the model where the IDs live, dropping null, 2 keeps the payload compact without hurting readability; models parse minified JSON fine.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

@kilo-code-bot

kilo-code-bot Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: 5 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 2
SUGGESTION 3
Issue Details (click to expand)

WARNING

File Line Issue
packages/opencode/src/kilocode/agent-manager/protection.ts 4 filepath.split(path.sep) on an unnormalized path — edit/write pass absolute paths verbatim, so C:/…/.kilo/agent-manager.json (Windows forward slashes), /…/.kilo/./agent-manager.json, and /…/.kilo//agent-manager.json all bypass the guard
packages/opencode/src/kilocode/tool/agent-manager.ts 284 Patches wire.properties.sectionID in place, mutating the WeakMap-cached schema object returned by ToolJsonSchema.fromSchema

SUGGESTION

File Line Issue
packages/opencode/src/kilocode/agent-manager/protection.ts 7 Unconditional global block (TUI / kilo run too) with no escape hatch, blocking documented agent-manager.json recovery; bash still bypasses it
packages/opencode/src/kilocode/tool/agent-manager.ts 101 sectionID bounds disagree across MoveParams (unbounded), the patched JSON Schema (minLength: 1), and protocol ID (1–200); also required here but optional on the wire
packages/opencode/src/kilocode/tool/agent-manager.ts 325 2-space pretty-printing roughly doubles overview output size, increasing truncation risk for the ungrouped / local.sessions tail the model needs for move
Files Reviewed (20 files)
  • packages/opencode/src/kilocode/agent-manager/protection.ts - 2 issues
  • packages/opencode/src/kilocode/tool/agent-manager.ts - 3 issues
  • packages/opencode/src/kilocode/agent-manager/protocol.ts
  • packages/opencode/src/kilocode/permission/agent-manager.ts
  • packages/opencode/src/kilocode/tool/agent-manager.txt
  • packages/opencode/src/server/routes/instance/httpapi/public.ts
  • packages/opencode/src/tool/apply_patch.ts
  • packages/opencode/src/tool/edit.ts
  • packages/opencode/src/tool/write.ts
  • packages/opencode/test/kilocode/agent-manager-protection.test.ts
  • packages/opencode/test/kilocode/agent-manager-tool.test.ts
  • packages/opencode/test/kilocode/permission/agent-manager-prompt.test.ts
  • packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts
  • packages/kilo-vscode/src/agent-manager/orchestration-bridge.ts
  • packages/kilo-vscode/src/agent-manager/orchestration-domain.ts
  • packages/kilo-vscode/tests/unit/agent-manager-orchestration-bridge.test.ts
  • packages/kilo-docs/pages/automate/agent-manager.md
  • packages/sdk/js/src/v2/gen/types.gen.ts
  • packages/sdk/openapi.json
  • .changeset/quiet-agent-sections.md
Verified as correct (click to expand)
  • restoreAgentManagerNulls runs after the component-level stripOptionalNull pass and before the per-operation passes, and the generated SDK correctly emits sectionID: string | null for both AgentManagerMoveRequest and AgentManagerMoveResult.
  • The move branch in orchestration-bridge.ts intentionally skips the options.managed(...) check; move() already requires state.getSession(...), and admit() has already verified the request directory belongs to this workspace, so there is no cross-workspace gap.
  • moveToSection(worktreeIds, sectionId: string | null) already accepts null and expands multi-version siblings, matching the documented behavior.
  • No new unbounded collections or subscriptions were introduced in the bridge; push() is a plain callback and the existing RETAINED caps are unchanged.
  • AgentManagerProvider.ts is at 1837 lines, still under the 1900 maxLines cap in agent-manager-arch.test.ts.

Fix these issues in Kilo Cloud


Reviewed by claude-opus-5 · Input: 80 · Output: 26.4K · Cached: 5.1M

Review guidance: REVIEW.md from base branch main

@marius-kilocode
marius-kilocode merged commit 3d4294e into main Aug 3, 2026
33 checks passed
@marius-kilocode
marius-kilocode deleted the allow-agent-manager-to-reassign-session-sections branch August 3, 2026 14:32
t7tran pushed a commit to t7tran/kilocode that referenced this pull request Aug 14, 2026
…ctions or ungroup (Kilo-Org#12815)

* feat(agent-manager): allow sessions to move their worktree between sections or ungroup

Add a new  to the  tool so a session can
reassign its own worktree to another section or ungroup it by passing
. The move operation validates the target section and
the session's worktree, then pushes refreshed state to the panel.

Also tighten the model-facing contract so the list-to-move workflow is
unambiguous:  is required first, its result is the
source of truth for section and session IDs, and direct edits to
 are rejected by , , and
 with an explicit pointer to the  tool.

- New / in the Agent Manager protocol
- New  domain function with section/session validation
- New  OpenAPI hook so  stays
  nullable in the generated SDK
- Tool schema descriptions now spell out the list-first workflow and
  the no-direct-edit rule
- List output now includes an instruction block and is pretty-printed
- Optional nullable Task fields tolerate  from models
- Focused CLI tests, bridge unit test, and protection test

* chore: remove local PR screenshot

* style(vscode): format agent manager orchestration

* fix(agent-manager): protect state paths on Windows
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants