Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
---
id: BACK-465
title: Add first-class ADR decision management
status: Done
assignee:
- '@abbyssoul'
created_date: '2026-05-05 01:12'
updated_date: '2026-05-05 01:13'
labels:
- feature
- decisions
- mcp
- cli
dependencies: []
modified_files:
- src/cli.ts
- src/core/backlog.ts
- src/core/content-store.ts
- src/file-system/operations.ts
- src/markdown/parser.ts
- src/markdown/serializer.ts
- src/mcp/tools/decisions/handlers.ts
- src/mcp/tools/decisions/index.ts
- src/mcp/tools/decisions/schemas.ts
- src/mcp/utils/decision-response.ts
- src/guidelines/mcp/overview.md
- src/guidelines/mcp/overview-tools.md
priority: high
ordinal: 23000
---

## Description

<!-- SECTION:DESCRIPTION:BEGIN -->
Backlog.md needs public support for Architectural Decision Records so users and agents can manage decisions through the documented CLI and MCP surfaces instead of relying on file-system conventions.
<!-- SECTION:DESCRIPTION:END -->

## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 Users can create, list, and view decisions from the CLI using persisted decision files.
- [x] #2 MCP clients can list, view, create, update, and search decisions with schema validation and formatted responses.
- [x] #3 Decision records are indexed for shared search/content-store flows and preserve metadata and markdown content.
- [x] #4 Agent-facing MCP guidance documents decisions as a supported public surface.
<!-- AC:END -->

## Implementation Plan

<!-- SECTION:PLAN:BEGIN -->
1. Add decision CLI and MCP commands. 2. Reuse core filesystem persistence and search indexing. 3. Cover create/list/view/update/search behavior with tests.
<!-- SECTION:PLAN:END -->

## Implementation Notes

<!-- SECTION:NOTES:BEGIN -->
Implemented first-class ADR/decision support on this branch, then fixed content/path regressions found during review.
<!-- SECTION:NOTES:END -->

## Final Summary

<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Added first-class ADR/decision management across CLI, MCP, core persistence, and search. Decisions can now be created, listed, viewed, updated, and searched through public surfaces; supplied markdown content and metadata are preserved, including nested decision paths. Added MCP/filesystem/remote-ID regression coverage.
<!-- SECTION:FINAL_SUMMARY:END -->
54 changes: 54 additions & 0 deletions backlog/tasks/back-466 - Fix-task-list-milestone-filtering.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
---
id: BACK-466
title: Fix task list milestone filtering
status: Done
assignee:
- '@abbyssoul'
created_date: '2026-05-05 01:12'
updated_date: '2026-05-05 01:13'
labels:
- bug
- milestone
- cli
- mcp
dependencies: []
references:
- handover-task-list-milestone-filter-bug.md
modified_files:
- src/core/backlog.ts
- src/mcp/tools/tasks/index.ts
- src/test/cli-milestone-filter.test.ts
priority: high
ordinal: 24000
---

## Description

<!-- SECTION:DESCRIPTION:BEGIN -->
The task list filter should return tasks assigned to a requested milestone, including tasks stored with scalar milestone frontmatter as described in the handover reproducer, instead of returning an empty result set.
<!-- SECTION:DESCRIPTION:END -->

## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 CLI task list filtering returns tasks whose scalar milestone field matches the requested milestone.
- [x] #2 MCP task_list filtering resolves milestone IDs and titles consistently with the rest of the milestone model.
- [x] #3 Regression coverage proves valid milestone assignments are not filtered out.
<!-- AC:END -->

## Implementation Plan

<!-- SECTION:PLAN:BEGIN -->
1. Reproduce milestone filtering against scalar task milestone values. 2. Route task-list filtering through the canonical milestone resolver. 3. Add regression coverage for CLI/MCP-visible behavior.
<!-- SECTION:PLAN:END -->

## Implementation Notes

<!-- SECTION:NOTES:BEGIN -->
Implemented milestone filter resolution so task list results include tasks assigned with scalar milestone frontmatter.
<!-- SECTION:NOTES:END -->

## Final Summary

