Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions kompass.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,10 @@
"description": {
"type": "string"
},
"name": {
"type": "string",
"minLength": 1
},
"promptPath": {
"type": "string"
},
Expand All @@ -320,6 +324,10 @@
"enabled": {
"type": "boolean"
},
"name": {
"type": "string",
"minLength": 1
},
"description": {
"type": "string"
},
Expand All @@ -341,6 +349,10 @@
"enabled": {
"type": "boolean"
},
"name": {
"type": "string",
"minLength": 1
},
"template": {
"type": "string",
"minLength": 1
Expand Down
51 changes: 48 additions & 3 deletions packages/core/agents/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,15 @@
import { loadKompassConfig, mergeWithDefaults, type AgentDefinition } from "../lib/config.ts";
import { renderTemplate } from "../lib/components.ts";
import {
getConfiguredAgentNames,
getConfiguredCommandNames,
getConfiguredToolNames,
loadKompassConfig,
mergeWithDefaults,
type AgentDefinition,
type AgentName,
type CommandName,
type ToolName,
} from "../lib/config.ts";
import { loadProjectText } from "../lib/text.ts";

export interface ResolvedAgentDefinition
Expand All @@ -18,20 +29,54 @@ export function getAgentDefinitions(config: ReturnType<typeof mergeWithDefaults>

export async function resolveAgents(
projectRoot: string,
options?: {
names?: {
tools?: Partial<Record<ToolName, { name: string }>>;
commands?: Partial<Record<CommandName, { name: string }>>;
agents?: Partial<Record<AgentName, { name: string }>>;
};
},
): Promise<Record<string, ResolvedAgentDefinition>> {
const userConfig = await loadKompassConfig(projectRoot);
const config = mergeWithDefaults(userConfig);
const agentDefinitions = getAgentDefinitions(config);
const names = {
tools: {
...getConfiguredToolNames(config.tools),
...(options?.names?.tools ?? {}),
},
commands: {
...getConfiguredCommandNames(config.commands),
...(options?.names?.commands ?? {}),
},
agents: {
...getConfiguredAgentNames(config.agents),
...(options?.names?.agents ?? {}),
},
};
const agents: Record<string, ResolvedAgentDefinition> = {};

for (const name of config.agents.enabled) {
const definition = agentDefinitions[name];
if (!definition) continue;

agents[name] = {
const resolvedName = names.agents[name as AgentName]?.name ?? name;

agents[resolvedName] = {
description: definition.description,
mode: definition.mode,
...(definition.promptPath ? { prompt: await loadProjectText(definition.promptPath) } : {}),
...(definition.promptPath
? {
prompt: renderTemplate(await loadProjectText(definition.promptPath), {}, {
config: {
shared: config.shared,
tools: names.tools,
commands: names.commands,
agents: names.agents,
},
}),
}
: {}),
permission: definition.permission,
};
}
Expand Down
2 changes: 1 addition & 1 deletion packages/core/agents/navigator.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ You coordinate structured, multi-step workflows.

When you see a `<delegate agent="AGENT_NAME" command="COMMAND_NAME">...</delegate>` block, you MUST make TWO tool calls in sequence:

1. **Expand**: Call `command_expansion` with `command` from the tag and `body` set to the rendered block content
1. **Expand**: Call `<%= it.config.tools.command_expansion.name %>` with `command` from the tag and `body` set to the rendered block content
2. **Delegate**: IMMEDIATELY call `task` with `subagent_type: AGENT_NAME` and `prompt` set to the expanded text from step 1

**CRITICAL RULES:**
Expand Down
4 changes: 2 additions & 2 deletions packages/core/agents/reviewer.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@ Before reviewing, always check repository guidance:

## Review Workflow

1. **Load Changes**: Use `changes_load` to get the changed-file set and structured diffs
1. **Load Changes**: Use `<%= it.config.tools.changes_load.name %>` to get the changed-file set and structured diffs
- In CI or shallow clones, pass explicit base and head refs
- For branch comparisons, treat the returned commit list as the authoritative scope: review the commits ahead of base and use file diffs only as supporting context
- Scan the summary first to understand scope, file states, and risk clusters
- Never switch branches, create local review branches, or otherwise mutate `HEAD`; if a loader fails, prefer reporting the blocker over changing checkout state

2. **Read Code**: Read every changed file individually before finalizing
- Read full current files for added/modified/renamed/copied/untracked files
- `changes_load` gives the changed-file set and diffs, but not full deleted-file contents; for deleted files, inspect previous contents from git
- `<%= it.config.tools.changes_load.name %>` gives the changed-file set and diffs, but not full deleted-file contents; for deleted files, inspect previous contents from git
- Use diffs to prioritize, but don't treat hunks as sufficient context
- Prefer loading changed files directly in the current session so that file context remains available during analysis and write-up
- Use a helper agent only when the changed-file set is too large to review comfortably in one session after the changed paths are already known
Expand Down
2 changes: 1 addition & 1 deletion packages/core/commands/branch.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ $ARGUMENTS

### Load Changes

<%~ include("@change-summary", { rules: "- pass `uncommitted: true` to get uncommitted changes only" }) %>
<%~ include("@change-summary", { config: it.config, rules: "- pass `uncommitted: true` to get uncommitted changes only" }) %>
- Store the loaded change result as `<changes>`
- Store the current branch as `<current-branch>` when it is available

Expand Down
2 changes: 1 addition & 1 deletion packages/core/commands/commit-and-push.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ $ARGUMENTS

### Load Changes

<%~ include("@change-summary", { rules: "- pass `uncommitted: true` to get uncommitted changes only" }) %>
<%~ include("@change-summary", { config: it.config, rules: "- pass `uncommitted: true` to get uncommitted changes only" }) %>
- Store the loaded change result as `<changes>`

### Check Blockers
Expand Down
2 changes: 1 addition & 1 deletion packages/core/commands/commit.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ $ARGUMENTS

### Load Changes

<%~ include("@change-summary", { rules: "- pass `uncommitted: true` to get uncommitted changes only" }) %>
<%~ include("@change-summary", { config: it.config, rules: "- pass `uncommitted: true` to get uncommitted changes only" }) %>
- Store the loaded change result as `<changes>`

### Check Blockers
Expand Down
2 changes: 1 addition & 1 deletion packages/core/commands/dev.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ $ARGUMENTS
### Load Request Context

- If `<request-source>` is defined:
<%~ include("@load-ticket", { source: "<request-source>", result: "<request-context>" }) %>
<%~ include("@load-ticket", { config: it.config, source: "<request-source>", result: "<request-context>" }) %>
- Otherwise, treat `<request>` as `<request-context>`
- If `<request-context>` cannot be determined, STOP and report that the implementation request is missing

Expand Down
43 changes: 39 additions & 4 deletions packages/core/commands/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
import { renderTemplate } from "../lib/components.ts";
import { loadKompassConfig, mergeWithDefaults } from "../lib/config.ts";
import {
getConfiguredAgentNames,
getConfiguredCommandNames,
getConfiguredToolNames,
loadKompassConfig,
mergeWithDefaults,
type AgentName,
type CommandName,
type ToolName,
} from "../lib/config.ts";
import { loadProjectText } from "../lib/text.ts";

interface CommandDefinition {
Expand Down Expand Up @@ -144,13 +153,34 @@ async function loadComponents(

export async function resolveCommands(
projectRoot: string,
options?: { ci?: boolean },
options?: {
ci?: boolean;
names?: {
tools?: Partial<Record<ToolName, { name: string }>>;
commands?: Partial<Record<CommandName, { name: string }>>;
agents?: Partial<Record<AgentName, { name: string }>>;
};
},
): Promise<Record<string, ResolvedCommandDefinition>> {
const userConfig = await loadKompassConfig(projectRoot);
const config = mergeWithDefaults(userConfig);
const isCi = options?.ci ?? !!process.env.CI;

const components = await loadComponents(config.components.paths);
const names = {
tools: {
...getConfiguredToolNames(config.tools),
...(options?.names?.tools ?? {}),
},
commands: {
...getConfiguredCommandNames(config.commands),
...(options?.names?.commands ?? {}),
},
agents: {
...getConfiguredAgentNames(config.agents),
...(options?.names?.agents ?? {}),
},
};
const commands: Record<string, ResolvedCommandDefinition> = {};

for (const name of config.commands.enabled) {
Expand All @@ -171,6 +201,9 @@ export async function resolveCommands(
...commandConfig,
config: {
shared: config.shared,
tools: names.tools,
commands: names.commands,
agents: names.agents,
},
};

Expand All @@ -182,9 +215,11 @@ export async function resolveCommands(
continue;
}

commands[name] = {
const resolvedName = names.commands[name as CommandName]?.name ?? name;

commands[resolvedName] = {
Comment thread
dbpolito marked this conversation as resolved.
description: definition.description,
agent: definition.agent,
agent: names.agents[definition.agent as AgentName]?.name ?? definition.agent,
subtask: definition.subtask ?? !isCi,
template,
...(Object.keys(commandConfig).length > 0 ? { config: commandConfig } : {}),
Expand Down
12 changes: 6 additions & 6 deletions packages/core/commands/pr/create.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ $ARGUMENTS

### Load & Analyze Changes

<%~ include("@change-summary", { rules: "- If `<base>` is defined: call `changes_load` with the `base` parameter set to `<base>`\n- Otherwise: call `changes_load` with no parameters\n- Never pass `uncommitted: true` in this command" }) %>
<%~ include("@change-summary", { config: it.config, rules: "- If `<base>` is defined: call `" + it.config.tools.changes_load.name + "` with the `base` parameter set to `<base>`\n- Otherwise: call `" + it.config.tools.changes_load.name + "` with no parameters\n- Never pass `uncommitted: true` in this command" }) %>

- Store the loaded change result as `<changes>`
- Store the current branch from `<changes>` as `<current-branch>` when it is available
Expand All @@ -44,7 +44,7 @@ $ARGUMENTS
- Report: "There are uncommitted changes. Please commit or stash them before creating a PR."
- List the changed files from `<changes>`
- Do NOT proceed further
- Treat this as a blocker only when `changes_load` returns `comparison: "uncommitted"` from the default call above; never force that mode during PR creation
- Treat this as a blocker only when `<%= it.config.tools.changes_load.name %>` returns `comparison: "uncommitted"` from the default call above; never force that mode during PR creation
- If `<current-branch>` equals `<resolved-base>`:
- STOP immediately
- Report: "You are currently on the base branch (<resolved-base>). Please checkout a feature branch before creating a PR."
Expand Down Expand Up @@ -84,8 +84,8 @@ $ARGUMENTS
### Prepare Ticket Reference

When `<ticket-mode>` is `auto`, create the ticket before creating the PR:
<%~ include("@changes-summary") %>
- Use `ticket_sync` with `refUrl` unset
<%~ include("@changes-summary", { config: it.config }) %>
- Use `<%= it.config.tools.ticket_sync.name %>` with `refUrl` unset
- Set `assignees` to `[@me]` so the created ticket is assigned to yourself as the author
- Store the created issue reference or URL as `<ticket-url>`

Expand All @@ -106,7 +106,7 @@ Run `git push` and use its output as the source of truth.

### Create PR

Use `pr_sync` to create the pull request:
Use `<%= it.config.tools.pr_sync.name %>` to create the pull request:
- This step is PR creation only
- Omit `review`, `replies`, `commentBody`, and `commitId` entirely unless you are intentionally updating or reviewing an existing PR instead of creating one
- Generate a concise title (max 70 chars) summarizing the change and store it as `<pr-title>`
Expand All @@ -127,7 +127,7 @@ Use `pr_sync` to create the pull request:
- Do NOT rely on the branch diff alone to describe the PR; the description must match the commits ahead of `<resolved-base>`
- Keep it compact and directional
- Store the returned URL as `<pr-url>`
- If `pr_sync` reports that a PR already exists for the branch, treat the result as an existing PR
- If `<%= it.config.tools.pr_sync.name %>` reports that a PR already exists for the branch, treat the result as an existing PR
- Track whether the branch was pushed during this run and report that status in the final response

### Output
Expand Down
14 changes: 7 additions & 7 deletions packages/core/commands/pr/fix.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,19 @@ $ARGUMENTS
- If `<arguments>` looks like a PR number (e.g., "123") or URL, store it as `<pr-ref>`
- If `<arguments>` includes extra fix guidance, scope constraints, or priorities, store it as `<additional-context>`
- Otherwise, store `<execution-mode>` as `review`
- If empty, leave `<pr-ref>` undefined and let `pr_load` resolve the default PR context
- If empty, leave `<pr-ref>` undefined and let `<%= it.config.tools.pr_load.name %>` resolve the default PR context

### Load PR Context

<%~ include("@load-pr", { ref: "<pr-ref>", result: "<pr-context>" }) %>
<%~ include("@load-pr", { config: it.config, ref: "<pr-ref>", result: "<pr-context>" }) %>

### Align Local Branch

<%~ include("@align-pr-branch", { action: "analyzing repository files or making code changes for this PR", scope: "inspect or modify local code for this PR" }) %>

### Load Changes

Call `changes_load` with `base: <pr-context.pr.baseRefName>`, `head: <active-branch>`, and `depthHint: <pr-context.pr.commitCount>` only when it is a positive integer. Store as `<changes>`.
Call `<%= it.config.tools.changes_load.name %>` with `base: <pr-context.pr.baseRefName>`, `head: <active-branch>`, and `depthHint: <pr-context.pr.commitCount>` only when it is a positive integer. Store as `<changes>`.

### Analyze Feedback

Expand Down Expand Up @@ -113,17 +113,17 @@ If validation passes:

Only after commit and push succeed, reply to addressed threads:
- Keep replies short and factual—clear signals, no chatter
- Use `pr_sync` to post comments or replies:
- Use `<%= it.config.tools.pr_sync.name %>` to post comments or replies:

```
# General PR comment
pr_sync refUrl="<pr-context.pr.url>" commentBody="<reply-text>"
<%= it.config.tools.pr_sync.name %> refUrl="<pr-context.pr.url>" commentBody="<reply-text>"

# Reply to a specific review thread (use comment.id from threads.comments)
pr_sync refUrl="<pr-context.pr.url>" replies=[{"inReplyTo": <comment-id>, "body": "<reply-text>"}]
<%= it.config.tools.pr_sync.name %> refUrl="<pr-context.pr.url>" replies=[{"inReplyTo": <comment-id>, "body": "<reply-text>"}]

# Follow-up inline review comment on a specific line
pr_sync refUrl="<pr-context.pr.url>" commitId="<commit-sha>" review={"comments": [{"path": "<file-path>", "line": <line-number>, "body": "<reply-text>"}]}
<%= it.config.tools.pr_sync.name %> refUrl="<pr-context.pr.url>" commitId="<commit-sha>" review={"comments": [{"path": "<file-path>", "line": <line-number>, "body": "<reply-text>"}]}
```

Confirm which feedback was addressed and which was intentionally not followed.
Expand Down
18 changes: 9 additions & 9 deletions packages/core/commands/pr/review.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@ $ARGUMENTS

- If `<arguments>` looks like a PR number (e.g., "123") or URL, store it as `<pr-ref>`
- If `<arguments>` includes review focus areas, related tickets, or special concerns, store it as `<additional-context>`
- If empty, leave `<pr-ref>` undefined and let `pr_load` resolve the default PR context
- If empty, leave `<pr-ref>` undefined and let `<%= it.config.tools.pr_load.name %>` resolve the default PR context

### Load PR Context

<%~ include("@load-pr", { ref: "<pr-ref>", result: "<pr-context>" }) %>
<%~ include("@load-pr", { config: it.config, ref: "<pr-ref>", result: "<pr-context>" }) %>

### Align Local Branch

Expand All @@ -32,12 +32,12 @@ $ARGUMENTS

If `<pr-context.pr.body>` links to exactly one clear ticket:
- Store that reference as `<ticket-ref>`
<%~ include("@load-ticket", { source: "<ticket-ref>", result: "<ticket-context>", comments: true }) %>
<%~ include("@load-ticket", { config: it.config, source: "<ticket-ref>", result: "<ticket-context>", comments: true }) %>
- Use `<ticket-context>` for consideration during review

### Load Changes

Call `changes_load` with `base: <pr-context.pr.baseRefName>`, `head: <active-branch>`, and `depthHint: <pr-context.pr.commitCount>` only when it is a positive integer. Store as `<changes>`.
Call `<%= it.config.tools.changes_load.name %>` with `base: <pr-context.pr.baseRefName>`, `head: <active-branch>`, and `depthHint: <pr-context.pr.commitCount>` only when it is a positive integer. Store as `<changes>`.

### Review Changes

Expand Down Expand Up @@ -87,25 +87,25 @@ For multi-line: add `startLine`. For deleted lines: use `side: "LEFT"`.
**If `<publish-grade>` is `★★★★★`:**
<% if (it.config.shared.prApprove === true) { -%>
- Already approved → skip
- Otherwise → first call `pr_sync` with `refUrl: <pr-context.pr.url>` and only `review.approve: true`
- If that approval call fails, immediately call `pr_sync` again with `refUrl: <pr-context.pr.url>` and `review.body` starting with `★★★★★`
- Otherwise → first call `<%= it.config.tools.pr_sync.name %>` with `refUrl: <pr-context.pr.url>` and only `review.approve: true`
- If that approval call fails, immediately call `<%= it.config.tools.pr_sync.name %>` again with `refUrl: <pr-context.pr.url>` and `review.body` starting with `★★★★★`
- If there are no positive summary notes for the fallback review, the fallback body must be exactly `★★★★★`
- Do not pass `review.comments` in the approval attempt or the fallback review
<% } else { -%>
- `pr_sync` with `refUrl: <pr-context.pr.url>` and `review.body` starting with `★★★★★`
- `<%= it.config.tools.pr_sync.name %>` with `refUrl: <pr-context.pr.url>` and `review.body` starting with `★★★★★`
- If there are no positive summary notes, the body must be exactly `★★★★★`
- Do not pass `review.comments`
<% } %>
**If `<publish-grade>` is below `★★★★★`:**
- Call `pr_sync` with:
- Call `<%= it.config.tools.pr_sync.name %>` with:
- `refUrl: <pr-context.pr.url>`
- `review.body`: the grade line first (for example `★★★☆☆`), followed by any non-inline notes
- `review.comments`: inline comments (changed lines only) - **skip lines or concerns already covered by open threads in `<pr-context.threads>` unless the new diff introduces a materially different failure mode**
- Include only findings from `<eligible-findings>`
- Never omit the grade from `review.body` in this branch
- Do not pass any other fields

If `pr_sync` returns a review URL, store it as `<review-url>`.
If `<%= it.config.tools.pr_sync.name %>` returns a review URL, store it as `<review-url>`.

### Output

Expand Down
Loading
Loading