npc — drive a real provider CLI from a tracked goal
NPCAgent turns a natural-language goal into a durable run inside a local repository. It plans, validates the plan against the tools that actually exist, cuts a git worktree, launches a real provider terminal through tmux, exposes its own tools to that terminal over MCP, and records every state change in repo-local SQLite.
$ npc init NPCAgent ready. created dir .npcagent created dir .npcagent-worktrees created dir .npcagent/db created dir .npcagent/memory created dir .npcagent/docs created dir .npcagent/agents created dir .npcagent/projects created file .npcagent/memory/current.md created file .npcagent/db/npcagent.db initialized SQLite database updated .gitignore Try: npcagent "do something" $ npc "create a python script named current_time.py that prints the current date and time, then run it once and write the output to current_time.txt" \ --compact started project 2352067a step execute developer tool cao.spawn provider=claude_code tool result cao.teardown ok phase idle->complete completed 2352067a completed run 2352067a-c26d-43f8-891e-64b1c9a91b44 project create-a-python-script… workflow quick-fix worktree .npcagent-worktrees/2352067ac26d events 7 $ cat .npcagent-worktrees/2352067ac26d/current_time.txt 2026-04-28 15:48:27 $
Transcribed from docs/acceptance-log.md, the v0.1.0 acceptance
run. --compact selects the one-line event format in
plugins/surfaces/cli/live.py. Seven events; the work itself was done by a real
Claude Code terminal, which signalled completion by calling
mcp__npcagent__complete_workflow_successfully.
Name
- npc — short entry point
- With no arguments, attaches to (or launches) a real provider TUI through CAO. With any argument, delegates to the full command tree.
- npcagent — same tree under the package name
- Identical Typer application;
npcis a thin wrapper that adds the bare-invocation behaviour. - npc-mcp-server — downstream MCP shim
- Serves NPCAgent's own tool registry to the spawned provider CLI. Not normally run by hand; it is written into the CAO agent profile.
Declared in pyproject.toml under
[project.scripts].
Synopsis
Every flag above is declared with typer.Option in
npcagent/plugins/surfaces/cli/main.py. See
npc-commands(1) for the full option list, defaults and exit
statuses.
Description
NPCAgent does not talk to a model API to do your work. It drives a coding-agent
CLI you already have installed — Claude Code, Codex, Gemini CLI, OpenCode — inside a
tmux session managed by cli-agent-orchestrator (CAO, pinned at
==2.1.1). NPCAgent's contribution is everything around that terminal:
classification, planning, validation, isolation, a durable state machine, and an audit
log.
The distinguishing design choice is stated in the README and enforced in code: there is exactly one production goal path. Three historical stub environment variables are checked at CLI entry and cause a hard exit if set.
089 _FORBIDDEN_STUB_ENV_VARS: tuple[str, ...] = ( 090 "NPCAGENT_FORCE_STUB_CAO", 091 "NPCAGENT_TEST_STUB_GOALS", 092 "NPCAGENT_FAKE_SPAWNER", 093 ) ... 108 console.print( 109 f"[bold red]error:[/bold red] forbidden stub env var set: {joined}. " 110 "NPCAgent has exactly one production goal path (real CAO + real " 111 "provider CLI). Unset these variables; tests must inject fake " 112 "spawners directly via execute_goal(spawner_factory=...)." 113 ) 114 raise typer.Exit(code=2)
Tests inject fakes through the spawner_factory
parameter of execute_goal() instead. The one env var that does change
behaviour, NPCAGENT_FORCE_OFFLINE_PLANNER, only swaps the Stage 1 planner
for a deterministic one — the work still runs through a real provider CLI.
What state it owns
Everything is repo-local. npc init creates .npcagent/, runs
Alembic to head against .npcagent/db/npcagent.db, and appends
three lines to .gitignore. Eight tables carry all durable state:
runs, events, primitives,
workflow_state, workflow_signals, tasks,
spawn_envelopes, and the FTS5 virtual table
conversation_turns. See files(5).
Three primitives
- Project — the default
- One-shot work.
Project.create()writes aprimitivesrow and apendingrow inruns, then the run executes immediately. - Routine —
--routine --schedule "0 9 * * 1" - Recurring. Persistent by construction. The schedule accepts either a cron expression
(validated by
croniter) or a duration such asevery 1h. Fired by the cron trigger undernpc serve. - Reaction —
--reaction --trigger-type webhook - Event-triggered. Trigger types are a closed set:
webhook,chat,file_watch,external. A webhook reaction gets asecrets.token_urlsafe(32)secret generated at creation time.
Creating a Routine or Reaction does not run anything. execute_goal()
plans, validates, writes the Stage 1 plan artefact, creates the primitive, and returns
with run_id=None. Only npc serve fires them.
Command tree
no_args_is_help=True. GoalDefaultGroup.resolve_command
rewrites any unrecognised first word into run, which is what makes
npc "build X" work without a subcommand. The dashed row holds three hidden
slash aliases registered so the same words work from the dashboard surface.Goal pipeline
Everything below is one function: execute_goal() in
npcagent/plugins/surfaces/cli/goals.py (lines 1197–1532). The REST, ACP and
trigger surfaces all call into the same function or its sibling
execute_existing_run(), so no surface has a private code path.
npc in an initialised repo re-migrates to head and
no-ops. Three background services are started at step 9 and torn down in the
finally block.Maturity
The package declares Development Status :: 2 - Pre-Alpha and version
0.1.0. That is an accurate self-assessment, and it is worth being specific
about what is behind it rather than rounding in either direction.
| Area | State | Evidence |
|---|---|---|
| Run lifecycle & event log | Implemented | 7 statuses, an explicit VALID_TRANSITIONS table,
compare-and-set writes, 13 typed Pydantic events persisted before fan-out. |
| Workflow state machine | Implemented | 9 phases, 13 events, optimistic-concurrency
version column. A direct port of a C++ predecessor, and the
docstrings say so. |
| Claude Code provider | Implemented | The only provider in the
npcagent.plugins entry-point group. |
| Codex · Gemini · OpenCode | Via CAO only | pyproject.toml states they "remain parser/regression
fixtures until their direct subprocess drivers are implemented";
opencode.py and gemini_cli.py raise
NotImplementedError from their direct-drive methods. |
| Plan validator pass 3 | Disabled at the call site | goals.py constructs
PlanValidator(sandbox_dry_run=None) with the comment "Pass 3
(sandbox dry-run) is deferred to T100/T101". |
ACP session/cancel |
Unsupported | docs/acp.md: returns an unsupported error "because
active goal work cannot be interrupted yet".
initialize returns an empty command list. |
| Memory search | Substring scan | An FTS5 index exists for agent messages, but
LocalMemoryProvider.search() is a plain substring walk over
*.md. decay() is a documented no-op. |
| Plugin hot-reload | Deferred | Registry.discover() is one-shot; the docstring defers
hot-reload to "v1.5+". |
| OpenTelemetry | Stub | NPCAGENT_OTEL=1 logs "OTEL enabled (stub - no
exporter wired yet)". |
Against that, the engineering discipline is unusually visible for a 0.1.0. CI runs
ruff check, mypy in strict = True mode, and
pytest on every push and pull request. tests/test_layering.py
greps every source file to prove that kernel/ never imports
plugins/ and that contracts/ imports nothing else in the
package — an architectural rule enforced by a test rather than a convention.
The repository holds 114 commits from a single author, and every one of them is dated
28 or 29 April 2026 — the whole package was built in about two days. Read alongside
CLAUDE.md, which instructs its reader to act as "the build PM", drive an
81-task list, and dispatch subagents to write the code, the history is consistent with
an agent-driven build. There are no release tags and no published distribution. Treat
the version number as a marker of intent, not of field exposure.