github-issue-tower-defence-management
v1.157.0
Published
[](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
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
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
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)
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 a token falls back to when the default Sonnet model's 7-day weekly limit is exhausted for that token while its fallback weekly window still has capacity; routing is decided per token, so tokens with Sonnet headroom keep using Sonnet in the same pass (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
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)
--workflowBlockerResolvedWebhookUrl <url> Webhook URL to notify when a workflow blocker issue status changes
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 every required file is present the server composes the /tdpm.txt fragment from them, 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
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); 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 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 fromThe 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|todo-by-agent}) 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 every required data file is present (machine-status.json, token-status.json, and a projects/<projectName>.json for every name in --dashboardProjectNames), the server composes the fragment at request time from those files emitted by the scheduled run — one projects/<projectName>.json per project, keyed by the full project name (the same key the scheduled run writes; that project's actionable status counts), machine-status.json (mem%, cpu%, disk%, loadavg, cycle-minutes), and token-status.json (per-token rate-limit utilization, reset countdown, status color, and prep/hum counts). It renders the fixed-width fragment — the two host-metrics lines (line 1 M{mem}% C{cpu}% D{disk}% cy{cycle}, line 2 LA {load1} {load5} {load15}), the project grid (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 . Otherwise — when --dashboardDataDir is unset, or it is set but any required data file is missing — 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|todo-by-agent}/list.jsonand the matchingdetail/<key>.jsonfiles are read from{consoleDataOutputDir}/{pjcode}/{tab}/, andGET /projects/{pjcode}/in-tmux-by-human/*is read from{consoleDataOutputDir}/{pjcode}/in-tmux-by-human/. Each served list has the.donecross-tab exclusion applied. The schedule cycle writes the full tab files;notifyFinishedIssuePreparationadditionally patches the affected tab files immediately after each status transition. - Flat in-tmux-by-human static files (GET):
GET /in-tmux-by-human/<file>.jsonserves the flat static JSON files (for exampleindex.v4.jsonand{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}.yamlserves the owner call file of exactly one session byte-for-byte from--inTmuxDataDir, astext/yaml, andDELETEon 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;DELETEis 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, andGET /api/issuetitle. Each takes aurlquery parameter.GET /api/issuetitlereturns{ state, merged, isPullRequest, title }, wheretitlecomes 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_1day,snooze_1week), andPOST /api/intmux(set_intmux). These routes are multi-project: every request body carries thepjcodeof the project the UI is currently viewing (taken from the UI's own/projects/{pjcode}URL path), the server resolves thatpjcodeto its GitHub Project URL through thepjcode → projectUrlmapping described below, loads that project's status and story options (lazily, cached perpjcode), and applies the operation against the resolved project. A request with nopjcode, or apjcodethat has no configured project URL, is rejected with HTTP 400. Each confirmed operation records the affectedprojectItemIdinto the.doneexclusion under the resolvedpjcodeso it disappears from every tab's served list for that project only. One runningserveWebinstance therefore serves both reads and writes for every configured project. - Comment APIs (POST, JSON body, backed by the server-side
GH_TOKEN):POST /api/commentposts a top-level comment on the issue or pull request named by the request body'surl.POST /api/reviewcommentposts a line-anchored inline review comment on a pull request diff; its request body carriesurl(the pull request URL),path(the changed file path),line(a positive integer diff line number),side(RIGHTfor added or context lines on the new side,LEFTfor removed lines on the old side), andbody. The server resolves owner, repo, and pull request number fromurl, fetches the pull request head commit sha, and calls GitHubPOST /repos/{owner}/{repo}/pulls/{number}/commentswithcommit_idset to that sha. When GitHub rejects the request, the response is HTTP 502 with the GitHub error message in theerrorfield 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.
The optional storyProgressCommentEnabled config key controls the daily story progress comment. Once per day the schedule cycle posts a comment containing a mermaid flowchart TD of the story and its child issues onto every story issue. Setting the key to false stops that comment being posted for the project; the key defaults to true, so omitting it leaves the existing behaviour unchanged.
storyProgressCommentEnabled: falseThe 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, REPORT_HAS_NEXT_STEP, 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 25% free (5-hour utilization at most 0.75) AND whose 7-day window is at least 3% free (7-day utilization at most 0.97), 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 the identical rate-limit eligibility filter as selectOauthToken (5-hour window at least 25% free AND 7-day window at least 3% 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). 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 CLAUDE_CODE_SESSION_ID. A token's occupancy is the number of distinct CLAUDE_CODE_SESSION_ID values bound to it, so child processes that inherit one session id count once. 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, never dropping under 1. 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. Both tuning numbers 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 soonest-resetting one is returned rather than none. A token that is the only eligible candidate is always chosen regardless of its weight or occupancy. 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 and cache directory are resolved exactly as for selectOauthToken. Because occupancy is read from /proc/<pid>/environ, this sub-command is Linux-specific.
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.
Example 📖
Here's a quick example to illustrate its usage:
npx github-issue-tower-defence-management schedule -t schedule -c ./config.ymlnpx github-issue-tower-defence-management schedule -t issue -c ./config.yml -i https://github.com/HiromiShikata/test-repository/issues/1npx github-issue-tower-defence-management startDaemon --configFilePath ./preparator-config.ymlnpx github-issue-tower-defence-management notifyFinishedIssuePreparation --configFilePath ./preparator-config.yml --issueUrl https://github.com/HiromiShikata/test-repository/issues/1npx github-issue-tower-defence-management checkIssueReviewReadiness --configFilePath ./preparator-config.yml --issueUrl https://github.com/HiromiShikata/test-repository/issues/1npx github-issue-tower-defence-management serveWeb --configFilePath ./preparator-config.yml --port 9980TOKEN=$(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.jsonConfig
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, Preparation, Failed Preparation, Awaiting Quality Check, Todo by human, Todo by agent, 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 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 a token falls back to when defaultLlmModelName is a Sonnet model and that token's 7-day Sonnet weekly limit is exhausted while its fallback weekly window still has capacity (default: claude-opus-4-8). Routing is decided per token, so tokens with Sonnet headroom keep using Sonnet in the same pass. 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 = all authors)
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 Quality Check; otherwise they fall back to Awaiting Workspace
awaitingQualityCheckStatus?: string | null # Optional: Project status name for issues awaiting quality check. When set with preparationProcessCheckCommand, orphaned issues with no rejections advance to this status instead of awaitingWorkspaceStatus
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
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
labelsNotRequiringPullRequest?: string[] | null # Optional: List of issue labels whose issues are exempt from the pull-request part of the review-readiness check, without that label also being used as an agent name. An issue carrying any of these labels is never rejected for a missing, draft, conflicted, or failing pull request, and its agent is still resolved by the normal precedence (explicit `llm-agent:` label, labelsAsLlmAgentName entry match, `category:` label, defaultLlmAgentName, defaultAgentName). Every label in labelsAsLlmAgentName is exempt as well, so an existing config that relied on that key keeps its behaviour. Use this key for a label such as `story` whose issues produce child issues rather than a pull request and for which no agent of the same name is installed. Declared at the top level (sibling of startPreparation), not inside it.
changeTargetPathAliases?: Record<string, string> | null # Optional: Map of short alias keys to full repository-root-relative directory paths, for use with `change-target:<alias>` labels. Allows deeply nested paths that exceed GitHub's 50-character label limit to be referenced via a short alias. Example: `{ "adapters": "src/domain/usecases/adapter-interfaces" }` — a label `change-target:adapters` then matches files under `src/domain/usecases/adapter-interfaces/`. Keys not matching any `change-target:` label value are ignored; values with leading or trailing slashes are normalized automatically. Declared at the top level (sibling of startPreparation), not inside it.
claudeCodeOauthTokenListJsonPath?: string # Optional: Path to a JSON file listing long-term Claude Code OAuth tokens to rotate across (see "Claude OAuth Token Rotation" below). Declared at the top level (sibling of startPreparation), not inside it.
autoAssignManagerAuthors?: string | string[] | null # Optional: Restrict which authors' open, unassigned issues and pull requests are auto-assigned to the manager. Accepts a comma-separated string or an array of GitHub author logins (bot logins carry the `[bot]` suffix, for example `renovate[bot]`). When set to a non-empty list, only items whose author is in the list are auto-assigned; when omitted, null, or empty, every open unassigned item is auto-assigned (current behavior). Author matching is exact equality. Declared at the top level (sibling of startPreparation), not inside it.
queryToAddProjectEnabled?: boolean # Optional: Enable the queryToAddProject search (default false). The search runs only when this is true, so setting queryToAddProject alone changes nothing. Declared at the top level (sibling of startPreparation), not inside it.
queryToAddProject?: string | null # Optional: GitHub search query string, used only when queryToAddProjectEnabled is true. Each schedule cycle, issues and pull requests matching this query that are not already on the project board are added to the project and, when their author satisfies autoAssignManagerAuthors, assigned to the manager. Omit or set to null to skip this search. Declared at the top level (sibling of startPreparation), not inside it.
dailySecurityScan?: # Optional: Run a daily OSV-Scanner security scan across locally cloned repositories of the org. Declared at the top level (sibling of startPreparation). When the findings of one repository cannot be written to GitHub — for example because that repository is archived and therefore read-only, or because the rendered findings exceed what the issues API accepts — the failure is logged with the repository name and the cause, that repository is skipped, and the remaining repositories are still scanned and the CISA KEV report still runs.
scanBaseDirectory: string # Base directory searched (4 levels deep) for cloned repositories to scan
targetHourUtc: number # UTC hour (0-23) at which the scan runs once per day; the scan runs only when a target date matches this hour at minute zero
enableKevNvdReport?: boolean # Optional: When true, also report CISA KEV catalog additions that have not been reported yet. A watermark recording the newest reported dateAdded, plus the CVE identifiers already reported on that date, is persisted at $XDG_CACHE_HOME/tdpm/kev-report-watermark.json (falling back to ~/.cache when XDG_CACHE_HOME is unset) and advanced over every addition the run considered, whether or not that addition matched a package the scan actually found and therefore produced a report issue. Only an addition whose CVE identifier matches a package the scan found at an installed version is written into the report issue. On the first run, when no watermark is stored yet, the additions dated on or after the day before the run are considered. Known limitations: (1) if the watermark write fails after the report issue was created, the failure is logged with the state file path and the cause, the run continues so that unrelated scheduled work is not aborted, and the same additions are considered again on the next run, so the guarantee is at-least-once at that boundary rather than exactly-once; (2) an addition whose dateAdded is strictly earlier than the stored watermark date, whether back-dated or published very late, is never selected again, so the protection covers late additions on the watermark date itself and not arbitrary lateness. When the stored watermark file exists but cannot be read or is malformed (including a stored date that is not a real calendar date in YYYY-MM-DD form), the KEV report is skipped for that run, an error naming the file and the cause is logged, the file is left untouched for inspection, and the rest of the scheduled work continues
kevReportRepo?: string # Optional: Repository name (within org) where the CISA KEV additions issue is created. Required for KEV reportingExample:
disabled: false
org: 'my-org'
projectUrl: 'https://github.com/orgs/my-org/projects/1'
manager: 'HiromiShikata'
workingReport:
repo: 'work-report'
members:
- 'HiromiShikata'
- 'octokit'
warningThresholdHour: 40
spreadsheetUrl: 'https://docs.google.com/spreadsheets/d/xxx'
reportIssueTemplate: |
## Working Time Report
### Summary
Period: {period}
Team: {team}
### Details
{details}
reportIssueLabels:
- 'report'
- 'working-time'
slack:
userToken: 'xoxp-xxx'
startPreparation:
defaultAgentName: 'aw'
configFilePath: '/path/to/agent-config.yml'
defaultLlmModelName: 'claude-opus-4-5'
maximumPreparingIssuesCount: 3
utilizationPercentageThreshold: 90
preparationProcessCheckCommand: 'pgrep -fa "claude-agent.*{URL}"'
awaitingQualityCheckStatus: 'Awaiting Quality Check'
awLogDirectoryPath: '/home/user/logs-aw'
awLogStaleThresholdMinutes: 15
labelsAsLlmAgentName:
- story
- story:body-condition
dailySecurityScan:
scanBaseDirectory: '/home/user/repos'
targetHourUtc: 0
enableKevNvdReport: true
kevReportRepo: 'security-reports'startDaemon and notifyFinishedIssuePreparation Commands Config
The config YAML for startDaemon and notifyFinishedIssuePreparation commands:
projectUrl: string # URL of the GitHub project
projectName: string # Project name (used for cache directory path)
defaultAgentName: string # Default agent name for issue preparation
defaultLlmModelName?: string # Optional: Default LLM model name
fallbackLlmModelName?: string # Optional: LLM model a token falls back to when defaultLlmModelName is a Sonnet model and that token's 7-day Sonnet weekly limit is exhausted while its fallback weekly window still has capacity (default: claude-opus-4-8). Routing is decided per token, so tokens with Sonnet headroom keep using Sonnet in the same pass. Per-issue llm-model: labels remain authoritative
defaultLlmAgentName?: string # Optional: Default LLM agent name
maximumPreparingIssuesCount?: number # Optional: Max concurrent preparing issues. When token rotation is active, effective concurrency is also capped at 6 per available token. Omitted defaults to 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 # Optional: Comma-separated list of allowed issue authors
autoAssignManagerAuthors?: string # Optional: Comma-separated list of author logins whose open, unassigned issues and pull requests are auto-assigned to the manager. When set to a non-empty list, only items whose author is in the list are auto-assigned; when omitted or empty, every open unassigned item is auto-assigned (current behavior). Author matching is exact equality; bot logins carry the `[bot]` suffix (for example `renovate[bot]`)
queryToAddProjectEnabled?: boolean # Optional: Enable the queryToAddProject search (default false). The search runs only when this is true, so setting queryToAddProject alone changes nothing
queryToAddProject?: string # Optional: GitHub search query string, used only when queryToAddProjectEnabled is true. Each schedule cycle, issues and pull requests matching this query that are not already on the project board are added to the project and, when their author satisfies autoAssignManagerAuthors, assigned to the manager. Omit to skip this search.
thresholdForAutoReject?: number # Optional: Consecutive rejections before escalation (default: 3)
workflowBlockerResolvedWebhookUrl?: string # Optional: Webhook URL. Supports {URL} and {MESSAGE} placeholders
preparationProcessCheckCommand?: string # Optional: Shell command template with {URL} placeholder to check if a preparation process is alive. Orphaned Preparation issues (process exits non-zero, or stale aw log) are evaluated for completion: if work is done they advance to Awaiting Quality Check; otherwise an `Auto Status Check: REJECTED` comment is posted and the issue falls back to Awaiting Workspace, except that once `thresholdForAutoReject` cumulative orphan-time rejections accumulate within the recent comment window (and no earlier escalation marker is present) the issue is transitioned to Failed Preparation instead
codexHomeCandidates?: string[] # Optional: Ordered list of CODEX_HOME directory paths for Codex profile cycling. Absent or empty keeps current behavior
claudeCodeOauthTokenListJsonPath?: string # Optional: Path to a JSON file listing long-term Claude Code OAuth tokens to rotate across (see "Claude OAuth Token Rotation" below)
awLogDirectoryPath?: string # Optional: Directory path where aw log files named {org}_{repo}_{number}_* are written. Used with awLogStaleThresholdMinutes to detect zombie-wrapper orphans
awLogStaleThresholdMinutes?: number # Optional: Minutes since last aw log mtime after which a Preparation issue is considered orphaned even when pgrep still returns 0. Requires awLogDirectoryPath
labelsAsLlmAgentName?: string[] # 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
consoleAccessToken?: string # Optional: Access token for the Console HTTP server. When set, `startDaemon` TCP-probes port 9980 before the first preparation cycle and, if nothing responds, spawns `serveWeb` as a detached background process on that port. SIGTERM and SIGINT sent to the daemon are forwarded to the console server child before the daemon exits. When unset, no console server is started automatically.
consoleDataOutputDir?: string # Optional: Base output directory for the per-project Console list.json files. When set, the schedule cycle writes the full tab files and notifyFinishedIssuePreparation immediately patches the affected tab files after each status transition (no additional GitHub fetch). When unset, Console list generation and patching are skipped
workflowBlockerStoryName?: string # Optional: Story field name that the Console "workflow-blocker" tab matches (case-insensitive). Every non-closed issue with this story is listed regardless of status or reactivation-trigger fields. When unset, the workflow-blocker list is always empty
inTmuxDataOutputDir?: string # Optional: Base output directory for the in-tmux-by-human per-project and index JSON files written each schedule cycle. When unset, in-tmux-by-human generation is skipped
inTmuxConsoleBaseUrl?: string # Optional: Console base URL used to build the tdpmConsoleUrl in the v3/v4 in-tmux-by-human files (for example https://console.example.com). When unset, the v3 and v4 files are skipped
inTmuxConsoleToken?: string # Optional: Token embedded in the ?k= query string of the v4 in-tmux-by-human files. When unset, the v4 per-project file and index.v4.json are skipped
inTmuxProjectOrder?: string[] # Optional: Ordered list of project codes used to build the in-tmux-by-human index files. When unset or empty, the index files are skipped
newIssueRepo?: string # Optional: Repository name used only for the repo segment of the v4 in-tmux-by-human newIssueUrl. When unset, the newIssueUrl repo segment falls back to workingReport.repo, preserving the existing behavior. Only the newIssueUrl is affected; workingReport.repo and every other use of it are unchanged
inTmuxLauncherCommand?: string # Optional: Launcher command that starts an interactive session for an `In Tmux by human` issue. Each schedule cycle, any open assigned `In Tmux by human` issue without a live session is restarted by running this command with the issue URL as its argument inside a new detached tmux session named after the issue URL. When unset, session restarting is skipped
tokenExhaustionHandoverEnabled?: boolean # Optional: When true, the daemon detects Claude sessions whose OAuth token is near/at the rate limit (five-hour free < 0.10, seven-day free < 0.05 for tmux leaders, blocked/rejected, blockedUntilEpoch in the future, or a rejected weekly hard-cap whose resetsAt is still in the future) and, only when a DIFFERENT token is confirmed not-stale-not-exhausted, performs the handover: a checkpoint instruction is sent to tmux leaders via `tmux send-keys` (impl `-p` subagents receive SIGTERM instead) and the session is killed after the grace period. Issue-URL leaders are relaunched by the preparation daemon; bare-name resident leaders are relaunched by this step via `cl <name>` on a fresher token; impl subagents are left for the daemon to re-dispatch. Requires `claudeCodeOauthTokenListJsonPath`. When false, detection and logging still run but no send-keys, kill, or relaunch is performed (dry-run). Default false
tokenExhaustionHandoverMessage?: string # Optional: Checkpoint instruction sent to an exhausted issue-URL leader via `tmux send-keys`. When unset, a built-in default that tells the session to checkpoint to its task issue and then self-kill is used
tokenExhaustionHandoverBareNameLeaderMessage?: string # Optional: Checkpoint instruction sent to an exhausted bare-name resident leader (a leader started with a bare `--name` and no task issue) via `tmux send-keys`. When unset, a built-in default that tells the leader to checkpoint into each in-flight subagent's own issue and NOT self-kill (this step relaunches it) is used
tokenRateLimitSnapshotBaseDir?: string # Optional: Directory path for the per-token rate-limit snapshot JSON files read by the token exhaustion handover step. When unset, defaults to ${XDG_CACHE_HOME:-~/.cache}/tdpm/ratelimit
tokenExhaustionGracePeriodSeconds?: number # Optional: Seconds to wait after sending the handover message before killing the tmux session, giving the session time to shut down gracefully. Default 180
tokenExhaustionHandoverStateFilePath?: string # Optional: File path for the durable handover grace/debounce state (which sessions were signaled and when), written atomically so grace tracking survives daemon restarts. When unset, defaults to ${XDG_CACHE_HOME:-~/.cache}/tdpm/token-exhaustion-handover-state-tdpm-native.json (a TDPM-native filename kept distinct from any standalone monitor's state file so the two never share state)
silentNotificationEnabled?: boolean # Optional: Master switch for the silent live session self-check notification. The step is a no-op unless this is `true` (or the `TDPM_SILENT_NOTIFICATION_ENABLED` environment variable equals the string `true`). This gate prevents the step from acting automatically when the daemon is run with a default configuration; it must be explicitly turned on. Default false
subAgentOutputRootDirectory?: string # Optional: Root directory holding one output file per sub-process (file name derived from the sub-process label). The modification time of each file is read to compute how long the sub-process output has been idle. When unset, sub-process idle time is reported as 0 and only the running-time threshold can trigger a sub-process notification
subAgentProcessMatchPattern?: string # Optional: Regular expression matched against each process command line to discover the sub-processes that belong to a monitored session. The expression must define a named capture group `session` whose value equals the monitored session name, and may define a named capture group `label` used as the display name. When unset, the sub-process check is skipped
ownerCallMarker?: string # Optional: Marker substring that identifies an assistant message asking the owner for a decision or confirmation. When set, the main-session stalled section is suppressed while a session's latest marker-bearing assistant message is newer (by full timestamp) than its latest genuine owner reply. A marker ending with `>` also matches its candidate form, produced by replacing that trailing `>` with `-pending>`, so a configured `<call-to-user>` matches both `<call-to-user>` and `<call-to-user-pending>`; any marker not ending with `>` matches only itself. When unset, no session is treated as waiting on the owner
subAgentTranscriptRootDirectory?: string # Optional: Projects root directory (for example `~/.claude/projects`) under which each session's transcript is stored as `<cwd-slug>/<sessionId>.jsonl` and its sub-agent transcripts live in the sibling `<cwd-slug>/<sessionId>/subagents/` directory as one `agent-<id>.jsonl` file per sub-agent. Because the on-disk directory is keyed by the session's working-directory slug and session id rather than by the tmux session name, the sub-agent directory is derived from the already-resolved main-session transcript path (the `.jsonl` suffix is stripped and `subagents` is appended). A sub-agent is treated as finished when the last meaningful entry of its transcript is a completion or termination marker: an assistant message whose `stop_reason` is `end_turn` or `stop_sequence`, an assistant message whose final content block is `text` (a final answer returned to the parent, which Claude Code often writes with a `null` `stop_reason`), a user message whose final content block is `text` (a terminal user entry such as `[Request interrupted by user]`), a user message whose final content block is the `tool_result` of a `StructuredOutput` tool call (the structured output the sub-agent delivered as its final answer), or a user message with no content blocks. A finished sub-agent is skipped only when it is no longer listed in the harness running-sub-agent set (`subAgentRuntimeRootDirectory`), or when that set cannot be read at all; a finished sub-agent the running set still lists is kept and reported as an unconsumed result, because the leader clears its record line once it has acted on the completion. Only a sub-agent whose last entry is an assistant message with a pending `tool_use` block and no following tool result is still considered active; its idle time is computed from the transcript file modification time (following symlinks) and the running time from its first entry timestamp. An active sub-agent whose pending tool command matches a live host process is classified as waiting on that external process and is exempt from the idle threshold (a genuinely hung sub-agent whose matching process is absent is still flagged), and the running threshold announces once per crossing per label. When set, this transcript-based discovery is used instead of `subAgentProcessMatchPattern`. When unset, the process-match discovery is used
subAgentRuntimeRootDirectory?: string # Optional: Runtime root directory (for example `/tmp/claude-<uid>`) under which each session's positive running-sub-agent set is expected at `<cwd-slug>/<sessionId>/running-subagents.txt`. Each non-blank line has the form `<agentId> <label-or-branch-or-URL>`; only the first whitespace-delimited token of the line is taken as the sub-agent id, and any remaining text on the line (a label, branch name, or pull request URL) is ignored. Blank and whitespace-only lines are skipped. When transcript-based discovery is used, this enables a positive process-liveness gate: a sub-agent whose id is absent from the running set has already exited, so its stale on-disk transcript is not flagged even when its tail is a plain in-flight `tool_result`. Membership in the running set is also what distinguishes a consumed completion from an unconsumed one: a finished sub-agent the set still lists is reported as an unconsumed result and drives the unconsumed-result reminder section, while one absent from the set is dropped. When this directory is unset it defaults to `<os-tmpdir>/claude-<uid>` (or is disabled on platforms without a numeric user id); when the directory or the running-set file cannot be read the gate is skipped (fail-open) and the terminal-status and completion-marker exclusions remain the sole guards
mainSilentThresholdSeconds?: number # Optional: Seconds of main-session output silence after which the main-session self-check section is sent. Default 600
unansweredOwnerCallGraceSeconds?: number # Optional: Retained only for backward compatibility of the configuration surface and no longer consulted. An unanswered owner call now suppresses the main-session stalled section unconditionally, with no age or grace expiry, so no value of this key changes any behaviour. Default 3600
subAgentSilentThresholdSeconds?: number # Optional: Seconds of sub-process output silence after which the sub-process section is sent. Default 300
subAgentRunningThresholdSeconds?: number # Optional: Seconds a sub-process may run before the sub-process section is sent regardless of its output silence. Default 900
silentNotificationStaggerSeconds?: number # Optional: Seconds to wait between consecutive session notifications within one cycle, so the targets are notified sequentially rather than all at once. Default 25
candidateDebounceRecencyWindowSeconds?: number # Optional: Recency window in seconds within which a session must have been recorded as a notification candidate in a previous cycle for the two-consecutive-cycle debounce to allow a notification this cycle. A small multiple of the schedule interval to tolerate interval jitter. Default 900
candidateDebounceStateFilePath?: string # Optional: Path to the JSON state file that persists the per-cycle candidate session set used by the two-consecutive-cycle debounce. When unset, defaults to ${XDG_CACHE_HOME:-~/.cache}/tdpm/silent-session-candidates.json
activeHubTaskStatus?: string # Optional: GitHub Project Status value that marks a session's hub task as still actively worked in an interactive session. For a session whose name is a `https://github.com/{owner}/{repo}/issues/{N}` issue URL, the hub task's latest state and Status are resolved before sending and the notification is suppressed when the issue is closed or merged, or when its Status differs from this value. Each resolved status is cached by issue URL (see hubTaskStatusCacheStateFilePath and hubTaskStatusCacheTtlSeconds) so a fresh cached entry decides the gate without a new GitHub query. When the hub task cannot be resolved (the resolver returns no tracked task, or a transient API error is thrown), a cached status is used as a fallback even after it expires: a cached active entry keeps the notification (the session is still notified if genuinely stalled) and a cached closed or non-active entry suppresses it. Only when there is no cached status at all and live resolution fails does the gate fail open once, logging a distinct warning. Sessions whose name is not a github.com issue URL are never checked. When unset, this hub-task active-status check is a no-op and existing behavior is preserved
hubTaskStatusCacheStateFilePath?: string # Optional: Path to the JSON state file that caches each hub task's resolved state and Status keyed by issue URL for the hub-task active-status gate. When unset, defaults to ${XDG_CACHE_HOME:-~/.cache}/tdpm/silent-session-hub-task-status.json
hubTaskStatusCacheTtlSeconds?: number # Optional: Seconds a cached hub-task status is considered fresh enough to decide the gate without re-querying GitHub. A cached entry older than this is re-resolved, but is still used as a fallback when re-resolution fails. Default 300
silentMainStalledStaleOwnerCallMessage?: string # Optional: Overrides the stale-owner-call reminder body. That reminder variant is currently never composed, because an unanswered owner call suppresses the main-session stalled section unconditionally, so this key has no effect. The owner-call format guidance is appended. When unset, a built-in message directing the agent to re-raise its ask self-containedly or continue autonomously is used
silentSubAgentIdleMessageHeader?: string # Optional: Overrides the line shown above the sub-process list in the idle (no-output) sub-process message. When unset, a generic built-in line is used
silentSubAgentIdleMessageFooter?: string # Optional: Overrides the line shown below the sub-process list in the idle (no-output) sub-process message. When unset, a generic built-in line is used
silentSubAgentLongRunningMessageHeader?: string # Optional: Overrides the line shown above the sub-process list in the long-running sub-process message. When unset, a generic built-in line is used
silentSubAgentLongRunningMessageFooter?: string # Optional: Overrides the line shown below the sub-process list in the long-running sub-process message. When unset, a generic built-in line is used
outputDegenerationResetEnabled?: boolean # Optional: Master switch for the output-degeneration reset step. When true, the daemon inspects every live interactive Claude Code session named after a github.com issue or pull-request URL and, when the session's most recent assistant turn is a collapsed sin