Git-native project management for builders and agents.
Tasks live inside the repository as Markdown. Create a file, open a branch, update YAML — the whole team (and any agent) stays in sync. No separate board to drift out of date.
Status: early design — convention + CLI first.
A 30-second tour — list, claim, create, finish, validate, board (autoplay below; also available as MP4):
arggon list — the whole plan, right from the repo:
arggon create / arggon update — claim and move work by editing files:
arggon update --status done + arggon validate — finish work and verify the tree (CI-ready):
arggon board — a static, self-contained HTML snapshot of the same tree:
Work is a folder tree that mirrors Agile structure:
ArggonManager/ # tracker root (legacy: tasks/)
<initiative>/
<epic>/
<story>/
task-....md
bug-....md
docs/ # product docs (convention, engineering, ADRs, specs, plans, ...)
Each item is a Markdown file with YAML frontmatter (status and other fields). Updating work means editing the file and committing — same flow for developers, QA, and agents.
Convention (v0) is locked. See ArggonManager/docs/convention.md for folder layout, frontmatter schema, statuses, and claim rules. A sample tree lives in the fixture fixtures/tasks-valid/tasks/launch-mvp/ (plus its tracker .convention.yml). Copy-paste templates for each type live in templates/.
- Builders who want tasks next to the code
- Teams that share work through git, not a SaaS board
- Agents that should pick up and create tasks like humans
- Repo is source of truth — if it’s not in git, it isn’t the plan
- Files over forms — an
.mdis the ticket - One work loop for humans and agents, named authority for the product owner
- Simple & self-hostable — open, lightweight, no lock-in
ArggonManager's process — find → claim → work → review → merge → done, carried by the docs below, with specs, plans, ADRs, explorations and playbooks — is the product.
- Scope: any project — not only software.
- Invariants: work-loop parity: any human and any agent may claim, work, review, comment and merge under the same kernel rules and the same JSON contracts; state lives in git; discipline is enforceable; docs travel with code; never-steal / never-reopen, with authority asymmetric and named — the irreversible overrides stay product-owner powers, structurally human-only.
- Version: tracks the ArggonManager package version; changes reach adopters through the adopter upgrade channel (ADR 0020).
Roles, not job titles (ADR 0021 §6.1): agents are the primary workers, and every agent role is named by what it decides — delivery lead (sequences work, dispatches, owns tracker state), practice & standards (whether a change is right by the project's own bar), maker (produces the change), verifier (executes the project's verification gates and reports observed-versus-expected; it does not decide the verdict). The human in the loop is the product owner, and its authority is bounded and named: direction and priority, product acceptance, risk waiver, claim takeover, release — the last four irreversible steps are structurally human-only.
The promotion policy (ADR 0021 §3): product-owner review is tiered by blast radius, so no leaf waits on a human — task/bug leaves are agent-self-certified (the default), story containers may carry a recorded accept: approval, and the irreversible powers stay gated. A repo with no product owner is compliant by default and never blocked.
The carriers, one hop from here:
- Agent playbook
- Engineering conventions — §Roles and authority (role table, authority map, human-only powers) and §Review bar (bars, tiers, verdict and acceptance conventions)
- Task convention
- arggon-cli skill
- Capability matrix — methodology invariant × agent: which native mechanism each shipped seam actually delivers, and where the client-native layer is still missing (reported by
arggon doctor, report-only)
Per-agent enforcement (OpenCode V2, ZCode, Claude Code) builds on the same kernel — one rules path, no forks — see the native agent adapters epic and spec methodology-adapters-017.
Every other channel is one-directional: the product reaches the adopter and nothing comes back. arggon friction closes that half of the loop for friction in ArggonManager itself — a flag that refuses, a rule that contradicts another rule, a generated file that will not regenerate (ADR 0024). One command appends a bounded, redacted record to a local, append-only log that is never the tracker — it cannot become a task by accident, never commits, never stages and never appears in arggon validate — and --report renders the deduped report (sorted by how often a class was seen, because the maintainer's attention is the scarce resource) with a prefilled issue URL for tier b. The agent never opens an issue; a human does, and no tier auto-submits. The trigger lives in the generated agent files, not only in a skill reference — a capability in the wrong place is absent, and arggon doctor --json's friction block reports per file whether the trigger is there and at which version. Opting out (x-friction: false, or ARGGON_NO_FRICTION=1 per invocation) gates capture, never discoverability: the command stays visible so the channel can be switched back on.
Upgrades are safe by construction. Every doc the tool generates is adopter-owned the moment it exists and is never overwritten: re-running arggon init silently refreshes the docs you haven't touched, skips the ones you've modified and reports them, and --backup archives a modified doc before regenerating it. Your customization survives every tool upgrade.
| Phase | Deliverable |
|---|---|
| 1 | Folder + frontmatter convention and templates |
| 1 | CLI to create, list, and update tasks |
| 2 | Viewer / board UI over the tree |
| 3 | Agent hooks / SDK so agents follow the same rules |
---
type: task
status: todo
id: task-rate-limit
parent: story-login
labels: [security]
created: "2026-09-03"
updated: "2026-09-03"
---
# Add login rate limiting
## Context
...
## Acceptance
- [ ] ...Exact v0 fields are documented in ArggonManager/docs/convention.md (layout, schema, and status transitions are locked). Omit assignee when unassigned (do not write an empty assignee:).
- Task convention — folder layout, frontmatter schema, statuses (v0 locked)
- Agent playbook — find, claim, create, PR loop for humans and agents
- OpenCode V2 playbook — V2 setup, conventions, testing and upgrade policy (pinned 2.0.12)
- OpenCode2 native integration — the native OpenCode V2 experience (shipped in 0.4.0, on
main), how to adopt it, and the guarantees - Headless bootstrap and CI — the packaged
arggonbin for bootstrap + CI (no model, no MCP), and the adopter workflowinitvendors - Claim / concurrency — claim definition, conflict/
--force, unclaim recovery - Engineering conventions — repo structure, review bar, testing, ADRs (Phase 1)
- Phase 2 viewer spike — proposed constraints for board/viewer over the tracker (towards #19; not an ADR)
- Sample tree:
fixtures/tasks-valid/tasks/launch-mvp/
v0 stubs (YAML frontmatter + Context / Acceptance / Notes) live in templates/:
templates/initiative.mdtemplates/epic.mdtemplates/story.mdtemplates/task.mdtemplates/bug.mdtemplates/spec.md/templates/plan.md(rendered byarggon spec new)
Copy a stub into the tracker root (ArggonManager/) per ArggonManager/docs/convention.md, or use arggon create (copies from these templates). Spec/plan stubs are rendered into ArggonManager/docs/specs/ / ArggonManager/docs/plans/ by arggon spec new (see below).
Requires Node.js 22.12+ (engines enforces it). Both packages are published — the released install is one line:
npm install -g arggon-manager # pulls @arggondev/lib from the registry
arggon --versionFrom a pinned checkout (development or pre-release), build and install both tarballs together — no pre-build step needed. arggon-manager (bin + templates/, skills/, opencode/) declares the kernel package @arggondev/lib; packing and installing both from the same directory keeps the checkout's versions pinned (the root tarball also resolves the kernel from the registry since 0.4.0):
git clone https://github.com/Arggon/ArggonManager
cd ArggonManager
npm install # deps; prepare builds lib/dist + dist/
mkdir -p /tmp/arggon-packs # `npm pack --pack-destination` does not create it
npm pack --workspace lib --pack-destination /tmp/arggon-packs
npm pack --pack-destination /tmp/arggon-packs
npm install -g /tmp/arggon-packs/arggondev-lib-<version>.tgz /tmp/arggon-packs/arggon-manager-<version>.tgz
arggon --versionNode projects that prefer no global install can do npm install --no-save /tmp/arggon-packs/*.tgz and run npx arggon. The tarball ships production dist/, the templates/, skills/ and opencode/ assets arggon init reads, README and LICENSE. Installing it needs no scripts; npm may still warn that the tarball's blocked prepare was skipped — benign, the build is already inside the tarball.
The headless bin is what CI and bootstrap use (no model, no MCP): see Headless bootstrap and CI for the adopter recipe — the job arggon init writes to .github/workflows/arggon.yml and the packed-install variants. The same bin installs from the registry in one line (npm install -g arggon-manager); releasing is automated per ADR 0018 — the operator procedure lives in release.md (the historical manual runbook is retired; ArggonManager/docs/runbooks/release.md keeps only the binding invariants).
A checkout installs directly in this order: npm install first (its root prepare builds dist/), then npm link or npm install -g .. With npm 12 install scripts run only when approved, so linking an unbuilt checkout exits 0 without a dist/ or a bin — build first, or approve the script by its resolved identity (npm install -g . --allow-scripts=file:$PWD).
The bin is dist/cli.js and the build sets its executable bit, so a plain ln -s <checkout>/dist/cli.js ~/.local/bin/arggon also works (a manual symlink used to fail with Permission denied).
arggon --version prints the package version plus the build's git sha and branch when one can be determined — 0.3.0 (abc1234, opencode2) from a checkout, the recorded build metadata after installation, or the bare version when there is no git — so parallel installs of different branches stay distinguishable. Running a development checkout side by side with an installed release is documented in ArggonManager/docs/opencode2.md.
Requires Node.js 22.12+ (needed by vitest 5 in the dev toolchain; engines enforces it). Stack: ArggonManager/docs/adr/0001-cli-stack.md (ADR 0001 Accepted with this scaffold).
Root install; two packages — the CLI in cli/, the kernel in lib/
(ADR 0013). npm run build
builds the kernel first, then the root; build before npm test (the suites that
exercise the built kernel, and any CLI run outside the test harness, resolve
@arggondev/lib through lib/dist; the test harness itself resolves the kernel
source — see CONTRIBUTING.md):
npm install
npm run skills:sync # regenerate the gitignored .agents/skills/arggon-cli/SKILL.md copy
npm run arggon -- hello
npm run arggon -- init /path/to/repo
npm run arggon -- list
npm run arggon -- validate
npm run build
npm test
npm run lintThe SKILL command reference is generated, not hand-written: the arggon <cmd> ... lines inside the arggon:generated-commands marker regions of skills/arggon-cli/SKILL.md are rendered from the live commander definitions in cli/src/cli.ts — run npm run skills:sync after any command/flag/description change (cli/src/skill-generated-commands.test.ts fails on drift). Curated narrative stays outside the regions; a coverage invariant in the same test file fails when any non-excluded command lacks a generated-region line, so description edits always feed a region mechanically.
After a fresh clone, .agents/skills/arggon-cli/SKILL.md is absent (it is generated from the single source skills/arggon-cli/SKILL.md, not committed): run npm run skills:sync so agent clients that read skills from disk can see it (npm test regenerates it too — even when a local copy exists but is stale, so a stale copy can never fail the suite).
arggon init creates ArggonManager/.convention.yml, copies templates/, and generates the governing document set from master templates in templates/docs/. Adopter-owned content is never overwritten (not even with --force); on already-initialized repos init is an idempotent upgrade — see Re-running init below. In a git tree init auto-commits the files it wrote (one chore(tasks): generated init docs (N files) commit, surgical staging — --no-commit opts out); paths matched by .gitignore (e.g. generated bundles an adopter ignores by design) are skipped and listed in the additive commit.ignored array instead of aborting the commit, so a fresh init leaves a clean tree and arggon start is never blocked by untracked tool-generated state.
Generated docs (placeholders {{YEAR}} and {{PROJECT_NAME}} are rendered at write time; {{PROJECT_NAME}} comes from the target dir name on a fresh scaffold, and on every re-run it is recovered — first from x-generated.projectName in ArggonManager/.convention.yml, then from the existing generated docs' content — so worktrees and renamed clones render identically to the primary checkout; when the name cannot be recovered, name-bearing writes/comparisons are skipped with a project-name-unrecoverable reason instead of guessing):
- Default (tier-1):
AGENTS.md(spec-compliant agent workflow; mandates the bundled arggon-cli skill by default),CLAUDE.md(one-line@AGENTS.mdshim),.github/copilot-instructions.md(pointer),CONTRIBUTING.md,SECURITY.md,.editorconfig,adapters/capability-matrix.json(the capability matrix — methodology invariant × agent, reported byarggon doctor; product data, so it is generated whatever--agentssays),.mcp.json(registers the arggon MCP server so MCP clients pick it up — part of theclaudeseam, so--no-agentsomits it), the OpenCode V2 seam —opencode.jsonc(generated only when the repo has no OpenCode config of its own; formatter + compaction, no MCP stanza, minimal W4 shell gates: no force-push, no--no-verify),.opencode/agents/arggon-delivery-lead.md+arggon-maker.md+arggon-standards-reviewer.md+arggon-verifier.md,.opencode/commands/arggon-next|start|done|handoff|review|status|spec|adr|explore|playbook|adopt.md(native prompt templates drivingtools.arggon.*), and the vendored single-file.opencode/plugins/arggon/index.ts(auto-discovered; kernel inlined, nonode_modules; native tools + session item context) with the vendored TUI entry.opencode/plugins/arggon/tui.tsxbeside it (board/status panel:/arggon-board; runtime-resolvedsolid-js) —.github/CODEOWNERS(placeholder),.github/PULL_REQUEST_TEMPLATE.md,.github/workflows/arggon.yml(headless CI recipe:init+validate, no model/MCP — see Headless bootstrap and CI),ArggonManager/docs/tracking.md(work tracking in the tracker, not GitHub issues), and the arggon-cli skill bundle at.agents/skills/arggon-cli/(umbrellaSKILL.md+references/{json-contract,methodology,exploration,orchestration,pitfalls}.md, read on demand; copied from this repo'sskills/arggon-cli/— single source, never a duplicate). --fulladds (tier-2):ARCHITECTURE.md,ArggonManager/docs/convention.md+ArggonManager/docs/engineering.md(adopter-owned project templates),CHANGELOG.md,SUPPORT.md,ArggonManager/docs/runbooks/README.md,ArggonManager/docs/deploy.md(per-shape deployment defaults from ADR 0005: target, dated cost, config-in-repo sketch, exit note).
Everything created is listed in created[]; files left untouched land in skipped[] (see docs/json-output.md). JSON/JSONC destinations (.mcp.json, opencode.jsonc, adapters/capability-matrix.json) ship as pure JSON without the arggon:generated HTML comment — MCP clients, OpenCode and the matrix reader parse them directly — and OpenCode Markdown artifacts (.opencode/**/*.md) carry it as a # YAML comment inside their frontmatter so the file stays frontmatter-first. Provenance is tracked either way via the x-generated checksum.
Every generated file carries a visible provenance marker — an HTML comment first line (<!-- arggon:generated template="..." -->), or a # YAML comment inside the frontmatter for OpenCode agents/commands (JSON/JSONC destinations carry none) — and the generation state is recorded in ArggonManager/.convention.yml under the namespaced x-generated: section (destination path → { template, checksum, arggonVersion, generatedAt }; the checksum is the sha256 of the file as written, marker included). Re-running arggon init upgrades templates safely, Copier/Helm-style, per destination:
- Not on disk → generated as today (
created[]). - Acknowledged via
arggon adopt --ack(acknowledged: truein the state) → skip, never regenerated: the acked content is the adopter's sanctioned baseline — it is yours. - On disk, checksum matches the recorded one → generated-and-untouched: silently regenerated from the current template and the state refreshed (
updated[]). This is how template improvements reach an adopted repo. - On disk, checksum differs (or no state entry — pre-provenance files) → adopter-modified: skipped by default (
modified[]+skipped[]; never overwritten, not even with--force). With--backup, the modified file is first moved tobackup/<YYYY-MM-DD>/<dest>and then regenerated with fresh state (backedUp[]). - Recorded template no longer shipped by this arggon version → orphan: init can neither refresh nor keep generating the destination, yet the file is still on disk — and under an auto-discovered seam directory (
.opencode/agents/) a stranded file stays dispatchable.initdeletes an orphan when all of these hold: inside the repo root · a regular file (never a directory, never a symlink) · recorded by no newer arggon (a downgrade reports instead of deleting) · bytes unchanged since the recorded baseline. It refuses and names it otherwise — notably an edit since that baseline, which is yours and is never deleted. The one middle case is an acknowledged orphan (bytes unchanged): refused by default, reaped only when the repo armsx-tracker.reap-acked-orphans: true, becauseadopt --ackacks every entry and re-records its checksum from disk — so for an acknowledged entry a matching checksum proves only unchanged since the ack, never arggon wrote exactly these bytes, and no recorded state separates "unedited" from "curated then acked". Arming is how an adopted repo clears a dead agent across a rename; the default is the side that cannot lose content. Removals ridereaped[](and refusalsreapRefused[]) — their own action family, never folded into the create/update/skip counts — plus one human line per file;init --dry-runpreviews both without writing, and a reaped destination loses its state entry in the same run, so a laterinitreaps nothing. Runarggon doctor --agentsbeforeinitwhen upgrading across a rename: theorphanedstatus shows what would be removed while it is still reversible.
The arggon-cli skill bundle (.agents/skills/arggon-cli/SKILL.md) follows exactly the same rules. Hand edits keep the marker (harmless) but the checksum betrays the edit. arggon validate accepts the x-generated section (namespaced extension, ignore-unknown).
--force no longer discards .convention.yml configuration (bug-init-force-drops-x-tracker-block). --force re-copies templates/ and re-scaffolds the tracker .convention.yml — and it used to re-derive that file from a fixed scaffold string, silently dropping the whole x-tracker extension block (allow-steal, strict-gate-bins, strict-worktree-writes, reap-acked-orphans, product-acceptance — every arming flag disarmed on a forced re-scaffold, nothing reported the loss) together with any adopter-set branch_patterns. The forced write now starts from the existing file's bytes: the namespaced extensions (x-tracker, x-views, x-playbooks, x-import, x-worktree, x-github), version, branch_patterns and comments survive byte-for-byte, and only the x-generated provenance section is rewritten — it is the one part that must track the fresh scaffold. The keep/replace split is reported, never silent: the run's result carries the convention report (preserved — the top-level sections kept; replaced — the sections rewritten; rescaffolded — the enumerated exception), on both surfaces — --json carries the additive convention object and human output prints one arggon init: .convention.yml preserved …; replaced … line — and --dry-run's plan[] row for .convention.yml names the preserved sections in its reason (see docs/json-output.md). The exception is enumerated with its reason rather than left to fail silently: an existing file that is unreadable, empty, or cannot be read by this arggon carries no configuration worth keeping, so the fixed scaffold string is re-derived for it and rescaffolded: true says the whole file was replaced. The reader is a line parser, so --force re-derives only what no line parser can read — and the parser was widened so that the adopter's natural spellings are read rather than refused (bug-convention-parser-rejects-valid-yaml-force-rescaffolds): a trailing comment (allow-steal: true # armed is the boolean true), a post-start: | block scalar, and a quoted key ("x-tracker": is x-tracker). What it still refuses, it refuses by name — flow style x-tracker: {allow-steal: true}, anchors, aliases and tags are valid YAML this reader does not handle, and the parse error says so, because a diagnosis an adopter can act on is the difference between fixing a typo and losing their configuration. The boolean options keep one strictness that is a safety property, not a limitation: only the exact tokens true/false arm, so yes/on (YAML-1.1 spellings that YAML 1.2 reads as the string "yes") and a quoted boolean (allow-steal: "true", the string "true") are parse errors — widening for comments and quoting never widens what an arming flag accepts. Full syntax: .convention.yml syntax arggon reads. The generated docs lose nothing on the same path — the x-generated provenance has always ridden a forced re-scaffold (carried/carriedName), which is why untouched docs keep regenerating and adopter-modified docs stay skipped under --force too.
Line endings: CRLF working trees are supported (bug-crlf-provenance-breakage). Generated docs are always written with LF bytes; a .gitattributes like * text=auto eol=crlf in your repo may smudge them to CRLF on checkout, and that is fine: every provenance comparison — the x-generated checksum match, doctor's untouched/modified/acknowledged/acknowledgedDrifted buckets and its outdated render-vs-disk compare, init --propose's divergence decision and diffs, and {{PROJECT_NAME}} recovery — normalizes \r\n/\r to \n at compare time, in both directions (LF-recorded state on a CRLF tree, and an --ack recorded over CRLF bytes compared from an LF checkout). Checksums are still recorded over the bytes as generated/acked, and your files are never rewritten to change EOLs — so an eol=crlf working tree reports the same doctor buckets and proposals as the LF primary checkout. Regeneration keeps writing LF; the tolerant compares absorb whatever git smudges back, so it never re-breaks checksums.
Preview first: arggon init --dry-run (task-init-dry-run-plan). Before committing to an upgrade, --dry-run computes the exact same per-destination decisions a real run would make — without writing anything: no files, no backup/ dir, no auto-commit, no state mutation (a pure read; even the git tree stays untouched). It combines with --force / --full / --backup so the plan reflects the real invocation. Human output renders the plan as a decision table with a nothing was written (dry run) footer; with --json the usual init envelope gains dryRun: true and a plan[] array (per destination: decision — created | updated | modified-skip | modified-backup | acked-skip | orphan-reap | orphan-refused | overwritten — plus reason, see docs/json-output.md). Because the plan and a real run share one decision implementation, the plan's buckets map 1:1 onto what the real run then reports:
arggon init . --dry-run --full --backup # what would this upgrade do?
arggon init . --dry-run --json # same plan for agents
arggon init . --full --backup # then apply itUpgrading acked/modified docs: arggon init --propose (task-init-propose-acked-updates). The skip rules above mean a fully-adopted repo — every doc acked via arggon adopt --ack — receives zero template updates by mechanism ("it is yours"). --propose is the safe upgrade channel for exactly those docs: for every acked OR adopter-modified destination whose CURRENT template render differs from what is on disk, init writes the fresh render to a side file next to the original (<dest>.proposed-<arggonVersion>, e.g. ArggonManager/docs/agents.md.proposed-0.2.0). Originals are byte-untouched, the x-generated state is never mutated, and nothing is committed — proposals are untracked working files (add *.proposed-* to .gitignore if you do not want to commit them). Each proposal carries a header comment above the generated marker (<!-- arggon:proposed-update dest="..." version="..." generated="..."; diff against the original, merge what you want, then re-ack via arggon adopt --ack and delete this file -->) so an agent reading only the proposal file knows the flow.
Section-level backports are the default (spec-propose-section-backports-007). A whole-file render swap is a regression for a completed doc: it replaces curated content with a generic skeleton. So for every destination whose as-generated baseline is recoverable from git history (the dest's FIRST committed version — init auto-commits what it writes — is exactly what the old template rendered), --propose diffs that baseline against the current template render and proposes only the regions the template ADDED or CHANGED: one side file per dest, one delimited block per region, each quoting unchanged surrounding text (anchor-before/anchor-after) so the adopting agent can locate the insertion point in THEIR file. A removed-only diff (the template lost content the adopter has) is never proposed as a deletion — it is reported as informational and nothing is written. --propose-whole-file forces the old whole-render side files; whole-file is also the automatic fallback when the baseline cannot be recovered (no git history, untracked dest).
The full loop: propose → diff/merge → re-ack — run arggon init . --propose, the adopting agent diffs each side file against its original and merges what it wants as a normal work item, then refreshes the ack (arggon adopt --ack) and deletes the proposal file. Re-running --propose is idempotent: it overwrites its own same-version proposal (never accumulates <dest>.proposed-<same-version> junk); a destination that now matches upstream gets its same-version leftover removed and reported as absorbed; a proposal from a different (older) arggon version left behind is reported as stale and never silently deleted. Combine with --full to include tier-2 docs; --dry-run --propose lists what would be written/removed without touching anything; --backup/--force do not combine with --propose (proposals never touch originals or regenerate — that is a clear error). With --json, the init envelope gains an additive proposals[] array ({ dest, proposalPath, decision: proposed | absorbed | stale | informational, template, basedOnVersion, added?, removed?, mode?: sections | whole-file, regions?: [{ kind, added, removed }], note? }); human output lists each proposal with its mode and +added/-removed line summary (section mode lists every region). Pilot: ArggonStores-am's 14 acked docs.
arggon init . --propose --dry-run # which docs would get proposals?
arggon init . --propose # write <dest>.proposed-<version> side files
# ... agent diffs/merges each proposal as a work item ...
arggon adopt --ack # re-ack the merged docs; delete the .proposed-* filesThe per-client seams are enhancers: docs + CLI alone are a complete methodology, and an absent or failing adapter never bricks the workflow (ADR 0020, spec §S2). init therefore lets you choose which ones materialize — and nothing more: a deselected seam is simply never generated, and init never deletes an adapter file it was not asked for.
arggon init --agents opencode # only the OpenCode seam (config + agents/commands + vendored plugin)
arggon init --agents opencode,zcode # two of the three seams
arggon init --no-agents # docs + CLI only — no adapter seam at all
arggon init # default: every agent DETECTED in this tree--agents <list>takes the agent idsopencode,zcode,claude(comma-separated, case-insensitive, duplicates collapse; an empty list is an error, never a silent "all"). An unknown name is refused by name withINIT_FAILED(unknown agent "cursor" in --agents (known: claude, opencode, zcode)) and nothing is written — a typo must never silently install a different seam than the operator asked for.--agentsand--no-agentsdo not combine: passing both is refused in either order rather than letting the last one silently win.- With no flag the selection is every detected agent, from the tree's own markers (OpenCode's four config shapes or
.opencode/,.zcode-marketplace/,CLAUDE.md/.claude/.claude-marketplace— the repo's config, never what happens to be on your PATH). A brand-new empty tree carries no marker at all, and narrowing there to nothing would silently delete the seamsinithas always shipped, so a tree with no marker selects every known agent; narrow an existing tree with--agents. - Each agent owns a seam:
opencode—opencode.jsonc,.opencode/agents/**,.opencode/commands/**and the vendored plugin pair;zcode— the.zcode-marketplace/bundle;claude— the Claude Code plugin bundle under.claude-marketplace/(marketplace catalog + the vendored plugin: the thirteen/arggon:<command>commands, the maker/standards-reviewer subagents, the bundledarggon-cliskill and the hooks gate) plus theCLAUDE.md→@AGENTS.mdpointer and the.mcp.jsonregistration ofarggon mcp. The agent-agnostic artifacts — the generated docs, the templates, the CI recipe, the capability matrix and the.agents/skills/**copies (including the arggon-cli skill) — are never adapter artifacts and are generated whatever--agentssays. --jsonreports the outcome per artifact in an additiveadaptersblock: the resolvedselection(mode,known,detected,selected, and areasonnaming the rule that fired) plus one row per adapter destination —written,replaced(--backuparchived the original and regenerated it) orskipped— each with that destination's reason, and honestcountsthat sum to the total.--dry-runreports the identical block from the identical plan, so the preview's rows are exactly the ones the run writes. Full shape:docs/json-output.md§init.
arggon init --json --agents opencode | jq '.adapters | {mode: .selection.mode, counts}'Report-only installation check (exit 0, pure read): is ArggonManager installed here, and in what shape?
arggon doctor
arggon doctor --agents # + per-agent adapter seams + their matrix gap rows
arggon doctor --json
arggon doctor --json --budgetinitialized: whetherArggonManager/.convention.ymlwas found, plus the convention version (0-5).docs: generated-doc provenance counts fromx-generated—managed(tracked destinations),untouched(checksum matches),modified(checksum differs),acknowledged(sanctioned-diverged baselines fromadopt --ack),acknowledgedDrifted(acknowledged docs whose current checksum differs from the acked baseline — a hand edit after the ack; informational, still adopter-owned),stale(template no longer generated),missing(tracked but absent),outdated(the CURRENT template render differs from the on-disk content — task-doctor-outdated-bucket, additive: upstream template improvements are not reaching the doc, regardless of local state;docs.outdatedDocslists the affected destinations). Human output adds a hint line ("N doc(s) have newer templates — runarggon init --dry-runfor the plan") when any doc is outdated. The outdated check is pure read (re-render, compare, never write) and graceful: a template that is absent or unreadable is never counted outdated (it isstalewhen no longer generated).tracker: cheap tracker sanity — total work items andtodocount.matrix: the capability matrix read from the examined tree (adapters/capability-matrix.json, aninit-generated destination) —present/source, the invariant/row/gap counts and the capped gap rows; absent or malformed is a normal report, never a failure. Seedocs/json-output.md§matrixblock.agents(only with--agents, additive): per supported agent, the state of its adapter seam and that agent's capability-matrix gap rows — see below.budget(only with--budget, task-adr0006-remeasure): re-measures the ADR 0006 agent-facing context budgets with the 2026-09-14 baseline method — a freshinit --fullin a throwaway temp tree (always deleted), a deterministic 8-item fixture forlist --json(compact AND--full) andshow --jsonpayload bytes, the generated AGENTS.md bytes against its <=2048 B budget, and the live MCPtools/listpayload against its advisory <=12,288 B budget (task-schema-budget). Report-only; seeArggonManager/docs/json-output.md§doctor.
Non-initialized repos report initialized: false with zeroed counts (no crash, still exit 0); failures use error.code: "DOCTOR_FAILED" only for unexpected errors.
doctor --agents — the per-agent adapter report (additive; the mirror image of what init --agents selects, built from the same agent↔destination registry so the two can never disagree). Absent without the flag, so a plain doctor --json payload is unchanged:
- Per adapter file, one status, each naming what
arggon initwould actually do with the file:present(byte-equal to the current template render),acknowledged(byte-equal to theadopt --ackbaseline — sanctioned content init never regenerates; the normal state of a fully-adopted tree),acknowledged-drifted(a hand edit landed after the ack — still yours, still never regenerated),adopter-edited(a hand edit on an ordinary generated file, or a file on disk with no provenance state at all, which init also refuses to rewrite),stale(state-backed, untouched, but upstream moved — the one status that meansinitwould refresh it),missing,unverified("cannot decide", never a failure), ororphaned(the recorded template is gone from this arggon version — the one status a singleinitwould delete). Eachorphanedrow carries its own verdict (reap.action:reap=initremoves it,refuse=initkeeps it) plus the reason, so the report can never promise a removalinitwould refuse; a refusal names the arming flag (x-tracker.reap-acked-orphans) or the edit that caused it; the human report adds one indentedorphan: <path> — <reason>line per file it would keep. No status promises a protection, a refresh or a deletion init does not provide. "The template moved on" is orthogonal and stays indocs.outdated. The derived vendored plugin copies are alwayspresentwhen they exist — init re-vendors those from the committed bundle rather than skipping them — and this report'sopencodeline carries the sharpervendored plugin current/STALE/unverifiedverdict for them. - Per agent, that agent's capability-matrix gap rows, read through the same reader the
matrixblock uses (never a second parse), with the honest per-agent total — so zero gaps is never ambiguous between "no gaps" and "no matrix". Gap rows stay advisory: printed, never blocking, in every surface. - Still report-only: nothing is written, the exit code is unchanged, and the block prints on non-initialized trees too. Shape:
docs/json-output.md§agentsblock.
$ arggon init --agents opencode . && arggon doctor --agents
agents: opencode (adapter selection: init --agents / --no-agents; report-only)
agent claude: 28 file(s) — 0 present, 0 acknowledged (yours, never regenerated), 0 acknowledged-drifted (hand edit after the ack), 0 adopter-edited (yours; init skips it unless --backup), 0 stale (arggon init would refresh it), 28 missing, 0 unverified, 0 orphaned (template gone from this arggon; init removes it when unmodified, else you delete it by hand); 1 capability gap(s) — not detected in this tree
gap: docs-travel-with-code x claude — the bundle ships no session context hook: Claude Code has no plugin surface that injects the active work item, so the agent fetches it with `arggon show`. (report-only)
agent opencode: 18 file(s) — 18 present, 0 acknowledged (yours, never regenerated), 0 acknowledged-drifted (hand edit after the ack), 0 adopter-edited (yours; init skips it unless --backup), 0 stale (arggon init would refresh it), 0 missing, 0 unverified, 0 orphaned (template gone from this arggon; init removes it when unmodified, else you delete it by hand); 0 capability gap(s)
agent zcode: 23 file(s) — 0 present, 0 acknowledged (yours, never regenerated), 0 acknowledged-drifted (hand edit after the ack), 0 adopter-edited (yours; init skips it unless --backup), 0 stale (arggon init would refresh it), 23 missing, 0 unverified, 0 orphaned (template gone from this arggon; init removes it when unmodified, else you delete it by hand); 0 capability gap(s) — not detected in this tree
After the arggon version that shipped a template drops it, the same block shows the stranded destination before anything is removed, and any orphan init would keep gets its own line with the reason:
agent opencode: 19 file(s) — 17 present, 0 acknowledged (yours, never regenerated), 0 acknowledged-drifted (hand edit after the ack), 1 adopter-edited (yours; init skips it unless --backup), 0 stale (arggon init would refresh it), 0 missing, 0 unverified, 1 orphaned (template gone from this arggon; init removes it when unmodified, else you delete it by hand); 0 capability gap(s)
orphan: .opencode/agents/arggon-verifier.md — adopter-edited since generation — refused; your edits are never deleted. Delete the file by hand if the template is really gone
Agent-assisted adoption for existing repos (run after arggon init): turns "start using ArggonManager here" into a tracked, agent-executable migration task instead of a half-finished doc sweep.
arggon adopt # files task-adopt-arggon with the migration checklist
arggon adopt --dry-run # print the inventory + planned actions, write nothing
arggon adopt --story story-onboarding
arggon adopt --ack # acknowledge the current generated docs as the new checksum baseline
arggon adopt --json # v1 envelope: { taskId, storyId, storyCreated, createdContainers, taskCreated, skipped, taskPath, inventory, dryRun }
arggon adopt --ack --json # v1 envelope: { acked: [{ path, checksum }], count }- Pre-flight: requires an initialized tree (same detection as
arggon doctor); otherwise fails withADOPT_FAILED("not an arggon-managed tree — runarggon initfirst"). - Inventory (read-only): every governing doc at the standard destinations —
AGENTS.md,CLAUDE.md,CONTRIBUTING.md,SECURITY.md,.editorconfig,ARCHITECTURE.md, the.githubfiles,ArggonManager/docs/convention.md,ArggonManager/docs/engineering.md,ArggonManager/docs/runbooks/,README.md— plus common alternates (.cursorrules,ArggonManager/docs/index.md, CONTRIBUTING variants). Each entry recordsexists,bytes, andmanaged(true when anx-generatedprovenance entry exists — arggon-generated; an existing file without one is adopter-owned, i.e. content to extract). Cheap stack hints: presence ofpackage.json/requirements.txt/go.mod/Cargo.toml/pom.xml(filenames only). - Task:
task-adopt-arggon— "Adopt ArggonManager in this repo", statustodo, under--story <story-id>when given, else understory-arggon-adoption(auto-created under the first epic). On a freshinit --fulltree that has NO epic at all, adopt auto-creates the minimal container chain — initiativearggon-adoption→ epicepic-arggon-adoption(both titled "ArggonManager adoption") — so the first-run adoption needs zero guesswork (the created ids are reported in the human output and in the additivecreatedContainersJSON field). Adopting an EXISTING repo that already has its own structure should pass--storyto place the task under the right story instead. The body is the full agent checklist: read the generated docs, sweep the adopter docs, fill the TODO placeholders, archive replaced originals tobackup/<YYYY-MM-DD>/(never archive README.md — merge into it), create one playbook per detected technology, ack the sanctioned edits, verify, and report viaarggon comment. - Idempotent: an already-open (
todo/in_progress)task-adopt-arggonis reported (skipped: true), not duplicated; a terminal one means adoption already completed and re-running errors. The files written this run (task + story) are auto-committed as ONEchore(tasks): adopted task-adopt-arggoncommit (--no-commitkeeps them dirty). - Baseline (
--ack, standalone): after the sweep filled the generated docs,arggon adopt --ackacknowledges their CURRENT on-disk content as the newx-generatedbaseline — checksums are recomputed from disk and the state entries refreshed (version + timestamp) — so the sanctioned sweep edits stop reporting as modified and no longer shadow every future template upgrade. It writes onlyArggonManager/.convention.yml, creates nothing, ignores files absent from the state, and works even whentask-adopt-arggonis already done. Hand edits made AFTER the ack still report modified — the protection stays intact.
--dry-run prints the same inventory (managed vs adopter-owned vs absent, per-doc sizes, stack hints) and the planned story/task creation without writing anything — including the default story. Executing agents follow the checklist in the task body; the full procedure is documented in ArggonManager/docs/agents.md §Adoption sweep.
Finds the tracker root with walk-up from cwd (same as create), loads work items with the shared kernel, and prints a table (or --json). Listing order is lexicographic by id. Filters compose with AND.
arggon list
arggon list --status todo
arggon list --type bug --assignee @me
arggon list --parent story-login
arggon list --json
arggon list --json --full # complete WorkItem shapes (default: compact per ADR 0006 — null/empty optional fields omitted)
arggon --json list --type task --status in_progress
arggon list --filter "status:todo !label:security"
arggon list --view my-open-bugs
arggon list --stale --older-than 7d- --status : exact v0 status (
todo,in_progress,blocked,done,cancelled) - --type : exact v0 type (
initiative,epic,story,task,bug) - --parent : exact parent item id — sugar for the
parent:filter predicate; ANDed with the other flags. Unlike the raw predicate, an unknown parent id fails withLIST_FAILEDinstead of returning an empty list - --assignee : exact assignee. Special @me resolves via
GITHUB_USER, thenGITHUB_ACTOR, thengh api user -q .login - --filter : compact filter ANDed with the flags (fields
status,type,assignee,label,parent,depends-on,blocked-by,ancestor;!negates; quotes allow spaces, e.g.assignee:"Jane Doe"); unknown fields are usage errors.depends-on:<id>matches items whosedepends_oncontains<id>;blocked-by:<id>matches the computed inverse — items that<id>depends on;ancestor:<id>matches items with<id>anywhere in their parent chain (e.g.arggon list --filter "status:todo ancestor:launch-mvp"— everything still open under the initiative) - --view : named saved view from the
x-viewsmap inArggonManager/.convention.yml, ANDed with the flags and--filter(e.g.x-views:\n my-open-bugs: "type:bug status:todo !assignee:someone"); unknown names fail listing the known views - --stale --older-than : stale-claim report (advisory only). Matches claimed items (in_progress + assignee) whose
claimed_atlease is older than the threshold relative to now;<duration>is<number><d|h|m>(e.g.7d,12h,30m); invalid durations fail withLIST_FAILED. Items claimed beforeclaimed_atexisted (noclaimed_at) count as stale. Composes with the other list filters;--stalerequires--older-thanand vice versa - --json: one compact JSON object on stdout (envelope v1:
ok,schemaVersion: 1,conventionVersion,command: "list",items: WorkItem[]); failures emitok: falsewithcode: "LIST_FAILED"
Empty results exit 0 (items: [] with --json). Missing a tracker, invalid enums, unresolvable @me, or unreadable work-item files exit non-zero.
arggon create <type> <title> writes a work item under the tracker root (ArggonManager/) from the templates. Non-initiative types need --parent <id>. Defaults: status: todo, created/updated today. Flags: --id, --assignee, --labels <csv> (label at creation; same kebab-case rules as update --labels), --status (not done), --blocked-reason. Tracker hygiene: the created item file is auto-committed (chore(tasks): created <id>, staging only that path — your pre-existing dirty files are never swept in); opt out per call with --no-commit or tree-wide with ArggonManager/.convention.yml x-tracker.auto-commit: false. Non-git trees skip the commit silently.
arggon update <id> edits frontmatter in place (only requested fields). Enforces v0 status transitions, the claim rule (in_progress on story/task/bug needs --assignee), and blocked_reason rules; always touches updated. Done gate (task-done-gate-acceptance-waiver, ADR 0015): a task/bug cannot flip to done while its body still carries unchecked acceptance checkboxes (- [ ] with text after the box; an empty box is a scaffold placeholder, not a criterion — bug-empty-template-checkbox) — the refusal names the escape hatch. Tick every box (the honest path), or pass --waive "<reason>" with a non-empty reason: the kernel records a dated ### Waiver <date> section in the item body BEFORE flipping, so the rationale travels with the item (agents cannot waive — the MCP/native surfaces expose no waive parameter and the kernel refuses agent callers; containers are not gated, their contract is the acceptance-aware cascade; --waive is refused when there is nothing to waive). Transitions in_progress → todo clear assignee and branch by default (override with explicit --assignee / --branch); leaving blocked clears blocked_reason. Reassigning a claimed item fails with a claim conflict unless --force (see ArggonManager/docs/claim.md). Claiming sets the additive claimed_at lease (ISO date-time, also set by arggon start); unclaim/todo or closing the item clears it. claimed_at is reporting only — it never gates a transition. Unclaiming does not release a worktree: in_progress → todo on an item with a recorded worktree_path keeps the worktree, its .arggon.env and its arggon-claim.json stamp (only assignee, branch and the lease go), so the update reports the surviving footprint as the additive claimFootprint receipt ({ worktreePath, release: { cli, native } }) and names the release path — arggon cleanup --release <id>; the native update tool carries the same receipt with the tools.arggon.cleanup({ release }) spelling. The receipt is unclaim-only: a → done flip leaves the same footprint but its remedy is cleanup --prune (merge-gated), and a blocked item keeps its assignee, so neither names a release. claimFootprint never gates anything — it is reporting only. Flags: --title, --status, --assignee, --branch <name> (empty clears), --unassign, --labels <csv> (replace), --depends-on <csv> (replace the dependency-id list, v3; empty clears), --add-depends-on <id> (append one dependency), --blocked-reason, --force, --steal --reason "<why>" --assignee <you> (human-only supervised takeover of a claimed item: reassigns to you, refreshes claimed_at, and appends a dated > stolen <date> by <you>: <reason> note to the item body; hardening in bug-cli-steal-not-gated — BREAKING: the repo must opt in via x-tracker.allow-steal: true in ArggonManager/.convention.yml, and the command must run at an interactive terminal with a y/N confirmation, so scripts/CI/agents are always refused; agents are also refused by the playbook rules like --force; mutually exclusive with --force), --waive "<reason>" (human-only recorded waiver for the done gate, above), --json (envelope v1 { item } + additive autoCompleted/cascadeLevels/cascadeSkipped/commit, failures UPDATE_FAILED). Dependencies are advisory: they never block updates — validate checks unknown ids, self-references, and cycles (ArggonManager/docs/convention.md, v3). Tracker hygiene: a run that actually changes something auto-commits every item file it wrote — the updated item plus any cascade-completed ancestors in ONE chore(tasks): done <id> (cascade: <ancestor ids>) commit (verbs done/claimed/updated; staging only those paths), so done-flips land as commits that report --trend can see; --no-commit keeps the tracker dirty, x-tracker.auto-commit: false opts out tree-wide, non-git trees skip silently. --parent <new-parent> reparents the item (task-update-reparent): it validates the edge exactly like create (task/bug under a story, story under an epic, epic under an initiative; an unknown parent, a wrong parent type, or a parent that is the item's own descendant fails with UPDATE_FAILED and moves nothing — the current parent is a documented no-op), rewrites the parent field, and MOVES the file/directory per the convention layout rules — leaves (task/bug) move as a file into the target story's directory, containers (story/epic) move their whole directory with all children (children's frontmatter is untouched; parent is an id, not a path) — and the tracker auto-commit stages both the old and the new paths so git history follows the move. Example: arggon update task-rate-limit --parent story-handbook (human output prints moved from: <old path>; --json adds the additive movedFrom field). --type story promotes a task to a story IN PLACE (task-promote-task-to-story) — the natural repair after import-issues flattens an idea as a leaf: it validates the target epic first (the task's grandparent; missing/invalid epic, bugs, already-story items, and demotion --type task all fail with UPDATE_FAILED and move nothing), then moves the file to the story layout under that epic exactly where create story would put it, flips type and parent, and renames the id task-x → story-x (container ids must not start with task-/bug-; depends_on references are rewritten tree-wide). Everything else rides along untouched — including the issue field, so a promoted item keeps its GitHub Closes #N round-trip. The promoted story starts empty of children. Example: arggon update task-issue-12 --type story → story-issue-12 (human output prints moved from: and renamed from id:; --json adds additive movedFrom/renamedFrom).
arggon branch <id> checks out the working branch for an item: uses the recorded branch field when set (attach), else generates it from branch_patterns in ArggonManager/.convention.yml ({id}/{type} placeholders; defaults feat/{id}, fix/{id} for bugs) and persists it. Fails clearly when the branch exists without matching the field. Flags: --json (envelope { item, branch, created }, failures BRANCH_FAILED).
arggon start <id> claims (in_progress + --assignee, never --force), checks out the branch, commits the claim, pushes, and with --open-pr opens a draft PR with the item id in the body — when the item carries an imported GitHub issue number (the issue frontmatter field set by import-issues), the PR body ends with Closes #N so GitHub closes the issue on merge. The clean-tree precondition is scoped: modified or staged tracked files always block (they could collide with the claim commit), as do untracked files inside the tracker dir (they would pollute the tracker), but untracked files elsewhere (agent-environment dirs like .v2c/) do not — the claim commit stages only the item file, so unrelated untracked files can never land in it. Refuses taken claims. With --worktree the whole flow runs inside a linked git worktree at ../<repo-name>-<id> (created from, or attached to, the item's branch): the claim commit, push, and draft PR run there, the main checkout stays on its current branch, and the absolute worktree path is recorded on the item's additive worktree_path field. The branch — plain start and --worktree alike — is cut from the primary checkout's current HEAD, so unpushed commits there — tracker auto-commits included — ride the branch's PR diff as unrelated churn; when this run cuts a branch and inherits any, start reports them additively as inheritedCommits ({ base, branchPoint, total, trackerTotal, workTotal, commits, itemOnBase, remedy }, commits capped at 10, each tagged [tracker] for the CLI's own chore(tasks): … auto-commits or [work] for yours) and prints two note: lines naming the remedy — push the primary branch (git push origin main) so the inherited commits are already on the base, or drop them (git rebase --onto <base> <branchPoint> <branch>) — never drop them when itemOnBase is false, because the item file itself rides one of them; report-only, the claim always proceeds. Before the claim commit, start prepares the worktree: when the primary checkout has node_modules and the worktree does not (a fresh worktree never does), the primary install is linked in (best-effort; additive linkedNodeModules in --json, printed on stdout) so a dependency-needing pre-commit gate (npm run arggon -- validate) can run. The install is a per-worktree link farm — a real node_modules directory whose entries link the primary's packages — or a bare symlink to the primary install when the worktree shadows no workspace package; a workspace package the worktree carries its own copy of resolves to the worktree copy: start runs the package's own build script before the claim commit when that copy has no build output yet (the built packages are printed on stdout), so the gate loads the branch's kernel instead of the primary's build. The build's exit is honored — a failed build falls back to the primary's copy even when it still emitted the entry — and a build the install cannot consume (a bare symlink with no farm to flip) is skipped, so an attach re-run does not rebuild for nothing. A copy that could not be built stays on the primary's install and is named by the additive linkedWorkspaces in --json (printed on stdout too) — build where the imports resolve, or give the worktree its own install (x-worktree.post-start: npm ci reifies the workspace links locally and its prepare builds them). Start never commits the install — the claim commit stages only the item file — so stage explicit paths, never git add -A. Before a configured x-worktree.post-start hook runs, start removes the link (so the canonical npm ci cannot reify through it and empty the primary checkout) and re-links only when the hook leaves no node_modules — hooks are never bypassed (--no-verify is never passed). A start that created the worktree must leave a gate-usable install: when the preparation leaves a gate binary resolving outside the worktree, the claim is refused before the claim update with the named bins, the prepSteps preparation log (which prep path ran — link farm created / reused / skipped-and-why, per-workspace build decisions) and the npm ci fix (bug-start-install-ordering); attach re-runs keep the report-only receipt unless x-tracker.strict-gate-bins is armed. A failure after the worktree exists never rolls it back: the worktree and branch are kept, and the error names the failing step, the worktree path, a remediation and the fact that re-running attaches; re-running with --worktree attaches instead of failing and (when the claim commit was the failing step) retries it. Every start --worktree stamps its ownership in <worktree git dir>/arggon-claim.json and, on an attach, reads the previous stamp first: a different owning session plus tracked files newer than the stamped claim is a named, bounded warning (claim.foreignWrites, the single-writer check for concurrent writers in a claimed worktree — advisory by default; x-tracker.strict-worktree-writes: true refuses the attach instead, and a refused attach never re-stamps the worktree, so retrying cannot unlock the gate). When the stamped session is dead, start --worktree --take-over-worktree is the designed recovery (default OFF): it applies only to a fired detection, re-stamps the worktree with your identity and records a dated take-over naming the replaced stamp (claim.takeOver in --json, printed as note: single-writer take-over — …, and kept as a bounded takeovers chain in the stamp) — never use it against a live owner. After creating a new worktree, a configured x-worktree.post-start shell command (e.g. npm ci) runs once inside it to bootstrap the checkout — failure is reported, never fatal; --no-hook skips it per invocation (see ArggonManager/docs/convention.md). Flags: --assignee (default GITHUB_USER/GITHUB_ACTOR), --open-pr, --worktree, --take-over-worktree, --no-hook, --post-start-shell <inherit|login> (run the hook via your login shell so rustup/mise/asdf toolchains are on PATH), --json (envelope { item, branch, created, pushed, prUrl, worktreePath, inheritedCommits?, linkedNodeModules, linkedWorkspaces, postStart? }, failures START_FAILED).
Every --worktree checkout gets a portable runtime environment (spec worktree-env-contract-016, ADR 0019 layer 1): start writes a gitignored, dotenv-style .arggon.env at the worktree root carrying the per-worktree identity — ARGON_ITEM (the name the OpenCode plugin already correlates sessions with), ARGGON_WORKTREE_ID (<repo>-<item-id>), ARGGON_WORKTREE_PATH, ARGGON_WORKTREE_BRANCH, and ARGGON_STATE_DIR/ARGGON_CACHE_DIR (per-OS state/cache bases — XDG on Linux, ~/Library on macOS, %LOCALAPPDATA% on Windows — suffixed with the worktree id, created mkdir -p). Two worktrees of one repo never share these paths, so N parallel runs stop colliding on singleton state; pair the file with your own conventions: servers bind an ephemeral port (:0) or one derived from the worktree id, and services are isolated by identifier (per-worktree database name/schema) against one local server. A primary .env is seeded into the worktree copy-if-absent (never overwritten, never interpreted); an existing .arggon.env (attach re-run) is left byte-identical; the git check-ignore .arggon.env probe is reported as gitignored in the additive env receipt field (written, path, keys, seededDotenv, gitignored, warning — written false with a warning never blocks the claim). Generated .gitignores from arggon init ignore .arggon.env from day one; cleanup --prune reaps a start-created env file (contract shape only) before removing the worktree, so an unignored one never wedges pruning. Opt out tree-wide with x-worktree.env: false in ArggonManager/.convention.yml (see ArggonManager/docs/convention.md).
arggon cleanup reaps worktrees of closed work (see start --worktree above). Every item with a recorded worktree_path is classified: removable when the item is done/cancelled and its recorded branch is fully merged into the default branch (origin/HEAD, else local main/master); anything else (open item, unmerged or missing branch) is reported as skipped and never touched. Squash merges: a squash commit is never an ancestor of the feature branch, so when the ancestry check fails, cleanup queries gh for a MERGED PR whose head branch matches (gh pr list --state merged --head <branch>); a merged PR is treated as proof of integration and the candidate becomes prunable (its branch is deleted with git branch -D, and the prune actions are annotated via: "squash-merged PR #N"). When gh finds no merged PR the candidate is skipped with branch not merged and no merged PR found; when gh is unavailable (missing binary, unauthenticated, failed) it is skipped with ancestry check failed and gh is unavailable to check for squash-merged PRs — the gh lookup never breaks the run. --no-gh skips the fallback entirely (ancestry-only) for offline/CI use. Remote safety: when an origin/<branch> upstream exists, its tip must also be merged into the base before anything is removed (a lost final push leaves the remote tip behind); such candidates are skipped with remote branch divergent or behind (<origin/branch>) — push or delete the remote branch first. The default mode only lists; --prune removes removable worktrees, deletes their merged branches (git branch -d), and clears the worktree_path records, reporting each action. A recorded path whose worktree is already gone — missing on disk, or a leftover directory whose gitdir link is gone after an interrupted git worktree remove (git lists the record as prunable and then refuses its removal forever) — is a stale record, not a removal candidate: --prune skips the removal, prunes the dead git record itself (no manual git worktree prune step is ever needed — and the dead record pins the branch, so it has to go before git branch -d), still deletes the merged branch and clears the worktree_path record, and leaves any leftover directory in place. Removal-blocking content is classified up front (bug-cleanup-prune-untracked-artifacts-no-force): untracked blockers (generated scaffolding that only ever lived in the worktree) are skipped with blockingPaths/blockingTotal and a reason naming --force and the manual sequence, unless --force is passed — which reaps exactly that entry through git worktree remove --force (reported as forced: true) and is refused without --prune; tracked modifications are refused with or without it (the opt-in never discards uncommitted tracked work); the start-owned artifacts every run reaps itself (start's install link/farm, a start-created .arggon.env of the exact contract shape) are never blockers. The cleared records are auto-committed (chore(tasks): pruned <ids>, one commit, only those item paths; --no-commit keeps them dirty, x-tracker.auto-commit: false opts out tree-wide). Flags: --prune, --force, --release <id>, --take-over-worktree, --no-commit, --no-gh, --json (envelope { base, candidates, pruned, failures } + additive commit, and additive release/released under --release; pruned entries may carry action: "failed" with additive error and leftoverBranch — per-candidate, the run continues; CLEANUP_FAILED is reserved for top-level errors like a non-git tree or undetectable default branch). Declared per-worktree services (ADR 0019 layer 2, worktree-services.md): when the repo's .convention.yml declares x-worktree.services (the Compose project name — true for <repo>-<item-id>, or the pattern's base name for <base>-<repo>-<item-id>), --prune also tears down each removable worktree's Compose project with docker compose -p <project> down -v --remove-orphans before removing the worktree; with no declaration — or no docker CLI — cleanup never invokes Docker, so the safety gate is by construction: an adopter-run project the convention does not declare is never touched (see ArggonManager/docs/convention.md for the full semantics). Releasing an abandoned claim (--release <id>): --prune only reaps FINISHED work (terminal item + merged branch), so it cannot reap a claim that went back to todo; that is the gap update --status todo leaves (bug-unclaim-leaves-worktree-record-without-reaper). --release <id> is the inverse of start --worktree: it removes that item's worktree, deletes its branch (force — an abandoned branch is by definition not provably merged), reaps the start-created .arggon.env and the claim stamp, and clears the worktree_path record, reported as its own release/released action family (reaped compose project <name>, removed worktree <path>, reaped arggon-claim.json stamp / arggon-claim.json stamp gone with the worktree, deleted branch <b>, cleared worktree_path) — never mixed into pruned. Run it from the checkout whose tracker copy carries the record: the claim commit rides the feature branch, so before it merges that copy is the worktree itself. A recorded path whose worktree is already gone (missing on disk, or leftover debris whose gitdir link is gone after an interrupted git worktree remove) is a stale record here too (bug-cleanup-release-stale-record-wedge): the release skips the removal, still reaps the declared Compose project exactly as --prune does for its stale entries (the project name derives from the recorded path, not a live directory — task-release-stale-compose-reaping), prunes the dead git record itself, still deletes the branch that record names and clears the worktree_path record — the leftover directory is left in place. Refusals keep the whole footprint intact and are reported on both released (action: "failed") and failures: an item still in_progress with an assignee is refused unconditionally (--take-over-worktree does NOT bypass it — a re-claimed item has a live owner), a stamp held by another identity whose window shows a live writer is refused with that evidence, and a worktree with uncommitted/untracked content is refused before anything is reaped (blockingPaths/blockingTotal) so a failed release never strips a surviving worktree of its stamp. --take-over-worktree is the audited hatch for a presumed-dead stamped owner on an unclaimed item — the only thing that forces a release's removal (prune's own, narrower opt-in is --force) — and is refused without --release. --release and --prune are mutually exclusive.
arggon next suggests the next claimable leaf item: unclaimed todo of claimable type, lexicographic by id, with parent chain and reason. Stories are excluded from the default pool (task-next-pool-stories): next answers "what do I implement next" — leaf work (task/bug leaves) first — while claiming a story is a planning act done explicitly via update/start; --include-stories opts stories back into the pool. Dependency-aware (v3): ready items — those whose depends_on are all done/cancelled — rank first, ordered priority-major (ADR 0009): the orchestrator's priority (p0 best; unprioritized orders with the p3 tier) decides first, then downstream weight — how many items become claimable once it completes (transitive dependents closure, cycle-safe; the additive unblocks: number field and the reason line unblocks N item(s) downstream surface it, so the cost of a misprioritization is visible) — with lexicographic id as the deterministic tie-break, and when the suggestion still has open dependencies the reason lists them and the JSON suggestion carries the additive blockedBy: string[] (open dependency ids). Dependencies are advisory: they shape suggestions only, never updates. Empty pool exits 0 with a friendly message ("only stories are left" is distinguished from a fully empty pool). Flags: --ready (limit the pool to ready items only), --include-stories (include unclaimed stories), --json (envelope { suggestion: { item, parentChain, reason, blockedBy } | null }, failures NEXT_FAILED).
Reads ONE item with bounded output (ADR 0006): comment tails grow every whole-file read, so show returns the frontmatter fields plus only the LAST 3 comments by default. Pure read — never writes, no lock, no tracker commit. Flags: --meta (frontmatter only), --body (full body including ALL comments — the unbounded explicit opt-in), --tail-comments <n> (compact-view tail size), --json (envelope { item, path, comments } + body under --body; failures SHOW_FAILED).
arggon show task-rate-limit
arggon show task-rate-limit --meta
arggon show task-rate-limit --tail-comments 10
arggon show task-rate-limit --bodyShared kernel (@arggondev/lib, ADR 0013): lib/src/paths.ts, frontmatter.ts, ids.ts, status.ts, items.ts, relations.ts, dates.ts.
Renders the goal-mode contract for ONE claimed item — the objective, the verification contract, the plan→execute→verify loop and the hard boundaries — derived from that item's acceptance checklist in git (pure read; no lock, no commit). It is the ZCode Goal Mode adapter (spec methodology-adapters-017 §S5): arggon init generates the template at .zcode-marketplace/arggon/templates/goal-mode.md (same provenance + never-overwrite rules as the rest of the seam) and arggon goal instantiates it — your copy of the template wins when present, the packaged one is the fallback. Whether work remains is the kernel's answer, not this command's, and the kernel is the ONLY parser left to ask. Since bug-three-acceptance-parsers-diverging (PR #611) the one acceptance grammar lives in lib/src/items.ts (acceptanceRows / acceptanceCriteria / acceptanceUnchecked / acceptanceComplete, plus acceptanceBody for the one canonical input) and this command reads BOTH the verdict and the criterion text through it — the same rows, over acceptanceBody(item) (the whole body, comment sections included), so the contract cannot disagree with the done gate or with its own text. The grammar's decisions (which lines are rows, which rows gate) are documented in ArggonManager/docs/convention.md §Acceptance rows; the kernel's own parity corpus is cli/src/acceptance-parity.test.ts. Normally the objective is the first unchecked criterion and the verification contract is every unchecked one; when the gate says the contract is satisfied the goal is an explicit “define the goal first” contract, never an empty one. Bounded output: the objective and each verification line are byte-clipped, at most 8 criteria are inlined and the rest are counted in verificationOmitted (clipping the ROWS is the kernel's guidance for a consumer that renders them), and the template copy is clipped before rendering — so a tampered or oversized item (or an inflated template) cannot produce an unbounded contract.
One goal per claimed item, one worktree per item. arggon goal refuses — no override flag, refusal codes in the envelope — an unresolvable caller identity (GOAL_IDENTITY_UNKNOWN: the claim holder cannot be proven to be you, so nothing renders), a done/cancelled item (GOAL_ITEM_CLOSED), an unclaimed item (GOAL_UNCLAIMED; claim it with arggon start <id>, which creates the worktree), an item claimed by another identity (GOAL_FOREIGN_CLAIM), and any run from a checkout that is not the item's recorded worktree_path or where that worktree no longer exists (GOAL_WORKTREE_MISMATCH / GOAL_WORKTREE_MISSING); a missing template is GOAL_TEMPLATE_UNAVAILABLE and an unknown id / missing tracker is GOAL_FAILED. Pointing a goal at another session's worktree is a refusal, not a mode. The boundaries and refusals are appended by the CLI, not read from the template file, so editing the template cannot drop them — and they name the dispatch-scoped reviewer backstop (read-only while a reviewer is in flight) and the kernel as the enforcement of record. Flags: --json (envelope { item, path, goal: { objective, verification, boundaries, refusals, truncated, hasGoal, gateUnchecked, checklist, worktree, template, identity, contract } }; failures GOAL_FAILED + the refusal codes above).
arggon goal task-rate-limit # the rendered contract (paste into Goal Mode)
arggon goal task-rate-limit --json # the structured view + the same contract textBeside the goal-mode contract the ZCode seam ships two automation contract templates (spec methodology-adapters-017 §S5, plan T5): arggon init vendors .zcode-marketplace/arggon/templates/automations/spec-drift-scan.md (daily) and stale-claim-sweep.md (weekly) with the same provenance + never-overwrite rules as every other seam file. Opt-in: init schedules and runs nothing. A template is a contract an adopter wires into their own scheduler (cron, CI timer, agent loop) at a cadence they chose; ignoring or deleting the file is the off switch. Each carries the claim/branch preconditions — it holds no claim, runs inside no item's worktree_path, never claims/branches/checks out, and refuses to run anywhere but the primary checkout on the default branch with a clean tree (filing appends the tracker auto-commit, which must never land on an item's feature branch). The scan is read-only — arggon validate, arggon spec validate, arggon spec analyze, arggon doctor --agents, and for the sweep arggon list --stale --older-than 7d + arggon show — and the only write either may ever make is arggon create: one item per NEW drift finding or per deduplicated stale claim, filed under the drifted spec's story (resp. the stale item's parent story) with the evidence in an arggon comment on the new item. Neither automation ever touches the items it scans — no arggon update, no comment on a scanned or stale item, no claim release, no hand git commit — the kernel invariants outrank the contract. Pinned by cli/src/init-zcode.test.ts (provenance, never-overwrite, preconditions, a read-only allowlist over every quoted scan command, create-only filing, the opt-in declaration).
Checks tracker frontmatter and tree integrity (schema, parents, naming, claim/blocked rules). Uses the shared items soft-scan (walkTasksTree / softTryLoadItem) so broken YAML still reports a file path. Exits non-zero when there are errors; warnings alone stay exit 0. Suitable for CI before commit.
arggon validate
arggon validate --json
npm run arggon -- validate--json: v1 envelope witherrors/warnings(andVALIDATE_FAILEDwhen failed)
Validate: arggon validate / arggon validate --json (CI gate; ArggonManager/docs/json-output.md).
Writes a self-contained HTML board (columns = v0 statuses; cards within each column sort by most-recently-modified updated descending, id on ties — the grouped-ancestry tree keeps its id-sorted ancestry grouping; cards show type, id, title, assignee, labels, parent, blocked_reason, milestone, plus a ↳ blocked by <id> line per open dependency — v3; deps that are done/cancelled render nothing) from the same kernel read path as list. No writes to the tracker — the output is a generated snapshot; git files remain the source of truth. Stack decision: ArggonManager/docs/adr/0002-board-viewer-v0.md. Drag-and-drop is a client-side pre-check: the embedded script applies exactly the CLI drop rules, and every edit routes through the kernel update path — never raw file writes from the browser.
arggon board # writes board.html at the repo root (where the tracker lives)
arggon board --out report/board.html # explicit path, relative to cwd
arggon board --json # v1 envelope: { path, itemCount }
arggon board --github # overlay live GitHub PR state on cards with a branch (read-only)
arggon board --group-by milestone # prototype (ADR 0003): group cards within each column
arggon board --group-by story # group cards within each column by parent story
arggon board --group-by tree # grouped ancestry (initiative → epic → story → leaf): opens in the tree view
arggon board --details # embed a bounded per-item detail so the drawer works offline (file://)
arggon board --serve # local live-reload server on 127.0.0.1 (edits via the update path)
arggon board --serve --port 4173 # pick the port
arggon board --tui # interactive terminal kanban (raw ANSI, no deps; c/m write via the update path)--out <file>: output path (defaultboard.htmlat the repo root regardless of cwd); parent directories must exist--json: one JSON object on stdout; failures emitcode: "BOARD_FAILED". The payload contract for every mode (which fields appear with--serve,--github,--group-by) is documented once in ArggonManager/docs/json-output.md — theboardsection there is the normative source--github: onegh pr listread matched by head ref name → per-card badge (#N · draft/open/merged/closed+ checks✓/✗/…, neutral○ no PRwithout branch or PR); without gh auth fails clearly suggesting plainboard; never writes to the tracker--group-by milestone: prototype per ADR 0003; items without a milestone group last--group-by story: group cards within each column under parent-story headers (sorted ascending); parent-less cards render last under ano storyheader only when the column also has parent groups; stories without cards never render. Not combinable with--tui(the TUI groups by status column only) — combining them fails withBOARD_FAILED; combinable with--serveand--json--group-by tree: opens the board in the grouped-ancestry layout (the layout bullet below has the full contract); combinable with--serveand--json; refused with--tuilike every--group-byvalue- filter lens (client-side; static export and
--servealike): every board carries a search/filter box, and the tracker'sx-viewssaved views render as lens chips (name +name: expressiontooltip;@meresolves at generation time likelist --view). Semantics are 1:1 witharggon list --filterfor free text on id/title plustype:,status:,label:,assignee:,priority:,ancestor:,parent:,depends-on:andblocked-by:— quoting,!negation, enum errors and@meresolution included, with a parity suite pinning the mirror against the kernel (blocked-by:is the computed inverse over the whole board, and dependencies are read in either shape, kerneldependsOnor contractdepends_on). Readiness filters asready:true/ready:false— the board spelling of the kernel readiness lens: every dependency terminal (done/cancelled), with unknown dep ids counting as open. The active expression round-trips through the URL hash (#filter=<expr>), so reload/share/copy restores it; clearing restores the full board, and the per-column counts plus theN of Mcounter reflect the filtered set. Nox-viewsin the tracker → no chips, everything else unchanged - blocked cards: cards with open dependencies (deps not
done/cancelled) render dimmed with ablocked by Nbadge in the card head plus the per-dep↳ blocked by <id>lines; in--tuisuch items carry a⌫tag - column controls (client-side; static export and
--servealike): every column heading carries a collapse toggle (–/+,aria-expanded) — a collapsed column keeps only its heading, with the count badge still visible; the filter bar gains ahide done/cancelledtoggle (aria-pressed) that hides both terminal columns, and areset layoutbutton that restores the default. The toggle is one layout preference for BOTH layouts: in the grouped-ancestry tree it hides terminal rows the same way — live, without a reload — and a terminal row is only kept when hiding it would orphan a visible row below it (it stays, muted, as context: the tree's own "nothing visible is ever orphaned" rule), so a terminal story/epic still holding open work remains as the path to it; a tree whose composed state has no visible row left renders the same—empty state an empty column uses instead of a blank gap. Counters never move:N of M, the per-column counts and the tree heading stay the lens verdict in both layouts. Column headings are sticky (position: sticky) and stay visible while the board scrolls. The layout persists per browser in localStorage underarggon-board-columns-v1({"collapsed":[...],"terminalHidden":bool,"treeCollapsed":[...]}) — a deliberate choice: it is a view preference, not tracker data, so the git files stay the sole source of truth; missing, corrupt or unavailable storage degrades to the default layout, and the keyboard roving anchor re-seats when its column collapses.treeCollapsedholds the folded ancestry rows (task-tree-rows-open-detail): every container row in the grouped-ancestry tree carries a–/+disclosure (aria-expanded, named<id> subtree) that folds and extends its subtree, keyboard-operable as an ordinary button, andreset layoutrestores the default expanded tree with the rest of the layout - derived displayed status (computed server-side at render; static export and
--servealike, task-board-no-done-over-open-work): stored statuses are the closure record, but a container (initiative/epic/story) whose subtree still holds open work (todo/in_progress/blocked, any depth) never DISPLAYSdoneon the board — its grouped-ancestry row and its column card render theopenchip (the tracker's antonym of terminal) plus the existingN opendisclosure, and the accessible name carries the displayed state.cancelledis deliberately not masked (an abandoned scope reads abandoned, not finished), and leaves or containers with a fully terminal subtree render exactly as before. One server-side walk over the complete item set feeds both layouts, so a row and its card can never disagree; thehide done/cancelledtoggle and the lens read the displayed state, so a masked container is never hidden by them (in the columns the done column stays and only its genuinely terminal cards hide). The item files are untouched — the drawer keeps the stored status and explains the effective one (reads open · N open below). TUI and the OpenCode panel are out of scope (recorded on the item) - theme and density (client-side; static export and
--servealike, task-board-theme-density): the filter bar carries atheme: autobutton that cycles auto → light → dark and a compact-density toggle (aria-pressed).auto(the default) followsprefers-color-scheme— live, while the page stays open; a manual choice persists per browser in localStorage underarggon-board-theme-v1({"theme":"auto"|"light"|"dark"}) andarggon-board-density-v1({"density":"comfortable"|"compact"}), the same view-preference-not-tracker-data choice as the column layout, degrading to the defaults on missing/corrupt/unavailable storage. A boot script in<head>resolves the persisted (or OS) preference intodata-theme/data-densityon<html>before first paint, so there is no light flash for a dark user, andcolor-schemeon the root always matches the painted surface so native controls follow. Every color is a CSS custom property: the dark block swaps values only (same geometry), and the palette holds the same contrast bars as light — text ≥ 4.5:1, interactive-control boundaries ≥ 3:1 — asserted against the exact hex values incli/src/board.test.tsand scanned by axe in both themes on the@smokelane. Compact shrinks card padding, gaps and the card text (13px → 12px, never smaller) and leaves the grid columns untouched, so cards stay readable and the columns never re-wrap --tui: interactive terminal kanban over the same kernel read path (reads everywhere; the only writes are thec/mactions below, through the kernel update path) — five v0 status columns, dependency-light (raw ANSI escapes, no TUI framework, zero new dependencies). Re-reads the tree after every keypress, so it always shows the current tree, and watches the tracker dir (recursivefs.watch, debounced like--serve's reload) so an idle board repaints in place while other agents/sessions write — selection and filter survive, the footer shows the freshness stamp of the rendered data (updated HH:MM:SS), andrforces a re-read.scycles the card order per column (most-recently-modifiedupdatedby default, ADR 0009prioritytier, or thenextrank: ready first, priority tier, downstream weight, recency then id on ties;idkeeps lexicographic) andltoggles a ready-only lens (claimable unclaimed todo with terminal dependencies — the kernelnext --readyrule), with the active sort/lens named in the header and each card surfacing its priority, assignee and dependency-blocked (⌫) marker./filters through the kernel filter language — all documented predicates (status:,type:,label:,assignee:,priority:,parent:,depends-on:,blocked-by:,ancestor:; quoting,!negation, enum errors and lazily memoized@meresolution shared witharggon list --filter, task-tui-filter-language) — plus free-text tokens on id/title ANDed with the predicates (the web board lens' extension); an expression that fails to parse is refused inline at the prompt with the kernel's error and the previous lens stays active (nothing crashes, nothing silently matches nothing).vcycles the tracker'sx-viewssaved views: the active view ANDs with the manual filter (likelist --view+--filter), its name + expression show in the header, an invalid view is refused with its error, andescclears the filter and the view. The footer carries the active lens withK/M matchtotals, and columns the lens empties keep their(0)header count plus a dimmed(empty)mark.cclaims the selected item (assignee prompt prefilled with the resolved login) andmmoves it through a menu of kernel-legal transitions —blockedasks for a reason, a claim asks for the assignee, and every move ends in ay/Nconfirmation naming the exact change. Both run the SAME kernel write path asarggon updateand the served board (runUpdate): force and claim steals are structurally impossible (no key or prompt can carry them; the kernel refuses a reassignment anyway), a kernel refusal surfaces as sanitized footer text with the board intact, and — like the served board's drag-and-drop — a TUI write is a working-tree change, not a tracker commit. Reopeningdone/cancelledtotodostays available throughmbehind the same confirmation (the interactive equivalent of the CLI's reopen gate). Vim motions ride the arrows everywhere (h/lcolumns,j/kcards,g/Gfirst/last — the ready-only lens moved toL),?opens a help overlay listing every key grouped (navigation, filter, view, quit;esccloses), and--no-color(or theNO_COLORenv) renders a plain frame with the selection on the>marker. A watcher that cannot open or fails later degrades transparently to per-keypress reads (never crashes, never exits);enteropens a read-only detail pane (item body, acceptance rows, dependencies) without leaving the board. Requires an interactive terminal (piped stdout fails withBOARD_FAILED); not combinable with--json(it is a view, not a data format) or--serve. Keybindings:- summary header (client-side render, static export and
--servealike, task-board-progress-header): the header carries a compact progress panel that makes the board the standing PM view — per-epic completion (epic-id done/totalover the epic's task/bug leaves), the WIP count (claimable items inin_progress), the priority mix over open claimable items (p0–p3by exact token; unset or invalid tokens land innone) and the blocked count with each blocked leaf'sid: reason. Everything computes at render from the kernel report aggregation (arggon report's ownaggregateReport— the same numbers the report and its standup markdown print, never a copied rule) over the already-loaded items, so the payload stays bounded: one row per epic, one entry per blocked leaf, five priority buckets — no per-item payload. The panel rolls up the whole tracker at generation time; the filter lens narrows the cards, not the summary --group-by storygroup heads carry a completion fraction (⚑ story-id done/total) — the report'sdone + cancelledrule over every card with that parent across all columns (task-board-progress-header); milestone grouping renders heads exactly as before--group-by tree: the board opens in the grouped ancestry layout (task-web-board-hierarchy-view): every item nested under its recordedparentchain — initiative → epic → story → leaf — one row per level carrying its own type badge, id and status chip (plus the leaf's priority, assignee and open-deps badge), the web-board reading of the OpenCode panel's tree (boardTreeLines). The recordedparentedges are the only hierarchy source. Every board carries both layouts: the filterbar'sgrouped ancestrytoggle switches between them in the static export and--servealike (no server needed), and the choice round-trips through the URL hash (#view=tree, written only when it differs from the generated default), so a copied link or a reload restores the layout. Orphans and partial chains render honestly: a parentless item is a top-level row, an item whose recorded parent is not in the snapshot is a top-level row with a muted↳ recorded parent <id> (missing)marker (and amissing-parentclass), a parent cycle renders each item exactly once, and the tree carries no anchors at all — a dangling edge can never become a dead link. The lens applies inside the tree: a match stays with its ancestor path (marked as muted context, so a filtered leaf is never orphaned) while non-matching subtrees hide whole, and theN of Mcounter plus the tree heading keep counting matches, never context rows. The filterbar'shide done/cancelledlayout preference applies to this layout too (the column-controls bullet has the contract): terminal rows hide live, and the ones kept only because a visible row hangs under them stay muted as context. Rows are interactive where the drawer exists (task-tree-rows-open-detail): with--serveor a static--detailsexport a click, Enter or Space on a row opens the same read-only detail drawer the columns open, for that row's item (Esc/close returns focus to the row); the plain static export keeps its rows inert — no focus, no click target, no fake affordance — while the fold control still works. Keyboard reach mirrors the cards (roving tabindex: one row is the tab stop,↑/↓walk the visible rows in reading order,Home/Endjump to its ends, and the disclosure is a normal button in the row's tab order). Every container row also carries the–/+compact/extend disclosure: folding hides its subtree, extending restores it, and the fold is remembered per browser in the same layout preference (treeCollapsed;reset layoutrestores the default expanded tree). Folding composes with the other tree mechanics instead of bypassing them: it is a display rule below the terminal verdict (a folded row the terminal rule keeps stays on screen, so the control cannot vanish with its subtree), counters never move, and a lens match hidden inside a folded branch stays discoverable — the row and the disclosure's accessible name carryN matches inside(the recorded decision: mark the folded branch, never silently auto-expand it), so the lens answer survives the fold and expanding reveals it. Moves, drag-and-drop and the live PR overlay stay in the status-columns view, one toggle away (rows are not draggable). Payload: the tree markup costs ~565 bytes per item and grows linearly (measured here 2026-10-14 on the 662-item tracker: 365 KiB, ~32% of the 1.1 MiB page; the interactive rows of a--detailsexport add the row's role/label/tabindex attributes — 484 KiB, ~749 bytes per item);milestone/storykeep grouping cards within each column, unchanged.--tuistill refuses--group-byas a whole (the TUI groups by status column only)--details(static export opt-in, task-board-static-details): embeds the detail drawer's payload for every rendered item at generation time, so a sharedboard.htmlkeeps its drawer with no server and no runtime dependencies — it works straight fromfile://. The embedded payload is the same bounded read the serve drawer fetches (GET /api/item): prose clipped at 8 KiB and each of the last 3 comments at 4 KiB of UTF-8 (prose_truncated/truncated/acceptance_truncatedflag each cut). Acceptance rows are parsed from the item's canonical body — the whole body, comment sections included — through the kernel's one row parser, the same input the done gate reads, so the drawer can never report "nothing unchecked" while--status doneis refused;acceptance_completecarries that gate's verdict verbatim and the list is capped at 64 rows (bug-three-acceptance-parsers-diverging), plus dependency states and the PR match when--githubis on. Measured on this repo's tracker (368 items, 2026-09-30): the page grows from ~375 KiB to ~2.2 MiB — the embedded snapshot is ~1.7 MiB, ~4.9 KiB per item on average (the per-item caps bound the growth; trackers with heavier bodies scale linearly in items, never in body size). Without the flag the export stays drawer-free and otherwise byte-identical (no drawer markup, no embedded payload, no extra script). Not combinable with--serve(the served drawer always fetches) or--tui--serve: serves the board locally, bound to 127.0.0.1 only, and reloads the page whenever any file under the tracker root (ArggonManager/) changes; drag-and-drop posts to the update endpoint, which runs the same kernel update rules as the CLI. The mutating route additionally validates the request origin (task-board-serve-hardening):Hostmust be the served loopback host and port (127.0.0.1:<port>/localhost:<port>/[::1]:<port>), aSec-Fetch-Site: cross-siteheader is refused, and anOriginheader — when the client sends one — must parse to the served loopback origin; anything else answers403before the kernel write path runs, so a stray web page or a rebound hosts-file name cannot knock a move through the port (non-browser clients like curl are held to theHostcheck only and keep working). Moves that need a value collect it in an in-page dialog (task-board-move-dialogs, rendered on the static export too): claiming intoin_progressasks for the assignee, moving toblockedasks for the reason — inline validation on empty input, Esc/cancel/backdrop aborts the move, focus trapped and restored. A successful move offers an explicitUndobutton in the toast whenever the reverse transition is legal under the same drop rules (never force, never steal — undo re-enters the same move flow, dialogs included); a failed move reverts the card as before. A minimal SVG favicon is served at/favicon.icoand linked from the served page (the static export has neither), so the browser console stays clean.--openlaunches the default browser after listen — best-effort: a missing opener is reported (human output) or recorded as additiveopen: truein the--jsonenvelope, and the server keeps serving either way. Each live reload preserves the view state around the page swap (serve-only, task-board-live-reload-state): scroll position, the filter expression and an open detail drawer survive and are restored after the reload (state travels insessionStorage; a drawer whose item was deleted stays closed). A connection pill in the top-right corner (#board-conn,role=status) reports the SSE stream asconnecting …,live, orreconnecting — board may be staleso a silently dead stream is visible.--serveis combinable with--json(the envelope is emitted once, then the server keeps running); seeArggonManager/docs/json-output.mdfor the payload fields. The served board is also the review surface (serve-only, not in the static export or--tui): cards with abranchcarry a live PR strip — state (draft/open/merged/closed) + checks summary + adifflink to the PR's files view — polled from the sharedgh pr listread path on a fixed interval (60s); a changed snapshot pushes an SSE reload. gh missing or unauthenticated, or a failed poll, degrades cleanly: cards fall back to the neutral○ no PRbadge and the server keeps serving with the last good snapshot- build stamp (static export and
--servealike, task-board-build-stamp): every board names the build it was rendered from — thearggon --versionstring (live git sha/branch, else the bakeddist/build-info.jsonfromcli/write-build-info.mjs), never hand-maintained — in the header meta next to the generation timestamp, in the page footer, and (with--serveor a--detailsexport) inside the detail drawer panel, all readable without devtools, so a stale deployment is diagnosable from the drawer itself. The static export stamps at export time; serve mode stamps once at server start and every reload keeps it - friction indicator (static export and
--servealike, task-board-footer-links-the-friction-report): the page footer carries a friction badge next to the build stamp (friction Nwith the redacted snapshot's record count) that opens the redacted friction report without devtools — the same deduped textarggon friction --reportprints (renderFrictionReport), baked at export time for the static export and captured once at server start for serve mode, never a live per-request log read. Narratives are untrusted data rendered as plain escaped text; only the redacted report surface is embedded, never the raw jsonl and never a raw log path. Absent/empty (no log on the host) renders no indicator and the export stays byte-identical. A served board shows its own host's snapshot - item detail drawer: click or Enter a card — or, in the grouped-ancestry layout, click or press Enter/Space on a tree row — to open a read-only drawer — serve mode fetches it from
GET /api/item?id=<id>, the static--detailsexport renders the bounded payload embedded at generation time (same caps, offline) — title, type/priority/status, body, acceptance checklist rows (read-only), labels, dependencies with their kernel statuses (open vs terminal), the parent as a link that opens the parent's detail plus a children section with one link per child (id + status; parentless/childless items say so instead of linking anywhere), branch/worktree_path, the PR badge/link from the live overlay, and the item path. The read goes through the kernel boundedshowpath (prose + the last 3 comments) and the payload is capped (8 KiB of prose, 4 KiB per comment of UTF-8); a truncated body says so and points at the item file. Esc (or the close button/backdrop) closes it and returns focus to the card that opened it, and a live reload that removes the item closes it too. The plain static export stays lean and byte-identical (no drawer markup, no endpoint, no extra script) — item bodies reach an offlineboard.htmlonly through the explicit--detailsopt-in above; the payload trade-off and the route contract are documented inArggonManager/docs/json-output.md. Rendering is text-only (textContent/DOM text nodes), so hostile markdown lines in a body stay literal text. No tracker writes - keyboard and touch (serve-only; the static
--detailsexport ships the same surface offline): the board is navigable without a mouse — Tab reaches the cards once (roving tabindex), then↑/↓move within a column,←/→cross to the nearest non-empty column,Home/Endjump to the column's first/last card, Enter (or click) opens the detail drawer, andm— or the per-cardmovebutton — opens a card action menu listing only the legal status transitions for that card. The grouped-ancestry layout uses the same pattern on its rows (task-tree-rows-open-detail): one row is the tab stop,↑/↓walk the visible rows in reading order — skipping lens-hidden, terminal-hidden and folded-away rows —Home/Endjump to its ends, Enter/Space opens that row's drawer, and each container's–/+disclosure is a normal button in the row's tab order. The menu runs the exact same drop-rule parity path as drag-and-drop (one embeddedevaluateDrop, including the claim--assigneeand--blocked-reasonvalue dialogs, with no force), and every dialog (menu, value prompt, drawer) traps focus and restores it on close; the toast is a live region, columns are named landmarks, and every card carries an accessible name that follows its moves. Touch screens get the same moves through themovebutton (tap): HTML5 drag-and-drop does not fire on touch devices, so dragging deliberately has no touch equivalent — the menu is the fallback. The plain static export renders the same labels (columns and cards are announced to assistive tech) but carries no interactive surface;--detailsopts it in to the focusable cards, the roving tabindex, the move menu and the drawer (moves refuse offline with the static-snapshot toast, exactly like a drag on the plain export)
| Key | Board |
|---|---|
← / → · h / l |
move the selected column (v0 status order) |
↑ / ↓ · j / k |
move the selected card within the column (long columns scroll; footer shows row N/M) |
PgUp / PgDn |
move the selection one body page up/down, window included |
Home / End · g / G |
jump to the first / last card of the selected column |
/ |
open the filter prompt (free text + kernel predicates); enter applies — an invalid expression is refused inline — esc cancels |
esc |
clear the active filter and the saved view |
enter |
open the read-only detail pane for the selected item (an empty column shows (no item selected)) |
r |
force a refresh (re-read the tree and repaint now) |
s |
cycle the card order: updated → priority (ADR 0009 tier) → next (ready first, tier, downstream weight, recency then id) → id |
? |
open the help overlay (every key, grouped); esc closes it |
L |
toggle the ready-only lens (pullable work only; blocked/claimed/non-todo cards hidden) |
v |
cycle the saved views (x-views in the tracker .convention.yml): on → next → … → off; the view ANDs with the filter; an invalid view is refused with its error |
c |
claim the selected item (assignee prompt, prefilled); claimable types only — a claimed item refuses (never steals) |
m |
move the selected item: menu of kernel-legal targets → reason/assignee prompt if needed → y/N confirm; writes go through runUpdate |
q / Ctrl-C |
quit, restoring the screen (Ctrl-C quits even mid-action-prompt) |
| Key | Detail pane (enter on a card) |
|---|---|
↑ / ↓ |
scroll the content one line |
PgUp / PgDn |
scroll one page |
Home / End |
jump to the top / bottom of the content |
p |
open the parent's detail (reports in-pane when there is none) |
1–9 · c |
open a child's detail — the number opens that child, c the first (reports in-pane when there is none) |
esc / enter |
back to the board — selection, filter and scroll window untouched |
q / Ctrl-C |
quit, restoring the screen |
The detail pane reads one item without leaving the board: title, type, status, priority, assignee, labels, milestone, parent, branch, worktree path, file path, dependencies (each with its status; open deps carry the ⌫ marker), the children section (each child with its number, id, status and title, in id order — children: (none) when there are none), the acceptance checkbox rows with their count (the same task-list rule the container cascade checks) and the full markdown body (prose + comment history). From an open pane p jumps to the parent's detail and 1–9 / c open a child's detail (the number opens that child, c the first) — all resolved through the kernel's recorded parent edges in the same snapshot the board renders, so the pane can never disagree with the board about the tree; a jump with no target (no parent / no such child) reports that in the pane footer instead of misfiring. The pane is read-only — it never writes the tracker — and every repo-controlled value and body line goes through the same human-text escaping as the cards. Bodies wrap to the terminal width: values are wrapped, never clipped, and long bodies are capped at 400 source lines / 1000 rendered lines (each cap is named by a marker line when it truncates). Below 40 columns the pane falls back to a stacked layout (one field per line, values wrapped) because packed field lines wrap mid-pair in a narrow terminal.
The static export is a snapshot: re-run after tree changes to refresh (or use --serve). The generated file is a build artifact — safe to gitignore; deleting it loses nothing.
arggon report aggregates leaf (task/bug) statuses per story, grouped by epic, for display only (never writes). Every story gets a row — leafless stories show zeros with (empty — no leaves), storiless epics (empty — no stories) — and cancelled has its own explicit column. Flags: --format markdown (standup summary: per-epic progress with (done + cancelled)/total plus a Blocked section with reasons) and --json (envelope { groups } mirroring the table rows exactly, failures REPORT_FAILED).
arggon report --trend mines git history (story-report-trend): every status transition is a frontmatter diff in some commit under the tracker root (ArggonManager/), so one git log -p pass yields time-series analytics with zero extra state. Pure read — git log only, never writes. Output: weekly completions bucketed by the ISO week of each item's first terminal transition (done/cancelled; every type counts, so story-driven projects get non-empty trends) and average cycle time per leaf type — task/bug/story, containers excluded (first in_progress claim → terminal, in days, 1 decimal). Items still open count as not-completed. --since <YYYY-MM-DD> (UTC) limits the considered window: transitions before it are ignored, so items completed before the window drop out entirely. With --trend, the trend block is appended to the human table/markdown output and an additive trend payload is added to the --json envelope ({ weeks: [{ week, completions }], cycleTime: [{ type, avgDays, count }] }); failures (e.g. non-git tree) use TREND_FAILED. Default output without --trend is unchanged.
Closing work is easy on containers too: when an update reaches a terminal state and an ancestor's entire subtree is terminal, the ancestor auto-completes as done (cascade up to the initiative; opt out with --no-cascade). Cascades reaching epic level or above print a visible warning in human output — administrative closures (adoption/migration tasks) should pass --no-cascade so their containers stay open.
Reconciles the tracker with the repo's open GitHub PRs: --check (default, CI-safe) reports matched/unmatched items and exits non-zero when sync is pending; --write fills only empty branch fields — never overwrites a set branch, never guesses an ambiguous match, never touches status. Flags: --repo <owner/repo>, --json. Failures: SYNC_FAILED.
The --json envelope additively reports verdicts — a report-only review-verdict classification (approved / changes-requested / none) per item reconciled with an open PR, read from the item body's verdict comments (convention: ArggonManager/docs/engineering.md §Review bar → Review verdicts). It never affects matching or the exit code.
Validates and scaffolds feature specs (ArggonManager/docs/specs/spec-<slug>-NNN.md) and implementation plans (docs/plans/plan-<slug>-NNN.md) so agents can trust and check them. spec validate is a pure read: it checks frontmatter (spec_id/plan_id kebab-case, title, status (proposed | implemented | superseded), created as YYYY-MM-DD), required sections (Purpose or a non-empty intro, a Synopsis/Design/Model-of-data equivalent, Acceptance criteria — lenient about naming, including the Spanish sections of the existing specs, strict about acceptance), plans pointing at an existing spec file, and spec_id uniqueness across docs/specs/. Exits non-zero on errors. Corpus mode also warns (DOC_NUMBER_COLLISION, exit unaffected) when two documents in one directory share a number under different slugs — 0020-alpha.md + 0020-beta.md — which spec_id/plan_id uniqueness cannot see, across docs/adr/, docs/explorations/, docs/specs/ and docs/plans/.
arggon spec validate # check ArggonManager/docs/specs/ + ArggonManager/docs/plans/ (pure read, CI-safe)
arggon spec validate --file ArggonManager/docs/specs/spec-deps-022.md # single file, also outside the specs|plans dirs
arggon spec validate --json # v1 envelope { errors, warnings }; failures SPEC_FAILED
arggon spec new my-feature # scaffold ArggonManager/docs/specs/spec-my-feature-NNN.md
arggon spec new my-feature --title "My feature" --plan # also scaffold ArggonManager/docs/plans/plan-my-feature-NNN.md
arggon spec new my-feature --json # v1 envelope { files: string[] }
arggon spec import openspec ./openspec # migrate an OpenSpec corpus (zero-loss)
arggon spec import openspec ./openspec --dry-run # inventory only, writes nothingspec new numbers globally (max existing NNN across ArggonManager/docs/specs + ArggonManager/docs/plans, plus 1) and never overwrites an existing file. Templates live in templates/spec.md / templates/plan.md ({{SLUG}}, {{NNN}}, {{ID}}, {{TITLE}}, {{DATE}} placeholders; an embedded copy in the CLI is the fallback). See the pipeline spec: ArggonManager/docs/specs/spec-spec-pipeline-002.md.
spec analyze [--spec <path>] is a report-only quality pass over the specs (default: every ArggonManager/docs/specs/*.md): a checklist-driven ambiguity scan (vague quantifiers like "fast"/"several", TODO/TBD markers, no error path, missing or untestable acceptance criteria) plus a spec ↔ tasks/plans consistency check (specs marked implemented that no item or plan cites; plans whose spec: points at a missing file; duplicate-doc-number — two documents in docs/adr/, docs/explorations/, docs/specs/ or docs/plans/ sharing one number under different slugs, the whole-directory property spec_id/plan_id uniqueness is blind to, reported with every colliding file named). The decision-pipeline check (findings.decisions, corpus mode only) flags the explore → ADR → spec/plan loop leaking: DECISION-PENDING-EXPLORATION (an exploration whose Decision section records no decision after 7 days — no ADR link and no No ADR required — <reason> marker), STALE-PROPOSED-ADR (an ADR stuck in Proposed after 14 days), and SPEC-STATUS-DRIFT (a proposed spec whose linked plan is already implemented). Findings never fail the run — exit 0 with findings; only an unreadable file exits 1 with SPEC_FAILED. Run it before implementation starts to catch what structural validation cannot. See ArggonManager/docs/specs/spec-spec-analyze-004.md and ArggonManager/docs/specs/spec-analyze-decision-gaps-013.md.
Baselines (wave gate). spec analyze --save-baseline <file> writes the findings snapshot to <file> — deterministic, committable JSON (schemaVersion, conventionVersion, count, findings sorted by file/kind/line/severity/message; no timestamps), so a re-run over unchanged specs is byte-identical and committed baselines diff cleanly. spec analyze --baseline <file> compares the current run against a snapshot and reports only NEW and resolved findings (plus unchanged/total counts). A finding matches only when all of file/kind/line/severity/message are equal. Exit policy: >=1 NEW finding exits 1 (the regression gate), zero new exits 0; --no-fail-on-new makes the run report-only. --baseline and --save-baseline are mutually exclusive in one run. With --json a failing gate still emits a success envelope (ok: true) carrying the additive baseline payload — the exit code carries the gate. The recommended multi-wave refactor flow (ArggonStores-am style):
arggon spec analyze --save-baseline ArggonManager/docs/specs/.analyze-baseline.json # once at wave 0, commit the file
arggon spec analyze --baseline ArggonManager/docs/specs/.analyze-baseline.json # per PR/wave: non-zero = regressionspec import openspec <path> [--dry-run] mechanically migrates an OpenSpec corpus (<path>/specs/<capability>/spec.md) into Arggon spec docs: for each capability (sorted) it scaffolds ArggonManager/docs/specs/spec-<capability>-NNN.md with consecutive global numbering. Mapping (proven in production): OpenSpec ## Purpose -> Purpose; ## Requirements (### Requirement: / #### Scenario: Given/When/Then) -> Acceptance criteria verbatim, plus a ### Verification checklist per requirement and an italic provenance line. Zero-loss: the re-assembled mapped content must equal the source body after normalization - on mismatch the run fails loudly with a per-file diff. All-or-nothing per run: map + assert every file first, then write; any failure (parse, zero-loss, collision, non-kebab-case capability) writes nothing. Never overwrites. The format parsing lives behind a CorpusAdapter extension point (cli/src/spec-import.ts) so other corpus formats plug in. See ArggonManager/docs/specs/spec-spec-import-openspec-005.md.
spec audit [--json] is a report-only duplication detector over every pair of docs/specs/*.md: word-shingle Jaccard similarity (k=3) over normalized text (frontmatter and code fences stripped, lowercased, whitespace collapsed) plus the verbatim intersection of each spec's ### Requirement: / #### Scenario: titles. Pairs are classified with evidence (file pair, similarity, shared titles, one-line note) as DUPLICATE (similarity >= --duplicate-threshold, default 0.85), MERGE (>= --merge-threshold, default 0.45, or >= --min-shared-titles shared titles (default 2) with similarity >= --shared-title-floor (default 0.15) — the "diverging duplicate" shape: same requirements, rewritten prose), or KEEP-SEPARATE (similarity >= --report-floor (default 0.15) or at least one shared title — evidence in hand, a human decides); pairs below the report floor are only counted. Findings never fail the run; only structural failures (missing/empty ArggonManager/docs/specs, invalid thresholds) exit 1 with SPEC_FAILED. Provenance: it productizes the ad-hoc ArggonStores-am migration audit (9,453 pairwise comparisons -> 1 diverging duplicate + 6 consolidations among ~130 healthy specs). See ArggonManager/docs/specs/spec-spec-audit-006.md.
arggon spec audit # classified duplication report over ArggonManager/docs/specs/*.md (read-only)
arggon spec audit --json # v1 envelope { specs, pairs, thresholds, findings, counts }
arggon spec audit --duplicate-threshold 0.9 --merge-threshold 0.5 # tune for your corpusScaffolds an exploration record — the spike note that precedes a stack ADR — at ArggonManager/docs/explorations/exploration-<slug>-NNN.md with sections Candidates, Criteria, Findings (dated source links), Recommendation, and a Decision ADR placeholder. The research itself (comparing candidates, collecting dated sources) is the caller's job; the command records it. NNN is the max existing number across ArggonManager/docs/explorations plus 1 (zero-padded to 3), and the command never overwrites an existing file.
arggon stack explore "vector database" # ArggonManager/docs/explorations/exploration-vector-database-001.md
arggon stack explore "vector database" --title "Vector DB options"
arggon stack explore "cache layer" --json # v1 envelope { command: "explore", files: string[] }Template: templates/exploration.md ({{SLUG}}, {{NNN}}, {{ID}}, {{TITLE}}, {{DATE}} placeholders; an embedded copy in the CLI is the fallback). Fails outside a tracker tree with an actionable error; failures exit non-zero (EXPLORE_FAILED under --json).
Technology playbooks with version-freshness tracking (story-tech-playbooks): one playbook per tech at ArggonManager/docs/playbooks/<tech>.md pinning the chosen version and the current best practices, generated after a documented exploration (arggon stack explore → ADR → playbook new). The version/best-practices research is the caller's job at creation time; the CLI records it and tracks freshness — a stale playbook files a re-research task, like the container cascade drives completion.
arggon playbook new postgres --version 16.3 # ArggonManager/docs/playbooks/postgres.md (status: current, researched: today)
arggon playbook new vector-db # version defaults to "unpinned"
arggon playbook status # table: tech, version, researched, age-days, current/STALE
arggon playbook status --max-age-days 30 # flag wins over x-playbooks.max-age-days and the 90-day default
arggon playbook status --file-task story-x # one re-research task per stale playbook (idempotent)
arggon playbook refresh postgres --version 16.4 # after re-research: version + researched: today + status: currentplaybook new <tech>: kebab-case tech slug; refuses to overwrite an existing playbook (one per tech — re-research instead). Frontmatter:playbook_id,version(--versionorunpinned),researched(today),status: current. Sections Setup / Conventions / Testing / Security / Upgrade policy, each a<!-- fill me -->stub reminding the caller to research current best practices with dated sourcesplaybook status: stale whentoday − researched > max-age-days(default 90; override viaArggonManager/.convention.ymlx-playbooks.max-age-days:, the namespaced extension parsed likex-views;--max-age-dayswins over both). Unknown/missing research dates count as staleplaybook status --file-task <story-id>: creates onetodotask per stale playbook via the same kernel ascreate— idtask-re-research-<tech>, titleRe-research <tech> playbook (v<version>, N days old), body linking the playbook path; skips ids that already exist; errors when the story does not existplaybook refresh <tech> --version <v>: frontmatter-only write (body byte-identical) — setsversion,researched: today,status: current--json: v1 envelopecommand: "playbook"—new/refreshreturn{ files }/{ path, version, researched };statusreturns{ playbooks: [{ id, version, researched, ageDays, stale, path }], staleCount, maxAgeDays, created, skipped }. Failures exit non-zero (PLAYBOOK_FAILEDunder--json;EXPLORE_FAILEDforstack explore)
Priority (convention v4). Items of every type carry an optional judgment priority — p0 (drop everything) | p1 | p2 | p3. Set it at creation with arggon create <type> <title> --priority p1, re-rank with arggon update <id> --priority p2 (clear: --priority ""), and find unprioritized work with arggon list --filter "status:todo priority:none". Legacy pN labels migrate into the field with arggon priority migrate (highest label wins, idempotent, never auto-commits — review and land one commit). arggon next ranks the ready pool priority-first (then downstream weight) and says so in its reason.
One-shot migration of an existing GitHub issue backlog into the tracker root (ArggonManager/) (the ArggonManager/docs/agents.md §0 promise). Reads issues via gh issue list --state all --limit 200 --json number,title,state,body,labels and writes one task per issue through the same kernel as create/update. Idempotent: target ids are task-issue-<number>, so a re-run imports nothing (created: 0, everything skipped).
- status mapping: open →
todo, closed →done(closed items are createdtodoand closed through the legal kernel path in the same run; the container auto-completion cascade may fire) - title
issue #N: <issue title>; body: the original issue body plus a> imported from issue #Nprovenance line - issue linking: the GitHub issue number is recorded in the item's additive
issuefrontmatter field (contract-visible asWorkItem.issue);arggon start <id> --open-prthen appendsCloses #Nto the PR body, closing the issue when the PR merges. Items without it are unaffected - hand-built items can link issues too (no import needed):
arggon create --issue <n>records the number at creation, andarggon update <id> --issue <n>sets it later (--issue 0clears it; positive integers only). Same field, sameCloses #Nbehavior, same round-trip eligibility as imported items - promotion path (task-promote-task-to-story): the import flattens every idea as a leaf under one container story — when an imported idea turns out to be a FEATURE, promote it in place with
arggon update <task-id> --type story: the file moves to the story layout under the same epic, the id renamestask-issue-N→story-issue-N(depends_onreferences follow), and theissuelink, labels, and body ride along untouched. Refusals (bugs, already-story items, demotion, missing epic) move nothing — see theupdatesection above - issue round-trip (opt-in): with
x-github.issue-roundtrip: trueinArggonManager/.convention.yml(default OFF), flipping an item that carriesissue: Ntodonecloses the linked GitHub issue viagh issue close N --repo <detected> --comment(annotated with the resolving item id) — so dual-tracker teams don't drift. Best effort: gh absent/unauthenticated, a non-GitHub origin, or a failed close never block the done flip; the skip is reported in the--jsonpayload (issueRoundtrip.skipped) and as a stderr warning. Flips through bots and MCP (arggon_update) are covered too, since the round-trip lives in the update kernel, not in a CLI flag - labels: issue labels slugified to kebab-case and deduped; invalid ones are skipped silently but counted in the report; type mapping — the
buglabel imports as a bug (everything else as a task), customizable viax-import.label-typesinArggonManager/.convention.yml(see ArggonManager/docs/convention.md) - target story:
story-imported-issues, created under the first epic when missing (actionable error when the tree has no epic); override with--parent <story-id> - tracker hygiene: a successful run auto-commits everything it wrote (the story and every created/updated item) as ONE
chore(tasks): imported N issuescommit;--no-commitkeeps the tracker dirty,x-tracker.auto-commit: falseopts out tree-wide; a mid-run failure keeps the historic no-commit behavior (partial runs leave the tracker dirty and reportIMPORT_FAILED)
arggon import-issues # gh resolves the repo from cwd
arggon import-issues --repo owner/name # explicit repository
arggon import-issues --dry-run # print the would-create / would-skip plan, write nothing
arggon import-issues --parent story-backlog
arggon import-issues --json # v1 envelope: { dryRun, story, entries, created, skipped, labels, commit? }Failures exit non-zero (IMPORT_FAILED under --json): gh missing/unauthenticated (gh auth login), missing a tracker, a tree without an epic for the default story, or a malformed --repo / unresolvable --parent.
Appends a timestamped, author-attributed comment section to an item's body — the handoff channel for agent context ("why blocked", "what the next agent should know"). Comments are history, not status changes: the write is body-only (frontmatter is re-serialized unchanged, updated is NOT bumped), it works on any item in any status including done/cancelled (this is NOT a reopen), and agents may comment on closed items. Section format:
### 2026-09-11 @<author>
<text>arggon comment story-login "Blocked on OAuth credentials; next agent: ping #ops"
arggon comment task-rate-limit "why blocked:
- waiting on repro from QA" --author octocat
arggon comment story-login "handoff note" --json
# text from stdin — no shell quoting to get wrong (backticks, $ and quotes survive verbatim):
arggon comment task-rate-limit --file - <<'EOF'
rerun `npm test` — the failure was $?, not the fixture
EOF<text>: comment text; multiline supported (each line lands under the heading; a blank line separates the section from the body). Multiple comments append in order--file <path>: read the comment text from a file instead;--file -reads stdin. UTF-8, verbatim — the text bypasses the shell entirely, so backticks, double quotes and$land in the body unmangled (pass either the text or--file, not both; an empty file/stdin fails like empty text)--author <login>: attribution (rendered as@<author>); defaults to@meresolution —GITHUB_USER, thenGITHUB_ACTOR, thengh api user -q .login. Unresolvable author fails with an actionable error- tracker hygiene: the commented item file is auto-committed (
chore(tasks): commented <id>;--no-commitkeeps it dirty,x-tracker.auto-commit: falseopts out tree-wide, non-git trees skip silently) --json: one compact JSON object on stdout (envelope v1:ok,schemaVersion: 1,conventionVersion,command: "comment",id,path,comment: { author, date, lines }+ additivecommit); failures emitok: falsewithcode: "COMMENT_FAILED"(unknown id, empty text, unresolvable author, text and--filetogether, unreadable--file)
arggon validate keeps passing on a commented tree — comment sections are plain freeform Markdown in the body. --file is CLI-only ergonomics; the MCP arggon_comment tool takes the comment text directly as its text argument (no file/stdin indirection).
Appends a structured, bounded session-end handoff section to an item's body — comment with structure, for the "resume exactly where you left off" moment. Same body-only machinery as comment: frontmatter never touched, works on done/cancelled items, not a reopen. Section format (each field capped at 200 characters — the optional session identifier at 64; longer input truncates with …, so the whole section stays under ~800 characters):
### handoff 2026-09-15 @<author>[ (session: <id>)] — next: <step>
- branch: <branch>
- open questions: <q1; q2>arggon handoff task-rate-limit --next "write the cascade tests"
arggon handoff task-rate-limit --next "fix the lock retry" --open-questions "does the lease survive rebase?" --session "sess_abc123" --json--next <text>(required): the first thing the resuming agent should do--branch <name>: working branch; auto-detected from git when omitted (unknownoutside a git repo)--open-questions <text>: optional; semicolon-separated by convention--session <id>: optional session identifier for provenance (task-handoff-provenance-session-identifier-in-handoff-sections) — rendered in the heading as(session: <id>)so the resuming agent knows which session produced the handoff; explicit flag only (the CLI cannot reliably know the caller's session id — callers pass what they have); omitted cleanly when absent; capped at 64 chars--author <login>: attribution, same@meresolution ascomment- tracker hygiene: auto-commits the item (
chore(tasks): commented <id>;--no-commitandx-tracker.auto-commit: falseopt out, non-git trees skip silently) --json: envelope v1 withcommand: "handoff", payloadid,path,comment: { author, date, lines },handoff: { branch, next, openQuestions?, session? }+ additivecommit; failures emitok: falsewithcode: "COMMENT_FAILED"— reused by design, the handoff kernel IS the comment kernel (same body-append path, same failure modes)
The MCP arggon_handoff tool takes id, next, branch, open_questions, session, author and returns the same envelope.
Prints the agent wiring (install commands, pre-commit hook, CI gate, AGENTS.md snippet) extracted at runtime from ArggonManager/docs/agents.md — the CLI never duplicates the playbook text, so doc and command cannot drift. Flags: --json ({ source, snippets: { install, precommit, ci, agent } }, failures INSTRUCTIONS_FAILED).
Starts a stdio MCP server (JSON-RPC 2.0) exposing sixteen tools — arggon_list, arggon_create, arggon_update, arggon_comment, arggon_handoff, arggon_show, arggon_next, arggon_report, arggon_validate, arggon_priority, arggon_sync, arggon_import_issues, arggon_start, arggon_branch, arggon_cleanup and arggon_friction — with the same rules and JSON envelopes as the CLI. Optional: OpenCode V2 does not need it (the vendored plugin serves the native arggon tools in-process); it remains for other MCP clients, which arggon init wires through .mcp.json. The MCP layer always runs with agent playbook rules: no reopening done/cancelled, no claim stealing. The item-addressed tools (arggon_update/arggon_comment/arggon_handoff/arggon_show) resolve their tree through the item's worktree_path when its claim lives in another worktree, so writes land on the item's branch — a write whose recorded claim tree is gone refuses with the named TRACKER_ROOT_MISMATCH error instead of landing in the server's launch tree (bug-mcp-server-binds-session-cwd-tracker-root). See ArggonManager/docs/agents.md §MCP server.
Fixtures: fixtures/.
The package version in package.json is bumped manually, once per release wave — not per commit or per PR. CHANGELOG.md documents what lands in each wave, so arggon --version (the package version plus the build's git sha/branch when available) plus the changelog answer "which build is this and what's in it?". No semver guarantees are made beyond that.
Ideas on folder layout, frontmatter schema, and CLI UX are especially useful right now. Create a work item in the tracker (arggon create task) describing the idea. Please follow the task convention when proposing sample trees or templates.
MIT — see LICENSE.
Built in the open by Arggon.




