diff --git a/AGENTS.md b/AGENTS.md index e11c909..de913e3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -63,9 +63,9 @@ packages/opencode/.opencode/ # Generated OpenCode output for review - For navigator-style commands, separate context loading, blocker checks, delegated execution, and final reporting into distinct workflow subsections so the control flow is easy to follow - Prefer explicit subsection names like `### Load ... Context`, `### Check Blockers`, `### Delegate ...`, and `### Mark Complete And Loop` when the command coordinates multiple phases or subagents - Treat loader tools and provided attachments as the source of truth for orchestration inputs; avoid extra exploratory commands when an existing tool result already answers the question -- Before delegating to a subagent, say what result should be stored and whether the workflow must stop, pause, or continue based on that result -- Use literal `` tags when the workflow must forward exact text as the next user message to a subagent session; `agent` is required, the block body is the exact rendered message to send, and slash commands belong on the first line of the body when needed -- Do not use `` blocks in command docs; author navigator delegation with `` blocks only +- Before dispatching a same-session command step, say what result should be stored and whether the workflow must stop, pause, or continue based on that result +- Use literal `` tags when the workflow must queue exact text through `session_command`; `agent` and `command` are required, and the block body is the exact rendered body to send for that command +- Do not use `` blocks in command docs; author navigator delegation with `` blocks only - When a command can pause for approval or loop over repeated work, describe the resume condition and the exact cases that must STOP without mutating state - Use `## Additional Context` for instructions about how optional guidance, related tickets, focus areas, or other stored context should influence analysis and response formatting - Use `### Output` as the final workflow step to define the exact user-facing response shape, including placeholders for generated values @@ -105,25 +105,23 @@ $ARGUMENTS ### Delegate Planning - -/ticket/plan + Task: Task context: Additional context: - + - Store the result as `` - STOP if planning is blocked or unusable ### Delegate Implementation - -/dev + Plan: Constraints: - + - STOP if implementation is blocked or incomplete @@ -136,13 +134,13 @@ Constraints: Example delegation rule: ```text -Before delegating, write the exact `...` block, say what result should be stored, and whether the workflow should continue or STOP based on that result. +Before dispatching, write the exact `...` block, say what queue acknowledgement should be stored, and whether the workflow should continue or STOP based on that acknowledgement. ``` -Example literal dispatch rule: +Example literal session command rule: ```text -Before literal command forwarding, write the exact `...` block, put the slash command on the first line of the body when needed, and say what result should be stored and whether the workflow should continue or STOP based on that result. +Before literal command forwarding, write the exact `...` block, then call `session_command` with the rendered body, exact `agent`, and exact `command`, and say what queue acknowledgement should be stored and whether the workflow should continue or STOP based on that acknowledgement. ``` ## Component Authoring diff --git a/README.md b/README.md index d91c7c6..99235eb 100644 --- a/README.md +++ b/README.md @@ -18,9 +18,9 @@ Kompass keeps AI coding agents on course with token-efficient, composable workfl ## Bundled Surface -- Commands cover direct work (`/ask`, `/commit`), orchestration (`/dev`, `/ship`, `/todo`), ticket planning/sync, and PR review/shipping flows. +- Commands cover direct work (`/ask`, `/commit`, `/merge`), orchestration (`/dev`, `/ship`, `/todo`), ticket planning/sync, and PR review/shipping flows. - Agents are intentionally narrow: `worker` is generic, `planner` is no-edit planning, `navigator` owns multi-step orchestration, and `reviewer` is a no-edit review specialist. -- Structured tools keep workflows grounded in repo and GitHub state: `changes_load`, `pr_load`, `pr_sync`, `ticket_load`, `ticket_sync`. +- Structured tools keep workflows grounded in repo and GitHub state: `changes_load`, `session_command` (resolve a slash command and queue it into the current session), `pr_load`, `pr_sync`, `ticket_load`, `ticket_sync`. - Reusable command-template components live in `packages/core/components/` and are documented in the components reference. ## Installation diff --git a/kompass.jsonc b/kompass.jsonc index 48c53fd..3c1ebf8 100644 --- a/kompass.jsonc +++ b/kompass.jsonc @@ -17,6 +17,7 @@ "commit-and-push": { "enabled": true }, "dev": { "enabled": true }, "learn": { "enabled": true }, + "merge": { "enabled": true }, "pr/create": { "enabled": true }, "pr/fix": { "enabled": true }, "pr/review": { "enabled": true }, @@ -40,6 +41,7 @@ "tools": { "changes_load": { "enabled": true }, + "session_command": { "enabled": true }, "pr_load": { "enabled": true }, "pr_sync": { "enabled": true }, "ticket_load": { "enabled": true }, diff --git a/kompass.schema.json b/kompass.schema.json index ef1f6e0..60cbee5 100644 --- a/kompass.schema.json +++ b/kompass.schema.json @@ -50,6 +50,9 @@ "learn": { "$ref": "#/$defs/commandConfig" }, + "merge": { + "$ref": "#/$defs/commandConfig" + }, "pr/create": { "$ref": "#/$defs/commandConfig" }, @@ -97,6 +100,7 @@ "commit-and-push", "dev", "learn", + "merge", "pr/create", "pr/fix", "pr/review", @@ -124,6 +128,7 @@ "commit-and-push", "dev", "learn", + "merge", "pr/create", "pr/fix", "pr/review", @@ -179,6 +184,9 @@ "changes_load": { "$ref": "#/$defs/toolConfig" }, + "session_command": { + "$ref": "#/$defs/toolConfig" + }, "pr_load": { "$ref": "#/$defs/toolConfig" }, @@ -353,6 +361,7 @@ "type": "string", "enum": [ "changes_load", + "session_command", "pr_load", "pr_sync", "ticket_sync", diff --git a/packages/core/agents/navigator.md b/packages/core/agents/navigator.md index 31cd368..2aead7d 100644 --- a/packages/core/agents/navigator.md +++ b/packages/core/agents/navigator.md @@ -1,41 +1,25 @@ -You are a navigation specialist for structured, multi-step workflows. +You coordinate structured, multi-step workflows. -## Operating Boundaries +## Rules - Follow the active command and provided context. -- Own the workflow yourself: decide the next step, load only the local context the command requires, dispatch when the command tells you to, and keep going until the command says to stop. -- Owning the workflow means managing step order, state, and stop conditions; it does not let you rewrite an explicit `` body. -- Delegate only explicit leaf tasks when the user explicitly requests a subagent or the command explicitly requires one. -- Gather only the context needed for the current step. -- Preserve workflow state, ordering, stop conditions, and approval gates across the whole command. -- Execute required user-interaction steps exactly as the active command defines them. -- If a required interaction tool is unavailable, follow the active command's non-interactive fallback instead of pausing or inventing a question. -- If a delegated step is blocked, incomplete, or fails, stop and report it clearly. - -## Dispatch Execution - -- Treat each `...` block as a literal message dispatch instruction. -- Dispatch blocks take precedence over generic delegation guidance; the rendered body is opaque. -- `agent` is required; invoke that exact subagent type. -- Set `prompt` to the dispatch body exactly after variable substitution. -- Do not add wrapper text or rewrite, summarize, interpret, expand, normalize, or improve the body. -- Preserve line breaks and ordering exactly. -- Send the rendered body as a real user turn to the target subagent session. -- Never infer what a slash command means when handling a dispatch block. Forward it literally. -- Process every valid dispatch block you receive. -- Run independent dispatch blocks in parallel only when the workflow makes that independence clear; otherwise run them sequentially in source order. -- If a dispatch block is malformed, report it as invalid, explain why briefly, and continue with remaining valid blocks when safe. -- If no valid dispatch blocks are present, continue with the command workflow. - -## Delegation - -- Treat delegated work as one step inside a larger workflow, not as a handoff of orchestration responsibility. -- For an explicit `` step, your job is only to render variables, send the exact body, store the result, and apply the command's continue-or-stop rules. -- Pass only the context that task needs. -- Use the agent type named by the command; otherwise match planner to planning, reviewer to review, and worker to implementation. -- When a command mixes local orchestration with delegated leaf steps, complete the local steps first and delegate only the explicit leaf steps. +- Manage step order, stored state, approvals, and stop conditions yourself. +- Load only the context needed for the current step. +- Execute required user-interaction steps exactly as the command defines them. +- If a required interaction tool is unavailable, use the command's non-interactive fallback. +- If a step is blocked, incomplete, or fails, stop and report it clearly. + +## Session Commands + +- Treat each `...` block as literal input. +- Render variables, then call `session_command` with `command` set to the tag value, `body` set to the rendered block body, and `agent` set to the tag value. +- `session_command` queues the next same-session user turn and returns immediately; it does not wait for the queued command result. +- Do not rewrite, summarize, or interpret the block body. +- Preserve line breaks and ordering. +- Run `session_command` blocks sequentially unless the workflow clearly makes them independent. +- If a `session_command` block is malformed, report it as invalid and continue with remaining valid blocks when safe. ## Output - Follow any explicit command output exactly. -- Otherwise report what finished, any delegated result, and whether the workflow is continuing, paused, blocked, or complete. +- Otherwise report what finished and whether the workflow is continuing, paused, blocked, or complete. diff --git a/packages/core/commands/index.ts b/packages/core/commands/index.ts index 886f7cd..52ac8db 100644 --- a/packages/core/commands/index.ts +++ b/packages/core/commands/index.ts @@ -42,6 +42,11 @@ export const commandDefinitions: Record = { templatePath: "commands/learn.md", subtask: false, }, + merge: { + description: "Merge a branch and auto-resolve conflicts best-effort", + agent: "worker", + templatePath: "commands/merge.md", + }, "pr/create": { description: "Summarize branch work and create a PR", agent: "worker", diff --git a/packages/core/commands/merge.md b/packages/core/commands/merge.md new file mode 100644 index 0000000..44ce4e2 --- /dev/null +++ b/packages/core/commands/merge.md @@ -0,0 +1,101 @@ +## Goal + +Merge a provided branch into the current branch, defaulting to the repo base branch, and resolve merge conflicts with a best-effort preference for the incoming branch. + +## Additional Context + +Consider `` when choosing between competing conflict resolutions. +- Default to preserving both sides when the intent is clear and the merged result remains coherent. +- When a confident manual merge is not obvious, prefer the incoming `` version to keep the command moving. +- Do not create a merge commit if any conflicts remain unresolved. + +## Workflow + +### Arguments + + +$ARGUMENTS + + +### Interpret Arguments + +- If `` starts with or contains a clear branch or ref name, store it as `` +- If `` includes additional merge guidance, store it as `` +- If no branch or ref was provided, leave `` undefined for now + +### Resolve Merge Source + +- If `` is already defined, keep it +- Otherwise, resolve the repo base branch and store it as `` +- Store the current checked out branch as `` + +### Check Blockers + +- If the working tree has uncommitted or untracked changes, STOP and report that merge automation requires a clean working tree +- If `` cannot be resolved to an existing local or remote ref, STOP and report that the merge source could not be found +- If `` equals ``, STOP and report that the current branch is already the merge source + +### Run Merge + +- Start the merge with `git merge ` +- If git reports a clean merge with no conflicts, store the new merge commit hash as `` +- If git reports conflicts, continue to `Resolve Conflicts` + +### Resolve Conflicts + +- Treat `git status` and the conflicted file markers as the source of truth for unresolved files +- For each conflicted file, attempt a best-effort merge that preserves the intended behavior of both sides +- Use `` when it helps disambiguate intent +- If a conflict can be resolved confidently by combining both sides, do that and stage that file with `git add ` +- If a confident manual merge is not obvious, prefer the incoming `` side for that conflict, then stage that file with `git add ` +- Do not wait until the end to stage everything implicitly; each resolved conflicted file must be explicitly staged before continuing +- After resolving all conflicts, finish the merge non-interactively with `GIT_EDITOR=true git merge --continue` and store the new merge commit hash as `` +- If any conflicts remain unresolved, STOP and report the remaining files without continuing the merge + +### Output + +If merge automation is blocked by a dirty working tree, display: +``` +Merge blocked: working tree is not clean + +Commit, stash, or discard local changes before running `/merge`. + +No additional steps are required. +``` + +If the merge source cannot be found, display: +``` +Merge blocked: source branch not found + +Source: + +No additional steps are required. +``` + +If the current branch already matches the merge source, display: +``` +Merge skipped: already on + +No additional steps are required. +``` + +If any conflicts remain unresolved, display: +``` +Merge blocked: unresolved conflicts remain + +Source: +Branch: + +Resolve the remaining conflicted files, then retry. + +No additional steps are required. +``` + +When the merge succeeds, display: +``` +Merged into + +Commit: + +No additional steps are required. +``` diff --git a/packages/core/commands/ship.md b/packages/core/commands/ship.md index e03f270..fa511f1 100644 --- a/packages/core/commands/ship.md +++ b/packages/core/commands/ship.md @@ -1,10 +1,10 @@ ## Goal -Ship the current work by delegating branch creation, commit creation, and PR creation. +Ship the current work by dispatching branch creation, commit creation, and PR creation in the current session. ## Additional Context -Use `` to steer delegated branch naming. Use `` to refine the delegated commit and PR summaries. Pass `` through to PR creation when it was provided. This command is delegation-first: send each `` body literally and use the subagent result as the source of truth for the next step. +Use `` to steer dispatched branch naming. Use `` to refine the dispatched commit and PR summaries. Pass `` through to PR creation when it was provided. This command is session-command-first: send each `` body literally through `session_command` and use the result as the source of truth for the next step. ## Workflow @@ -23,24 +23,22 @@ $ARGUMENTS ### Ensure Feature Branch - -/branch + Branch naming guidance: - + -- Store the subagent result as `` +- Store the dispatch result as `` - If `` is blocked or incomplete, STOP and report the branch blocker - If `` says there was nothing to branch from, continue without changing branches - Otherwise, continue with the created branch ### Delegate Commit - -/commit + Additional context: - + -- Store the subagent result as `` +- Store the dispatch result as `` - If `` says there was nothing to commit, continue without creating a new commit - If `` is blocked or incomplete, STOP and report the commit blocker @@ -48,13 +46,12 @@ Additional context: ### Delegate PR Creation - -/pr/create + Base branch: Additional context: - + -- Store the subagent result as `` +- Store the dispatch result as `` - If `` is blocked or incomplete, STOP and report the PR blocker - If `` says there is nothing to include in a PR, STOP and report that there is nothing to ship diff --git a/packages/core/commands/ticket/dev.md b/packages/core/commands/ticket/dev.md index d793dde..007290a 100644 --- a/packages/core/commands/ticket/dev.md +++ b/packages/core/commands/ticket/dev.md @@ -4,7 +4,7 @@ Implement a ticket by orchestrating development, branching, commit-and-push, and ## Additional Context -Use `` to refine scope, sequencing, and tradeoffs across the delegated `/dev`, `/branch`, `/commit-and-push`, and `/pr/create` steps. +Use `` to refine scope, sequencing, and tradeoffs across the dispatched `/dev`, `/branch`, `/commit-and-push`, and `/pr/create` steps. ## Workflow @@ -31,27 +31,25 @@ $ARGUMENTS ### Delegate Implementation -- Before delegating, send the exact dispatch block below +- Before continuing, send the exact `session_command` block below through `session_command` - -/dev + Ticket reference: Ticket context: Additional context: - + - Store the result as `` - If `` is blocked or incomplete, STOP and report the implementation blocker ### Delegate Branch Creation -- Before delegating, send the exact dispatch block below +- Before continuing, send the exact `session_command` block below through `session_command` - -/branch + Branch naming guidance: Additional context: - + - Store the result as `` - If `` is blocked or incomplete, STOP and report the branch blocker @@ -59,14 +57,13 @@ Additional context: ### Delegate Commit And Push -- Before delegating, send the exact dispatch block below +- Before continuing, send the exact `session_command` block below through `session_command` - -/commit-and-push + Ticket reference: Ticket summary: Additional context: - + - Store the result as `` - If `` is blocked or incomplete, STOP and report the commit or push blocker @@ -74,14 +71,13 @@ Additional context: ### Delegate PR Creation -- Before delegating, send the exact dispatch block below +- Before continuing, send the exact `session_command` block below through `session_command` - -/pr/create + Ticket reference: Ticket context: Additional context: - + - Store the result as `` - If `` is blocked or incomplete, STOP and report the PR blocker diff --git a/packages/core/commands/todo.md b/packages/core/commands/todo.md index de04e65..f4b68f2 100644 --- a/packages/core/commands/todo.md +++ b/packages/core/commands/todo.md @@ -41,12 +41,11 @@ $ARGUMENTS ### Delegate Planning - -/ticket/plan + Task: Task context: Additional context: - + - Ask the planner for a concise implementation plan with clear scope, risks, and validation steps - Store the result as `` @@ -63,14 +62,13 @@ Additional context: - `Revise` - update the plan based on feedback - custom answers enabled so the user can provide specific plan changes - If the user requests changes, store that feedback as `` - -/ticket/plan + Task: Task context: Current plan: Plan feedback: Additional context: - + - Store the revised result as `` and continue the review loop - If the revised planner result is blocked or unusable, store that blocker as ``, then STOP and report it before continuing the review loop @@ -79,26 +77,24 @@ Additional context: ### Delegate Implementation - -/dev + Plan: Task: Task context: Additional context: - + -- Store the subagent result as `` +- Store the dispatch result as `` - If `` is incomplete, blocked, or fails validation, store the issue as ``, then STOP and report it without marking the task complete ### Delegate Commit - -/commit + Task: Additional context: - + -- Store the subagent result as `` +- Store the dispatch result as `` - If `` does not succeed, store the commit status as ``, then STOP and report it without marking the task complete ### Mark Complete And Loop diff --git a/packages/core/index.ts b/packages/core/index.ts index 56f127d..8518e3c 100644 --- a/packages/core/index.ts +++ b/packages/core/index.ts @@ -12,11 +12,13 @@ export type { AgentDefinition, KompassConfig, MergedKompassConfig, - ToolConfig, ToolName, + ToolConfig, } from "./lib/config.ts"; export { createTools } from "./tools/index.ts"; export { createChangesLoadTool } from "./tools/changes-load.ts"; +export { createSessionCommandTool, resolveSessionCommand } from "./tools/dispatch.ts"; +export type { SessionCommandResolution } from "./tools/dispatch.ts"; export { createPrLoadTool } from "./tools/pr-load.ts"; export { createPrSyncTool } from "./tools/pr-sync.ts"; export { createTicketLoadTool } from "./tools/ticket-load.ts"; diff --git a/packages/core/kompass.jsonc b/packages/core/kompass.jsonc index 48c53fd..3c1ebf8 100644 --- a/packages/core/kompass.jsonc +++ b/packages/core/kompass.jsonc @@ -17,6 +17,7 @@ "commit-and-push": { "enabled": true }, "dev": { "enabled": true }, "learn": { "enabled": true }, + "merge": { "enabled": true }, "pr/create": { "enabled": true }, "pr/fix": { "enabled": true }, "pr/review": { "enabled": true }, @@ -40,6 +41,7 @@ "tools": { "changes_load": { "enabled": true }, + "session_command": { "enabled": true }, "pr_load": { "enabled": true }, "pr_sync": { "enabled": true }, "ticket_load": { "enabled": true }, diff --git a/packages/core/lib/config.ts b/packages/core/lib/config.ts index 9074724..9e2332f 100644 --- a/packages/core/lib/config.ts +++ b/packages/core/lib/config.ts @@ -14,6 +14,7 @@ export interface AgentDefinition { export const DEFAULT_TOOL_NAMES = [ "changes_load", + "session_command", "pr_load", "pr_sync", "ticket_sync", @@ -27,6 +28,7 @@ export const DEFAULT_COMMAND_NAMES = [ "commit-and-push", "dev", "learn", + "merge", "pr/create", "pr/fix", "pr/review", @@ -90,6 +92,7 @@ export interface KompassConfig { "commit-and-push"?: CommandConfig; dev?: CommandConfig; learn?: CommandConfig; + merge?: CommandConfig; "pr/create"?: CommandConfig; "pr/fix"?: CommandConfig; "pr/review"?: CommandConfig; @@ -114,6 +117,7 @@ export interface KompassConfig { }; tools?: { changes_load?: ToolConfig; + session_command?: ToolConfig; pr_load?: ToolConfig; pr_sync?: ToolConfig; ticket_sync?: ToolConfig; @@ -161,6 +165,7 @@ export interface MergedKompassConfig { }; tools: { changes_load: ToolConfig; + session_command: ToolConfig; pr_load: ToolConfig; pr_sync: ToolConfig; ticket_sync: ToolConfig; @@ -420,7 +425,7 @@ const defaultAgentReviewer: AgentDefinition = { }; const defaultAgentNavigator: AgentDefinition = { - description: "Coordinate structured multi-step workflows by delegating focused leaf work to subagents.", + description: "Coordinate structured multi-step workflows and run focused slash-command steps in the current session.", promptPath: "agents/navigator.md", permission: { edit: "deny", task: "allow", question: "allow", todowrite: "allow" }, }; @@ -443,6 +448,7 @@ const defaultComponentPaths: Record = { const defaultToolConfig: Record = { changes_load: { enabled: true }, + session_command: { enabled: true }, pr_load: { enabled: true }, pr_sync: { enabled: true }, ticket_sync: { enabled: true }, @@ -560,6 +566,10 @@ export function mergeWithDefaults( }, tools: { changes_load: { ...defaultToolConfig.changes_load, ...config?.tools?.changes_load }, + session_command: { + ...defaultToolConfig.session_command, + ...config?.tools?.session_command, + }, pr_load: { ...defaultToolConfig.pr_load, ...config?.tools?.pr_load }, pr_sync: { ...defaultToolConfig.pr_sync, ...config?.tools?.pr_sync }, ticket_sync: { ...defaultToolConfig.ticket_sync, ...config?.tools?.ticket_sync }, diff --git a/packages/core/test/dispatch.test.ts b/packages/core/test/dispatch.test.ts new file mode 100644 index 0000000..35bbdbd --- /dev/null +++ b/packages/core/test/dispatch.test.ts @@ -0,0 +1,64 @@ +import { describe, test } from "node:test"; +import assert from "node:assert/strict"; +import os from "node:os"; +import path from "node:path"; + +import { resolveSessionCommand } from "../index.ts"; + +process.env.HOME = path.join(os.tmpdir(), `kompass-test-home-${process.pid}-core-dispatch`); + +describe("resolveSessionCommand", () => { + test("expands known slash commands and infers their default agent", async () => { + const result = await resolveSessionCommand(process.cwd(), { command: "review", body: "auth bug" }); + + assert.equal(result.agent, "reviewer"); + assert.equal(result.command, "review"); + assert.equal(result.body, "auth bug"); + assert.equal(result.expanded, true); + assert.match(result.prompt, /\s*auth bug\s*<\/arguments>/); + }); + + test("preserves explicit agent routing when present", async () => { + const result = await resolveSessionCommand(process.cwd(), { + agent: "planner", + command: "ticket/plan", + body: "auth bug", + }); + + assert.equal(result.agent, "planner"); + assert.equal(result.command, "ticket/plan"); + assert.equal(result.body, "auth bug"); + assert.equal(result.expanded, true); + }); + + test("allows a dispatch-tag agent override at tool level", async () => { + const { createSessionCommandTool } = await import("../index.ts"); + const tool = createSessionCommandTool(process.cwd()); + const output = await tool.execute( + { command: "review", body: "auth bug", agent: "worker" }, + { worktree: process.cwd(), directory: process.cwd() }, + ); + const result = JSON.parse(output); + + assert.equal(result.agent, "worker"); + assert.equal(result.command, "review"); + }); + + test("keeps unknown slash commands dispatchable without expansion", async () => { + const result = await resolveSessionCommand(process.cwd(), { command: "unknown", body: "auth bug" }); + + assert.deepEqual(result, { + command: "unknown", + body: "auth bug", + prompt: "/unknown\nauth bug", + expanded: false, + }); + }); + + test("rejects missing commands", async () => { + await assert.rejects( + resolveSessionCommand(process.cwd(), { command: " ", body: "auth bug" }), + /requires a command/, + ); + }); +}); diff --git a/packages/core/tools/dispatch.ts b/packages/core/tools/dispatch.ts new file mode 100644 index 0000000..0a3d531 --- /dev/null +++ b/packages/core/tools/dispatch.ts @@ -0,0 +1,109 @@ +import { resolveCommands } from "../commands/index.ts"; +import { stringifyJson, type ToolDefinition, type ToolExecutionContext } from "./shared.ts"; + +export type SessionCommandResolution = { + agent?: string; + command: string; + body: string; + prompt: string; + expanded: boolean; +}; + +type ResolveSessionCommandOptions = { + rewriteBody?: (body: string) => string; +}; + +type SessionCommandInput = { + command: string; + body?: string; + agent?: string; +}; + +function renderSlashCommand(command: string, body: string) { + const trimmedBody = body.trim(); + return trimmedBody ? `/${command}\n${trimmedBody}` : `/${command}`; +} + +function expandCommandTemplate(template: string, commandBody: string) { + const trimmedBody = commandBody.trim(); + const positionalArguments = trimmedBody ? trimmedBody.split(/\s+/) : []; + + let expandedTemplate = template.replaceAll("$ARGUMENTS", trimmedBody); + + for (const [index, argument] of positionalArguments.entries()) { + expandedTemplate = expandedTemplate.replaceAll(`$${index + 1}`, argument); + } + + return expandedTemplate; +} + +export async function resolveSessionCommand( + projectRoot: string, + input: SessionCommandInput, + options?: ResolveSessionCommandOptions, +): Promise { + const normalizedCommand = input.command.trim(); + const normalizedBody = input.body?.trim() ?? ""; + + if (!normalizedCommand) { + throw new Error("session_command requires a command"); + } + + const commands = await resolveCommands(projectRoot); + const definition = commands[normalizedCommand]; + + if (!definition) { + return { + ...(input.agent?.trim() ? { agent: input.agent.trim() } : {}), + command: normalizedCommand, + body: normalizedBody, + prompt: renderSlashCommand(normalizedCommand, normalizedBody), + expanded: false, + }; + } + + let prompt = expandCommandTemplate(definition.template, normalizedBody); + + if (options?.rewriteBody) { + prompt = options.rewriteBody(prompt); + } + + return { + agent: input.agent?.trim() || definition.agent, + command: normalizedCommand, + body: normalizedBody, + prompt, + expanded: true, + }; +} + +export function createSessionCommandTool( + projectRoot: string, + options?: ResolveSessionCommandOptions, +) { + return { + description: "Resolve a command and body for same-session queuing", + args: { + command: { + type: "string", + description: "Command name to resolve, without the leading slash", + }, + body: { + type: "string", + optional: true, + description: "Literal body content from the session_command block", + }, + agent: { + type: "string", + optional: true, + description: "Optional agent override from the session_command tag", + }, + }, + async execute( + args: { command: string; body?: string; agent?: string }, + _ctx: ToolExecutionContext, + ) { + return stringifyJson(await resolveSessionCommand(projectRoot, args, options)); + }, + } satisfies ToolDefinition<{ command: string; body?: string; agent?: string }>; +} diff --git a/packages/core/tools/index.ts b/packages/core/tools/index.ts index 1927e06..ffaf5b3 100644 --- a/packages/core/tools/index.ts +++ b/packages/core/tools/index.ts @@ -5,18 +5,20 @@ import { mergeWithDefaults, } from "../lib/config.ts"; import { createChangesLoadTool } from "./changes-load.ts"; +import { createSessionCommandTool } from "./dispatch.ts"; import { createPrLoadTool } from "./pr-load.ts"; import { createPrSyncTool } from "./pr-sync.ts"; import { createTicketLoadTool } from "./ticket-load.ts"; import { createTicketSyncTool } from "./ticket-sync.ts"; import type { Shell, ToolDefinition } from "./shared.ts"; -const toolCreators: Record ToolDefinition> = { - changes_load: createChangesLoadTool, - pr_load: createPrLoadTool, - pr_sync: createPrSyncTool, - ticket_sync: createTicketSyncTool, - ticket_load: createTicketLoadTool, +const toolCreators: Record ToolDefinition> = { + changes_load: ($) => createChangesLoadTool($), + session_command: (_, projectRoot) => createSessionCommandTool(projectRoot), + pr_load: ($) => createPrLoadTool($), + pr_sync: ($) => createPrSyncTool($), + ticket_sync: ($) => createTicketSyncTool($), + ticket_load: ($) => createTicketLoadTool($), }; export async function createTools($: Shell, projectRoot: string) { @@ -28,7 +30,7 @@ export async function createTools($: Shell, projectRoot: string) { for (const toolName of getEnabledToolNames(config.tools)) { const creator = toolCreators[toolName]; if (creator) { - tools[getConfiguredToolName(config.tools, toolName)] = creator($); + tools[getConfiguredToolName(config.tools, toolName)] = creator($, projectRoot); } } diff --git a/packages/opencode/.opencode/agents/navigator.md b/packages/opencode/.opencode/agents/navigator.md index 82c798e..d4ba60e 100644 --- a/packages/opencode/.opencode/agents/navigator.md +++ b/packages/opencode/.opencode/agents/navigator.md @@ -1,6 +1,6 @@ --- -description: Coordinate structured multi-step workflows by delegating focused - leaf work to subagents. +description: Coordinate structured multi-step workflows and run focused + slash-command steps in the current session. permission: edit: deny task: allow @@ -8,44 +8,28 @@ permission: todowrite: allow --- -You are a navigation specialist for structured, multi-step workflows. +You coordinate structured, multi-step workflows. -## Operating Boundaries +## Rules - Follow the active command and provided context. -- Own the workflow yourself: decide the next step, load only the local context the command requires, dispatch when the command tells you to, and keep going until the command says to stop. -- Owning the workflow means managing step order, state, and stop conditions; it does not let you rewrite an explicit `` body. -- Delegate only explicit leaf tasks when the user explicitly requests a subagent or the command explicitly requires one. -- Gather only the context needed for the current step. -- Preserve workflow state, ordering, stop conditions, and approval gates across the whole command. -- Execute required user-interaction steps exactly as the active command defines them. -- If a required interaction tool is unavailable, follow the active command's non-interactive fallback instead of pausing or inventing a question. -- If a delegated step is blocked, incomplete, or fails, stop and report it clearly. - -## Dispatch Execution - -- Treat each `...` block as a literal message dispatch instruction. -- Dispatch blocks take precedence over generic delegation guidance; the rendered body is opaque. -- `agent` is required; invoke that exact subagent type. -- Set `prompt` to the dispatch body exactly after variable substitution. -- Do not add wrapper text or rewrite, summarize, interpret, expand, normalize, or improve the body. -- Preserve line breaks and ordering exactly. -- Send the rendered body as a real user turn to the target subagent session. -- Never infer what a slash command means when handling a dispatch block. Forward it literally. -- Process every valid dispatch block you receive. -- Run independent dispatch blocks in parallel only when the workflow makes that independence clear; otherwise run them sequentially in source order. -- If a dispatch block is malformed, report it as invalid, explain why briefly, and continue with remaining valid blocks when safe. -- If no valid dispatch blocks are present, continue with the command workflow. - -## Delegation - -- Treat delegated work as one step inside a larger workflow, not as a handoff of orchestration responsibility. -- For an explicit `` step, your job is only to render variables, send the exact body, store the result, and apply the command's continue-or-stop rules. -- Pass only the context that task needs. -- Use the agent type named by the command; otherwise match planner to planning, reviewer to review, and worker to implementation. -- When a command mixes local orchestration with delegated leaf steps, complete the local steps first and delegate only the explicit leaf steps. +- Manage step order, stored state, approvals, and stop conditions yourself. +- Load only the context needed for the current step. +- Execute required user-interaction steps exactly as the command defines them. +- If a required interaction tool is unavailable, use the command's non-interactive fallback. +- If a step is blocked, incomplete, or fails, stop and report it clearly. + +## Session Commands + +- Treat each `...` block as literal input. +- Render variables, then call `kompass_session_command` with `command` set to the tag value, `body` set to the rendered block body, and `agent` set to the tag value. +- `kompass_session_command` queues the next same-session user turn and returns immediately; it does not wait for the queued command result. +- Do not rewrite, summarize, or interpret the block body. +- Preserve line breaks and ordering. +- Run `kompass_session_command` blocks sequentially unless the workflow clearly makes them independent. +- If a `kompass_session_command` block is malformed, report it as invalid and continue with remaining valid blocks when safe. ## Output - Follow any explicit command output exactly. -- Otherwise report what finished, any delegated result, and whether the workflow is continuing, paused, blocked, or complete. +- Otherwise report what finished and whether the workflow is continuing, paused, blocked, or complete. diff --git a/packages/opencode/.opencode/commands/merge.md b/packages/opencode/.opencode/commands/merge.md new file mode 100644 index 0000000..ce758b1 --- /dev/null +++ b/packages/opencode/.opencode/commands/merge.md @@ -0,0 +1,106 @@ +--- +description: Merge a branch and auto-resolve conflicts best-effort +agent: worker +--- + +## Goal + +Merge a provided branch into the current branch, defaulting to the repo base branch, and resolve merge conflicts with a best-effort preference for the incoming branch. + +## Additional Context + +Consider `` when choosing between competing conflict resolutions. +- Default to preserving both sides when the intent is clear and the merged result remains coherent. +- When a confident manual merge is not obvious, prefer the incoming `` version to keep the command moving. +- Do not create a merge commit if any conflicts remain unresolved. + +## Workflow + +### Arguments + + +$ARGUMENTS + + +### Interpret Arguments + +- If `` starts with or contains a clear branch or ref name, store it as `` +- If `` includes additional merge guidance, store it as `` +- If no branch or ref was provided, leave `` undefined for now + +### Resolve Merge Source + +- If `` is already defined, keep it +- Otherwise, resolve the repo base branch and store it as `` +- Store the current checked out branch as `` + +### Check Blockers + +- If the working tree has uncommitted or untracked changes, STOP and report that merge automation requires a clean working tree +- If `` cannot be resolved to an existing local or remote ref, STOP and report that the merge source could not be found +- If `` equals ``, STOP and report that the current branch is already the merge source + +### Run Merge + +- Start the merge with `git merge ` +- If git reports a clean merge with no conflicts, store the new merge commit hash as `` +- If git reports conflicts, continue to `Resolve Conflicts` + +### Resolve Conflicts + +- Treat `git status` and the conflicted file markers as the source of truth for unresolved files +- For each conflicted file, attempt a best-effort merge that preserves the intended behavior of both sides +- Use `` when it helps disambiguate intent +- If a conflict can be resolved confidently by combining both sides, do that and stage that file with `git add ` +- If a confident manual merge is not obvious, prefer the incoming `` side for that conflict, then stage that file with `git add ` +- Do not wait until the end to stage everything implicitly; each resolved conflicted file must be explicitly staged before continuing +- After resolving all conflicts, finish the merge non-interactively with `GIT_EDITOR=true git merge --continue` and store the new merge commit hash as `` +- If any conflicts remain unresolved, STOP and report the remaining files without continuing the merge + +### Output + +If merge automation is blocked by a dirty working tree, display: +``` +Merge blocked: working tree is not clean + +Commit, stash, or discard local changes before running `/merge`. + +No additional steps are required. +``` + +If the merge source cannot be found, display: +``` +Merge blocked: source branch not found + +Source: + +No additional steps are required. +``` + +If the current branch already matches the merge source, display: +``` +Merge skipped: already on + +No additional steps are required. +``` + +If any conflicts remain unresolved, display: +``` +Merge blocked: unresolved conflicts remain + +Source: +Branch: + +Resolve the remaining conflicted files, then retry. + +No additional steps are required. +``` + +When the merge succeeds, display: +``` +Merged into + +Commit: + +No additional steps are required. +``` diff --git a/packages/opencode/.opencode/commands/ship.md b/packages/opencode/.opencode/commands/ship.md index 1aabed0..99aad77 100644 --- a/packages/opencode/.opencode/commands/ship.md +++ b/packages/opencode/.opencode/commands/ship.md @@ -5,11 +5,11 @@ agent: navigator ## Goal -Ship the current work by delegating branch creation, commit creation, and PR creation. +Ship the current work by dispatching branch creation, commit creation, and PR creation in the current session. ## Additional Context -Use `` to steer delegated branch naming. Use `` to refine the delegated commit and PR summaries. Pass `` through to PR creation when it was provided. This command is delegation-first: send each `` body literally and use the subagent result as the source of truth for the next step. +Use `` to steer dispatched branch naming. Use `` to refine the dispatched commit and PR summaries. Pass `` through to PR creation when it was provided. This command is session-command-first: send each `` body literally through `kompass_session_command` and use the result as the source of truth for the next step. ## Workflow @@ -28,24 +28,22 @@ $ARGUMENTS ### Ensure Feature Branch - -/branch + Branch naming guidance: - + -- Store the subagent result as `` +- Store the dispatch result as `` - If `` is blocked or incomplete, STOP and report the branch blocker - If `` says there was nothing to branch from, continue without changing branches - Otherwise, continue with the created branch ### Delegate Commit - -/commit + Additional context: - + -- Store the subagent result as `` +- Store the dispatch result as `` - If `` says there was nothing to commit, continue without creating a new commit - If `` is blocked or incomplete, STOP and report the commit blocker @@ -53,13 +51,12 @@ Additional context: ### Delegate PR Creation - -/pr/create + Base branch: Additional context: - + -- Store the subagent result as `` +- Store the dispatch result as `` - If `` is blocked or incomplete, STOP and report the PR blocker - If `` says there is nothing to include in a PR, STOP and report that there is nothing to ship diff --git a/packages/opencode/.opencode/commands/ticket/dev.md b/packages/opencode/.opencode/commands/ticket/dev.md index 2556245..c987cd6 100644 --- a/packages/opencode/.opencode/commands/ticket/dev.md +++ b/packages/opencode/.opencode/commands/ticket/dev.md @@ -9,7 +9,7 @@ Implement a ticket by orchestrating development, branching, commit-and-push, and ## Additional Context -Use `` to refine scope, sequencing, and tradeoffs across the delegated `/dev`, `/branch`, `/commit-and-push`, and `/pr/create` steps. +Use `` to refine scope, sequencing, and tradeoffs across the dispatched `/dev`, `/branch`, `/commit-and-push`, and `/pr/create` steps. ## Workflow @@ -46,27 +46,25 @@ $ARGUMENTS ### Delegate Implementation -- Before delegating, send the exact dispatch block below +- Before continuing, send the exact `kompass_session_command` block below through `kompass_session_command` - -/dev + Ticket reference: Ticket context: Additional context: - + - Store the result as `` - If `` is blocked or incomplete, STOP and report the implementation blocker ### Delegate Branch Creation -- Before delegating, send the exact dispatch block below +- Before continuing, send the exact `kompass_session_command` block below through `kompass_session_command` - -/branch + Branch naming guidance: Additional context: - + - Store the result as `` - If `` is blocked or incomplete, STOP and report the branch blocker @@ -74,14 +72,13 @@ Additional context: ### Delegate Commit And Push -- Before delegating, send the exact dispatch block below +- Before continuing, send the exact `kompass_session_command` block below through `kompass_session_command` - -/commit-and-push + Ticket reference: Ticket summary: Additional context: - + - Store the result as `` - If `` is blocked or incomplete, STOP and report the commit or push blocker @@ -89,14 +86,13 @@ Additional context: ### Delegate PR Creation -- Before delegating, send the exact dispatch block below +- Before continuing, send the exact `kompass_session_command` block below through `kompass_session_command` - -/pr/create + Ticket reference: Ticket context: Additional context: - + - Store the result as `` - If `` is blocked or incomplete, STOP and report the PR blocker diff --git a/packages/opencode/.opencode/commands/todo.md b/packages/opencode/.opencode/commands/todo.md index 6855fae..62274b1 100644 --- a/packages/opencode/.opencode/commands/todo.md +++ b/packages/opencode/.opencode/commands/todo.md @@ -46,12 +46,11 @@ $ARGUMENTS ### Delegate Planning - -/ticket/plan + Task: Task context: Additional context: - + - Ask the planner for a concise implementation plan with clear scope, risks, and validation steps - Store the result as `` @@ -68,14 +67,13 @@ Additional context: - `Revise` - update the plan based on feedback - custom answers enabled so the user can provide specific plan changes - If the user requests changes, store that feedback as `` - -/ticket/plan + Task: Task context: Current plan: Plan feedback: Additional context: - + - Store the revised result as `` and continue the review loop - If the revised planner result is blocked or unusable, store that blocker as ``, then STOP and report it before continuing the review loop @@ -84,26 +82,24 @@ Additional context: ### Delegate Implementation - -/dev + Plan: Task: Task context: Additional context: - + -- Store the subagent result as `` +- Store the dispatch result as `` - If `` is incomplete, blocked, or fails validation, store the issue as ``, then STOP and report it without marking the task complete ### Delegate Commit - -/commit + Task: Additional context: - + -- Store the subagent result as `` +- Store the dispatch result as `` - If `` does not succeed, store the commit status as ``, then STOP and report it without marking the task complete ### Mark Complete And Loop diff --git a/packages/opencode/.opencode/kompass.jsonc b/packages/opencode/.opencode/kompass.jsonc index f4bb040..bbbd650 100644 --- a/packages/opencode/.opencode/kompass.jsonc +++ b/packages/opencode/.opencode/kompass.jsonc @@ -25,6 +25,9 @@ "learn": { "enabled": true }, + "merge": { + "enabled": true + }, "pr/create": { "enabled": true }, @@ -80,6 +83,9 @@ "changes_load": { "enabled": true }, + "session_command": { + "enabled": true + }, "pr_load": { "enabled": true }, diff --git a/packages/opencode/README.md b/packages/opencode/README.md index d91c7c6..79634ed 100644 --- a/packages/opencode/README.md +++ b/packages/opencode/README.md @@ -20,7 +20,7 @@ Kompass keeps AI coding agents on course with token-efficient, composable workfl - Commands cover direct work (`/ask`, `/commit`), orchestration (`/dev`, `/ship`, `/todo`), ticket planning/sync, and PR review/shipping flows. - Agents are intentionally narrow: `worker` is generic, `planner` is no-edit planning, `navigator` owns multi-step orchestration, and `reviewer` is a no-edit review specialist. -- Structured tools keep workflows grounded in repo and GitHub state: `changes_load`, `pr_load`, `pr_sync`, `ticket_load`, `ticket_sync`. +- Structured tools keep workflows grounded in repo and GitHub state: `changes_load`, `session_command` (resolve a slash command and queue it into the current session), `pr_load`, `pr_sync`, `ticket_load`, `ticket_sync`. - Reusable command-template components live in `packages/core/components/` and are documented in the components reference. ## Installation diff --git a/packages/opencode/index.ts b/packages/opencode/index.ts index 2d05182..3f0a30c 100644 --- a/packages/opencode/index.ts +++ b/packages/opencode/index.ts @@ -3,6 +3,7 @@ import { tool, type ToolDefinition } from "@opencode-ai/plugin/tool"; import { createChangesLoadTool, + createSessionCommandTool, createPrLoadTool, createPrSyncTool, createTicketLoadTool, @@ -10,7 +11,6 @@ import { getEnabledToolNames, loadKompassConfig, mergeWithDefaults, - resolveCommands, type MergedKompassConfig, type Shell, } from "../core/index.ts"; @@ -31,6 +31,12 @@ type CommandExecuteBeforeInput = Parameters[0]; type CommandExecuteBeforeOutput = Parameters[1]; type ChatMessageHook = NonNullable; type ChatMessageOutput = Parameters[1]; +type OpenCodeToolCreator = ( + $: PluginInput["$"], + client: PluginInput["client"], + config: MergedKompassConfig, + projectRoot: string, +) => ToolDefinition; function asShell(shell: PluginInput["$"]): Shell { return shell as unknown as Shell; @@ -42,8 +48,6 @@ export type TaskToolExecution = { description?: string; subagent_type?: string; command?: string; - command_name?: string; - command_arguments?: string; }; export type CommandExecution = { @@ -52,11 +56,6 @@ export type CommandExecution = { prompt: string; }; -type ParsedSlashCommand = { - command: string; - arguments: string; -}; - function getString(value: unknown): string | undefined { return typeof value === "string" ? value : undefined; } @@ -73,76 +72,39 @@ async function logObservedFailure( }); } -function parseSlashCommand(value: string): ParsedSlashCommand | undefined { - const match = value.trim().match(/^(?:@\S+\s+)?\/([^\s]+)(?:\s+([\s\S]*))?$/); - - if (!match) return; - - return { - command: match[1], - arguments: match[2]?.trim() ?? "", - }; -} - -function expandCommandTemplate(template: string, commandArguments: string) { - const trimmedArguments = commandArguments.trim(); - const positionalArguments = trimmedArguments ? trimmedArguments.split(/\s+/) : []; - - let expandedTemplate = template.replaceAll("$ARGUMENTS", trimmedArguments); - - for (const [index, argument] of positionalArguments.entries()) { - expandedTemplate = expandedTemplate.replaceAll(`$${index + 1}`, argument); - } - - return expandedTemplate; +function getSessionError(result: unknown) { + if (!result || typeof result !== "object") return undefined; + if (!("error" in result)) return undefined; + return (result as { error?: unknown }).error; } -export async function expandSlashCommandPrompt( - projectRoot: string, - value: string, - logger?: PluginLogger, -): Promise { - const parsedCommand = parseSlashCommand(value); - - if (!parsedCommand) return; +async function executeSessionCommand( + client: PluginInput["client"], + context: { sessionID: string; directory: string }, + sessionCommand: { agent?: string; command: string; body: string; prompt: string; expanded: boolean }, +) { + const result = await client.session.promptAsync({ + path: { id: context.sessionID }, + query: { directory: context.directory }, + body: { + ...(sessionCommand.agent ? { agent: sessionCommand.agent } : {}), + parts: [{ type: "text", text: sessionCommand.prompt }], + }, + }); - const userConfig = await loadKompassConfig(projectRoot); - const config = mergeWithDefaults(userConfig); - const commands = await resolveCommands(projectRoot); - const definition = commands[parsedCommand.command]; - - if (!definition) return; - - const configuredToolNames = Object.fromEntries( - getEnabledToolNames(config.tools).map((toolName) => [ - toolName, - getConfiguredOpenCodeToolName(toolName, config.tools[toolName].name), - ]), - ); - const template = prefixKompassToolReferences(definition.template, configuredToolNames); - const expandedPrompt = expandCommandTemplate(template, parsedCommand.arguments); - - if (logger) { - await logger.info("Resolved slash command", { - input: value, - command: parsedCommand.command, - arguments: parsedCommand.arguments, - output: expandedPrompt, - }); + const error = getSessionError(result); + if (error) { + throw new Error(`Session command enqueue failed: ${JSON.stringify(error)}`); } return { - command: parsedCommand.command, - arguments: parsedCommand.arguments, - prompt: expandedPrompt, + mode: "prompt_async", }; } export async function getTaskToolExecution( input: ToolExecuteBeforeInput, output: ToolExecuteBeforeOutput, - projectRoot: string, - logger?: PluginLogger, ): Promise { if (input.tool !== "task") return; if (!output.args || typeof output.args !== "object") return; @@ -153,35 +115,12 @@ export async function getTaskToolExecution( if (!prompt && !command) return; - let expandedCommand: CommandExecution | undefined; - try { - expandedCommand = prompt - ? await expandSlashCommandPrompt(projectRoot, prompt, logger) - : command - ? await expandSlashCommandPrompt(projectRoot, command, logger) - : undefined; - } catch (error) { - if (logger) { - await logObservedFailure(logger, "Failed to expand slash command for task tool", error, { - projectRoot, - tool: input.tool, - command, - prompt, - }); - } - } - const finalPrompt = expandedCommand?.prompt ?? prompt ?? command ?? ""; - - args.prompt = finalPrompt; - return { - prompt: finalPrompt, + prompt: prompt ?? command ?? "", raw_prompt: prompt, description: getString(args.description), subagent_type: getString(args.subagent_type), command, - command_name: expandedCommand?.command, - command_arguments: expandedCommand?.arguments, }; } @@ -216,8 +155,8 @@ export function removeSyntheticAgentHandoff(output: ChatMessageOutput): boolean return true; } -const opencodeToolCreators = { - changes_load($: PluginInput["$"], _: PluginInput["client"], __: MergedKompassConfig) { +const opencodeToolCreators: Record = { + changes_load($: PluginInput["$"], _: PluginInput["client"], __: MergedKompassConfig, _projectRoot: string) { const definition = createChangesLoadTool(asShell($)); return tool({ description: definition.description, @@ -234,7 +173,51 @@ const opencodeToolCreators = { execute: (args, context) => definition.execute(args, context), }); }, - pr_load($: PluginInput["$"], _: PluginInput["client"], __: MergedKompassConfig) { + session_command(_: PluginInput["$"], client: PluginInput["client"], config: MergedKompassConfig, projectRoot: string) { + const configuredToolNames = Object.fromEntries( + getEnabledToolNames(config.tools).map((toolName) => [ + toolName, + getConfiguredOpenCodeToolName(toolName, config.tools[toolName].name), + ]), + ); + const definition = createSessionCommandTool(projectRoot, { + rewriteBody: (body) => prefixKompassToolReferences(body, configuredToolNames), + }); + + return tool({ + description: "Resolve a command and body and queue it in the current session", + args: { + command: tool.schema.string().describe("Command name to execute, without the leading slash"), + body: tool.schema.string().describe("Literal body content from the session_command block").optional(), + agent: tool.schema.string().describe("Optional agent override from the session_command tag").optional(), + }, + execute: async (args, context) => { + context.metadata({ + title: `Command /${args.command.trim()}`, + metadata: { + command: args.command, + agent: args.agent, + }, + }); + + const resolved = JSON.parse(await definition.execute(args, context)) as { + agent?: string; + command: string; + body: string; + prompt: string; + expanded: boolean; + }; + const dispatched = await executeSessionCommand(client, context, resolved); + + return JSON.stringify({ + ...resolved, + queued: true, + mode: dispatched.mode, + }); + }, + }); + }, + pr_load($: PluginInput["$"], _: PluginInput["client"], __: MergedKompassConfig, _projectRoot: string) { const definition = createPrLoadTool(asShell($)); return tool({ description: definition.description, @@ -244,7 +227,7 @@ const opencodeToolCreators = { execute: (args, context) => definition.execute(args, context), }); }, - pr_sync($: PluginInput["$"], _: PluginInput["client"], config: MergedKompassConfig) { + pr_sync($: PluginInput["$"], _: PluginInput["client"], config: MergedKompassConfig, _projectRoot: string) { const definition = createPrSyncTool(asShell($)); return tool({ @@ -290,7 +273,7 @@ const opencodeToolCreators = { execute: (args, context) => definition.execute(args, context), }); }, - ticket_sync($: PluginInput["$"], _: PluginInput["client"], __: MergedKompassConfig) { + ticket_sync($: PluginInput["$"], _: PluginInput["client"], __: MergedKompassConfig, _projectRoot: string) { const definition = createTicketSyncTool(asShell($)); return tool({ description: definition.description, @@ -313,7 +296,7 @@ const opencodeToolCreators = { execute: (args, context) => definition.execute(args, context), }); }, - ticket_load($: PluginInput["$"], _: PluginInput["client"], __: MergedKompassConfig) { + ticket_load($: PluginInput["$"], _: PluginInput["client"], __: MergedKompassConfig, _projectRoot: string) { const definition = createTicketLoadTool(asShell($)); return tool({ description: definition.description, @@ -324,7 +307,7 @@ const opencodeToolCreators = { execute: (args, context) => definition.execute(args, context), }); }, -} as const; +}; export async function createOpenCodeTools( $: PluginInput["$"], @@ -340,7 +323,7 @@ export async function createOpenCodeTools( const creator = opencodeToolCreators[toolName as keyof typeof opencodeToolCreators]; if (creator) { const registeredName = getConfiguredOpenCodeToolName(toolName, config.tools[toolName].name); - tools[registeredName] = creator($, client, config); + tools[registeredName] = creator($, client, config, projectRoot); await logger.info("Loaded Kompass tool", { tool: toolName, registeredName, @@ -428,7 +411,7 @@ export const OpenCodeCompassPlugin: Plugin = async (input: PluginInput) => { }, async "tool.execute.before"(input, output) { try { - const taskExecution = await getTaskToolExecution(input, output, worktree, logger); + const taskExecution = await getTaskToolExecution(input, output); if (!taskExecution) return; diff --git a/packages/opencode/kompass.jsonc b/packages/opencode/kompass.jsonc index 48c53fd..3c1ebf8 100644 --- a/packages/opencode/kompass.jsonc +++ b/packages/opencode/kompass.jsonc @@ -17,6 +17,7 @@ "commit-and-push": { "enabled": true }, "dev": { "enabled": true }, "learn": { "enabled": true }, + "merge": { "enabled": true }, "pr/create": { "enabled": true }, "pr/fix": { "enabled": true }, "pr/review": { "enabled": true }, @@ -40,6 +41,7 @@ "tools": { "changes_load": { "enabled": true }, + "session_command": { "enabled": true }, "pr_load": { "enabled": true }, "pr_sync": { "enabled": true }, "ticket_load": { "enabled": true }, diff --git a/packages/opencode/test/agents-config.test.ts b/packages/opencode/test/agents-config.test.ts index 0984dcc..cd84790 100644 --- a/packages/opencode/test/agents-config.test.ts +++ b/packages/opencode/test/agents-config.test.ts @@ -35,7 +35,7 @@ describe("applyAgentsConfig", () => { assert.equal(cfg.agent.worker?.mode, undefined); assert.equal( cfg.agent.navigator?.description, - "Coordinate structured multi-step workflows by delegating focused leaf work to subagents.", + "Coordinate structured multi-step workflows and run focused slash-command steps in the current session.", ); assert.deepEqual(cfg.agent.navigator?.permission, { edit: "deny", @@ -54,9 +54,10 @@ describe("applyAgentsConfig", () => { todowrite: "allow", }); assert.equal(cfg.agent.worker?.prompt, undefined); - assert.match(cfg.agent.navigator?.prompt ?? "", /navigation specialist/i); - assert.match(cfg.agent.navigator?.prompt ?? "", /delegate only explicit leaf tasks/i); - assert.match(cfg.agent.navigator?.prompt ?? "", /complete the local steps first/i); + assert.match(cfg.agent.navigator?.prompt ?? "", /structured, multi-step workflows/i); + assert.match(cfg.agent.navigator?.prompt ?? "", /call `?kompass_session_command`?/i); + assert.match(cfg.agent.navigator?.prompt ?? "", /manage step order/i); + assert.match(cfg.agent.navigator?.prompt ?? "", / { const expectedCommands = [ "ask", "branch", + "merge", "pr/create", "pr/review", "pr/fix", @@ -60,6 +61,7 @@ describe("applyCommandsConfig", () => { assert.ok(cfg.command); assert.equal(cfg.command!["pr/review"]?.agent, "reviewer"); assert.equal(cfg.command!["branch"]?.agent, "worker"); + assert.equal(cfg.command!["merge"]?.agent, "worker"); assert.equal(cfg.command!["pr/create"]?.agent, "worker"); assert.equal(cfg.command!["ticket/create"]?.agent, "worker"); assert.equal(cfg.command!["ticket/plan"]?.agent, "planner"); @@ -420,7 +422,7 @@ describe("applyCommandsConfig", () => { assert.ok(cfg.command!["review"]?.template); }); - test("embeds literal dispatch blocks in ship command", async () => { + test("embeds literal session_command blocks in ship command", async () => { delete process.env.CI; const cfg: { command?: Record } = {}; @@ -430,15 +432,17 @@ describe("applyCommandsConfig", () => { const shipTemplate = cfg.command!["ship"].template; assert.match(shipTemplate, /## Goal/); - assert.match(shipTemplate, /Ship the current work by delegating/); + assert.match(shipTemplate, /Ship the current work by dispatching/); assert.match(shipTemplate, /Ensure Feature Branch/); - assert.match(shipTemplate, //); - assert.match(shipTemplate, /\n\/branch\nBranch naming guidance: \n<\/dispatch>/); - assert.match(shipTemplate, /Store the subagent result as ``/); - assert.match(shipTemplate, /Store the subagent result as ``/); - assert.match(shipTemplate, /\n\/commit\nAdditional context: \n<\/dispatch>/); - assert.match(shipTemplate, /Store the subagent result as ``/); - assert.match(shipTemplate, /\n\/pr\/create\nBase branch: \nAdditional context: \n<\/dispatch>/); + assert.match(shipTemplate, //); + assert.match(shipTemplate, /\nBranch naming guidance: \n<\/kompass_session_command>/); + assert.match(shipTemplate, /Store the dispatch result as ``/); + assert.match(shipTemplate, /Store the dispatch result as ``/); + assert.match(shipTemplate, //); + assert.match(shipTemplate, /\nAdditional context: \n<\/kompass_session_command>/); + assert.match(shipTemplate, /Store the dispatch result as ``/); + assert.match(shipTemplate, //); + assert.match(shipTemplate, /\nBase branch: \nAdditional context: \n<\/kompass_session_command>/); assert.doesNotMatch(shipTemplate, /<%/); }); @@ -509,17 +513,20 @@ describe("applyCommandsConfig", () => { // PR Author content is now inline in pr/create, not embedded here assert.match(ticketDevTemplate, /## Goal/); assert.match(ticketDevTemplate, /Implement a ticket/); - assert.match(ticketDevTemplate, //); - assert.match(ticketDevTemplate, /\n\/dev\nTicket reference: \nTicket context: \nAdditional context: \n<\/dispatch>/); - assert.match(ticketDevTemplate, /\n\/branch\nBranch naming guidance: \nAdditional context: \n<\/dispatch>/); - assert.match(ticketDevTemplate, /\n\/commit-and-push\nTicket reference: \nTicket summary: \nAdditional context: \n<\/dispatch>/); - assert.match(ticketDevTemplate, /\n\/pr\/create\nTicket reference: \nTicket context: \nAdditional context: \n<\/dispatch>/); + assert.match(ticketDevTemplate, //); + assert.match(ticketDevTemplate, /\nTicket reference: \nTicket context: \nAdditional context: \n<\/kompass_session_command>/); + assert.match(ticketDevTemplate, //); + assert.match(ticketDevTemplate, /\nBranch naming guidance: \nAdditional context: \n<\/kompass_session_command>/); + assert.match(ticketDevTemplate, //); + assert.match(ticketDevTemplate, /\nTicket reference: \nTicket summary: \nAdditional context: \n<\/kompass_session_command>/); + assert.match(ticketDevTemplate, //); + assert.match(ticketDevTemplate, /\nTicket reference: \nTicket context: \nAdditional context: \n<\/kompass_session_command>/); assert.doesNotMatch(ticketDevTemplate, / { + test("embeds literal session_command blocks in todo command", async () => { delete process.env.CI; const cfg: { command?: Record } = {}; @@ -530,12 +537,13 @@ describe("applyCommandsConfig", () => { assert.match(todoTemplate, /## Goal/); assert.match(todoTemplate, /Work through a todo file one pending item at a time/); - assert.match(todoTemplate, //); - assert.match(todoTemplate, /\n\/ticket\/plan\nTask: \nTask context: \nAdditional context: \n<\/dispatch>/); - assert.match(todoTemplate, //); + assert.match(todoTemplate, //); + assert.match(todoTemplate, /\nTask: \nTask context: \nAdditional context: \n<\/kompass_session_command>/); + assert.match(todoTemplate, //); assert.match(todoTemplate, /Current plan: \nPlan feedback: /); - assert.match(todoTemplate, /\n\/dev\nPlan: \nTask: \nTask context: \nAdditional context: \n<\/dispatch>/); - assert.match(todoTemplate, /\n\/commit\nTask: \nAdditional context: \n<\/dispatch>/); + assert.match(todoTemplate, /\nPlan: \nTask: \nTask context: \nAdditional context: \n<\/kompass_session_command>/); + assert.match(todoTemplate, //); + assert.match(todoTemplate, /\nTask: \nAdditional context: \n<\/kompass_session_command>/); assert.doesNotMatch(todoTemplate, / { - test("expands slash commands for task tool calls", async () => { + test("keeps raw task prompts unchanged", async () => { const output = { args: { prompt: "/review auth bug", @@ -25,20 +24,16 @@ describe("getTaskToolExecution", () => { callID: "call-1", }, output, - process.cwd(), ); + assert.equal(execution?.prompt, "/review auth bug"); assert.equal(execution?.raw_prompt, "/review auth bug"); assert.equal(execution?.description, "Run review command"); assert.equal(execution?.subagent_type, "reviewer"); assert.equal(execution?.command, "@reviewer /review auth bug"); - assert.equal(execution?.command_name, "review"); - assert.equal(execution?.command_arguments, "auth bug"); - assert.match(execution?.prompt ?? "", /\s*auth bug\s*<\/arguments>/); - assert.equal(output.args.prompt, execution?.prompt); }); - test("prefers the raw prompt over summarized command metadata", async () => { + test("prefers the raw prompt over command metadata", async () => { const output = { args: { prompt: "/review auth bug with multiline\nextra context", @@ -55,12 +50,10 @@ describe("getTaskToolExecution", () => { callID: "call-1", }, output, - process.cwd(), ); - assert.equal(execution?.command_name, "review"); - assert.equal(execution?.command_arguments, "auth bug with multiline\nextra context"); - assert.match(execution?.prompt ?? "", /\s*auth bug with multiline\nextra context\s*<\/arguments>/); + assert.equal(execution?.prompt, "/review auth bug with multiline\nextra context"); + assert.equal(execution?.command, "@reviewer /review auth bug"); }); test("ignores non-task tool calls", async () => { @@ -75,7 +68,6 @@ describe("getTaskToolExecution", () => { prompt: "should not be read", }, }, - process.cwd(), ); assert.equal(execution, undefined); @@ -94,13 +86,12 @@ describe("getTaskToolExecution", () => { subagent_type: "planner", }, }, - process.cwd(), ); assert.equal(execution, undefined); }); - test("falls back to the raw task prompt when the command is unknown", async () => { + test("returns the raw task prompt when the command is unknown", async () => { const execution = await getTaskToolExecution( { tool: "task", @@ -113,15 +104,12 @@ describe("getTaskToolExecution", () => { command: "/unknown auth bug", }, }, - process.cwd(), ); assert.deepEqual(execution, { prompt: "/unknown auth bug", raw_prompt: "/unknown auth bug", command: "/unknown auth bug", - command_name: undefined, - command_arguments: undefined, description: undefined, subagent_type: undefined, }); @@ -144,25 +132,11 @@ describe("getTaskToolExecution", () => { callID: "call-1", }, output, - process.cwd(), ); + assert.equal(execution?.prompt, "/branch Branch naming guidance: fix login redirect"); assert.equal(execution?.command, "/branch Branch naming guidance: fix login redirect"); - assert.equal(execution?.command_name, "branch"); - assert.equal(execution?.command_arguments, "Branch naming guidance: fix login redirect"); - assert.match(execution?.prompt ?? "", /## Goal/); - assert.match(execution?.prompt ?? "", /\s*Branch naming guidance: fix login redirect\s*<\/arguments>/); - assert.equal(output.args.prompt, execution?.prompt); - }); -}); - -describe("expandSlashCommandPrompt", () => { - test("expands command templates using slash command arguments", async () => { - const execution = await expandSlashCommandPrompt(process.cwd(), "@general /review asd"); - - assert.deepEqual(execution?.command, "review"); - assert.deepEqual(execution?.arguments, "asd"); - assert.match(execution?.prompt ?? "", /\s*asd\s*<\/arguments>/); + assert.equal(output.args.prompt, undefined); }); }); diff --git a/packages/opencode/test/tool-registration.test.ts b/packages/opencode/test/tool-registration.test.ts index 9220fa2..629b6b5 100644 --- a/packages/opencode/test/tool-registration.test.ts +++ b/packages/opencode/test/tool-registration.test.ts @@ -13,9 +13,17 @@ type MockLogEntry = { type MockClient = { logs: MockLogEntry[]; + sessionCommands: Array>; + sessionPrompts: Array>; + sessionPromptAsyncs: Array>; app: { log(entry: MockLogEntry): Promise; }; + session: { + command(options: Record): Promise>; + prompt(options: Record): Promise>; + promptAsync(options: Record): Promise>; + }; instance: { dispose(): Promise; }; @@ -42,15 +50,55 @@ async function withTempHome(run: (homeDir: string) => Promise): Promise function createMockClient(): MockClient { const logs: MockLogEntry[] = []; + const sessionCommands: Array> = []; + const sessionPrompts: Array> = []; + const sessionPromptAsyncs: Array> = []; + + function response(text: string, id: string) { + return { + data: { + info: { id }, + parts: [{ type: "text", text }], + }, + error: undefined, + request: new Request("http://localhost/mock"), + response: new Response(), + }; + } return { logs, + sessionCommands, + sessionPrompts, + sessionPromptAsyncs, app: { log: async (entry: (typeof logs)[number]) => { logs.push(entry); return true; }, }, + session: { + command: async (options: Record) => { + sessionCommands.push(options); + const body = (options.body ?? {}) as { command?: string; arguments?: string }; + return response(`Ran /${body.command ?? "unknown"} ${body.arguments ?? ""}`.trim(), "assistant-command"); + }, + prompt: async (options: Record) => { + sessionPrompts.push(options); + const parts = ((options.body ?? {}) as { parts?: Array<{ text?: string }> }).parts ?? []; + const text = parts.map((part) => part.text ?? "").join("\n").trim(); + return response(text, "assistant-prompt"); + }, + promptAsync: async (options: Record) => { + sessionPromptAsyncs.push(options); + return { + data: undefined, + error: undefined, + request: new Request("http://localhost/mock"), + response: new Response(null, { status: 204 }), + }; + }, + }, instance: { dispose: async () => true, }, @@ -65,11 +113,13 @@ describe("createOpenCodeTools", () => { }) as never, createMockClient() as never, process.cwd()); assert.ok(tools.kompass_changes_load); + assert.ok(tools.kompass_session_command); assert.ok(tools.kompass_pr_load); assert.ok(tools.kompass_pr_sync); assert.ok(tools.kompass_ticket_load); assert.ok(tools.kompass_ticket_sync); assert.equal(tools.changes_load, undefined); + assert.equal(tools.session_command, undefined); assert.equal(tools.pr_load, undefined); assert.equal(tools.pr_sync, undefined); assert.equal(tools.ticket_load, undefined); @@ -88,6 +138,7 @@ describe("createOpenCodeTools", () => { `{ "tools": { "changes_load": { "enabled": false }, + "session_command": { "enabled": false }, "pr_load": { "enabled": false }, "pr_sync": { "enabled": false }, "ticket_sync": { @@ -123,6 +174,9 @@ describe("createOpenCodeTools", () => { `{ // jsonc config should work "tools": { + "session_command": { + "enabled": false + }, "pr_load": { "enabled": true, "name": "pull_request_context", @@ -202,7 +256,80 @@ describe("createOpenCodeTools", () => { }); }); - test("observes slash-command expansion failures without throwing", async () => { + test("session_command queues expanded commands into the current session", async () => { + await withTempHome(async () => { + const client = createMockClient(); + const tools = await createOpenCodeTools((() => { + throw new Error("not implemented"); + }) as never, client as never, process.cwd()); + + const output = await (tools.kompass_session_command as any).execute( + { command: "review", body: "auth bug" }, + { + sessionID: "session-1", + messageID: "message-1", + agent: "worker", + directory: process.cwd(), + worktree: process.cwd(), + abort: new AbortController().signal, + metadata() {}, + ask: async () => {}, + }, + ); + + const result = JSON.parse(output); + + assert.equal(result.agent, "reviewer"); + assert.equal(result.command, "review"); + assert.equal(result.body, "auth bug"); + assert.equal(result.expanded, true); + assert.equal(result.queued, true); + assert.equal(result.mode, "prompt_async"); + assert.match(result.prompt, /kompass_changes_load/); + assert.doesNotMatch(result.prompt, /`changes_load`/); + assert.equal(client.sessionCommands.length, 0); + assert.equal(client.sessionPromptAsyncs.length, 1); + assert.deepEqual(client.sessionPromptAsyncs[0], { + path: { id: "session-1" }, + query: { directory: process.cwd() }, + body: { + agent: "reviewer", + parts: [{ type: "text", text: result.prompt }], + }, + }); + assert.equal(client.sessionPrompts.length, 0); + }); + }); + + test("command tool rejects missing commands", async () => { + await withTempHome(async () => { + const client = createMockClient(); + const tools = await createOpenCodeTools((() => { + throw new Error("not implemented"); + }) as never, client as never, process.cwd()); + + await assert.rejects( + (tools.kompass_session_command as any).execute( + { command: " ", body: "Investigate the redirect bug", agent: "worker" }, + { + sessionID: "session-2", + messageID: "message-2", + agent: "worker", + directory: process.cwd(), + worktree: process.cwd(), + abort: new AbortController().signal, + metadata() {}, + ask: async () => {}, + }, + ), + /requires a command/, + ); + assert.equal(client.sessionCommands.length, 0); + assert.equal(client.sessionPrompts.length, 0); + }); + }); + + test("does not expand slash commands in the task hook", async () => { await withTempHome(async () => { const client = createMockClient(); const tempDir = await mkdtemp(path.join(os.tmpdir(), "kompass-tools-bad-config-")); @@ -244,7 +371,7 @@ describe("createOpenCodeTools", () => { assert.equal(output.args.prompt, "/review auth bug"); assert.ok(client.logs.some((entry) => entry.body?.message?.includes("Skipping Kompass tool registration"))); - assert.ok(client.logs.some((entry) => entry.body?.message?.includes("Failed to expand slash command for task tool"))); + assert.ok(client.logs.some((entry) => entry.body?.message?.includes("Executing Kompass task tool"))); } finally { await rm(tempDir, { recursive: true, force: true }); } diff --git a/packages/web/src/components/CommandShowcase.astro b/packages/web/src/components/CommandShowcase.astro index 14b0f65..559792c 100644 --- a/packages/web/src/components/CommandShowcase.astro +++ b/packages/web/src/components/CommandShowcase.astro @@ -123,6 +123,25 @@ const scenarios: CommandScenario[] = [ ] } ] }, + { + id: 'merge', + label: '/merge', + command: '/merge origin/main', + agentName: 'Worker', + task: 'Merge another branch into the current branch', + group: 'core', + steps: [ + { id: 'thinking', phase: 'thinking', content: 'Confirm the working tree is clean, resolve the merge source, run the merge, and only continue if conflicts can be resolved confidently.' }, + { id: 'tools', phase: 'tool_calls', toolCalls: [ + { tool: 'bash', args: 'git status --short', status: 'complete' }, + { tool: 'bash', args: 'git merge origin/main', status: 'complete' } + ] }, + { id: 'output', phase: 'output', output: [ + 'Merged origin/main into docs/reference-cleanup', + 'Commit: `9f8e7d6`' + ] } + ] + }, { id: 'rmslop', label: '/rmslop', @@ -193,10 +212,10 @@ const scenarios: CommandScenario[] = [ { id: 'thinking', phase: 'thinking', content: 'Load the ticket, then orchestrate implementation, branch creation, commit-and-push, and PR creation in order.' }, { id: 'tools', phase: 'tool_calls', toolCalls: [ { tool: 'ticket_load', args: '#42', status: 'complete' }, - { tool: 'dispatch', args: 'worker · /dev', status: 'complete' }, - { tool: 'dispatch', args: 'worker · /branch', status: 'complete' }, - { tool: 'dispatch', args: 'worker · /commit-and-push', status: 'complete' }, - { tool: 'dispatch', args: 'worker · /pr/create', status: 'complete' } + { tool: 'session_command', args: 'worker · dev', status: 'complete' }, + { tool: 'session_command', args: 'worker · branch', status: 'complete' }, + { tool: 'session_command', args: 'worker · commit-and-push', status: 'complete' }, + { tool: 'session_command', args: 'worker · pr/create', status: 'complete' } ] }, { id: 'output', phase: 'output', output: [ 'Implemented ticket: refresh command docs', @@ -362,9 +381,9 @@ const scenarios: CommandScenario[] = [ steps: [ { id: 'thinking', phase: 'thinking', content: 'Ensure a work branch, delegate commit creation, then delegate PR creation and stop on blockers.' }, { id: 'tools', phase: 'tool_calls', toolCalls: [ - { tool: 'dispatch', args: 'worker · /branch', status: 'complete' }, - { tool: 'dispatch', args: 'worker · /commit', status: 'complete' }, - { tool: 'dispatch', args: 'worker · /pr/create', status: 'complete' } + { tool: 'session_command', args: 'worker · branch', status: 'complete' }, + { tool: 'session_command', args: 'worker · commit', status: 'complete' }, + { tool: 'session_command', args: 'worker · pr/create', status: 'complete' } ] }, { id: 'output', phase: 'output', output: [ 'Ship flow complete', @@ -384,10 +403,10 @@ const scenarios: CommandScenario[] = [ { id: 'thinking', phase: 'thinking', content: 'Take the first unchecked item only, plan it, get approval, implement it, commit it, mark it complete, and loop.' }, { id: 'tools', phase: 'tool_calls', toolCalls: [ { tool: 'read', args: '@TODO.md', status: 'complete' }, - { tool: 'dispatch', args: 'planner · /ticket/plan', status: 'complete' }, + { tool: 'session_command', args: 'planner · ticket/plan', status: 'complete' }, { tool: 'question', args: 'Plan Review', status: 'complete' }, - { tool: 'dispatch', args: 'worker · /dev', status: 'complete' }, - { tool: 'dispatch', args: 'worker · /commit', status: 'complete' } + { tool: 'session_command', args: 'worker · dev', status: 'complete' }, + { tool: 'session_command', args: 'worker · commit', status: 'complete' } ] }, { id: 'output', phase: 'output', output: [ 'Todo complete: @TODO.md', @@ -556,19 +575,19 @@ const orchestrationScenarios = scenarios.filter(s => s.group === 'orchestration'
- {step.toolCalls.some(t => t.tool === 'dispatch') ? 'Delegated' : 'Explored'} + {step.toolCalls.some(t => t.tool === 'session_command') ? 'Queued' : 'Explored'} - {step.toolCalls.length} {step.toolCalls.some(t => t.tool === 'dispatch') ? 'subagents' : 'reads, 1 search'} + {step.toolCalls.length} {step.toolCalls.some(t => t.tool === 'session_command') ? 'session commands' : 'reads, 1 search'}
{step.toolCalls.map((tool) => ( -
- {tool.tool === 'dispatch' ? ( +
+ {tool.tool === 'session_command' ? ( <> - {tool.args} + {tool.args} ) : ( <> @@ -1362,7 +1381,7 @@ const orchestrationScenarios = scenarios.filter(s => s.group === 'orchestration' font-size: 0.8rem; } - .tool-call--dispatch { + .tool-call--session-command { display: flex; align-items: center; gap: 0.6rem; @@ -1381,7 +1400,7 @@ const orchestrationScenarios = scenarios.filter(s => s.group === 'orchestration' animation: subagentPulse 1.5s ease-in-out infinite; } - .tool-call__dispatch-cmd { + .tool-call__session-command { color: var(--text); font-family: var(--font-mono); font-size: 0.85rem; diff --git a/packages/web/src/content/docs/docs/adapters/opencode.mdx b/packages/web/src/content/docs/docs/adapters/opencode.mdx index c8128d9..9c855e2 100644 --- a/packages/web/src/content/docs/docs/adapters/opencode.mdx +++ b/packages/web/src/content/docs/docs/adapters/opencode.mdx @@ -33,7 +33,9 @@ You can use overrides to: Most command execution runs on the bundled `worker` agent. -Kompass also provides the `worker`, `navigator`, `planner`, and `reviewer` agent roles for structured subagent workflows and review-specific execution. +Kompass also provides the `worker`, `navigator`, `planner`, and `reviewer` agent roles for structured workflow execution and review-specific execution. + +OpenCode also exposes the `session_command` tool, which resolves explicit command-plus-body inputs and queues the rendered prompt back into the current session. ## Useful OpenCode notes diff --git a/packages/web/src/content/docs/docs/reference/agents/index.mdx b/packages/web/src/content/docs/docs/reference/agents/index.mdx index 3ba6948..a081790 100644 --- a/packages/web/src/content/docs/docs/reference/agents/index.mdx +++ b/packages/web/src/content/docs/docs/reference/agents/index.mdx @@ -15,7 +15,7 @@ Generic worker role with minimal built-in behavior. It is the default execution ### `navigator` -Owns structured multi-step workflows locally, preserves state and stop conditions, and forwards literal `` bodies to subagents when commands require delegation. +Owns structured multi-step workflows locally, preserves state and stop conditions, and forwards literal `` blocks through the `session_command` tool when commands require queued same-session work. ### `planner` diff --git a/packages/web/src/content/docs/docs/reference/agents/navigator.mdx b/packages/web/src/content/docs/docs/reference/agents/navigator.mdx index 9c7d0d0..33b6423 100644 --- a/packages/web/src/content/docs/docs/reference/agents/navigator.mdx +++ b/packages/web/src/content/docs/docs/reference/agents/navigator.mdx @@ -5,17 +5,17 @@ description: Orchestrator agent for structured multi-step workflows. ## Role -`navigator` owns multi-step command workflows locally. It keeps state, ordering, approval gates, and stop conditions in one place instead of handing orchestration off to subagents. +`navigator` owns multi-step command workflows locally. It keeps state, ordering, approval gates, and stop conditions in one place instead of handing orchestration off elsewhere. ## Best for - pause-and-resume workflows - stepwise orchestration such as `/ship` and `/todo` -- commands that mix local state handling with delegated leaf work +- commands that mix local state handling with queued `session_command` leaf steps ## Key behavior -- forwards literal `` bodies exactly after variable substitution -- delegates only explicit focused leaf work -- preserves workflow ownership even when subagents are used -- stops and reports blockers when a delegated step fails or returns incomplete work +- forwards literal `...` bodies exactly after variable substitution +- calls `session_command` with explicit `command`, `body`, and `agent` +- queues the next same-session turn instead of waiting for the queued command result +- stops and reports blockers when the workflow itself cannot continue safely diff --git a/packages/web/src/content/docs/docs/reference/commands/index.mdx b/packages/web/src/content/docs/docs/reference/commands/index.mdx index 7cf6e3b..f5f021b 100644 --- a/packages/web/src/content/docs/docs/reference/commands/index.mdx +++ b/packages/web/src/content/docs/docs/reference/commands/index.mdx @@ -7,7 +7,7 @@ Kompass ships workflow-oriented commands authored as explicit templates in `pack ## Groups -- Core: `/ask`, `/branch`, `/commit`, `/commit-and-push`, `/learn`, `/rmslop` +- Core: `/ask`, `/branch`, `/commit`, `/commit-and-push`, `/learn`, `/merge`, `/rmslop` - Ticket: `/ticket/ask`, `/ticket/create`, `/ticket/dev`, `/ticket/plan`, `/ticket/plan-and-sync` - PR: `/pr/create`, `/pr/fix`, `/pr/review`, `/review` - Orchestration: `/dev`, `/ship`, `/todo` @@ -16,7 +16,7 @@ Kompass ships workflow-oriented commands authored as explicit templates in `pack - each command is a documented workflow, not an opaque prompt - placeholders like ``, ``, and `` are normalized before execution -- navigator workflows orchestrate locally and delegate focused leaf work with literal `` blocks +- navigator workflows orchestrate locally and queue focused leaf work with literal `` blocks - outputs are deterministic, reviewable, and usually terminal ## Core workflows @@ -61,6 +61,14 @@ Extracts non-obvious learnings from the current session and documents them into - arguments: optional focus scope or extra guidance - expected tools: `read`, file edit tools +### `/merge` + +Merges a provided branch into the current branch, defaulting to the repo base branch, and resolves conflicts with a best-effort preference for the incoming branch. + +- usage: `/merge [source-or-guidance]` +- arguments: optional merge source branch or ref plus additional merge guidance +- expected tools: `git status`, `git merge`, file-edit tools for conflict resolution, `git add`, `git merge --continue` + ### `/rmslop` Removes AI-generated code slop and inconsistencies from branch changes. @@ -93,7 +101,7 @@ Implements a ticket by orchestrating `/dev`, `/branch`, `/commit-and-push`, and - usage: `/ticket/dev ` - arguments: ticket reference, URL, file path, or raw request -- expected tools: `ticket_load`, delegated `/dev`, delegated `/branch`, delegated `/commit-and-push`, delegated `/pr/create` +- expected tools: `ticket_load`, `session_command(dev)`, `session_command(branch)`, `session_command(commit-and-push)`, `session_command(pr/create)` ### `/ticket/plan` @@ -161,7 +169,7 @@ Ships current work by delegating branch creation, commit creation, and PR creati - usage: `/ship [context]` - arguments: optional base branch, branch naming guidance, or additional shipping context -- expected tools: delegated `/branch`, delegated `/commit`, delegated `/pr/create` +- expected tools: `session_command(branch)`, `session_command(commit)`, `session_command(pr/create)` ### `/todo` @@ -169,4 +177,4 @@ Works through a todo file one pending item at a time with planning, approval, im - usage: `/todo [@todo-file]` - arguments: optional `@file` reference and execution guidance; defaults to `@TODO.md` -- expected tools: `read`, delegated `/ticket/plan`, `question`, delegated `/dev`, delegated `/commit` +- expected tools: `read`, `question`, `session_command(ticket/plan)`, `session_command(dev)`, `session_command(commit)` diff --git a/packages/web/src/content/docs/docs/reference/commands/merge.mdx b/packages/web/src/content/docs/docs/reference/commands/merge.mdx new file mode 100644 index 0000000..7cf1dfc --- /dev/null +++ b/packages/web/src/content/docs/docs/reference/commands/merge.mdx @@ -0,0 +1,38 @@ +--- +title: /merge +description: Merge a branch into the current branch with best-effort conflict resolution. +--- + +## Purpose + +Use `/merge` to merge another branch or ref into the current branch and keep moving when conflicts can be resolved confidently. + +## Usage + +```text +/merge [source-or-guidance] +``` + +## Typical flow + +- resolve the merge source from arguments or the repo base branch +- stop when the working tree is dirty or the source ref cannot be found +- run `git merge ` +- if conflicts appear, resolve them file by file and stage each resolved file explicitly +- finish with `git merge --continue` only after all conflicts are staged and resolved + +## Common tools + +- `bash(git status)` +- `bash(git merge)` +- file edit tools +- `bash(git add)` +- `bash(git merge --continue)` + +## Output + +- `Merge blocked: working tree is not clean` +- `Merge blocked: source branch not found` +- `Merge skipped: already on ` +- `Merge blocked: unresolved conflicts remain` +- `Merged into ` with the merge commit hash diff --git a/packages/web/src/content/docs/docs/reference/commands/ship.mdx b/packages/web/src/content/docs/docs/reference/commands/ship.mdx index 9729b58..6890e35 100644 --- a/packages/web/src/content/docs/docs/reference/commands/ship.mdx +++ b/packages/web/src/content/docs/docs/reference/commands/ship.mdx @@ -5,7 +5,7 @@ description: Move current work through branch, commit, and PR creation as a fast ## Purpose -Use `/ship` when you want a fast path from current work to a PR through local orchestration. +Use `/ship` when you want a fast path from current work to a PR through local orchestration and queued same-session command steps. ## Usage @@ -16,12 +16,11 @@ Use `/ship` when you want a fast path from current work to a PR through local or ## Typical flow - ensure there is a suitable work branch -- delegate `/commit` -- delegate `/pr/create` +- queue `branch`, `commit`, and `pr/create` through `session_command` - stop on blockers and report `Nothing to ship` when PR creation finds no shippable work ## Common tools -- `dispatch(/branch)` -- `dispatch(/commit)` -- `dispatch(/pr/create)` +- `session_command(worker, branch)` +- `session_command(worker, commit)` +- `session_command(worker, pr/create)` diff --git a/packages/web/src/content/docs/docs/reference/commands/ticket-dev.mdx b/packages/web/src/content/docs/docs/reference/commands/ticket-dev.mdx index 32a822f..2db2ffd 100644 --- a/packages/web/src/content/docs/docs/reference/commands/ticket-dev.mdx +++ b/packages/web/src/content/docs/docs/reference/commands/ticket-dev.mdx @@ -5,7 +5,7 @@ description: Implement a ticket by orchestrating development, branching, commit- ## Purpose -Use `/ticket/dev` when implementation starts from a tracked ticket and should run through shipping. +Use `/ticket/dev` when implementation starts from a tracked ticket and should run through shipping with queued same-session command steps. ## Usage @@ -16,15 +16,15 @@ Use `/ticket/dev` when implementation starts from a tracked ticket and should ru ## Typical flow - load the ticket and its surrounding context -- delegate `/dev` for implementation and validation -- delegate `/branch` for branch naming from the ticket summary -- delegate `/commit-and-push` -- delegate `/pr/create` and return the resulting PR URL +- queue `dev` for implementation and validation +- queue `branch` for branch naming from the ticket summary +- queue `commit-and-push` +- queue `pr/create` as the next shipping step ## Common tools - `ticket_load` -- `dispatch(/dev)` -- `dispatch(/branch)` -- `dispatch(/commit-and-push)` -- `dispatch(/pr/create)` +- `session_command(worker, dev)` +- `session_command(worker, branch)` +- `session_command(worker, commit-and-push)` +- `session_command(worker, pr/create)` diff --git a/packages/web/src/content/docs/docs/reference/commands/todo.mdx b/packages/web/src/content/docs/docs/reference/commands/todo.mdx index 82884a5..04de854 100644 --- a/packages/web/src/content/docs/docs/reference/commands/todo.mdx +++ b/packages/web/src/content/docs/docs/reference/commands/todo.mdx @@ -5,7 +5,7 @@ description: Work through a todo file one item at a time with planning, approval ## Purpose -Use `/todo` for structured pause-and-resume work from a checklist file. +Use `/todo` for structured pause-and-resume work from a checklist file with explicit planning, approval, and queued implementation steps. ## Usage @@ -17,14 +17,14 @@ Use `/todo` for structured pause-and-resume work from a checklist file. - load `@TODO.md` by default or a provided `@file` - take the first unchecked item only -- delegate `/ticket/plan`, show the plan, and require explicit approval before implementation -- delegate `/dev`, then `/commit`, then mark the checklist item complete +- queue `ticket/plan`, show the plan, and require explicit approval before implementation +- queue `dev`, then `commit`, then mark the checklist item complete - repeat until there are no pending tasks left ## Common tools - `read` - `question` -- `dispatch(/ticket/plan)` -- `dispatch(/dev)` -- `dispatch(/commit)` +- `session_command(planner, ticket/plan)` +- `session_command(worker, dev)` +- `session_command(worker, commit)` diff --git a/packages/web/src/content/docs/docs/reference/tools/index.mdx b/packages/web/src/content/docs/docs/reference/tools/index.mdx index d781db8..51f3862 100644 --- a/packages/web/src/content/docs/docs/reference/tools/index.mdx +++ b/packages/web/src/content/docs/docs/reference/tools/index.mdx @@ -8,6 +8,7 @@ Kompass uses structured tools to keep workflows grounded in repository and GitHu Current built-in tools: - `changes_load` +- `session_command` - `pr_load` - `pr_sync` - `ticket_load` @@ -19,6 +20,10 @@ Current built-in tools: Loads either uncommitted worktree changes or a base/head git comparison, returning structured file diffs and optional commit metadata. +### `session_command` + +Resolves an explicit command plus body into a rendered prompt and queues that prompt back into the current session. + ### `pr_load` Loads normalized PR metadata, review history, issue comments, review threads, repo identity, and viewer identity from GitHub. diff --git a/packages/web/src/content/docs/docs/reference/tools/session-command.mdx b/packages/web/src/content/docs/docs/reference/tools/session-command.mdx new file mode 100644 index 0000000..64f07a5 --- /dev/null +++ b/packages/web/src/content/docs/docs/reference/tools/session-command.mdx @@ -0,0 +1,32 @@ +--- +title: session_command +description: Resolve an explicit command plus body and queue it back into the current session. +--- + +## Purpose + +Use `session_command` when a workflow should queue another Kompass command into the same session instead of spawning separate subagent execution. + +## Inputs + +- `command`: command name without the leading slash +- `body`: literal body content from the `` block +- `agent`: optional target agent override + +## Behavior + +- resolves the command template from the bundled command set +- substitutes `$ARGUMENTS` and positional placeholders from `body` +- prefixes Kompass tool references for the active adapter when needed +- queues the rendered prompt with the adapter's same-session async prompt API +- returns a queue acknowledgement immediately instead of waiting for the queued command result + +## Typical use + +```xml + +auth bug + +``` + +This queues the equivalent of the `/review` command with `auth bug` as the body content.