Skip to content

Commit 8f13429

Browse files
djm81cursoragentgithub-code-quality[bot]
authored
Release: dev → main (post-#181) (#195)
* perf: optimize startup performance with metadata tracking and update command (#142) * feat: implement backlog field mapping and refinement improvements - Add FieldMapper abstract base class with canonical field names - Implement GitHubFieldMapper and AdoFieldMapper - Add custom field mapping support with YAML templates - Add field validation in refinement (story_points, business_value, priority) - Add comprehensive unit and integration tests (42 tests) - Add custom field mapping documentation - Fix custom_field_mapping parameter connection - Add early validation for custom mapping files Implements OpenSpec change: improve-backlog-field-mapping-and-refinement * perf: optimize startup performance with metadata tracking and update command - Add metadata management module for tracking version and check timestamps - Optimize startup checks to only run when needed: - Template checks: Only after version changes detected - Version checks: Limited to once per day (24h threshold) - Add --skip-checks flag for CI/CD environments - Add new 'specfact update' command for manual update checking and installation - Add comprehensive unit and integration tests (35 tests, all passing) - Update startup_checks to use metadata for conditional execution - Ensure backward compatibility (first-time users still get all checks) Performance Impact: - Startup time: Reduced from several seconds to < 1-2 seconds - Network requests: Reduced from every startup to once per day - File system operations: Reduced from every startup to only after version changes Fixes #140 Implements OpenSpec change: optimize-startup-performance * feat: request offline_access scope for Azure DevOps refresh tokens - Add offline_access scope to Azure DevOps OAuth requests - Refresh tokens now last 90 days (vs 1 hour for access tokens) - Automatic token refresh via persistent cache (no re-authentication needed) - Update documentation to reflect 90-day refresh token lifetime This addresses the issue where tokens were expiring too quickly. Refresh tokens obtained via offline_access scope enable automatic token renewal for 90 days without user interaction. Fixes token lifetime limitation issue * feat: improve CLI UX with banner control and upgrade command - Change banner to hidden by default, shown on first run or with --banner flag - Add simple version line (SpecFact CLI - vXYZ) for regular use - Rename 'update' command to 'upgrade' to avoid confusion - Update documentation for new banner behavior and upgrade command - Update startup checks message to reference 'specfact upgrade' * fix: suppress version line in test mode and fix field mapping issues - Suppress version line output in test mode and for help/version commands to prevent test failures - Fix ADO custom field mapping to honor --custom-field-mapping on writeback - Fix GitHub issue body updates to prevent duplicate sections - Ensure proper type handling for story points and business value calculations * Fix failed tests * chore: bump version to 0.26.7 and update changelog - Fixed adapter token validation tests (ADO and GitHub) - Resolved test timeout issues (commit history, AST parsing, Semgrep) - Improved test file discovery to exclude virtual environments - Added file size limits for AST parsing to prevent timeouts --------- Co-authored-by: Dominikus Nold <djm81@users.noreply.github.com> * fix: add missing ADO field mappings and assignee display (#145) * fix: add missing ADO field mappings and assignee display - Add Microsoft.VSTS.Common.AcceptanceCriteria to default field mappings - Update AdoFieldMapper to support multiple field name alternatives - Fix assignee extraction to include displayName, uniqueName, and mail - Add assignee display in preview output - Add interactive template mapping command (specfact backlog map-fields) - Update specfact init to copy backlog field mapping templates - Extend documentation with step-by-step guides Fixes #144 * test: add unit tests for ADO field mapping and assignee fixes - Add tests for Microsoft.VSTS.Common.AcceptanceCriteria field extraction - Add tests for multiple field name alternatives - Add tests for assignee extraction with displayName, uniqueName, mail - Add tests for assignee filtering with multiple identifiers - Add tests for assignee display in preview output - Add tests for interactive mapping command - Add tests for template copying in init command - Update existing tests to match new assignee extraction behavior * docs: update init command docstring to mention template copying * docs: update documentation for ADO field mapping and interactive mapping features - Update authentication guide with ADO token resolution priority - Update custom field mapping guide with interactive mapping details - Update backlog refinement guide with progress indicators and required field display - Update Azure DevOps adapter guide with field mapping improvements - Update command reference with map-fields command documentation - Update troubleshooting guide with ADO-specific issues - Update README files with new features - Update getting started guide with template initialization Co-authored-by: Cursor <cursoragent@cursor.com> * fix: address review findings for ADO field mapping - Prefer System.* fields over Microsoft.VSTS.Common.* when writing updates (fixes issue where PATCH requests could fail for Scrum templates) - Preserve existing work_item_type_mappings when saving field mappings (prevents silent erasure of custom work item type mappings) Fixes review comments: - P1: Prefer System.AcceptanceCriteria when writing updates - P2: Preserve existing work_item_type_mappings on save Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Dominikus Nold <djm81@users.noreply.github.com> Co-authored-by: Cursor <cursoragent@cursor.com> * fix: mitigate code scanning vulnerabilities (#148) * fix: mitigate code scanning vulnerabilities - Fix ReDoS vulnerability in github_mapper.py by replacing regex with line-by-line processing - Fix incomplete URL sanitization in github.py, bridge_sync.py, and ado.py using proper URL parsing - Add explicit permissions blocks to 7 GitHub Actions jobs following least-privilege model Resolves all 13 code scanning findings: - 1 ReDoS error - 5 URL sanitization warnings - 7 missing workflow permissions warnings Fixes #147 Co-authored-by: Cursor <cursoragent@cursor.com> * fix: accept GitHub SSH host aliases in repo detection Accept ssh.github.com (port 443) in addition to github.com when detecting GitHub repositories via SSH remotes. This ensures repositories using git@ssh.github.com:owner/repo.git are properly detected as GitHub repos. Addresses review feedback on PR #148 Co-authored-by: Cursor <cursoragent@cursor.com> * fix: prevent async cleanup issues in test mode Remove manual Live display cleanup that could cause EOFError. The _safe_progress_display function already handles test mode by skipping progress display, so direct save path is sufficient. Fixes test_unlock_section failure with EOFError/ValueError. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Dominikus Nold <djm81@users.noreply.github.com> Co-authored-by: Cursor <cursoragent@cursor.com> * fix: detect GitHub remotes using ssh:// and git:// URLs Extend URL pattern matching to support ssh://git@github.com/owner/repo.git and git://github.com/owner/repo.git formats in addition to existing https?:// and scp-style git@host:path URLs. This fixes a regression where these valid GitHub URL formats were not detected, causing detect() to return false for repos using these schemes. Addresses review feedback on PR #149 Co-authored-by: Cursor <cursoragent@cursor.com> * chore: bump version to 0.26.9 and update changelog - Update version from 0.26.8 to 0.26.9 - Add changelog entry for GitHub remote detection fix and code scanning fixes Co-authored-by: Cursor <cursoragent@cursor.com> * fix: compare GitHub SSH hostnames case-insensitively Lowercase host_part before comparison to handle mixed-case hostnames like git@GitHub.com:org/repo.git. This restores the case-insensitive behavior from the previous config_content.lower() check and prevents regression where valid GitHub repos with mixed-case hostnames would not be detected. Addresses review feedback on PR #150 Co-authored-by: Cursor <cursoragent@cursor.com> * Add openspec and workflow commands for transparency * Add specs from openspec * Remove aisp change which wasn't implemented * Fix openspec gitignore pattern * Update gitignore * Update contribution standards to use openspec for SDD * Migrate to new opsx openspec commands * Migrate workflow and openspec config * fix: bump version to 0.26.10 for PyPI publish - Sync version across pyproject.toml, setup.py, src/__init__.py, src/specfact_cli/__init__.py - Add CHANGELOG entry for 0.26.10 (fixes incorrect version publish issue) Co-authored-by: Cursor <cursoragent@cursor.com> * Update version and changelog * Add canonical user-friendly workitem url for ado workitems * Update to support OSPX * feat(backlog): implement refine --import-from-tmp and fix type-check (#156) * feat(backlog): implement --import-from-tmp for refine export/import round-trip - Add _parse_refined_export_markdown() to parse export-format markdown (ID, Body, Acceptance Criteria, optional title/metrics) - Import branch: read file, match by ID, update items; --write calls adapter.update_backlog_item() - Remove 'Import functionality pending implementation' message - Unit tests for parser (single item, AC/metrics, header-only, blocks without ID) - Bump version to 0.26.11 and sync across pyproject.toml, setup.py, src/__init__.py, src/specfact_cli/__init__.py - OpenSpec change: implement-backlog-refine-import-from-tmp (proposal, tasks, spec delta) Fixes #155 Co-authored-by: Cursor <cursoragent@cursor.com> * Fix type check issues --------- Co-authored-by: Dominikus Nold <djm81@users.noreply.github.com> Co-authored-by: Cursor <cursoragent@cursor.com> * feat: debug logs under ~/.specfact/logs and release 0.26.13 (#159) * feat: add debug logs under ~/.specfact/logs with operation metadata - User-level log dir: get_specfact_home_logs_dir() (~/.specfact/logs, 0o755) - debug_print() routes to console and rotating specfact-debug.log when --debug - debug_log_operation() for structured metadata (ADO, GitHub, backlog, init) - CLI init_debug_log_file() when --debug; help text updated Closes #158 OpenSpec change: add-debug-logs-specfact-home Co-authored-by: Cursor <cursoragent@cursor.com> * Add debug logging for selected commands at first * release: 0.26.13 - debug log parity for upgrade, versions and changelog - Log upgrade success (up to date) to ~/.specfact/logs/specfact-debug.log - Bump version to 0.26.13; sync pyproject.toml, setup.py, src/__init__.py, specfact_cli/__init__.py - CHANGELOG: 0.26.13 Fixed entry for upgrade debug parity Co-authored-by: Cursor <cursoragent@cursor.com> * Remove pr markdown --------- Co-authored-by: Dominikus Nold <djm81@users.noreply.github.com> Co-authored-by: Cursor <cursoragent@cursor.com> * Potential fix for pull request finding 'Empty except' Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> Signed-off-by: Dom <39115308+djm81@users.noreply.github.com> * Fix unused variable review * Fix unused variable review * Fix type and test errors * Finalize change * Change for debug logs archived * fix: improve ADO backlog refine error logging and user-facing error UX (#164) * Improving error logging capabilities * small fix on changelog * Archived change --------- Co-authored-by: Dominikus Nold <djm81@users.noreply.github.com> * feat: backlog refine --ignore-refined and --id, startup docs (fixes #166) (#167) * feat: backlog refine --ignore-refined and --id, startup docs (fixes #166) OpenSpec change: improve-backlog-refine-and-cli-startup. Adds --ignore-refined/--no-ignore-refined, --id <issue-id>; helper _item_needs_refinement; interactive refinement prompt section; version 0.26.15. * Add change for this branch and improve change create workflow * Improve refinement prompt and add specification feedback, update docs and add backlog refinement tutorial * Fix spec update and tasks * Improve pr orchestrator pipeline triggers --------- Co-authored-by: Dominikus Nold <djm81@users.noreply.github.com> * Add change proposals for full scrum support * Add support for systematic, structured issue creation with copilot help * feat(backlog): daily standup defaults, iteration/sprint, unassigned items view (#174) * Issue 179 resolution (#180) * fix(backlog): address CodeQL/Codex PR 181 findings - Replace empty except with debug_log_operation in _load_standup_config and _load_backlog_config (correct signature: operation, target, status, error) - Add dim console message in sprint end date parse except block - Gate summarize prompt description/comments on --comments; add include_comments to _build_summarize_prompt_content and call site - Add test for metadata-only summarize when include_comments=False; update existing test to pass include_comments=True Co-authored-by: Cursor <cursoragent@cursor.com> * Update openspec enforcement rules * Structure openspec changes * Fix ruff finding * Fix linter issues with StrEnum and parameters * Fix tests and depcreation warnings * Improve sync script * Add change for modular command registry * Fix review finding on dev sync script --------- Signed-off-by: Dom <39115308+djm81@users.noreply.github.com> Co-authored-by: Dominikus Nold <djm81@users.noreply.github.com> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
1 parent 32bf31e commit 8f13429

File tree

9 files changed

+417
-0
lines changed

9 files changed

+417
-0
lines changed
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# SpecFact CLI project – gh CLI automation
2+
3+
How to set **Projects** and project fields (Status, Type, Parent issue) that appear on each issue/PR **sidebar** for the [SpecFact CLI project](https://github.com/orgs/nold-ai/projects/1). All of this can be done with the GitHub CLI (`gh`).
4+
5+
**Project:** SpecFact CLI · <https://github.com/orgs/nold-ai/projects/1>
6+
**Owner:** `nold-ai` · **Project number:** `1`
7+
8+
---
9+
10+
## Add issue or PR to the project (Projects field on sidebar)
11+
12+
```bash
13+
gh project item-add 1 --owner nold-ai --url "https://github.com/nold-ai/specfact-cli/issues/ISSUE_NUMBER"
14+
# or for a PR:
15+
gh project item-add 1 --owner nold-ai --url "https://github.com/nold-ai/specfact-cli/pull/PR_NUMBER"
16+
```
17+
18+
Requires project scope: `gh auth refresh -s project` if it fails.
19+
20+
---
21+
22+
## Set Status (single-select)
23+
24+
You need the **project item ID** (not the issue number), then:
25+
26+
```bash
27+
gh project item-edit --id ITEM_ID --field-id PVTSSF_lADODWwjB84BKws4zg6iOak --project-id PVT_kwDODWwjB84BKws4 --single-select-option-id OPTION_ID
28+
```
29+
30+
**IDs for SpecFact CLI project (as of 2026-02):**
31+
32+
- Project ID: `PVT_kwDODWwjB84BKws4`
33+
- Status field ID: `PVTSSF_lADODWwjB84BKws4zg6iOak`
34+
- Status options: Todo `f75ad846`, In Progress `47fc9ee4`, Done `98236657`, Rejected `82beb238`
35+
36+
**Get issue item ID (after the issue is on the project):**
37+
38+
```bash
39+
ISSUE_ITEM_ID=$(gh api graphql -f query='{organization(login: "nold-ai") {projectV2(number: 1) {items(first: 100) {nodes {id content {... on Issue {number}}}}}}}' | jq -r '.data.organization.projectV2.items.nodes[] | select(.content.number == ISSUE_NUMBER) | .id')
40+
```
41+
42+
Only one field can be updated per `gh project item-edit` call for non-draft items.
43+
44+
---
45+
46+
## List project fields and option IDs (Type, etc.)
47+
48+
```bash
49+
gh project field-list 1 --owner nold-ai --format json
50+
```
51+
52+
Use this to get field and option IDs for any single-select (e.g. Type), then set them with `gh project item-edit ... --single-select-option-id ID`.
53+
54+
---
55+
56+
## Parent issue (Epic) field
57+
58+
The project has a **Parent issue** field (ID `PVTF_lADODWwjB84BKws4zg6iObA`). It is an item-link field. `gh project item-edit` does not support setting it (no item-link flag). Use the GraphQL mutation `updateProjectV2ItemFieldValue` if you need to set it from automation.
59+
60+
---
61+
62+
## Summary
63+
64+
| Sidebar property | gh CLI |
65+
|------------------|--------|
66+
| **Projects** (add to SpecFact CLI) | `gh project item-add 1 --owner nold-ai --url URL` |
67+
| **Status** | `gh project item-edit --id ITEM_ID --field-id PVTSSF_lADODWwjB84BKws4zg6iOak --project-id PVT_kwDODWwjB84BKws4 --single-select-option-id OPTION_ID` |
68+
| **Type** (if added as single-select) | Same pattern; get IDs with `gh project field-list 1 --owner nold-ai --format json` |
69+
| **Parent issue** | GraphQL only (not `gh project item-edit`) |
70+
71+
Refs: [gh project item-add](https://cli.github.com/manual/gh_project_item-add), [gh project item-edit](https://cli.github.com/manual/gh_project_item-edit), [gh project field-list](https://cli.github.com/manual/gh_project_field-list).

openspec/CHANGE_ORDER.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ One parent issue per main command for grouping. **Do not add an Epic label** —
5454
| Thorough validation | [Epic] Thorough codebase validation | [#190](https://github.com/nold-ai/specfact-cli/issues/190) |
5555
| Sidecar validation | [Epic] Sidecar validation | [#191](https://github.com/nold-ai/specfact-cli/issues/191) |
5656
| Bundle mapping | [Epic] Bundle/spec mapping | [#192](https://github.com/nold-ai/specfact-cli/issues/192) |
57+
| **Architecture** | [Epic] Architecture (CLI structure, modularity, performance) | [#194](https://github.com/nold-ai/specfact-cli/issues/194) |
5758

5859
**Linking child issues**: On each change issue (e.g. #116, #173, #175, …), use the project **Type** and **Parent** (or GitHub Relationships) to associate it with the epic above. Type (Epic, Feature, Story, etc.) is set via the project **Type** property only; do not use an Epic or other type label.
5960

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# Design: CLI Modular Command Registry
2+
3+
## Overview
4+
5+
This change introduces a **CommandRegistry** (analogous to AdapterRegistry) so command groups are registered by name with a loader and metadata. The root Typer app builds its tree from the registry and loads a command module only when that command is invoked (lazy load). On **specfact init**, discovery writes command metadata to `~/.specfact/registry/` so root `--help` can render from cache without loading any command module. No new external systems; integration is with existing cli.py, commands/, and init.
6+
7+
## Architecture
8+
9+
```text
10+
┌─────────────────────────────────────────────────────────────────────────┐
11+
│ Root Typer (cli.py) │
12+
│ - No top-level command imports │
13+
│ - Knows: command names + order (from registry or cache) │
14+
├─────────────────────────────────────────────────────────────────────────┤
15+
│ │
16+
│ specfact --help / -h / -ha (root) │
17+
│ → Read ~/.specfact/registry/commands.json if valid │
18+
│ → Render from cache (no module loads) │
19+
│ │
20+
│ specfact <cmd> ... (e.g. specfact init, specfact backlog --help) │
21+
│ → CommandRegistry.get_typer("<cmd>") (lazy: load module on first use) │
22+
│ → add_typer / delegate to returned Typer │
23+
│ → Only <cmd> module loaded │
24+
│ │
25+
└─────────────────────────────────────────────────────────────────────────┘
26+
27+
┌────────────────▼─────────────────────────┐
28+
│ CommandRegistry │
29+
│ - register(name, loader, metadata) │
30+
│ - get_typer(name) → lazy load │
31+
│ - list_commands() / list_commands_...() │
32+
│ - CommandMetadata: name, help, tier, … │
33+
└──────────────────────────────────────────┘
34+
35+
┌────────────────▼─────────────────────────┐
36+
│ specfact init │
37+
│ - Ensure ~/.specfact/registry/ │
38+
│ - Discovery: registry reports all │
39+
│ commands + metadata │
40+
│ - Write commands.json (version/hash) │
41+
└──────────────────────────────────────────┘
42+
```
43+
44+
## Integration Points
45+
46+
### cli.py
47+
48+
- **Current**: Imports all command modules; calls `app.add_typer(init.app, ...)`, etc. Order fixed in code.
49+
- **Change**: Import only CommandRegistry and a bootstrap that registers built-in commands (loaders + metadata). Root app iterates registry (or reads cache for help) and adds each command via a lazy callback: on first use of a name, call `CommandRegistry.get_typer(name)` and add the returned Typer (or use Click/Typer pattern for lazy command group). No direct import of `specfact_cli.commands.init`, etc.
50+
51+
### commands/ and Built-in Registration
52+
53+
- **Current**: Each command module defines `app = typer.Typer(...)`; cli.py imports and adds them.
54+
- **Change**: A single "register_builtin_commands()" (or equivalent) runs at startup; it registers each built-in by name with a loader (e.g. lambda or importlib wrapper that imports the module and returns `module.app`) and metadata. Loaders are not invoked until `get_typer(name)` is called. Order can be a fixed list of names (e.g. init, auth, backlog, import, …) so display order is preserved.
55+
56+
### AdapterRegistry Pattern
57+
58+
- **Current**: AdapterRegistry has register(type, class), get_adapter(type), list_adapters(), is_registered(type). Built-ins register in `adapters/__init__.py`.
59+
- **Change**: CommandRegistry mirrors this: register(name, loader, metadata), get_typer(name), list_commands(). Built-ins register in one place (e.g. registry module or `commands/__init__.py`) without cli.py importing each command module.
60+
61+
### specfact init
62+
63+
- **Current**: IDE setup, templates, repo .specfact; may create ~/.specfact for first-run detection.
64+
- **Change**: After existing init logic, run discovery: ask registry for all commands and metadata (without invoking loaders); write ~/.specfact/registry/commands.json (or .yaml) with name, help, tier, version/hash. Create ~/.specfact/registry/ if missing.
65+
66+
### Help Path (progressive_disclosure, -ha)
67+
68+
- **Current**: Typer walks full app tree for --help / --help-advanced.
69+
- **Change**: For root only (no subcommand): if cache exists and is valid, render help from cache (same content as today). For `specfact <cmd> --help`, lazy-load <cmd> and delegate to Typer. Progressive disclosure (advanced options) can remain as today for the loaded command.
70+
71+
## Contract Enforcement
72+
73+
- CommandRegistry: @icontract @require/@ensure on register, get_typer, list_commands; @beartype on public API.
74+
- CommandMetadata: Pydantic model or validated dict; no loader invocation when reading metadata.
75+
- Cache file: Validate schema on read; invalid or missing → fall back to building from registry in memory (no load of Typer apps for root help if cache is used; otherwise minimal path).
76+
77+
## Fallback and Offline
78+
79+
- No network required: discovery and cache are local. Cache invalid → re-run discovery on next init or show help from in-memory registry metadata only (if we store metadata without loading Typer, we can list commands and help without loading any command module).
80+
- Offline-first: unchanged.
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Change: CLI Modular Command Registry (Dynamic Registration and Lazy Load)
2+
3+
## Why
4+
5+
All CLI command groups are hard-wired in `cli.py` via top-level imports and `app.add_typer(...)`. Adding or reordering a command requires editing `cli.py`, which is a merge-conflict hotspot when multiple features touch the same file. Every command module is imported at startup even when the user runs a single command (e.g. `specfact init`), slowing startup. There is no clean extension point for addons or for gating commands by license (community vs enterprise). A registry-based, lazy-load design—mirroring the existing AdapterRegistry pattern—reduces conflicts, improves performance, and prepares for addons and licensing.
6+
7+
## What Changes
8+
9+
- **NEW**: Introduce **CommandRegistry** (and optional **CommandMetadata** model) with `register(name, loader, metadata)`, `get_typer(name)` (lazy load), `list_commands()`, `list_commands_for_help()`.
10+
- **NEW**: Metadata schema: name, help string, tier (community/enterprise), optional addon_id, optional subcommand list.
11+
- **CHANGE**: **cli.py** no longer imports command modules at top level; it builds the Typer tree from the registry (or cached metadata) and adds commands via a lazy callback that loads only the invoked command.
12+
- **NEW**: On **specfact init**, run discovery: write command metadata to `~/.specfact/registry/` (e.g. `commands.json`) so root `-h` / `--help` / `-ha` / `--help-advanced` can render from cache without loading all command modules.
13+
- **EXTEND** (optional in this change or follow-up): Tier and addon_id in metadata; filter list/help and execution by license.
14+
15+
## Capabilities
16+
17+
- **command-registry**: CommandRegistry with register, get_typer (lazy), list_commands, list_commands_for_help; CommandMetadata model; built-in commands registered via registry (no hard-wiring in cli.py).
18+
- **lazy-loading**: Root app adds command groups by name from registry; only the invoked command module is loaded at runtime.
19+
- **help-cache**: Discovery on specfact init writes ~/.specfact/registry/commands.json; root help uses cache when valid; cache invalidation on version change or init.
20+
21+
## Impact
22+
23+
- **Affected specs**: New `openspec/changes/arch-01-cli-modular-command-registry/specs/command-registry/spec.md`, `specs/lazy-loading/spec.md`, `specs/help-cache/spec.md`.
24+
- **Affected code**: New module (e.g. `src/specfact_cli/registry/` or `src/specfact_cli/commands/registry.py`); refactor of `cli.py` (remove direct command imports, use registry); init command extended to run discovery and write cache.
25+
- **Affected documentation** (<https://docs.specfact.io>): docs/ (reference for CLI structure, addons if added); README.md if CLI behavior is documented.
26+
- **Integration points**: Existing AdapterRegistry pattern (mirror); AgentRegistry; all command modules (each registers with CommandRegistry).
27+
- **Backward compatibility**: CLI names, flags, and behavior remain the same; only loading and help source change. No breaking changes to user-facing CLI.
28+
29+
## Source Tracking
30+
31+
- **GitHub Issue**: #193
32+
- **Issue URL**: <https://github.com/nold-ai/specfact-cli/issues/193>
33+
- **Repository**: nold-ai/specfact-cli
34+
- **Parent epic**: [Architecture](https://github.com/nold-ai/specfact-cli/issues/194) (#194)
35+
- **Last Synced Status**: proposed
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# Command Registry
2+
3+
## ADDED Requirements
4+
5+
### Requirement: CommandRegistry with Lazy Load
6+
7+
The CLI SHALL provide a **CommandRegistry** that registers command groups by name with a loader and metadata, and resolves the Typer app only when requested (lazy load).
8+
9+
**Rationale**: Enables modular command registration without importing all command modules at startup; mirrors AdapterRegistry pattern.
10+
11+
#### Scenario: Register and Resolve Command
12+
13+
**Given**: CommandRegistry is initialized and a command "init" is registered with a loader (callable returning typer.Typer) and metadata (name, help, tier)
14+
15+
**When**: Code calls `CommandRegistry.get_typer("init")`
16+
17+
**Then**: The loader is invoked (if not already cached), the Typer app for "init" is returned, and subsequent calls for "init" return the same instance (or same Typer) without re-importing
18+
19+
**Acceptance Criteria**:
20+
21+
- `register(name, loader, metadata)` stores entry without invoking loader
22+
- `get_typer(name)` invokes loader on first use and returns typer.Typer
23+
- `list_commands()` returns all registered command names in registration order (or configured order)
24+
- `list_commands_for_help()` returns names (and optional metadata) for help display; MAY be filtered by tier/license when implemented
25+
26+
#### Scenario: Unknown Command
27+
28+
**Given**: CommandRegistry has no entry for "unknown-cmd"
29+
30+
**When**: Code calls `CommandRegistry.get_typer("unknown-cmd")`
31+
32+
**Then**: A clear error is raised (e.g. ValueError or KeyError) with message listing registered commands or suggesting typo
33+
34+
**Acceptance Criteria**:
35+
36+
- No silent failure; caller can distinguish "not registered" from "load failed"
37+
38+
---
39+
40+
### Requirement: CommandMetadata Model
41+
42+
The CLI SHALL support a **CommandMetadata** model (or equivalent dict schema) with at least: name, help string, tier (e.g. community | enterprise), optional addon_id, optional subcommand list.
43+
44+
**Rationale**: Enables discovery, cached help, and future licensing without loading modules.
45+
46+
#### Scenario: Metadata Available Without Loading Module
47+
48+
**Given**: A command "backlog" is registered with metadata (help="Backlog refinement and template management", tier="community")
49+
50+
**When**: Code calls `CommandRegistry.get_metadata("backlog")` or equivalent (or metadata is returned by list_commands_for_help)
51+
52+
**Then**: Metadata is returned without invoking the command's loader
53+
54+
**Acceptance Criteria**:
55+
56+
- Metadata is stored at registration time
57+
- Accessing metadata does not trigger module load
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# Help Cache (~/.specfact/registry)
2+
3+
## ADDED Requirements
4+
5+
### Requirement: Discovery on specfact init Writes Command Metadata to ~/.specfact
6+
7+
When the user runs **specfact init**, the CLI SHALL run a discovery step that collects all registered commands' metadata (name, help, tier, optional subcommands) and SHALL write this metadata under `~/.specfact/registry/` (e.g. `commands.json` or `commands.yaml`), including a version or hash for cache invalidation.
8+
9+
**Rationale**: Enables fast root help without loading any command module.
10+
11+
#### Scenario: Init Writes Cache
12+
13+
**Given**: User has not run specfact init before (or cache is missing/invalid)
14+
15+
**When**: User runs `specfact init` (with or without subcommand, e.g. `specfact init` or `specfact init cursor`)
16+
17+
**Then**: After init logic runs, discovery runs: registry reports all commands and metadata; result is written to `~/.specfact/registry/commands.json` (or equivalent) with version/hash (e.g. SpecFact version)
18+
19+
**Acceptance Criteria**:
20+
21+
- `~/.specfact` and `~/.specfact/registry/` are created if missing
22+
- File format is deterministic and readable (JSON or YAML)
23+
- Cache includes at least: command names, help strings, optional tier; and a version or hash field for invalidation
24+
25+
#### Scenario: Root Help Uses Cache When Valid
26+
27+
**Given**: Cache exists at `~/.specfact/registry/commands.json` and is valid (e.g. version matches current SpecFact version or hash matches)
28+
29+
**When**: User runs `specfact --help` or `specfact -h` or `specfact --help-advanced` (root level, no subcommand)
30+
31+
**Then**: Root help is rendered from cached metadata without loading any command module; output is consistent with previous behavior (same commands and help strings)
32+
33+
**Acceptance Criteria**:
34+
35+
- If cache exists and is valid, no command module is loaded for root help
36+
- If cache is missing or invalid, fall back to current behavior (e.g. build from registry in memory, which may load metadata only or lazy-load; or show help by iterating registry without loading Typer apps)
37+
- Subcommand help (e.g. `specfact backlog --help`) may still lazy-load that command and use Typer's help
38+
39+
#### Scenario: Cache Invalidation
40+
41+
**Given**: Cache was written by an older SpecFact version or after init
42+
43+
**When**: SpecFact version changes (e.g. upgrade) or user runs `specfact init` again
44+
45+
**Then**: Cache is refreshed (discovery re-run, file overwritten) so root help reflects current commands and version
46+
47+
**Acceptance Criteria**:
48+
49+
- Version or hash in cache file allows comparison with current runtime; if mismatch, treat cache as invalid and refresh on next init or root help
50+
- Running `specfact init` always refreshes cache for current version

0 commit comments

Comments
 (0)