<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Fixed task-list milestone filtering so valid milestone assignments are returned instead of empty results. The filter path now resolves milestone values consistently with the milestone model, and regression coverage protects the scalar milestone frontmatter case described in the handover.
<!-- SECTION:FINAL_SUMMARY:END -->
49 changes: 49 additions & 0 deletions backlog/tasks/back-467 - Prevent-priority-filter-test-timeout.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
---
id: BACK-467
title: Prevent priority filter test timeout
status: Done
assignee:
- '@abbyssoul'
created_date: '2026-05-05 01:12'
updated_date: '2026-05-05 01:13'
labels:
- test
- performance
- cli
dependencies: []
modified_files:
- src/test/cli-priority-filtering.test.ts
priority: medium
ordinal: 25000
---

## Description

<!-- SECTION:DESCRIPTION:BEGIN -->
The case-insensitive priority filter integration test should avoid unnecessary serial CLI runtime so the suite remains reliable on slower environments.
<!-- SECTION:DESCRIPTION:END -->

## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 The test still verifies high, medium, and low priority filtering case-insensitively.
- [x] #2 The CLI task list checks run concurrently to reduce wall-clock runtime.
- [x] #3 The test remains stable under the Bun test runner.
<!-- AC:END -->

## Implementation Plan

<!-- SECTION:PLAN:BEGIN -->
1. Identify the serial CLI calls causing timeout risk. 2. Keep the same priority assertions while running list commands in parallel. 3. Verify the targeted test passes.
<!-- SECTION:PLAN:END -->

## Implementation Notes

<!-- SECTION:NOTES:BEGIN -->
Updated the case-insensitive priority filtering test to run the three task list commands concurrently.
<!-- SECTION:NOTES:END -->

## Final Summary

<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Reduced timeout risk in the priority filtering integration test by running the high, medium, and low CLI list checks in parallel while preserving the same case-insensitive assertions.
<!-- SECTION:FINAL_SUMMARY:END -->
96 changes: 78 additions & 18 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,13 @@ import { type CompletionInstallResult, installCompletion, registerCompletionComm
import { configureAdvancedSettings } from "./commands/configure-advanced-settings.ts";
import { registerMcpCommand } from "./commands/mcp.ts";
import { pickTaskForEditWizard, runTaskCreateWizard, runTaskEditWizard } from "./commands/task-wizard.ts";
import { DEFAULT_DIRECTORIES, DEFAULT_FILES, DEFAULT_STATUSES } from "./constants/index.ts";
import {
DECISION_ID_PREFIX_RE,
DEFAULT_DIRECTORIES,
DEFAULT_FILE_PREFIXES,
DEFAULT_FILES,
DEFAULT_STATUSES,
} from "./constants/index.ts";
import { initializeProject } from "./core/init.ts";
import { buildMilestoneBuckets, collectArchivedMilestoneKeys, milestoneKey } from "./core/milestones.ts";
import { computeSequences } from "./core/sequences.ts";
Expand Down Expand Up @@ -1401,8 +1407,8 @@ export async function generateNextDecisionId(core: Core): Promise<string> {
const files = await core.gitOps.listFilesInTree(branch, `${backlogDir}/decisions`);
return files
.map((file) => {
const match = file.match(/decision-(\d+)/);
return match ? `decision-${match[1]}` : null;
const match = file.match(/decision-(\d+)/i);
return match ? `${DEFAULT_FILE_PREFIXES.DECISION}${match[1]}` : null;
})
.filter((id): id is string => id !== null);
});
Expand All @@ -1424,24 +1430,21 @@ export async function generateNextDecisionId(core: Core): Promise<string> {
}

// Find the highest numeric ID
let max = 0;
for (const id of allIds) {
const match = id.match(/^decision-(\d+)$/);
if (match) {
const num = Number.parseInt(match[1] || "0", 10);
if (num > max) max = num;
}
}
const max = allIds
.map((id) => id.match(DECISION_ID_PREFIX_RE))
.filter((match): match is RegExpMatchArray => match !== null)
.map((match) => Number.parseInt(match[1] || "0", 10))
.reduce((a, b) => Math.max(a, b), 0);

const nextIdNumber = max + 1;
const padding = config?.zeroPaddedIds;

