-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathcoding-agent.config.example
More file actions
584 lines (521 loc) · 32.5 KB
/
Copy pathcoding-agent.config.example
File metadata and controls
584 lines (521 loc) · 32.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
# coding-agent workflow configuration
# Copy to coding-agent.config and tweak. All scripts + systemd units source this file.
# Required fields use ${VAR:?} in scripts for fail-fast (never silently skipped).
#
# 复制为 coding-agent.config 并按需修改。所有脚本 + systemd 单元都会 source 这个文件。
# 必填项已用 ${VAR:?} 在脚本里 fail-fast,不会被 silently 跳过。
# ── GitHub repo / GitHub 仓库 ──
# Required. Format: owner/repo
# 必填。格式:owner/repo
REPO="myorg/myrepo"
# ── Paths / 路径 ──
# Required. Host project worktree root (git repo root).
# 必填。host project 工作树根(git 仓库根目录)
PROJECT_ROOT="$HOME/github/myproject"
# Worktree storage base (one subdir per issue).
# worktree 存放基础目录(每个 issue 一个子目录)
WORKTREE_BASE="$HOME/github/worktree/myproject"
# Daemon state dir (state.json + logs + lock).
# daemon state 目录(state.json + 日志 + 锁)
STATE_DIR="$HOME/.local/state/coding-agent-poll"
# 日志级别:info(默认);debug 额外显示优先级读取、greedy 跳过与 review 轮次用尽明细。
# Log level: info (default), or debug for routine priority and skipped-work details.
LOG_LEVEL="info"
# Tmux pane log dir (one append-only log per worker session, survives session exit).
# Default: $STATE_DIR/sessions. Set empty (SESSION_LOG_DIR="") to disable.
# View: scripts/session-log.sh <issue-N> / -f follow / -c cat
#
# tmux pane 日志目录(每个 worker session 一份 append-only 日志,session 退出后仍可回看)
# 默认:$STATE_DIR/sessions。显式置空 (SESSION_LOG_DIR="") 关闭该功能。
# 查日志:scripts/session-log.sh <issue-N> / -f 跟随 / -c cat
# SESSION_LOG_DIR="$STATE_DIR/sessions"
# ── Naming / 命名规范 ──
# Tmux session name prefix: tmux session = ${TMUX_PREFIX}-issue<N>
# E.g. myproject → tmux session "myproject-issue42"
#
# tmux session 命名前缀:tmux session = ${TMUX_PREFIX}-issue<N>
# 例 myproject → tmux session "myproject-issue42"
TMUX_PREFIX="myproject"
# Branch naming: ${BRANCH_PREFIX}<N>
# E.g. "feature/issue-" → "feature/issue-42"
#
# Branch 命名:${BRANCH_PREFIX}<N>
# 例 "feature/issue-" → "feature/issue-42"
BRANCH_PREFIX="feature/issue-"
# Claude Code session name (CLI -n flag): ${SESSION_NAME_PREFIX}<N>
# E.g. "issue" → Claude session shown as "issue42"
#
# Claude Code session 名(CLI -n flag):${SESSION_NAME_PREFIX}<N>
# 例 "issue" → claude session shown as "issue42"
SESSION_NAME_PREFIX="issue"
# ── GitHub labels / GitHub label ──
# Five states:
# pending/agent → set by you; agent will pick it up
# pending/agent/fable → same trigger, but use Claude model claude-fable-5
# doing/agent → set by daemon; dispatching / worker running
# pending/human → set by worker; awaits your review / decision
# pending/PR → set by worker on issue when opening a PR; work moved to PR
# Done → set by daemon's auto-cleanup; merge done = truly closed
# Make sure the labels exist before changing names (setup.sh creates them).
#
# 五态:
# pending/agent → 用户标,等 agent pick up
# pending/agent/fable → 同样触发派工,但改用 Claude 模型 claude-fable-5
# doing/agent → daemon 标,正在 dispatching / worker 跑着
# pending/human → worker 标,issue/PR 等你 review 决策
# pending/PR → worker 标 issue(开 PR 时),表示「工作转 PR 跟踪」
# Done → daemon auto-cleanup 标,merge 完成 = 真彻底结束
# 改下面值前确保仓库已建对应 label(setup.sh 会自动建)
LABEL_PENDING_AGENT="pending/agent"
LABEL_PENDING_AGENT_FABLE="pending/agent/fable"
# This label overrides only the current dispatch; the project's default worker stays unchanged.
FABLE_WORKER_AGENT="claude"
FABLE_MODEL="claude-fable-5"
# Extra trigger labels (comma-separated, default empty). Identical semantics to
# LABEL_PENDING_AGENT — same prompt, same worker, same model — they are just extra
# names that also trigger a dispatch. NOT a model selector (that is the fable label).
#
# The reason it exists: splitting work across machines that watch the SAME repo.
# Give each host its own label so neither steals the other's items:
# on host 05: LABEL_PENDING_AGENT="pending/agent" LABEL_PENDING_AGENT_EXTRA="pending/agent/05"
# on host 01: LABEL_PENDING_AGENT="pending/agent/01" LABEL_PENDING_AGENT_EXTRA=""
# Multi-host self-heal ownership. Turn BOTH of these on whenever two machines poll
# the same repo — the labels above are not enough on their own.
#
# Why: self-heal decides "this worker is dead" from "the item is labelled
# doing/agent but this machine has no tmux session by that name". Both machines
# name sessions identically, so machine A looking at machine B's running worker
# sees exactly that, and takes the item over. The same blindness also inflates the
# concurrency count: A counts B's doing/agent items against its own max and then
# never dispatches anything, while the log looks perfectly healthy.
# Measured 2026-09-04: a freshly configured second host hijacked four live workers
# within 25 seconds of its first poll, one of them already rebuilding a worktree.
#
# Cost: after losing state.json a machine no longer claims pre-existing doing/agent
# items (no worker_hosts record = not mine). Re-label them by hand to recover.
# Single-host setups should leave this false — that is the original behaviour.
#
# 多机分工的 self-heal 归属判定。两台机器盯同一个仓库时,**上面的 label 分工不够**,
# 这两个也必须一起开。
#
# 原因:self-heal 判「worker 死了」的依据是「挂着 doing/agent,但本机没有同名 tmux
# session」。两台机器的 session 命名规则一模一样,所以 A 机看 B 机正在跑的活,看到的
# 就是这个形状,于是接管过去。同一个盲区还会撑爆并发计数:A 把 B 的 doing/agent 算进
# 自己的 max,slot 永远满,一条活都派不出去,日志上却一切正常。
# 2026-09-04 实测:新配的第二台机器第一次 poll,25 秒内接管了四个正在跑的 worker,
# 其中一个已经开始重建 worktree。
#
# 代价:state.json 丢了之后,存量 doing/agent 条目本机不再认领(worker_hosts 没记录
# = 不是我的),人工重标一次即可恢复。单机场景保持 false,就是原来的行为。
SELFHEAL_ONLY_OWN_WORKERS="false"
# Defaults to the short hostname; set explicitly only if two machines share one.
# 默认取短主机名;两台机器重名时才需要显式写死。
# SELFHEAL_HOST_ID="futurelab-01"
# In greedy mode you MUST also list the other host's label in GREEDY_SKIP_LABELS —
# the greedy sweep ignores trigger labels and would otherwise pick up their work.
# Everything downstream follows the label you set: the reject path of the review
# gate, self-heal, and the "re-label X and I'll start" line in the templates all
# use this project's own labels, so an item routed to a host stays on that host.
#
# 追加触发 label(逗号分隔,默认空)。语义与 LABEL_PENDING_AGENT 完全相同 —— 同一套
# prompt、同一个 worker、同一个模型 —— 只是多认几个名字,**不是**模型选择(那是 fable)。
#
# 存在的理由:两台机器盯同一个仓库时分工。各自配自己的主机标签,谁的标签谁接:
# 05 上:LABEL_PENDING_AGENT="pending/agent" LABEL_PENDING_AGENT_EXTRA="pending/agent/05"
# 01 上:LABEL_PENDING_AGENT="pending/agent/01" LABEL_PENDING_AGENT_EXTRA=""
# greedy 模式下**必须**同时把对方的标签写进 GREEDY_SKIP_LABELS:兜底趟不看触发 label,
# 否则照样把别人家的活收走。
# 下游是自洽的:review 关卡的打回、self-heal 回填、模板里那句「重新标 X 我开干」
# 用的都是本项目自己的 label,所以派到哪台机器就一直留在哪台。
LABEL_PENDING_AGENT_EXTRA=""
# ── Optional cross-review gate / 交叉 review 关卡(默认关闭)──
# Set LABEL_PENDING_REVIEW to enable: instead of flipping straight to pending/human,
# the worker flips here after producing code; the daemon then dispatches a *different*
# agent (codex by default) with the `review` prompt template. Pass → pending/human.
# Fail → bounced back to pending/agent with concrete feedback, capped at REVIEW_MAX_ROUNDS.
# Leave empty to keep the previous behaviour exactly.
#
# 设了才启用:worker 产出代码后翻到这个 label(而不是直接 pending/human),daemon 用
# **另一个** agent(默认 codex)+ review 专用模板起独立 session 把关。通过才 pending/human;
# 不通过带具体意见打回 pending/agent,最多 REVIEW_MAX_ROUNDS 轮,超了转人工。留空 = 关闭。
# 需要自备 prompts/review.template.md,并在仓库建好该 label。
LABEL_PENDING_REVIEW=""
REVIEW_WORKER_AGENT="codex"
REVIEW_MODEL=""
REVIEW_MAX_ROUNDS=3
LABEL_AGENT_DOING="doing/agent"
LABEL_PENDING_HUMAN="pending/human"
LABEL_PENDING_PR="pending/PR"
LABEL_DONE="Done"
# ── Output language / 输出语言 ──
# Language for everything the worker writes BACK TO GitHub: issue / PR comments,
# design proposals, PR body. ISO 639-1 code (e.g. "en", "zh", "ja"). Default "en".
# Code, commit messages, branch names stay in repo / English conventions either way.
#
# worker 写回 GitHub 的内容(issue / PR 评论、设计提案、PR body)用什么语言。
# ISO 639-1 代码(如 "en"、"zh"、"ja")。默认 "en"。
# 代码、commit message、分支名仍按仓库 / 英文惯例,不受这个选项影响。
OUTPUT_LANGUAGE="en"
# ── Worker setup / Worker 安装 ──
# Install / init command run after creating the worktree (one line; chain with &&).
# Examples:
# Node: "npm ci || npm install"
# uv: "uv sync"
# Cargo: "cargo fetch"
# none: ":"
#
# worktree 创建后跑的安装/初始化命令(一行;可用 && 串接)
# 例:
# Node: "npm ci || npm install"
# uv: "uv sync"
# Cargo: "cargo fetch"
# none: ":"
WORKTREE_SETUP_CMD="npm ci || npm install"
# Gitignored files to copy into the worktree (space-separated relative paths).
# Typical: .env / any locally-only config not in git.
#
# 在 worktree 里要 cp 进去的「gitignored」文件(空格分隔的相对路径)
# 典型场景:.env / 任何 commit 不到 git 的本地配置文件
COPY_TO_WORKTREE=".env"
# ── Worker identity / Worker 身份 (commit author / pusher 显示成谁) ──
# Empty (default) = worktree uses your global ~/.gitconfig user.name/email (you).
# To make worker commits/push appear as a separate bot account, set bot identity here:
# WORKTREE_GIT_USER_NAME="my-bot"
# WORKTREE_GIT_USER_EMAIL="<numeric-id>+my-bot@users.noreply.github.com"
# Note: this only controls commit author. Push attribution depends on GH_TOKEN
# (EnvironmentFile) + ~/.git-credentials routing — keep them consistent.
#
# 默认空 = worktree 用你 global ~/.gitconfig 的 user.name/email(也就是你自己)
# 想让 worker 的 commit / push 显示为单独的 bot 账号,在这里写 bot 的身份:
# WORKTREE_GIT_USER_NAME="my-bot"
# WORKTREE_GIT_USER_EMAIL="<numeric-id>+my-bot@users.noreply.github.com"
# 注意:这只控制 commit author。push 用谁的 token 还看 GH_TOKEN (EnvironmentFile) +
# ~/.git-credentials 的路径路由,要一致设置才能 attribution 干净。
WORKTREE_GIT_USER_NAME=""
WORKTREE_GIT_USER_EMAIL=""
# ── Worker agent / Worker agent CLI ──
# Which agent CLI runs as the worker. Built-in drivers (see docs/drivers.md):
# claude — Claude Code (default; behavior identical to pre-driver era)
# opencode — OpenCode (sst/opencode)
# codex — Codex CLI (OpenAI)
# cursor — Cursor Agent CLI (`agent`; headless with -p --trust --force)
# Custom: drop scripts/drivers/<name>.sh into the skill, or project-level into
# <host>/.agents/skills/coding-agent-work-loop/drivers/<name>.sh
#
# 哪个 agent CLI 当 worker。内置 driver(详见 docs/drivers.zh.md):
# claude — Claude Code (默认;行为完全等同未引入 driver 抽象前)
# opencode — OpenCode (sst/opencode)
# codex — Codex CLI (OpenAI)
# cursor — Cursor Agent CLI(`agent`;headless 用 -p --trust --force)
# 自定义:往 skill 的 scripts/drivers/<name>.sh 加,或放在项目级
# <host>/.agents/skills/coding-agent-work-loop/drivers/<name>.sh
WORKER_AGENT="claude"
# ── Claude Code launch / Claude Code 启动 ──
# Extra flags when launching claude. --dangerously-skip-permissions strongly
# recommended; otherwise the session blocks on permission prompts.
# (Only used when WORKER_AGENT=claude. Other drivers read their own *_EXTRA_FLAGS.)
#
# 启动 claude 时的额外 flag。--dangerously-skip-permissions 强烈推荐,否则会卡权限弹窗。
# (只有 WORKER_AGENT=claude 时生效;其他 driver 读各自的 *_EXTRA_FLAGS。)
CLAUDE_EXTRA_FLAGS="--dangerously-skip-permissions"
# Codex defaults to bypassing approvals and its internal sandbox so unattended
# workers do not block. Only use this in an externally isolated environment.
# Set an explicit empty string to use Codex's normal approval and sandbox policy.
#
# Codex 默认跳过确认并关闭内部 sandbox,避免无人值守 worker 卡住。
# 只应用于已有外部隔离的环境;显式设为空字符串可恢复 Codex 的常规策略。
CODEX_EXTRA_FLAGS="--dangerously-bypass-approvals-and-sandbox"
# OpenCode / Cursor 启动 flag(按需打开):
# OPENCODE_EXTRA_FLAGS=""
# CURSOR_AGENT_EXTRA_FLAGS=""
# ── Env passed to worker / 传给 worker 的 env ──
# tmux doesn't inherit parent shell env by default; vars to pass must be listed
# here (space-separated). GH_TOKEN strongly recommended so the worker's gh CLI
# uses the PAT from systemd EnvironmentFile rather than falling back to
# ~/.config/gh/'s default account (common multi-account pitfall).
#
# tmux 默认不继承父 shell 的 env,要透传必须列在这里(空格分隔)。
# GH_TOKEN 强烈推荐:让 worker 里的 gh CLI 用 systemd EnvironmentFile 里的 PAT,
# 而不是回退到 ~/.config/gh/ 默认账号(多账号场景常见坑)。
WORKER_PASS_ENV="GH_TOKEN"
# Of the vars above, which are SECRETS. These are never passed as `-e VAR=value`
# (that value lands in tmux's argv, and /proc/<pid>/cmdline is world-readable —
# any local user can `ps aux` and read the PAT; a long-lived tmux server keeps it
# visible for as long as it runs). Instead the daemon writes each one to a 0600
# file under $STATE_DIR/secrets/ and passes only `-e VAR_FILE=<path>`; the worker
# command re-reads it in its own shell. Missing secret => dispatch is refused
# outright (a worker that starts without a token just 403s and gets re-dispatched
# forever by self-heal).
#
# 上面那些变量里,哪些属于**机密**。机密不走 `-e VAR=值`——值会落进 tmux 的 argv,
# 而 /proc/<pid>/cmdline 是全局可读的,同机任何用户 `ps aux` 就能抄走 PAT;tmux
# server 又是长驻进程,它 argv 里那份会一直挂到 server 退出(实测挂过 11 小时)。
# 改成:daemon 把值写到 $STATE_DIR/secrets/ 下的 0600 文件,argv 里只出现
# `-e VAR_FILE=<路径>`,worker 在自己的 shell 里读回来。缺机密 = 直接拒绝派工
# (没 token 的 worker 只会 403,然后被 self-heal 无限重派,烧 API 还查不出根因)。
WORKER_SECRET_ENV="GH_TOKEN"
# ── Auto-cleanup after PR merge / PR merge 后自动 cleanup ──
# true = daemon auto-runs cleanup-issue.sh on newly-merged PRs (incl. CLEANUP_HOOK)
# false = fully manual; run cleanup-issue.sh yourself after merge
# Safety: refuses to clean a busy worker session (retried next tick).
#
# Boundaries (important):
# Cleaned → $WORKTREE_BASE/issue-N/, tmux session, CLEANUP_HOOK
# (ports / tunnel / notifications)
# NOT cleaned → git branch, ~/.claude/projects/<encoded-cwd>/*.jsonl (Claude
# conversation history), $STATE_DIR/sessions/*.log (tmux pane
# log), $STATE_DIR/state.json, GitHub issue/PR/comments
# So even with auto-cleanup on, you can look things up months later and resume
# the conversation via the SOPs. Details: docs/persistence.md.
#
# true = daemon 检测到新 merge 的 PR 后自动跑 cleanup-issue.sh(含 CLEANUP_HOOK)
# false = 全手动;merge 完自己跑 cleanup-issue.sh
# 自动 cleanup 安全策略:worker session 仍 busy 时拒绝清(下轮重试)
#
# 边界(重要):
# 会清 → $WORKTREE_BASE/issue-N/、tmux session、CLEANUP_HOOK 钩子(端口/tunnel/通知)
# 不清 → git branch、~/.claude/projects/<encoded-cwd>/*.jsonl(Claude 对话历史)、
# $STATE_DIR/sessions/*.log(tmux pane log)、$STATE_DIR/state.json、
# GitHub issue/PR/comment
# 所以即使开着自动 cleanup,半年后仍能事后查阅、按 SOP 续上对话。
# 详见 docs/persistence.md。
AUTO_CLEANUP_ON_MERGE="true"
# ── Post-acceptance project cleanup hook / 验收完成后的项目级 cleanup hook ──
# When you run `bash $SKILL_DIR/scripts/cleanup-issue.sh <N>`, this hook runs
# BEFORE the generic cleanup (kill session, remove worktree). Use it to:
# - Close local ports / dev server occupied by this issue
# - Tear down tailscale serve / cloudflare tunnel routes
# - Push end-state metric / notify chat group
#
# Relative paths are interpreted relative to PROJECT_ROOT. Empty = skip.
# Env available to the hook: ISSUE, WORKTREE, BRANCH, REPO, PROJECT_ROOT.
# Recommended path: CLEANUP_HOOK=".agents/skills/coding-agent-work-loop/cleanup-hook.sh"
# (matches the Agent Skill project-level layout; prompt overrides
# go under prompts/ in the same dir)
#
# 当你跑 `bash $SKILL_DIR/scripts/cleanup-issue.sh <N>` 验收清理时,本 hook 在
# 通用清理(杀 session、删 worktree)之前先跑。可用来:
# - 关该 issue 占用的本地端口 / dev server
# - 撤销 tailscale serve / cloudflare tunnel 路由
# - 推送结束态 metric / 通知群组
#
# 相对路径会被解释为相对 PROJECT_ROOT。留空跳过。
# Hook 拿到的 env: ISSUE, WORKTREE, BRANCH, REPO, PROJECT_ROOT
# 推荐路径:CLEANUP_HOOK=".agents/skills/coding-agent-work-loop/cleanup-hook.sh"
# (配合 Agent Skill 项目级目录约定;prompts 覆盖也放同目录下 prompts/)
CLEANUP_HOOK=""
# ── On-demand preview server / 按需预览服务(socket 激活,默认关闭)──
# Reviewers need a clickable preview URL per issue, but a preview server that
# stays resident for every open issue is pure idle RSS — measured on one host:
# 10 open `pending/PR` issues × ~85 MiB, several idle for over a week.
#
# Set PREVIEW_EXEC to switch previews to systemd socket activation:
# coding-agent-preview@<port>.socket always listening, ~0 memory
# └ first connection starts →
# coding-agent-preview@<port>.service systemd-socket-proxyd, exits when idle
# └ requires →
# coding-agent-preview-app@<port>.service your server; stops with the proxy
# The URL never breaks; the process comes back on the next hit (~1s cold start,
# no rebuild — the worktree's build output is already on disk).
#
# 每个 issue 都要给审阅者一条可点的预览 URL,但为每个 open issue 常驻一个 server 就是
# 纯粹的空转内存。实测某台机器:10 个 pending/PR 的 issue × ~85 MiB,其中几个空转一周以上。
#
# 配了 PREVIEW_EXEC 就切到 systemd socket 激活:
# coding-agent-preview@<端口>.socket 常驻监听,几乎不占内存
# └ 首次连接触发 →
# coding-agent-preview@<端口>.service systemd-socket-proxyd,闲置自己退出
# └ Requires →
# coding-agent-preview-app@<端口>.service 你的 server,跟着 proxy 一起停
# URL 永远有效;进程在下次访问时自动回来(冷启 ~1s,不重新 build——worktree 里
# 的构建产物还在)。
#
# 起法由 preview-serve.sh 注册、systemd 拉起:
# bash scripts/preview-serve.sh <issue> 注册 + 起监听,打印 URL
# bash scripts/preview-serve.sh --list 看本项目所有 preview 及其死活
# bash scripts/preview-unserve.sh --issue <N> 注销(cleanup hook 里调)
#
# 进程必须从 $PORT / $HOST 读监听参数(不能写死端口)——它拿到的是**后端**端口,
# 公开端口被 .socket 占着。留空 = 不启用,preview 怎么起完全由 worker prompt 决定。
PREVIEW_EXEC=""
# 公开端口 = PREVIEW_PORT_BASE + issue 编号。撞 Chromium unsafe ports 的话换一个 base。
PREVIEW_PORT_BASE=4000
# 后端端口 = 公开端口 + 这个偏移。只要不跟别的服务撞就行。
PREVIEW_BACKEND_OFFSET=10000
# 闲置多久后 app 退出(systemd 时间格式:30min / 2h / 90s)。
# 调大 = 验收时更少冷启;调小 = 回收更快。冷启才 ~1s,不必调得太保守。
PREVIEW_IDLE="30min"
# app 从 exec 到能 accept 连接的等待上限(秒)。超时 = 本次启动判失败。
PREVIEW_READY_TIMEOUT=90
# preview-serve.sh 是否顺手 `tailscale serve --bg --https=<端口>` 把预览挂上 tailnet。
# 需要 sudo -n 跑 tailscale 的权限;失败只告警,本地 127.0.0.1 仍可用。
PREVIEW_TAILSCALE_SERVE=false
# 打印 URL 时用的主机名(一般是 tailnet MagicDNS 名)。留空则打印 http://127.0.0.1:<端口>/。
PREVIEW_URL_HOST=""
# ── Post-PR-creation hook / PR 创建后的 hook ──
# When the agent creates a PR (via `gh pr create`), this hook runs immediately
# after label flipping. Use it to:
# - Deploy a preview/beta version and post QR code on the PR
# - Trigger CI/CD pipelines
# - Notify team channels
#
# agent 创建 PR(`gh pr create`)并翻完 label 后,立刻跑这个 hook。
# 可用来:
# - 发布体验版并把二维码贴到 PR 评论
# - 触发 CI/CD 流水线
# - 通知团队
#
# 相对路径会被解释为相对 PROJECT_ROOT。留空跳过。
# Hook 拿到的 env: PR, ISSUE, WORKTREE, BRANCH, REPO, PROJECT_ROOT
# 推荐路径:PR_CREATED_HOOK=".agents/skills/coding-agent-work-loop/pr-created-hook.sh"
PR_CREATED_HOOK=""
# ── Max concurrency / 并发上限 ──
# Max live worker sessions at any time. 1 (serial) is the safest default.
# 同一时刻最多多少个 worker session 同时存活。建议 1(串行最稳)。
MAX_CONCURRENT_WORKERS=1
# ── Dispatch mode / 派工模式 ──
# What makes the daemon pick an item up.
# label (default) — only items carrying a trigger label (pending/agent[/fable],
# pending/review) are dispatched. Label nothing, and nothing
# happens: the safest default.
# greedy — every OPEN issue / PR is dispatched unless a blocking label
# stops it. Built-in blockers: pending/human, doing/agent,
# Done, pending/PR, pending/review (that one has its own pass).
# Extend with GREEDY_SKIP_LABELS below.
# The two are not either/or: in greedy mode the label passes still run FIRST, so
# fable / review labels keep choosing their own model and agent — greedy only
# sweeps up whatever is left, with the default agent and model.
# ⚠️ In greedy mode anyone opening an issue (anonymous outside users on a public
# repo included) immediately spends a worker's tokens. Use it on repos where
# "everything filed here is work to do" is actually true.
#
# 什么条件下 daemon 会把一个条目捞起来干。
# label(默认)—— 只有挂着触发 label(pending/agent[/fable]、pending/review)的才派工。
# 什么都不标 = 什么都不做,最安全的默认值。
# greedy —— **所有开着的** issue / PR,只要没被挡工 label 挡住就一律派工。
# 内置挡工 label:pending/human、doing/agent、Done、pending/PR、
# pending/review(它有自己那一趟)。要加自己的见下面 GREEDY_SKIP_LABELS。
# 两种不是二选一:greedy 模式下 label 那几趟**照样先跑**,所以 fable / review 这些带
# 模型和 agent 选择的 label 仍然生效;greedy 只是最后兜底把剩下的用默认 agent/模型收进来。
# ⚠️ greedy 下任何人新开一个 issue(公开仓里包括匿名外部用户)都会立刻烧一个 worker 的
# token。只在「这个仓库里躺着的就是要干的活」确实成立时用。
DISPATCH_MODE="label"
# Extra blocking labels for greedy mode (comma-separated, ADDED to the built-ins).
# greedy 模式的额外挡工 label(逗号分隔,**追加**在内置那几个之后)。
# 例:GREEDY_SKIP_LABELS="blocked,discussion,wontfix"
GREEDY_SKIP_LABELS=""
# How many open items greedy scans per poll (gh defaults to 30).
# greedy 每轮扫多少条开着的条目(gh 默认只给 30)。积压多时调大。
GREEDY_SCAN_LIMIT=100
# ── Queue order when full / 并发满时的取工顺序 ──
# When more items carry a pending label than there are free slots, the daemon
# picks by three sort keys, in order:
# 1. priority label — earlier in this list wins; ITEMS MARKED NOWHERE SORT LAST,
# behind every marked one, so you only ever label the urgent ones
# 2. stage — review bounce-back < resumed work < brand-new issue
# 3. waiting time — oldest updatedAt first
# Empty = disable key 1; ordering then falls back to stage + waiting time.
#
# 待派工的条目多于空闲 slot 时,daemon 按三个键依次比较决定先派谁:
# ① 优先级 label —— 本列表里越靠前越优先;**哪儿都没标的垫底**(排在所有标过的
# 之后,也排在原生字段最低档之后),所以平时什么都不用打,急了才挂 priority/p0
# ② 阶段 —— review 打回 < 续作 < 全新 issue
# (在飞的活上下文还热,先收尾更快腾出 slot;全新 issue 晚一轮没有沉没成本)
# ③ 等待时长 —— updatedAt 早的先派
# 留空 = 关掉 ① 这一层,只按 ②③ 排。setup.sh 会把这三个标签建到仓库里。
PRIORITY_LABELS="priority/p0,priority/p1,priority/p2"
# ── Priority source / 优先级来源 ──
# Where sort key #1 comes from.
# label (default) — the PRIORITY_LABELS above
# project — a single-select field on the linked GitHub Project (v2);
# tiers follow the ORDER THE OPTIONS ARE DEFINED IN on the
# project, so reordering there is the only place you edit.
# Items with no value fall to the last tier.
# both — project value wins where set, label as the fallback.
# Keep the two scales aligned (P0 ↔ priority/p0).
# Reading a project needs GraphQL: a classic PAT must have `read:project`, and a
# USER-owned project also requires that account to be a project collaborator.
# If the read fails for any reason the daemon logs one warning and falls back to
# label ordering for that poll — dispatch never stops over a board it can't read.
#
# 第一排序键从哪儿来。
# label(默认)—— 用上面的 PRIORITY_LABELS
# project —— 用关联的 GitHub Project (v2) 上那个单选字段;**档位顺序直接取该
# 字段在 Project 里定义的选项顺序**,要改顺序只在 Project 里拖一次,
# 这儿不用抄。没设值的条目落最后一档。
# both —— Project 上设了就用 Project 的,没设的回落到 label。
# ⚠️ 两套档位各自独立编号,混用时请让它们对齐(P0 ↔ priority/p0)。
# 读 Project 只能走 GraphQL:classic PAT 必须勾 read:project;**个人名下**的 Project
# (不是组织的)还要求这个账号是该 Project 的协作者。
# 任何一环读不到,daemon 只记一条警告并在本轮回落到 label 排序——不会因为看板读不到
# 就停下派工。
PRIORITY_SOURCE="label"
# Which project, and whose. Two different lookups:
# PROJECT_NUMBER set → looked up directly by <owner, number>; the project does
# NOT have to be linked to this repo. The number is the last
# path segment of the board URL:
# github.com/orgs/<org>/projects/N or /users/<you>/projects/N
# PROJECT_NUMBER empty → the lowest-numbered project LINKED to this repo
# (explicit orderBy, not the API's unpromised default order)
# PROJECT_OWNER only matters for the first case, and defaults to this repo's owner.
# Set it when the board lives under a different owner than the repo — a personal board
# holding cards from org repos is the common case.
#
# 用哪个 Project、在谁名下。两条路,行为不一样:
# PROJECT_NUMBER 有值 → 按 <owner, 编号> **直取**,Project 不需要跟本仓库有关联。
# 编号就是看板 URL 的最后一段:
# github.com/orgs/<组织>/projects/N 或 /users/<你>/projects/N
# PROJECT_NUMBER 留空 → 取**本仓库关联的** Project 里编号最小的那个
# (查询里写死 orderBy,不靠 API 没承诺的默认顺序)
# PROJECT_OWNER 只在第一种情况下起作用,默认取本仓库的 owner。
# 看板挂在跟仓库不同的 owner 名下时必须配 —— 个人看板装着组织仓库的卡片是常见配置。
# 每轮日志会写明实际用了哪个看板:`Project 优先级:读到 N 条(看板 7 board7,...)`
PROJECT_NUMBER=""
PROJECT_OWNER=""
# Field name to read. GitHub's newer Priority is an ISSUE-level field that is
# projected onto the board as a column (and is clickable in the project's field
# settings), which makes it look like a project custom field — it is not. Read
# through the Projects API it reports ZERO options and its item values come back as
# ProjectV2ItemIssueFieldValue, so that path cannot see it at all. It is only
# readable via issue.issueFieldValues. The daemon therefore tries, in order:
# 1. the native ISSUE field (values live on the issue, board-independent)
# 2. a genuine board single-select custom field (like Status)
# Only #2 can be set on PRs — PRs have no native issue fields — so with #1 in use,
# PRs always fall back to labels.
#
# 要读的字段名。GitHub 新版的 Priority 是 **issue 级字段**,但会投影到看板上显示成
# 一列、在看板的字段设置里也点得进去,所以看着像看板自定义字段 —— 它不是。
# 从 Projects API 读它会得到「options 为空」,条目值的类型也是 ProjectV2ItemIssueFieldValue
# 而非单选值,**那条路根本读不到**;只有 issue.issueFieldValues 读得到。
# 所以 daemon 按这个顺序试:
# ① issue 原生字段(值在 issue 上,跟看板无关)
# ② 真·看板自定义单选字段(像 Status 那样的)
# 只有 ② 能标到 PR 上(PR 没有 issue 原生字段),所以用着 ① 时 PR 一律回落到 label。
# 每轮日志会写明这次用的是哪一种。
PROJECT_PRIORITY_FIELD="Priority"
# Token used ONLY for the project read. Empty = reuse the daemon's GH_TOKEN.
# Handy when the bot account can't see the board but you have another token that can.
# 只用于读 Project 的 token。留空 = 复用 daemon 自己的 GH_TOKEN。
# bot 账号看不到看板、但你手上另有一个能看的 token 时,塞这里最省事。
PROJECT_GH_TOKEN=""
# ── Poll interval / 轮询间隔 ──
# Daemon poll cycle (seconds). Used by the systemd timer.
# Daemon 轮询周期(秒)。systemd timer 用这个值。
POLL_INTERVAL_SECS=60
# ── Latest-site refresh on merge / merge 钩子刷新常驻「最新站」 ──
# When the poller detects a newly-merged PR (§ 3 auto-cleanup scan), it can
# fast-forward the main checkout and rebuild + restart a resident production
# server (dist + /api in one process) so a fixed URL always serves latest base.
# The refresh script is idempotent (skips when HEAD is already built and the
# session is alive) and runs in the background — it never blocks polling.
# First bring-up / force rebuild: `FORCE=1 bash scripts/refresh-latest-site.sh`.
#
# poller 在 § 3 扫到新 merge 的 PR 时,可顺手把主 checkout ff-only 拉到最新并
# rebuild + 重启一个常驻正式 server(dist 前端 + /api 同源单进程),让固定 URL
# 永远指向最新 base 版本。刷新脚本幂等(HEAD 已构建且 session 活着直接跳过)、
# 后台执行不阻塞轮询。首次拉起 / 强制重建:`FORCE=1 bash scripts/refresh-latest-site.sh`。
LATEST_SITE_REFRESH=false
# LATEST_SITE_PORT=3000 # server 监听 127.0.0.1:<PORT>
# LATEST_SITE_SESSION="${TMUX_PREFIX}-latest" # 常驻 tmux session 名
# LATEST_SITE_BUILD_CMD="npm run build"
# LATEST_SITE_SERVER_CMD="node server/index.mjs"