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
14 changes: 6 additions & 8 deletions src/agents/definitions/backlog-manager.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,13 @@ triggers:
defaultEnabled: false
contextPipeline: [pipelineSnapshot]
- event: pm:status-changed
label: Item Added to Backlog
description: When an item is moved to Backlog, check if pipeline is empty and it should be moved to TODO immediately
label: Backlog / Merged Card Move
description: >
Triggers when a card is moved to the Backlog list (new work available) OR to the
Merged list (a blocking dependency was resolved). Both cases may allow the next
backlog item to be pulled into TODO. Note: when enabled, this fires for both
list moves — they cannot be independently toggled.
defaultEnabled: false
parameters:
- name: targetStatus
type: select
label: Target Status
options: [backlog]
defaultValue: backlog
contextPipeline: [pipelineSnapshot]
- event: internal:auto-chain
label: Auto-chain after Splitting
Expand Down
2 changes: 1 addition & 1 deletion src/agents/definitions/contextSteps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -486,7 +486,7 @@ function appendPipelineSection(

if (!PIPELINE_DETAIL_LISTS.has(list.name)) {
for (const item of items) {
sections.push(`- [${item.id}] ${item.title}`);
sections.push(`- [${item.id}] ${item.title}${item.url ? ` (${item.url})` : ''}`);
}
sections.push('');
return;
Expand Down
8 changes: 5 additions & 3 deletions src/agents/prompts/templates/backlog-manager.eta
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ A **Pipeline Snapshot** has been pre-loaded into your context containing the cur

3. **Only if the active pipeline count is below the capacity limit**, proceed to backlog selection. The number of <%= it.workItemNounPlural || 'cards' %> you may move = capacity limit (<%= it.maxInFlightItems ?? 1 %>) minus current active count.

Note: DONE and MERGED <%= it.workItemNounPlural || 'cards' %> are completed work and do not block new work from being selected. The snapshot shows their titles for dependency checking.
Note: DONE and MERGED <%= it.workItemNounPlural || 'cards' %> are completed work and do not block new work from being selected. The snapshot shows their titles and URLs for dependency checking.

## Backlog Selection Process

Expand All @@ -51,7 +51,8 @@ When the active pipeline has capacity:
- References to other <%= it.workItemNounPlural || 'cards' %>: "blocked by", "depends on", "waiting for", "after"
- Cross-references to <%= it.workItemNoun || 'card' %> IDs, URLs, or titles
- Comments indicating external dependencies
- **IMPORTANT**: Before declaring a <%= it.workItemNoun || 'card' %> blocked, check whether the dependency exists in the MERGED list. A dependency in MERGED is **resolved** — it does NOT block. Check the pre-loaded Pipeline Snapshot MERGED section (titles are provided for dependency checking).
- **Stale annotations**: Text like "(not yet merged)" in a description was written when the <%= it.workItemNoun || 'card' %> was created and is **always stale**. Do NOT use it as evidence of blocked status — only the MERGED list itself is authoritative.
- **IMPORTANT — MERGED check**: Before declaring a <%= it.workItemNoun || 'card' %> blocked, scan the MERGED section of the Pipeline Snapshot. Use **substring matching**: if the dependency name (e.g., "SCMIntegration", "OpenCodeEngine", "integrationRoles") appears anywhere within a MERGED title, that dependency is **resolved** and does NOT block. Each MERGED entry also shows its URL in parentheses — if the description references a <%= it.pmName || 'PM' %> link, match it against the URL too. A module or class name found anywhere in a title counts as a match.
4. **Select the best unblocked <%= it.workItemNoun || 'card' %>(s)** considering:
- Smaller, self-contained <%= it.workItemNounPlural || 'cards' %> are preferred
- <%= it.workItemNounPluralCap || 'Cards' %> with clear acceptance criteria
Expand Down Expand Up @@ -101,6 +102,7 @@ Manual intervention may be needed to unblock the backlog.
- ALWAYS read <%= it.workItemNoun || 'card' %> contents before making a selection decision
- <%= it.maxInFlightItems == null || it.maxInFlightItems === 1 ? 'ALWAYS move exactly ONE ' + (it.workItemNoun || 'card') + ' per run' : 'Move only as many ' + (it.workItemNounPlural || 'cards') + ' as needed to reach capacity (limit: ' + it.maxInFlightItems + ')' %>
- ALWAYS post a comment BEFORE moving the <%= it.workItemNoun || 'card' %> — comment first, then move to TODO
- BE CONSERVATIVE with dependency detection - when unsure, treat as blocked
- CONSERVATIVE on detecting dependencies — when unsure if text implies a dependency, treat it as one. But GENEROUS on MERGED resolution — use substring matching and prefer resolved over blocked for ambiguous matches.
- LOOK FOR dependency keywords: "blocked by", "depends on", "waiting for", "after", "requires"
- IGNORE "(not yet merged)" and similar stale annotations in descriptions — they are written at card creation and never updated. The MERGED list is the only source of truth.
- EXECUTE COMMANDS — DO NOT JUST DESCRIBE THEM: When you decide to post a comment or move a card, you MUST actually invoke the command as a tool call. Writing a command inside a code block without invoking it does NOT execute it — text output has no effect on the system. If you find yourself writing out a command without calling it, stop and call it instead.
2 changes: 2 additions & 0 deletions src/triggers/trello/register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { TrelloCommentMentionTrigger } from './comment-mention.js';
import { ReadyToProcessLabelTrigger } from './label-added.js';
import {
TrelloStatusChangedBacklogTrigger,
TrelloStatusChangedMergedTrigger,
TrelloStatusChangedPlanningTrigger,
TrelloStatusChangedSplittingTrigger,
TrelloStatusChangedTodoTrigger,
Expand All @@ -33,6 +34,7 @@ export function registerTrelloTriggers(registry: TriggerRegistry): void {
registry.register(TrelloStatusChangedPlanningTrigger);
registry.register(TrelloStatusChangedTodoTrigger);
registry.register(TrelloStatusChangedBacklogTrigger);
registry.register(TrelloStatusChangedMergedTrigger);

registry.register(new ReadyToProcessLabelTrigger());
}
10 changes: 9 additions & 1 deletion src/triggers/trello/status-changed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { type TrelloWebhookPayload, isTrelloWebhookPayload } from './types.js';
interface StatusChangedConfig {
name: string;
description: string;
listKey: 'splitting' | 'planning' | 'todo' | 'backlog';
listKey: 'splitting' | 'planning' | 'todo' | 'backlog' | 'merged';
agentType: 'splitting' | 'planning' | 'implementation' | 'backlog-manager';
}

Expand Down Expand Up @@ -115,3 +115,11 @@ export const TrelloStatusChangedBacklogTrigger = createStatusChangedTrigger({
listKey: 'backlog',
agentType: 'backlog-manager',
});

export const TrelloStatusChangedMergedTrigger = createStatusChangedTrigger({
name: 'trello-status-changed-merged',
description:
'Re-triggers backlog-manager when any card is moved to MERGED, so manually resolved dependencies unblock the backlog',
listKey: 'merged',
agentType: 'backlog-manager',
});
21 changes: 19 additions & 2 deletions tests/unit/agents/definitions/pipelineSnapshot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ describe('fetchPipelineSnapshotStep', () => {
expect(output).toContain('Full details here');
});

it('uses title-only format for DONE and MERGED lists', async () => {
it('uses title-and-url format for DONE and MERGED lists', async () => {
mockGetPMProviderOrNull.mockReturnValue(mockProvider as never);

const card = { id: 'card-done', title: 'Done Card', url: 'http://trello.com/c/2', labels: [] };
Expand All @@ -161,8 +161,25 @@ describe('fetchPipelineSnapshotStep', () => {
expect(mockReadWorkItem).not.toHaveBeenCalledWith('card-done', true);

const output = result[0].result as string;
// Title-only format
// Title + URL format
expect(output).toContain('[card-done] Done Card');
expect(output).toContain('http://trello.com/c/2');
});

it('omits URL parentheses for DONE/MERGED items when url is empty', async () => {
mockGetPMProviderOrNull.mockReturnValue(mockProvider as never);

const card = { id: 'card-done', title: 'Done Card', url: '', labels: [] };
mockProvider.listWorkItems.mockImplementation(async (listId: string) => {
if (listId === 'list-done') return [card];
return [];
});

const result = await fetchPipelineSnapshotStep(makeParams({}, makeProject()));

const output = result[0].result as string;
expect(output).toContain('[card-done] Done Card');
expect(output).not.toContain('()');
});

it('handles list fetch errors gracefully', async () => {
Expand Down
1 change: 1 addition & 0 deletions tests/unit/triggers/backlog-status-changed.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ describe('TrelloStatusChangedBacklogTrigger', () => {
expect(result?.agentType).toBe('backlog-manager');
expect(result?.workItemId).toBe('card789');
expect(result?.agentInput.workItemId).toBe('card789');
expect(result?.agentInput.triggerEvent).toBe('pm:status-changed');
});

it('returns null when card ID is missing from payload', async () => {
Expand Down
8 changes: 5 additions & 3 deletions tests/unit/triggers/builtins.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ vi.mock('../../../src/triggers/trello/status-changed.js', () => ({
TrelloStatusChangedPlanningTrigger: { name: 'trello-status-changed-planning' },
TrelloStatusChangedTodoTrigger: { name: 'trello-status-changed-todo' },
TrelloStatusChangedBacklogTrigger: { name: 'trello-status-changed-backlog' },
TrelloStatusChangedMergedTrigger: { name: 'trello-status-changed-merged' },
}));
vi.mock('../../../src/triggers/trello/comment-mention.js', () => ({
TrelloCommentMentionTrigger: vi
Expand Down Expand Up @@ -87,8 +88,8 @@ describe('registerBuiltInTriggers', () => {

registerBuiltInTriggers(registry as unknown as TriggerRegistry);

// Should have registered all 20 built-in triggers (18 + 2 Sentry alerting triggers)
expect(registry.register).toHaveBeenCalledTimes(20);
// Should have registered all 21 built-in triggers (19 + 2 Sentry alerting triggers)
expect(registry.register).toHaveBeenCalledTimes(21);
});

it('registers TrelloCommentMentionTrigger first', () => {
Expand All @@ -100,7 +101,7 @@ describe('registerBuiltInTriggers', () => {
expect(firstCall.name).toBe('trello-comment-mention');
});

it('registers all four status-changed triggers (Trello)', () => {
it('registers all five status-changed triggers (Trello)', () => {
const registry = createMockRegistry();

registerBuiltInTriggers(registry as unknown as TriggerRegistry);
Expand All @@ -110,6 +111,7 @@ describe('registerBuiltInTriggers', () => {
expect(registeredNames).toContain('trello-status-changed-planning');
expect(registeredNames).toContain('trello-status-changed-todo');
expect(registeredNames).toContain('trello-status-changed-backlog');
expect(registeredNames).toContain('trello-status-changed-merged');
});

it('registers GitHub triggers', () => {
Expand Down
Loading
Loading