if (padding && typeof padding === "number" && padding > 0) {
const paddedId = String(nextIdNumber).padStart(padding, "0");
return `decision-${paddedId}`;
}
const paddedId =
padding && typeof padding === "number" && padding > 0
? String(nextIdNumber).padStart(padding, "0")
: String(nextIdNumber);

return `decision-${nextIdNumber}`;
return `${DEFAULT_FILE_PREFIXES.DECISION}${paddedId}`;
}

const taskCmd = program.command("task").aliases(["tasks"]);
Expand Down Expand Up @@ -2091,7 +2094,7 @@ taskCmd
...archivedMilestones,
]);
const resolvedMilestone = resolveClosestMilestoneFilterValue(
options.milestone,
resolveMilestoneFilterValue(options.milestone),
filtered.map((task) => resolveMilestoneFilterValue(task.milestone ?? "")),
);
if (resolvedMilestone) {
Expand Down Expand Up @@ -3165,7 +3168,7 @@ docCmd
}
});

const decisionCmd = program.command("decision");
const decisionCmd = program.command("decision").aliases(["decisions"]);

decisionCmd
.command("create <title>")
Expand All @@ -3188,6 +3191,63 @@ decisionCmd
console.log(`Created decision ${id}`);
});

decisionCmd
.command("list")
.option("--plain", "use plain text output instead of interactive UI")
.action(async (options) => {
const cwd = await requireProjectRoot();
const core = new Core(cwd);
const docs = await core.filesystem.listDecisions();
if (docs.length === 0) {
console.log("No decisions found.");
return;
}

// Plain text output for non-interactive environments
const usePlainOutput = isPlainRequested(options) || shouldAutoPlain;
if (usePlainOutput) {
for (const d of docs) {
console.log(`${d.id} - ${d.title}`);
}
return;
}

// Interactive UI
const selected = await genericSelectList("Select a decision", docs);
if (selected) {
// Show decision details (recursive search)
const files = await Array.fromAsync(
new Bun.Glob("**/*.md").scan({ cwd: core.filesystem.decisionsDir, followSymlinks: true }),
);
const docFile = files.find(
(f) => f.startsWith(`${selected.id} -`) || f.endsWith(`/${selected.id}.md`) || f === `${selected.id}.md`,
);
if (docFile) {
const filePath = join(core.filesystem.decisionsDir, docFile);
const content = await Bun.file(filePath).text();
await scrollableViewer(content);
}
}
});

decisionCmd
.command("view <decisionId>")
.description("view a decision")
.action(async (decisionId: string) => {
const cwd = await requireProjectRoot();
const core = new Core(cwd);
try {
const content = await core.getDecisionContent(decisionId);
if (content === null) {
console.error(`Decision ${decisionId} not found.`);
return;
}
await scrollableViewer(content);
} catch {
console.error(`Decision ${decisionId} not found.`);
}
});

// Agents command group
const agentsCmd = program.command("agents");

Expand Down
19 changes: 19 additions & 0 deletions src/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,22 @@ export const DEFAULT_DIRECTORIES = {
MILESTONES: "milestones",
} as const;

/**
* Default file naming prefixes for persisted backlog items.
*/
export const DEFAULT_FILE_PREFIXES = {
/** Task file prefix (e.g., task-1.md) */
TASK: "task-",
/** Draft file prefix (e.g., draft-1.md) */
DRAFT: "draft-",
/** Milestone file prefix (e.g., m-1 - Milestone-slug.md) */
MILESTONE: "m-",
/** Decision file prefix (e.g., decision-1 - Decision-slug.md) */
DECISION: "decision-",
/** Document file prefix (e.g., doc-1 - Some-Document.md) */
DOC: "doc-",
} as const;

/**
* Default configuration file names
*/
Expand Down Expand Up @@ -72,4 +88,7 @@ export const DEFAULT_INIT_CONFIG = {
autoOpenBrowser: true,
} as const;

export const DECISION_ID_PREFIX_RE = new RegExp(`^${DEFAULT_FILE_PREFIXES.DECISION}(\\d+)$`);
export const DOC_ID_PREFIX_RE = new RegExp(`^${DEFAULT_FILE_PREFIXES.DOC}(\\d+)$`);

export * from "../guidelines/index.ts";
Loading