feat: armillary context β where was I? instant project re-entry#26
feat: armillary context β where was I? instant project re-entry#26
Conversation
New CLI command + MCP tool that shows everything needed to resume
work on a project: branch, dirty files, recent commits, recent
branches. Sub-second response, all local git operations.
UX reviewed: branch on first line, bold dirty count, actionable
hint ("commit or stash before switching").
CLI: `armillary context <name>`
MCP: `armillary_context(project_name)` β explicit only, not auto-triggered
9 tests covering: found/not-found, git/idea, ambiguous/exact match,
zero commits, dirty cap at 5, case-insensitive, null metadata.
Completes the daily loop:
next β "what should I work on?"
search β "where is this code?"
context β "where was I on this project?"
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds a βproject re-entry contextβ feature to armillary, exposing it via both the CLI and MCP so users/agents can quickly see the current branch, working tree state, and recent activity for a project.
Changes:
- Introduces
context_service.pyto assemble cached metadata + live git state into aProjectContext. - Adds
armillary context <name>CLI command andarmillary_contextMCP tool to surface that context. - Adds a dedicated
test_context_service.pytest suite and updates the feature tree doc.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/test_context_service.py | New tests covering context lookup/matching and mocked git-state retrieval. |
| src/armillary/context_service.py | New service building project context from cache + git subprocess calls. |
| src/armillary/cli_tools.py | New armillary context CLI command rendering the context to terminal. |
| src/armillary/mcp_server.py | New MCP tool armillary_context returning context as compact JSON. |
| docs/feature-tree.yaml | Adds/updates roadmap documentation for the context feature and MCP tool. |
π‘ Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| @app.command("context") | ||
| def context_command( | ||
| project_name: str = typer.Argument(..., help="Project name (substring match)."), | ||
| ) -> None: | ||
| """Where was I? Show project state for instant re-entry. | ||
|
|
||
| Displays branch, dirty files, recent commits, and recent branches | ||
| so you can resume work without re-reading code. Sub-second response. | ||
| """ | ||
| from armillary.cli_helpers import _shorten_home | ||
| from armillary.context_service import get_context | ||
|
|
||
| try: | ||
| ctx = get_context(project_name) | ||
| except ValueError as exc: | ||
| typer.secho(str(exc), fg=typer.colors.RED, err=True) | ||
| raise typer.Exit(2) from exc | ||
|
|
||
| if ctx is None: | ||
| typer.secho( | ||
| f"No project matches '{project_name}'. Run `armillary scan` first.", | ||
| fg=typer.colors.RED, | ||
| err=True, | ||
| ) | ||
| raise typer.Exit(2) | ||
|
|
There was a problem hiding this comment.
New armillary context CLI command doesnβt appear to be exercised in the existing CLI test suite. Add a tests/test_cli.py case covering: successful output (git + idea project), ambiguous-name exit code/message, and not-found exit code/message to prevent regressions.
src/armillary/context_service.py
Outdated
| if not (project.path / ".git").is_dir(): | ||
| return ProjectContext( | ||
| name=project.name, | ||
| path=project.path, | ||
| status=status, | ||
| work_hours=work_hours, | ||
| is_git=False, | ||
| ) |
There was a problem hiding this comment.
(project.path / ".git").is_dir() will treat git worktrees/submodules (where .git is a file) as non-git projects. Consider detecting git repos via git rev-parse --is-inside-work-tree or checking .git with .exists() (and optionally is_file()), so valid worktrees still return branch/dirty/commit context.
| def _run_git(project_path: Path, *args: str) -> str: | ||
| """Run a git command and return stdout. Empty string on failure.""" | ||
| try: | ||
| result = subprocess.run( | ||
| ["git", *args], | ||
| cwd=project_path, | ||
| capture_output=True, | ||
| text=True, | ||
| timeout=5, | ||
| ) | ||
| return result.stdout.strip() if result.returncode == 0 else "" | ||
| except (subprocess.TimeoutExpired, FileNotFoundError, OSError): | ||
| return "" |
There was a problem hiding this comment.
_run_git() uses result.stdout.strip(), which removes leading whitespace. For commands like git status --porcelain, leading spaces are meaningful (e.g., " M file" vs "M file"). Prefer preserving leading spaces (e.g., only trim trailing newlines) so status codes arenβt corrupted.
src/armillary/context_service.py
Outdated
| if not output: | ||
| return [] | ||
| lines = output.splitlines() | ||
| return [line.strip() for line in lines[:max_show]] |
There was a problem hiding this comment.
_dirty_files() calls line.strip(), which will also remove the leading status column spaces from git status --porcelain output. If you want to keep accurate porcelain codes, avoid stripping leading whitespace (e.g., use rstrip() or keep the raw line).
| return [line.strip() for line in lines[:max_show]] | |
| return [line.rstrip() for line in lines[:max_show]] |
| return ProjectContext( | ||
| name=project.name, | ||
| path=project.path, | ||
| status=status, | ||
| work_hours=work_hours, | ||
| branch=_current_branch(project.path), | ||
| dirty_files=_dirty_files(project.path), | ||
| dirty_count=_dirty_count(project.path), | ||
| recent_commits=_recent_commits(project.path), | ||
| recent_branches=_recent_branches(project.path), | ||
| is_git=True, |
There was a problem hiding this comment.
get_context() runs extra git commands that can be avoided: _dirty_files() and _dirty_count() each call git status, and _recent_branches() calls _current_branch() again. Since this is intended to be sub-second, consider running git status once (derive both list+count) and pass the already-fetched branch into _recent_branches() to reduce subprocess overhead.
src/armillary/cli_tools.py
Outdated
| # Actionable hint | ||
| if ctx.dirty_count > 0: | ||
| console.print( | ||
| f"\n [dim]β {ctx.dirty_count} unstaged change{s}" |
There was a problem hiding this comment.
The actionable hint says "unstaged changes", but dirty_count/dirty_files come from git status --porcelain and include staged + unstaged + untracked entries. Either adjust the wording (e.g., "uncommitted changes" / "dirty files") or compute unstaged-only counts if thatβs the intent.
| f"\n [dim]β {ctx.dirty_count} unstaged change{s}" | |
| f"\n [dim]β {ctx.dirty_count} uncommitted change{s}" |
src/armillary/cli_tools.py
Outdated
|
|
||
| console = Console() | ||
| status_str = ctx.status or "?" | ||
| hours_str = f" β {ctx.work_hours:.1f} h" if ctx.work_hours else "" |
There was a problem hiding this comment.
hours_str is computed with if ctx.work_hours, so a valid value of 0.0 wonβt render. Use an explicit is not None check so zero-hour projects display correctly.
| hours_str = f" β {ctx.work_hours:.1f} h" if ctx.work_hours else "" | |
| hours_str = f" β {ctx.work_hours:.1f} h" if ctx.work_hours is not None else "" |
| @mcp.tool() | ||
| def armillary_context(project_name: str) -> str: | ||
| """Where was I? Get project state for instant re-entry. | ||
|
|
||
| Returns branch, dirty files, recent commits, and recent branches | ||
| so you can resume work on a project without re-reading code. | ||
|
|
||
| Call this when the user says "switch to X", "where was I on X", | ||
| or "what's the state of X". NOT auto-triggered on directory change. | ||
|
|
||
| Examples: | ||
| - armillary_context("pdf_to_quiz") β branch, 1 dirty file, last 5 commits | ||
| - armillary_context("speak-faster") β dormant, last commit 3 months ago | ||
| """ | ||
| from armillary.context_service import get_context | ||
|
|
||
| try: | ||
| ctx = get_context(project_name) | ||
| except ValueError as exc: | ||
| return f"Ambiguous project name: {exc}" | ||
|
|
||
| if ctx is None: | ||
| return f"No project matches '{project_name}'. Run `armillary scan` first." | ||
|
|
There was a problem hiding this comment.
New armillary_context MCP tool isnβt covered by the existing tests/test_mcp_server.py suite (which tests other tool functions). Add tests for: successful JSON shape, ambiguous-name error behavior, and not-found behavior so MCP consumers donβt regress.
| next: built # momentum / zombie / forgotten gold (CLI + MCP) | ||
| skip: built # hide from next for 30 days | ||
| context: planned # #1 # ADR 0013 β "where was I?" per project (CLI + MCP) | ||
| decide: planned # keep / pivot / kill β three buttons, minimal | ||
| archive: planned # just a status value β ARCHIVED |
There was a problem hiding this comment.
This feature tree marks decisions.context as planned, but this PR introduces the armillary context command + context_service. Update the status here to reflect the current shipped state so the roadmap stays accurate.
| ai_integration: | ||
| mcp_server: built # armillary_next, armillary_search, armillary_projects | ||
| mcp_context: planned # #1 # armillary_context β AI gets project resumption state | ||
| claude_bridge: built # compact repos-index.md (ACTIVE/PAUSED only, 15 max) | ||
|
|
There was a problem hiding this comment.
Under ai_integration, mcp_server still lists only three tools and mcp_context is marked planned, but armillary_context is added in this PR. Update this section so the doc matches the implemented MCP surface area.
Address all 10 Copilot review comments: - .git detection uses .exists() not .is_dir() (worktrees/submodules) - Single _dirty_state() call replaces _dirty_files() + _dirty_count() - _run_git uses .rstrip() to preserve porcelain leading spaces - _recent_branches takes current= kwarg, no duplicate branch lookup - hours_str uses `is not None` check (0.0 renders correctly) - "unstaged" β "uncommitted" (porcelain includes staged+untracked) - feature-tree.yaml: context + mcp_context marked as built - 6 new tests: 3 CLI (success/ambiguous/not-found), 3 MCP (same) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Summary
armillary context <name>CLI command β instant project re-entryarmillary_contextMCP tool β AI gets project resumption statecontext_service.pyβ pure logic, sub-second, all local git opsThe daily loop (complete)
Example output
Test plan
π€ Generated with Claude Code