Skip to content

feat: armillary context β€” where was I? instant project re-entry#26

Merged
justi merged 2 commits intomainfrom
feature/armillary-context
Apr 13, 2026
Merged

feat: armillary context β€” where was I? instant project re-entry#26
justi merged 2 commits intomainfrom
feature/armillary-context

Conversation

@justi
Copy link
Copy Markdown
Owner

@justi justi commented Apr 13, 2026

Summary

  • New armillary context <name> CLI command β€” instant project re-entry
  • New armillary_context MCP tool β€” AI gets project resumption state
  • context_service.py β€” pure logic, sub-second, all local git ops
  • UX reviewed: branch on first line, bold dirty count, actionable hint

The daily loop (complete)

armillary next    β†’ "what should I work on?"
armillary search  β†’ "where is this code?"
armillary context β†’ "where was I on this project?"

Example output

  my-project on feature/export β€” ACTIVE β€” 279.5 h
  ~/projects_prod/my-project

  3 dirty files
    M app/services/payment_service.rb
    M test/services/payment_test.rb
    ?? tmp/debug.log

  Last commits
  a1b2c3d    2 days ago   fix webhook retry logic
  e4f5a6b    3 days ago   add PDF export for results
  c7d8e9f    5 days ago   refactor parser into service

  Recent branches
  feature/pdf-export        3 days ago
  fix/webhook-retry         1 week ago

  β†’ 3 unstaged changes β€” commit or stash before switching

Test plan

  • 289 tests pass (9 new for context_service)
  • ruff check + format clean
  • Live tested with real 178-project cache
  • UX reviewed β€” 3 improvements applied

πŸ€– Generated with Claude Code

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>
Copilot AI review requested due to automatic review settings April 13, 2026 17:31
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.py to assemble cached metadata + live git state into a ProjectContext.
  • Adds armillary context <name> CLI command and armillary_context MCP tool to surface that context.
  • Adds a dedicated test_context_service.py test 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.

Comment on lines +211 to +236
@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)

Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +80 to +87
if not (project.path / ".git").is_dir():
return ProjectContext(
name=project.name,
path=project.path,
status=status,
work_hours=work_hours,
is_git=False,
)
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(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.

Copilot uses AI. Check for mistakes.
Comment on lines +103 to +115
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 ""
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_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.

Copilot uses AI. Check for mistakes.
if not output:
return []
lines = output.splitlines()
return [line.strip() for line in lines[:max_show]]
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_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).

Suggested change
return [line.strip() for line in lines[:max_show]]
return [line.rstrip() for line in lines[:max_show]]

Copilot uses AI. Check for mistakes.
Comment on lines +89 to +99
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,
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
# Actionable hint
if ctx.dirty_count > 0:
console.print(
f"\n [dim]β†’ {ctx.dirty_count} unstaged change{s}"
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
f"\n [dim]β†’ {ctx.dirty_count} unstaged change{s}"
f"\n [dim]β†’ {ctx.dirty_count} uncommitted change{s}"

Copilot uses AI. Check for mistakes.

console = Console()
status_str = ctx.status or "?"
hours_str = f" β€” {ctx.work_hours:.1f} h" if ctx.work_hours else ""
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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 ""

Copilot uses AI. Check for mistakes.
Comment on lines +220 to +243
@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."

Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +18 to +22
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
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +29 to +33
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)

Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
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>
@justi justi merged commit 39d9942 into main Apr 13, 2026
2 checks passed
@justi justi deleted the feature/armillary-context branch April 13, 2026 17:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants