npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

github-issue-tower-defence-management

v2.157.6

Published

[![Test](https://github.com/HiromiShikata/npm-cli-github-issue-tower-defence-management/actions/workflows/test.yml/badge.svg)](https://github.com/HiromiShikata/npm-cli-github-issue-tower-defence-management/actions/workflows/test.yml) [![code style: pretti

Readme

npm-cli-github-issue-tower-defence-management

Test code style: prettier semantic-release: angular

Welcome to npm-cli-github-issue-tower-defence-management :tada:

Usage 🛠️

Here's how you can use github-issue-tower-defence-management:

Usage: github-issue-tower-defence-management [command] [options]

CLI tool for GitHub Issue Tower Defence Management

Commands:
  schedule [options]                    Handle scheduled events (trigger: issue or schedule) (default)
  startDaemon [options]                 Start daemon to prepare GitHub issues
  notifyFinishedIssuePreparation [options]  Notify that issue preparation is finished
  selectLlmLaunchFlags [options]        Print the --effort and --autocompact launch flags for a worker session, derived from the defaultLlmEffortLevel and defaultLlmAutocompactMode fields of the config file at --configFilePath. Writes "--effort <value>" using defaultLlmEffortLevel, falling back to "xhigh" when defaultLlmEffortLevel is unset or empty. Writes "--autocompact <value>" after it on the same stdout line only when defaultLlmAutocompactMode is set to a non-empty value. The launcher decides no value itself and only forwards these flags to claude-agent, the same split as the selectLiveSessionOauthToken command.
  checkIssueReviewReadiness [options]   Check whether an issue is review-ready (read-only; does not change Status or post any comment)
  serveWeb [options]                    Start the local TDPM web server (console tabs, dashboard, and in-tmux session list)
  serveConsole [options]                Deprecated alias for serveWeb. Use serveWeb instead.
  selectOauthToken [options]            Print one rate-limit-aware Claude Code OAuth token to stdout (pipeable; read-only)
  selectLiveSessionOauthToken [options] Print one Claude Code OAuth token chosen for a new live interactive session (soonest 7d reset among tokens under their concurrent session limit; read-only)
  countInTmuxByHumanSessionsPerToken [options]  Print, per OAuth token, the count of live "In Tmux by human" interactive sessions using that token (read-only)
  killTmuxSession [options]             Cleanly kill a tmux session by running tmux kill-session and stopping its cl-*.scope systemd --user unit
  attachOrCreate [options]              Attach to an existing registered tmux session for the issue URL, or create a new one
  ownerCallFileAppend [options]         Append one owner call to the per-session owner call file (writes nothing to stdout)
  ownerCallFileDelete [options]         Delete the per-session owner call file (writes nothing to stdout; an already absent file is not an error)
  archive-unresumable-session [options]  Move aside the conversation record of a session that ended because its prompt exceeded the model context limit, so the next launch starts a fresh conversation
  select-resumable-session [options]    Print the claude session id a dispatched worker resumes, or nothing when it starts a new claude session, copying the transcript file from another worktree when needed
  check-issue-silent-dispatch-allowed [options]  Check whether an issue's body allows a dispatch cycle to end with no agent comment (read-only)
  workerRequestTextRead [options]       Print the request text a worker session receives for an issue, "Take ownership of <issue url>" (read-only)
  help [command]                        display help for command

Options for schedule:
  -t, --trigger <type>          Trigger type: issue or schedule
  -c, --config <path>           Path to config YAML file
  -v, --verbose                 Verbose output
  -i, --issue <url>             GitHub Issue URL
  --inTmuxProjectOrder <names>  Comma-separated project names, in display order, for the in-tmux-by-human session list. When omitted, falls back to the inTmuxProjectOrder value in the config file.

Options for startDaemon:
  --configFilePath <path>                          Path to config file for tower defence management (required)
  --projectUrl <url>                               GitHub project URL
  --manager <login>                                GitHub login of the manager; only Awaiting Workspace issues assigned to this login are picked up (required)
  --defaultAgentName <name>                        Default agent name
  --defaultLlmModelName <name>                     Default LLM model name
  --fallbackLlmModelName <name>                    LLM model used when no default model is specified (default: claude-opus-4-8)
  --defaultLlmAgentName <name>                     Default LLM agent name
  --maximumPreparingIssuesCount <count>            Maximum number of issues in preparation status (default: 6 per available Claude OAuth token, otherwise 6)
  --utilizationPercentageThreshold <percent>       5-hour utilization hard threshold; tokens at or above this percentage are excluded from rotation. Per-token concurrency also tapers from 6 slots down to 1 as either the 5h or 7d utilization rises from 80% toward 100%, taking the more restrictive of the two (default: 90)
  --allowedIssueAuthors <authors>                  Comma-separated list of allowed issue authors
  --preparationProcessCheckCommand <template>      Shell command template with {URL} placeholder to check if a preparation process is alive
  --fleetConfigFilePath <path>                     Path to the fleet-wide YAML config file holding the preparationWorker mapping (normalConcurrentLimit); falls back to the TDPM_FLEET_CONFIG environment variable, and to the built-in values when neither is set

Options for notifyFinishedIssuePreparation:
  --configFilePath <path>                          Path to config file for tower defence management (required)
  --issueUrl <url>                                 GitHub issue URL (required)
  --projectUrl <url>                               GitHub project URL
  --thresholdForAutoReject <count>                 Threshold for auto-escalation after consecutive rejections (default: 3)
  --thresholdForDispatchLoop <count>               Threshold for auto-escalation after one agent is dispatched this many times since the last human comment, whether it names itself or two agents name each other in turn (default: 6)
  --workflowBlockerResolvedWebhookUrl <url>        Webhook URL to notify when a workflow blocker issue status changes
  --missingAgentName <name>                        Agent definition name that was not found; triggers task issue creation (assigned to the manager from config) and blocks the item until that issue is closed
  --sessionErrorLine <line>                        Exact error line from the session log to include in the task issue body
  --deferPreparation                               Defer the item via the Reactivation Trigger fields (sets nextActionDate to tomorrow) and return it to Awaiting Workspace without creating any issue, recording the value of --sessionErrorLine as the stop reason to the console log; use for transient upstream failures that should retry the next day
  --moveToFailedPreparation                        Move the item to Failed Preparation status after reaching the consecutive failure threshold; takes precedence over --deferPreparation when both are supplied; records --sessionErrorLine in the comment
  --fleetConfigFilePath <path>                     Path to the fleet-wide YAML config file holding the workflowIssueReporter mapping; falls back to the TDPM_FLEET_CONFIG environment variable. When set and the file contains a workflowIssueReporter section, a silent-redispatch escalation automatically creates (or comments on) an issue in the configured repository instead of only logging locally.
  --dispatchStartedAt <timestamp>                  ISO-8601 UTC timestamp (for example 2026-01-31T09:00:00Z) at which the item entered Preparation for the session that just ended. An agent report posted before this time is not counted as that session's report, so a session that posts nothing is recorded as NO_REPORT_FROM_AGENT_BOT and counts toward the consecutive-no-report threshold (thresholdForAutoReject) that moves the item to Failed Preparation. When omitted, the latest agent report on the issue is used whatever its age.
  --sessionLogFilePath <path>                      Path to the log of the worker session that just ended. The command reads it once, before the notification, and sets --rateLimitRejected for a usage-limit ending, or for a rejected rate-limit event when the session ended neither with terminal_reason blocking_limit nor on an HTTP 429 session-limit error, --promptTooLongOnResume for a resumed session that ended with terminal_reason blocking_limit before any tool call, and --moveToFailedPreparation with its own --sessionErrorLine on the third consecutive ending with the same non-completed terminal_reason. The consecutive count is kept per issue under the TDPM cache directory (worker-session-failure-streaks) and is reset by a completed ending or a log without a terminal_reason. The result is combined with the explicit flags; usage-limit and rate-limit classification is skipped when a non-empty --missingAgentName is given, which also keeps the count from moving the item to Failed Preparation.
  --sessionWasResumed                              The worker session that just ended resumed a previous conversation; used with --sessionLogFilePath to decide --promptTooLongOnResume
  The notification is attempted up to 3 times, waiting 30 seconds and then 60 seconds after a failed attempt; the session log is classified only once. A GitHub API rate limit ends the command without an error and without a retry, and the third failure ends the command with that error.

Options for selectLlmLaunchFlags:
  --configFilePath <path>                          Path to config file for tower defence management (required)

Options for checkIssueReviewReadiness:
  --configFilePath <path>                          Path to config file for tower defence management (required)
  --issueUrl <url>                                 GitHub issue URL (required)
  --projectUrl <url>                               GitHub project URL

Options for serveWeb (and its deprecated alias serveConsole):
  --configFilePath <path>                          Path to config file for tower defence management (required)
  --port <number>                                  Port for the web HTTP server (default: 9980)
  --consoleDataOutputDir <path>                    Directory where console data files are written and served from
  --inTmuxDataDir <path>                           Directory containing the flat in-tmux-by-human static JSON files served at /in-tmux-by-human/*.json
  --dashboardDir <path>                            Directory containing the static dashboard HTML fragment tdpm.txt served at /tdpm.txt when compose mode is not active (default: the jsonpub directory)
  --dashboardDataDir <path>                        Directory containing the dashboard data files (projects/<projectName>.json, machine-status.json, token-status.json); when set and at least one listed project has its own data file present the server composes the /tdpm.txt fragment live from whatever data is available, showing -- for any project without a file, otherwise it falls back to serving the static tdpm.txt from --dashboardDir (unset when not configured)
  --dashboardProjectNames <names>                  Comma-separated project names, in display order, for the dashboard project grid; the display label of each project is its first 2 characters, which must be unique across the listed names
  --fleetConfigFilePath <path>                     Path to the fleet-wide YAML config file; falls back to the TDPM_FLEET_CONFIG environment variable. When the file contains a top-level `workflowImprovementIssueUrl` string, the console tab bar displays a link that opens that URL in a new browser tab. When the file contains a `workflowIssueReporter` section with `owner` and `repo` fields, the console tab bar displays a + link for creating fleet-level tasks; the link opens `https://github.com/{owner}/{repo}/issues/new?assignees={owner}` in a new browser tab, pre-assigning the owner, and, when `projectUrl` starts with a GitHub org (`https://github.com/orgs/{org}/projects/{n}`) or user (`https://github.com/users/{user}/projects/{n}`) project URL (optionally followed by a `/views/N` path suffix or query parameters such as `?fullscreen=true`), also appends `?projects={owner}/{n}` so the new issue is automatically added to that project.

Options for selectOauthToken:
  --tokenListJsonPath <path>                       Path to the JSON array of { name, token, selectionWeight? } records; selectionWeight is an optional positive number (default 1) that biases how often a token is chosen among eligible candidates (falls back to the CLAUDE_CODE_OAUTH_TOKEN_LIST_JSON_PATH environment variable)
  --cacheDir <path>                                Directory holding per-token rate-limit cache files (falls back to the TDPM_RATELIMIT_CACHE_DIR environment variable, then to ${XDG_CACHE_HOME:-~/.cache}/tdpm/ratelimit)

Options for selectLiveSessionOauthToken:
  --tokenListJsonPath <path>                       Path to the JSON array of { name, token, selectionWeight? } records; selectionWeight is an optional positive number (default 1) that scales this token concurrent live session limit; a smaller weight allows fewer simultaneous sessions and never bypasses eligibility filtering or starves a sole eligible token. Falls back to the CLAUDE_CODE_OAUTH_TOKEN_LIST_JSON_PATH environment variable.
  --cacheDir <path>                                Directory holding per-token rate-limit cache files (falls back to the TDPM_RATELIMIT_CACHE_DIR environment variable, then to ${XDG_CACHE_HOME:-~/.cache}/tdpm/ratelimit)
  --fleetConfigFilePath <path>                     Path to the fleet-wide YAML config file holding the liveSessionOauthTokenSelection mapping (maxConcurrentSessionCount, fullSpeedFiveHourFreeRatio, minFiveHourFreeRatio, minSevenDayFreeRatio, fiveHourShareConsumedPerSessionHour); falls back to the TDPM_FLEET_CONFIG environment variable, and to the built-in values when neither is set

Options for countInTmuxByHumanSessionsPerToken:
  --configFilePath <path>                          Path to config file for tower defence management (required)
  --projectUrl <url>                               GitHub project URL
  --tokenListJsonPath <path>                       Path to the JSON array of { name, token } records (falls back to the claudeCodeOauthTokenListJsonPath config value, then to the CLAUDE_CODE_OAUTH_TOKEN_LIST_JSON_PATH environment variable)

Options for killTmuxSession:
  --session <name>                                 Name of the tmux session to kill
  --self                                            Terminate the current session by stopping its own cl-*.scope systemd user unit, derived from /proc/self/cgroup

Options for attachOrCreate:
  --issueUrl <url>                                 GitHub issue URL to attach to or create a session for (required)

Options for ownerCallFileAppend:
  --session <name>                                 tmux session name that raised the call (required)
  --calledAt <timestamp>                           Time the call was raised, as UTC ISO-8601 with second precision and a trailing Z (required)
  --body-file <path>                               Path to the file holding the call body; the body is read from a file because it is multi-line and can be long (required)
  --inTmuxDataDir <path>                           Directory the owner call files are written under, the same directory serveWeb serves them from

Options for ownerCallFileDelete:
  --session <name>                                 tmux session name that raised the call (required)
  --inTmuxDataDir <path>                           Directory the owner call files are written under, the same directory serveWeb serves them from

Options for archive-unresumable-session:
  --log-file <path>                                Path to the finished run stream-json log (required)
  --session-dir <path>                             Directory holding conversation records named <session-id>.jsonl (required)
  --archive-dir <path>                             Destination directory the conversation record is moved into; created when it does not exist (required)

Options for select-resumable-session:
  --session-name <name>                            Name the worker claude session is started with (claude --name); a transcript file is a candidate only when its first line contains "customTitle":"<name>" (required)
  --session-dir <path>                             Session directory holding the transcript files (<session id>.jsonl) of the current working directory of the worker; claude --resume <session id> finds transcript files only in this directory (required)
  --archive-root <path>                            Directory archive-unresumable-session moves transcript files into; a transcript file is archived when <archive-root>/<session id>.jsonl or <archive-root>/<directory>/<session id>.jsonl exists (required)
  --other-session-dir <path>                       Session directory of another worktree of the same repository, searched when --session-dir yields no resumable transcript file; repeatable, and of transcript files with the same modification time the one in the directory given first wins

Options for check-issue-silent-dispatch-allowed:
  --issue-url <url>                                GitHub issue URL (required)

Options for workerRequestTextRead:
  --issueUrl <url>                                 GitHub issue URL the worker session takes ownership of (required)

The serveWeb sub-command starts a local HTTP server that serves the TDPM web surface — the console tabs, the dashboard, and the in-tmux-by-human session list. serveConsole remains available as a deprecated alias that maps to the same handler, so existing invokers keep working during rollout; new usage should prefer serveWeb. One running instance serves every project: the user opens a per-project URL path /projects/{pjcode} (or /projects/{pjcode}/{workflow-blocker|prs|triage|unread|failed-preparation|todo-by-human}) and the bundled UI reads the pjcode from its own URL path and loads that project's list data. The server serves the bundled single-page-application index.html at /, /index.html, and every /projects/{pjcode} and /projects/{pjcode}/{tab} app route. Every response is sent with Cache-Control: no-store. Any request path containing a segment that begins with a dot (for example /.git or /.env) is rejected with HTTP 404. The UI bootstrap assets (HTML and JS) are served without authentication; served *.json files and /api/* paths require an access token supplied either as the k query parameter (?k=<token>) or the X-PV-Token request header. The access token is read from the consoleAccessToken config value and never appears on the command line. When the built UI bundle directory (ui-dist) is absent the server still starts and serves a minimal placeholder index for /, /index.html, and the per-project app routes.

The dashboard fragment is served unauthenticated at GET /tdpm.txt as text/html. Compose mode is opt-in: when --dashboardDataDir is set and at least one project named in --dashboardProjectNames has its own projects/<projectName>.json readable, the server composes the fragment live at request time from whatever of machine-status.json, token-status.json, and the per-project projects/<projectName>.json files are actually present — a project with no readable data file renders -- in its row independently of the others, and a missing machine-status.json or token-status.json no longer blocks composition. Each present projects/<projectName>.json is keyed by the full project name (the same key the scheduled run writes; that project's actionable status counts), machine-status.json holds mem%, cpu%, disk%, loadavg, cycle-minutes, and token-status.json holds per-token rate-limit utilization, reset countdown, status color, and prep/hum counts. It renders the fixed-width fragment — the host-metrics line ({memDot}M{mem}% {cpuDot}C{cpu}% {diskDot}D{disk}% cy{cycle} {loadDot}LA {load1} {load5} {load15} for the no-disks case; each dot is 🔴 at or above the danger threshold — ≥90% for mem/cpu/disk, ≥10 for load1m — or 🟡 at or above the warning threshold — ≥80% for mem/cpu/disk, ≥5 for load1m — and absent when the metric is normal; the per-disk case still emits multiple lines), the project grid (a totals row summing each column across projects with data, then the header, then one row per name in --dashboardProjectNames order, labelled with the project's two-character display code derived from its name and prefixed with its severity dot), a blank separator line, then one row per token sorted by soonest seven-day reset — wrapped in <tt>…</tt><br> with spaces encoded as &nbsp;. Otherwise — when --dashboardDataDir is unset, or it is set but none of the listed projects has any data file present — the server falls back to serving the static tdpm.txt byte-for-byte from --dashboardDir, exactly as before compose mode existed. The route returns HTTP 404 only when neither source is available (compose mode inactive and no static tdpm.txt present). Only GET is accepted on this path.

Behind the token gate the server exposes three groups of routes:

  • Data delivery (GET): GET /projects/{pjcode}/{workflow-blocker|prs|triage|unread|failed-preparation|todo-by-human}/list.json and the matching detail/<key>.json files are read from {consoleDataOutputDir}/{pjcode}/{tab}/, and GET /projects/{pjcode}/in-tmux-by-human/* is read from {consoleDataOutputDir}/{pjcode}/in-tmux-by-human/. Each served list has the .done cross-tab exclusion applied. The schedule cycle writes the full tab files; notifyFinishedIssuePreparation additionally patches the affected tab files immediately after each status transition.
  • Flat in-tmux-by-human static files (GET): GET /in-tmux-by-human/<file>.json serves the flat static JSON files (for example index.v4.json and {project}.v4.json, plus the v3 files for backward compatibility) byte-for-byte from --inTmuxDataDir. Only flat file names directly under that directory are served; any other nested path and any path traversal attempt are rejected with HTTP 404. This route is distinct from the per-project /projects/{pjcode}/in-tmux-by-human/* data-delivery route above. These files are still gated by the same access token, so requests carry ?k=<token>. The token-keyed file generation is performed by an external watcher, not by this server.
  • Per-session owner call files (GET and DELETE): GET /in-tmux-by-human/call-to-user/{pjcode or NA}/{session}.yaml serves the owner call file of exactly one session byte-for-byte from --inTmuxDataDir, as text/yaml, and DELETE on the same path removes it and answers HTTP 204 whether or not the file was still there, so a client that deletes twice is not in error. Only this one-directory-deep shape is accepted; DELETE is refused with HTTP 404 on every other path under the prefix, including the flat JSON files. Both methods sit behind the same access token as every other served file. There is no route that returns several sessions at once: a client derives one path from one session name and fetches that.
  • Read APIs (GET, backed by the server-side GH_TOKEN): GET /api/itembody, GET /api/comments, GET /api/prfiles, GET /api/prcommits, GET /api/relatedprs, and GET /api/issuetitle. Each takes a url query parameter. GET /api/issuetitle returns { state, merged, isPullRequest, title }, where title comes from the pull request summary for pull request URLs and from the issue for issue URLs; it is served through an in-process cache: a merged result is cached permanently and every other result is re-fetched after 300 seconds. The console UI uses this endpoint both for the opened item header and to decorate full PR/issue URL links inside rendered markdown with their open/closed/merged state icon and title.
  • Operation APIs (POST, JSON body): POST /api/review (approve, request_changes, close), POST /api/triage (set_status, set_story, close, close_not_planned, snooze_1hour, snooze_3hours, snooze_6hours, snooze_1day, snooze_2days, snooze_3days, snooze_5days, snooze_1week, snooze_1month), and POST /api/intmux (set_intmux). These routes are multi-project: every request body carries the pjcode of the project the UI is currently viewing (taken from the UI's own /projects/{pjcode} URL path), the server resolves that pjcode to its GitHub Project URL through the pjcode → projectUrl mapping described below, loads that project's status and story options (lazily, cached per pjcode), and applies the operation against the resolved project. A request with no pjcode, or a pjcode that has no configured project URL, is rejected with HTTP 400. Each confirmed operation records the affected projectItemId into the .done exclusion under the resolved pjcode so it disappears from every tab's served list for that project only. One running serveWeb instance therefore serves both reads and writes for every configured project.
  • Comment APIs (POST, JSON body, backed by the server-side GH_TOKEN): POST /api/comment posts a top-level comment on the issue or pull request named by the request body's url. POST /api/reviewcomment posts a line-anchored inline review comment on a pull request diff; its request body carries url (the pull request URL), path (the changed file path), line (a positive integer diff line number), side (RIGHT for added or context lines on the new side, LEFT for removed lines on the old side), and body. The server resolves owner, repo, and pull request number from url, fetches the pull request head commit sha, and calls GitHub POST /repos/{owner}/{repo}/pulls/{number}/comments with commit_id set to that sha. When GitHub rejects the request, the response is HTTP 502 with the GitHub error message in the error field of the body so the UI can show the real reason.

The .done exclusion is persisted per tab in a .done.json file alongside each tab's list.json under consoleDataOutputDir. The file is never directly servable because the dot-segment block rejects any path containing it. It is an optimistic-hide store scoped to a single regeneration cycle: each confirmed operation records the affected projectItemId so the item disappears immediately, and the schedule cycle resets every tab's .done.json back to { "projectItemIds": [] } right after it regenerates that project's list.json files. This bounds the store to at most one cycle of recorded ids, so it can never accumulate and over-hide items that legitimately re-enter a visible status.

Multi-project operation routing is configured through an optional consoleProjects mapping in the config file. It maps each pjcode to that project's GitHub Project URL so the operation APIs can resolve the correct project per request. The configured projectName is always added to this mapping automatically (pointing at the configured projectUrl), so a single-project deployment needs no consoleProjects entry at all. When the same serveWeb instance serves write operations for several projects, list each additional project explicitly:

consoleAccessToken: '<console access token>'
projectUrl: 'https://github.com/orgs/my-org/projects/1'
projectName: 'my-project'
consoleProjects:
  my-project: 'https://github.com/orgs/my-org/projects/1'
  other-project: 'https://github.com/orgs/other-org/projects/2'

Each project's status and story options are loaded lazily the first time a pjcode is used and then cached for the life of the process, so the additional projects add no startup cost.

When the console serves multiple projects whose owners require distinct GitHub tokens, set consoleGithubTokens to an inline map of pjcode to token. When a request arrives for a repository owner, serveWeb finds the pjcode whose project URL owner matches and uses the corresponding token from the map. When consoleGithubTokens is absent or null, or when no pjcode maps to the owner, the fleet-wide GH_TOKEN is used as the fallback for that owner. When a pjcode is found in consoleProjects but is absent or blank in consoleGithubTokens, an error is thrown rather than falling back silently:

consoleAccessToken: '<console access token>'
projectUrl: 'https://github.com/orgs/my-org/projects/1'
projectName: 'my-project'
consoleProjects:
  my-project: 'https://github.com/orgs/my-org/projects/1'
  other-project: 'https://github.com/orgs/other-org/projects/2'
consoleGithubTokens:
  my-project: '<fine-grained-token-for-my-org>'
  other-project: '<fine-grained-token-for-other-org>'

In the example above, my-project's token is used for my-org and other-project's token is used for other-org. The fleet-wide GH_TOKEN is still required for the startDaemon preparation cycle; only the console HTTP server routes go through the per-project token map.

The checkIssueReviewReadiness sub-command lets an agent self-check whether an issue is currently review-ready. It does NOT change the issue Status field and does NOT post any comment. It writes a single JSON line to stdout of the shape { "reviewReady": boolean, "rejections": [{ "type": string, "detail": string }] } and exits 0 on a successful evaluation regardless of readiness; a non-zero exit indicates an operational error (auth failure, network error). The rejection types include: ISSUE_NOT_FOUND, NO_REPORT_FROM_AGENT_BOT, PULL_REQUEST_NOT_FOUND, PULL_REQUEST_IS_DRAFT, PULL_REQUEST_CONFLICTED, ANY_CI_JOB_FAILED_OR_IN_PROGRESS, REQUIRED_CI_JOB_NEVER_STARTED, ANY_REVIEW_COMMENT_NOT_RESOLVED, and MULTIPLE_PULL_REQUESTS_FOUND. The --projectUrl option is optional; when omitted the command still runs using only the issue URL.

The selectOauthToken sub-command reads the same per-token rate-limit cache that the startDaemon proxy writes (see "Claude OAuth Token Rotation" below) and prints exactly one token string to stdout so a caller can choose an appropriate token before launching Claude Code. It is read-only: it never starts the proxy, never mutates any cache file, and never writes the token anywhere. Selection runs in two stages. First, a candidate filter keeps tokens whose 5-hour window is at least 60% free (5-hour utilization at most 0.40) AND whose 7-day window is at least 14% free (7-day utilization at most 0.86), where "% free" is 1 - utilization. A token with no cache file, or whose window reset epoch has already passed, is treated as fully free (utilization 0) for these checks. The filter additionally excludes any token carrying a non-expired reactive seven_day_fable rejection marker — set when a fable-model request on that token was rejected with HTTP 429 (see "Claude OAuth Token Rotation" below) — because these interactive sessions run on the fable model; a token without the marker is treated as fable-usable, and the marker is ignored once its stored reset epoch has passed. Second, among the surviving candidates it selects the single token whose 7-day window reset epoch is nearest in the future (soonest reset), so weekly quota that would otherwise reset unused is consumed first; a candidate with no active 7-day window is treated as having the farthest reset (now + 7 days) and therefore sorts last. Each token entry may carry an optional selectionWeight (a positive number, default 1); when the eligible candidates carry differing weights the selection among them becomes weighted-random by that weight, so a token with a smaller weight is chosen proportionally less often, while a token that is the only eligible candidate is always chosen regardless of its weight. When every eligible candidate shares the same weight (the default), the deterministic soonest-reset selection above is used unchanged. The selected token string is written to stdout (pipeable) and the per-candidate decision trace is written to stderr. When no token passes the filter, nothing is written to stdout and the command exits non-zero with an explanatory message on stderr. The token-list path comes from --tokenListJsonPath or the CLAUDE_CODE_OAUTH_TOKEN_LIST_JSON_PATH environment variable; the cache directory comes from --cacheDir or the TDPM_RATELIMIT_CACHE_DIR environment variable, defaulting to ${XDG_CACHE_HOME:-~/.cache}/tdpm/ratelimit.

The selectLiveSessionOauthToken sub-command picks a token for a new live interactive Claude Code session a human is about to start, so that concurrent interactive sessions spread across distinct tokens instead of stacking onto the same one. It loads the token list and reads the same per-token rate-limit cache as selectOauthToken, and it is read-only in exactly the same way: it never starts the proxy, never mutates any cache file, and never writes the token anywhere. It applies a base rate-limit eligibility filter (5-hour window at least 25% free AND 7-day window at least 1% free, with a missing cache file or an expired window treated as fully free, and any token carrying a non-expired reactive seven_day_fable rejection marker excluded), and additionally excludes any token whose 5-hour window is less than minFiveHourFreeRatio (default 60%) free or whose 7-day window is less than minSevenDayFreeRatio (default 14%) free; these stricter thresholds prevent the cl script from starting a new interactive session on a token that is nearly exhausted. It additionally measures current live occupancy per token by scanning running Claude Code processes on the local Linux host: for each process under /proc it reads the NUL-separated /proc/<pid>/environ and, when the process is a Claude Code process, takes its CLAUDE_CODE_OAUTH_TOKEN and a session key: CLAUDE_CONFIG_DIR when set, else CLAUDE_CODE_SESSION_ID when set, else the process id. A token's occupancy is the number of distinct session keys bound to it, so child processes that inherit one config dir or session id count once, and a non-interactive claude -p process that carries neither still counts as one session. Processes without CLAUDE_CODE_OAUTH_TOKEN (for example API-key sessions) are ignored, and a process whose environ cannot be read is skipped. Among the eligible tokens the selection is deterministic and reset-first. Each eligible token is given a concurrent session limit of maxConcurrentSessionCount (default 10), scaled by its optional selectionWeight (a positive number, default 1). That limit is held at its full value while the free share of the token's 5-hour window is at or above fullSpeedFiveHourFreeRatio (default 0.5), and is tapered linearly in proportion to the free share below that point. It is further capped at the number of sessions the free 5-hour share can carry until that window resets: the free share divided by the hours left until the reset (5 hours when the reset is unknown or has passed), never more than one fifth of a window per hour, divided by fiveHourShareConsumedPerSessionHour (default 0.05, the share of a 5-hour window one session uses in an hour) and rounded down. The limit never drops under 1. For example, a token with 68% of its 5-hour window free and 4.6 hours until the reset gets a limit of 2, and a fresh window gets a limit of 4, so sessions stop piling onto a token whose window would run out before it resets. The 5-hour window is the only window that lowers the limit: it is the one that runs out within a single working stretch, and a session that hits it mid-flight has to be restarted, which costs more than it saves. The 7-day window never lowers the limit, so a weekly allowance that is about to expire is drained at full speed rather than discarded unused; the 7-day window is instead what orders the candidates. The token whose 7-day window resets soonest among those still under their limit is selected, so allowance that is about to expire is consumed before the reset discards it; ties are broken by the fewer live sessions. All five tuning numbers (maxConcurrentSessionCount, fullSpeedFiveHourFreeRatio, minFiveHourFreeRatio, minSevenDayFreeRatio, fiveHourShareConsumedPerSessionHour) are read from the fleet-wide config file named by --fleetConfigFilePath or the TDPM_FLEET_CONFIG environment variable, under a liveSessionOauthTokenSelection mapping; a key the file omits keeps its built-in value, while an unreadable file or an out-of-range value is reported as an error rather than silently ignored. A token at its limit is skipped in favour of the next soonest-resetting token that still has room, and when every eligible token is at its limit the one least over its limit after one more session (live sessions plus one, divided by the limit) is returned rather than none, so an overflow spreads across tokens. A token that is the only eligible candidate is always chosen regardless of its weight or occupancy. When no token passes the primary live session filter (5h >= 60%, 7d >= 14%), a fallback stage runs: it keeps any non-excluded token (not subscription-disabled, not unified-rejected, not fable-rejected) whose 7-day window is at least 3% free, then selects the one with the highest 5-hour free ratio among those (ties broken by fewer live sessions), so a session can still start when all tokens are near the strict threshold. The selected token string is written to stdout (pipeable) and the per-candidate decision trace is written to stderr; when no token passes both the primary filter and the fallback, nothing is written to stdout and the command exits non-zero with an explanatory message on stderr. The token-list path and cache directory are resolved exactly as for selectOauthToken. Because occupancy is read from /proc/<pid>/environ, this sub-command is Linux-specific.

The startDaemon command supports a preparationWorker mapping in the fleet-wide config file named by --fleetConfigFilePath or the TDPM_FLEET_CONFIG environment variable. This section controls per-token concurrency for the preparation worker pool. Supported keys:

  • normalConcurrentLimit (positive integer, default 6): the maximum number of concurrent preparation workers each Claude OAuth token may run at full-speed utilization. When either the 5-hour or 7-day utilization of a token rises above 80%, its per-token concurrency tapers linearly from normalConcurrentLimit down to 1.
  • maxConcurrentWorkers (positive integer, default 40): the host-wide ceiling on the total number of concurrently running preparation workers across all tokens. Once this many workers are already running, no new worker is spawned in that daemon cycle regardless of per-token limits.
  • graphqlRateLimitFloor (non-negative integer, default 500): the minimum number of GitHub GraphQL API requests that must remain in the current rate-limit window before a daemon cycle spawns any workers. When the remaining count falls at or below this value the entire spawn step is skipped for that cycle.

A key the file omits keeps its built-in value; an unreadable file or an out-of-range value is reported as an error rather than silently ignored. Example:

preparationWorker:
  normalConcurrentLimit: 8
  maxConcurrentWorkers: 40
  graphqlRateLimitFloor: 500

The schedule command (trigger schedule) supports a startPreparation mapping in the fleet-wide config file named by the TDPM_FLEET_CONFIG environment variable. Supported keys:

  • maximumPreparingIssuesCount (integer of at least 1, default 80): the maximum number of issues in preparation status, used when neither the project README nor the project config sets maximumPreparingIssuesCount.
  • urgentStoryNames (list of strings, default empty): the names of the stories whose tasks are urgent. When the list is non-empty, every preparation worker launch is held before aw starts while an urgent-story task of another project waits in Awaiting Workspace and the free worker slots across all Claude OAuth tokens do not exceed the number of such waiting tasks. A task whose own story is urgent, or that an urgent-story task in Awaiting Workspace depends on, is never held. The hold is re-evaluated every 10 seconds; after 300 seconds the still-waiting urgent-story tasks are recorded as timed out, are not waited for again during the next 1800 seconds, and the launch goes ahead; the whole hold is bounded at 420 seconds, and any error during the hold is logged as urgentStoryLaunchHold: failed (<message>); the spawn goes ahead. While a launch is held, other projects' launches do not wait for that project's urgent-story tasks. Each evaluation logs one line prefixed by urgentStoryLaunchHold: . The free worker slots are counted from the token list named by the project config's claudeCodeOauthTokenListJsonPath, otherwise by the fleet config's top-level claudeCodeOauthTokenListJsonPath (a leading ~/ is expanded to the home directory); when neither is set, or the token list has no available token, the launch goes ahead without the hold.

A key the file omits keeps its built-in value; a urgentStoryNames value that is not a list of strings, or a top-level claudeCodeOauthTokenListJsonPath that is not a string, is reported as an error. Example:

claudeCodeOauthTokenListJsonPath: ~/.config/tdpm/claude-code-oauth-tokens.json
startPreparation:
  maximumPreparingIssuesCount: 80
  urgentStoryNames:
    - urgent / production incident

The notifyFinishedIssuePreparation and revertOrphanedPreparation commands support a workflowIssueReporter mapping in the fleet-wide config file named by --fleetConfigFilePath or the TDPM_FLEET_CONFIG environment variable. When a silent-redispatch escalation fires (an agent is dispatched and returns no report three consecutive times, indicating a TDPM process-level problem rather than a task-specific one), TDPM automatically creates a tracking issue in the configured repository or, when an open issue with the same title already exists, adds a comment to it instead of opening a duplicate. Supported keys:

  • owner (string, required): GitHub owner (organisation or user) of the repository where the workflow issue is created.
  • repo (string, required): repository name under that owner.
  • projectUrl (string, optional): URL of a GitHub ProjectV2 to which the newly created issue is added. When the project contains a story option whose name includes "workflow blocker" (case-insensitive), the issue's Story field is set to that option automatically. When this URL starts with a GitHub org (https://github.com/orgs/{org}/projects/{n}) or user (https://github.com/users/{user}/projects/{n}) project URL (optionally followed by a /views/N path suffix or query parameters such as ?fullscreen=true), the fleet task creation console link also appends ?projects={org}/{n} or ?projects={user}/{n} (in addition to assignees={owner}) so that any new issue created from that link is automatically added to the project.

Example:

workflowIssueReporter:
  owner: my-org
  repo: workflow-issues
  projectUrl: https://github.com/orgs/my-org/projects/5

The startDaemon, notifyFinishedIssuePreparation, checkIssueReviewReadiness, and schedule (trigger=schedule) commands support a top-level errorReportingRepository string in the fleet-wide config file named by --fleetConfigFilePath or the TDPM_FLEET_CONFIG environment variable. When set, it takes precedence over the per-project errorReportingRepository config key and routes all TDPM-level error issues to that single repository: (1) any unhandled error that escapes the CLI process creates a new GitHub issue there, or adds a comment to an existing open issue with the same title; (2) "Register missing agent definition" tasks and "Unregistered agent in workflow configuration" blocker issues are created there instead of the product repository. The originating TDPM project name is included in the blocker issue body (as - TDPM project: <name>) when a projectName is configured in the per-project config file, so the source project is identifiable even when all errors route to a central repository. Requires GH_TOKEN.

Example:

errorReportingRepository: my-org/secretary

The countInTmuxByHumanSessionsPerToken sub-command reports, per Claude Code OAuth token, how many live interactive sessions running under that token belong to an issue that is currently in the GitHub Project Status In Tmux by human. It is read-only: it never starts the proxy, never mutates any cache file, and never writes any token anywhere. It enumerates live interactive sessions by scanning running processes on the local Linux host: for each process under /proc it reads the NUL-separated /proc/<pid>/cmdline and selects only processes launched for an interactive session, identified by a --name <issue-url> argument whose value is an HTTP or HTTPS URL; Take ownership background spawns (which have no --name argument) are excluded. For each selected process it reads /proc/<pid>/environ and takes its CLAUDE_CODE_OAUTH_TOKEN and CLAUDE_CODE_SESSION_ID; a process missing either is skipped, and a process whose files cannot be read is skipped. It then loads the project's issues with their Status (reusing the same on-disk issue cache the daemon uses), keeps only the sessions whose --name issue URL maps to an open issue in Status In Tmux by human, and counts the distinct CLAUDE_CODE_SESSION_ID values per token so child processes that inherit one session id count once. It writes one tab-separated line per token to stdout in the form <tokenName>\t<count> (the token name comes from the token list; the raw token value is never printed), and writes the per-run decision trace to stderr. The token-list path comes from --tokenListJsonPath, the claudeCodeOauthTokenListJsonPath config value, or the CLAUDE_CODE_OAUTH_TOKEN_LIST_JSON_PATH environment variable. Because occupancy is read from /proc/<pid>/cmdline and /proc/<pid>/environ, this sub-command is Linux-specific.

The killTmuxSession sub-command cleanly kills a tmux session by running tmux kill-session together with stopping that session's per-session cl-*.scope systemd --user unit, consolidating logic that would otherwise need to be hand-written by callers. It takes exactly one of two mutually exclusive options. --session <name> kills another named session: it first stops the session's cl-*.scope unit (wrapping the systemctl --user stop call with systemctl --user reset-failed both before and after), then runs tmux kill-session -t "=<name>" using the exact-name = prefix so a session name that is itself a prefix of another session's name is never matched by mistake. --self terminates the current session from inside it: because a live session cannot run tmux kill-session on itself without being killed before its own scope-stop step completes, this mode derives the caller's own cl-*.scope unit name from /proc/self/cgroup and stops only that scope, without calling tmux kill-session at all. Neither option may be combined with the other, and providing neither is an error.

The ownerCallFileAppend and ownerCallFileDelete sub-commands maintain one temporary file per tmux session holding the owner calls that session raised and the owner has not answered yet. Both take the tmux session name alone and derive the file location from a single rule — call-to-user/{pjcode or NA}/{session key}.yaml under --inTmuxDataDir, where the session key is the session-name derivation the in-tmux sub-commands already apply to an issue url (every . and : replaced by an underscore) followed by the replacement of every / by an underscore. That derivation gives the same key for a raw issue url and for the tmux session name of it, so the writing side, which knows the session name, and the reading side, which knows the issue url, reach the same file and can never drift apart; it is the same rule the serveWeb route above resolves. Neither sub-command takes a project code: ownerCallFileAppend resolves it from the in-tmux-by-human data in --inTmuxDataDir, which holds one {pjcode}.v4.json per project listing that project's sessions, and the project whose session list holds the session gives the code. NA is the project code of a session no project lists, such as a long-running session that is not tied to one project. ownerCallFileAppend appends one YAML document per call, separated by YAML's own --- document delimiter, creating the file when it does not exist so the oldest call stays first; each document repeats sessionName so a reader can reject a file that does not belong to the session it opened, carries calledAt as UTC ISO-8601 with second precision and a trailing Z, and carries body as a literal block with the explicit indentation indicator 2 so a body whose first line begins with a space is still valid YAML. The body is passed as --body-file rather than as an argument because a call body is multi-line and can be long. ownerCallFileDelete removes the file of that session under whichever project directory holds it, so a session that moved between projects after its call was appended leaves nothing behind, and exits successfully when the file is already absent, so a reset that runs twice is not an error. Neither sub-command writes anything to stdout on success. The scheduled run additionally deletes the file of every session whose issue is closed.

The archive-unresumable-session sub-command moves aside the stored conversation record of a session that ended because its prompt exceeded the model context limit, so the next launch for that task starts a fresh conversation instead of resuming a record that can never load again. Without it, a launcher that resumes the same stored conversation on every attempt keeps terminating on the same condition and the task never starts. It reads the finished run's stream-json log named by --log-file line by line, parsing each line that is a JSON object and ignoring every line that is not. A record matches when all three of type equals result, is_error is true, and result equals Prompt is too long hold; the session whose record is archived is the session_id of that matching record. A session that merely hit a usage limit ends with terminal_reason api_error, api_error_status 429, and a different result, so it does not match and nothing is moved. When no record matches, the sub-command writes no-op to stdout, changes nothing, and exits 0. When a record matches, it creates --archive-dir if it does not exist, moves <--session-dir>/<session-id>.jsonl to <--archive-dir>/<session-id>.jsonl, writes archived <destination path> to stdout, and exits 0. The conversation record is moved, never deleted, so the conversation stays recoverable. When a record matches but that conversation record file is absent, the sub-command writes a diagnostic naming the expected source path to stderr and exits 1. It only changes where the record is stored; it does not change how a session is chosen for a task.

The select-resumable-session sub-command decides whether a dispatched worker resumes an earlier claude session of the same task and agent, and which session id it passes to claude --resume <session id>, so the worker launcher only gathers directory paths and passes the answer to claude. A transcript file is the JSON Lines file <session id>.jsonl in which claude stores one session: a regular file in a session directory whose name ends in .jsonl and does not start with .; a missing session directory holds no transcript files. A transcript file is a candidate only when its first line contains "customTitle":"<--session-name>", closing quote included, so a title that only starts with the name is not a candidate. A candidate is not resumable when <--archive-root>/<session id>.jsonl or <--archive-root>/<directory>/<session id>.jsonl exists, or when its last main-chain assistant entry is an API error whose text is Prompt is too long. A main-chain assistant entry is a JSON line with type assistant and isSidechain not true; its error text is the text values of its message.content items joined together when isApiErrorMessage is true, and empty otherwise; lines that are not JSON objects are ignored. In --session-dir the resumable candidate with the newest modification time is selected, and of candidates with the same modification time the one whose file name comes first in ascending order wins. When --session-dir yields no selection, the transcript files of all --other-session-dir directories are ranked together by modification time: of candidates with the same modification time the one in the directory given first wins, and within one directory the file name that comes first in ascending order wins. A directory equal to --session-dir is left out, and a transcript file whose file name also exists in --session-dir is skipped. The selected transcript file must have an entry time, the first JSON object line with a string timestamp that parses as a date; without one the sub-command writes Session resumption: <session id> has no entry time to compare with the definitions; starting a new session to stderr, copies nothing, and prints nothing. A transcript file selected from another session directory is copied into --session-dir, which is created with mode 700 when absent: the copy keeps the source's mode and its access and modification times, is written as <session id>.jsonl.copying, and is renamed to <session id>.jsonl, and the source stays where it is. After the transcript file is copied, its session id directory <session id>/ next to the source transcript file, which holds the subagent transcripts and tool results of that session, is copied recursively, keeping its times, when it exists and <--session-dir>/<session id> does not. The sub-command then writes Session resumption: copied <source transcript file> into <--session-dir> (original kept) to stderr, preceded by Session resumption: warning: could not copy session directory <source session id directory> into <--session-dir> when only the session id directory copy failed, in which case the session is still resumed. When the transcript file copy fails, or <--session-dir>/<session id>.jsonl already exists, it writes Session resumption: could not copy <source transcript file> into <--session-dir>; starting a new session to stderr and prints nothing. stdout carries the session id to resume followed by a newline, or nothing when the worker starts a new claude session, and the exit code is 0 in every one of these cases; a missing required option exits non-zero.

The check-issue-silent-dispatch-allowed sub-command reports whether the issue named by --issue-url declares, in its own body, that a dispatch cycle ending with no agent comment is expected and must not be treated as a missing report. This lets notifyFinishedIssuePreparation skip the NO_REPORT_FROM_AGENT_BOT rejection for a task whose body already instructs the assigned agent to stay silent during some or all dispatch cycles, a design used for certain recurring, owner-directed tasks. It parses owner, repo, and the issue number from --issue-url, fetches that issue's body from the GitHub REST API using the GH_TOKEN environment variable, and evaluates it against the exact literal marker <!-- TDPM_SILENT_DISPATCH_ALLOWED -->: the marker must appear as a contiguous substring of the body, so the bare token without its <!-- --> wrapper does not count. It writes nothing to stdout. It exits 0 when the marker is present, 1 when it is absent, and 2 with a diagnostic on stderr when --issue-url cannot be parsed or the GitHub API call fails.

The workerRequestTextRead sub-command prints the request text a worker session receives for the issue named by --issueUrl, exactly Take ownership of <issue url> followed by one newline, to stdout, writes nothing to stderr, and exits 0. The worker launcher calls it and passes the printed text to the worker session unchanged, so the text is defined in one place in this CLI, the same CLI that parses it back out of running worker processes to find which issues already have a running worker. It reads nothing and changes nothing. When --issueUrl is missing, it prints nothing to stdout, prints an error naming --issueUrl to stderr, and exits non-zero.

Example 📖

Here's a quick example to illustrate its usage:

npx github-issue-tower-defence-management schedule -t schedule -c ./config.yml
npx github-issue-tower-defence-management schedule -t issue -c ./config.yml -i https://github.com/HiromiShikata/test-repository/issues/1
npx github-issue-tower-defence-management startDaemon --configFilePath ./preparator-config.yml
npx github-issue-tower-defence-management notifyFinishedIssuePreparation --configFilePath ./preparator-config.yml --issueUrl https://github.com/HiromiShikata/test-repository/issues/1
npx github-issue-tower-defence-management checkIssueReviewReadiness --configFilePath ./preparator-config.yml --issueUrl https://github.com/HiromiShikata/test-repository/issues/1
npx github-issue-tower-defence-management serveWeb --configFilePath ./preparator-config.yml --port 9980
TOKEN=$(npx github-issue-tower-defence-management selectOauthToken --tokenListJsonPath ./claudeCodeOauthTokenList.json)
TOKEN=$(npx github-issue-tower-defence-management selectLiveSessionOauthToken --tokenListJsonPath ./claudeCodeOauthTokenList.json)
npx github-issue-tower-defence-management countInTmuxByHumanSessionsPerToken --configFilePath ./preparator-config.yml --tokenListJsonPath ./claudeCodeOauthTokenList.json
REQUEST_TEXT=$(npx github-issue-tower-defence-management workerRequestTextRead --issueUrl https://github.com/HiromiShikata/test-repository/issues/1)

Config

Schedule Command Config

The config.yaml for the schedule command must match the input type of HandleScheduledEventUseCase.run(). Below is the structure:

Workflow status names (Unread, Awaiting Workspace, Awaiting Owner, Preparation, Failed Preparation, Todo by human, In Tmux by human, In Tmux by agent, Done, Icebox) are fixed code constants and cannot be overridden from CLI options, config files, or project README. The schedule command automatically creates any missing required statuses on the target project on each run via SetupTowerDefenceProjectUseCase. Projects with the legacy Todo and In Tmux status names are automatically migrated to Todo by human and In Tmux by human respectively by reusing the existing option IDs so that task associations are preserved. The legacy PC Todo status is removed from the required list and excluded from the project status list on the next setup run. The legacy Awaiting Task Breakdown status is removed from the required list; any project items in that status are automatically moved to Todo by human and the status option is removed on the next setup run. The Awaiting Owner status is a required workflow status.

The two tmux-related statuses have distinct meanings. In Tmux by human means a task being handled in a live tmux session together with the human owner, who attends the session and converses with it; the owner does look at these tasks. In Tmux by agent means a task managed by an agent in tmux that the human owner does not look at. A task launched into a live, owner-attended session therefore belongs to In Tmux by human, and setting such a task to In Tmux by agent would remove it from the owner's view.

disabled: boolean # When true, skip all processing and return null
org: string # Organization name
projectUrl: string # URL of the target project
manager: string # GitHub account name of the manager
workingReport:
  repo: string # Repository name
  members: # Array of member's GitHub account names
    - string
    - string
  warningThresholdHour?: number # Optional: Warning threshold in hours
  spreadsheetUrl: string # URL of the Google Spreadsheet
  reportIssueTemplate?: string # Optional: Template for issue reports
  reportIssueLabels: # Array of issue labels
    - string
    - string
startPreparation?: # Optional: Enable automatic issue preparation workflow
  defaultAgentName: string # Default agent name to assign for preparation
  configFilePath: string # Path to config file passed to the aw command
  defaultLlmModelName?: string | null # Optional: Default LLM model name (overridable via llm-model: label)
  fallbackLlmModelName?: string | null # Optional: LLM model used when defaultLlmModelName is not specified (default: claude-opus-4-8). Per-issue llm-model: labels remain authoritative and are never overridden by the fallback
  defaultLlmAgentName?: string | null # Optional: Default LLM agent name (overridable via llm-agent: label)
  maximumPreparingIssuesCount: number | null # Max concurrent preparing issues. When token rotation is active, effective concurrency is also capped at 6 per available token. When null, the default is 6 per available token, or 6 without token rotation
  utilizationPercentageThreshold?: number # Optional: 5-hour utilization hard threshold (percentage, default 90). Tokens at or above this value are excluded from rotation
  allowedIssueAuthors?: string[] | null # Optional: Only start preparation for issues from these authors (null or omitted = no authors allowed; set an explicit list)
  preparationProcessCheckCommand?: string # Optional: Shell command template with {URL} placeholder to check if a preparation process is alive. When set, orphaned Preparation issues (process exits non-zero, or stale aw log) are evaluated for completion: if work is done they advance to Awaiting Owner; otherwise they fall back to Awaiting Workspace.
  awaitingOwnerStatus?: string | null # Optional: Project status name for issues awaiting owner review. When set with preparationProcessCheckCommand, orphaned issues with no rejections advance to this status instead of awaitingWorkspaceStatus
  autoAdvanceQualityCheckEnabled?: boolean # Optional: When true, issues in the Awaiting Owner status are automatically advanced to Done on each scheduled cycle. Default false (issues remain in Awaiting Owner for human review). Use awaitingOwnerStatus to configure the status name when it differs from the default
  autoRevertReopenedDoneEnabled?: boolean # Optional: When true, issues in the Done status that were reopened on GitHub (stateReason = REOPENED) are automatically moved back to Awaiting Workspace on each scheduled cycle. Default false. Requires startPreparation to be configured.
  codexHomeCandidates?: string[] | null # Optional: Ordered list of CODEX_HOME directory paths. Each launched Codex job cycles through the list; absent or empty keeps current behavior
  awLogDirectoryPath?: string # Optional: Directory path where aw log files named {org}_{repo}_{number}_* are written. Used with awLogStaleThresholdMinutes to detect zombie-wrapper orphans, and by startPreparation when an issue has more than one open same-repository PR: the PR whose head branch is checked out in the working directory recorded on the `Current directory:` line of the issue's newest {org}_{repo}_{number}_{YYYYMMDD}_{HHMMSS}.log is adopted and every other PR is closed. When this is unset, or that branch is the head branch of no open PR, the PR created first is adopted
  awLogStaleThresholdMinutes?: number # Optional: Minutes since last aw log mtime after which a Preparation issue is considered orphaned even when pgrep still returns 0 (outer wrapper alive but inner claude dead). Requires awLogDirectoryPath
  labelsAsLlmAgentName?: string[] | null # Optional: List of issue labels that are themselves agent names. When an issue carries any label that is included in this list, that label name is used as the agent name. Selection precedence is: (1) explicit `llm-agent:` label, (2) labelsAsLlmAgentName entry match, (3) `category:` label, (4) defaultLlmAgentName, (5) defaultAgentName
developerAgentNames?: string[] # Optional: The agent names treated as "developer" agents for PR link, CI, and conflict checks in both the notifyFinishedIssuePreparation and scheduled revert flows. When omitted or empty, no issue is treated as a developer agent issue (no silent fallback to `developer`). Set this when your project uses one or more agent names for coding tasks and you want those agents' issues to undergo PR/CI/conflict validation. Readable from the project README config section; readme value takes precedence over the config file value. Declared at the top level (sibling of startPreparation), not inside it. Also accepts the legacy single