Repository navigation
Expand file tree
/
Copy pathcoding-agent.config.example
More file actions
799 lines (722 loc) · 46.7 KB
/
Copy pathcoding-agent.config.example
File metadata and controls
799 lines (722 loc) · 46.7 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
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
# 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"
# Optional model passed only to ordinary issue / PR workers. Empty means the
# selected driver's own current default model.
WORKER_MODEL=""
# ── 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"
# Codex token values use the built-in standard API text-price table when this
# variable is UNSET. Checked 2026-09-28; see scripts/drivers/token-usage/codex-prices.json.
# Setting CODEX_PRICES replaces the whole table; {} disables estimates.
# Values are USD / million tokens, a converted reference value, never a bill.
# Cached writes without a configured rate remain unpriced.
#
# Codex token 折算在未设置此变量时用内置标准 API 文本价目(核对于 2026-09-28)。
# 显式设置会整表替换;{} 关闭估算。金额只是折算参考价值,不是账单。
# 没有单价的缓存写入仍如实报缺价。
# Worker scripts run inside tmux; to use a custom table, uncomment this export
# AND add CODEX_PRICES to WORKER_PASS_ENV below.
# worker 在 tmux 内运行;启用自填表时须取消下面的注释,并把 CODEX_PRICES
# 加进下方 WORKER_PASS_ENV。
# export CODEX_PRICES='{"gpt-6-astra":{"in":10,"cached_in":1,"out":50,"cache_write":12.5},"gpt-6-sol":{"in":2,"cached_in":0.2,"out":10,"cache_write":2.5},"gpt-6-luna":{"in":0.1,"cached_in":0.01,"out":0.5,"cache_write":0.125},"gpt-5.6-sol":{"in":4,"cached_in":0.4,"out":20,"cache_write":5},"gpt-5.6-terra":{"in":2,"cached_in":0.2,"out":12,"cache_write":2.5},"gpt-5.6-luna":{"in":0.2,"cached_in":0.02,"out":1.2,"cache_write":0.25},"gpt-5.3-codex":{"in":1.75,"cached_in":0.175,"out":14},"gpt-5.2-codex":{"in":1.75,"cached_in":0.175,"out":14},"gpt-5-codex":{"in":1.25,"cached_in":0.125,"out":10},"codex-mini-latest":{"in":1.5,"cached_in":0.375,"out":6}}'
# 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"
# ── Two GitHub identities / 两把 GitHub 身份 ──
# By default one token does everything: it polls GitHub every 30-60s AND pushes
# commits, posts comments, flips labels. That coupling gets expensive when the
# account is rate-limited or suspended -- the polling volume burns the account,
# and what you lose with it is every write path plus your commit authorship.
#
# Set WRITE_GH_TOKEN to split them:
# GH_TOKEN -> polling and all reads. Can be a strictly read-only token.
# WRITE_GH_TOKEN -> every write: label flips, the daemon's own alert issue, the
# post-merge retrospective push, and the token handed to the
# worker for its commits / comments.
# Empty (the default) falls back to GH_TOKEN, so single-account setups are
# completely unaffected.
#
# Fine-grained PAT permissions on the repo:
# poll token: Metadata R, Contents R, Issues R, Pull requests R
# write token: Metadata R, Contents RW, Issues RW, Pull requests RW
#
# WHERE THE VALUE GOES: here is fine -- this file is gitignored and setup.sh
# chmods it 600 -- and under the cron fallback it is the ONLY place, because that
# path has no EnvironmentFile at all. If GH_TOKEN stays in the systemd / launchd
# EnvironmentFile, putting only WRITE_GH_TOKEN here is the tidier split: the
# worker is routinely pointed at this config file for local dry-runs, and the
# write token is one it already holds anyway.
# Both `WRITE_GH_TOKEN=...` and `export WRITE_GH_TOKEN=...` work.
# KEEP THIS FILE 0600 whenever it holds a token -- the daemon warns if it isn't.
#
# 默认一把 token 干所有事:每 30~60 秒轮询 GitHub,同时还要 push、发评论、翻 label。
# 这个耦合在账号被限流 / 封号时代价很具体 —— 轮询量把账号烧掉,连带失去的是所有写
# 能力和 commit 署名身份。
#
# 设 WRITE_GH_TOKEN 就把两者分开:
# GH_TOKEN -> 轮询和所有读。可以是一把严格只读的 token。
# WRITE_GH_TOKEN -> 所有写:翻 label、daemon 自己的告警 issue、合并后复盘的 push,
# 以及交给 worker 去 commit / 发评论的那一把。
# 留空(默认)= 回落 GH_TOKEN,单账号部署零改动。
#
# 细粒度 PAT 在本仓库需要的权限:
# 轮询 token:Metadata 读、Contents 读、Issues 读、Pull requests 读
# 写 token:Metadata 读、Contents 读写、Issues 读写、Pull requests 读写
#
# 值放哪:放这里就行 —— 本文件已 gitignore,setup.sh 会 chmod 600;cron 兜底那条路
# 更是**只能**放这里,它根本没有 EnvironmentFile。如果 GH_TOKEN 留在 systemd /
# launchd 的 EnvironmentFile 里,这里只写 WRITE_GH_TOKEN 是更干净的分法:worker 会被
# 正常指使读这个 config 做本地干跑,而写 token 本来就在它手上。
# 写 `WRITE_GH_TOKEN=...` 或 `export WRITE_GH_TOKEN=...` 都可以。
# 只要本文件装着 token 就保持 0600 —— 不是的话 daemon 每轮都会告警。
#
# 注意这里**故意保持注释状态**:本文件是在环境变量之后 source 的,写一行
# `WRITE_GH_TOKEN=""` 会把 EnvironmentFile 里同名的值清掉。要用就填上真值。
# WRITE_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"
# After each new merge, archive the review in the SOURCE PROJECT repository.
# Requires Claude CLI and permission to push the project's origin/$BASE_BRANCH.
# First run starts at now; one background review at a time, hourly retries.
# Project reports: docs/retrospectives/pr-N.md
# Project lessons: .agents/skills/coding-agent-work-loop/prompts/lessons.md
# Never publishes project evidence or generated text to the public skill repo.
# Public workflow improvements require a separate de-identification review.
POST_MERGE_RETROSPECTIVE="true"
RETROSPECTIVE_MODEL="sonnet"
# ── 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> --expect-worktree <W> 注销(cleanup hook 里调,W = hook 拿到的实际 WORKTREE)
#
# 进程必须从 $PORT / $HOST 读监听参数(不能写死端口)——它拿到的是**后端**端口,
# 公开端口被 .socket 占着。留空 = 不启用,preview 怎么起完全由 worker prompt 决定。
PREVIEW_EXEC=""
# 公开端口 = PREVIEW_PORT_BASE + issue 编号。撞 Chromium unsafe ports 的话换一个 base。
PREVIEW_PORT_BASE=4000
# 设了就改成 PREVIEW_PORT_BASE + issue % PREVIEW_PORT_MODULO,让端口留在一个号段里
# (例:1000 → 4000–4999,issue 过千也不会越界撞进别的号段)。留空 = 不取模。
# 取模后同余 issue 同端口(#40 与 #1040 都是 4040):后注册的那个会被拒绝并报出占用方,
# 不会覆盖;注销也只认登记在自己名下的端口。
PREVIEW_PORT_MODULO=""
# 后端端口 = 公开端口 + 这个偏移。只要不跟别的服务撞就行。
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
# ── Retry limits / 重试上限 ──
# How many consecutive dispatch failures one item may rack up before the daemon
# strips its trigger labels and hands it to pending/human.
#
# Why this exists: a dispatch that fails changes nothing — the label is not
# flipped, the comment cursors do not advance — so the very next poll picks the
# same item up again. With no cap that is an unbounded hot loop. Measured
# 2026-09-18: one PR whose branch was checked out by an unrelated worktree was
# re-dispatched 270 times over 2h24m, ~32s apart, and only stopped when GitHub
# suspended the bot account.
#
# Why not 1: transient failures (network blips, GitHub 5xx) deserve a retry.
# Only a *consecutive* run of failures means something is actually wrong.
# The counter is reset on the first success and again when the item is escalated,
# so re-labelling by hand gives it a fresh set of attempts.
# Counters live in $STATE_DIR/dispatch-fail/. There is deliberately no "off"
# value — turning it off just puts the hot loop back.
#
# 一条活连续派工失败多少次之后,daemon 摘掉它的触发 label 转 pending/human。
#
# 为什么需要它:派工失败时什么状态都没变——label 没翻、comment cursor 没动——于是
# 下一轮 poll 又把同一条活捡起来。没有上限就是一个无限热循环。2026-09-18 实测:一条
# PR 因为目标分支被一个无关的 worktree 签出,2 小时 24 分钟里每 32 秒重派一次、共
# 270 次,直到 GitHub 把 bot 账号封停才停下。
#
# 为什么不设成 1:瞬时失败(网络抖动、GitHub 5xx)该允许重试,只有**连续**失败才
# 说明真出了问题。计数在第一次成功时清零,升级转人工时也清零——所以人工重标一次
# 就重新获得完整的重试次数。计数文件在 $STATE_DIR/dispatch-fail/。
# 刻意不提供「关掉」的取值:关掉就等于把那个热循环放回去。
DISPATCH_MAX_RETRIES=3
# How many times self-heal may auto-redispatch a worker whose tmux session died
# before it gives up and hands the item to pending/human instead. Same shape as
# DISPATCH_MAX_RETRIES but a different failure: the dispatch succeeded, the
# worker just died right after. Counters live in $STATE_DIR/selfheal/.
#
# self-heal 在放弃、转 pending/human 之前,最多自动重派多少次 tmux session 已经
# 死掉的 worker。形状跟 DISPATCH_MAX_RETRIES 一样,但针对的是另一种失败:派工本身
# 成功了,worker 起来之后立刻死。计数文件在 $STATE_DIR/selfheal/。
SELFHEAL_MAX_RETRIES=3
# ── 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 + idle backoff / 轮询间隔与空闲退避 ──
# How often the scheduler wakes the poller (seconds). setup.sh writes this value
# into the per-instance systemd timer drop-in (Linux) or the generated launchd
# plist (macOS), so changing it takes effect after a re-run of setup.sh — it is
# no longer the dead config it used to be.
#
# 闹钟多久把 poller 叫醒一次(秒)。setup.sh 会把这个值写进本实例的 systemd timer
# drop-in(Linux)/ 生成的 launchd plist(macOS),所以改完要重跑一次 setup.sh 才
# 生效 —— 它不再是从前那个没人读的死配置了。
#
# 注意它**不是**「脚本最快多久干一次活」的下限:没退避的时候每个 tick 都真跑,哪怕
# timer 实际比这个值更快(比如手写 drop-in 把某个实例调到 30 秒)。它只在两个地方被
# 当成数值用:驱动 timer,以及作为「读不到 GitHub」那套失败阶梯的起步值。
POLL_INTERVAL_SECS=60
# Idle backoff ladder: `<quiet seconds>:<poll interval seconds>`, comma separated.
# A project that has seen no activity for that long polls at that interval.
# Default: quiet for 1 day -> every 5 min, 3 days -> 10 min, 7 days -> 30 min.
# **Empty string turns the whole feature off** (idle backoff AND failure backoff),
# restoring the pre-#35 behaviour byte for byte.
#
# 空闲退避阶梯:`<安静了多少秒>:<用多大间隔轮询>`,逗号分隔。按**安静了多久**分档,
# 不是按空闲了几轮。默认 = 安静 1 天 → 5 分钟,3 天 → 10 分钟,7 天 → 30 分钟。
#
# 什么算「有动静」(任意一条成立就立刻回到不退避、安静计时归零):daemon 自己派了工 /
# 自愈翻了标签 / 回收了 session / 清理动了东西;GitHub 上还挂着 doing/agent 或本机还有
# worker session 活着;队列里有待派工的活;仓库里相关条目的最后更新时间或标签变了。
#
# 代价只有一个,写在这里免得以后找:项目安静越久,你打上 pending/agent 之后要等的越久,
# 最坏就是最后那一档(默认 30 分钟)。活跃项目完全不受影响——第一档要安静满一整天。
#
# **留空 = 整个特性关闭**(空闲退避和故障退避一起关),行为逐字节回到改动前。
POLL_BACKOFF_LADDER="86400:300,259200:600,604800:1800"
# Heartbeat: force a full poll if this long has passed since the last successful
# GitHub read, whatever the ladder or the local state says. This is the backstop
# that makes "the local pace state can never wedge a project" true.
#
# 心跳:距上一次成功读到 GitHub 超过这么久,就无条件跑一次完整轮询——不管退到哪一档、
# 不管指纹说什么、也不管这套逻辑本身有没有 bug。它是「本地状态坏了也不会把项目卡死」
# 这条保证的兜底。阶梯里比它还慢的档位会被夹到这个值(并打一行警告)。
POLL_FORCE_SYNC_SECS=1800
# Failure backoff cap. Reading GitHub is not the same as having nothing to do,
# so failures get their own ladder — it starts at POLL_INTERVAL_SECS, doubles on
# each further failure, and is capped here — and it never advances the quiet
# timer, so an outage cannot silently slow a busy project down.
# Measured 2026-09-18..22: five projects burned 30040 polls over 88 hours, every
# one of them a 403 "account suspended". Same window with this: 905.
#
# 读不到 GitHub 时的退避上限。「读不到」不等于「没活干」,所以它单独一套阶梯:从
# POLL_INTERVAL_SECS 起步、每多失败一次翻倍、封顶在这个值;而且**不推进安静计时**,
# 故障恢复后从故障前那一档继续,一次网络抖动不会把活跃项目打慢。
# 实测依据:2026-09-18~22 那 88 小时,5 个项目跑了 30040 轮全是 403「账号已封」;
# 同一窗口加上这套之后只剩 905 轮。
POLL_FAIL_BACKOFF_MAX_SECS=1800
# Slack for timer jitter. systemd's AccuracySec lets a 60s timer fire at 60-65s;
# without slack a 60s tier would be stretched into 120s.
# 给 timer 抖动留的余量。systemd 的 AccuracySec 会让 60 秒的 timer 落在 60~65 秒之间,
# 不留余量的话 60 秒那一档会被抖成 120 秒。
POLL_DUE_SLACK_SECS=10
# ── Main-checkout staleness alert / 主 checkout 落后告警 ──
# The daemon fast-forwards the main checkout before every dispatch, but it never
# touches a dirty tree, a non-base branch, or a diverged local base — by design.
# Those skips only reach poll.log, which nobody reads: a checkout once sat 9 days
# and 1523 skipped dispatches behind with no outward signal. When the local base
# falls this many commits behind origin, the daemon opens an issue labelled
# pending/human (only a human can commit the WIP or switch the branch back, and
# in greedy mode that label is what keeps a worker from picking it up) and closes
# it once the checkout catches up.
#
# Off by default (0). Only a checkout people actually work in gets stuck this
# way — a project that only ever runs unattended never leaves WIP behind — and
# opening issues in someone's repo shouldn't come free with the framework.
#
# daemon 每次派工前会 ff-only 同步主 checkout,但工作区脏 / 不在 base 分支 / 本地
# base 分叉时一律不碰 —— 这是有意的。代价是这些跳过只写进 poll.log,而没人翻它:
# 实测有过主 checkout 卡住 9 天、1523 次派工全被挡却毫无外部信号。本地 base 落后
# origin 超过这个数就自动开一个 issue,跟上后自动关闭。issue 挂 pending/human:
# 这事只能人解(commit 掉 WIP / 切回分支 / rebase),且 greedy 模式下全靠这个
# label 才不会被 worker 捡去白干一轮。
#
# **默认 0 = 关**。会被卡住的只有人真的上手干活的那个 checkout;纯自动跑的项目
# 不会有人在上面留 WIP,给它开一个永不触发的检查只是噪音。何况告警要往仓库里开
# issue,不该是装上框架就自动获得的行为。要的项目自己设个数字(推荐 20)。
CHECKOUT_STALE_ALERT_COMMITS=0
# CHECKOUT_STALE_ALERT_TITLE="[daemon] 主 checkout 长期落后 origin"
# ── 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"