From 1beaf86a68809427770053576763e9f2f1b0be0e Mon Sep 17 00:00:00 2001 From: dbpolito Date: Mon, 30 Mar 2026 18:58:19 -0300 Subject: [PATCH 1/2] feat: add session command dispatch support - add the merge command and session_command tool across core and OpenCode surfaces - switch navigator-driven workflows from dispatch tags to literal session_command tags - sync docs, config, schema, generated output, and tests for the new command flow --- AGENTS.md | 20 +- README.md | 4 +- kompass.jsonc | 2 + kompass.schema.json | 9 + packages/core/agents/navigator.md | 52 ++--- packages/core/commands/index.ts | 5 + packages/core/commands/merge.md | 101 ++++++++++ packages/core/commands/ship.md | 22 +-- packages/core/commands/ticket/dev.md | 26 +-- packages/core/commands/todo.md | 20 +- packages/core/index.ts | 4 +- packages/core/kompass.jsonc | 2 + packages/core/lib/config.ts | 12 +- packages/core/test/dispatch.test.ts | 61 ++++++ packages/core/tools/dispatch.ts | 119 ++++++++++++ packages/core/tools/index.ts | 16 +- .../opencode/.opencode/agents/navigator.md | 56 ++---- packages/opencode/.opencode/commands/merge.md | 106 +++++++++++ packages/opencode/.opencode/commands/ship.md | 22 +-- .../opencode/.opencode/commands/ticket/dev.md | 26 +-- packages/opencode/.opencode/commands/todo.md | 20 +- packages/opencode/.opencode/kompass.jsonc | 6 + packages/opencode/README.md | 2 +- packages/opencode/index.ts | 179 ++++++++---------- packages/opencode/kompass.jsonc | 2 + packages/opencode/test/agents-config.test.ts | 9 +- .../opencode/test/commands-config.test.ts | 42 ++-- packages/opencode/test/task-hook.test.ts | 42 +--- .../opencode/test/tool-registration.test.ts | 131 ++++++++++++- 29 files changed, 799 insertions(+), 319 deletions(-) create mode 100644 packages/core/commands/merge.md create mode 100644 packages/core/test/dispatch.test.ts create mode 100644 packages/core/tools/dispatch.ts create mode 100644 packages/opencode/.opencode/commands/merge.md diff --git a/AGENTS.md b/AGENTS.md index e11c909..c31363a 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` 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 - 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,25 @@ $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 +136,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 result should be stored, and whether the workflow should continue or STOP based on that result. ``` -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, put the slash command on the first line of the body when needed, then call `session_command` with the rendered body and exact `agent`, 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..1e6bf84 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 `input` set to the rendered 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..85bbfb3 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,24 @@ $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 +48,13 @@ 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..1e46571 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,27 @@ $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 +59,14 @@ 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 +74,14 @@ 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..d16ac85 100644 --- a/packages/core/commands/todo.md +++ b/packages/core/commands/todo.md @@ -41,12 +41,12 @@ $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 +63,14 @@ 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 +79,26 @@ 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..ea03cee --- /dev/null +++ b/packages/core/test/dispatch.test.ts @@ -0,0 +1,61 @@ +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(), "/review auth bug"); + + assert.equal(result.agent, "reviewer"); + assert.equal(result.command, "review"); + assert.equal(result.arguments, "auth bug"); + assert.equal(result.expanded, true); + assert.match(result.body, /\s*auth bug\s*<\/arguments>/); + }); + + test("preserves explicit agent routing when present", async () => { + const result = await resolveSessionCommand(process.cwd(), "@planner /ticket/plan auth bug"); + + assert.equal(result.agent, "planner"); + assert.equal(result.command, "ticket/plan"); + assert.equal(result.arguments, "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( + { input: "/review 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(), "/unknown auth bug"); + + assert.deepEqual(result, { + input: "/unknown auth bug", + command: "unknown", + arguments: "auth bug", + body: "/unknown auth bug", + expanded: false, + }); + }); + + test("rejects plain-text input", async () => { + await assert.rejects( + resolveSessionCommand(process.cwd(), "Investigate the login redirect bug"), + /requires slash-command input/, + ); + }); +}); diff --git a/packages/core/tools/dispatch.ts b/packages/core/tools/dispatch.ts new file mode 100644 index 0000000..64b1974 --- /dev/null +++ b/packages/core/tools/dispatch.ts @@ -0,0 +1,119 @@ +import { resolveCommands } from "../commands/index.ts"; +import { stringifyJson, type ToolDefinition, type ToolExecutionContext } from "./shared.ts"; + +export type SessionCommandResolution = { + input: string; + agent?: string; + command: string; + arguments: string; + body: string; + expanded: boolean; +}; + +type ResolveSessionCommandOptions = { + rewriteBody?: (body: string) => string; +}; + +type ParsedSlashCommand = { + agent?: string; + command: string; + arguments: string; +}; + +function parseSlashCommand(value: string): ParsedSlashCommand | undefined { + const match = value.trim().match(/^(?:@(\S+)\s+)?\/([^\s]+)(?:\s+([\s\S]*))?$/); + + if (!match) return; + + return { + agent: match[1], + command: match[2], + arguments: match[3]?.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; +} + +export async function resolveSessionCommand( + projectRoot: string, + input: string, + options?: ResolveSessionCommandOptions, +): Promise { + const normalizedInput = input.trim(); + const parsedCommand = parseSlashCommand(input); + + if (!parsedCommand) { + throw new Error("session_command requires slash-command input"); + } + + const commands = await resolveCommands(projectRoot); + const definition = commands[parsedCommand.command]; + + if (!definition) { + return { + input: normalizedInput, + ...(parsedCommand.agent ? { agent: parsedCommand.agent } : {}), + command: parsedCommand.command, + arguments: parsedCommand.arguments, + body: normalizedInput, + expanded: false, + }; + } + + let body = expandCommandTemplate(definition.template, parsedCommand.arguments); + + if (options?.rewriteBody) { + body = options.rewriteBody(body); + } + + return { + input: normalizedInput, + agent: parsedCommand.agent ?? definition.agent, + command: parsedCommand.command, + arguments: parsedCommand.arguments, + body, + expanded: true, + }; +} + +export function createSessionCommandTool( + projectRoot: string, + options?: ResolveSessionCommandOptions, +) { + return { + description: "Resolve a slash command for same-session queuing", + args: { + input: { + type: "string", + description: "Raw slash command to resolve", + }, + agent: { + type: "string", + optional: true, + description: "Optional agent override from the dispatch tag", + }, + }, + async execute( + args: { input: string; agent?: string }, + _ctx: ToolExecutionContext, + ) { + const resolved = await resolveSessionCommand(projectRoot, args.input, options); + + return stringifyJson({ + ...resolved, + agent: args.agent?.trim() || resolved.agent, + }); + }, + } satisfies ToolDefinition<{ input: 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..001d518 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 `input` set to the rendered 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..de64eca 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,24 @@ $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 +53,13 @@ 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..b8b952c 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,27 @@ $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 +74,14 @@ 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 +89,14 @@ 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..75a2255 100644 --- a/packages/opencode/.opencode/commands/todo.md +++ b/packages/opencode/.opencode/commands/todo.md @@ -46,12 +46,12 @@ $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 +68,14 @@ 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 +84,26 @@ 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..60965df 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; arguments: string; body: 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.body }], + }, + }); - 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,49 @@ 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 slash command and queue it in the current session", + args: { + input: tool.schema.string().describe("Raw slash command to execute"), + agent: tool.schema.string().describe("Optional agent override from the dispatch tag").optional(), + }, + execute: async (args, context) => { + context.metadata({ + title: `Command ${args.input.trim()}`, + metadata: { + agent: args.agent, + }, + }); + + const resolved = JSON.parse(await definition.execute({ input: args.input, agent: args.agent }, context)) as { + agent?: string; + command: string; + arguments: string; + body: 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 +225,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 +271,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 +294,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 +305,7 @@ const opencodeToolCreators = { execute: (args, context) => definition.execute(args, context), }); }, -} as const; +}; export async function createOpenCodeTools( $: PluginInput["$"], @@ -340,7 +321,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 +409,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,15 @@ 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, /\n\/branch\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, /\n\/commit\nAdditional context: \n<\/kompass_session_command>/); + assert.match(shipTemplate, /Store the dispatch result as ``/); + assert.match(shipTemplate, /\n\/pr\/create\nBase branch: \nAdditional context: \n<\/kompass_session_command>/); assert.doesNotMatch(shipTemplate, /<%/); }); @@ -509,17 +511,17 @@ 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, /\n\/dev\nTicket reference: \nTicket context: \nAdditional context: \n<\/kompass_session_command>/); + assert.match(ticketDevTemplate, /\n\/branch\nBranch naming guidance: \nAdditional context: \n<\/kompass_session_command>/); + assert.match(ticketDevTemplate, /\n\/commit-and-push\nTicket reference: \nTicket summary: \nAdditional context: \n<\/kompass_session_command>/); + assert.match(ticketDevTemplate, /\n\/pr\/create\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 +532,12 @@ 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, /\n\/ticket\/plan\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, /\n\/dev\nPlan: \nTask: \nTask context: \nAdditional context: \n<\/kompass_session_command>/); + assert.match(todoTemplate, /\n\/commit\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..bda20be 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( + { input: "/review 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.arguments, "auth bug"); + assert.equal(result.expanded, true); + assert.equal(result.queued, true); + assert.equal(result.mode, "prompt_async"); + assert.match(result.body, /kompass_changes_load/); + assert.doesNotMatch(result.body, /`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.body }], + }, + }); + assert.equal(client.sessionPrompts.length, 0); + }); + }); + + test("command tool rejects plain-text input", 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( + { input: "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 slash-command input/, + ); + 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 }); } From ecdb6f5660b56667819fb74369bda87b17b74577 Mon Sep 17 00:00:00 2001 From: dbpolito Date: Tue, 31 Mar 2026 09:28:09 -0300 Subject: [PATCH 2/2] refactor: update session command dispatch to use structured command/body/agent format - Replace slash command parsing with explicit command/body/agent structure - Update session_command tool to require command parameter - Add command attribute to all session_command tags in command docs - Update navigator agent docs for new session command format - Update all tests to use new API signatures - Update OpenCode adapter tool registration and execution - Update web documentation for session command changes - Add merge command documentation - Add session-command tool documentation --- AGENTS.md | 12 ++- packages/core/agents/navigator.md | 4 +- packages/core/commands/ship.md | 9 +- packages/core/commands/ticket/dev.md | 12 +-- packages/core/commands/todo.md | 12 +-- packages/core/test/dispatch.test.ts | 29 +++--- packages/core/tools/dispatch.ts | 88 ++++++++----------- .../opencode/.opencode/agents/navigator.md | 4 +- packages/opencode/.opencode/commands/ship.md | 9 +- .../opencode/.opencode/commands/ticket/dev.md | 12 +-- packages/opencode/.opencode/commands/todo.md | 12 +-- packages/opencode/index.ts | 18 ++-- .../opencode/test/commands-config.test.ts | 34 ++++--- .../opencode/test/tool-registration.test.ts | 16 ++-- .../web/src/components/CommandShowcase.astro | 53 +++++++---- .../content/docs/docs/adapters/opencode.mdx | 4 +- .../docs/docs/reference/agents/index.mdx | 2 +- .../docs/docs/reference/agents/navigator.mdx | 12 +-- .../docs/docs/reference/commands/index.mdx | 18 ++-- .../docs/docs/reference/commands/merge.mdx | 38 ++++++++ .../docs/docs/reference/commands/ship.mdx | 11 ++- .../docs/reference/commands/ticket-dev.mdx | 18 ++-- .../docs/docs/reference/commands/todo.mdx | 12 +-- .../docs/docs/reference/tools/index.mdx | 5 ++ .../docs/reference/tools/session-command.mdx | 32 +++++++ 25 files changed, 278 insertions(+), 198 deletions(-) create mode 100644 packages/web/src/content/docs/docs/reference/commands/merge.mdx create mode 100644 packages/web/src/content/docs/docs/reference/tools/session-command.mdx diff --git a/AGENTS.md b/AGENTS.md index c31363a..de913e3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -64,7 +64,7 @@ packages/opencode/.opencode/ # Generated OpenCode output for review - 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 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` 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 +- 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 @@ -105,8 +105,7 @@ $ARGUMENTS ### Delegate Planning - -/ticket/plan + Task: Task context: @@ -118,8 +117,7 @@ Additional context: ### Delegate Implementation - -/dev + Plan: Constraints: @@ -136,13 +134,13 @@ Constraints: Example delegation rule: ```text -Before dispatching, 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 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, then call `session_command` with the rendered body and exact `agent`, and say what queue acknowledgement should be stored and whether the workflow should continue or STOP based on that acknowledgement. +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/packages/core/agents/navigator.md b/packages/core/agents/navigator.md index 1e6bf84..2aead7d 100644 --- a/packages/core/agents/navigator.md +++ b/packages/core/agents/navigator.md @@ -11,8 +11,8 @@ You coordinate structured, multi-step workflows. ## Session Commands -- Treat each `...` block as literal input. -- Render variables, then call `session_command` with `input` set to the rendered body and `agent` set to the tag value. +- 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. diff --git a/packages/core/commands/ship.md b/packages/core/commands/ship.md index 85bbfb3..fa511f1 100644 --- a/packages/core/commands/ship.md +++ b/packages/core/commands/ship.md @@ -23,8 +23,7 @@ $ARGUMENTS ### Ensure Feature Branch - -/branch + Branch naming guidance: @@ -35,8 +34,7 @@ Branch naming guidance: ### Delegate Commit - -/commit + Additional context: @@ -48,8 +46,7 @@ Additional context: ### Delegate PR Creation - -/pr/create + Base branch: Additional context: diff --git a/packages/core/commands/ticket/dev.md b/packages/core/commands/ticket/dev.md index 1e46571..007290a 100644 --- a/packages/core/commands/ticket/dev.md +++ b/packages/core/commands/ticket/dev.md @@ -33,8 +33,7 @@ $ARGUMENTS - Before continuing, send the exact `session_command` block below through `session_command` - -/dev + Ticket reference: Ticket context: Additional context: @@ -47,8 +46,7 @@ Additional context: - Before continuing, send the exact `session_command` block below through `session_command` - -/branch + Branch naming guidance: Additional context: @@ -61,8 +59,7 @@ Additional context: - Before continuing, send the exact `session_command` block below through `session_command` - -/commit-and-push + Ticket reference: Ticket summary: Additional context: @@ -76,8 +73,7 @@ Additional context: - Before continuing, send the exact `session_command` block below through `session_command` - -/pr/create + Ticket reference: Ticket context: Additional context: diff --git a/packages/core/commands/todo.md b/packages/core/commands/todo.md index d16ac85..f4b68f2 100644 --- a/packages/core/commands/todo.md +++ b/packages/core/commands/todo.md @@ -41,8 +41,7 @@ $ARGUMENTS ### Delegate Planning - -/ticket/plan + Task: Task context: Additional context: @@ -63,8 +62,7 @@ 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: @@ -79,8 +77,7 @@ Additional context: ### Delegate Implementation - -/dev + Plan: Task: Task context: @@ -92,8 +89,7 @@ Additional context: ### Delegate Commit - -/commit + Task: Additional context: diff --git a/packages/core/test/dispatch.test.ts b/packages/core/test/dispatch.test.ts index ea03cee..35bbdbd 100644 --- a/packages/core/test/dispatch.test.ts +++ b/packages/core/test/dispatch.test.ts @@ -9,21 +9,25 @@ process.env.HOME = path.join(os.tmpdir(), `kompass-test-home-${process.pid}-core describe("resolveSessionCommand", () => { test("expands known slash commands and infers their default agent", async () => { - const result = await resolveSessionCommand(process.cwd(), "/review auth bug"); + const result = await resolveSessionCommand(process.cwd(), { command: "review", body: "auth bug" }); assert.equal(result.agent, "reviewer"); assert.equal(result.command, "review"); - assert.equal(result.arguments, "auth bug"); + assert.equal(result.body, "auth bug"); assert.equal(result.expanded, true); - assert.match(result.body, /\s*auth bug\s*<\/arguments>/); + assert.match(result.prompt, /\s*auth bug\s*<\/arguments>/); }); test("preserves explicit agent routing when present", async () => { - const result = await resolveSessionCommand(process.cwd(), "@planner /ticket/plan auth bug"); + 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.arguments, "auth bug"); + assert.equal(result.body, "auth bug"); assert.equal(result.expanded, true); }); @@ -31,7 +35,7 @@ describe("resolveSessionCommand", () => { const { createSessionCommandTool } = await import("../index.ts"); const tool = createSessionCommandTool(process.cwd()); const output = await tool.execute( - { input: "/review auth bug", agent: "worker" }, + { command: "review", body: "auth bug", agent: "worker" }, { worktree: process.cwd(), directory: process.cwd() }, ); const result = JSON.parse(output); @@ -41,21 +45,20 @@ describe("resolveSessionCommand", () => { }); test("keeps unknown slash commands dispatchable without expansion", async () => { - const result = await resolveSessionCommand(process.cwd(), "/unknown auth bug"); + const result = await resolveSessionCommand(process.cwd(), { command: "unknown", body: "auth bug" }); assert.deepEqual(result, { - input: "/unknown auth bug", command: "unknown", - arguments: "auth bug", - body: "/unknown auth bug", + body: "auth bug", + prompt: "/unknown\nauth bug", expanded: false, }); }); - test("rejects plain-text input", async () => { + test("rejects missing commands", async () => { await assert.rejects( - resolveSessionCommand(process.cwd(), "Investigate the login redirect bug"), - /requires slash-command input/, + resolveSessionCommand(process.cwd(), { command: " ", body: "auth bug" }), + /requires a command/, ); }); }); diff --git a/packages/core/tools/dispatch.ts b/packages/core/tools/dispatch.ts index 64b1974..0a3d531 100644 --- a/packages/core/tools/dispatch.ts +++ b/packages/core/tools/dispatch.ts @@ -2,11 +2,10 @@ import { resolveCommands } from "../commands/index.ts"; import { stringifyJson, type ToolDefinition, type ToolExecutionContext } from "./shared.ts"; export type SessionCommandResolution = { - input: string; agent?: string; command: string; - arguments: string; body: string; + prompt: string; expanded: boolean; }; @@ -14,29 +13,22 @@ type ResolveSessionCommandOptions = { rewriteBody?: (body: string) => string; }; -type ParsedSlashCommand = { - agent?: string; +type SessionCommandInput = { command: string; - arguments: string; + body?: string; + agent?: string; }; -function parseSlashCommand(value: string): ParsedSlashCommand | undefined { - const match = value.trim().match(/^(?:@(\S+)\s+)?\/([^\s]+)(?:\s+([\s\S]*))?$/); - - if (!match) return; - - return { - agent: match[1], - command: match[2], - arguments: match[3]?.trim() ?? "", - }; +function renderSlashCommand(command: string, body: string) { + const trimmedBody = body.trim(); + return trimmedBody ? `/${command}\n${trimmedBody}` : `/${command}`; } -function expandCommandTemplate(template: string, commandArguments: string) { - const trimmedArguments = commandArguments.trim(); - const positionalArguments = trimmedArguments ? trimmedArguments.split(/\s+/) : []; +function expandCommandTemplate(template: string, commandBody: string) { + const trimmedBody = commandBody.trim(); + const positionalArguments = trimmedBody ? trimmedBody.split(/\s+/) : []; - let expandedTemplate = template.replaceAll("$ARGUMENTS", trimmedArguments); + let expandedTemplate = template.replaceAll("$ARGUMENTS", trimmedBody); for (const [index, argument] of positionalArguments.entries()) { expandedTemplate = expandedTemplate.replaceAll(`$${index + 1}`, argument); @@ -47,42 +39,40 @@ function expandCommandTemplate(template: string, commandArguments: string) { export async function resolveSessionCommand( projectRoot: string, - input: string, + input: SessionCommandInput, options?: ResolveSessionCommandOptions, ): Promise { - const normalizedInput = input.trim(); - const parsedCommand = parseSlashCommand(input); + const normalizedCommand = input.command.trim(); + const normalizedBody = input.body?.trim() ?? ""; - if (!parsedCommand) { - throw new Error("session_command requires slash-command input"); + if (!normalizedCommand) { + throw new Error("session_command requires a command"); } const commands = await resolveCommands(projectRoot); - const definition = commands[parsedCommand.command]; + const definition = commands[normalizedCommand]; if (!definition) { return { - input: normalizedInput, - ...(parsedCommand.agent ? { agent: parsedCommand.agent } : {}), - command: parsedCommand.command, - arguments: parsedCommand.arguments, - body: normalizedInput, + ...(input.agent?.trim() ? { agent: input.agent.trim() } : {}), + command: normalizedCommand, + body: normalizedBody, + prompt: renderSlashCommand(normalizedCommand, normalizedBody), expanded: false, }; } - let body = expandCommandTemplate(definition.template, parsedCommand.arguments); + let prompt = expandCommandTemplate(definition.template, normalizedBody); if (options?.rewriteBody) { - body = options.rewriteBody(body); + prompt = options.rewriteBody(prompt); } return { - input: normalizedInput, - agent: parsedCommand.agent ?? definition.agent, - command: parsedCommand.command, - arguments: parsedCommand.arguments, - body, + agent: input.agent?.trim() || definition.agent, + command: normalizedCommand, + body: normalizedBody, + prompt, expanded: true, }; } @@ -92,28 +82,28 @@ export function createSessionCommandTool( options?: ResolveSessionCommandOptions, ) { return { - description: "Resolve a slash command for same-session queuing", + description: "Resolve a command and body for same-session queuing", args: { - input: { + command: { type: "string", - description: "Raw slash command to resolve", + 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 dispatch tag", + description: "Optional agent override from the session_command tag", }, }, async execute( - args: { input: string; agent?: string }, + args: { command: string; body?: string; agent?: string }, _ctx: ToolExecutionContext, ) { - const resolved = await resolveSessionCommand(projectRoot, args.input, options); - - return stringifyJson({ - ...resolved, - agent: args.agent?.trim() || resolved.agent, - }); + return stringifyJson(await resolveSessionCommand(projectRoot, args, options)); }, - } satisfies ToolDefinition<{ input: string; agent?: string }>; + } satisfies ToolDefinition<{ command: string; body?: string; agent?: string }>; } diff --git a/packages/opencode/.opencode/agents/navigator.md b/packages/opencode/.opencode/agents/navigator.md index 001d518..d4ba60e 100644 --- a/packages/opencode/.opencode/agents/navigator.md +++ b/packages/opencode/.opencode/agents/navigator.md @@ -21,8 +21,8 @@ You coordinate structured, multi-step workflows. ## Session Commands -- Treat each `...` block as literal input. -- Render variables, then call `kompass_session_command` with `input` set to the rendered body and `agent` set to the tag value. +- 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. diff --git a/packages/opencode/.opencode/commands/ship.md b/packages/opencode/.opencode/commands/ship.md index de64eca..99aad77 100644 --- a/packages/opencode/.opencode/commands/ship.md +++ b/packages/opencode/.opencode/commands/ship.md @@ -28,8 +28,7 @@ $ARGUMENTS ### Ensure Feature Branch - -/branch + Branch naming guidance: @@ -40,8 +39,7 @@ Branch naming guidance: ### Delegate Commit - -/commit + Additional context: @@ -53,8 +51,7 @@ Additional context: ### Delegate PR Creation - -/pr/create + Base branch: Additional context: diff --git a/packages/opencode/.opencode/commands/ticket/dev.md b/packages/opencode/.opencode/commands/ticket/dev.md index b8b952c..c987cd6 100644 --- a/packages/opencode/.opencode/commands/ticket/dev.md +++ b/packages/opencode/.opencode/commands/ticket/dev.md @@ -48,8 +48,7 @@ $ARGUMENTS - Before continuing, send the exact `kompass_session_command` block below through `kompass_session_command` - -/dev + Ticket reference: Ticket context: Additional context: @@ -62,8 +61,7 @@ Additional context: - Before continuing, send the exact `kompass_session_command` block below through `kompass_session_command` - -/branch + Branch naming guidance: Additional context: @@ -76,8 +74,7 @@ Additional context: - Before continuing, send the exact `kompass_session_command` block below through `kompass_session_command` - -/commit-and-push + Ticket reference: Ticket summary: Additional context: @@ -91,8 +88,7 @@ Additional context: - Before continuing, send the exact `kompass_session_command` block below through `kompass_session_command` - -/pr/create + Ticket reference: Ticket context: Additional context: diff --git a/packages/opencode/.opencode/commands/todo.md b/packages/opencode/.opencode/commands/todo.md index 75a2255..62274b1 100644 --- a/packages/opencode/.opencode/commands/todo.md +++ b/packages/opencode/.opencode/commands/todo.md @@ -46,8 +46,7 @@ $ARGUMENTS ### Delegate Planning - -/ticket/plan + Task: Task context: Additional context: @@ -68,8 +67,7 @@ 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: @@ -84,8 +82,7 @@ Additional context: ### Delegate Implementation - -/dev + Plan: Task: Task context: @@ -97,8 +94,7 @@ Additional context: ### Delegate Commit - -/commit + Task: Additional context: diff --git a/packages/opencode/index.ts b/packages/opencode/index.ts index 60965df..3f0a30c 100644 --- a/packages/opencode/index.ts +++ b/packages/opencode/index.ts @@ -81,14 +81,14 @@ function getSessionError(result: unknown) { async function executeSessionCommand( client: PluginInput["client"], context: { sessionID: string; directory: string }, - sessionCommand: { agent?: string; command: string; arguments: string; body: string; expanded: boolean }, + 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.body }], + parts: [{ type: "text", text: sessionCommand.prompt }], }, }); @@ -185,24 +185,26 @@ const opencodeToolCreators: Record = { }); return tool({ - description: "Resolve a slash command and queue it in the current session", + description: "Resolve a command and body and queue it in the current session", args: { - input: tool.schema.string().describe("Raw slash command to execute"), - agent: tool.schema.string().describe("Optional agent override from the dispatch tag").optional(), + 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.input.trim()}`, + title: `Command /${args.command.trim()}`, metadata: { + command: args.command, agent: args.agent, }, }); - const resolved = JSON.parse(await definition.execute({ input: args.input, agent: args.agent }, context)) as { + const resolved = JSON.parse(await definition.execute(args, context)) as { agent?: string; command: string; - arguments: string; body: string; + prompt: string; expanded: boolean; }; const dispatched = await executeSessionCommand(client, context, resolved); diff --git a/packages/opencode/test/commands-config.test.ts b/packages/opencode/test/commands-config.test.ts index d5ea9e8..5066617 100644 --- a/packages/opencode/test/commands-config.test.ts +++ b/packages/opencode/test/commands-config.test.ts @@ -434,13 +434,15 @@ describe("applyCommandsConfig", () => { assert.match(shipTemplate, /## Goal/); 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<\/kompass_session_command>/); + 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, /\n\/commit\nAdditional context: \n<\/kompass_session_command>/); + assert.match(shipTemplate, //); + assert.match(shipTemplate, /\nAdditional context: \n<\/kompass_session_command>/); assert.match(shipTemplate, /Store the dispatch result as ``/); - assert.match(shipTemplate, /\n\/pr\/create\nBase branch: \nAdditional context: \n<\/kompass_session_command>/); + assert.match(shipTemplate, //); + assert.match(shipTemplate, /\nBase branch: \nAdditional context: \n<\/kompass_session_command>/); assert.doesNotMatch(shipTemplate, /<%/); }); @@ -511,11 +513,14 @@ 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<\/kompass_session_command>/); - assert.match(ticketDevTemplate, /\n\/branch\nBranch naming guidance: \nAdditional context: \n<\/kompass_session_command>/); - assert.match(ticketDevTemplate, /\n\/commit-and-push\nTicket reference: \nTicket summary: \nAdditional context: \n<\/kompass_session_command>/); - assert.match(ticketDevTemplate, /\n\/pr\/create\nTicket reference: \nTicket context: \nAdditional context: \n<\/kompass_session_command>/); + 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, / { 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<\/kompass_session_command>/); - 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<\/kompass_session_command>/); - assert.match(todoTemplate, /\n\/commit\nTask: \nAdditional context: \n<\/kompass_session_command>/); + 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, / { }) as never, client as never, process.cwd()); const output = await (tools.kompass_session_command as any).execute( - { input: "/review auth bug" }, + { command: "review", body: "auth bug" }, { sessionID: "session-1", messageID: "message-1", @@ -281,12 +281,12 @@ describe("createOpenCodeTools", () => { assert.equal(result.agent, "reviewer"); assert.equal(result.command, "review"); - assert.equal(result.arguments, "auth bug"); + 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.body, /kompass_changes_load/); - assert.doesNotMatch(result.body, /`changes_load`/); + 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], { @@ -294,14 +294,14 @@ describe("createOpenCodeTools", () => { query: { directory: process.cwd() }, body: { agent: "reviewer", - parts: [{ type: "text", text: result.body }], + parts: [{ type: "text", text: result.prompt }], }, }); assert.equal(client.sessionPrompts.length, 0); }); }); - test("command tool rejects plain-text input", async () => { + test("command tool rejects missing commands", async () => { await withTempHome(async () => { const client = createMockClient(); const tools = await createOpenCodeTools((() => { @@ -310,7 +310,7 @@ describe("createOpenCodeTools", () => { await assert.rejects( (tools.kompass_session_command as any).execute( - { input: "Investigate the redirect bug", agent: "worker" }, + { command: " ", body: "Investigate the redirect bug", agent: "worker" }, { sessionID: "session-2", messageID: "message-2", @@ -322,7 +322,7 @@ describe("createOpenCodeTools", () => { ask: async () => {}, }, ), - /requires slash-command input/, + /requires a command/, ); assert.equal(client.sessionCommands.length, 0); assert.equal(client.sessionPrompts.length, 0); 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.