From cd7f157bd28d5d7cb4c06f212f314fd6057d71f7 Mon Sep 17 00:00:00 2001 From: Dominikus Nold Date: Wed, 11 Feb 2026 01:03:22 +0100 Subject: [PATCH 1/8] feat(backlog): finalize daily/refine comment context, interactive posting, and docs parity --- .cursor/rules/automatic-openspec-workflow.mdc | 4 +- AGENTS.md | 421 +++++---- CHANGELOG.md | 23 + CLAUDE.md | 71 +- .../tutorial-daily-standup-sprint-review.md | 17 +- docs/guides/agile-scrum-workflows.md | 21 +- docs/guides/backlog-refinement.md | 53 ++ docs/guides/devops-adapter-integration.md | 12 +- .../CHANGE_VALIDATION.md | 26 + .../TDD_EVIDENCE.md | 599 ++++++++++++ .../proposal.md | 9 + .../specs/backlog-refinement/spec.md | 179 ++++ .../specs/daily-standup/spec.md | 189 ++++ .../tasks.md | 66 +- openspec/config.yaml | 8 + pyproject.toml | 2 +- resources/prompts/specfact.backlog-daily.md | 16 +- resources/prompts/specfact.backlog-refine.md | 13 +- setup.py | 2 +- src/specfact_cli/__init__.py | 2 +- src/specfact_cli/adapters/ado.py | 62 +- src/specfact_cli/adapters/github.py | 63 +- src/specfact_cli/backlog/ai_refiner.py | 20 +- .../modules/backlog/src/commands.py | 878 ++++++++++++++++-- .../unit/adapters/test_ado_backlog_adapter.py | 51 + .../adapters/test_github_backlog_adapter.py | 29 + tests/unit/backlog/test_ai_refiner.py | 22 + tests/unit/commands/test_backlog_commands.py | 181 ++++ tests/unit/commands/test_backlog_daily.py | 234 ++++- 29 files changed, 2902 insertions(+), 371 deletions(-) create mode 100644 openspec/changes/backlog-scrum-01-standup-exceptions-first/TDD_EVIDENCE.md create mode 100644 openspec/changes/backlog-scrum-01-standup-exceptions-first/specs/backlog-refinement/spec.md diff --git a/.cursor/rules/automatic-openspec-workflow.mdc b/.cursor/rules/automatic-openspec-workflow.mdc index 8a52b3d7..30047c1f 100644 --- a/.cursor/rules/automatic-openspec-workflow.mdc +++ b/.cursor/rules/automatic-openspec-workflow.mdc @@ -186,7 +186,7 @@ When user requests a change, **do not write or modify any application code yet** - Confirm with user: "OpenSpec change ready. Proceed with implementation?" 2. **If user confirms or if workflow requires immediate application:** - - Execute: `/opsx:apply ` (or `/specfact-cli/wf-apply-change `) + - Execute: `/opsx:apply ` - Follow workflow steps: - Change selection - Read change artifacts @@ -235,7 +235,7 @@ AI (automatically): - Creates change proposal, spec deltas, tasks - Validates: openspec validate --strict 5. Verifies: Shows change summary; "Proceed with implementation?" -6. Applies: /opsx:apply (or /wf-apply-change ) +6. Applies: /opsx:apply - Implements the change (TDD: tests first, then code) - Updates tasks 7. After implementation: Verify (quality gates), validate (openspec validate), sync backlog diff --git a/AGENTS.md b/AGENTS.md index a66289ef..18cc03f3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,243 +1,236 @@ -# SpecFact CLI - Repository Guidelines - -## Project Structure & Module Organization - -- `src/specfact_cli/` contains the CLI command implementation - - `cli.py` - Main Typer application entry point - - `modules//src/commands.py` - Primary command implementations (module-local) - - `commands/` - Legacy compatibility shims (app-only re-exports from module-local commands) - - `models/` - Pydantic data models (plan, protocol, deviation) - - `generators/` - Code generators (protocol, plan, report) - - `validators/` - Validation logic (schema, contract, FSM) - - `utils/` - Shared utilities (git, YAML, console) -- `src/common/` contains shared utilities (logger_setup, platform_base) -- `tests/` mirrors CLI modules via `unit/`, `integration/`, and `e2e/` -- `tools/` hosts contract automation and semgrep rules -- `resources/` contains templates, schemas, and mapping files - -## Build, Test, and Development Commands - -### YAML and Workflows: Formatting and Linting - -- Format everything: `hatch run yaml-fix-all` -- Check everything: `hatch run yaml-check-all` -- Format workflows only: `hatch run workflows-fmt` -- Lint workflows (actionlint): `hatch run workflows-lint` - -### Contract-First Testing (Recommended) - -- `hatch run contract-test` runs the contract-first test workflow (auto-detect best level) -- Contract validation: `hatch run contract-test-contracts` (runtime contract validation) -- Contract exploration: `hatch run contract-test-exploration` (CrossHair exploration) -- Scenario tests: `hatch run contract-test-scenarios` (integration/E2E with contract references) -- Full contract suite: `hatch run contract-test-full` (all contract-first layers) -- Status check: `hatch run contract-test-status` - -### Legacy Smart Testing (Backward Compatibility) - -- `hatch test --cover -v` runs tests with coverage -- Incremental levels: `hatch run smart-test-unit`, `hatch run smart-test-folder`, `hatch run smart-test-integration` - -### Development Tools - -- Lint/format: `hatch run lint` (black, isort, basedpyright, ruff, pylint) -- Format only: `hatch run format` -- Type check: `hatch run type-check` (basedpyright) -- Dev shell: `hatch shell` -- **Faster startup**: Use `specfact --skip-checks ` to skip template and version checks (useful in CI or when security scanning causes delay). - -## Coding Style & Naming Conventions - -- Python 3.11+ with 4-space indentation and Black line length 120 -- Apply Google-style docstrings and full type hints -- Use `common.logger_setup.get_logger()` for logging, avoid `print()` -- Name files and modules in `snake_case`; classes stay `PascalCase`, constants `UPPER_SNAKE_CASE` - -## Testing Guidelines - -### Contract-First Approach (Recommended) - -- **Runtime contracts**: Use `@icontract` decorators on all public APIs -- **Type validation**: Use `@beartype` for runtime type checking -- **Contract exploration**: Use CrossHair to discover counterexamples -- **Scenario tests**: Focus on CLI command workflows with contract references -- **Test diet**: Remove redundant unit tests as contracts provide the same coverage - -### Unit Testing (Backward Compatibility) - -- Place tests alongside modules (`tests/unit/specfact_cli/test_.py`) -- Use pytest with `@pytest.mark.asyncio` for async tests -- Ensure environment-sensitive logic guards `os.environ.get("TEST_MODE") == "true"` - -## Commit & Pull Request Guidelines - -- **Branch Protection**: This repository has branch protection enabled for `dev` and `main` branches. All changes must be made via Pull Requests: - - **Never commit directly to `dev` or `main`** - create a feature/bugfix/hotfix branch instead - - Create a feature branch: `git checkout -b feature/your-feature-name` - - Create a bugfix branch: `git checkout -b bugfix/your-bugfix-name` - - Create a hotfix branch: `git checkout -b hotfix/your-hotfix-name` - - Push your branch and create a PR to `dev` or `main` - - All PRs must pass CI/CD checks before merging -- Follow Conventional Commits (`feat:`, `fix:`, `docs:`, `test:`, `refactor:`) -- **Contract-first workflow**: Before pushing, run `hatch run format`, `hatch run lint`, and `hatch run contract-test` -- PRs should link to CLI-First Strategy docs, describe contract impacts, and include tests -- Attach contract validation notes and screenshots/logs when behavior changes -- **Version Updates**: When updating the version in `pyproject.toml`, ensure it's newer than the latest PyPI version. The CI/CD pipeline will automatically publish to PyPI after successful merge to `main` only if the version is newer. Sync versions across `pyproject.toml`, `setup.py`, `src/__init__.py`, `src/specfact_cli/__init__py` - -## CLI Command Development Notes - -- All commands extend `typer.Typer()` for consistent CLI interface -- New command logic belongs in `src/specfact_cli/modules//src/commands.py` -- Legacy import path compatibility is limited to `from specfact_cli.commands. import app` -- Replacement path for module code is `from specfact_cli.modules..src.commands import ...` -- Compatibility shims are planned for removal no earlier than `v0.30` (or next major migration window) -- Use `rich.console.Console()` for beautiful terminal output -- Validate inputs with Pydantic models at command boundaries -- Apply `@icontract` decorators to enforce contracts at runtime -- Use `@beartype` for automatic type checking -- Handle errors gracefully with try/except and user-friendly error messages - -## Data Model Conventions - -- Use Pydantic `BaseModel` for all data structures -- Add contracts with `@require` and `@ensure` decorators -- Include `Field` validators for complex validation logic -- Document all models with docstrings and field descriptions - -## CLI Command Pattern Example +# AGENTS.md -```python -import typer -from pathlib import Path -from icontract import require, ensure -from beartype import beartype -from rich.console import Console -from pydantic import BaseModel +This file provides guidance to coding agents when working with code in this repository. -app = typer.Typer() -console = Console() +## Project Overview -class AnalysisConfig(BaseModel): - """Configuration for code analysis.""" - repo_path: Path - confidence: float = 0.5 - shadow_only: bool = False +SpecFact CLI is a Python CLI tool for agile DevOps teams. It keeps backlogs, specs, tests, and code in sync with contract-driven development, validation, and enforcement. Built with Typer + Rich, using Hatch as the build system. Python 3.11+. -@app.command() -@require(lambda repo_path: repo_path.exists(), "Repository path must exist") -@ensure(lambda result: result.success, "Analysis must succeed") -@beartype -def analyze( - repo_path: Path = typer.Argument(..., help="Path to repository"), - confidence: float = typer.Option(0.5, help="Minimum confidence score"), - shadow_only: bool = typer.Option(False, help="Shadow mode only"), -) -> AnalysisResult: - """ - Analyze repository and generate plan bundle. - - Args: - repo_path: Path to the repository to analyze - confidence: Minimum confidence score (0.0-1.0) - shadow_only: If True, don't enforce, just observe - - Returns: - Analysis result with generated plan bundle - """ - config = AnalysisConfig( - repo_path=repo_path, - confidence=confidence, - shadow_only=shadow_only - ) - - console.print(f"[bold]Analyzing {repo_path}...[/bold]") - - # Implementation here - return AnalysisResult(success=True) +## Essential Commands + +```bash +# Development environment +pip install -e ".[dev]" +hatch shell + +# Format & lint (run after every code change, in this order) +hatch run format # ruff format + fix +hatch run type-check # basedpyright strict mode +hatch run contract-test # contract-first validation (primary) +hatch test --cover -v # full pytest suite + +# Contract-first testing layers +hatch run contract-test-contracts # runtime contract validation only +hatch run contract-test-exploration # CrossHair symbolic execution +hatch run contract-test-scenarios # integration/E2E with contract refs +hatch run contract-test-full # all layers +hatch run contract-test-status # coverage status report + +# Run a single test file +hatch test -- tests/unit/specfact_cli/test_example.py -v + +# Lint subsystems +hatch run lint # full lint suite +hatch run governance # pylint detailed analysis +hatch run yaml-lint # YAML validation +hatch run lint-workflows # GitHub Actions actionlint + +# Code scanning +hatch run scan-all # semgrep analysis ``` -## Project-Specific Patterns +## Architecture -### Contract Decorators +### Modular Command Registry with Lazy Loading -```python -from icontract import require, ensure, invariant -from beartype import beartype +The CLI uses a module package system in `src/specfact_cli/modules/`. Each module is a self-contained package: -@invariant(lambda self: self.version == "1.0") -class PlanBundle: - """Plan bundle with contract enforcement.""" - version: str - features: list[Feature] - - @require(lambda feature: feature.key.startswith("FEATURE-")) - @ensure(lambda result: result is not None) - @beartype - def add_feature(self, feature: Feature) -> bool: - """Add feature with contract validation.""" - self.features.append(feature) - return True +``` +modules/{name}/ + module-package.yaml # metadata: name, version, commands, dependencies + src/{name}/ + __init__.py + main.py # typer.Typer app with command definitions ``` -### Rich Console Output +The registry (`src/specfact_cli/registry/`) discovers modules at startup but defers imports until a command is actually invoked. `bootstrap.py` registers all modules; `registry.py` manages lazy loading; `module_packages.py` handles discovery from `module-package.yaml` files. -```python -from rich.console import Console -from rich.table import Table -from rich.progress import track +**Entry flow**: `cli.py:cli_main()` → Typer app with global options → `ProgressiveDisclosureGroup` for help → lazy-loaded command groups from registry. -console = Console() +### Contract-First Development -# Progress bars -for item in track(items, description="Processing..."): - process(item) +All public APIs must use `@icontract` decorators (`@require`, `@ensure`, `@invariant`) and `@beartype` for runtime type checking. CrossHair discovers counterexamples via symbolic execution. Contracts are the primary validation mechanism; traditional unit tests are secondary. -# Tables -table = Table(title="Deviations") -table.add_column("Type", style="cyan") -table.add_column("Severity", style="magenta") -table.add_column("Description", style="green") +### Key Subsystems -for deviation in deviations: - table.add_row(deviation.type, deviation.severity, deviation.description) +- **`models/`** - Pydantic BaseModel classes for all data structures +- **`parsers/`**, **`analyzers/`** - Code analysis +- **`generators/`** - Code/spec generation using Jinja2 templates from `resources/templates/` +- **`validators/`** - Schema, contract, FSM validation +- **`adapters/`** - Bridge pattern for tool integrations (GitHub, Azure DevOps, Jira, Linear) +- **`modes/`** - Operational modes: CICD (fast, deterministic, non-interactive) vs Copilot (interactive, IDE-aware). Auto-detected from environment. +- **`resources/`** - Bundled prompts, templates, schemas, mappings (force-included in wheel) -console.print(table) +### Logging -# Status messages -console.print("[bold green]✓[/bold green] Analysis complete") -console.print("[bold red]✗[/bold red] Validation failed") -``` +Use `from specfact_cli.common import get_bridge_logger` and avoid `print()` in production command paths. Debug logs go to `~/.specfact/logs/specfact-debug.log` when `--debug` is passed. + +## Development Workflow + +### Branch Protection + +`dev` and `main` are protected. Always work on feature/bugfix/hotfix branches and submit PRs: +- `feature/your-feature-name` +- `bugfix/your-bugfix-name` +- `hotfix/your-hotfix-name` + +### Pre-Commit Checklist + +Run all steps in order before committing. Every step must pass with no errors. + +1. `hatch run format` # ruff format + autofix +2. `hatch run type-check` # basedpyright strict +3. `hatch run lint` # full lint suite +4. `hatch run yaml-lint` # YAML + markdown validation +5. `hatch run contract-test` # contract-first validation +6. `hatch run smart-test` # targeted test run (use `smart-test-full` for larger modifications) + +### OpenSpec Workflow + +Before modifying application code, **always** verify that an active OpenSpec change in `openspec/changes/` **explicitly covers the requested modification**. This is the spec-driven workflow defined in `openspec/config.yaml`. Skip only when the user explicitly says `"skip openspec"` or `"implement without openspec change"`. + +**Agent MUST NOT apply any code edits** when a fix, change, modification, or edit to any codebase file is requested unless an active OpenSpec change exists that explicitly covers the requested scope. If no such change exists, ask for clarification: + +- **a) New change** — create a new OpenSpec change proposal (`/opsx:new`) +- **b) Modify existing** — select and continue an existing change in `openspec/changes/` +- **c) Delta** — add a targeted delta to an existing change's specs + +The existence of *any* open change is not sufficient — the change must specifically address the requested modification. Do not proceed until one of the above is resolved. + +### Hard Gate: Strict TDD Order (Non-Negotiable) + +For any behavior change, the implementation order is mandatory and must be auditable: + +1. Update or add spec deltas first. +2. Add/modify tests next, mapped to spec scenarios. +3. Run tests and capture a **failing** result before implementation. +4. Only then modify production code. +5. Re-run tests and quality gates until passing. -## Distribution & Packaging +Required evidence: -- Package name: `specfact-cli` -- CLI command: `specfact` -- PyPI distribution: `pip install specfact-cli` -- uvx usage: `uvx specfact-cli@latest ` (recommended) or `uvx --from specfact-cli specfact ` -- Container: `docker run ghcr.io/nold-ai/specfact-cli:latest` +- Create/update `openspec/changes//TDD_EVIDENCE.md` with: + - test command(s) and timestamp for the pre-implementation failing run + - short failure summary + - test command(s) and timestamp for the post-implementation passing run -## Success Criteria +Agent enforcement: -### Code Quality +- Agents MUST NOT edit production code for new/changed behavior until failing-test evidence is recorded. +- If this order cannot be followed, stop and ask the user for explicit override before proceeding. -- **Type coverage**: 100% with basedpyright strict mode -- **Contract coverage**: All public APIs have `@icontract` decorators -- **Test coverage**: Scenario tests cover all CLI commands -- **Zero warnings**: Clean basedpyright, ruff, and pylint output +#### Change Order (`openspec/CHANGE_ORDER.md`) -### CLI User Experience +`openspec/CHANGE_ORDER.md` is the **single source of truth** for change sequencing, module grouping, and inter-change dependencies. Always use it to avoid redundant analysis of `openspec/changes/` folders. + +**Read it first** — before creating, implementing, or archiving any change, consult `CHANGE_ORDER.md` to: +- Check which changes are already archived (implemented) and their dates +- Verify hard blockers are resolved before starting implementation +- Understand where a new change fits in module order and wave sequencing + +**Keep it updated** — whenever a change lifecycle event occurs, update `CHANGE_ORDER.md` in the same commit: +- **New change created**: add a row to the correct module group table with folder name, GitHub issue link, and blocked-by dependencies +- **Change archived**: move the entry from "Pending" to "Implemented (archived)" with the archive date; update wave status if a wave is now complete +- **Change modified/renamed**: update the folder name and any affected dependency references +- **Blocker resolved**: update the "Blocked by" column (append ✅ to resolved blockers) + +Use the `specfact-openspec-workflows` skill as the default execution path for OpenSpec lifecycle work. + +- When a Markdown plan exists and the intent is to create a change from that plan, use `.cursor/commands/wf-create-change-from-plan.md` (`/wf-change-from-plan`) to generate the proposal/tasks/spec deltas. +- For plans targeting an internal repository, still run the same workflow but follow its repo rules (for example, skip public GitHub issue creation where required). +- After any change is created or modified, run `.cursor/commands/wf-validate-change.md` (`/wf-validate-change`) and capture its output in `openspec/changes//CHANGE_VALIDATION.md`. +- Treat validation output as required context for dependency and interface impact, including any workflow-provided GitHub issue sync context. + +### Version Updates + +When bumping version, sync across: `pyproject.toml`, `setup.py`, `src/specfact_cli/__init__.py`. CI/CD auto-publishes to PyPI on merge to `main` only if version exceeds the published one. + +**Version semantics (SemVer):** +- `feature/*` branches → **minor** increment (e.g. `0.5.0 → 0.6.0`) +- `bugfix/*` / `hotfix/*` branches → **patch** increment (e.g. `0.5.0 → 0.5.1`) +- Breaking changes or major milestones → **major** increment (requires explicit confirmation) + +Always propose the increment type based on the branch name and ask for confirmation before applying the bump. + +### Changelog + +Keep `CHANGELOG.md` updated with every meaningful change. Update it in the same commit that bumps the version and do not let them diverge. + +- Follow [Keep a Changelog](https://keepachangelog.com/) format: `Added`, `Changed`, `Fixed`, `Removed`, `Security` +- Each version entry must match the version in `pyproject.toml` +- Unreleased changes accumulate under `## [Unreleased]` until a version bump + +### Commits + +Follow Conventional Commits: `feat:`, `fix:`, `docs:`, `test:`, `refactor:`. + +### Documentation + +Keep docs current with every code change that affects user-facing behaviour. + +- Docs source lives in `docs/` and is published to [docs.specfact.io](https://docs.specfact.io) via GitHub Pages (Jekyll) +- **Preserve all front-matter** on every edit (`title`, `layout`, `nav_order`, `permalink`, etc.) and check `docs/_layouts/default.html` and `docs/index.md` before adding or removing front-matter keys +- When a command, option, or behaviour changes, update the corresponding doc page in the same PR +- Broken or outdated docs for users are P0; prefer a small doc fix over shipping undocumented changes + +### README Maintenance + +`README.md` (repo root) and the docs landing page (`docs/index.md` or `docs/README.md`) must stay in sync with what SpecFact actually does. + +- On larger refactorings or feature additions, reconsider the README from an **external/new-user perspective** and lead with value and USP, not internal architecture +- A first-time reader should understand what SpecFact does, why they'd use it, and how to get started within the first screen +- Do not let the README drift from the actual CLI interface or command list + +## Code Conventions + +- Python 3.11+, line length 120, Google-style docstrings +- `snake_case` for files/modules, `PascalCase` for classes, `UPPER_SNAKE_CASE` for constants +- All data structures use Pydantic `BaseModel` with `Field(...)` and descriptions +- CLI commands use `typer.Typer()` + `rich.console.Console()` +- Only write high-value comments and avoid verbose or redundant commentary +- `rich~=13.5.2` is pinned for semgrep compatibility and should not be upgraded without validation + +## CLI Command Pattern + +```python +import typer +from beartype import beartype +from icontract import require, ensure +from rich.console import Console + +app = typer.Typer() +console = Console() + +@app.command() +@require(lambda repo_path: repo_path.exists(), "Repository path must exist") +@beartype +def my_command( + repo_path: Path = typer.Argument(..., help="Path to repository"), +) -> None: + """Command docstring.""" + console.print("[bold]Processing...[/bold]") +``` -- **Fast**: Commands complete in < 5 seconds for typical repos -- **Clear**: Rich console output with progress bars and tables -- **Helpful**: Comprehensive help text and error messages -- **Reliable**: Contract validation prevents invalid inputs +## Testing -## Related Documentation +**Contract-first approach**: `@icontract` contracts on public APIs are the primary coverage mechanism (target 80%+ API coverage). Redundant unit tests that only assert input validation or type checks should be removed because contracts and beartype already cover them. -- **[README.md](./README.md)** - Project overview and quick start -- **[Contributing Guide](./CONTRIBUTING.md)** - Contribution guidelines and workflow -- **[Testing Guide](./.cursor/rules/testing-and-build-guide.mdc)** - Testing procedures -- **[Python Rules](./.cursor/rules/python-github-rules.mdc)** - Development standards +Test structure mirrors source: `tests/unit/`, `tests/integration/`, `tests/e2e/`. Use `@pytest.mark.asyncio` for async tests. Guard environment-sensitive logic with `os.environ.get("TEST_MODE") == "true"`. ---- +## CI/CD -**Trademarks**: All product names, logos, and brands mentioned in this document are the property of their respective owners. NOLD AI (NOLDAI) is a registered trademark (wordmark) at the European Union Intellectual Property Office (EUIPO). See [TRADEMARKS.md](./TRADEMARKS.md) for more information. +Key workflows in `.github/workflows/`: +- `tests.yml` — contract-first test execution +- `specfact.yml` — contract validation on PR/push (`hatch run specfact repro --verbose`) +- `pr-orchestrator.yml` — coordinates PR workflows +- `build-and-push.yml` — Docker image building (depends on all above passing) diff --git a/CHANGELOG.md b/CHANGELOG.md index 878ec759..fe7f3dcf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,29 @@ All notable changes to this project will be documented in this file. --- +## [0.30.2] - 2026-02-11 + +### Fixed (0.30.2) + +- **Backlog daily/refine filter parity and selection semantics** + - Added missing global filter flags to `specfact backlog daily`: `--search`, `--release`, `--id` (parity with refine). + - Fixed daily issue-window semantics so `--first-issues`/`--last-issues` are applied over the full filtered candidate set (not pre-truncated by default limit). + - Added assignee column in daily standup tables and fixed GitHub `--assignee me`/`@me` handling to use provider semantics without incorrect literal local post-filtering. +- **Interactive comment UX** + - `specfact backlog daily --interactive` now renders comments in scoped panel blocks (refine-like) for clearer context. + - Interactive default remains latest-comment-first; explicit `--first-comments`/`--last-comments` now controls the displayed comment window and shows omitted-count hints. + - Interactive navigation now supports **Post standup update** on the currently selected story; successful post feedback includes explicit story ID and URL. +- **GitHub adapter contract binding bug** + - Fixed icontract decorator placement in `GitHubAdapter` so interactive standup comment posting no longer fails with contract-argument binding errors (`item`/`update_fields`) when checking comment capability. +- **Docs and prompt updates** + - Updated daily/refine docs and prompt templates with standardized filter parity guidance (`--search`, `--release`, `--id`, `--first-issues`, `--last-issues`) and clarified comment behavior (interactive latest-only vs export/summarize full context by default). + +### Changed (0.30.2) + +- **Version**: Bumped to `0.30.2` (patch). + +--- + ## [0.30.1] - 2026-02-10 ### Fixed (0.30.1) diff --git a/CLAUDE.md b/CLAUDE.md index 622af728..607b200b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -84,25 +84,84 @@ Use `from specfact_cli.common import get_bridge_logger` — never `print()`. Deb - `bugfix/your-bugfix-name` - `hotfix/your-hotfix-name` -### Post-Change Checklist +### Pre-Commit Checklist -1. `hatch run format` -2. `hatch run type-check` -3. `hatch run contract-test` -4. `hatch test --cover -v` +Run all steps in order before committing. Every step must pass with no errors. + +1. `hatch run format` # ruff format + autofix +2. `hatch run type-check` # basedpyright strict +3. `hatch run lint` # full lint suite +4. `hatch run yaml-lint` # YAML + markdown validation +5. `hatch run contract-test` # contract-first validation +6. `hatch run smart-test` # targeted test run (use `smart-test-full` for larger modifications) ### OpenSpec Workflow -Before modifying application code, check if an OpenSpec change exists in `openspec/`. This is the spec-driven workflow defined in `openspec/config.yaml`. Skip only when explicitly told ("skip openspec", "direct implementation", "simple fix"). +Before modifying application code, **always** verify that an active OpenSpec change in `openspec/changes/` **explicitly covers the requested modification**. This is the spec-driven workflow defined in `openspec/config.yaml`. Skip only when the user explicitly says `"skip openspec"` or `"implement without openspec change"`. + +**Claude MUST NOT apply any code edits** when a fix, change, modification, or edit to any codebase file is requested unless an active OpenSpec change exists that explicitly covers the requested scope. If no such change exists, ask for clarification: + +- **a) New change** — create a new OpenSpec change proposal (`/opsx:new`) +- **b) Modify existing** — select and continue an existing change in `openspec/changes/` +- **c) Delta** — add a targeted delta to an existing change's specs + +The existence of *any* open change is not sufficient — the change must specifically address the requested modification. Do not proceed until one of the above is resolved. + +#### Change Order (`openspec/CHANGE_ORDER.md`) + +`openspec/CHANGE_ORDER.md` is the **single source of truth** for change sequencing, module grouping, and inter-change dependencies. Always use it to avoid redundant analysis of `openspec/changes/` folders. + +**Read it first** — before creating, implementing, or archiving any change, consult `CHANGE_ORDER.md` to: +- Check which changes are already archived (implemented) and their dates +- Verify hard blockers are resolved before starting implementation +- Understand where a new change fits in module order and wave sequencing + +**Keep it updated** — whenever a change lifecycle event occurs, update `CHANGE_ORDER.md` in the same commit: +- **New change created**: add a row to the correct module group table with folder name, GitHub issue link, and blocked-by dependencies +- **Change archived**: move the entry from "Pending" to "Implemented (archived)" with the archive date; update wave status if a wave is now complete +- **Change modified/renamed**: update the folder name and any affected dependency references +- **Blocker resolved**: update the "Blocked by" column (append ✅ to resolved blockers) ### Version Updates When bumping version, sync across: `pyproject.toml`, `setup.py`, `src/specfact_cli/__init__.py`. CI/CD auto-publishes to PyPI on merge to `main` only if version exceeds the published one. +**Version semantics (SemVer):** +- `feature/*` branches → **minor** increment (e.g. `0.5.0 → 0.6.0`) +- `bugfix/*` / `hotfix/*` branches → **patch** increment (e.g. `0.5.0 → 0.5.1`) +- Breaking changes or major milestones → **major** increment (requires explicit confirmation) + +Always propose the increment type based on the branch name and ask for confirmation before applying the bump. + +### Changelog + +Keep `CHANGELOG.md` updated with every meaningful change. Update it in the same commit that bumps the version — never let them diverge. + +- Follow [Keep a Changelog](https://keepachangelog.com/) format: `Added`, `Changed`, `Fixed`, `Removed`, `Security` +- Each version entry must match the version in `pyproject.toml` +- Unreleased changes accumulate under `## [Unreleased]` until a version bump + ### Commits Follow Conventional Commits: `feat:`, `fix:`, `docs:`, `test:`, `refactor:`. +### Documentation + +Keep docs current with every code change that affects user-facing behaviour. + +- Docs source lives in `docs/` and is published to [docs.specfact.io](https://docs.specfact.io) via GitHub Pages (Jekyll) +- **Preserve all front-matter** on every edit (`title`, `layout`, `nav_order`, `permalink`, etc.) — check `docs/_layouts/default.html` and `docs/index.md` before adding or removing front-matter keys +- When a command, option, or behaviour changes, update the corresponding doc page in the same PR +- Broken or outdated docs for users are P0 — prefer a small doc fix over shipping undocumented changes + +### README Maintenance + +`README.md` (repo root) and the docs landing page (`docs/index.md` or `docs/README.md`) must stay in sync with what SpecFact actually does. + +- On larger refactorings or feature additions, reconsider the README from an **external / new-user perspective**: lead with value and USP, not internal architecture +- A first-time reader should understand what SpecFact does, why they'd use it, and how to get started within the first screen +- Do not let the README drift from the actual CLI interface or command list + ## Code Conventions - Python 3.11+, line length 120, Google-style docstrings diff --git a/docs/getting-started/tutorial-daily-standup-sprint-review.md b/docs/getting-started/tutorial-daily-standup-sprint-review.md index d5da5853..e57531ac 100644 --- a/docs/getting-started/tutorial-daily-standup-sprint-review.md +++ b/docs/getting-started/tutorial-daily-standup-sprint-review.md @@ -19,7 +19,7 @@ This tutorial walks you through a complete **daily standup and sprint review** w - Run **`specfact backlog daily`** and see your standup table (assigned + unassigned items) with **auto-detected** GitHub org/repo or Azure DevOps org/project from the git remote - Use **`.specfact/backlog.yaml`** or environment variables when you're not in the repo (e.g. CI) or to override - **Post a standup comment** to the first (or selected) item with `--yesterday`, `--today`, `--blockers` and `--post` -- Use **`--interactive`** for step-by-step story review (arrow-key selection, full detail, **existing comments on each issue** when the adapter supports them) +- Use **`--interactive`** for step-by-step story review (arrow-key selection, full detail, latest comment + hidden-count hint, and optional in-flow posting on the selected story) - Use **`--copilot-export `** to write a Markdown summary for Copilot slash-command during standup; add **`--comments`** (alias **`--annotations`**) to include descriptions and comment annotations when the adapter supports fetching comments @@ -27,7 +27,7 @@ This tutorial walks you through a complete **daily standup and sprint review** w + standup data) for a slash command (e.g. `specfact.daily`) or copy-paste to Copilot to **generate a standup summary**; add **`--comments`**/**`--annotations`** to include comment annotations in the prompt - Use the **`specfact.backlog-daily`** (or `specfact.daily`) slash prompt for interactive walkthrough with the DevOps team story-by-story (focus, issues, open questions, discussion notes as comments) -- Filter by **`--assignee`**, **`--sprint`** / **`--iteration`**, **`--blockers-first`**, and optional **`--suggest-next`** +- Filter by **`--assignee`**, **`--sprint`** / **`--iteration`**, **`--search`**, **`--release`**, **`--id`**, **`--first-issues`** / **`--last-issues`**, **`--blockers-first`**, and optional **`--suggest-next`** --- @@ -88,7 +88,7 @@ Default scope is **state=open**, **limit=20**; overridable via `SPECFACT_STANDUP ## Step 3: Post a Standup Comment (Optional) -To add a **standup comment** to the **first** item in the list (e.g. the one you're working on), pass **values** for yesterday/today/blockers and `--post`: +To add a **standup comment** to the **first** item in the list, pass **values** for yesterday/today/blockers and `--post`: ```bash specfact backlog daily github \ @@ -98,7 +98,7 @@ specfact backlog daily github \ --post ``` -**Expected**: The CLI posts a comment on that item's issue (GitHub issue or ADO work item) with a standup block (Yesterday / Today / Blockers). You'll see: `✓ Standup comment posted to `. +**Expected**: The CLI posts a comment on that item's issue (GitHub issue or ADO work item) with a standup block (Yesterday / Today / Blockers). You'll see: `✓ Standup comment posted to story : `. **Important**: You must pass **values** for at least one of `--yesterday`, `--today`, or `--blockers`. Using `--post` alone (or with flags but no text) will prompt you to add values; see the in-command message and help. @@ -113,8 +113,8 @@ specfact backlog daily github --interactive ``` - Use the menu to **select** an item (arrow keys). -- View **full detail** (description, acceptance criteria, standup fields, and **existing comments annotated to that issue** when the adapter supports fetching comments—e.g. GitHub issue comments, ADO work item discussion). -- Choose **Next story**, **Previous story**, **Back to list**, or **Exit**. +- View **full detail** (description, acceptance criteria, standup fields, and comment context). Interactive detail shows the **latest comment only** plus a hint when older comments exist. +- Choose **Next story**, **Previous story**, **Post standup update** (posts to the currently selected story), **Back to list**, or **Exit**. Use **`--suggest-next`** to show a suggested next item by value score (business value / (story points × priority)) when the data is available. @@ -183,7 +183,7 @@ issues/open questions, discussion notes as comments). 4. **Optional: interactive review** or **Copilot export**: ```bash - specfact backlog daily github --interactive + specfact backlog daily github --interactive --last-comments 3 # or specfact backlog daily github --copilot-export ./standup.md ``` @@ -197,7 +197,8 @@ issues/open questions, discussion notes as comments). | View standup without typing org/repo | Run `specfact backlog daily github` or `ado` from **repo root**; org/repo or org/project are **auto-detected** from git remote. | | Override or use outside repo | Use `.specfact/backlog.yaml`, env vars (`SPECFACT_GITHUB_REPO_OWNER`, etc.), or CLI `--repo-owner`/`--repo-name` or `--ado-org`/`--ado-project`. | | Post standup to first item | Use `--yesterday "..."` `--today "..."` `--blockers "..."` and `--post` (values required). | -| Step through stories with full detail (including issue comments) | Use `--interactive`; optionally `--suggest-next`. | +| Post standup while reviewing selected story | Use `--interactive` and choose **Post standup update** from navigation. | +| Step through stories with readable comment context | Use `--interactive`; it shows latest comment + hidden-count hint. Use `--first-comments`/`--last-comments` to tune comment density. | | Feed standup into Copilot | Use `--copilot-export `; add `--comments`/`--annotations` for comment annotations. | | Generate standup summary via AI (slash command or Copilot) | Use `--summarize` (stdout) or `--summarize-to `; add `--comments`/`--annotations` for comment annotations; use with `specfact.backlog-daily` slash prompt. | diff --git a/docs/guides/agile-scrum-workflows.md b/docs/guides/agile-scrum-workflows.md index e70940d4..50d14e04 100644 --- a/docs/guides/agile-scrum-workflows.md +++ b/docs/guides/agile-scrum-workflows.md @@ -26,12 +26,17 @@ SpecFact CLI supports real-world agile/scrum practices through: (yesterday/today/blockers) from item body; optionally post standup comment to linked issue via `--post` when the adapter supports comments (e.g. GitHub). **Interactive step-by-step review**: Use `--interactive` to select stories with arrow keys (questionary) - and view full detail (refine-like: description, acceptance criteria, standup fields, comments when adapter - supports); navigate with Next/Previous/Back to list/Exit. Use `--suggest-next` to show suggested next - item by value score (business_value / (story_points × priority)). + and view full detail (refine-like: description, acceptance criteria, standup fields). Interactive detail + shows the **latest comment only** plus a hint when older comments exist; use export options for full + comment history. Navigate with Next/Previous/**Post standup update**/Back to list/Exit. `Post standup update` + posts yesterday/today/blockers to the currently selected story (adapter support required). Use `--suggest-next` + to show suggested next item by value score (business_value / (story_points × priority)). **Copilot export**: Use `--copilot-export ` to write a summarized Markdown file of each story for Copilot. Add `--comments` (alias `--annotations`) to include descriptions and comment annotations in - `--copilot-export` and `--summarize` outputs when the adapter supports `get_comments` (GitHub). + `--copilot-export` and `--summarize` outputs when the adapter supports `get_comments` (GitHub, ADO). Use + `--first-comments N` or `--last-comments N` to scope comment volume when needed (default: include all). + Use `--first-issues N` or `--last-issues N` (mutually exclusive) to scope daily output to oldest/newest + items by numeric issue/work-item ID. **Kanban**: omit iteration/sprint and use state + limit; unassigned = pullable work. **Scrum/SAFe**: use `--sprint current` and optional priority/value. **Out of scope**: Sprint goal is in your board/sprint settings (not displayed by CLI). Stale/at-risk flags (e.g. "no update in N days") are not in scope—use @@ -76,9 +81,9 @@ specfact backlog daily github \ --post # 4. Optional: interactive step-through, Copilot export, or standup summary prompt -specfact backlog daily github --interactive # step-through; detail view shows existing comments on each issue +specfact backlog daily github --interactive # step-through; detail view shows latest comment + hidden-count hint # or -specfact backlog daily github --copilot-export ./standup.md --comments +specfact backlog daily github --copilot-export ./standup.md --comments --last-comments 5 # or specfact backlog daily github --summarize --comments # prompt to stdout for AI to generate standup summary specfact backlog daily github --summarize-to ./standup-prompt.md @@ -88,7 +93,9 @@ Use the **`specfact.backlog-daily`** (or `specfact.daily`) slash prompt for inte DevOps team story-by-story (current focus, issues/open questions, discussion notes as comments). Default scope: **state=open**, **limit=20**; configure via `SPECFACT_STANDUP_*` or `.specfact/standup.yaml`. Use `--assignee me`, `--sprint current`, `--blockers-first`, `--interactive`, `--suggest-next`, -`--copilot-export `, `--summarize`, `--summarize-to `, and `--comments`/`--annotations` as +`--copilot-export `, `--summarize`, `--summarize-to `, `--comments`/`--annotations`, and optional +`--first-comments`/`--last-comments` plus `--first-issues`/`--last-issues` as well as global filters +`--search`, `--release`, and `--id` to narrow scope consistently with backlog refine needed. See [Tutorial: Daily Standup and Sprint Review](../getting-started/tutorial-daily-standup-sprint-review.md) for the full walkthrough. diff --git a/docs/guides/backlog-refinement.md b/docs/guides/backlog-refinement.md index 2582369c..c82c7dd4 100644 --- a/docs/guides/backlog-refinement.md +++ b/docs/guides/backlog-refinement.md @@ -481,6 +481,59 @@ specfact backlog refine ado \ specfact backlog refine ado --iteration "Project\\Release 1\\Sprint 1" ``` +### 4. Export Full Comment Context for Copilot + +`specfact backlog refine --export-to-tmp` now includes issue/work item comments (when adapter supports comments, including ADO) so refinement context is complete by default. + +```bash +# Export with full comment history (default, no truncation) +specfact backlog refine ado \ + --ado-org my-org \ + --ado-project my-project \ + --state Active \ + --export-to-tmp + +# Optional: preview only first N comments in terminal output +specfact backlog refine ado \ + --ado-org my-org \ + --ado-project my-project \ + --state Active \ + --preview \ + --first-comments 3 + +# Optional: preview only last N comments in terminal output +specfact backlog refine ado \ + --ado-org my-org \ + --ado-project my-project \ + --state Active \ + --preview \ + --last-comments 4 +``` + +Preview defaults to the last 2 comments per item to keep CLI output readable. +`--first-comments N` and `--last-comments N` are mutually exclusive and affect preview density and write-mode prompt comment context. +In `--write` workflows, prompts include full comment history by default unless a first/last comment window is provided. +`--export-to-tmp` always writes full comment history. +The export file now includes a `## Copilot Instructions` block and per-item template guidance, and Copilot should follow those embedded instructions when refining. +For export-driven refinement, treat the embedded file instructions as the canonical format contract. +For `--import-from-tmp`, ensure the refined artifact excludes the instruction header and retains only `## Item N:` sections with refined fields. + +Use `--first-issues N` or `--last-issues N` to process only a first/last slice of filtered issues in a refine run (mutually exclusive). +Issue windowing is based on numeric issue/work-item IDs: lower IDs are treated as older (`--first-issues`), higher IDs as newer (`--last-issues`). + +### 5. Shared Backlog Filter Parity (Refine + Daily) + +`specfact backlog refine` and `specfact backlog daily` now share the same global backlog scoping semantics for common workflows: + +- `--search`, `--release`, `--id` for consistent item selection +- `--first-issues N` / `--last-issues N` for deterministic oldest/newest issue windows (numeric ID ordering) +- comment-window options where applicable: + - **Refine**: `--first-comments N` / `--last-comments N` affect preview and write-prompt context + - **Daily export/summarize**: `--first-comments N` / `--last-comments N` scope `--comments` output + - **Daily interactive**: latest comment by default; explicit comment-window flags override that default + +For day-to-day team flow, this means you can switch between `backlog daily` and `backlog refine` without changing filter mental models. + --- ## Workflow Integration diff --git a/docs/guides/devops-adapter-integration.md b/docs/guides/devops-adapter-integration.md index 63bf15a3..9ea46d25 100644 --- a/docs/guides/devops-adapter-integration.md +++ b/docs/guides/devops-adapter-integration.md @@ -27,10 +27,14 @@ SpecFact CLI supports **bidirectional synchronization** between OpenSpec change end date support depend on the adapter (ADO supports current iteration and iteration path; see adapter docs). Use `--blockers-first` and config `show_priority`/`show_value` for time-critical and value-driven standups. **Interactive review** (`--interactive`): step-through stories with arrow-key selection; detail - view shows **existing comments annotated to each issue** when the adapter implements `get_comments(item)` - (GitHub adapter supports it). **Comment annotations in exports**: add `--comments` (alias - `--annotations`) to include descriptions and comment annotations in `--copilot-export` and - `--summarize`/`--summarize-to` outputs when the adapter supports fetching comments. **Value score / + view shows the **latest comment** and hints when older comments exist; interactive navigation includes + **Post standup update** to post yesterday/today/blockers directly on the currently selected story. + **Comment annotations in exports**: + add `--comments` (alias `--annotations`) to include descriptions and comment annotations in + `--copilot-export` and `--summarize`/`--summarize-to` outputs when the adapter supports fetching comments + (GitHub and ADO). Use optional `--first-comments N` or `--last-comments N` to scope comment volume; + default is full comment context. Use `--first-issues N` / `--last-issues N` and global filters + `--search`, `--release`, `--id` for consistent backlog scope across daily/refine commands. **Value score / suggested next**: when BacklogItem has `story_points`, `business_value`, and `priority`, use `--suggest-next` or config `suggest_next` to show suggested next item (business_value / (story_points × priority)). **Standup summary prompt** (`--summarize` or `--summarize-to PATH`): output a prompt diff --git a/openspec/changes/backlog-scrum-01-standup-exceptions-first/CHANGE_VALIDATION.md b/openspec/changes/backlog-scrum-01-standup-exceptions-first/CHANGE_VALIDATION.md index 75d1de02..4be5f8f4 100644 --- a/openspec/changes/backlog-scrum-01-standup-exceptions-first/CHANGE_VALIDATION.md +++ b/openspec/changes/backlog-scrum-01-standup-exceptions-first/CHANGE_VALIDATION.md @@ -40,3 +40,29 @@ This change was re-validated after renaming and updating to align with the modul - All old change ID references updated to new module-scoped naming **Result**: Pass — format compliant, module architecture aligned, no breaking changes introduced. + +## Delta Re-Validation (2026-02-10) + +- **Scope extension**: Added focused delta for comment-context behavior across `backlog daily` and `backlog refine`: + - ADO comments API usage and pagination (`workItems/{id}/comments`, `api-version=7.1-preview.4`) + - Default full comment inclusion for export/summarize flows + - Refine preview default comment scope (last 2 comments) with optional `--first-comments N` / `--last-comments N` + - Refine issue window controls with `--first-issues N` / `--last-issues N` (mutually exclusive) + - Refine export always includes full comments (no truncation) + - Refine preview shows progress feedback while fetching comments (`Fetching issue n/m ...`) + - Refine preview renders comments in scoped panel blocks for clear boundaries + - Refine preview explicitly shows a no-comments hint when comment history is empty + - Refine write-mode prompts include comment context (full by default, optional first/last windowing) + - Refine export includes a top instruction header for Copilot and explicit note to omit that header in refined import artifacts + - Refine export instructions now mirror interactive refinement rules and include per-item template guidance + - Interactive daily detail view scoped to latest comment with hidden-count/export guidance + - Prompt and documentation alignment updates +- **OpenSpec strict validation**: `openspec validate backlog-scrum-01-standup-exceptions-first --strict` → **valid**. +- **Breaking changes**: 0 (additive behavior and optional flags only). +- **Dependency impact**: limited to backlog command/comment retrieval paths; no public API removals. + +## TDD Evidence Note + +- Evidence file: `openspec/changes/backlog-scrum-01-standup-exceptions-first/TDD_EVIDENCE.md`. +- This pass includes a documented sequencing gap: failing-test evidence was not captured before implementation for the comment-context delta. +- A follow-up incremental refine-preview delta in the same change now includes captured failing-first evidence and passing evidence. diff --git a/openspec/changes/backlog-scrum-01-standup-exceptions-first/TDD_EVIDENCE.md b/openspec/changes/backlog-scrum-01-standup-exceptions-first/TDD_EVIDENCE.md new file mode 100644 index 00000000..67068d0a --- /dev/null +++ b/openspec/changes/backlog-scrum-01-standup-exceptions-first/TDD_EVIDENCE.md @@ -0,0 +1,599 @@ +# TDD Evidence: backlog-scrum-01-standup-exceptions-first + +## Scope + +Delta scope implemented in this pass: + +- ADO comments API pagination + `get_comments` +- `backlog daily` comment window options (`--first-comments`, `--last-comments`) +- interactive daily detail comment scoping (latest comment + hidden-count hint) +- `backlog refine --export-to-tmp` comment export context + comment window options + +## Pre-Implementation (Expected Failing Tests) + +**Status**: Not captured before implementation for this pass (process violation). +**Recorded at**: 2026-02-10 22:02:36Z + +Notes: + +- Tests and code were edited in the same implementation window. +- This does not satisfy strict SDD+TDD order (tests failing first before code edits). +- Future changes must capture failing evidence before any production code edits. + +## Post-Implementation (Passing Tests) + +**Recorded at**: 2026-02-10 22:02:36Z + +### Targeted behavior tests + +Command: + +```bash +hatch run pytest tests/unit/adapters/test_ado_backlog_adapter.py tests/unit/commands/test_backlog_daily.py tests/unit/commands/test_backlog_commands.py -q +``` + +Result: + +- **83 passed** (later rerun after extra assertion: **83/83 passed**; then smart-test run includes these tests and reports **89 passed** in selected set). + +### Quality/validation runs + +Commands: + +```bash +hatch run format +hatch run type-check +hatch run contract-test +hatch run lint +hatch run yaml-lint +hatch run smart-test +openspec validate backlog-scrum-01-standup-exceptions-first --strict +``` + +Result summary: + +- format: pass +- type-check: pass (0 errors, warnings present in repo baseline) +- contract-test: pass +- lint: pass +- yaml-lint: pass +- smart-test: pass (selected unit set) +- openspec strict validation: pass + +## Incremental Delta: Refine Preview Comment Scope (2026-02-10) + +### Pre-Implementation (Expected Failure Captured) + +**Recorded at**: 2026-02-10 22:10:49Z + +Command: + +```bash +hatch run pytest tests/unit/commands/test_backlog_commands.py -k "RefineCommentWindowResolution" -v +``` + +Result: + +- **failed during collection** (expected before implementation): + - `ImportError: cannot import name '_resolve_refine_export_comment_window'` + +### Post-Implementation (Passing) + +**Recorded at**: 2026-02-10 22:10:49Z + +Commands: + +```bash +hatch run pytest tests/unit/commands/test_backlog_commands.py -k "RefineCommentWindowResolution or BuildRefineExportContent" -v +hatch run pytest tests/unit/commands/test_backlog_commands.py tests/unit/commands/test_backlog_daily.py tests/unit/adapters/test_ado_backlog_adapter.py -q +``` + +Result: + +- Targeted refine tests: **6 passed** +- Regression set: **87 passed** + +## Incremental Delta: Refine Preview UX Feedback (2026-02-10) + +### Pre-Implementation (Expected Failure Captured) + +**Recorded at**: 2026-02-10 22:21:17Z + +Command: + +```bash +hatch run pytest tests/unit/commands/test_backlog_commands.py -k "RefinePreviewCommentUx" -v +``` + +Result: + +- **failed during collection** (expected before implementation): + - `ImportError: cannot import name '_build_comment_fetch_progress_description'` + +### Post-Implementation (Passing) + +**Recorded at**: 2026-02-10 22:21:17Z + +Commands: + +```bash +hatch run pytest tests/unit/commands/test_backlog_commands.py -k "RefinePreviewCommentUx or RefineCommentWindowResolution" -v +hatch run pytest tests/unit/commands/test_backlog_commands.py tests/unit/commands/test_backlog_daily.py tests/unit/adapters/test_ado_backlog_adapter.py -q +``` + +Result: + +- Targeted refine UX tests: **6 passed** +- Regression set: **89 passed** + +## Incremental Delta: Refine Issue Window Controls (2026-02-10) + +### Pre-Implementation (Expected Failure Captured) + +**Recorded at**: 2026-02-10 22:25:53Z + +Command: + +```bash +hatch run pytest tests/unit/commands/test_backlog_commands.py -k "RefineIssueWindow" -v +``` + +Result: + +- **failed during collection** (expected before implementation): + - `ImportError: cannot import name '_apply_issue_window'` + +### Post-Implementation (Passing) + +**Recorded at**: 2026-02-10 22:25:53Z + +Commands: + +```bash +hatch run pytest tests/unit/commands/test_backlog_commands.py -k "RefineIssueWindow" -v +hatch run pytest tests/unit/commands/test_backlog_commands.py tests/unit/commands/test_backlog_daily.py tests/unit/adapters/test_ado_backlog_adapter.py -q +``` + +Result: + +- Targeted issue-window tests: **3 passed** +- Regression set: **92 passed** + +## Incremental Delta: Refine Issue Window Ordering Fix (2026-02-10) + +### Pre-Implementation (Expected Failure Captured) + +**Recorded at**: 2026-02-10 22:28:42Z + +Command: + +```bash +hatch run pytest tests/unit/commands/test_backlog_commands.py -k "RefineIssueWindow" -v +``` + +Result: + +- **2 tests failed** (expected before ordering fix): + - `test_apply_issue_window_first_issues` + - `test_apply_issue_window_last_issues` +- Failure showed current behavior using input order instead of numeric ID order. + +### Post-Implementation (Passing) + +**Recorded at**: 2026-02-10 22:28:42Z + +Commands: + +```bash +hatch run pytest tests/unit/commands/test_backlog_commands.py -k "RefineIssueWindow" -v +hatch run pytest tests/unit/commands/test_backlog_commands.py tests/unit/commands/test_backlog_daily.py tests/unit/adapters/test_ado_backlog_adapter.py -q +``` + +Result: + +- Targeted issue-window tests: **3 passed** +- Regression set: **92 passed** + +## Incremental Delta: No-Comments Preview Hint (2026-02-10) + +### Pre-Implementation (Expected Failure Captured) + +**Recorded at**: 2026-02-10 22:33:57Z + +Command: + +```bash +hatch run pytest tests/unit/commands/test_backlog_commands.py -k "RefinePreviewCommentUx" -v +``` + +Result: + +- **failed during collection** (expected before implementation): + - `ImportError: cannot import name '_build_refine_preview_comment_empty_panel'` + +### Post-Implementation (Passing) + +**Recorded at**: 2026-02-10 22:33:57Z + +Commands: + +```bash +hatch run pytest tests/unit/commands/test_backlog_commands.py -k "RefinePreviewCommentUx" -v +hatch run pytest tests/unit/commands/test_backlog_commands.py tests/unit/commands/test_backlog_daily.py tests/unit/adapters/test_ado_backlog_adapter.py -q +``` + +Result: + +- Targeted preview UX tests: **3 passed** +- Regression set: **93 passed** + +## Incremental Delta: Write-Mode Prompt Comment Context (2026-02-10) + +### Pre-Implementation (Expected Failure Captured) + +**Recorded at**: 2026-02-10 22:42:10Z + +Command: + +```bash +hatch run pytest tests/unit/backlog/test_ai_refiner.py -k "includes_comments_when_provided or mentions_no_comments_when_empty" -v +``` + +Result: + +- **2 tests failed** (expected before implementation): + - `test_generate_refinement_prompt_includes_comments_when_provided` + - `test_generate_refinement_prompt_mentions_no_comments_when_empty` +- Failure root cause: `generate_refinement_prompt()` did not accept `comments` argument. + +### Post-Implementation (Passing) + +**Recorded at**: 2026-02-10 22:42:10Z + +Commands: + +```bash +hatch run pytest tests/unit/backlog/test_ai_refiner.py -k "includes_comments_when_provided or mentions_no_comments_when_empty" -v +hatch run pytest tests/unit/backlog/test_ai_refiner.py tests/unit/commands/test_backlog_commands.py tests/unit/commands/test_backlog_daily.py tests/unit/adapters/test_ado_backlog_adapter.py -q +``` + +Result: + +- Targeted AI refiner comment-context tests: **2 passed** +- Regression set: **106 passed** + +## Incremental Delta: Refine Export Copilot Instruction Header (2026-02-10) + +### Pre-Implementation (Expected Failure Captured) + +**Recorded at**: 2026-02-10 22:50:31Z + +Command: + +```bash +hatch run pytest tests/unit/commands/test_backlog_commands.py -k "BuildRefineExportContent" -v +``` + +Result: + +- **2 tests failed** (expected before implementation): + - `test_refine_export_includes_comments_when_available` + - `test_refine_export_places_instructions_before_first_item` +- Failure root cause: export did not include a top-level instruction block. + +### Post-Implementation (Passing) + +**Recorded at**: 2026-02-10 22:50:31Z + +Commands: + +```bash +hatch run pytest tests/unit/commands/test_backlog_commands.py -k "BuildRefineExportContent" -v +hatch run pytest tests/unit/backlog/test_ai_refiner.py tests/unit/commands/test_backlog_commands.py tests/unit/commands/test_backlog_daily.py tests/unit/adapters/test_ado_backlog_adapter.py -q +``` + +Result: + +- Targeted export-content tests: **3 passed** +- Regression set: **107 passed** + +## Incremental Delta: Export Instruction Parity with Interactive Mode (2026-02-10) + +### Pre-Implementation (Expected Failure Captured) + +**Recorded at**: 2026-02-10 22:55:51Z + +Command: + +```bash +hatch run pytest tests/unit/commands/test_backlog_commands.py -k "BuildRefineExportContent" -v +``` + +Result: + +- **2 tests failed** (expected before implementation): + - `test_refine_export_includes_comments_when_available` (missing full interactive-equivalent rule text) + - `test_refine_export_includes_template_guidance_for_items` (missing per-item template guidance fields) + +### Post-Implementation (Passing) + +**Recorded at**: 2026-02-10 22:55:51Z + +Commands: + +```bash +hatch run pytest tests/unit/commands/test_backlog_commands.py -k "BuildRefineExportContent" -v +hatch run pytest tests/unit/backlog/test_ai_refiner.py tests/unit/commands/test_backlog_commands.py tests/unit/commands/test_backlog_daily.py tests/unit/adapters/test_ado_backlog_adapter.py -q +``` + +Result: + +- Targeted export-content tests: **4 passed** +- Regression set: **108 passed** + +## Incremental Delta: Daily Assignee Visibility + GitHub `me` Filter (2026-02-11) + +### Pre-Implementation (Expected Failure Captured) + +**Recorded at**: 2026-02-11 00:12:34Z + +Command: + +```bash +hatch run pytest tests/unit/commands/test_backlog_daily.py -k "row_includes_assignees_for_table_rendering or AssigneeFilterResolution" -q +``` + +Result: + +- **failed during collection** (expected before implementation): + - `ImportError: cannot import name '_resolve_post_fetch_assignee_filter'` + +### Post-Implementation (Passing) + +**Recorded at**: 2026-02-11 00:12:34Z + +Commands: + +```bash +hatch run pytest tests/unit/commands/test_backlog_daily.py -k "row_includes_assignees_for_table_rendering or AssigneeFilterResolution" -q +hatch run pytest tests/unit/adapters/test_github_backlog_adapter.py -k "me_assignee or assignee_filter" -q +hatch run pytest tests/unit/commands/test_backlog_daily.py tests/unit/commands/test_backlog_commands.py tests/unit/adapters/test_github_backlog_adapter.py tests/unit/adapters/test_ado_backlog_adapter.py -q +``` + +Result: + +- Targeted daily tests: **3 passed** +- Targeted GitHub adapter tests: **2 passed** +- Regression set: **109 passed** + +## Incremental Delta: Daily Issue Window Parity with Refine (2026-02-11) + +### Pre-Implementation (Expected Failure Captured) + +**Recorded at**: 2026-02-11 00:24:07Z + +Command: + +```bash +hatch run pytest tests/unit/commands/test_backlog_daily.py -k "issue_window" -q +``` + +Result: + +- **failed during collection** (expected before implementation): + - `ImportError: cannot import name '_resolve_daily_issue_window'` + +### Post-Implementation (Passing) + +**Recorded at**: 2026-02-11 00:24:07Z + +Commands: + +```bash +hatch run pytest tests/unit/commands/test_backlog_daily.py -k "issue_window" -q +hatch run pytest tests/unit/commands/test_backlog_daily.py tests/unit/commands/test_backlog_commands.py tests/unit/adapters/test_github_backlog_adapter.py tests/unit/adapters/test_ado_backlog_adapter.py -q +``` + +Result: + +- Targeted issue-window tests: **4 passed** +- Regression set: **113 passed** + +## Incremental Delta: Daily Issue Window Before Pre-Limit Truncation (2026-02-11) + +### Pre-Implementation (Expected Failure Captured) + +**Recorded at**: 2026-02-11 00:31:27Z + +Command: + +```bash +hatch run pytest tests/unit/commands/test_backlog_daily.py -k "DailyFetchLimitResolution" -q +``` + +Result: + +- **failed during collection** (expected before implementation): + - `ImportError: cannot import name '_resolve_daily_fetch_limit'` + +### Post-Implementation (Passing) + +**Recorded at**: 2026-02-11 00:31:27Z + +Commands: + +```bash +hatch run pytest tests/unit/commands/test_backlog_daily.py -k "DailyFetchLimitResolution or DailyIssueWindowResolution" -q +hatch run pytest tests/unit/commands/test_backlog_daily.py tests/unit/commands/test_backlog_commands.py tests/unit/adapters/test_github_backlog_adapter.py tests/unit/adapters/test_ado_backlog_adapter.py -q +``` + +Result: + +- Targeted fetch-limit/issue-window tests: **5 passed** +- Regression set: **115 passed** + +## Incremental Delta: Interactive Comment Window Override in Daily (2026-02-11) + +### Pre-Implementation (Expected Failure Captured) + +**Recorded at**: 2026-02-11 00:36:11Z + +Command: + +```bash +hatch run pytest tests/unit/commands/test_backlog_daily.py -k "honors_explicit_comment_window_in_interactive" -q +``` + +Result: + +- **1 test failed** (expected before implementation): + - `TypeError: _format_daily_item_detail() got an unexpected keyword argument 'show_all_provided_comments'` + +### Post-Implementation (Passing) + +**Recorded at**: 2026-02-11 00:36:11Z + +Commands: + +```bash +hatch run pytest tests/unit/commands/test_backlog_daily.py -k "honors_explicit_comment_window_in_interactive or shows_latest_comment_only_with_hint" -q +hatch run pytest tests/unit/commands/test_backlog_daily.py tests/unit/commands/test_backlog_commands.py tests/unit/adapters/test_github_backlog_adapter.py tests/unit/adapters/test_ado_backlog_adapter.py -q +``` + +Result: + +- Targeted interactive comment tests: **2 passed** +- Regression set: **116 passed** + +## Incremental Delta: Daily Interactive Comment Panel Formatting (2026-02-11) + +### Pre-Implementation (Expected Failure Captured) + +**Recorded at**: 2026-02-11 00:44:03Z + +Command: + +```bash +hatch run pytest tests/unit/commands/test_backlog_daily.py -k "DailyInteractiveCommentPanels or omits_comment_block" -q +``` + +Result: + +- **failed during collection** (expected before implementation): + - `ImportError: cannot import name '_build_daily_interactive_comment_panels'` + +### Post-Implementation (Passing) + +**Recorded at**: 2026-02-11 00:44:03Z + +Commands: + +```bash +hatch run pytest tests/unit/commands/test_backlog_daily.py -k "DailyInteractiveCommentPanels or omits_comment_block" -q +hatch run pytest tests/unit/commands/test_backlog_daily.py tests/unit/commands/test_backlog_commands.py tests/unit/adapters/test_github_backlog_adapter.py tests/unit/adapters/test_ado_backlog_adapter.py -q +``` + +Result: + +- Targeted panel-format tests: **3 passed** +- Regression set: **117 passed** + +## Incremental Delta: Daily Global Filter Parity (`--search`, `--release`, `--id`) (2026-02-11) + +### Pre-Implementation (Expected Failure Captured) + +**Recorded at**: 2026-02-11 01:02:19Z + +Command: + +```bash +hatch run pytest tests/unit/commands/test_backlog_daily.py -k "search_release_and_id_options or IssueIdFilter" -q +``` + +Result: + +- **failed during collection** (expected before implementation): + - `ImportError: cannot import name '_apply_issue_id_filter'` + +### Post-Implementation (Passing) + +**Recorded at**: 2026-02-11 01:02:19Z + +Commands: + +```bash +hatch run pytest tests/unit/commands/test_backlog_daily.py -k "search_release_and_id_options or IssueIdFilter" -q +hatch run pytest tests/unit/commands/test_backlog_daily.py tests/unit/commands/test_backlog_commands.py tests/unit/adapters/test_github_backlog_adapter.py tests/unit/adapters/test_ado_backlog_adapter.py -q +``` + +Result: + +- Targeted global-filter parity tests: **3 passed** +- Regression set: **120 passed** + +## Incremental Delta: Exceptions-First Ordering + Daily Mode/Patch Completion (2026-02-11) + +### Pre-Implementation (Expected Failure Captured) + +**Recorded at**: 2026-02-11 01:14:00Z + +Command: + +```bash +hatch run pytest tests/unit/commands/test_backlog_daily.py -k "orders_blockers_then_policy_then_aging" -q +``` + +Result: + +- **1 test failed** (expected before implementation): + - `test_split_exception_rows_orders_blockers_then_policy_then_aging` +- Failure showed only blocker rows were classified as exceptions; policy/aging rows were not included. + +### Post-Implementation (Passing) + +**Recorded at**: 2026-02-11 01:15:00Z + +Commands: + +```bash +hatch run pytest tests/unit/commands/test_backlog_daily.py -k "ExceptionsFirstAndMode" -q +hatch run pytest tests/unit/commands/test_backlog_daily.py tests/unit/commands/test_backlog_commands.py tests/unit/adapters/test_github_backlog_adapter.py tests/unit/adapters/test_ado_backlog_adapter.py -q +``` + +Result: + +- Targeted exceptions/mode/patch tests: **5 passed** +- Regression set: **126 passed** + +## Incremental Delta: Interactive Daily Post Action on Selected Story (2026-02-11) + +### Pre-Implementation (Expected Failure Captured) + +**Recorded at**: 2026-02-11 01:31:00Z + +Command: + +```bash +hatch run pytest tests/unit/commands/test_backlog_daily.py -k "InteractivePostAction" -q +``` + +Result: + +- **failed during collection** (expected before implementation): + - `ImportError: cannot import name '_build_daily_navigation_choices'` + +### Post-Implementation (Passing) + +**Recorded at**: 2026-02-11 01:33:00Z + +Commands: + +```bash +hatch run pytest tests/unit/commands/test_backlog_daily.py -k "InteractivePostAction" -q +hatch run pytest tests/unit/commands/test_backlog_daily.py tests/unit/commands/test_backlog_commands.py -q +``` + +Result: + +- Targeted interactive post tests: **4 passed** +- Regression set (`daily` + `backlog commands`): **97 passed** diff --git a/openspec/changes/backlog-scrum-01-standup-exceptions-first/proposal.md b/openspec/changes/backlog-scrum-01-standup-exceptions-first/proposal.md index cec3819a..b415c8cb 100644 --- a/openspec/changes/backlog-scrum-01-standup-exceptions-first/proposal.md +++ b/openspec/changes/backlog-scrum-01-standup-exceptions-first/proposal.md @@ -70,9 +70,18 @@ modules/backlog-scrum/ - **NEW**: Add `--mode scrum|kanban|safe` flag to `specfact backlog daily`; `scrum` is the default when this module is loaded. - **EXTEND** (policy-engine-01): When policy-engine-01 is present, query policy results for each item and surface failures in section (2); graceful no-op if not installed. - **EXTEND** (patch-mode-01): Integrate `--patch` flag to propose standup notes or missing fields as a patch file; graceful no-op if patch-mode-01 not installed. +- **EXTEND** (ADO comment context): Fetch ADO work item comments using the dedicated comments API resource (`workItems/{id}/comments`, API `7.1-preview.4`) with pagination so `backlog daily` and `backlog refine` can use complete comment history. +- **NEW**: Add optional comment windowing controls `--first-comments N` and `--last-comments N` for daily exports/summaries and refine preview output; refine export always keeps full comments (no truncation). +- **EXTEND**: Include comment context in refine write-mode prompts (full by default; first/last windowing optional for noise control). +- **EXTEND**: Add a Copilot instruction header to refine export files; refined import artifacts must omit the header and keep only item blocks. +- **EXTEND**: Make refine export guidance parity with interactive prompts by embedding equivalent refinement rules and per-item template guidance. +- **NEW**: Add optional issue windowing controls `--first-issues N` and `--last-issues N` for refine runs to process deterministic first/last item slices. +- **EXTEND** (interactive output UX): In `specfact backlog daily --interactive`, show only the latest comment plus a count hint for remaining comments and guidance to use export-to-file options for full comment context. +- **EXTEND** (prompt/docs): Update slash prompt templates and user docs so comment context behavior and comment-windowing options are explicit for everyday team workflows. ## Capabilities - **backlog-scrum** (standup): Exceptions-first section order (blockers, policy failures, aging, normal); `--mode scrum|kanban|safe`; optional patch integration for standup notes; Policy Engine integration for policy failure surfacing. +- **backlog-scrum** (comment context): Full ADO comment retrieval for daily/refine, optional first/last comment limits, interactive last-comment-only rendering with export guidance, and aligned slash prompts/docs. --- diff --git a/openspec/changes/backlog-scrum-01-standup-exceptions-first/specs/backlog-refinement/spec.md b/openspec/changes/backlog-scrum-01-standup-exceptions-first/specs/backlog-refinement/spec.md new file mode 100644 index 00000000..cedd9c3c --- /dev/null +++ b/openspec/changes/backlog-scrum-01-standup-exceptions-first/specs/backlog-refinement/spec.md @@ -0,0 +1,179 @@ +# Backlog refinement comment context (E1 scoped delta) + +## MODIFIED Requirements + +### Requirement: Export refine context includes comments without truncation by default + +The system SHALL include issue/work item comments in `specfact backlog refine --export-to-tmp` output so exported refinement context is complete by default. Comment content SHALL not be truncated unless explicitly requested by the user. + +**Rationale**: Refinement quality depends on full historical discussion context, especially for ADO work items where key decisions are often in comments. + +#### Scenario: Refine export contains full comments by default + +**Given**: The user runs `specfact backlog refine --export-to-tmp` for an adapter that supports comments + +**And**: A backlog item has comments in the provider + +**When**: No explicit comment-window options are provided + +**Then**: The exported markdown includes all comments for the item + +**And**: Comment text is preserved without truncation + +#### Scenario: Refine export includes copilot instruction block + +**Given**: The user runs `specfact backlog refine --export-to-tmp` + +**When**: The export file is generated + +**Then**: The file starts with a clear copilot instruction/prompt block before item entries + +**And**: The instruction block tells the user/copilot how to process item sections consistently + +**And**: The instruction block explicitly states that the refined artifact for import must omit the instruction block and contain only item sections + +#### Scenario: Refine export instructions match interactive refinement rules + +**Given**: The user runs `specfact backlog refine --export-to-tmp` + +**When**: Copilot reads the exported file + +**Then**: The exported instruction block includes the same refinement rules used in interactive mode (preserve scope, required-section completion, ambiguity notes, provider-aware formatting) + +**And**: Each item includes template guidance (target template, required sections, optional sections) so export processing can follow the same structure as interactive prompts + +### Requirement: Refine preview includes scoped comment context + +The system SHALL include issue/work item comments in `specfact backlog refine --preview` output with a scoped default to keep terminal output readable. + +**Rationale**: Refinement decisions depend on discussion history, but preview output must stay concise for day-to-day CLI usage. + +#### Scenario: Refine preview shows last two comments by default + +**Given**: The user runs `specfact backlog refine --preview` for an adapter that supports comments + +**And**: A backlog item has multiple comments + +**When**: No explicit comment-window options are provided + +**Then**: The preview shows the two newest comments for that item + +#### Scenario: First-comments limit on refine preview + +**Given**: The user runs `specfact backlog refine --preview --first-comments 5` + +**When**: A backlog item has more than five comments + +**Then**: The preview comment section contains only the first five comments for that item + +#### Scenario: Last-comments limit on refine preview + +**Given**: The user runs `specfact backlog refine --preview --last-comments 4` + +**When**: A backlog item has more than four comments + +**Then**: The preview comment section contains only the last four comments for that item + +#### Scenario: Preview shows comment-fetch progress for large batches + +**Given**: The user runs `specfact backlog refine --preview` for many backlog items + +**When**: The command fetches comments across adapters + +**Then**: The CLI shows progress feedback with item position (for example `Fetching issue n/m ...`) until comment fetch completes + +#### Scenario: Preview comment output is clearly scoped + +**Given**: The preview includes comments for an item + +**When**: The command renders preview detail + +**Then**: Each comment is rendered in a clearly scoped block-style container so users can distinguish comment boundaries from body/metadata + +#### Scenario: Preview indicates when no comments exist + +**Given**: The preview fetches comments for an item + +**When**: No comments are available for that issue/work item + +**Then**: The preview still shows a comments section with an explicit "no comments found" hint + +**Acceptance Criteria**: + +- Default refine preview includes the last two comments per item. +- Limits are optional and deterministic for preview output. +- If both first and last limits are provided, command fails with a clear validation error. +- `--export-to-tmp` always includes full comments, independent of preview comment-window options. +- Preview provides visible comment-fetch progress for multi-item runs. +- Preview comment rendering uses block-style formatting to make comment boundaries explicit. +- Preview explicitly indicates when an item has no comments. + +### Requirement: Refine write prompts include comment context + +The system SHALL include issue/work item comments in generated refinement prompts during `specfact backlog refine --write` so AI-assisted refinement reflects the latest discussion state. + +**Rationale**: Comment threads are the living source of truth; prompt context must include them to avoid refining against stale issue bodies. + +#### Scenario: Write-mode prompt includes full comments by default + +**Given**: The user runs `specfact backlog refine --write` + +**And**: The selected issue/work item has comments + +**When**: No explicit comment-window options are provided + +**Then**: The generated refinement prompt includes all available comments for that item + +#### Scenario: Write-mode prompt applies comment-window options + +**Given**: The user runs `specfact backlog refine --write --last-comments 5` + +**When**: The item has more than five comments + +**Then**: The generated refinement prompt includes only the configured comment window + +### Requirement: Refine supports first/last issue windowing + +The system SHALL support optional issue window controls for `specfact backlog refine` so users can process the first or last subset of currently filtered backlog items. + +**Rationale**: Teams often need a deterministic window over a larger result set (for example oldest/newest slice) without re-running broad filters manually. + +#### Scenario: First-issues limit on refine + +**Given**: The user runs `specfact backlog refine --first-issues 10` + +**When**: More than ten items match after filters/refinement eligibility rules + +**Then**: The command sorts items by issue/work-item number ascending and processes only the first ten (lowest IDs / oldest) + +#### Scenario: Last-issues limit on refine + +**Given**: The user runs `specfact backlog refine --last-issues 10` + +**When**: More than ten items match after filters/refinement eligibility rules + +**Then**: The command sorts items by issue/work-item number ascending and processes only the last ten (highest IDs / newest) + +#### Scenario: First/last issues flags are mutually exclusive + +**Given**: The user runs `specfact backlog refine --first-issues 5 --last-issues 5` + +**When**: The command validates options + +**Then**: The command exits with a clear validation error + +### Requirement: ADO comments are fetched from dedicated comments API + +For Azure DevOps, the system SHALL fetch work item comments via the dedicated comments endpoint and handle comment pagination to collect complete history. + +**Rationale**: ADO work item retrieval and comments retrieval are separate API resources and versions. + +#### Scenario: ADO comment pagination retrieves complete history + +**Given**: An ADO work item has comments spanning multiple comment pages + +**When**: The adapter fetches comments for refine or daily context + +**Then**: The adapter calls the ADO comments API and follows continuation tokens until complete + +**And**: All comments are returned in stable order for downstream rendering/export diff --git a/openspec/changes/backlog-scrum-01-standup-exceptions-first/specs/daily-standup/spec.md b/openspec/changes/backlog-scrum-01-standup-exceptions-first/specs/daily-standup/spec.md index cc230f59..0b5850fc 100644 --- a/openspec/changes/backlog-scrum-01-standup-exceptions-first/specs/daily-standup/spec.md +++ b/openspec/changes/backlog-scrum-01-standup-exceptions-first/specs/daily-standup/spec.md @@ -57,3 +57,192 @@ The system SHALL integrate with patch mode (patch-mode-preview-apply) to propose **Acceptance Criteria**: - When patch mode is available and `--patch` is set, standup can propose patch; no silent writes. + +### Requirement: Interactive standup comment display is scoped + +The system SHALL avoid dumping full comment history in interactive standup detail views. When comments exist, it SHALL show only the latest comment by default and provide a clear hint that full comments are available via export options. + +**Rationale**: Interactive review must stay readable while still giving users access to complete context in export workflows. + +#### Scenario: Interactive view shows latest comment and export hint + +**Given**: The selected backlog item has multiple comments (e.g., from ADO work item discussion) + +**When**: The user runs `specfact backlog daily --interactive` and opens the item detail view + +**Then**: The detail view shows only the latest comment text + +**And**: The detail view shows how many additional older comments exist + +**And**: The detail view includes a hint to use export-to-file options to retrieve all comments + +**Acceptance Criteria**: + +- Interactive detail output does not render all comments inline when more than one exists. +- The output explicitly guides users to export for full comment context. + +#### Scenario: Interactive comment-window override is honored + +**Given**: The user runs `specfact backlog daily --interactive --first-comments N` or `--last-comments N` + +**When**: The selected backlog item has more comments than N + +**Then**: The interactive detail view renders the selected window of N comments (instead of latest-only default) + +**And**: The detail view clearly indicates how many additional comments were omitted by the window + +#### Scenario: Interactive comments use scoped panel-style blocks + +**Given**: The user runs `specfact backlog daily --interactive` + +**When**: Comment context is rendered for a selected item + +**Then**: Comments are rendered in clear scoped blocks (panel-style), separate from the story detail body, for readability + +### Requirement: Comment window controls for standup exports and summarize prompts + +The system SHALL support optional comment-window controls `--first-comments N` and `--last-comments N` for `specfact backlog daily` exports/prompts that include comments. By default, no comment truncation is applied. + +**Rationale**: Teams need complete context by default, but must be able to constrain prompt size when needed. + +#### Scenario: Export/summarize uses all comments by default + +**Given**: The user runs `specfact backlog daily --comments` with `--copilot-export`, `--summarize`, or `--summarize-to` + +**When**: No `--first-comments` or `--last-comments` option is provided + +**Then**: The command includes all available comments per item (no truncation) + +#### Scenario: First-comments limit is applied + +**Given**: The user runs `specfact backlog daily --comments --first-comments 3 --copilot-export ` + +**When**: An item has more than three comments + +**Then**: The output includes only the first three comments for that item + +#### Scenario: Last-comments limit is applied + +**Given**: The user runs `specfact backlog daily --comments --last-comments 2 --summarize` + +**When**: An item has more than two comments + +**Then**: The output includes only the last two comments for that item + +**Acceptance Criteria**: + +- Default behavior is full comment inclusion. +- First/last limits are optional and deterministic. +- If both are provided, command fails with a clear validation error. + +### Requirement: Assignee visibility and GitHub `me` filter semantics + +The system SHALL show assignment context directly in `specfact backlog daily` table output and SHALL handle GitHub assignee filter value `me` (or `@me`) as provider-relative current-user semantics rather than a literal username string. + +**Rationale**: Daily standup output must make ownership explicit, and GitHub users commonly use `me` as shorthand in issue filtering. + +#### Scenario: Daily table includes assignee column + +**Given**: The user runs `specfact backlog daily` and at least one item has assignees + +**When**: The standup table is rendered + +**Then**: The table includes an `Assignee` column + +**And**: Each row shows comma-separated assignees or `—` when unassigned + +#### Scenario: GitHub `--assignee me` uses provider semantics + +**Given**: The adapter is GitHub and the user passes `--assignee me` (or `--assignee @me`) + +**When**: The command fetches and post-filters backlog items + +**Then**: The GitHub query uses provider-relative current-user qualifier semantics + +**And**: Local post-fetch filtering does not treat `me` as a literal assignee login + +**Acceptance Criteria**: + +- `backlog daily` output includes an assignee column. +- GitHub `me`/`@me` filtering is deterministic and does not get incorrectly narrowed by literal local matching. + +### Requirement: Issue window controls for backlog daily + +The system SHALL support optional issue-window controls `--first-issues N` and `--last-issues N` on `specfact backlog daily` with deterministic numeric issue ID ordering semantics matching `specfact backlog refine`. + +**Rationale**: Users need harmonized backlog command ergonomics to focus on oldest/newest slices without changing workflows between subcommands. + +#### Scenario: Daily command supports first-issues window + +**Given**: More than N items match `specfact backlog daily` filters + +**When**: The user runs `specfact backlog daily ... --first-issues N` + +**Then**: Only the lowest N numeric issue/work-item IDs are included in output + +#### Scenario: Daily command supports last-issues window + +**Given**: More than N items match `specfact backlog daily` filters + +**When**: The user runs `specfact backlog daily ... --last-issues N` + +**Then**: Only the highest N numeric issue/work-item IDs are included in output + +#### Scenario: Daily command rejects conflicting issue windows + +**Given**: The user passes both `--first-issues` and `--last-issues` + +**When**: The command validates CLI inputs + +**Then**: The command exits with a clear validation error + +**Acceptance Criteria**: + +- `backlog daily` has `--first-issues` and `--last-issues` options. +- Only one issue window option can be used at once. +- Ordering semantics align with refine (`first`=lowest numeric IDs, `last`=highest numeric IDs). +- Issue windowing is applied over the full filtered candidate set (not a pre-truncated default-limit subset). + +### Requirement: Global filter parity across backlog commands + +The system SHALL provide consistent global backlog filtering flags across `specfact backlog daily` and `specfact backlog refine` for shared backlog-item selection semantics. + +#### Scenario: Daily supports shared global filter flags + +**Given**: The user runs `specfact backlog daily` + +**When**: They use global filters available on refine + +**Then**: Daily accepts and applies `--search`, `--release`, and `--id` consistently with refine semantics + +**Acceptance Criteria**: + +- `backlog daily` accepts `--search`, `--release`, and `--id`. +- `--search` and `--release` are applied in fetch/filter flow. +- `--id` narrows to the exact backlog item ID after other filters; when not found, command exits with a clear error. + +### Requirement: Interactive standup can post comment for selected issue + +The system SHALL allow posting standup comments directly from the interactive review flow for the currently selected item. + +**Rationale**: Teams review one story at a time during daily standup; posting from the selected item avoids context switching and reduces mistakes. + +#### Scenario: Post standup comment from selected story in interactive mode + +**Given**: The user runs `specfact backlog daily --interactive` + +**And**: The adapter supports `add_comment` + +**When**: The user opens a story and chooses the interactive post action + +**Then**: The CLI prompts for standup fields (yesterday/today/blockers) + +**And**: The CLI posts the comment to that selected story (not implicitly to another item) + +**And**: The CLI shows a clear success or failure message + +**Acceptance Criteria**: + +- Interactive navigation includes a post action for the selected story. +- Empty post input is rejected with a clear message. +- Posting uses existing standup comment format and adapter capability checks. diff --git a/openspec/changes/backlog-scrum-01-standup-exceptions-first/tasks.md b/openspec/changes/backlog-scrum-01-standup-exceptions-first/tasks.md index 2fbdd672..0b35ad9e 100644 --- a/openspec/changes/backlog-scrum-01-standup-exceptions-first/tasks.md +++ b/openspec/changes/backlog-scrum-01-standup-exceptions-first/tasks.md @@ -13,27 +13,69 @@ Per `openspec/config.yaml`, **tests before code** apply to any task that adds or ## 1. Create git branch from dev (linked to issue #175) - [ ] 1.1 Ensure we're on dev and up to date: `git checkout dev && git pull origin dev` -- [ ] 1.2 Create branch linked to #175: `gh issue develop 175 --repo nold-ai/specfact-cli --name feature/backlog-scrum-01-standup-exceptions-first --checkout` (or `git checkout -b feature/backlog-scrum-01-standup-exceptions-first` if no gh) -- [ ] 1.3 Verify branch: `git branch --show-current` +- [x] 1.2 Create branch linked to #175: `gh issue develop 175 --repo nold-ai/specfact-cli --name feature/backlog-scrum-01-standup-exceptions-first --checkout` (or `git checkout -b feature/backlog-scrum-01-standup-exceptions-first` if no gh) +- [x] 1.3 Verify branch: `git branch --show-current` ## 2. Tests first (exceptions-first order, --mode, patch hook) -- [ ] 2.1 Write tests from spec: exceptions-first section order, --mode scrum|kanban|safe, patch hook when available. -- [ ] 2.2 Run tests: `hatch run smart-test-unit`; **expect failure**. +- [x] 2.1 Write tests from spec: exceptions-first section order, --mode scrum|kanban|safe, patch hook when available. +- [x] 2.2 Run tests: `hatch run smart-test-unit`; **expect failure**. + +## 2b. Tests first (comment context, daily + refine) + +- [x] 2b.1 Add unit tests for ADO comments retrieval via dedicated comments API with pagination (continuation token). +- [x] 2b.2 Add unit tests for `backlog daily` comment rendering controls: default full comment inclusion for export/summarize, `--first-comments N`, `--last-comments N`, and interactive last-comment-only view with hint. +- [x] 2b.3 Add unit tests for `backlog refine --export-to-tmp` to include comment context by default (full) and respect first/last comment limits. +- [x] 2b.4 Run targeted tests and expect failures before implementation. +- [x] 2b.5 Add unit tests for `backlog refine --preview` comment context: default last 2 comments, optional `--first-comments` / `--last-comments`. +- [x] 2b.6 Verify `backlog refine --export-to-tmp` always includes full comments even when preview comment-window options are set. +- [x] 2b.7 Add unit tests for refine preview comment-fetch progress text (`Fetching issue n/m ...`) and block-style comment rendering helpers. +- [x] 2b.8 Add unit tests for refine issue windowing: `--first-issues`, `--last-issues`, and mutual exclusivity validation. +- [x] 2b.9 Add unit tests ensuring issue windowing is based on numeric issue/work-item ID ordering (ascending). +- [x] 2b.10 Add unit tests for refine preview comments section when no comments are returned. +- [x] 2b.11 Add unit tests for refinement prompt generation to include comment context in `--write` workflows. +- [x] 2b.12 Add unit tests ensuring refine export includes a copilot instruction block before item sections. +- [x] 2b.13 Add unit tests ensuring refine export includes interactive-equivalent refinement rules and per-item template guidance. +- [x] 2b.14 Add unit tests for daily standup assignee column row data and GitHub `--assignee me` post-filter semantics. +- [x] 2b.15 Add unit tests for `backlog daily` issue-window CLI flags (`--first-issues`, `--last-issues`) and validation parity with refine. +- [x] 2b.16 Add unit tests for daily issue-window semantics over full filtered set (no default pre-limit truncation). +- [x] 2b.17 Add unit tests for interactive daily comment-window override (`--first-comments`/`--last-comments`) behavior. +- [x] 2b.18 Add unit tests for panel-style interactive comment rendering in daily mode. +- [x] 2b.19 Add unit tests for daily global filter parity flags (`--search`, `--release`, `--id`) and ID filter behavior. +- [x] 2b.20 Add unit tests for interactive daily navigation/post helpers to support posting comment on the selected story. ## 3. Implement exceptions-first and mode -- [ ] 3.1 Implement default section order: blockers → policy failures → aging → normal (when data available). -- [ ] 3.2 Add `--mode scrum|kanban|safe` to `specfact backlog daily`; adjust defaults per mode. -- [ ] 3.3 Integrate patch hook when patch-mode-preview-apply available and `--patch` set. -- [ ] 3.4 Run tests; **expect pass**. +- [x] 3.1 Implement default section order: blockers → policy failures → aging → normal (when data available). +- [x] 3.2 Add `--mode scrum|kanban|safe` to `specfact backlog daily`; adjust defaults per mode. +- [x] 3.3 Integrate patch hook when patch-mode-preview-apply available and `--patch` set. +- [x] 3.4 Run tests; **expect pass**. +- [x] 3.5 Implement refine preview comment rendering (default last 2, optional first/last windows) for adapters supporting `get_comments`. +- [x] 3.6 Ensure refine export ignores preview comment-window options and always exports full comments. +- [x] 3.7 Add preview-time progress spinner/action text while collecting comments (`Fetching issue n/m ...`). +- [x] 3.8 Render preview comments in block-style panels (clear start/end scope per comment). +- [x] 3.9 Implement refine issue-window controls (`--first-issues`, `--last-issues`) and validation. +- [x] 3.10 Fix refine issue-window ordering to use numeric ID semantics (lower first, higher last). +- [x] 3.11 Add explicit preview hint/panel for items with no comments. +- [x] 3.12 Include comments in write-mode refinement prompts (full by default, optional first/last windows). +- [x] 3.13 Add copilot instruction block at top of refine export files. +- [x] 3.14 Add interactive-equivalent instructions and template guidance to refine export item blocks. +- [x] 3.15 Add assignee column to daily standup table output (including pending/unassigned section). +- [x] 3.16 Implement GitHub `--assignee me`/`@me` handling without literal local post-filter mismatch. +- [x] 3.17 Add `--first-issues` / `--last-issues` support to `backlog daily` with numeric ID ordering and mutual exclusivity validation. +- [x] 3.18 Ensure daily issue-windowing is evaluated before default limit truncation (refine parity). +- [x] 3.19 Make interactive daily honor explicit comment-window flags while keeping latest-only as default. +- [x] 3.20 Render daily interactive comments in refine-like scoped panels outside the story body block. +- [x] 3.21 Add daily support for shared global filters `--search`, `--release`, and `--id` (refine parity). +- [x] 3.22 Add interactive standup post action to publish yesterday/today/blockers comment to the currently selected story. ## 4. Quality gates and documentation -- [ ] 4.1 Run format and type-check: `hatch run format`, `hatch run type-check`. -- [ ] 4.2 Run contract test: `hatch run contract-test`. -- [ ] 4.3 Update docs: agile-scrum-workflows.md, devops-adapter-integration.md (exceptions-first, --mode). -- [ ] 4.4 Add CHANGELOG entry; sync version. +- [x] 4.1 Run format and type-check: `hatch run format`, `hatch run type-check`. +- [x] 4.2 Run contract test: `hatch run contract-test`. +- [x] 4.3 Update docs: agile-scrum-workflows.md, backlog-refinement.md, devops-adapter-integration.md (comment context behavior, first/last comment controls, export guidance). +- [x] 4.4 Update slash prompt templates: `resources/prompts/specfact.backlog-daily.md` and `resources/prompts/specfact.backlog-refine.md` for comment-context guidance. +- [ ] 4.5 Add CHANGELOG entry; sync version. ## 5. Create Pull Request to dev diff --git a/openspec/config.yaml b/openspec/config.yaml index db12cae3..573b7a14 100644 --- a/openspec/config.yaml +++ b/openspec/config.yaml @@ -56,6 +56,8 @@ context: | run tests and expect failure. - (3) Code last—implement until tests pass and behavior satisfies the spec. Code must batch (satisfy) both (a) spec scenarios and (b) tests. + - (4) Evidence required—record failing-before and passing-after test runs in + `openspec/changes//TDD_EVIDENCE.md` for every behavior change. - If the pattern does not work in practice, adjust the process until it does. # Per-artifact rules (only injected into matching artifacts) @@ -103,8 +105,10 @@ rules: - (2) Write/add spec deltas if not already done. - (3) Write tests from spec scenarios—translate each Given/When/Then scenario into test cases; run tests and expect failure (no implementation yet). + - (3a) Capture failing-test evidence in `openspec/changes//TDD_EVIDENCE.md`. - (4) Implement code until tests pass and behavior satisfies the spec; code must batch (satisfy) both (a) spec scenarios and (b) tests. + - (4a) Capture passing-test evidence in `openspec/changes//TDD_EVIDENCE.md`. - (5) Quality gates (format, lint, type-check). - (6) Documentation research and review (see below). - (7) PR creation (last). @@ -123,6 +127,10 @@ rules: - >- Test tasks MUST come before implementation tasks: write tests derived from specs first, then implement. Do not implement before tests exist for the changed behavior. + - >- + TDD evidence is mandatory: each behavior change must include + openspec/changes//TDD_EVIDENCE.md with failing-before and passing-after test commands, + timestamps, and short result summaries. - Include quality gate tasks: format, lint, type-check, test coverage - Reference existing test patterns in tests/unit/, tests/integration/, tests/e2e/ - |- diff --git a/pyproject.toml b/pyproject.toml index 51f0207f..9ba388e3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "specfact-cli" -version = "0.30.1" +version = "0.30.2" description = "The swiss knife CLI for agile DevOps teams. Keep backlog, specs, tests, and code in sync with validation and contract enforcement for new projects and long-lived codebases." readme = "README.md" requires-python = ">=3.11" diff --git a/resources/prompts/specfact.backlog-daily.md b/resources/prompts/specfact.backlog-daily.md index 5fb26a6e..a56338e6 100644 --- a/resources/prompts/specfact.backlog-daily.md +++ b/resources/prompts/specfact.backlog-daily.md @@ -37,8 +37,12 @@ When run from a **clone**, org/repo or org/project are inferred from `git remote - `--state STATE` - Filter by state (e.g. open, Active) - `--assignee USERNAME` or `--assignee me` - Filter by assignee +- `--search QUERY` - Provider-specific search query +- `--release RELEASE` - Filter by release identifier +- `--id ISSUE_ID` - Filter to one exact backlog item ID - `--sprint SPRINT` / `--iteration PATH` - Filter by sprint/iteration (e.g. `current`) - `--limit N` - Max items (default 20) +- `--first-issues N` / `--last-issues N` - Optional issue window (oldest/newest by numeric ID, mutually exclusive) - `--blockers-first` - Sort items with blockers first - `--show-unassigned` / `--unassigned-only` - Include or show only unassigned items @@ -48,8 +52,11 @@ When run from a **clone**, org/repo or org/project are inferred from `git remote - `--copilot-export PATH` - Write summarized progress per story to a file for Copilot slash-command use - `--summarize` - Output a prompt (instruction + filter context + standup data) to **stdout** for Copilot or slash command to generate a standup summary - `--summarize-to PATH` - Write the same summarize prompt to a **file** +- `--comments` / `--annotations` - Include descriptions and comments in `--copilot-export` and summarize output +- `--first-comments N` / `--last-comments N` - Optional comment window for export/summarize outputs (`--comments`); default includes all comments - `--suggest-next` - In interactive mode, show suggested next item by value score - `--post` with `--yesterday`, `--today`, `--blockers` - Post a standup comment to the first item's issue (when adapter supports comments) +- Interactive navigation action `Post standup update` - Post yesterday/today/blockers to the currently selected story during `--interactive` walkthrough ## Workflow @@ -70,10 +77,12 @@ Or use the slash command with arguments: `/specfact.backlog-daily --adapter ado When the user runs **`--interactive`** (or the slash command drives an interactive flow): 1. **For each story** (one at a time): - - **Present** the item: ID, title, status, assignees, last updated, description, acceptance criteria, standup fields (yesterday/today/blockers), and **existing comments** annotated to that issue (when the adapter supports fetching comments). + - **Present** the item: ID, title, status, assignees, last updated, description, acceptance criteria, standup fields (yesterday/today/blockers), and the **latest existing comment** (when the adapter supports fetching comments). + - **Interactive comment scope**: If older comments exist, explicitly mention the count of hidden comments and guide users to export options for full context. - **Highlight current focus**: What is the team member working on? What is the next intended step? - **Surface issues or open questions**: Blockers, ambiguities, dependencies, or decisions needed. - **Allow discussion notes**: If the team agrees, suggest or add a **comment** on the issue (e.g. "Standup YYYY-MM-DD: …" or "Discussion: …") so the discussion is captured as an annotation. Only add comments when the user explicitly approves (e.g. "add that as a comment"). + - If in CLI interactive navigation, use **Post standup update** to write the note to the selected story directly. - **Move to next** only when the team is done with this story (e.g. "next", "done"). 2. **Rules**: @@ -95,8 +104,9 @@ When the user has run `specfact backlog daily ... --summarize` or `--summarize-t ## Comments on Issues -- **Interactive detail view** shows **existing comments** on each issue (GitHub issue comments, ADO work item discussion) when the adapter supports it. Use them to understand prior discussion and avoid repeating questions. -- **Adding comments**: When the team agrees to record a discussion note or standup update, add it as a comment on the issue (via `--post` for standup lines, or by guiding the user to run the CLI/post manually). Do not invent comments; only suggest or add when the user approves. +- **Interactive detail view** shows only the **latest comment** plus a hint when additional comments exist, to keep standup readable. +- **Full comment context**: use `--copilot-export --comments` or `--summarize --comments` (optional `--first-comments N` / `--last-comments N`) to include full or scoped comment history. +- **Adding comments**: When the team agrees to record a discussion note or standup update, add it as a comment on the issue (via `--post` for first-item standup lines or interactive **Post standup update** for selected stories). Do not invent comments; only suggest or add when the user approves. ## CLI Enforcement diff --git a/resources/prompts/specfact.backlog-refine.md b/resources/prompts/specfact.backlog-refine.md index 50dd7636..3c800348 100644 --- a/resources/prompts/specfact.backlog-refine.md +++ b/resources/prompts/specfact.backlog-refine.md @@ -69,6 +69,7 @@ Refine backlog items from DevOps tools (GitHub Issues, Azure DevOps, etc.) into - Ambiguous name-only matches will prompt for explicit iteration path - `--release RELEASE` - Filter by release identifier (case-insensitive) - `--limit N` - Maximum number of items to process in this refinement session (caps batch size) +- `--first-issues N` / `--last-issues N` - Process only the first or last N items after filters/refinement checks (mutually exclusive; sorted by numeric issue/work-item ID, lower=older, higher=newer) - `--ignore-refined` / `--no-ignore-refined` - When set (default), exclude already-refined items so `--limit` applies to items that need refinement. Use `--no-ignore-refined` to process the first N items in order. - `--id ISSUE_ID` - Refine only this backlog item (issue or work item ID). Other items are ignored. - `--persona PERSONA` - Filter templates by persona (product-owner, architect, developer) @@ -91,13 +92,23 @@ Refine backlog items from DevOps tools (GitHub Issues, Azure DevOps, etc.) into - `--export-to-tmp` - Export backlog items to temporary file for copilot processing (default: `/tmp/specfact-backlog-refine-.md`) - `--import-from-tmp` - Import refined content from temporary file after copilot processing (default: `/tmp/specfact-backlog-refine--refined.md`) - `--tmp-file PATH` - Custom temporary file path (overrides default) +- `--first-comments N` / `--last-comments N` - Optional comment window for preview and write-mode prompt context (default preview shows last 2; write prompts include full comments by default) **Export/Import Workflow**: 1. Export items: `specfact backlog refine --adapter github --export-to-tmp --repo-owner OWNER --repo-name NAME` -2. Process with copilot: Open exported file, use copilot to refine items, save as `-refined.md` +2. Process with copilot: Open exported file and follow the embedded `## Copilot Instructions` and per-item template guidance (`Target Template`, `Required Sections`, `Optional Sections`). Save as `-refined.md` 3. Import refined: `specfact backlog refine --adapter github --import-from-tmp --repo-owner OWNER --repo-name NAME --write` +When refining from an exported file, treat the embedded instructions in that file as the source of truth for required structure and formatting. + +**Comment context in export**: + +- Export includes item comments when adapter supports comment retrieval (GitHub + ADO). +- Export always includes full comment history (no truncation). +- Use `--first-comments N` or `--last-comments N` only to adjust preview output density. +- For refined import readiness, the `-refined.md` artifact should omit the instruction header and keep only item sections. + ### Definition of Ready (DoR) - `--check-dor` - Check Definition of Ready (DoR) rules before refinement (loads from `.specfact/dor.yaml`) diff --git a/setup.py b/setup.py index 2c5534f3..f4c60580 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ if __name__ == "__main__": _setup = setup( name="specfact-cli", - version="0.30.1", + version="0.30.2", description=( "The swiss knife CLI for agile DevOps teams. Keep backlog, specs, tests, and code in sync with " "validation and contract enforcement for new projects and long-lived codebases." diff --git a/src/specfact_cli/__init__.py b/src/specfact_cli/__init__.py index 171b08cf..968bb1b1 100644 --- a/src/specfact_cli/__init__.py +++ b/src/specfact_cli/__init__.py @@ -8,6 +8,6 @@ - Supporting agile ceremonies and team workflows """ -__version__ = "0.30.1" +__version__ = "0.30.2" __all__ = ["__version__"] diff --git a/src/specfact_cli/adapters/ado.py b/src/specfact_cli/adapters/ado.py index 6a9c2bcf..6f45cbc7 100644 --- a/src/specfact_cli/adapters/ado.py +++ b/src/specfact_cli/adapters/ado.py @@ -2356,18 +2356,39 @@ def _get_work_item_comments(self, org: str, project: str, work_item_id: int) -> if not self.api_token: return [] - url = f"{self.base_url}/{org}/{project}/_apis/wit/workitems/{work_item_id}/comments?api-version=7.1" + url = f"{self.base_url}/{org}/{project}/_apis/wit/workItems/{work_item_id}/comments" headers = { "Accept": "application/json", **self._auth_headers(), } try: - response = requests.get(url, headers=headers, timeout=30) - response.raise_for_status() - # ADO API returns comments in a 'comments' array within the response - response_data = response.json() - return response_data.get("comments", []) + comments: list[dict[str, Any]] = [] + continuation_token: str | None = None + seen_tokens: set[str] = set() + + while True: + params: dict[str, Any] = {"api-version": "7.1-preview.4", "$top": 200, "order": "asc"} + if continuation_token: + params["continuationToken"] = continuation_token + + response = requests.get(url, headers=headers, params=params, timeout=30) + response.raise_for_status() + response_data = response.json() + + raw_comments = response_data.get("comments", []) + if isinstance(raw_comments, list): + comments.extend([c for c in raw_comments if isinstance(c, dict)]) + + next_token = response.headers.get("x-ms-continuationtoken") or response_data.get("continuationToken") + if not next_token or not isinstance(next_token, str): + break + if next_token in seen_tokens: + break + seen_tokens.add(next_token) + continuation_token = next_token + + return comments except requests.RequestException: # Return empty list on error - comments are optional return [] @@ -3145,6 +3166,35 @@ def add_comment(self, item: BacklogItem, comment: str) -> bool: except Exception: return False + @beartype + def get_comments(self, item: BacklogItem) -> list[str]: + """ + Fetch comments for an Azure DevOps work item. + + Args: + item: BacklogItem to fetch comments for + + Returns: + List of comment body strings, or empty list on error + """ + if not self.org or not self.project: + return [] + + if not item.id.isdigit(): + return [] + + raw = self._get_work_item_comments(self.org, self.project, int(item.id)) + comment_texts: list[str] = [] + for comment in raw: + if not isinstance(comment, dict): + continue + text = comment.get("text") or comment.get("body") + if isinstance(text, str): + stripped = text.strip() + if stripped: + comment_texts.append(stripped) + return comment_texts + @beartype @require(lambda item: isinstance(item, BacklogItem), "Item must be BacklogItem") @require( diff --git a/src/specfact_cli/adapters/github.py b/src/specfact_cli/adapters/github.py index 42eec208..152ad4ab 100644 --- a/src/specfact_cli/adapters/github.py +++ b/src/specfact_cli/adapters/github.py @@ -2526,7 +2526,11 @@ def fetch_backlog_items(self, filters: BacklogFilters) -> list[BacklogItem]: if filters.assignee: # Strip leading @ if present for GitHub search assignee_value = filters.assignee.lstrip("@") - query_parts.append(f"assignee:{assignee_value}") + normalized_assignee_value = BacklogFilters.normalize_filter_value(assignee_value) + if normalized_assignee_value == "me": + query_parts.append("assignee:@me") + else: + query_parts.append(f"assignee:{assignee_value}") if filters.labels: for label in filters.labels: @@ -2585,24 +2589,25 @@ def fetch_backlog_items(self, filters: BacklogFilters) -> list[BacklogItem]: # Normalize assignee filter (strip @, lowercase) assignee_filter = filters.assignee.lstrip("@") normalized_assignee = BacklogFilters.normalize_filter_value(assignee_filter) - - filtered_items = [ - item - for item in filtered_items - if any( - # Match against login (case-insensitive) - BacklogFilters.normalize_filter_value(assignee) == normalized_assignee - # Or match against display name if available (case-insensitive) - or ( - hasattr(item, "provider_fields") - and isinstance(item.provider_fields, dict) - and item.provider_fields.get("assignee_login") - and BacklogFilters.normalize_filter_value(item.provider_fields["assignee_login"]) - == normalized_assignee + # `me` is provider-relative identity and should rely on GitHub query semantics. + if normalized_assignee != "me": + filtered_items = [ + item + for item in filtered_items + if any( + # Match against login (case-insensitive) + BacklogFilters.normalize_filter_value(assignee) == normalized_assignee + # Or match against display name if available (case-insensitive) + or ( + hasattr(item, "provider_fields") + and isinstance(item.provider_fields, dict) + and item.provider_fields.get("assignee_login") + and BacklogFilters.normalize_filter_value(item.provider_fields["assignee_login"]) + == normalized_assignee + ) + for assignee in item.assignees ) - for assignee in item.assignees - ) - ] + ] if filters.iteration: filtered_items = [item for item in filtered_items if item.iteration and item.iteration == filters.iteration] @@ -2633,17 +2638,6 @@ def fetch_backlog_items(self, filters: BacklogFilters) -> list[BacklogItem]: return filtered_items - @beartype - @require(lambda item: isinstance(item, BacklogItem), "Item must be BacklogItem") - @require( - lambda item, update_fields: update_fields is None or isinstance(update_fields, list), - "Update fields must be None or list", - ) - @ensure(lambda result: isinstance(result, BacklogItem), "Must return BacklogItem") - @ensure( - lambda result, item: result.id == item.id and result.provider == item.provider, - "Updated item must preserve id and provider", - ) @beartype def supports_add_comment(self) -> bool: """Whether this adapter can add comments (requires token and repo).""" @@ -2711,6 +2705,17 @@ def get_comments(self, item: BacklogItem) -> list[str]: raw = self._get_issue_comments(self.repo_owner, self.repo_name, issue_number) return [str(c.get("body", "")).strip() for c in raw if isinstance(c, dict)] + @beartype + @require(lambda item: isinstance(item, BacklogItem), "Item must be BacklogItem") + @require( + lambda item, update_fields: update_fields is None or isinstance(update_fields, list), + "Update fields must be None or list", + ) + @ensure(lambda result: isinstance(result, BacklogItem), "Must return BacklogItem") + @ensure( + lambda result, item: result.id == item.id and result.provider == item.provider, + "Updated item must preserve id and provider", + ) def update_backlog_item(self, item: BacklogItem, update_fields: list[str] | None = None) -> BacklogItem: """ Update a GitHub issue. diff --git a/src/specfact_cli/backlog/ai_refiner.py b/src/specfact_cli/backlog/ai_refiner.py index f8e245bc..cc9acfb1 100644 --- a/src/specfact_cli/backlog/ai_refiner.py +++ b/src/specfact_cli/backlog/ai_refiner.py @@ -73,8 +73,14 @@ class BacklogAIRefiner: @beartype @require(lambda self, item: isinstance(item, BacklogItem), "Item must be BacklogItem") @require(lambda self, template: isinstance(template, BacklogTemplate), "Template must be BacklogTemplate") + @require( + lambda self, comments=None: comments is None or isinstance(comments, list), + "Comments must be a list of strings or None", + ) @ensure(lambda result: isinstance(result, str) and len(result) > 0, "Must return non-empty prompt string") - def generate_refinement_prompt(self, item: BacklogItem, template: BacklogTemplate) -> str: + def generate_refinement_prompt( + self, item: BacklogItem, template: BacklogTemplate, comments: list[str] | None = None + ) -> str: """ Generate prompt for IDE AI copilot to refine backlog item. @@ -122,6 +128,16 @@ def generate_refinement_prompt(self, item: BacklogItem, template: BacklogTemplat if item.work_item_type: metrics_info += f"\nWork Item Type: {item.work_item_type}" + comment_lines: list[str] = [] + if comments: + comment_lines.append("Comments (latest discussion context):") + for index, comment in enumerate(comments, 1): + comment_lines.append(f"{index}. {comment}") + else: + comment_lines.append("Comments (latest discussion context):") + comment_lines.append("- No comments found") + comments_info = "\n".join(comment_lines) + prompt = f"""Transform the following backlog item into the {template.name} template format. Original Backlog Item: @@ -132,6 +148,8 @@ def generate_refinement_prompt(self, item: BacklogItem, template: BacklogTemplat Body: {item.body_markdown} +{comments_info} + Target Template: {template.name} Description: {template.description} diff --git a/src/specfact_cli/modules/backlog/src/commands.py b/src/specfact_cli/modules/backlog/src/commands.py index ef4b4944..4f237526 100644 --- a/src/specfact_cli/modules/backlog/src/commands.py +++ b/src/specfact_cli/modules/backlog/src/commands.py @@ -19,6 +19,7 @@ import subprocess import sys import tempfile +from collections.abc import Callable from datetime import date, datetime from pathlib import Path from typing import Any @@ -45,7 +46,7 @@ from specfact_cli.models.project import BundleManifest, ProjectBundle from specfact_cli.models.validation import ValidationReport from specfact_cli.runtime import debug_log_operation, is_debug_mode -from specfact_cli.templates.registry import TemplateRegistry +from specfact_cli.templates.registry import BacklogTemplate, TemplateRegistry app = typer.Typer( @@ -299,6 +300,23 @@ def _resolve_standup_options( return (state, limit, assignee) +@beartype +def _resolve_post_fetch_assignee_filter(adapter: str, assignee: str | None) -> str | None: + """ + Resolve assignee value for local post-fetch filtering. + + For GitHub, `me`/`@me` should be handled by adapter-side query semantics and + not re-filtered locally as a literal username. + """ + if not assignee: + return assignee + if adapter.lower() == "github": + normalized = BacklogFilters.normalize_filter_value(assignee.lstrip("@")) + if normalized == "me": + return None + return assignee + + @beartype def _split_assigned_unassigned(items: list[BacklogItem]) -> tuple[list[BacklogItem], list[BacklogItem]]: """Split items into assigned and unassigned (assignees empty or None).""" @@ -343,6 +361,7 @@ def _build_standup_rows( "id": item.id, "title": item.title, "status": item.state, + "assignees": ", ".join(item.assignees) if item.assignees else "—", "last_updated": item.updated_at, "yesterday": yesterday or "", "today": today or "", @@ -400,7 +419,13 @@ def _compute_value_score(item: BacklogItem) -> float | None: @beartype -def _format_daily_item_detail(item: BacklogItem, comments: list[str]) -> str: +def _format_daily_item_detail( + item: BacklogItem, + comments: list[str], + *, + show_all_provided_comments: bool = False, + total_comments: int | None = None, +) -> str: """ Format a single backlog item for interactive detail view (refine-like). @@ -437,13 +462,305 @@ def _format_daily_item_detail(item: BacklogItem, comments: list[str]) -> str: parts.append(f"- **Business value:** {item.business_value}") if item.priority is not None: parts.append(f"- **Priority:** {item.priority}") - if comments: - parts.append("\n**Comments:**") - for c in comments: - parts.append(f"- {c}") + _ = (comments, show_all_provided_comments, total_comments) return "\n".join(parts) +@beartype +def _apply_comment_window( + comments: list[str], + *, + first_comments: int | None = None, + last_comments: int | None = None, +) -> list[str]: + """Apply optional first/last comment window; default returns all comments.""" + if first_comments is not None and last_comments is not None: + msg = "Use only one of --first-comments or --last-comments." + raise ValueError(msg) + if first_comments is not None: + return comments[: max(first_comments, 0)] + if last_comments is not None: + return comments[-last_comments:] if last_comments > 0 else [] + return comments + + +@beartype +def _apply_issue_window( + items: list[BacklogItem], + *, + first_issues: int | None = None, + last_issues: int | None = None, +) -> list[BacklogItem]: + """Apply optional first/last issue window to already-filtered items.""" + if first_issues is not None and last_issues is not None: + msg = "Use only one of --first-issues or --last-issues." + raise ValueError(msg) + if first_issues is not None or last_issues is not None: + + def _issue_number(item: BacklogItem) -> int: + if item.id.isdigit(): + return int(item.id) + issue_match = re.search(r"/issues/(\d+)", item.url or "") + if issue_match: + return int(issue_match.group(1)) + ado_match = re.search(r"/(?:_workitems/edit|workitems)/(\d+)", item.url or "", re.IGNORECASE) + if ado_match: + return int(ado_match.group(1)) + return sys.maxsize + + sorted_items = sorted(items, key=_issue_number) + if first_issues is not None: + return sorted_items[: max(first_issues, 0)] + if last_issues is not None: + return sorted_items[-last_issues:] if last_issues > 0 else [] + return items + + +@beartype +def _apply_issue_id_filter(items: list[BacklogItem], issue_id: str | None) -> list[BacklogItem]: + """Apply optional exact issue/work-item ID filter.""" + if issue_id is None: + return items + return [i for i in items if str(i.id) == str(issue_id)] + + +@beartype +def _resolve_refine_preview_comment_window( + *, + first_comments: int | None, + last_comments: int | None, +) -> tuple[int | None, int | None]: + """Resolve comment window for refine preview output.""" + if first_comments is not None: + return first_comments, None + if last_comments is not None: + return None, last_comments + # Keep preview concise by default while still showing current discussion. + return None, 2 + + +@beartype +def _resolve_refine_export_comment_window( + *, + first_comments: int | None, + last_comments: int | None, +) -> tuple[int | None, int | None]: + """Resolve comment window for refine export output (always full history).""" + _ = (first_comments, last_comments) + return None, None + + +@beartype +def _resolve_daily_issue_window( + items: list[BacklogItem], + *, + first_issues: int | None, + last_issues: int | None, +) -> list[BacklogItem]: + """Resolve and apply daily issue-window options with refine-aligned semantics.""" + if first_issues is not None and last_issues is not None: + msg = "Use only one of --first-issues or --last-issues" + raise ValueError(msg) + return _apply_issue_window(items, first_issues=first_issues, last_issues=last_issues) + + +@beartype +def _resolve_daily_fetch_limit( + effective_limit: int, + *, + first_issues: int | None, + last_issues: int | None, +) -> int | None: + """Resolve pre-fetch limit for daily command.""" + if first_issues is not None or last_issues is not None: + return None + return effective_limit + + +@beartype +def _resolve_daily_mode_state( + *, + mode: str, + cli_state: str | None, + effective_state: str | None, +) -> str | None: + """Resolve daily state behavior per mode while preserving explicit CLI state.""" + if cli_state is not None: + return effective_state + if mode == "kanban": + return None + return effective_state + + +@beartype +def _has_policy_failure(row: dict[str, Any]) -> bool: + """Return True when row indicates a policy failure signal.""" + policy_status = str(row.get("policy_status", "")).strip().lower() + if policy_status in {"failed", "fail", "violation", "violated"}: + return True + failures = row.get("policy_failures") + if isinstance(failures, list): + return len(failures) > 0 + return bool(failures) + + +@beartype +def _has_aging_or_stalled_signal(row: dict[str, Any]) -> bool: + """Return True when row indicates aging/stalled work.""" + stalled = row.get("stalled") + if isinstance(stalled, bool): + if stalled: + return True + elif str(stalled).strip().lower() in {"true", "yes", "1"}: + return True + days_stalled = row.get("days_stalled") + if isinstance(days_stalled, (int, float)): + return days_stalled > 0 + aging_days = row.get("aging_days") + if isinstance(aging_days, (int, float)): + return aging_days > 0 + return False + + +@beartype +def _exception_priority(row: dict[str, Any]) -> int: + """Return exception priority rank: blockers, policy, aging, normal.""" + if str(row.get("blockers", "")).strip(): + return 0 + if _has_policy_failure(row): + return 1 + if _has_aging_or_stalled_signal(row): + return 2 + return 3 + + +@beartype +def _split_exception_rows(rows: list[dict[str, Any]]) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]: + """Split standup rows into exceptions-first and normal rows with stable ordering.""" + exceptions = sorted((row for row in rows if _exception_priority(row) < 3), key=_exception_priority) + normal = [row for row in rows if _exception_priority(row) == 3] + return exceptions, normal + + +@beartype +def _build_daily_patch_proposal(items: list[BacklogItem], *, mode: str) -> str: + """Build a non-destructive patch proposal preview for standup notes.""" + lines: list[str] = [] + lines.append("# Patch Proposal") + lines.append("") + lines.append(f"- Mode: {mode}") + lines.append(f"- Items in scope: {len(items)}") + lines.append("- Action: Propose standup note/field updates only (no silent writes).") + lines.append("") + lines.append("## Candidate Items") + for item in items[:10]: + lines.append(f"- {item.id}: {item.title}") + if len(items) > 10: + lines.append(f"- ... and {len(items) - 10} more") + return "\n".join(lines) + + +@beartype +def _is_patch_mode_available() -> bool: + """Detect whether patch command group is available in current installation.""" + try: + result = subprocess.run( + ["specfact", "patch", "--help"], + check=False, + capture_output=True, + text=True, + timeout=5, + ) + return result.returncode == 0 + except (OSError, subprocess.TimeoutExpired): + return False + + +@beartype +def _build_comment_fetch_progress_description(index: int, total: int, item_id: str) -> str: + """Build progress text while fetching per-item comments.""" + return f"[cyan]Fetching issue {index}/{total} comments (ID: {item_id})...[/cyan]" + + +@beartype +def _build_refine_preview_comment_panels(comments: list[str]) -> list[Panel]: + """Render refine preview comments as scoped panel blocks.""" + total = len(comments) + panels: list[Panel] = [] + for index, comment in enumerate(comments, 1): + body = comment.strip() if comment.strip() else "[dim](empty comment)[/dim]" + panels.append(Panel(body, title=f"Comment {index}/{total}", border_style="cyan")) + return panels + + +@beartype +def _build_refine_preview_comment_empty_panel() -> Panel: + """Render explicit empty-state panel when no comments are found.""" + return Panel("[dim](no comments found)[/dim]", title="Comments", border_style="dim") + + +@beartype +def _build_daily_interactive_comment_panels( + comments: list[str], + *, + show_all_provided_comments: bool, + total_comments: int, +) -> list[Panel]: + """Render daily interactive comments with refine-like scoped panels.""" + if not comments: + return [_build_refine_preview_comment_empty_panel()] + + if show_all_provided_comments: + panels = _build_refine_preview_comment_panels(comments) + omitted_count = max(total_comments - len(comments), 0) + if omitted_count > 0: + panels.append( + Panel( + f"[dim]{omitted_count} additional comment(s) omitted by comment window.[/dim]\n" + "[dim]Hint: increase --first-comments/--last-comments or use export options for full history.[/dim]", + title="Comment Window", + border_style="dim", + ) + ) + return panels + + latest = comments[-1].strip() if comments[-1].strip() else "[dim](empty comment)[/dim]" + panels: list[Panel] = [Panel(latest, title="Latest Comment", border_style="cyan")] + hidden_count = max(total_comments - 1, 0) + if hidden_count > 0: + panels.append( + Panel( + f"[dim]{hidden_count} older comment(s) hidden in interactive view.[/dim]\n" + "[dim]Hint: use `specfact backlog refine --export-to-tmp` or " + "`specfact backlog daily --copilot-export --comments` for full history.[/dim]", + title="Comments Hint", + border_style="dim", + ) + ) + return panels + + +@beartype +def _build_daily_navigation_choices(*, can_post_comment: bool) -> list[str]: + """Build interactive daily navigation choices.""" + choices = ["Next story", "Previous story"] + if can_post_comment: + choices.append("Post standup update") + choices.extend(["Back to list", "Exit"]) + return choices + + +@beartype +def _build_interactive_post_body(yesterday: str | None, today: str | None, blockers: str | None) -> str | None: + """Build standup comment body from interactive inputs.""" + y = (yesterday or "").strip() + t = (today or "").strip() + b = (blockers or "").strip() + if not y and not t and not b: + return None + return _format_standup_comment(y, t, b) + + def _collect_comment_annotations( adapter: str, items: list[BacklogItem], @@ -454,6 +771,9 @@ def _collect_comment_annotations( ado_org: str | None, ado_project: str | None, ado_token: str | None, + first_comments: int | None = None, + last_comments: int | None = None, + progress_callback: Callable[[int, int, BacklogItem], None] | None = None, ) -> dict[str, list[str]]: """ Collect comment annotations for backlog items when the adapter supports get_comments(). @@ -478,10 +798,18 @@ def _collect_comment_annotations( get_comments_fn = getattr(adapter_instance, "get_comments", None) if not callable(get_comments_fn): return comments_by_item_id - for item in items: + total_items = len(items) + for index, item in enumerate(items, 1): + if progress_callback is not None: + progress_callback(index, total_items, item) with contextlib.suppress(Exception): raw = get_comments_fn(item) - comments_by_item_id[item.id] = list(raw) if isinstance(raw, list) else [] + comments = list(raw) if isinstance(raw, list) else [] + comments_by_item_id[item.id] = _apply_comment_window( + comments, + first_comments=first_comments, + last_comments=last_comments, + ) except Exception: return comments_by_item_id return comments_by_item_id @@ -628,6 +956,142 @@ def _build_summarize_prompt_content( return "\n".join(lines).strip() +@beartype +def _build_refine_export_content( + adapter: str, + items: list[BacklogItem], + comments_by_item_id: dict[str, list[str]] | None = None, + template_guidance_by_item_id: dict[str, dict[str, Any]] | None = None, +) -> str: + """Build markdown export content for `backlog refine --export-to-tmp`.""" + export_content = "# SpecFact Backlog Refinement Export\n\n" + export_content += f"**Export Date**: {datetime.now().isoformat()}\n" + export_content += f"**Adapter**: {adapter}\n" + export_content += f"**Items**: {len(items)}\n\n" + export_content += "## Copilot Instructions\n\n" + export_content += ( + "Use each `## Item N:` section below as refinement input. Preserve scope/intent and return improved markdown " + "per item.\n\n" + ) + export_content += ( + "For import readiness: the refined artifact (`--import-from-tmp`) must not include this instruction block; " + "it should contain only the `## Item N:` sections and refined fields.\n\n" + ) + export_content += "**Refinement Rules (same as interactive mode):**\n" + export_content += "1. Preserve all original requirements, scope, and technical details\n" + export_content += "2. Do NOT add new features or change the scope\n" + export_content += "3. Transform content to match the target template structure\n" + export_content += "4. If required information is missing, use a Markdown checkbox: `- [ ] describe what's needed`\n" + export_content += ( + "5. If information is conflicting or ambiguous, add a `[NOTES]` section at the end explaining ambiguity\n" + ) + export_content += "6. Use markdown headings for sections (`## Section Name`)\n" + export_content += "7. Include story points, business value, priority, and work item type when available\n" + export_content += "8. For high-complexity stories, suggest splitting when appropriate\n" + export_content += "9. Follow provider-aware formatting guidance listed per item\n\n" + export_content += "---\n\n" + comments_map = comments_by_item_id or {} + template_map = template_guidance_by_item_id or {} + + for idx, item in enumerate(items, 1): + export_content += f"## Item {idx}: {item.title}\n\n" + export_content += f"**ID**: {item.id}\n" + export_content += f"**URL**: {item.url}\n" + if item.canonical_url: + export_content += f"**Canonical URL**: {item.canonical_url}\n" + export_content += f"**State**: {item.state}\n" + export_content += f"**Provider**: {item.provider}\n" + item_template = template_map.get(item.id, {}) + if item_template: + export_content += f"\n**Target Template**: {item_template.get('name', 'N/A')}\n" + export_content += f"**Template ID**: {item_template.get('template_id', 'N/A')}\n" + template_desc = str(item_template.get("description", "")).strip() + if template_desc: + export_content += f"**Template Description**: {template_desc}\n" + required_sections = item_template.get("required_sections", []) + export_content += "\n**Required Sections**:\n" + if isinstance(required_sections, list) and required_sections: + for section in required_sections: + export_content += f"- {section}\n" + else: + export_content += "- None\n" + optional_sections = item_template.get("optional_sections", []) + export_content += "\n**Optional Sections**:\n" + if isinstance(optional_sections, list) and optional_sections: + for section in optional_sections: + export_content += f"- {section}\n" + else: + export_content += "- None\n" + export_content += "\n**Provider-aware formatting**:\n" + export_content += "- GitHub: Use markdown headings in body (`## Section Name`).\n" + export_content += ( + "- ADO: Use markdown headings in body; adapter maps to provider fields during writeback.\n" + ) + + if item.story_points is not None or item.business_value is not None or item.priority is not None: + export_content += "\n**Metrics**:\n" + if item.story_points is not None: + export_content += f"- Story Points: {item.story_points}\n" + if item.business_value is not None: + export_content += f"- Business Value: {item.business_value}\n" + if item.priority is not None: + export_content += f"- Priority: {item.priority} (1=highest)\n" + if item.value_points is not None: + export_content += f"- Value Points (SAFe): {item.value_points}\n" + if item.work_item_type: + export_content += f"- Work Item Type: {item.work_item_type}\n" + + if item.acceptance_criteria: + export_content += f"\n**Acceptance Criteria**:\n{item.acceptance_criteria}\n" + + item_comments = comments_map.get(item.id, []) + if item_comments: + export_content += "\n**Comments (annotations):**\n" + for comment in item_comments: + export_content += f"- {comment}\n" + + export_content += f"\n**Body**:\n```markdown\n{item.body_markdown}\n```\n" + export_content += "\n---\n\n" + return export_content + + +@beartype +def _resolve_target_template_for_refine_item( + item: BacklogItem, + *, + detector: TemplateDetector, + registry: TemplateRegistry, + template_id: str | None, + normalized_adapter: str | None, + normalized_framework: str | None, + normalized_persona: str | None, +) -> BacklogTemplate | None: + """Resolve target template for an item using the same precedence as refine flows.""" + if template_id: + direct = registry.get_template(template_id) + if direct is not None: + return direct + detection_result = detector.detect_template( + item, + provider=normalized_adapter, + framework=normalized_framework, + persona=normalized_persona, + ) + if detection_result.template_id: + detected = registry.get_template(detection_result.template_id) + if detected is not None: + return detected + resolved = registry.resolve_template( + provider=normalized_adapter, + framework=normalized_framework, + persona=normalized_persona, + ) + if resolved is not None: + return resolved + templates = registry.list_templates(scope="corporate") + return templates[0] if templates else None + + def _run_interactive_daily( items: list[BacklogItem], standup_config: dict[str, Any], @@ -639,6 +1103,8 @@ def _run_interactive_daily( ado_org: str | None, ado_project: str | None, ado_token: str | None, + first_comments: int | None = None, + last_comments: int | None = None, ) -> None: """ Run interactive step-by-step review: questionary selection, detail view, next/previous/back/exit. @@ -686,12 +1152,32 @@ def _run_interactive_daily( while True: item = items[current_idx] comments: list[str] = [] + total_comments = 0 if callable(get_comments_fn): with contextlib.suppress(Exception): raw = get_comments_fn(item) - comments = list(raw) if isinstance(raw, list) else [] - detail = _format_daily_item_detail(item, comments) + raw_comments = list(raw) if isinstance(raw, list) else [] + total_comments = len(raw_comments) + comments = _apply_comment_window( + raw_comments, + first_comments=first_comments, + last_comments=last_comments, + ) + explicit_comment_window = first_comments is not None or last_comments is not None + detail = _format_daily_item_detail( + item, + comments, + show_all_provided_comments=explicit_comment_window, + total_comments=total_comments, + ) console.print(Panel(detail, title=f"Story: {item.id}", border_style="cyan")) + console.print("\n[bold]Comments:[/bold]") + for panel in _build_daily_interactive_comment_panels( + comments, + show_all_provided_comments=explicit_comment_window, + total_comments=total_comments, + ): + console.print(panel) if suggest_next and n > 1: pending = [i for i in items if not i.assignees or i.story_points is not None] @@ -708,10 +1194,24 @@ def _run_interactive_daily( f"[dim]Suggested next (value score {best_score:.2f}): {best.id} - {best.title}[/dim]" ) - nav_choices = ["Next story", "Previous story", "Back to list", "Exit"] + can_post_comment = _post_standup_comment_supported(adapter_instance, item) + nav_choices = _build_daily_navigation_choices(can_post_comment=can_post_comment) nav = questionary.select("Navigation", choices=nav_choices).ask() if nav is None or nav == "Exit": return + if nav == "Post standup update": + y = questionary.text("Yesterday (optional):").ask() + t = questionary.text("Today (optional):").ask() + b = questionary.text("Blockers (optional):").ask() + body = _build_interactive_post_body(y, t, b) + if body is None: + console.print("[yellow]No standup text provided; nothing posted.[/yellow]") + continue + if _post_standup_to_item(adapter_instance, item, body): + console.print(f"[green]✓ Standup comment posted to story {item.id}: {item.url}[/green]") + else: + console.print("[red]Failed to post standup comment for selected story.[/red]") + continue if nav == "Back to list": break if nav == "Next story": @@ -1127,9 +1627,30 @@ def daily( "--assignee", help="Filter by assignee (e.g. 'me' or username). Only matching items are listed.", ), + search: str | None = typer.Option( + None, "--search", "-s", help="Search query to filter backlog items (provider-specific syntax)" + ), state: str | None = typer.Option(None, "--state", help="Filter by state (e.g. open, closed, Active)"), labels: list[str] | None = typer.Option(None, "--labels", "--tags", help="Filter by labels/tags"), + release: str | None = typer.Option(None, "--release", help="Filter by release identifier"), + issue_id: str | None = typer.Option( + None, + "--id", + help="Show only this backlog item (issue or work item ID). Other items are ignored.", + ), limit: int | None = typer.Option(None, "--limit", help="Maximum number of items to show"), + first_issues: int | None = typer.Option( + None, + "--first-issues", + min=1, + help="Show only the first N backlog items after filters (lowest numeric issue/work-item IDs).", + ), + last_issues: int | None = typer.Option( + None, + "--last-issues", + min=1, + help="Show only the last N backlog items after filters (highest numeric issue/work-item IDs).", + ), iteration: str | None = typer.Option( None, "--iteration", @@ -1155,6 +1676,11 @@ def daily( "--blockers-first", help="Sort so items with non-empty blockers appear first.", ), + mode: str = typer.Option( + "scrum", + "--mode", + help="Standup mode defaults: scrum|kanban|safe.", + ), interactive: bool = typer.Option( False, "--interactive", @@ -1171,6 +1697,18 @@ def daily( "--annotations", help="Include item comments/annotations in summarize/copilot export (adapter must support get_comments).", ), + first_comments: int | None = typer.Option( + None, + "--first-comments", + min=1, + help="Include only the first N comments per item (optional; default includes all comments).", + ), + last_comments: int | None = typer.Option( + None, + "--last-comments", + min=1, + help="Include only the last N comments per item (optional; default includes all comments).", + ), summarize: bool = typer.Option( False, "--summarize", @@ -1186,6 +1724,11 @@ def daily( "--suggest-next", help="In interactive mode, show suggested next item by value score (business value / (story points * priority)).", ), + patch: bool = typer.Option( + False, + "--patch", + help="Emit a patch proposal preview for standup notes/missing fields when patch-mode is available (no silent writes).", + ), post: bool = typer.Option( False, "--post", @@ -1225,15 +1768,31 @@ def daily( Default scope: state=open, limit=20 (overridable via SPECFACT_STANDUP_* env or .specfact/standup.yaml). """ standup_config = _load_standup_config() + normalized_mode = mode.lower().strip() + if normalized_mode not in {"scrum", "kanban", "safe"}: + console.print("[red]Invalid --mode. Use one of: scrum, kanban, safe.[/red]") + raise typer.Exit(1) effective_state, effective_limit, effective_assignee = _resolve_standup_options( state, limit, assignee, standup_config ) + effective_state = _resolve_daily_mode_state( + mode=normalized_mode, + cli_state=state, + effective_state=effective_state, + ) + fetch_limit = _resolve_daily_fetch_limit( + effective_limit, + first_issues=first_issues, + last_issues=last_issues, + ) items = _fetch_backlog_items( adapter, + search_query=search, state=effective_state, assignee=effective_assignee, labels=labels, - limit=effective_limit, + release=release, + limit=fetch_limit, iteration=iteration, sprint=sprint, repo_owner=repo_owner, @@ -1248,10 +1807,23 @@ def daily( items, labels=labels, state=effective_state, - assignee=effective_assignee, + assignee=_resolve_post_fetch_assignee_filter(adapter, effective_assignee), iteration=iteration, sprint=sprint, + release=release, ) + filtered = _apply_issue_id_filter(filtered, issue_id) + if issue_id is not None and not filtered: + console.print( + f"[bold red]✗[/bold red] No backlog item with id {issue_id!r} found. " + "Check filters and adapter configuration." + ) + raise typer.Exit(1) + try: + filtered = _resolve_daily_issue_window(filtered, first_issues=first_issues, last_issues=last_issues) + except ValueError as exc: + console.print(f"[red]{exc}.[/red]") + raise typer.Exit(1) from exc if len(filtered) > effective_limit: filtered = filtered[:effective_limit] @@ -1259,6 +1831,10 @@ def daily( console.print("[yellow]No backlog items found.[/yellow]") return + if first_comments is not None and last_comments is not None: + console.print("[red]Use only one of --first-comments or --last-comments.[/red]") + raise typer.Exit(1) + comments_by_item_id: dict[str, list[str]] = {} if include_comments and (copilot_export is not None or summarize or summarize_to is not None): comments_by_item_id = _collect_comment_annotations( @@ -1270,6 +1846,8 @@ def daily( ado_org=ado_org, ado_project=ado_project, ado_token=ado_token, + first_comments=first_comments, + last_comments=last_comments, ) if copilot_export is not None: @@ -1319,6 +1897,8 @@ def daily( ado_org=ado_org, ado_project=ado_project, ado_token=ado_token, + first_comments=first_comments, + last_comments=last_comments, ) return @@ -1392,6 +1972,7 @@ def _add_standup_rows_to_table(tbl: Table, row_list: list[dict[str, Any]], inclu str(r["id"]), str(r["title"])[:50], str(r["status"]), + str(r.get("assignees", "—"))[:30], r["last_updated"].strftime("%Y-%m-%d %H:%M") if hasattr(r["last_updated"], "strftime") else str(r["last_updated"]), @@ -1403,18 +1984,32 @@ def _add_standup_rows_to_table(tbl: Table, row_list: list[dict[str, Any]], inclu cells.append(str(r["priority"])) tbl.add_row(*cells) - table = Table(title="Daily standup", show_header=True, header_style="bold cyan") - table.add_column("ID", style="dim") - table.add_column("Title") - table.add_column("Status") - table.add_column("Last updated") - table.add_column("Yesterday", style="dim", max_width=30) - table.add_column("Today", style="dim", max_width=30) - table.add_column("Blockers", style="dim", max_width=20) - if include_priority: - table.add_column("Priority", style="dim") - _add_standup_rows_to_table(table, rows, include_priority) - console.print(table) + def _make_standup_table(title: str) -> Table: + table_obj = Table(title=title, show_header=True, header_style="bold cyan") + table_obj.add_column("ID", style="dim") + table_obj.add_column("Title") + table_obj.add_column("Status") + table_obj.add_column("Assignee", style="dim", max_width=30) + table_obj.add_column("Last updated") + table_obj.add_column("Yesterday", style="dim", max_width=30) + table_obj.add_column("Today", style="dim", max_width=30) + table_obj.add_column("Blockers", style="dim", max_width=20) + if include_priority: + table_obj.add_column("Priority", style="dim") + return table_obj + + exceptions_rows, normal_rows = _split_exception_rows(rows) + if exceptions_rows: + exceptions_table = _make_standup_table("Exceptions") + _add_standup_rows_to_table(exceptions_table, exceptions_rows, include_priority) + console.print(exceptions_table) + if normal_rows: + normal_table = _make_standup_table("Daily standup") + _add_standup_rows_to_table(normal_table, normal_rows, include_priority) + console.print(normal_table) + if not exceptions_rows and not normal_rows: + empty_table = _make_standup_table("Daily standup") + console.print(empty_table) if not unassigned_only and show_unassigned and rows_unassigned: table_pending = Table( title="Pending / open for commitment", @@ -1424,6 +2019,7 @@ def _add_standup_rows_to_table(tbl: Table, row_list: list[dict[str, Any]], inclu table_pending.add_column("ID", style="dim") table_pending.add_column("Title") table_pending.add_column("Status") + table_pending.add_column("Assignee", style="dim", max_width=30) table_pending.add_column("Last updated") table_pending.add_column("Yesterday", style="dim", max_width=30) table_pending.add_column("Today", style="dim", max_width=30) @@ -1433,6 +2029,18 @@ def _add_standup_rows_to_table(tbl: Table, row_list: list[dict[str, Any]], inclu _add_standup_rows_to_table(table_pending, rows_unassigned, include_priority) console.print(table_pending) + if patch: + if _is_patch_mode_available(): + proposal = _build_daily_patch_proposal(filtered, mode=normalized_mode) + console.print("\n[bold]Patch proposal preview:[/bold]") + console.print(Panel(proposal, border_style="yellow")) + console.print("[dim]No changes applied. Review/apply explicitly via patch workflow.[/dim]") + else: + console.print( + "[dim]Patch proposal requested, but patch-mode is not available yet. " + "Continuing without patch output.[/dim]" + ) + @beartype @app.command() @@ -1482,6 +2090,18 @@ def refine( "--limit", help="Maximum number of items to process in this refinement session. Use to cap batch size and avoid processing too many items at once.", ), + first_issues: int | None = typer.Option( + None, + "--first-issues", + min=1, + help="Process only the first N backlog items after filters/refinement checks.", + ), + last_issues: int | None = typer.Option( + None, + "--last-issues", + min=1, + help="Process only the last N backlog items after filters/refinement checks.", + ), ignore_refined: bool = typer.Option( True, "--ignore-refined/--no-ignore-refined", @@ -1526,6 +2146,18 @@ def refine( "--tmp-file", help="Custom temporary file path (overrides default)", ), + first_comments: int | None = typer.Option( + None, + "--first-comments", + min=1, + help="For refine preview/write prompt context, include only the first N comments per item.", + ), + last_comments: int | None = typer.Option( + None, + "--last-comments", + min=1, + help="For refine preview/write prompt context, include only the last N comments per item (default preview shows last 2; write prompts default to full comments).", + ), # DoR validation check_dor: bool = typer.Option( False, "--check-dor", help="Check Definition of Ready (DoR) rules before refinement" @@ -1801,7 +2433,7 @@ def refine( ) raise typer.Exit(1) - # When ignore_refined (default), keep only items that need refinement; then apply limit + # When ignore_refined (default), keep only items that need refinement; then apply windowing/limit if ignore_refined: items = [ i @@ -1810,9 +2442,9 @@ def refine( i, detector, registry, template_id, normalized_adapter, normalized_framework, normalized_persona ) ] - if limit is not None and len(items) > limit: - items = items[:limit] - if ignore_refined and (limit is not None or issue_id is not None): + if ignore_refined and ( + limit is not None or issue_id is not None or first_issues is not None or last_issues is not None + ): console.print( f"[dim]Filtered to {len(items)} item(s) needing refinement" + (f" (limit {limit})" if limit is not None else "") @@ -1823,6 +2455,14 @@ def refine( if export_to_tmp and import_from_tmp: console.print("[bold red]✗[/bold red] --export-to-tmp and --import-from-tmp are mutually exclusive") raise typer.Exit(1) + if first_comments is not None and last_comments is not None: + console.print("[bold red]✗[/bold red] Use only one of --first-comments or --last-comments") + raise typer.Exit(1) + if first_issues is not None and last_issues is not None: + console.print("[bold red]✗[/bold red] Use only one of --first-issues or --last-issues") + raise typer.Exit(1) + + items = _apply_issue_window(items, first_issues=first_issues, last_issues=last_issues) # Handle export mode if export_to_tmp: @@ -1830,45 +2470,51 @@ def refine( export_file = tmp_file or (Path(tempfile.gettempdir()) / f"specfact-backlog-refine-{timestamp}.md") console.print(f"[bold cyan]Exporting {len(items)} backlog item(s) to: {export_file}[/bold cyan]") - - # Export items to markdown file - export_content = "# SpecFact Backlog Refinement Export\n\n" - export_content += f"**Export Date**: {datetime.now().isoformat()}\n" - export_content += f"**Adapter**: {adapter}\n" - export_content += f"**Items**: {len(items)}\n\n" - export_content += "---\n\n" - - for idx, item in enumerate(items, 1): - export_content += f"## Item {idx}: {item.title}\n\n" - export_content += f"**ID**: {item.id}\n" - export_content += f"**URL**: {item.url}\n" - if item.canonical_url: - export_content += f"**Canonical URL**: {item.canonical_url}\n" - export_content += f"**State**: {item.state}\n" - export_content += f"**Provider**: {item.provider}\n" - - # Include metrics - if item.story_points is not None or item.business_value is not None or item.priority is not None: - export_content += "\n**Metrics**:\n" - if item.story_points is not None: - export_content += f"- Story Points: {item.story_points}\n" - if item.business_value is not None: - export_content += f"- Business Value: {item.business_value}\n" - if item.priority is not None: - export_content += f"- Priority: {item.priority} (1=highest)\n" - if item.value_points is not None: - export_content += f"- Value Points (SAFe): {item.value_points}\n" - if item.work_item_type: - export_content += f"- Work Item Type: {item.work_item_type}\n" - - # Include acceptance criteria - if item.acceptance_criteria: - export_content += f"\n**Acceptance Criteria**:\n{item.acceptance_criteria}\n" - - # Include body - export_content += f"\n**Body**:\n```markdown\n{item.body_markdown}\n```\n" - - export_content += "\n---\n\n" + if first_comments is not None or last_comments is not None: + console.print( + "[dim]Note: --first-comments/--last-comments apply to preview and write prompt context; export always includes full comments.[/dim]" + ) + export_first_comments, export_last_comments = _resolve_refine_export_comment_window( + first_comments=first_comments, + last_comments=last_comments, + ) + comments_by_item_id = _collect_comment_annotations( + adapter, + items, + repo_owner=repo_owner, + repo_name=repo_name, + github_token=github_token, + ado_org=ado_org, + ado_project=ado_project, + ado_token=ado_token, + first_comments=export_first_comments, + last_comments=export_last_comments, + ) + template_guidance_by_item_id: dict[str, dict[str, Any]] = {} + for export_item in items: + target_template = _resolve_target_template_for_refine_item( + export_item, + detector=detector, + registry=registry, + template_id=template_id, + normalized_adapter=normalized_adapter, + normalized_framework=normalized_framework, + normalized_persona=normalized_persona, + ) + if target_template is not None: + template_guidance_by_item_id[export_item.id] = { + "template_id": target_template.template_id, + "name": target_template.name, + "description": target_template.description, + "required_sections": list(target_template.required_sections or []), + "optional_sections": list(target_template.optional_sections or []), + } + export_content = _build_refine_export_content( + adapter, + items, + comments_by_item_id=comments_by_item_id or None, + template_guidance_by_item_id=template_guidance_by_item_id or None, + ) export_file.write_text(export_content, encoding="utf-8") console.print(f"[green]✓ Exported to: {export_file}[/green]") @@ -1956,8 +2602,8 @@ def refine( console.print(f"[green]✓ Updated {len(updated_items)} backlog item(s)[/green]") return - # Apply limit if specified (when not ignore_refined; when ignore_refined we already filtered and sliced) - if not ignore_refined and limit is not None and len(items) > limit: + # Apply limit if specified + if limit is not None and len(items) > limit: items = items[:limit] console.print(f"[yellow]Limited to {limit} items (found {len(items)} total)[/yellow]") else: @@ -1967,6 +2613,83 @@ def refine( refined_count = 0 skipped_count = 0 cancelled = False + comments_by_item_id: dict[str, list[str]] = {} + if preview and not write: + preview_first_comments, preview_last_comments = _resolve_refine_preview_comment_window( + first_comments=first_comments, + last_comments=last_comments, + ) + with Progress( + SpinnerColumn(), + TextColumn("[progress.description]{task.description}"), + TimeElapsedColumn(), + console=console, + transient=False, + ) as preview_comment_progress: + preview_comment_task = preview_comment_progress.add_task( + _build_comment_fetch_progress_description(0, len(items), "-"), + total=None, + ) + + def _on_preview_comment_progress(index: int, total: int, item: BacklogItem) -> None: + preview_comment_progress.update( + preview_comment_task, + description=_build_comment_fetch_progress_description(index, total, item.id), + ) + + comments_by_item_id = _collect_comment_annotations( + adapter, + items, + repo_owner=repo_owner, + repo_name=repo_name, + github_token=github_token, + ado_org=ado_org, + ado_project=ado_project, + ado_token=ado_token, + first_comments=preview_first_comments, + last_comments=preview_last_comments, + progress_callback=_on_preview_comment_progress, + ) + preview_comment_progress.update( + preview_comment_task, + description=f"[green]✓[/green] Fetched comments for {len(items)} issue(s)", + ) + elif write: + with Progress( + SpinnerColumn(), + TextColumn("[progress.description]{task.description}"), + TimeElapsedColumn(), + console=console, + transient=False, + ) as write_comment_progress: + write_comment_task = write_comment_progress.add_task( + _build_comment_fetch_progress_description(0, len(items), "-"), + total=None, + ) + + def _on_write_comment_progress(index: int, total: int, item: BacklogItem) -> None: + write_comment_progress.update( + write_comment_task, + description=_build_comment_fetch_progress_description(index, total, item.id), + ) + + comments_by_item_id = _collect_comment_annotations( + adapter, + items, + repo_owner=repo_owner, + repo_name=repo_name, + github_token=github_token, + ado_org=ado_org, + ado_project=ado_project, + ado_token=ado_token, + first_comments=first_comments, + last_comments=last_comments, + progress_callback=_on_write_comment_progress, + ) + write_comment_progress.update( + write_comment_task, + description=f"[green]✓[/green] Fetched comments for {len(items)} issue(s)", + ) # Process items without progress bar during refinement to avoid conflicts with interactive prompts for idx, item in enumerate(items, 1): @@ -2123,6 +2846,14 @@ def refine( else: console.print(Panel(body_content)) + preview_comments = comments_by_item_id.get(item.id, []) + console.print("\n[bold]Comments:[/bold]") + if preview_comments: + for panel in _build_refine_preview_comment_panels(preview_comments): + console.print(panel) + else: + console.print(_build_refine_preview_comment_empty_panel()) + # Show template info console.print( f"\n[bold]Target Template:[/bold] {target_template.name} (ID: {target_template.template_id})" @@ -2144,7 +2875,8 @@ def refine( # Generate prompt for IDE AI copilot console.print(f"[bold]Generating refinement prompt for template: {target_template.name}...[/bold]") - prompt = refiner.generate_refinement_prompt(item, target_template) + prompt_comments = comments_by_item_id.get(item.id, []) + prompt = refiner.generate_refinement_prompt(item, target_template, comments=prompt_comments) # Display prompt for IDE AI copilot console.print("\n[bold]Refinement Prompt for IDE AI Copilot:[/bold]") @@ -2454,6 +3186,10 @@ def refine( console.print("[yellow]Session cancelled by user[/yellow]") if limit: console.print(f"[dim]Limit applied: {limit} items[/dim]") + if first_issues is not None: + console.print(f"[dim]Issue window applied: first {first_issues} items[/dim]") + if last_issues is not None: + console.print(f"[dim]Issue window applied: last {last_issues} items[/dim]") console.print(f"[green]Refined: {refined_count}[/green]") console.print(f"[yellow]Skipped: {skipped_count}[/yellow]") diff --git a/tests/unit/adapters/test_ado_backlog_adapter.py b/tests/unit/adapters/test_ado_backlog_adapter.py index f25a351e..f0d213c8 100644 --- a/tests/unit/adapters/test_ado_backlog_adapter.py +++ b/tests/unit/adapters/test_ado_backlog_adapter.py @@ -349,6 +349,57 @@ def test_auth_headers_no_token(self) -> None: headers = adapter._auth_headers() assert headers == {} + @beartype + @patch("specfact_cli.adapters.ado.requests.get") + def test_get_work_item_comments_follows_continuation_token(self, mock_get: MagicMock) -> None: + """Fetch all comment pages using ADO comments continuation token.""" + page1 = MagicMock() + page1.json.return_value = {"comments": [{"text": "c1"}, {"text": "c2"}]} + page1.raise_for_status = MagicMock() + page1.headers = {"x-ms-continuationtoken": "token-1"} + + page2 = MagicMock() + page2.json.return_value = {"comments": [{"text": "c3"}]} + page2.raise_for_status = MagicMock() + page2.headers = {} + + mock_get.side_effect = [page1, page2] + + adapter = AdoAdapter(org="test", project="project", api_token="token") + comments = adapter._get_work_item_comments("test", "project", 123) + + assert comments == [{"text": "c1"}, {"text": "c2"}, {"text": "c3"}] + assert mock_get.call_count == 2 + first_call = mock_get.call_args_list[0] + second_call = mock_get.call_args_list[1] + first_url = first_call.kwargs.get("url", first_call.args[0] if first_call.args else "") + assert "workItems/123/comments" in first_url + assert first_call.kwargs["params"]["api-version"] == "7.1-preview.4" + assert "continuationToken" not in first_call.kwargs["params"] + assert second_call.kwargs["params"]["continuationToken"] == "token-1" + + @beartype + @patch.object(AdoAdapter, "_get_work_item_comments") + def test_get_comments_returns_text_only(self, mock_get_work_item_comments: MagicMock) -> None: + """Convert ADO comment objects to normalized text lines.""" + mock_get_work_item_comments.return_value = [ + {"text": "First"}, + {"body": "Second"}, + {"text": " "}, + {}, + ] + adapter = AdoAdapter(org="test", project="project", api_token="token") + item = BacklogItem( + id="123", + provider="ado", + url="https://dev.azure.com/test/project/_workitems/edit/123", + title="Item", + body_markdown="", + state="Active", + ) + comments = adapter.get_comments(item) + assert comments == ["First", "Second"] + @beartype @patch("azure.identity.DeviceCodeCredential") @patch("azure.identity.TokenCachePersistenceOptions") diff --git a/tests/unit/adapters/test_github_backlog_adapter.py b/tests/unit/adapters/test_github_backlog_adapter.py index 80bdaac0..93dbb122 100644 --- a/tests/unit/adapters/test_github_backlog_adapter.py +++ b/tests/unit/adapters/test_github_backlog_adapter.py @@ -94,6 +94,35 @@ def test_fetch_backlog_items_with_assignee_filter(self, mock_get: MagicMock) -> call_args = mock_get.call_args assert "assignee:alice" in call_args[1]["params"]["q"] + @beartype + @patch("specfact_cli.adapters.github.requests.get") + def test_fetch_backlog_items_with_me_assignee_uses_at_me_query(self, mock_get: MagicMock) -> None: + """`me` assignee maps to GitHub provider-relative `@me` search qualifier.""" + mock_response = MagicMock() + mock_response.json.return_value = { + "items": [ + { + "number": 1, + "html_url": "https://github.com/test/repo/issues/1", + "title": "Issue assigned to current user", + "body": "Issue body", + "state": "open", + "assignees": [{"login": "actual-login"}], + "labels": [], + } + ] + } + mock_response.raise_for_status = MagicMock() + mock_get.return_value = mock_response + + adapter = GitHubAdapter(repo_owner="test", repo_name="repo", api_token="token") + filters = BacklogFilters(assignee="me") + items = adapter.fetch_backlog_items(filters) + + call_args = mock_get.call_args + assert "assignee:@me" in call_args[1]["params"]["q"] + assert len(items) == 1 + @beartype @patch("specfact_cli.adapters.github.requests.patch") def test_update_backlog_item(self, mock_patch: MagicMock) -> None: diff --git a/tests/unit/backlog/test_ai_refiner.py b/tests/unit/backlog/test_ai_refiner.py index 9dcc1bd7..31a310bd 100644 --- a/tests/unit/backlog/test_ai_refiner.py +++ b/tests/unit/backlog/test_ai_refiner.py @@ -68,6 +68,28 @@ def test_generate_refinement_prompt( assert "As a" in prompt assert "I want" in prompt + @beartype + def test_generate_refinement_prompt_includes_comments_when_provided( + self, refiner: BacklogAIRefiner, arbitrary_backlog_item: BacklogItem, user_story_template: BacklogTemplate + ) -> None: + """Prompt includes comment context so refinement sees evolving discussion.""" + prompt = refiner.generate_refinement_prompt( + arbitrary_backlog_item, + user_story_template, + comments=["First update from team", "Final clarification from PO"], + ) + assert "Comments" in prompt + assert "First update from team" in prompt + assert "Final clarification from PO" in prompt + + @beartype + def test_generate_refinement_prompt_mentions_no_comments_when_empty( + self, refiner: BacklogAIRefiner, arbitrary_backlog_item: BacklogItem, user_story_template: BacklogTemplate + ) -> None: + """Prompt explicitly states that comments were checked but none exist.""" + prompt = refiner.generate_refinement_prompt(arbitrary_backlog_item, user_story_template, comments=[]) + assert "No comments found" in prompt + @beartype def test_validate_and_score_complete_refinement( self, refiner: BacklogAIRefiner, arbitrary_backlog_item: BacklogItem, user_story_template: BacklogTemplate diff --git a/tests/unit/commands/test_backlog_commands.py b/tests/unit/commands/test_backlog_commands.py index 51832a26..9af82234 100644 --- a/tests/unit/commands/test_backlog_commands.py +++ b/tests/unit/commands/test_backlog_commands.py @@ -8,14 +8,22 @@ from unittest.mock import MagicMock, patch +from rich.panel import Panel from typer.testing import CliRunner from specfact_cli.backlog.template_detector import TemplateDetector from specfact_cli.cli import app from specfact_cli.models.backlog_item import BacklogItem from specfact_cli.modules.backlog.src.commands import ( + _apply_issue_window, + _build_comment_fetch_progress_description, + _build_refine_export_content, + _build_refine_preview_comment_empty_panel, + _build_refine_preview_comment_panels, _item_needs_refinement, _parse_refined_export_markdown, + _resolve_refine_export_comment_window, + _resolve_refine_preview_comment_window, ) from specfact_cli.templates.registry import BacklogTemplate, TemplateRegistry @@ -336,6 +344,179 @@ def foo(): assert "Then we see the error." in body +class TestBuildRefineExportContent: + """Tests for refine export content rendering.""" + + def test_refine_export_includes_comments_when_available(self) -> None: + """Refine export includes comment annotations by default when available.""" + item = BacklogItem( + id="42", + provider="ado", + url="https://dev.azure.com/org/project/_workitems/edit/42", + title="Story", + body_markdown="Body text", + state="Active", + assignees=[], + ) + content = _build_refine_export_content( + adapter="ado", + items=[item], + comments_by_item_id={"42": ["Comment A", "Comment B"]}, + ) + assert "Comments (annotations)" in content + assert "Comment A" in content + assert "Comment B" in content + assert "## Copilot Instructions" in content + assert "must not include this instruction block" in content + assert "Preserve all original requirements, scope, and technical details" in content + + def test_refine_export_omits_comments_section_when_none(self) -> None: + """Refine export omits comments section when no comments exist for item.""" + item = BacklogItem( + id="42", + provider="ado", + url="https://dev.azure.com/org/project/_workitems/edit/42", + title="Story", + body_markdown="Body text", + state="Active", + assignees=[], + ) + content = _build_refine_export_content(adapter="ado", items=[item], comments_by_item_id={}) + assert "Comments (annotations)" not in content + + def test_refine_export_places_instructions_before_first_item(self) -> None: + """Instruction block appears before exported item sections.""" + item = BacklogItem( + id="42", + provider="ado", + url="https://dev.azure.com/org/project/_workitems/edit/42", + title="Story", + body_markdown="Body text", + state="Active", + assignees=[], + ) + content = _build_refine_export_content(adapter="ado", items=[item], comments_by_item_id={}) + assert content.index("## Copilot Instructions") < content.index("## Item 1:") + + def test_refine_export_includes_template_guidance_for_items(self) -> None: + """Export includes template guidance similar to interactive prompts.""" + item = BacklogItem( + id="42", + provider="github", + url="https://github.com/org/repo/issues/42", + title="Story", + body_markdown="Body text", + state="open", + assignees=[], + ) + content = _build_refine_export_content( + adapter="github", + items=[item], + comments_by_item_id={}, + template_guidance_by_item_id={ + "42": { + "template_id": "enabler_v1", + "name": "Enabler", + "description": "Enabler work template", + "required_sections": ["Objective", "Technical Approach", "Success Criteria"], + "optional_sections": ["Dependencies", "Risks", "Timeline"], + } + }, + ) + assert "**Target Template**:" in content + assert "**Required Sections**:" in content + assert "**Optional Sections**:" in content + + +class TestRefineCommentWindowResolution: + """Tests for refine preview/export comment-window semantics.""" + + def test_refine_preview_defaults_to_last_two_comments(self) -> None: + """Preview uses last two comments when no explicit window flags are provided.""" + first, last = _resolve_refine_preview_comment_window(first_comments=None, last_comments=None) + assert first is None + assert last == 2 + + def test_refine_preview_respects_first_comments_override(self) -> None: + """Preview honors --first-comments when provided.""" + first, last = _resolve_refine_preview_comment_window(first_comments=5, last_comments=None) + assert first == 5 + assert last is None + + def test_refine_preview_respects_last_comments_override(self) -> None: + """Preview honors --last-comments when provided.""" + first, last = _resolve_refine_preview_comment_window(first_comments=None, last_comments=4) + assert first is None + assert last == 4 + + def test_refine_export_always_uses_full_comment_history(self) -> None: + """Export ignores preview comment-window flags and always requests full comments.""" + first, last = _resolve_refine_export_comment_window(first_comments=5, last_comments=None) + assert first is None + assert last is None + + first_2, last_2 = _resolve_refine_export_comment_window(first_comments=None, last_comments=3) + assert first_2 is None + assert last_2 is None + + +class TestRefinePreviewCommentUx: + """Tests for refine preview comment progress and block rendering.""" + + def test_build_comment_fetch_progress_description_includes_position(self) -> None: + """Progress message uses n/m indicator while fetching comments.""" + message = _build_comment_fetch_progress_description(3, 66, "123") + assert "3/66" in message + assert "123" in message + assert "Fetching issue" in message + + def test_build_refine_preview_comment_panels_returns_panels(self) -> None: + """Preview comments are rendered as panel blocks for clear scoping.""" + panels = _build_refine_preview_comment_panels(["first comment", "second comment"]) + assert len(panels) == 2 + assert all(isinstance(panel, Panel) for panel in panels) + + def test_build_refine_preview_comment_empty_panel_returns_panel(self) -> None: + """Preview shows explicit hint when no comments are found.""" + panel = _build_refine_preview_comment_empty_panel() + assert isinstance(panel, Panel) + + +class TestRefineIssueWindow: + """Tests for refine first/last issue window controls.""" + + @staticmethod + def _item(id_: str) -> BacklogItem: + return BacklogItem( + id=id_, + provider="github", + url=f"https://github.com/org/repo/issues/{id_}", + title=f"Item {id_}", + body_markdown="Body", + state="open", + assignees=[], + ) + + def test_apply_issue_window_first_issues(self) -> None: + items = [self._item("3"), self._item("1"), self._item("2")] + result = _apply_issue_window(items, first_issues=2, last_issues=None) + assert [i.id for i in result] == ["1", "2"] + + def test_apply_issue_window_last_issues(self) -> None: + items = [self._item("3"), self._item("1"), self._item("2")] + result = _apply_issue_window(items, first_issues=None, last_issues=2) + assert [i.id for i in result] == ["2", "3"] + + def test_apply_issue_window_rejects_both_first_and_last(self) -> None: + items = [self._item("1")] + try: + _apply_issue_window(items, first_issues=1, last_issues=1) + except ValueError as exc: + assert "--first-issues" in str(exc) + return + raise AssertionError("Expected ValueError when both first_issues and last_issues are set") + + class TestItemNeedsRefinement: """Tests for _item_needs_refinement helper.""" diff --git a/tests/unit/commands/test_backlog_daily.py b/tests/unit/commands/test_backlog_daily.py index fcd35914..80c56760 100644 --- a/tests/unit/commands/test_backlog_daily.py +++ b/tests/unit/commands/test_backlog_daily.py @@ -26,6 +26,7 @@ from unittest.mock import MagicMock import click +import pytest import typer.main from typer.testing import CliRunner @@ -33,14 +34,25 @@ from specfact_cli.cli import app from specfact_cli.models.backlog_item import BacklogItem from specfact_cli.modules.backlog.src.commands import ( + _apply_comment_window, _apply_filters, + _apply_issue_id_filter, _build_copilot_export_content, + _build_daily_interactive_comment_panels, + _build_daily_navigation_choices, + _build_daily_patch_proposal, + _build_interactive_post_body, _build_standup_rows, _build_summarize_prompt_content, _compute_value_score, _format_daily_item_detail, _format_standup_comment, _post_standup_comment_supported, + _resolve_daily_fetch_limit, + _resolve_daily_issue_window, + _resolve_daily_mode_state, + _resolve_post_fetch_assignee_filter, + _split_exception_rows, ) @@ -139,6 +151,25 @@ def test_assignee_filter_applied_by_caller(self) -> None: rows_me = _build_standup_rows([items[0]]) assert len(rows_me) == 1 and rows_me[0]["title"] == "Mine" + def test_row_includes_assignees_for_table_rendering(self) -> None: + """Standup row carries assignees so table can show assignment context.""" + rows = _build_standup_rows([_item("1", "Mine", assignees=["alice", "bob"])]) + assert rows[0]["assignees"] == "alice, bob" + + +class TestAssigneeFilterResolution: + """Normalize assignee behavior between adapter-side and post-fetch filtering.""" + + def test_github_me_alias_skips_post_fetch_assignee_filter(self) -> None: + """GitHub `me`/`@me` should rely on adapter-side filtering, not literal local matching.""" + assert _resolve_post_fetch_assignee_filter("github", "me") is None + assert _resolve_post_fetch_assignee_filter("github", "@me") is None + + def test_non_me_assignee_is_kept_for_post_fetch_filter(self) -> None: + """Explicit usernames still apply in local post-fetch filtering.""" + assert _resolve_post_fetch_assignee_filter("github", "djm81") == "djm81" + assert _resolve_post_fetch_assignee_filter("ado", "me") == "me" + class TestFormatStandupComment: """Format standup comment for posting (Yesterday / Today / Blockers).""" @@ -227,6 +258,35 @@ def test_daily_accepts_blockers_first(self) -> None: option_names = _get_daily_command_option_names() assert "--blockers-first" in option_names + def test_daily_accepts_mode_and_patch_options(self) -> None: + """Backlog daily supports mode and patch proposal options.""" + option_names = _get_daily_command_option_names() + assert "--mode" in option_names + assert "--patch" in option_names + + def test_daily_accepts_search_release_and_id_options(self) -> None: + """Backlog daily supports global filter parity options.""" + option_names = _get_daily_command_option_names() + assert "--search" in option_names + assert "--release" in option_names + assert "--id" in option_names + + +class TestIssueIdFilter: + """Shared issue-id filtering behavior.""" + + def test_apply_issue_id_filter_returns_matching_item(self) -> None: + """When item exists, only matching ID remains.""" + items = [_item("54", "A"), _item("55", "B")] + filtered = _apply_issue_id_filter(items, "55") + assert [i.id for i in filtered] == ["55"] + + def test_apply_issue_id_filter_returns_empty_when_not_found(self) -> None: + """When item ID doesn't exist, result is empty list.""" + items = [_item("54", "A"), _item("55", "B")] + filtered = _apply_issue_id_filter(items, "999") + assert filtered == [] + class TestDefaultStandupScope: """Scenario: Standup view uses default scope when no filters given (6.1).""" @@ -444,12 +504,63 @@ def test_format_daily_item_detail_includes_title_body_status(self) -> None: assert "Description" in detail or "here" in detail assert "open" in detail.lower() or "status" in detail.lower() - def test_format_daily_item_detail_includes_comments_when_provided(self) -> None: - """When comments are provided, they appear in the detail string.""" + def test_format_daily_item_detail_omits_comment_block(self) -> None: + """Interactive detail panel should keep comments out; comments render in dedicated panels.""" item = _item("1", "Story") detail = _format_daily_item_detail(item, comments=["Comment one", "Comment two"]) - assert "Comment one" in detail or "Comment" in detail - assert "Comment two" in detail or "two" in detail + assert "Comment one" not in detail + assert "Comment two" not in detail + assert "Latest comment" not in detail + assert "Comments:" not in detail + + +class TestDailyInteractiveCommentPanels: + """Daily interactive comment panels should mirror refine-style scoping.""" + + def test_default_mode_shows_latest_panel_plus_hint(self) -> None: + """Without comment-window overrides, show latest comment and hidden-count hint panel.""" + panels = _build_daily_interactive_comment_panels( + ["Comment one", "Comment two"], + show_all_provided_comments=False, + total_comments=2, + ) + assert len(panels) == 2 + + def test_window_mode_shows_all_windowed_panels_plus_omitted_hint(self) -> None: + """With explicit comment window, render each windowed comment panel and omitted-count hint panel.""" + panels = _build_daily_interactive_comment_panels( + ["Comment one", "Comment two", "Comment three"], + show_all_provided_comments=True, + total_comments=5, + ) + assert len(panels) == 4 + + +class TestDailyInteractivePostAction: + """Interactive daily post helpers.""" + + def test_navigation_choices_include_post_when_supported(self) -> None: + """Post action is available when adapter supports comments.""" + choices = _build_daily_navigation_choices(can_post_comment=True) + assert "Post standup update" in choices + + def test_navigation_choices_omit_post_when_not_supported(self) -> None: + """Post action is hidden when adapter cannot post comments.""" + choices = _build_daily_navigation_choices(can_post_comment=False) + assert "Post standup update" not in choices + + def test_build_interactive_post_body_rejects_empty(self) -> None: + """No text means no post body should be created.""" + assert _build_interactive_post_body(None, "", " ") is None + + def test_build_interactive_post_body_formats_standup(self) -> None: + """Any provided standup text creates a valid standup comment body.""" + body = _build_interactive_post_body("Did X", "Do Y", "None") + assert body is not None + assert "Standup " in body + assert "**Yesterday:** Did X" in body + assert "**Today:** Do Y" in body + assert "**Blockers:** None" in body class TestBacklogDailyInteractiveAndExportOptions: @@ -477,6 +588,121 @@ def test_daily_help_shows_comment_annotations(self) -> None: assert "--comments" in option_names assert "--annotations" in option_names + def test_daily_help_shows_comment_window_options(self) -> None: + """Backlog daily has --first-comments and --last-comments options.""" + option_names = _get_daily_command_option_names() + assert "--first-comments" in option_names + assert "--last-comments" in option_names + + def test_daily_help_shows_issue_window_options(self) -> None: + """Backlog daily has --first-issues and --last-issues options.""" + option_names = _get_daily_command_option_names() + assert "--first-issues" in option_names + assert "--last-issues" in option_names + + +class TestDailyIssueWindowResolution: + """Daily issue-window behavior should mirror refine semantics.""" + + def test_daily_issue_window_applies_first(self) -> None: + """`--first-issues` keeps the lowest numeric IDs.""" + items = [_item("10", "ten"), _item("2", "two"), _item("7", "seven")] + windowed = _resolve_daily_issue_window(items, first_issues=2, last_issues=None) + assert [i.id for i in windowed] == ["2", "7"] + + def test_daily_issue_window_applies_last(self) -> None: + """`--last-issues` keeps the highest numeric IDs.""" + items = [_item("10", "ten"), _item("2", "two"), _item("7", "seven")] + windowed = _resolve_daily_issue_window(items, first_issues=None, last_issues=2) + assert [i.id for i in windowed] == ["7", "10"] + + def test_daily_issue_window_rejects_both(self) -> None: + """Using both windows should raise a clear validation error.""" + with pytest.raises(ValueError, match="first-issues or --last-issues"): + _resolve_daily_issue_window([_item("1", "one")], first_issues=1, last_issues=1) + + +class TestExceptionsFirstAndMode: + """Exceptions-first and mode defaults for daily standup.""" + + def test_split_exception_rows_prioritizes_blockers(self) -> None: + """Rows with blockers go to exceptions section.""" + rows = [ + {"id": "1", "blockers": ""}, + {"id": "2", "blockers": "Waiting on API"}, + {"id": "3", "blockers": "Needs decision"}, + ] + exceptions, normal = _split_exception_rows(rows) + assert [r["id"] for r in exceptions] == ["2", "3"] + assert [r["id"] for r in normal] == ["1"] + + def test_split_exception_rows_orders_blockers_then_policy_then_aging(self) -> None: + """Exceptions include blockers, policy failures, and aging/stalled rows in required order.""" + rows = [ + {"id": "1", "blockers": "", "policy_status": "failed"}, + {"id": "2", "blockers": "", "days_stalled": 5}, + {"id": "3", "blockers": "Waiting on dependency"}, + {"id": "4", "blockers": "", "policy_failures": ["dor"]}, + {"id": "5", "blockers": ""}, + ] + exceptions, normal = _split_exception_rows(rows) + assert [r["id"] for r in exceptions] == ["3", "1", "4", "2"] + assert [r["id"] for r in normal] == ["5"] + + def test_mode_kanban_relaxes_default_open_state(self) -> None: + """Kanban mode removes default open-only filter when state not explicitly provided.""" + effective = _resolve_daily_mode_state(mode="kanban", cli_state=None, effective_state="open") + assert effective is None + + def test_mode_keeps_explicit_state(self) -> None: + """Explicit CLI state takes precedence regardless of mode.""" + effective = _resolve_daily_mode_state(mode="kanban", cli_state="closed", effective_state="closed") + assert effective == "closed" + + def test_patch_proposal_contains_item_ids(self) -> None: + """Patch proposal includes selected item IDs for review.""" + proposal = _build_daily_patch_proposal([_item("54", "A"), _item("55", "B")], mode="scrum") + assert "54" in proposal and "55" in proposal + assert "Patch Proposal" in proposal + + +class TestDailyFetchLimitResolution: + """Daily issue-window should evaluate over full candidate set before limit truncation.""" + + def test_fetch_limit_kept_without_issue_window(self) -> None: + """Without issue-window flags, keep effective limit for fetch.""" + assert _resolve_daily_fetch_limit(20, first_issues=None, last_issues=None) == 20 + + def test_fetch_limit_removed_with_first_or_last_issue_window(self) -> None: + """With issue-window flags, fetch full set first.""" + assert _resolve_daily_fetch_limit(20, first_issues=3, last_issues=None) is None + assert _resolve_daily_fetch_limit(20, first_issues=None, last_issues=3) is None + + +class TestCommentWindow: + """Comment window helpers.""" + + def test_apply_comment_window_default_full(self) -> None: + """Default includes all comments.""" + comments = ["c1", "c2", "c3"] + assert _apply_comment_window(comments) == comments + + def test_apply_comment_window_first(self) -> None: + """First-comments returns first N comments.""" + comments = ["c1", "c2", "c3"] + assert _apply_comment_window(comments, first_comments=2) == ["c1", "c2"] + + def test_apply_comment_window_last(self) -> None: + """Last-comments returns last N comments.""" + comments = ["c1", "c2", "c3"] + assert _apply_comment_window(comments, last_comments=2) == ["c2", "c3"] + + def test_apply_comment_window_rejects_both_first_and_last(self) -> None: + """Using both first and last comment windows at once raises ValueError.""" + comments = ["c1", "c2", "c3"] + with pytest.raises(ValueError): + _apply_comment_window(comments, first_comments=1, last_comments=1) + class TestBuildSummarizePromptContent: """Scenario: --summarize outputs prompt with filter context and per-item data (22.1).""" From 3c2781e60f2f0b7bbab130a6072806fdbc7dd01e Mon Sep 17 00:00:00 2001 From: Dominikus Nold Date: Wed, 11 Feb 2026 01:06:48 +0100 Subject: [PATCH 2/8] docs(openspec): mark backlog-scrum-01 standup change checklist complete --- .../backlog-scrum-01-standup-exceptions-first/tasks.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openspec/changes/backlog-scrum-01-standup-exceptions-first/tasks.md b/openspec/changes/backlog-scrum-01-standup-exceptions-first/tasks.md index 0b35ad9e..0569ed41 100644 --- a/openspec/changes/backlog-scrum-01-standup-exceptions-first/tasks.md +++ b/openspec/changes/backlog-scrum-01-standup-exceptions-first/tasks.md @@ -75,9 +75,9 @@ Per `openspec/config.yaml`, **tests before code** apply to any task that adds or - [x] 4.2 Run contract test: `hatch run contract-test`. - [x] 4.3 Update docs: agile-scrum-workflows.md, backlog-refinement.md, devops-adapter-integration.md (comment context behavior, first/last comment controls, export guidance). - [x] 4.4 Update slash prompt templates: `resources/prompts/specfact.backlog-daily.md` and `resources/prompts/specfact.backlog-refine.md` for comment-context guidance. -- [ ] 4.5 Add CHANGELOG entry; sync version. +- [x] 4.5 Add CHANGELOG entry; sync version. ## 5. Create Pull Request to dev -- [ ] 5.1 Commit and push: `git add .` then `git commit -m "feat(backlog): daily standup exceptions-first and --mode scrum|kanban|safe (fixes #175)"` and `git push origin feature/backlog-scrum-01-standup-exceptions-first` -- [ ] 5.2 Create PR to dev using repo PR template; PR body MUST include `Fixes nold-ai/specfact-cli#175` and this change ID for Development linking. +- [x] 5.1 Commit and push: `git add .` then `git commit -m "feat(backlog): daily standup exceptions-first and --mode scrum|kanban|safe (fixes #175)"` and `git push origin feature/backlog-scrum-01-standup-exceptions-first` +- [x] 5.2 Create PR to dev using repo PR template; PR body MUST include `Fixes nold-ai/specfact-cli#175` and this change ID for Development linking. From ada6fc41d3bb98ff3c0af79bdb3b81e08d5b01b3 Mon Sep 17 00:00:00 2001 From: Dominikus Nold Date: Wed, 11 Feb 2026 01:10:50 +0100 Subject: [PATCH 3/8] fix(openspec): mark backlog-refinement delta as ADDED for archive apply --- .../specs/backlog-refinement/spec.md | 179 ------------------ 1 file changed, 179 deletions(-) delete mode 100644 openspec/changes/backlog-scrum-01-standup-exceptions-first/specs/backlog-refinement/spec.md diff --git a/openspec/changes/backlog-scrum-01-standup-exceptions-first/specs/backlog-refinement/spec.md b/openspec/changes/backlog-scrum-01-standup-exceptions-first/specs/backlog-refinement/spec.md deleted file mode 100644 index cedd9c3c..00000000 --- a/openspec/changes/backlog-scrum-01-standup-exceptions-first/specs/backlog-refinement/spec.md +++ /dev/null @@ -1,179 +0,0 @@ -# Backlog refinement comment context (E1 scoped delta) - -## MODIFIED Requirements - -### Requirement: Export refine context includes comments without truncation by default - -The system SHALL include issue/work item comments in `specfact backlog refine --export-to-tmp` output so exported refinement context is complete by default. Comment content SHALL not be truncated unless explicitly requested by the user. - -**Rationale**: Refinement quality depends on full historical discussion context, especially for ADO work items where key decisions are often in comments. - -#### Scenario: Refine export contains full comments by default - -**Given**: The user runs `specfact backlog refine --export-to-tmp` for an adapter that supports comments - -**And**: A backlog item has comments in the provider - -**When**: No explicit comment-window options are provided - -**Then**: The exported markdown includes all comments for the item - -**And**: Comment text is preserved without truncation - -#### Scenario: Refine export includes copilot instruction block - -**Given**: The user runs `specfact backlog refine --export-to-tmp` - -**When**: The export file is generated - -**Then**: The file starts with a clear copilot instruction/prompt block before item entries - -**And**: The instruction block tells the user/copilot how to process item sections consistently - -**And**: The instruction block explicitly states that the refined artifact for import must omit the instruction block and contain only item sections - -#### Scenario: Refine export instructions match interactive refinement rules - -**Given**: The user runs `specfact backlog refine --export-to-tmp` - -**When**: Copilot reads the exported file - -**Then**: The exported instruction block includes the same refinement rules used in interactive mode (preserve scope, required-section completion, ambiguity notes, provider-aware formatting) - -**And**: Each item includes template guidance (target template, required sections, optional sections) so export processing can follow the same structure as interactive prompts - -### Requirement: Refine preview includes scoped comment context - -The system SHALL include issue/work item comments in `specfact backlog refine --preview` output with a scoped default to keep terminal output readable. - -**Rationale**: Refinement decisions depend on discussion history, but preview output must stay concise for day-to-day CLI usage. - -#### Scenario: Refine preview shows last two comments by default - -**Given**: The user runs `specfact backlog refine --preview` for an adapter that supports comments - -**And**: A backlog item has multiple comments - -**When**: No explicit comment-window options are provided - -**Then**: The preview shows the two newest comments for that item - -#### Scenario: First-comments limit on refine preview - -**Given**: The user runs `specfact backlog refine --preview --first-comments 5` - -**When**: A backlog item has more than five comments - -**Then**: The preview comment section contains only the first five comments for that item - -#### Scenario: Last-comments limit on refine preview - -**Given**: The user runs `specfact backlog refine --preview --last-comments 4` - -**When**: A backlog item has more than four comments - -**Then**: The preview comment section contains only the last four comments for that item - -#### Scenario: Preview shows comment-fetch progress for large batches - -**Given**: The user runs `specfact backlog refine --preview` for many backlog items - -**When**: The command fetches comments across adapters - -**Then**: The CLI shows progress feedback with item position (for example `Fetching issue n/m ...`) until comment fetch completes - -#### Scenario: Preview comment output is clearly scoped - -**Given**: The preview includes comments for an item - -**When**: The command renders preview detail - -**Then**: Each comment is rendered in a clearly scoped block-style container so users can distinguish comment boundaries from body/metadata - -#### Scenario: Preview indicates when no comments exist - -**Given**: The preview fetches comments for an item - -**When**: No comments are available for that issue/work item - -**Then**: The preview still shows a comments section with an explicit "no comments found" hint - -**Acceptance Criteria**: - -- Default refine preview includes the last two comments per item. -- Limits are optional and deterministic for preview output. -- If both first and last limits are provided, command fails with a clear validation error. -- `--export-to-tmp` always includes full comments, independent of preview comment-window options. -- Preview provides visible comment-fetch progress for multi-item runs. -- Preview comment rendering uses block-style formatting to make comment boundaries explicit. -- Preview explicitly indicates when an item has no comments. - -### Requirement: Refine write prompts include comment context - -The system SHALL include issue/work item comments in generated refinement prompts during `specfact backlog refine --write` so AI-assisted refinement reflects the latest discussion state. - -**Rationale**: Comment threads are the living source of truth; prompt context must include them to avoid refining against stale issue bodies. - -#### Scenario: Write-mode prompt includes full comments by default - -**Given**: The user runs `specfact backlog refine --write` - -**And**: The selected issue/work item has comments - -**When**: No explicit comment-window options are provided - -**Then**: The generated refinement prompt includes all available comments for that item - -#### Scenario: Write-mode prompt applies comment-window options - -**Given**: The user runs `specfact backlog refine --write --last-comments 5` - -**When**: The item has more than five comments - -**Then**: The generated refinement prompt includes only the configured comment window - -### Requirement: Refine supports first/last issue windowing - -The system SHALL support optional issue window controls for `specfact backlog refine` so users can process the first or last subset of currently filtered backlog items. - -**Rationale**: Teams often need a deterministic window over a larger result set (for example oldest/newest slice) without re-running broad filters manually. - -#### Scenario: First-issues limit on refine - -**Given**: The user runs `specfact backlog refine --first-issues 10` - -**When**: More than ten items match after filters/refinement eligibility rules - -**Then**: The command sorts items by issue/work-item number ascending and processes only the first ten (lowest IDs / oldest) - -#### Scenario: Last-issues limit on refine - -**Given**: The user runs `specfact backlog refine --last-issues 10` - -**When**: More than ten items match after filters/refinement eligibility rules - -**Then**: The command sorts items by issue/work-item number ascending and processes only the last ten (highest IDs / newest) - -#### Scenario: First/last issues flags are mutually exclusive - -**Given**: The user runs `specfact backlog refine --first-issues 5 --last-issues 5` - -**When**: The command validates options - -**Then**: The command exits with a clear validation error - -### Requirement: ADO comments are fetched from dedicated comments API - -For Azure DevOps, the system SHALL fetch work item comments via the dedicated comments endpoint and handle comment pagination to collect complete history. - -**Rationale**: ADO work item retrieval and comments retrieval are separate API resources and versions. - -#### Scenario: ADO comment pagination retrieves complete history - -**Given**: An ADO work item has comments spanning multiple comment pages - -**When**: The adapter fetches comments for refine or daily context - -**Then**: The adapter calls the ADO comments API and follows continuation tokens until complete - -**And**: All comments are returned in stable order for downstream rendering/export From cf9509d8f3b2526661da12fb2b9cffcb3fdb0fac Mon Sep 17 00:00:00 2001 From: Dominikus Nold Date: Wed, 11 Feb 2026 01:11:10 +0100 Subject: [PATCH 4/8] Archived completed change backlog-scrum-01 --- .../CHANGE_VALIDATION.md | 0 .../TDD_EVIDENCE.md | 0 .../proposal.md | 0 .../specs/backlog-refinement/spec.md | 179 +++++++++++++ .../specs/daily-standup/spec.md | 0 .../tasks.md | 2 +- openspec/specs/backlog-refinement/spec.md | 176 +++++++++++++ openspec/specs/daily-standup/spec.md | 243 ++++++++++++++++++ 8 files changed, 599 insertions(+), 1 deletion(-) rename openspec/changes/{backlog-scrum-01-standup-exceptions-first => archive/2026-02-11-backlog-scrum-01-standup-exceptions-first}/CHANGE_VALIDATION.md (100%) rename openspec/changes/{backlog-scrum-01-standup-exceptions-first => archive/2026-02-11-backlog-scrum-01-standup-exceptions-first}/TDD_EVIDENCE.md (100%) rename openspec/changes/{backlog-scrum-01-standup-exceptions-first => archive/2026-02-11-backlog-scrum-01-standup-exceptions-first}/proposal.md (100%) create mode 100644 openspec/changes/archive/2026-02-11-backlog-scrum-01-standup-exceptions-first/specs/backlog-refinement/spec.md rename openspec/changes/{backlog-scrum-01-standup-exceptions-first => archive/2026-02-11-backlog-scrum-01-standup-exceptions-first}/specs/daily-standup/spec.md (100%) rename openspec/changes/{backlog-scrum-01-standup-exceptions-first => archive/2026-02-11-backlog-scrum-01-standup-exceptions-first}/tasks.md (99%) diff --git a/openspec/changes/backlog-scrum-01-standup-exceptions-first/CHANGE_VALIDATION.md b/openspec/changes/archive/2026-02-11-backlog-scrum-01-standup-exceptions-first/CHANGE_VALIDATION.md similarity index 100% rename from openspec/changes/backlog-scrum-01-standup-exceptions-first/CHANGE_VALIDATION.md rename to openspec/changes/archive/2026-02-11-backlog-scrum-01-standup-exceptions-first/CHANGE_VALIDATION.md diff --git a/openspec/changes/backlog-scrum-01-standup-exceptions-first/TDD_EVIDENCE.md b/openspec/changes/archive/2026-02-11-backlog-scrum-01-standup-exceptions-first/TDD_EVIDENCE.md similarity index 100% rename from openspec/changes/backlog-scrum-01-standup-exceptions-first/TDD_EVIDENCE.md rename to openspec/changes/archive/2026-02-11-backlog-scrum-01-standup-exceptions-first/TDD_EVIDENCE.md diff --git a/openspec/changes/backlog-scrum-01-standup-exceptions-first/proposal.md b/openspec/changes/archive/2026-02-11-backlog-scrum-01-standup-exceptions-first/proposal.md similarity index 100% rename from openspec/changes/backlog-scrum-01-standup-exceptions-first/proposal.md rename to openspec/changes/archive/2026-02-11-backlog-scrum-01-standup-exceptions-first/proposal.md diff --git a/openspec/changes/archive/2026-02-11-backlog-scrum-01-standup-exceptions-first/specs/backlog-refinement/spec.md b/openspec/changes/archive/2026-02-11-backlog-scrum-01-standup-exceptions-first/specs/backlog-refinement/spec.md new file mode 100644 index 00000000..09a09c93 --- /dev/null +++ b/openspec/changes/archive/2026-02-11-backlog-scrum-01-standup-exceptions-first/specs/backlog-refinement/spec.md @@ -0,0 +1,179 @@ +# Backlog refinement comment context (E1 scoped delta) + +## ADDED Requirements + +### Requirement: Export refine context includes comments without truncation by default + +The system SHALL include issue/work item comments in `specfact backlog refine --export-to-tmp` output so exported refinement context is complete by default. Comment content SHALL not be truncated unless explicitly requested by the user. + +**Rationale**: Refinement quality depends on full historical discussion context, especially for ADO work items where key decisions are often in comments. + +#### Scenario: Refine export contains full comments by default + +**Given**: The user runs `specfact backlog refine --export-to-tmp` for an adapter that supports comments + +**And**: A backlog item has comments in the provider + +**When**: No explicit comment-window options are provided + +**Then**: The exported markdown includes all comments for the item + +**And**: Comment text is preserved without truncation + +#### Scenario: Refine export includes copilot instruction block + +**Given**: The user runs `specfact backlog refine --export-to-tmp` + +**When**: The export file is generated + +**Then**: The file starts with a clear copilot instruction/prompt block before item entries + +**And**: The instruction block tells the user/copilot how to process item sections consistently + +**And**: The instruction block explicitly states that the refined artifact for import must omit the instruction block and contain only item sections + +#### Scenario: Refine export instructions match interactive refinement rules + +**Given**: The user runs `specfact backlog refine --export-to-tmp` + +**When**: Copilot reads the exported file + +**Then**: The exported instruction block includes the same refinement rules used in interactive mode (preserve scope, required-section completion, ambiguity notes, provider-aware formatting) + +**And**: Each item includes template guidance (target template, required sections, optional sections) so export processing can follow the same structure as interactive prompts + +### Requirement: Refine preview includes scoped comment context + +The system SHALL include issue/work item comments in `specfact backlog refine --preview` output with a scoped default to keep terminal output readable. + +**Rationale**: Refinement decisions depend on discussion history, but preview output must stay concise for day-to-day CLI usage. + +#### Scenario: Refine preview shows last two comments by default + +**Given**: The user runs `specfact backlog refine --preview` for an adapter that supports comments + +**And**: A backlog item has multiple comments + +**When**: No explicit comment-window options are provided + +**Then**: The preview shows the two newest comments for that item + +#### Scenario: First-comments limit on refine preview + +**Given**: The user runs `specfact backlog refine --preview --first-comments 5` + +**When**: A backlog item has more than five comments + +**Then**: The preview comment section contains only the first five comments for that item + +#### Scenario: Last-comments limit on refine preview + +**Given**: The user runs `specfact backlog refine --preview --last-comments 4` + +**When**: A backlog item has more than four comments + +**Then**: The preview comment section contains only the last four comments for that item + +#### Scenario: Preview shows comment-fetch progress for large batches + +**Given**: The user runs `specfact backlog refine --preview` for many backlog items + +**When**: The command fetches comments across adapters + +**Then**: The CLI shows progress feedback with item position (for example `Fetching issue n/m ...`) until comment fetch completes + +#### Scenario: Preview comment output is clearly scoped + +**Given**: The preview includes comments for an item + +**When**: The command renders preview detail + +**Then**: Each comment is rendered in a clearly scoped block-style container so users can distinguish comment boundaries from body/metadata + +#### Scenario: Preview indicates when no comments exist + +**Given**: The preview fetches comments for an item + +**When**: No comments are available for that issue/work item + +**Then**: The preview still shows a comments section with an explicit "no comments found" hint + +**Acceptance Criteria**: + +- Default refine preview includes the last two comments per item. +- Limits are optional and deterministic for preview output. +- If both first and last limits are provided, command fails with a clear validation error. +- `--export-to-tmp` always includes full comments, independent of preview comment-window options. +- Preview provides visible comment-fetch progress for multi-item runs. +- Preview comment rendering uses block-style formatting to make comment boundaries explicit. +- Preview explicitly indicates when an item has no comments. + +### Requirement: Refine write prompts include comment context + +The system SHALL include issue/work item comments in generated refinement prompts during `specfact backlog refine --write` so AI-assisted refinement reflects the latest discussion state. + +**Rationale**: Comment threads are the living source of truth; prompt context must include them to avoid refining against stale issue bodies. + +#### Scenario: Write-mode prompt includes full comments by default + +**Given**: The user runs `specfact backlog refine --write` + +**And**: The selected issue/work item has comments + +**When**: No explicit comment-window options are provided + +**Then**: The generated refinement prompt includes all available comments for that item + +#### Scenario: Write-mode prompt applies comment-window options + +**Given**: The user runs `specfact backlog refine --write --last-comments 5` + +**When**: The item has more than five comments + +**Then**: The generated refinement prompt includes only the configured comment window + +### Requirement: Refine supports first/last issue windowing + +The system SHALL support optional issue window controls for `specfact backlog refine` so users can process the first or last subset of currently filtered backlog items. + +**Rationale**: Teams often need a deterministic window over a larger result set (for example oldest/newest slice) without re-running broad filters manually. + +#### Scenario: First-issues limit on refine + +**Given**: The user runs `specfact backlog refine --first-issues 10` + +**When**: More than ten items match after filters/refinement eligibility rules + +**Then**: The command sorts items by issue/work-item number ascending and processes only the first ten (lowest IDs / oldest) + +#### Scenario: Last-issues limit on refine + +**Given**: The user runs `specfact backlog refine --last-issues 10` + +**When**: More than ten items match after filters/refinement eligibility rules + +**Then**: The command sorts items by issue/work-item number ascending and processes only the last ten (highest IDs / newest) + +#### Scenario: First/last issues flags are mutually exclusive + +**Given**: The user runs `specfact backlog refine --first-issues 5 --last-issues 5` + +**When**: The command validates options + +**Then**: The command exits with a clear validation error + +### Requirement: ADO comments are fetched from dedicated comments API + +For Azure DevOps, the system SHALL fetch work item comments via the dedicated comments endpoint and handle comment pagination to collect complete history. + +**Rationale**: ADO work item retrieval and comments retrieval are separate API resources and versions. + +#### Scenario: ADO comment pagination retrieves complete history + +**Given**: An ADO work item has comments spanning multiple comment pages + +**When**: The adapter fetches comments for refine or daily context + +**Then**: The adapter calls the ADO comments API and follows continuation tokens until complete + +**And**: All comments are returned in stable order for downstream rendering/export diff --git a/openspec/changes/backlog-scrum-01-standup-exceptions-first/specs/daily-standup/spec.md b/openspec/changes/archive/2026-02-11-backlog-scrum-01-standup-exceptions-first/specs/daily-standup/spec.md similarity index 100% rename from openspec/changes/backlog-scrum-01-standup-exceptions-first/specs/daily-standup/spec.md rename to openspec/changes/archive/2026-02-11-backlog-scrum-01-standup-exceptions-first/specs/daily-standup/spec.md diff --git a/openspec/changes/backlog-scrum-01-standup-exceptions-first/tasks.md b/openspec/changes/archive/2026-02-11-backlog-scrum-01-standup-exceptions-first/tasks.md similarity index 99% rename from openspec/changes/backlog-scrum-01-standup-exceptions-first/tasks.md rename to openspec/changes/archive/2026-02-11-backlog-scrum-01-standup-exceptions-first/tasks.md index 0569ed41..2681604b 100644 --- a/openspec/changes/backlog-scrum-01-standup-exceptions-first/tasks.md +++ b/openspec/changes/archive/2026-02-11-backlog-scrum-01-standup-exceptions-first/tasks.md @@ -12,7 +12,7 @@ Per `openspec/config.yaml`, **tests before code** apply to any task that adds or ## 1. Create git branch from dev (linked to issue #175) -- [ ] 1.1 Ensure we're on dev and up to date: `git checkout dev && git pull origin dev` +- [x] 1.1 Ensure we're on dev and up to date: `git checkout dev && git pull origin dev` - [x] 1.2 Create branch linked to #175: `gh issue develop 175 --repo nold-ai/specfact-cli --name feature/backlog-scrum-01-standup-exceptions-first --checkout` (or `git checkout -b feature/backlog-scrum-01-standup-exceptions-first` if no gh) - [x] 1.3 Verify branch: `git branch --show-current` diff --git a/openspec/specs/backlog-refinement/spec.md b/openspec/specs/backlog-refinement/spec.md index 32051569..9a680cc0 100644 --- a/openspec/specs/backlog-refinement/spec.md +++ b/openspec/specs/backlog-refinement/spec.md @@ -418,3 +418,179 @@ The system SHALL support `--id ISSUE_ID` to refine only the backlog item with th - **WHEN** no item with id 999 is in the fetched set - **THEN** the system prints a clear error (e.g. "No backlog item with id 999 found") and exits with non-zero status +### Requirement: Export refine context includes comments without truncation by default + +The system SHALL include issue/work item comments in `specfact backlog refine --export-to-tmp` output so exported refinement context is complete by default. Comment content SHALL not be truncated unless explicitly requested by the user. + +**Rationale**: Refinement quality depends on full historical discussion context, especially for ADO work items where key decisions are often in comments. + +#### Scenario: Refine export contains full comments by default + +**Given**: The user runs `specfact backlog refine --export-to-tmp` for an adapter that supports comments + +**And**: A backlog item has comments in the provider + +**When**: No explicit comment-window options are provided + +**Then**: The exported markdown includes all comments for the item + +**And**: Comment text is preserved without truncation + +#### Scenario: Refine export includes copilot instruction block + +**Given**: The user runs `specfact backlog refine --export-to-tmp` + +**When**: The export file is generated + +**Then**: The file starts with a clear copilot instruction/prompt block before item entries + +**And**: The instruction block tells the user/copilot how to process item sections consistently + +**And**: The instruction block explicitly states that the refined artifact for import must omit the instruction block and contain only item sections + +#### Scenario: Refine export instructions match interactive refinement rules + +**Given**: The user runs `specfact backlog refine --export-to-tmp` + +**When**: Copilot reads the exported file + +**Then**: The exported instruction block includes the same refinement rules used in interactive mode (preserve scope, required-section completion, ambiguity notes, provider-aware formatting) + +**And**: Each item includes template guidance (target template, required sections, optional sections) so export processing can follow the same structure as interactive prompts + +### Requirement: Refine preview includes scoped comment context + +The system SHALL include issue/work item comments in `specfact backlog refine --preview` output with a scoped default to keep terminal output readable. + +**Rationale**: Refinement decisions depend on discussion history, but preview output must stay concise for day-to-day CLI usage. + +#### Scenario: Refine preview shows last two comments by default + +**Given**: The user runs `specfact backlog refine --preview` for an adapter that supports comments + +**And**: A backlog item has multiple comments + +**When**: No explicit comment-window options are provided + +**Then**: The preview shows the two newest comments for that item + +#### Scenario: First-comments limit on refine preview + +**Given**: The user runs `specfact backlog refine --preview --first-comments 5` + +**When**: A backlog item has more than five comments + +**Then**: The preview comment section contains only the first five comments for that item + +#### Scenario: Last-comments limit on refine preview + +**Given**: The user runs `specfact backlog refine --preview --last-comments 4` + +**When**: A backlog item has more than four comments + +**Then**: The preview comment section contains only the last four comments for that item + +#### Scenario: Preview shows comment-fetch progress for large batches + +**Given**: The user runs `specfact backlog refine --preview` for many backlog items + +**When**: The command fetches comments across adapters + +**Then**: The CLI shows progress feedback with item position (for example `Fetching issue n/m ...`) until comment fetch completes + +#### Scenario: Preview comment output is clearly scoped + +**Given**: The preview includes comments for an item + +**When**: The command renders preview detail + +**Then**: Each comment is rendered in a clearly scoped block-style container so users can distinguish comment boundaries from body/metadata + +#### Scenario: Preview indicates when no comments exist + +**Given**: The preview fetches comments for an item + +**When**: No comments are available for that issue/work item + +**Then**: The preview still shows a comments section with an explicit "no comments found" hint + +**Acceptance Criteria**: + +- Default refine preview includes the last two comments per item. +- Limits are optional and deterministic for preview output. +- If both first and last limits are provided, command fails with a clear validation error. +- `--export-to-tmp` always includes full comments, independent of preview comment-window options. +- Preview provides visible comment-fetch progress for multi-item runs. +- Preview comment rendering uses block-style formatting to make comment boundaries explicit. +- Preview explicitly indicates when an item has no comments. + +### Requirement: Refine write prompts include comment context + +The system SHALL include issue/work item comments in generated refinement prompts during `specfact backlog refine --write` so AI-assisted refinement reflects the latest discussion state. + +**Rationale**: Comment threads are the living source of truth; prompt context must include them to avoid refining against stale issue bodies. + +#### Scenario: Write-mode prompt includes full comments by default + +**Given**: The user runs `specfact backlog refine --write` + +**And**: The selected issue/work item has comments + +**When**: No explicit comment-window options are provided + +**Then**: The generated refinement prompt includes all available comments for that item + +#### Scenario: Write-mode prompt applies comment-window options + +**Given**: The user runs `specfact backlog refine --write --last-comments 5` + +**When**: The item has more than five comments + +**Then**: The generated refinement prompt includes only the configured comment window + +### Requirement: Refine supports first/last issue windowing + +The system SHALL support optional issue window controls for `specfact backlog refine` so users can process the first or last subset of currently filtered backlog items. + +**Rationale**: Teams often need a deterministic window over a larger result set (for example oldest/newest slice) without re-running broad filters manually. + +#### Scenario: First-issues limit on refine + +**Given**: The user runs `specfact backlog refine --first-issues 10` + +**When**: More than ten items match after filters/refinement eligibility rules + +**Then**: The command sorts items by issue/work-item number ascending and processes only the first ten (lowest IDs / oldest) + +#### Scenario: Last-issues limit on refine + +**Given**: The user runs `specfact backlog refine --last-issues 10` + +**When**: More than ten items match after filters/refinement eligibility rules + +**Then**: The command sorts items by issue/work-item number ascending and processes only the last ten (highest IDs / newest) + +#### Scenario: First/last issues flags are mutually exclusive + +**Given**: The user runs `specfact backlog refine --first-issues 5 --last-issues 5` + +**When**: The command validates options + +**Then**: The command exits with a clear validation error + +### Requirement: ADO comments are fetched from dedicated comments API + +For Azure DevOps, the system SHALL fetch work item comments via the dedicated comments endpoint and handle comment pagination to collect complete history. + +**Rationale**: ADO work item retrieval and comments retrieval are separate API resources and versions. + +#### Scenario: ADO comment pagination retrieves complete history + +**Given**: An ADO work item has comments spanning multiple comment pages + +**When**: The adapter fetches comments for refine or daily context + +**Then**: The adapter calls the ADO comments API and follows continuation tokens until complete + +**And**: All comments are returned in stable order for downstream rendering/export + diff --git a/openspec/specs/daily-standup/spec.md b/openspec/specs/daily-standup/spec.md index 7eac98d0..7fff097e 100644 --- a/openspec/specs/daily-standup/spec.md +++ b/openspec/specs/daily-standup/spec.md @@ -327,3 +327,246 @@ The system SHALL support storing project-level backlog context (org, project per - Config is loaded from `SPECFACT_CONFIG_DIR` or `.specfact/` in cwd; first found wins - When org/repo or org/project are still missing after CLI, env, and file, the system MAY infer from `git remote get-url origin` when run from a clone (GitHub or Azure DevOps URL formats); supported ADO formats: HTTPS, SSH with keys (`git@ssh.dev.azure.com:v3/...`), SSH without keys (`user@dev.azure.com:v3/...`). If inference fails or not in a clone, the system SHALL report a clear error with guidance (CLI, env, or `.specfact/backlog.yaml`). +### Requirement: Exceptions-first section order + +The system SHALL order `specfact backlog daily` output sections by default as: (1) blockers and dependency-critical items, (2) policy failures (DoR/DoD/flow when Policy Engine available), (3) aging items / stalled work (when data exists), (4) normal status. + +**Rationale**: Plan E1—teams see risks first. + +#### Scenario: Standup output shows exceptions first + +**Given**: Policy Engine (unify-policies-engine) and/or aging/flow data are available + +**When**: The user runs `specfact backlog daily` (no override) + +**Then**: The output includes an "Exceptions" section by default (blockers, policy failures, aging/stalled when available) before normal status + +**Acceptance Criteria**: + +- `backlog daily` includes an "Exceptions" section by default when exception data exists. + +### Requirement: Mode switch (scrum|kanban|safe) + +The system SHALL support `--mode scrum|kanban|safe` to change defaults for filters and sections (e.g. Kanban: flow columns; SAFe: PI context). + +**Rationale**: Plan E1—ceremony-native defaults per framework. + +#### Scenario: Standup with mode + +**Given**: SpecFact CLI and backlog adapter + +**When**: The user runs `specfact backlog daily --mode kanban` + +**Then**: Default filters and section behavior align with Kanban (e.g. flow-focused); when `--mode safe`, PI context when available + +**Acceptance Criteria**: + +- `--mode scrum|kanban|safe` changes defaults; existing backlog daily behavior otherwise unchanged. + +### Requirement: Patch integration for standup notes + +The system SHALL integrate with patch mode (patch-mode-preview-apply) to propose standup notes or missing fields as patch when `--patch` is used. + +**Rationale**: Plan E1—actionable standup output. + +#### Scenario: Standup with patch proposal + +**Given**: Patch mode is available + +**When**: The user runs `specfact backlog daily --patch` + +**Then**: The command may emit a patch proposal (standup notes or missing fields) for user review/apply + +**Acceptance Criteria**: + +- When patch mode is available and `--patch` is set, standup can propose patch; no silent writes. + +### Requirement: Interactive standup comment display is scoped + +The system SHALL avoid dumping full comment history in interactive standup detail views. When comments exist, it SHALL show only the latest comment by default and provide a clear hint that full comments are available via export options. + +**Rationale**: Interactive review must stay readable while still giving users access to complete context in export workflows. + +#### Scenario: Interactive view shows latest comment and export hint + +**Given**: The selected backlog item has multiple comments (e.g., from ADO work item discussion) + +**When**: The user runs `specfact backlog daily --interactive` and opens the item detail view + +**Then**: The detail view shows only the latest comment text + +**And**: The detail view shows how many additional older comments exist + +**And**: The detail view includes a hint to use export-to-file options to retrieve all comments + +**Acceptance Criteria**: + +- Interactive detail output does not render all comments inline when more than one exists. +- The output explicitly guides users to export for full comment context. + +#### Scenario: Interactive comment-window override is honored + +**Given**: The user runs `specfact backlog daily --interactive --first-comments N` or `--last-comments N` + +**When**: The selected backlog item has more comments than N + +**Then**: The interactive detail view renders the selected window of N comments (instead of latest-only default) + +**And**: The detail view clearly indicates how many additional comments were omitted by the window + +#### Scenario: Interactive comments use scoped panel-style blocks + +**Given**: The user runs `specfact backlog daily --interactive` + +**When**: Comment context is rendered for a selected item + +**Then**: Comments are rendered in clear scoped blocks (panel-style), separate from the story detail body, for readability + +### Requirement: Comment window controls for standup exports and summarize prompts + +The system SHALL support optional comment-window controls `--first-comments N` and `--last-comments N` for `specfact backlog daily` exports/prompts that include comments. By default, no comment truncation is applied. + +**Rationale**: Teams need complete context by default, but must be able to constrain prompt size when needed. + +#### Scenario: Export/summarize uses all comments by default + +**Given**: The user runs `specfact backlog daily --comments` with `--copilot-export`, `--summarize`, or `--summarize-to` + +**When**: No `--first-comments` or `--last-comments` option is provided + +**Then**: The command includes all available comments per item (no truncation) + +#### Scenario: First-comments limit is applied + +**Given**: The user runs `specfact backlog daily --comments --first-comments 3 --copilot-export ` + +**When**: An item has more than three comments + +**Then**: The output includes only the first three comments for that item + +#### Scenario: Last-comments limit is applied + +**Given**: The user runs `specfact backlog daily --comments --last-comments 2 --summarize` + +**When**: An item has more than two comments + +**Then**: The output includes only the last two comments for that item + +**Acceptance Criteria**: + +- Default behavior is full comment inclusion. +- First/last limits are optional and deterministic. +- If both are provided, command fails with a clear validation error. + +### Requirement: Assignee visibility and GitHub `me` filter semantics + +The system SHALL show assignment context directly in `specfact backlog daily` table output and SHALL handle GitHub assignee filter value `me` (or `@me`) as provider-relative current-user semantics rather than a literal username string. + +**Rationale**: Daily standup output must make ownership explicit, and GitHub users commonly use `me` as shorthand in issue filtering. + +#### Scenario: Daily table includes assignee column + +**Given**: The user runs `specfact backlog daily` and at least one item has assignees + +**When**: The standup table is rendered + +**Then**: The table includes an `Assignee` column + +**And**: Each row shows comma-separated assignees or `—` when unassigned + +#### Scenario: GitHub `--assignee me` uses provider semantics + +**Given**: The adapter is GitHub and the user passes `--assignee me` (or `--assignee @me`) + +**When**: The command fetches and post-filters backlog items + +**Then**: The GitHub query uses provider-relative current-user qualifier semantics + +**And**: Local post-fetch filtering does not treat `me` as a literal assignee login + +**Acceptance Criteria**: + +- `backlog daily` output includes an assignee column. +- GitHub `me`/`@me` filtering is deterministic and does not get incorrectly narrowed by literal local matching. + +### Requirement: Issue window controls for backlog daily + +The system SHALL support optional issue-window controls `--first-issues N` and `--last-issues N` on `specfact backlog daily` with deterministic numeric issue ID ordering semantics matching `specfact backlog refine`. + +**Rationale**: Users need harmonized backlog command ergonomics to focus on oldest/newest slices without changing workflows between subcommands. + +#### Scenario: Daily command supports first-issues window + +**Given**: More than N items match `specfact backlog daily` filters + +**When**: The user runs `specfact backlog daily ... --first-issues N` + +**Then**: Only the lowest N numeric issue/work-item IDs are included in output + +#### Scenario: Daily command supports last-issues window + +**Given**: More than N items match `specfact backlog daily` filters + +**When**: The user runs `specfact backlog daily ... --last-issues N` + +**Then**: Only the highest N numeric issue/work-item IDs are included in output + +#### Scenario: Daily command rejects conflicting issue windows + +**Given**: The user passes both `--first-issues` and `--last-issues` + +**When**: The command validates CLI inputs + +**Then**: The command exits with a clear validation error + +**Acceptance Criteria**: + +- `backlog daily` has `--first-issues` and `--last-issues` options. +- Only one issue window option can be used at once. +- Ordering semantics align with refine (`first`=lowest numeric IDs, `last`=highest numeric IDs). +- Issue windowing is applied over the full filtered candidate set (not a pre-truncated default-limit subset). + +### Requirement: Global filter parity across backlog commands + +The system SHALL provide consistent global backlog filtering flags across `specfact backlog daily` and `specfact backlog refine` for shared backlog-item selection semantics. + +#### Scenario: Daily supports shared global filter flags + +**Given**: The user runs `specfact backlog daily` + +**When**: They use global filters available on refine + +**Then**: Daily accepts and applies `--search`, `--release`, and `--id` consistently with refine semantics + +**Acceptance Criteria**: + +- `backlog daily` accepts `--search`, `--release`, and `--id`. +- `--search` and `--release` are applied in fetch/filter flow. +- `--id` narrows to the exact backlog item ID after other filters; when not found, command exits with a clear error. + +### Requirement: Interactive standup can post comment for selected issue + +The system SHALL allow posting standup comments directly from the interactive review flow for the currently selected item. + +**Rationale**: Teams review one story at a time during daily standup; posting from the selected item avoids context switching and reduces mistakes. + +#### Scenario: Post standup comment from selected story in interactive mode + +**Given**: The user runs `specfact backlog daily --interactive` + +**And**: The adapter supports `add_comment` + +**When**: The user opens a story and chooses the interactive post action + +**Then**: The CLI prompts for standup fields (yesterday/today/blockers) + +**And**: The CLI posts the comment to that selected story (not implicitly to another item) + +**And**: The CLI shows a clear success or failure message + +**Acceptance Criteria**: + +- Interactive navigation includes a post action for the selected story. +- Empty post input is rejected with a clear message. +- Posting uses existing standup comment format and adapter capability checks. + From 136188933a0d66850bc3ed2f56d8a259b7988e11 Mon Sep 17 00:00:00 2001 From: Dominikus Nold Date: Wed, 11 Feb 2026 01:39:16 +0100 Subject: [PATCH 5/8] fix(backlog): make map-fields exit cleanly under CliRunner --- src/specfact_cli/modules/backlog/src/commands.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/specfact_cli/modules/backlog/src/commands.py b/src/specfact_cli/modules/backlog/src/commands.py index 4f237526..9259d1fe 100644 --- a/src/specfact_cli/modules/backlog/src/commands.py +++ b/src/specfact_cli/modules/backlog/src/commands.py @@ -3236,9 +3236,6 @@ def map_fields( """ import base64 import re - import sys - - import questionary # type: ignore[reportMissingImports] import requests from specfact_cli.backlog.mappers.template_config import FieldMappingConfig @@ -3487,6 +3484,14 @@ def _find_potential_match(canonical_field: str, available_fields: list[dict[str, combined_mapping.update(existing_mapping) # Interactive mapping + try: + import questionary # type: ignore[reportMissingImports] + except ImportError: + console.print( + "[red]Interactive field mapping requires the 'questionary' package. Install with: pip install questionary[/red]" + ) + raise typer.Exit(1) from None + console.print() console.print(Panel("[bold cyan]Interactive Field Mapping[/bold cyan]", border_style="cyan")) console.print("[dim]Use ↑↓ to navigate, ⏎ to select. Map ADO fields to canonical field names.[/dim]") @@ -3546,7 +3551,7 @@ def _find_potential_match(canonical_field: str, available_fields: list[dict[str, selected_display = "" except KeyboardInterrupt: console.print("\n[yellow]Selection cancelled.[/yellow]") - sys.exit(0) + raise typer.Exit(0) from None # Convert display name back to reference name if selected_display and selected_display != "" and selected_display in field_choices_display: From 07c4ae391028a9b5e793b9882dee39c3d7312604 Mon Sep 17 00:00:00 2001 From: Dominikus Nold Date: Wed, 11 Feb 2026 01:39:34 +0100 Subject: [PATCH 6/8] Fix format --- src/specfact_cli/modules/backlog/src/commands.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/specfact_cli/modules/backlog/src/commands.py b/src/specfact_cli/modules/backlog/src/commands.py index 9259d1fe..a5b3d6d5 100644 --- a/src/specfact_cli/modules/backlog/src/commands.py +++ b/src/specfact_cli/modules/backlog/src/commands.py @@ -3236,6 +3236,7 @@ def map_fields( """ import base64 import re + import requests from specfact_cli.backlog.mappers.template_config import FieldMappingConfig From 4e91adaa63cf765480630a751d8d1d8f3fbd443e Mon Sep 17 00:00:00 2001 From: Dominikus Nold Date: Wed, 11 Feb 2026 01:50:45 +0100 Subject: [PATCH 7/8] fix(backlog): stabilize map-fields tests in non-interactive env --- src/specfact_cli/modules/backlog/src/commands.py | 2 +- tests/unit/commands/test_backlog_commands.py | 16 ++++++++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/specfact_cli/modules/backlog/src/commands.py b/src/specfact_cli/modules/backlog/src/commands.py index a5b3d6d5..65ac51b9 100644 --- a/src/specfact_cli/modules/backlog/src/commands.py +++ b/src/specfact_cli/modules/backlog/src/commands.py @@ -3550,7 +3550,7 @@ def _find_potential_match(canonical_field: str, available_fields: list[dict[str, ).ask() if selected_display is None: selected_display = "" - except KeyboardInterrupt: + except (KeyboardInterrupt, EOFError): console.print("\n[yellow]Selection cancelled.[/yellow]") raise typer.Exit(0) from None diff --git a/tests/unit/commands/test_backlog_commands.py b/tests/unit/commands/test_backlog_commands.py index 9af82234..71905a6e 100644 --- a/tests/unit/commands/test_backlog_commands.py +++ b/tests/unit/commands/test_backlog_commands.py @@ -102,10 +102,15 @@ class TestInteractiveMappingCommand: """Tests for interactive template mapping command.""" @patch("requests.get") + @patch("questionary.select") @patch("rich.prompt.Prompt.ask") @patch("rich.prompt.Confirm.ask") def test_map_fields_fetches_ado_fields( - self, mock_confirm: MagicMock, mock_prompt: MagicMock, mock_get: MagicMock + self, + mock_confirm: MagicMock, + mock_prompt: MagicMock, + mock_select: MagicMock, + mock_get: MagicMock, ) -> None: """Test that map-fields command fetches fields from ADO API.""" # Mock ADO API response @@ -130,6 +135,7 @@ def test_map_fields_fetches_ado_fields( # Mock rich.prompt.Prompt to avoid interactive input mock_prompt.return_value = "" mock_confirm.return_value = False + mock_select.return_value.ask.return_value = None runner.invoke( app, @@ -153,10 +159,15 @@ def test_map_fields_fetches_ado_fields( assert "_apis/wit/fields" in call_args[0][0] @patch("requests.get") + @patch("questionary.select") @patch("rich.prompt.Prompt.ask") @patch("rich.prompt.Confirm.ask") def test_map_fields_filters_system_fields( - self, mock_confirm: MagicMock, mock_prompt: MagicMock, mock_get: MagicMock + self, + mock_confirm: MagicMock, + mock_prompt: MagicMock, + mock_select: MagicMock, + mock_get: MagicMock, ) -> None: """Test that map-fields command filters out system-only fields.""" # Mock ADO API response with system and user fields @@ -187,6 +198,7 @@ def test_map_fields_filters_system_fields( # Mock rich.prompt.Prompt to avoid interactive input mock_prompt.return_value = "" mock_confirm.return_value = False + mock_select.return_value.ask.return_value = None runner.invoke( app, From 62ee5946fc24b633fc15233d5d5cfe0182efc630 Mon Sep 17 00:00:00 2001 From: Dominikus Nold Date: Wed, 11 Feb 2026 01:56:42 +0100 Subject: [PATCH 8/8] docs(agents): enforce signed-commit handoff flow --- AGENTS.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 18cc03f3..466fa4e0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -174,6 +174,15 @@ Keep `CHANGELOG.md` updated with every meaningful change. Update it in the same Follow Conventional Commits: `feat:`, `fix:`, `docs:`, `test:`, `refactor:`. +#### Commit Signing (GPG) + +- This repository may enforce signed commits (`commit.gpgsign=true`). +- If an agent-run commit fails with `gpg failed to sign the data` in a non-interactive shell, the agent MUST: + 1. Stage all intended files. + 2. Provide the exact `git commit -S -m ""` command for the user to run locally. + 3. Continue with push/PR steps after the user confirms the signed commit exists. +- Agents MUST NOT bypass signing with `--no-gpg-sign` unless the user explicitly requests that override. + ### Documentation Keep docs current with every code change that affects user-facing behaviour.