AGENTS.md@bin · git:20261014.ba027c1 · 2026-10-14 · sha256 b98e06b9a63adbb2
AGENTS.md@bin git:20261014.ba027c1A
Immutable. This exact content never changes and is served at /api/v1/blob/b98e06b9a63adbb2.
# bin/
CLI entry points (Python, Bash, and Node) that the dinostack
methodology exposes as PATH-wired commands - the table below is a curated
subset, not a full enumeration (see `ls bin/ds-*` for the live count).
Each binary ships with a
module-manifest docstring (Purpose / Public API / Upstream deps / Downstream
consumers / Failure modes / Performance) that is the authoritative description
of that command. Read the binary itself for full detail; this file is the
module-group map, not a duplicate of those manifests.
Every `ds-*` name below has a permanent `agentic-*` compat symlink (e.g.
`bin/agentic-cost` -> `bin/ds-cost`) - both invoke the same file and behave
identically. `ds-*` is the primary name; the `agentic-*` form is never
sunset (external cron jobs and shell aliases reference it).
## Entry points
| Command | Lang | One-line role |
|---|---|---|
| `ds-base-sync` | Bash | Fast-forward the local base branch to `origin/<base-branch>` after a merge, with a normalized exit-code contract (`synced`/`diverged`/`skipped-dirty`/`usage-error`/`inconclusive`); prints a non-blocking DS-54 hooks-snapshot-staleness advisory note after every invocation. |
| `ds-branch-prune` | Python | Squash-aware local branch prune (DS-153): deletes a local branch only when a four-layer, first-match-wins subsumption predicate (ancestry, squash-patch equivalence, tip-subsumption, content-on-main) proves its tip content is on the resolved base branch; absence of proof is always `SKIP_UNPROVEN`, never a force-delete. Resolves its own base via `bin/_lib.py`'s shared `resolve_base_branch` when `--base` is omitted, and deletes nothing (no ledger entry, exit 0) when that resolution fails. `--dry-run` reports without deleting; every deletion is recorded to an append-only ledger BEFORE the destructive `git branch -D`. Layers L1/L2 structurally cannot prove a squash-merged or rebased branch - those surface as `SKIP_UNPROVEN` and need archive-then-delete, not a longer PR list. The merged-PR window that feeds L2/L3 is fetched by cursor-paginated GraphQL and its completeness is PROVEN rather than assumed - node count must equal the `pullRequests` connection's own `totalCount`, and every other outcome (count mismatch, absent `totalCount`, mid-pagination failure, GraphQL `errors`, a `gh` call or whole-fetch timeout, or exhausting the page bound) prints a NOTE naming the reason and downgrades `pr_state` to `not_checked`, never the affirmative `NONE`. This replaced a `gh pr list --limit 500` ceiling that this repository's merged-PR count had already outgrown, so the truncation NOTE fired on every run; absence of a candidate has never been, and still is not, proof that a branch is unmerged. |
| `ds-calibrate` | Python | Render Skeptic calibration rollups (findings density, meta-Skeptic divergence rate) from `.agentic/events.jsonl`; with `$AGENTIC_DIR` unset, run from a linked worktree, also reads the primary checkout's hook spawn rows (DS-246). |
| `ds-change-delta` | Python | One-shot before/after telemetry delta tool (DS-241): resolves a `--cut` (git SHA, committer date, or ISO date) into a UTC instant, computes two equal-length windows either side of it, and re-derives sessions/spawns/hook-denies/tokens/wall-time (merged across every `--repo`) plus per-repo commits-per-merged-PR from the same live stores `ds-cost`/`ds-evaluate`/`ds-hook-fire-report`/`ds-branch-prune` already read. A 5-case status precedence (`ABSENT`/`NOT_YET_ELAPSED`/`INSUFFICIENT_COVERAGE`/`ZERO_SESSIONS`/`OK`) reports `null` rather than a misleading numeric zero when a store can't support a trustworthy comparison; tokens/wall-time additionally carry a `zero-filled`/`zero-filled-before`/`zero-filled-after` data-quality flag. DS-246 adds `agent_model` (per agent x model runs and dollars, exact-id and family-priced reported separately) and `per_ticket` (Skeptic reviews counted from every Skeptic run with `skeptic_reviews_with_findings` as findings coverage, findings, QA fail-backs, PRs, loops, end-to-end wall, escaped-defect signals), both computed only from `telemetry_v: 2` hook spawn rows and gated by their coverage extent; the per-ticket, PR-attribution and pricing helpers live in `bin/_eval.py`, shared with `ds-eval`. The `eval` block runs `_eval.evaluate(use_gh=False, include_sessions=True)` per window: its window figures, then cost-state dollars per session pooled and per entrypoint (empty sessions excluded and counted; fewer than 10 sessions on a side is PARTIAL), `mix_shifted` (an entrypoint's session share moved more than 10 points), model/version/base-commit confounders, and `prefix_test` at K = 5, 25, 100, pooled and per entrypoint. A failure inside it is an ABSENT `eval` block, never a non-zero exit; it calls neither `gh` nor `ds-feedback`, and costs tens of seconds on a repo with hundreds of sessions; `--no-eval` skips it. The report's session counts are over different populations and each is labelled (`metrics.sessions.basis`, `eval.meta.session_counts`): session-log rows, transcript sessions overlapping the window, sessions assigned by first timestamp, and the non-empty ones among them; `usd_per_reconciled_session` leaves empty sessions out. Read-only: never writes a telemetry file, never mutates git/gh state. |
| `ds-cleanup-worktrees` | Python | Single-repo worktree removal (classify -> lock/dirty -> merge-evidence -> disposition, `bin/tests/worktree_model.py`), PLUS an in-process `--multi-repo`/`--report` surface: discovers a set of repos (explicit `--repo` xN, positional root scan, `--init-config` to scaffold, or a `~/.agentic/cleanup-worktrees.json` fallback), sweeps each with its own independently-resolved base branch, `--max-repos N` to bound discovery cost, and can rank them read-only ("which project is worst") via a cheap 2-git-call FAST tier or a full-cost DEEP tier. DS-196 adds a LENIENT-only `origin_reachable` evidence source (resolves worktree removal, never branch deletion, for a squash-merged/never-PR'd branch that is nonetheless pushed and past a resolved PR state) plus a `--activity-window-hours` (default 3.0) file-activity liveness gate (`SKIP_RECENT_ACTIVITY`); also now invoked automatically, backgrounded, from the session-start prune block in `content/references/worktree-lifecycle.md` - suppressible via `AE_WORKTREE_REAP_DISABLE=1`. DS-245 moves the lock gate ahead of self and makes `--min-age-hours` explicit-supply only; every unattended caller (session-start reap, `/ds-wrap` Step 5, `--archive-unproven`) supplies it. |
| `ds-cost` | Python | Token / wall-time / dollar rollups per agent, session, task, and developer team from `.agentic/events.jsonl` and session logs; hook spawn rows are folded and priced by the shared `bin/_telemetry.py` (DS-246), which also merges the primary checkout's hook spawn rows when run from a linked worktree. The session/task/project/team/operator tables size each column to its widest value (`bin/_lib.py`'s `format_columns`). |
| `ds-disable` | Python | Append the opt-out marker to `AGENTS.md`; optionally update the global config. |
| `ds-doctor` | Python | Inspect and repair global install health (symlinks, bin wrappers, a non-blocking WARN when `~/.local/bin` is not on PATH, hook paths in `settings.json`, DS-54 hooks-snapshot staleness), foreign-plugin PreToolUse Agent/Task hook detection (DS-198), and a non-blocking WARN when the installed Claude Code CLI is inside the worktree nested-memory reload window 2.1.259 to <2.1.287 (`CLAUDE_RELOAD_FIRST_SUSPECT`/`CLAUDE_RELOAD_FIRST_CLEAN`). |
| `ds-emit` | Bash | Append one structured JSON event to `.agentic/events.jsonl` at orchestration boundaries. |
| `ds-eval` | Python | Read-only evaluation of one or more repos over one window (`bin/_eval.py`'s `evaluate()`): per-agent priced cost and value signals, Skeptic yield per round, ticket-keyed work items (tiers 1-4 with tier agreement) with PR outcome and rework, per-hook deny quality with operator `fire_label`s, cost reconciled exactly to the harness cost-state across every `~/.claude*` store, and ranked proposals (D1a-D3, each with a route) plus telemetry gaps. Every metric is `{status: OK\|ABSENT\|PARTIAL, value, coverage, missing}`; missing input is ABSENT with a reason, never 0. `--store` replaces the default store set (`_transcripts.resolve_stores`), and an unreadable or missing store is listed ABSENT in `meta.stores`. `--no-gh` makes no `gh` call. The text report's `[work_items]` lists the `--top N` (default 10) keyed items by priced cost (key, tier, priced usd, summed spawn wall, Skeptic rounds, PR outcome) plus a summary line (`work_items.summary`); `by_iteration` prints `iteration_tagged_share` and `findings_carrying_share` separately; `cost.reconciled.empty_sessions` and `cost.no_cost_state.empty_sessions` (no usage record and no cost-state spend; `_eval.is_empty_session`, the definition `ds-change-delta` shares) are left out of `cost.window_total.coverage`. PR outcome never asserts "not merged": an item the in-window ticket-id match misses is checked against its own PR numbers in the window's merge set (`own_prs_merged_in_window`), then one undated `git log --first-parent --grep` on the base ref for its PR numbers and ticket id (`merged_outside_window`, counting `(#N)` and `Merge pull request #N from` subjects and never a `Revert "` one; `ticket_mention_commits`). It prints one of `n/a (no merged-PR history)`, `merged #N` (with `, K follow-up` and `, reverted` when they apply), `merged #N (before window)`, `merged #N (after window)`, `no PR merge found (ticket id mentioned in K base commit(s))`, `no merge found in window (wider base history unreadable)`, or `no merge found` when that first-parent history has neither. `keepalive` (per repo, from transcripts only) reports pings fired/passed/dropped/deleted/wrapped, ping tokens and priced dollars per outcome, operator returns after more than 20 min idle (gap since the previous non-ping main-chain request) in 20-60/60-120/120-240/over-240 min buckets split by whether any ping that made a request fell in the gap, with count, cold count and average cache read/creation, and an `estimated_net_usd` (avoided creation minus all ping spend) that is an estimate, never a measured saving, and ABSENT when the avoided side cannot be estimated. Returns are reported in windows without pings too, so a pre-keepalive window is the baseline; they need `turnOrigin` (Claude Code 2.1.280+, else ABSENT "no turnOrigin in transcripts") and never count a session file's first human turn. A bucket under 10 returns is PARTIAL; with no ping in the window the ping figures are ABSENT. Window-edge skew: the estimate's avoided side counts in-window returns whose ping may have fired before `--since`, while its cost side counts only in-window pings, so short windows are skewed and comparisons need windows of similar length. Value change: `agents.conductor` no longer includes keepalive ping usage (`cost` and `mix` still do). For `ds-change-delta` on the keepalive, cut at the first real ping fire, not the merge date. |
| `ds-evaluate` | Python | Signal collector for `/ds-evaluate`: runs `bin/_eval.py`'s `evaluate()` over `--telemetry-repo` (repeatable; default the cwd's git toplevel) for a `--since`/`--until` window (default 30 days), adds the `--repo` DinoStack checkout's budget gates, resident floor and vision pillars, and ends with the ranked refinement proposals (rule, evidence, `rank_usd` or unpriced, route) and then the telemetry gaps (no route, no rank). Reads no session-log file. |
| `ds-feedback` | Python | Manage the home-dir feedback store (`~/.agentic/feedback.jsonl`) - append/list/mark operator and agent friction items. A draft may carry an optional `fire_label {hook, fire_key, fire_ts, verdict}` (operator verdict on one hook deny); such an item is born `triaged`, so `/ds-feedback-triage` never lists it, and `ds-eval` reads it for a hook's labelled false-positive rate. |
| `ds-help` | Python | Print the static slash-command reference to stdout. Zero file I/O; never fails. |
| `ds-hook-fire-report` | Python | Per-hook fire-count status for `hooks/enforce-*.py`, joining posture (ACTION_ONLY vs EVERY_VERDICT, via `scripts/lib/enforcer_facts.py`) against `.agentic/.enforcement-fires.jsonl`. Also merges any stranded `.claude/worktrees/*/.agentic/.enforcement-fires.jsonl` copies (deduped by exact line text) into the same analysis at query time - see `meta.stranded_worktree_logs_merged`/`meta.stranded_worktree_records_merged`. `--repo` may be a consumer repo with no `hooks/` dir: hook sources then come from the tool's own checkout while fire logs still come from `--repo` (`meta.hooks_dir` names which). Each row also carries `fire_count_window_no_session_id` (table column `no-sid`): the part of the window count whose rows have no `session_id` (a hook test fixture, or a row written before rows carried one), which is not a measured live-session figure, and two flags for a hook none of whose window rows has one: `window_predates_session_ids` (every such row is at or before `meta.first_session_id_ts`, so the missing id says nothing) and `window_all_unjoinable` (table status `ACTIVE*`: ids were being logged, the rows still cannot be attributed to a session, cause unknown - never zero-fire evidence). `meta.legend` explains all three. Input for `/ds-prune-harness` Signal 8. |
| `ds-identity` | Python | Manage per-developer identity files used by the Stop hook for session telemetry attribution. |
| `ds-lane` | Bash + Python stdlib helper | Launch harness-matched headless lanes: Claude streaming `-p` supports live `send`; Codex bounded `exec --json` requires post-exit exact-session `resume`, and `close` never terminates it. Commands: `start`/`send`/`close`/`reap`/`resume`/`list`/`tail`/`watch`/`open`/`open-all`/`stop`/`resume-hint`/`count`/`path`/`doctor`. Native identities and agreeing explicit bindings select the harness; missing/conflicting identity or policy refuses without fallback. Saved executable, config, cwd, and live permission context control resume; Claude preserves mode/background ceiling, Codex preserves supported sandbox fields with unattended approvals denied. State is the primary checkout's self-ignoring `.agentic/lanes/`, printed by `path`, even from a linked worktree; runtime cwd stays the caller's original cwd. Capacity is shared (`DS_LANE_MAX`, default 5). Assistant-only task markers produce done/blocked unless a fatal current-run event overrides them: alive/up while live, dead/down after exit. `reap` closes marked Claude input and records exited Codex outcomes, never killing a live Codex turn. PID birth time plus exact executable and backend command shape protect ownership. Legacy records with `claude_bin` stay Claude; resume requires explicit original config and captured init mode/cwd. Optional macOS Terminal watch tabs preserve focus and print `tab:`/`fix:` outcomes; `DS_LANE_NO_TAB=1` disables them. Read-only `doctor [--json]` separates selected runtime/policy readiness, tab readiness, and PATH launch offer; authentication, quota and macOS consent remain unverified. Canonical procedure: `content/references/runbook-conductor.md`. |
| `ds-learning-shard` | Python | Manage the home-dir per-session learning shard store (`~/.agentic/learnings-shards/<repo-key>/<session-key>.jsonl`) - `append` one in-flight learning (capped at 5 per session, soft-fail), `rollup` the not-yet-folded raw entries idempotently, `list` for diagnostics. Performs no classification. `--repo` accepts any path inside the repo - a linked worktree and its primary checkout resolve to one `<repo-key>`, via `git rev-parse --git-common-dir`. |
| `ds-learnings-retrieval-rate` | Python | Read-only, on-demand measurement of the per-role firing rate of the prior-learnings retrieval step, from Claude Code's own subagent transcripts (`<store>/*/*/subagents/agent-*.jsonl` plus each `.meta.json` sidecar's `agentType`) in every config-dir store `bin/_transcripts.py` finds (`~/.claude*/projects` plus the active config dir), not only the active one; repeatable `--store DIR` replaces that set, and output names every store with its transcript count (an unreadable store, or a `--store` path that is missing or not a directory, is reported `ABSENT` with a reason; a store with an unreadable directory inside it is `partial`, its count a floor (`>= N`) or `ABSENT` - an unreadable directory is never counted as `0`). Reports wired roles against unwired controls with both pooled lines; `wiring` is derived from whether `content/agents/<role>.md` carries a `<!-- shared:learnings-retrieval -->` span, so it self-updates. Exit 0 on every path, errors via envelope. Read-side only - DS-159 owns write-side per-entry outcome. |
| `ds-memory` | Python | Query `.agentic/events.jsonl`, `MEMORY.md`, and `.agentic/context.md`; return compact Markdown summaries. |
| `ds-memory-capture` | Python | Two-tier memory capture: `detect` reports standard vs two-tier from shard state, `flush` turns `.agentic/memory-capture-inbox.md` lines into shards through the project's own `ingest` (exit 2, nothing touched, in standard mode), `new-shards` lists shards absent from a ref for the Phase 11e / Part G ride-along, `repair` re-sequences after a merge collision. See `content/references/memory-shard-convention.md` §Two-tier capture. |
| `ds-migrate` | Python | Apply additive project scaffolding migrations (`check` / `apply` / `diff` subcommands). |
| `ds-parse-subagent-usage` | Python | Parse a Claude Code subagent transcript JSONL and emit `{tokens, model, wall_seconds}` for `spawn_complete` events. |
| `ds-retire-carrier` | Python | Completion-time retirement of one worktree carrier: removes the checkout (never the branch) only when it is unlocked, clean, free of protected ignored content, contains no other registered worktree and pins no commit that would lose its last ref; otherwise appends a hold (owner, reason, revisit event) to `.agentic/worktree-cleanup-skips.jsonl`. `--hold`/`--release` for explicit holds, `--pin` keeps unpinned commits on a `carrier-pin/*` branch, `--list-holds` folds the ledger, `--dry-run` writes and fetches nothing. Never forces, unlocks, prunes, pushes or deletes a branch. |
| `ds-skill-load-rate` | Python | Read-only, on-demand measurement (DS-227) of how often the `dinostack` skill actually loads per session, from Claude Code's own TOP-LEVEL session transcripts (`<store>/<project-dir>/*.jsonl` in every config-dir store `bin/_transcripts.py` finds, not only the active one; repeatable `--store DIR` replaces that set, and output names every store with its session count, an unreadable store or a missing or non-directory `--store` path as `ABSENT` with a reason, and a store with an unreadable in-scope project dir as `partial` with a floor count (`>= N`) or `ABSENT` - an unreadable directory is never counted as `0`; deliberately not the `*/subagents/agent-*.jsonl` glob `ds-learnings-retrieval-rate` reads). Headline figure is the unprompted (model-initiated `Skill` tool_use) load rate against operator-typed `/dinostack`, plus a nudge-fired-but-no-load count. Scoped by default to the current repo's project directories as `_transcripts.project_dirs()` maps them (the repo and its worktrees; `--repo-path`/`--project-dir`/`--all-projects` to override or widen); reports `ABSENT`, never `0`, when no transcripts are in scope. A diagnostic, not standing telemetry - see its own module docstring's retirement condition. |
| `ds-status` | Python | Read-only dump of the activation resolver state with provenance and plain-English explainer. |
| `ds-ticket-grant` | Python | Write a one-shot, TTL-bounded exception to `hooks/enforce-ticket-batching.py`'s deny on agent-initiated tracker-ticket creation (`grant --repo <path> --session-id <id> --reason <str>`); the hook itself reads, validates, and atomically consumes it on the next denied creation. |
| `ds-tracker` | Python | Manage the project-local, gitignored `.agentic/tracker.yml` tracker-config overlay (`init` / `show` / `set` / `resolve` / `path`), merged field-by-field over the `AGENTS.md` tracker resolution chain. |
| `ds-update` | Python | Non-interactive updater: fetch origin, rebuild adapters (forcing the loop even when `old_head==new_head` if the local DS-54 hooks-snapshot has diverged from the live checkout), reset version-check cache, run `ds-doctor --fix`. |
| `ds-wrap-acquire-lock` | Node | Poll-wait (background) for the /ds-wrap directory lock, exiting when acquired, on a 20-minute timeout, or (`--no-wait`) immediately busy; publishes a role-tagged (`--role=agent\|daemon\|commit`) lock descriptor and structurally never removes a lock. |
| `ds-wrap-release-lock` | Node | Release the `/ds-wrap` directory lock (`.agentic/wrap/lock`) safely where `rm -rf` is permission-denied; owner-scoped - refuses removal when the descriptor names a live foreign-process PID, and refuses to touch anything at the lock path that isn't a lock directory it created. |
## Upstream dependencies
- Python 3 stdlib only - no third-party installs required for any Python binary.
- `ds-wrap-acquire-lock` and `ds-wrap-release-lock` require Node; both load `hooks/lib/wrap-marker.js` via `__dirname`-relative path (not `cwd`).
- `ds-emit` shells out to `python3` and `date` for safe JSON assembly.
- `ds-cost` soft-depends on `pyyaml` for `~/.agentic/pricing.yml`; absent = token-only output.
- `ds-update` shells out to `git` and `bash <adapter>/install.sh`.
- `ds-parse-subagent-usage` reads `<config_dir>/projects/` transcript files, where `config_dir` is resolved via `bin/_lib.py`'s `resolve_claude_config_dir()`: `AGENTIC_CONFIG_DIR` > `CLAUDE_CONFIG_DIR` > `CODEX_HOME` > `PI_CODING_AGENT_DIR` (first non-empty wins), falling back to `~/.claude` only when none is set - not a hardcoded `~/.claude/projects/` path.
## Downstream consumers
`content/commands/` slash-command specs; adapter install scripts (`.claude/install.sh`, `.codex/install.sh`, etc.) that symlink these onto `PATH`; `hooks/stop-context.js` for `ds-identity` helpers; Activation preflight Step 6 for `ds-migrate`; `content/commands/ds-wrap.md` Part D.5, `content/commands/ds-feedback-triage.md` and `bin/_eval.py` for `ds-feedback`.
## Failure-mode discipline
Every binary is fail-open: unexpected input, missing files, and permission
errors are swallowed and surfaced via non-zero exit codes or stderr lines,
never uncaught exceptions. Exit-code conventions are per-command in each
binary's `Public API` block. Conductors must not treat a non-zero exit as a
session-fatal error.
## Why polling belongs in a binary, not conductor prose
An LLM cannot reliably run a long foreground poll loop (foreground `sleep` is blocked in the harness). `/ds-wrap`'s lock-wait was once prose the conductor hand-ran ("poll every 5s for 20min") and reliably gave up after one or two checks. Fix pattern: move the poll loop into a small binary invoked as a background spawn (`run_in_background: true`) so the conductor stays available and gets notified with an exit code on completion - a background spawn on this harness does deliver a synchronous exit code on completion. This is why `ds-wrap-acquire-lock` exists as a binary rather than as conductor prose.