NPC(1) NPCAgent Manual NPC(1)

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.

/tmp/npcagent-acceptance-final3 — npc v0.1.0 acceptance
$ 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.

Package
npcagent 0.1.0
Console scripts
3
Python modules
225 files
Test functions
1046
Classifier
Pre-Alpha

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; npc is 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

npc npc "<goal>" [--project slug] [--provider id] [--workflow id] [--routine --schedule spec] [--reaction --trigger-type type] [--live|--no-live] [--compact] [--worktree|--no-worktree] npc init npc run GOAL... npc list [--all] [--limit n] npc logs RUN_ID [--follow] [--limit n] npc status RUN_ID npc approve RUN_ID npc serve [--port n] [--no-webhook] [--no-cron] [--no-file-watch] [--log-json] npc launch [--provider id] [--model id] [--no-attach] npc model [ALIAS] [--provider id] npc auto [on|off|toggle|status] npc project (list [--all] | switch NAME | archive NAME | rename OLD NEW) npc agent (list | promote ID --as ROLE) npc plugin list [--tools] npc tool list npc dashboard [--host h] [--port n] [--no-open] npc codeman [--url u] [--no-start] [--no-session] [--send text]

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.

npcagent/plugins/surfaces/cli/main.pylines 89–114
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 a primitives row and a pending row in runs, 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 as every 1h. Fired by the cron trigger under npc serve.
Reaction --reaction --trigger-type webhook
Event-triggered. Trigger types are a closed set: webhook, chat, file_watch, external. A webhook reaction gets a secrets.token_urlsafe(32) secret generated at creation time.
Note

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

The npc command tree A tree rooted at the npc and npcagent entry points, branching to twenty-two top-level commands and four sub-applications with their sub-commands. ENTRY POINT COMMAND SUBCOMMAND EFFECT npc · npcagent (no arguments) attach or launch the provider TUI via CAO + tmux "<goal>" unknown first word is rewritten to `run` run classify · plan · validate · worktree · execute list active runs + Needs-You total logs <id> replay the event log; --follow polls at 1 s status <id> run row joined to workflow_state.current_phase approve <id> resume a run parked in needs_user init create .npcagent/ · alembic upgrade head serve cron · webhook · file-watch · signals · retry launch CAO terminal; --provider, --model, --no-attach model [alias] write ~/.npcagent/config.toml, or open picker auto [on|off] repo auto-approval flag under .npcagent/ switch re-pick provider, kill and replace the session project list [--all] switch <name> archive <name> rename <old> <new> projects live on disk under .npcagent/projects/ agent list promote --as reads CAO terminal rows; writes an agent manifest plugin list [--tools] builds a Runtime, walks the plugin registry tool list every ToolSpec incl. mcp.<server>.<tool> dashboard NPCAgent web frontend, default 127.0.0.1:8766 codeman bridge to an external Codeman web terminal /model /help /auto hidden slash aliases (hidden=True)
Figure 1 — the command tree. Green marks the production goal path. Blue marks the four Typer sub-applications, each declared with 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.

Execution pipeline for a goal Twelve sequential stages from repository initialisation through planning, validation, run creation, worktree isolation, workflow execution and finalisation, with the durable side effect of each stage shown on the right. STAGE CALL DURABLE EFFECT SETUP 1–4 PLAN 5–6 COMMIT 7–10 EXECUTE 11–12 1 · ensure_repo_initialized(repo_root) init_repo() then configure_db_path() .npcagent/{db,memory,docs,agents,projects} alembic upgrade head → 8 tables 2 · _resolve_project(...) ProjectClassifier.classify → materialize → switch .npcagent/projects/<slug>/master.md sticky active-project file 3 · Runtime.for_repo(repo_root) bus · registry · tools · memory · skills · catalog 36 builtin tools registered entry-point + user-dir + repo-dir plugins 4 · runtime.refresh_mcp_tools() connect each server in ~/.npcagent/mcp.json tools named mcp.<server>.<tool> failed servers skipped with a warning 5 · _build_plan(goal, planner, catalog) Stage1Planner, or HeuristicStage1Planner offline HighLevelPlan (in memory) model recorded on the plan 6 · PlanValidator.validate(plan) three passes: schema · semantic · sandbox fail → PlanRejected → exit 1 nothing has been spawned yet 7 · Project.create(goal, workflow_id, name) idempotency key f"project:{primitive.id}" INSERT primitives INSERT runs (status=pending) 8 · _write_plan_artifact(project, run_id, plan) frontmatter + phases + success criteria projects/<slug>/plans/ <run-id>-stage1.md 9 · start(run_id) compare-and-set on the previous status runs.status pending → running WorkflowStartedEvent → events 10 · WorktreeManager.add(task_id, workflow) task_id = run_id.hex[:12]; skip with --no-worktree .npcagent-worktrees/<task_id> branch npcagent/<workflow>/<8 hex> 11 · WorkflowExecutor.run() expand → prompt → spawn → wait → signal → drain workflow_state (CAS on version) workflow_signals · events 12 · _finalize_run_after_executor(run_id) maps terminal phase onto the run row complete() · escalate() · handle_failure() worktree removed per cleanup mode typer.Exit(code=1) running throughout (steps 9–12) SignalProcessor · DeadlineWatcher ledger stall monitor → replan or escalate
Figure 2 — one goal, twelve stages. The ordering matters: validation (6) happens before any row is written and long before any terminal is spawned, so a plan that references a tool the registry does not hold costs nothing. Steps 1–4 are idempotent; re-running 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.

Assessed against the source, not the README.
AreaStateEvidence
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.

Repository history

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.

See also

npcagent 0.1.0 Apache-2.0 NPC(1)