pi-claude-supervisor 0.8.1 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,21 @@
2
2
 
3
3
  All notable changes to this project will be documented here.
4
4
 
5
+ ## [0.9.1](https://github.com/btnalit/pi-claude-supervisor/compare/v0.9.0...v0.9.1) (2026-09-24)
6
+
7
+
8
+ ### Bug Fixes
9
+
10
+ * long-task stability findings from an independent 0.9.0 review ([#62](https://github.com/btnalit/pi-claude-supervisor/issues/62)) ([35c6789](https://github.com/btnalit/pi-claude-supervisor/commit/35c678962c357bc188eb2ed82141e4046ae793aa))
11
+ * **review:** pin Reviewer finding key order so quoted text cannot swap a P0's description ([#64](https://github.com/btnalit/pi-claude-supervisor/issues/64)) ([6df3fd2](https://github.com/btnalit/pi-claude-supervisor/commit/6df3fd2b75a9555eb6dc9112b1c19f5e35bfa3cd))
12
+
13
+ ## [0.9.0](https://github.com/btnalit/pi-claude-supervisor/compare/v0.8.1...v0.9.0) (2026-09-20)
14
+
15
+
16
+ ### Features
17
+
18
+ * **supervisor:** publish a verified candidate under a narrow, one-shot remote grant ([5e117cc](https://github.com/btnalit/pi-claude-supervisor/commit/5e117cc5695492d03684270ea25cac224f1fe41e))
19
+
5
20
  ## [0.8.1](https://github.com/btnalit/pi-claude-supervisor/compare/v0.8.0...v0.8.1) (2026-09-20)
6
21
 
7
22
 
package/README.cn.md CHANGED
@@ -202,12 +202,75 @@ stream-json` 的方式运行 Claude,完全没有终端界面;一旦设置
202
202
  时间。只有收尾窗口也耗尽,Worker 才会被硬停(`worker_watchdog_timeout`);对
203
203
  接管的交互式会话来说这个硬停只是 release:Claude 继续运行,但不再受监督。
204
204
  收尾窗口只属于自动模式任务;手动任务仍在到期时停止,`DEADLINE_GRACE_MS=0`
205
- 让自动任务也恢复这一行为。20 分钟无输出 watchdog(`NO_OUTPUT_TIMEOUT_MS`)
206
- 随时会停止沉默的 Worker。
205
+ 让自动任务也恢复这一行为。20 分钟无输出 watchdog(`NO_OUTPUT_TIMEOUT_MS`,
206
+ 从 Worker 最后一次输出或 Supervisor 最后一次发给它的消息起算)会停止在一轮
207
+ 中途沉默的 Worker;自动模式下只是空闲了这么久的 Worker(在等永远没回来的
208
+ 后台工作)则改为直接验收(`worker_idle_timeout`),人工接管中的 Worker 不会
209
+ 因沉默超时。
207
210
  - 验收命令、证据收集和 Reviewer 共用一个 abort signal,因此 stop 或 shutdown
208
211
  不必等待完整的命令或模型超时。
209
212
  - 每个任务只持有一个 cwd 租约;并发任务需要各自独立的 worktree。
210
213
 
214
+ ## 发布已验证的候选
215
+
216
+ 默认情况下任务止于**已验证的本地候选**:验收与独立 Reviewer 通过,而 Worker 全程
217
+ 没有任何远程权限。设 `REMOTE_AUTHORITY=push`(或 `--remote push`)会在该判定之后
218
+ 加一个**发布阶段**:Supervisor 记下已验证的 commit,发放一次性的窄授权,并让
219
+ Worker 推自己的分支;`pr` 还允许它开 PR。**push 由 Worker 自己执行**——Supervisor
220
+ 从不代劳——之后 Supervisor 以只读方式核实(`git ls-remote`,`pr` 还查 `gh pr list`)
221
+ 才把任务标记完成,候选通知里带上 PR 链接。核实不到则把候选标为 blocked,本地候选
222
+ 依然可交付。
223
+
224
+ 这个授权刻意严苛,而且两条命令都按字面匹配——没被审过的选项一律拒绝,而不是默认
225
+ 无害。它只认 `git -C '<任务目录>' -c core.hooksPath=/dev/null -c push.followTags=false push <remote> <已验证 commit>:refs/heads/<branch>`,
226
+ 不带其他任何选项。refspec 写的是**已验证的 commit** 而不是分支:git 只会推送这一个对象,
227
+ Worker 在发布轮里再提交的内容会留在本地("Everything up-to-date"),搭不上这次授权。
228
+ **`-C` 是必需的、必须是绝对路径、且必须与任务目录逐字节相同**——不做规范化、不做 realpath——因为 Claude 的 Bash 工具
229
+ 会在多次调用之间保留工作目录,而 `cd` 属于普通本地操作,没有 `-C` 的话授权可能被花在
230
+ 任何别的克隆上;任何更宽松的比较都出现过 Supervisor 与 git 解析不一致的写法(相对路径 `.` 按 Supervisor 的目录解析、`/proc/self/cwd`,以及 `<cwd>/link/..`——Node 自己的 realpath 会按字面折叠而内核会跟随 symlink)。指令写的就是精确目录,不需要接受任何别的写法。
231
+ 这条命令上**钉死了 hooks 路径**,所以 Worker 通过任何途径(`git init --template=`、
232
+ 解压归档、`chmod`)装进去的 `pre-push` hook 都不会在授权的 push 里以 Worker 的凭据执行;也钉死了
233
+ `push.followTags=false`,所以通过策略看不见的任何文件设的 `followTags=true` 都不能让这一次 push
234
+ 顺带推上授权没点名的 tag(tag 正是发布自动化的触发点)。两者都是 ref/hook 选择而非传输层,不会
235
+ 覆盖任何合理的仓库级设置。
236
+ `pr` 下另加 `gh pr create --repo <钉住的 remote URL> --head <候选分支> …`(只允许
237
+ title / body / base / draft / assignee / label):PR 只会开在授权 remote 对应的仓库里——
238
+ 不带 `--repo` 的话,gh 会从 remotes 里自己挑一个 base 仓库(fork 上是 `upstream`),
239
+ 那不是授权点名的仓库,核实也不会去查它。仓库取 remote URL 背后的 `host/owner/repo`
240
+ (SSH config 里的 host 别名会像 gh 那样经 `ssh -G` 翻译);URL 不是仓库的 remote(本地路径、
241
+ 翻译不了的别名)不会拿到 `pr` 授权。授权提供的每个词都做了 shell 引用,分支叫 `feat/$ticket`
242
+ 也能原样通过策略。
243
+
244
+ 授权只会发给"就是已验证工作树"的那个 commit:工作树必须干净(含未跟踪文件——新文件也可能
245
+ 是被验证行为的一部分),且 HEAD 自 Reviewer 评审的证据被读取以来没有移动过。仓库的 Git 目录必须是自己的 `.git` 或 linked worktree 的 `.git/worktrees/<name>`(不能是 `--separate-git-dir` 指针)。工作树不干净
246
+ 会先花一轮修复让 Worker 把属于候选的内容提交掉;只有修复轮用尽或 HEAD 移动过,任务才以本地
247
+ 候选结束并在通知里写明 `not published:` 原因,而不是发授权。核实时 remote 连不上,发布只是
248
+ "未确认"(候选仍可交付),绝不会被说成"没推上去"。
249
+
250
+ 有没有授权都拒绝:任何 push 选项(`-u`、`--force`、`--force-with-lease`、`--delete`、
251
+ `--mirror`、`--all`、`--tags`、`--no-verify`、`--push-option`、`--receive-pack` 等)、
252
+ 除这两个钉死项(且顺序固定)以外的任何 `-c`、以分支或 `HEAD` 作为 refspec 来源、裸 `git push`、
253
+ 别的 remote、分支或 commit、保护分支、被 shell 包装(含 heredoc 管进 shell)、带动态参数、
254
+ 第二条语句、不带 `-C` 的 `git push`、`-C` 与任务目录不逐字节相同(别的目录、相对路径、`/proc/self/cwd`、其中的 symlink 或 `..`)、不带
255
+ `--repo <钉住的 URL>` 或不带 `--head <候选分支>` 的 `gh pr create`(被别的选项当作值吞掉的
256
+ `--head` 不算)、`gh pr create --body-file/-F/--template`(会把任意本地文件内容发到 PR 上)、
257
+ `--web`、别的 `--repo`、`gh pr merge`、`gh api`、`gh release`、`npm publish`。改动仓库
258
+ remote(`git remote set-url|add|rename|…`,藏在 git 自己的 `--git-dir`/`--work-tree` 选项
259
+ 或 `remote` 自己的 `-v` 后面也一样)一律拒绝;改动 push 去向或 push 期间会执行什么也一律
260
+ 拒绝——`git config` 写 `remote.*`、`url.*.insteadOf`、`push.*`、`credential.*`、`http.*`、
261
+ `include.path`/`includeIf.*`、`init.*`、`core.sshCommand`、`core.hooksPath`,`git config --edit`、
262
+ `git init --template=…`、`git init|clone --separate-git-dir=…`,以及任何提到 `.git/config` 或 `.git/hooks` 的语句(除非它显然只读:
263
+ `cat`、`grep`、`ls` 等)——否则授权认的 remote 会被偷换,连 Supervisor 的核实也会被骗过;
264
+ 核实同时钉住 fetch 和 push 两个 URL,并从自己的环境里剔除 `GIT_DIR`/`GIT_CONFIG_*`。这些文本规则之下还有一条不依赖它们的规则:remote 解析后的**全部** fetch/push URL(`git remote get-url --all` / `--push --all`;git 会推到每一个 `pushurl`,而不只是它打印的第一个;已应用改写)在任务**开始**、Worker 还没跑任何命令时记录,并在发放授权时和授权的 push 被批准的那一刻都要求原样不变。任务期间用任何手段(`~/.gitconfig`、脚本、策略看不见的 include)、哪怕在发布轮的第一条命令里加进来的 `pushInsteadOf`、`pushurl` 或额外目的地,都会让 push 被拒、授权作废;操作者原本就有的改写已在基线里,不受影响。恢复的任务沿用记录的基线、绝不重新采集;首次启动时解析不到的 remote 会被记录下来,永不授权。这是一层
265
+ 作用于命令文本的策略:Worker 自己写一个脚本再运行,策略看不见——如
266
+ [autonomy-target.md](docs/autonomy-target.md) 对所有文本级规则所说,绝对隔离是 host 边界的事。
267
+
268
+ 授权是**一次性**的:发布轮一结束就收回(不等下一个决策),并且同时绑定 remote 的 URL
269
+ 而不只是名字。Worker 若在该轮里改动了工作树(未提交的修改同样算改动),授权立即作废并重新完整验收;因为授权写的是
270
+ commit,通知能说清已验证的 commit 是否在工作树变动之前就已经推上去了。验证之前
271
+ 被拒的 push 会说明授权稍后会来,而不是让 Worker 去猜;没能确认发布的任务会在候选
272
+ 通知里写明原因,而不是只报一句 "ready"。
273
+
211
274
  ## 任务 spec
212
275
 
213
276
  `--spec file.json` 接受如下格式:
@@ -225,7 +288,7 @@ stream-json` 的方式运行 Claude,完全没有终端界面;一旦设置
225
288
  "autonomy": {
226
289
  "unattended": true,
227
290
  "requireLocalCommit": true,
228
- "maxDecisionRetries": 2,
291
+ "maxDecisionRetries": 4,
229
292
  "permissionAuthority": "hybrid",
230
293
  "maxWorkerCostUsd": 20
231
294
  }
@@ -238,7 +301,9 @@ stream-json` 的方式运行 Claude,完全没有终端界面;一旦设置
238
301
  ## 配置参考
239
302
 
240
303
  环境变量(或 `~/.config/pi-claude-supervisor/env`),均以 `PI_CLAUDE_SUPERVISOR_`
241
- 为前缀;完整模板见 `.env.example`。
304
+ 为前缀;完整模板见 `.env.example`。env 文件每行是 `KEY=value`,可以带 `export `
305
+ 前缀和行尾 ` # 注释`。数值、时长或布尔类配置超出范围或无法解析时会沿用默认值,
306
+ 并以 `pi-claude-supervisor: ignoring …` 警告报告一次。
242
307
 
243
308
  | 变量 | 默认值 | 含义 |
244
309
  | --- | --- | --- |
@@ -261,8 +326,10 @@ stream-json` 的方式运行 Claude,完全没有终端界面;一旦设置
261
326
  | `HUMAN_WEBHOOK_SECRET` | 未设置 | HMAC 签名密钥;以 `x-pi-supervisor-signature` header 发送 |
262
327
  | `UNATTENDED` | `true` | 任务无需同步人工回调即可运行 |
263
328
  | `REQUIRE_LOCAL_COMMIT` | `true` | 完成前要求在候选所在分支上有本地 commit |
264
- | `MAX_DECISION_RETRIES` | `2`(0–10) | Decision Worker 调用超时或失败(429/529、网络、鉴权)时的重试次数 |
329
+ | `MAX_DECISION_RETRIES` | `4`(0–10) | Decision Worker 调用超时或失败(429/529、网络、鉴权)时的重试次数;两次尝试之间依次等待 15s、45s、60s |
265
330
  | `PERMISSION_AUTHORITY` | `hybrid` | `policy` \| `hybrid` \| `decision-worker` |
331
+ | `REMOTE_AUTHORITY` | `none` | `none` \| `push` \| `pr`;验收通过后开启发布阶段。`--remote` 可按任务覆盖 |
332
+ | `REMOTE_NAME` | `origin` | 发布授权唯一允许的 remote 名 |
266
333
  | `WORKER_MAX_BUDGET_USD` | 未设置 | 作为 `--max-budget-usd` 传入的硬上限;交互式 tmux 下不可用 |
267
334
  | `WORKER_MODEL` | 未设置(Claude 自身默认值) | Claude Worker 的 `--model` |
268
335
  | `WORKER_AUTOCOMPACT_TOKENS` | 自动模式默认 `200000` | 每轮上下文上限;`0` 保留 Claude 自身默认值 |
@@ -274,8 +341,8 @@ stream-json` 的方式运行 Claude,完全没有终端界面;一旦设置
274
341
  | `DECISION_SESSION_RETENTION_DAYS` | `30` | 启动时清理早于此天数的已关闭 Decision Worker session 记录;`0` 表示永久保留 |
275
342
  | `EVIDENCE_MAX_BYTES` | `1048576`(1 MiB) | 每个任务收集的最大仓库证据字节数 |
276
343
  | `EVIDENCE_MAX_UNTRACKED_FILES` | `512` | 每个任务作为证据收集的最大未跟踪文件数 |
277
- | `REVIEW_TIMEOUT_MS` | `600000`(10 分钟) | 每轮独立 Reviewer 的总预算,含一次针对 provider 错误的重试 |
278
- | `DEADLINE_MS` | `4h` | 每个任务的累计总时限(`8h`、`90m`、`2h30m` 或毫秒;5 分钟到 7 天);`0`(或 `0m`)关闭;`--deadline` 可按任务覆盖 |
344
+ | `REVIEW_TIMEOUT_MS` | `10m`(30s–1h) | 每轮独立 Reviewer 的总预算;预算未用完时,provider 错误会用新会话重试 |
345
+ | `DEADLINE_MS` | `4h` | 每个任务从启动起算的墙钟总时限,Pi 停机期间也计入,因此恢复时用 `recover --extend` 重新给预算(`8h`、`90m`、`2h30m` 或毫秒;5 分钟到 7 天);`0`(或 `0m`)关闭;`--deadline` 可按任务覆盖 |
279
346
  | `DEADLINE_GRACE_MS` | `30m` | 自动任务到期后的收尾窗口:空闲的 Worker 会被验收而不是停止;`0` 恢复到期立即停止 |
280
347
  | `DEADLINE_WARNING_MS` | `15m` | 到期前多久提醒并重新询问 Decision Worker;`0` 关闭提醒 |
281
348
  | `NO_OUTPUT_TIMEOUT_MS` | `20m` | Worker 多久没有输出就停止;`0` 关闭该检查 |
@@ -296,7 +363,10 @@ Worker,它不会静默恢复或重复执行任务。只有在租约证明旧 Wor
296
363
  会拒绝它;`recover --takeover --extend <duration> <task-id>` 从现在起再给这么
297
364
  多预算(恢复后的 Supervisor 会把新时限持久化),`--extend 0` 则立即进入收尾:
298
365
  新 Worker 的第一个 watchdog tick 就会对仓库现状做验收和 review,修复轮会告诉
299
- 它还剩多少时间。确定不再恢复的记录用 `/supervise discard <task-id>` 丢弃
366
+ 它还剩多少时间。带 `--extend` 时,恢复的任务会直接交回自动化:非零的延长会
367
+ 给新 Worker 发送原任务的续做指令(先让它查看已有的工作),`--extend 0` 则无需
368
+ 指令。不带 `--extend` 的普通 `recover` 仍让 Worker 在人工接管下空闲——先发送
369
+ 续做指令,再执行 `resume-auto`。确定不再恢复的记录用 `/supervise discard <task-id>` 丢弃
300
370
  (会话文件保留到保留期清理为止)。
301
371
 
302
372
  每个任务在 `CWD_LEASE_DIR` 下持有一个 cwd 租约;并发任务需要各自独立的
package/README.md CHANGED
@@ -247,14 +247,119 @@ Supervisor being able to see it, or when you don't need to attach.
247
247
  is a release: Claude keeps running, unsupervised. The close-out belongs to
248
248
  automatic tasks; a manual task is stopped at the deadline as before, and
249
249
  `DEADLINE_GRACE_MS=0` restores that for automatic ones too. A 20-minute
250
- no-output watchdog (`NO_OUTPUT_TIMEOUT_MS`) still stops a silent Worker at
251
- any time.
250
+ no-output watchdog (`NO_OUTPUT_TIMEOUT_MS`, counted from the Worker's last
251
+ output or the Supervisor's last message to it) stops a Worker that falls
252
+ silent mid-turn; an automatic Worker that is merely idle that long (waiting on
253
+ background work that never came back) is verified instead
254
+ (`worker_idle_timeout`), and a Worker under human takeover is never timed out.
252
255
  - Acceptance checks, evidence collection, and the Reviewer share an abort
253
256
  signal, so a stop or shutdown does not wait for a full command or model
254
257
  timeout.
255
258
  - Only one cwd lease is held per task; concurrent tasks need separate
256
259
  worktrees.
257
260
 
261
+ ## Publishing a verified candidate
262
+
263
+ By default a task ends at a **verified local candidate**: acceptance and the
264
+ independent Reviewer pass, and the Worker never had remote authority at any
265
+ point. `REMOTE_AUTHORITY=push` (or `--remote push`) adds a **publish phase**
266
+ after that verdict: the Supervisor records the verified commit, grants a narrow
267
+ one-shot authority, and asks the Worker to push its own branch. `pr` also lets
268
+ it open a pull request. The Worker performs the push — the Supervisor never
269
+ does — and the Supervisor then confirms it read-only (`git ls-remote`, and
270
+ `gh pr list` for `pr`) before completing the task; the candidate notice carries
271
+ the pull request URL. A publish that cannot be confirmed blocks the candidate,
272
+ which stays deliverable locally.
273
+
274
+ The grant is deliberately unforgiving, and both commands are matched literally —
275
+ an option nobody reviewed is refused rather than assumed harmless. It admits
276
+ exactly `git -C '<task directory>' -c core.hooksPath=/dev/null -c push.followTags=false push <remote> <verified commit>:refs/heads/<branch>`,
277
+ with no other option. The refspec names the **verified commit**, not the branch:
278
+ git pushes exactly that object, so a commit the Worker makes during the publish
279
+ turn stays local ("Everything up-to-date") instead of riding the grant. `-C` is
280
+ **required, absolute and byte for byte the task directory** — no normalization,
281
+ no realpath — because Claude's Bash tool keeps its working directory between
282
+ calls and `cd` is ordinary local work, so without it the grant could be spent
283
+ in any other clone, and every looser comparison had a spelling the Supervisor
284
+ resolved one way and git another (`.` against the Supervisor's cwd,
285
+ `/proc/self/cwd`, and `<cwd>/link/..`, which Node's own realpath collapses
286
+ lexically while the kernel follows the link). The instruction spells the exact
287
+ directory, so no other spelling is needed. The hooks path is **pinned** on that one command so no
288
+ `pre-push` hook a Worker could have installed (by any door: `git init
289
+ --template=`, an archive, a `chmod`) runs inside the granted push with the
290
+ Worker's credentials, and `push.followTags=false` is pinned so a
291
+ `followTags=true` set through any file the policy never sees cannot make the
292
+ one push also plant a tag the grant never named (a tag is what release
293
+ automation keys on). Both are ref and hook selection, not transport, so they
294
+ override no legitimate per-repository setting. For `pr` a `gh pr create --repo <pinned remote URL>
295
+ --head <candidate branch> …` limited to title, body, base, draft, assignee and
296
+ label: the pull request opens in the granted remote's repository, full stop —
297
+ without `--repo`, gh picks a base repository from the remotes (`upstream` on a
298
+ fork) that the grant never named and the confirmation never reads. The
299
+ repository is the `host/owner/repo` behind the remote's URL (an SSH-config host
300
+ alias is translated through `ssh -G`, as gh does); a remote whose URL is not
301
+ one — a local path, an alias with no translation — gets no `pr` grant. Every
302
+ word the grant supplies is shell-quoted, so a branch named `feat/$ticket` still
303
+ round-trips through the policy.
304
+
305
+ The grant is only issued for a commit that *is* the verified tree: the working
306
+ tree must be clean (untracked files included — a new file may be part of the
307
+ verified behavior), and HEAD must not have moved since the evidence the Reviewer
308
+ judged was read. The repository's Git directory must be its own `.git` or a linked worktree's
309
+ `.git/worktrees/<name>` (not a `--separate-git-dir` pointer). A dirty tree first costs a repair round asking the Worker to
310
+ commit what belongs to the candidate; only when none is left, or when HEAD
311
+ moved, does the task end at the local candidate with a `not published:` reason
312
+ instead of a grant. A remote that cannot be reached at confirmation time leaves
313
+ the publish *unconfirmed* (the candidate stays deliverable), never "refuted".
314
+
315
+ Refused with or without a grant: every push option (`-u`, `--force`,
316
+ `--force-with-lease`, `--delete`, `--mirror`, `--all`, `--tags`, `--no-verify`,
317
+ `--push-option`, `--receive-pack`, …), any `-c` but the two pins (in that order), a
318
+ branch or `HEAD` as the refspec source, a bare `git push`, another remote,
319
+ branch or commit, a protected branch, a shell wrapper (`sh -c`, and a heredoc
320
+ piped into a shell), a dynamic word, a second statement, `git push` without
321
+ `-C`, a `-C` that is not the task directory byte for byte (another directory, a
322
+ relative path, `/proc/self/cwd`, a symlink or `..` inside it),
323
+ `gh pr create` without `--repo <pinned URL>` or without `--head <candidate
324
+ branch>` (a `--head` swallowed as another option's value does not count),
325
+ `gh pr create --body-file/-F/--template` (which would post the contents of an
326
+ arbitrary local file), `--web`, another `--repo`, `gh pr merge`, `gh api`,
327
+ `gh release` and `npm publish`. Changing the repository's remotes (`git remote
328
+ set-url|add|rename|…`, behind git's own `--git-dir`/`--work-tree` options or
329
+ `remote`'s own `-v` too) is denied outright, and so is reconfiguring where a
330
+ push goes or what runs during it — `git config` writes to `remote.*`,
331
+ `url.*.insteadOf`, `push.*`, `credential.*`, `http.*`, `include.path`/
332
+ `includeIf.*`, `init.*`, `core.sshCommand` or `core.hooksPath`, `git config
333
+ --edit`, `git init --template=…`, `git init|clone --separate-git-dir=…`, and any statement that names `.git/config` or
334
+ `.git/hooks` unless it plainly only reads (`cat`, `grep`, `ls`, …) — so the
335
+ granted remote cannot be repointed underneath the confirmation, which pins both
336
+ the fetch and the push URL and scrubs `GIT_DIR`/`GIT_CONFIG_*` from its own
337
+ environment. Behind all of that sits one rule the text guards do not need:
338
+ the remote's resolved fetch and push URLs — **every** one of them
339
+ (`git remote get-url --all` / `--push --all`; git pushes to each `pushurl`,
340
+ not only the first it prints), rewrites applied — are recorded when the task
341
+ **starts**, before the Worker runs a command, and required unchanged both
342
+ when the grant is issued *and* at the moment the granted push is authorized.
343
+ So a `pushInsteadOf`, `pushurl` or extra destination added during the task by
344
+ *any* means (`~/.gitconfig`, a script, an include the policy never saw), even
345
+ as the first command of the publish turn, refuses the push and revokes the
346
+ grant, while an operator's pre-existing rewrite, already in the baseline, is
347
+ not. A recovered task keeps its recorded baseline and never takes a new one;
348
+ a remote that could not be resolved at the first start is recorded as such and
349
+ never granted. This is a policy over the command text: a script the Worker writes
350
+ and runs is outside what it can see, as [autonomy-target.md](docs/autonomy-target.md)
351
+ says of every text-level rule; absolute isolation is the host boundary's job.
352
+
353
+ The grant is **one-shot**: it is revoked the moment the publish turn completes,
354
+ not when the next decision arrives, and it is pinned to the remote's URL as well
355
+ as its name. A Worker that changes the tree during that turn voids it and is
356
+ re-verified in full — an edit left uncommitted counts as a change, exactly like a new
357
+ commit; because the grant named the commit, the notice can say whether the
358
+ verified commit landed before the tree moved on. Before verification a refused push says the grant is coming
359
+ rather than leaving the Worker to guess, and a task that ends without a
360
+ confirmed publish says so in its candidate notice instead of reporting a bare
361
+ "ready".
362
+
258
363
  ## Task specs
259
364
 
260
365
  `--spec file.json` accepts:
@@ -272,7 +377,7 @@ Supervisor being able to see it, or when you don't need to attach.
272
377
  "autonomy": {
273
378
  "unattended": true,
274
379
  "requireLocalCommit": true,
275
- "maxDecisionRetries": 2,
380
+ "maxDecisionRetries": 4,
276
381
  "permissionAuthority": "hybrid",
277
382
  "maxWorkerCostUsd": 20
278
383
  }
@@ -286,7 +391,11 @@ check (120s timeout) and the env autonomy defaults below.
286
391
  ## Configuration reference
287
392
 
288
393
  Environment variables (or `~/.config/pi-claude-supervisor/env`), all prefixed
289
- `PI_CLAUDE_SUPERVISOR_`; see `.env.example` for a template.
394
+ `PI_CLAUDE_SUPERVISOR_`; see `.env.example` for a template. The env file takes
395
+ `KEY=value` lines, optionally prefixed with `export ` and followed by a
396
+ ` # comment`. A numeric, duration or boolean value that is out of range or
397
+ unparsable keeps the default and is reported once as a
398
+ `pi-claude-supervisor: ignoring …` warning.
290
399
 
291
400
  | Variable | Default | Meaning |
292
401
  | --- | --- | --- |
@@ -309,8 +418,10 @@ Environment variables (or `~/.config/pi-claude-supervisor/env`), all prefixed
309
418
  | `HUMAN_WEBHOOK_SECRET` | unset | HMAC signing secret; sent as the `x-pi-supervisor-signature` header |
310
419
  | `UNATTENDED` | `true` | Task runs without a synchronous human callback |
311
420
  | `REQUIRE_LOCAL_COMMIT` | `true` | Require a local commit on the candidate's branch before completion |
312
- | `MAX_DECISION_RETRIES` | `2` (0–10) | Retries of a Decision Worker call that times out or fails (429/529, network, auth) |
421
+ | `MAX_DECISION_RETRIES` | `4` (0–10) | Retries of a Decision Worker call that times out or fails (429/529, network, auth); waits 15s, 45s, then 60s between attempts |
313
422
  | `PERMISSION_AUTHORITY` | `hybrid` | `policy` \| `hybrid` \| `decision-worker` |
423
+ | `REMOTE_AUTHORITY` | `none` | `none` \| `push` \| `pr`; grants the publish phase after verification passes. `--remote` overrides it per task |
424
+ | `REMOTE_NAME` | `origin` | The single remote a publish grant may name |
314
425
  | `WORKER_MAX_BUDGET_USD` | unset | Hard cap passed as `--max-budget-usd`; unavailable to interactive tmux |
315
426
  | `WORKER_MODEL` | unset (Claude's own default) | `--model` for the Claude Worker |
316
427
  | `WORKER_AUTOCOMPACT_TOKENS` | `200000` in automatic mode | Per-turn context bound; `0` keeps Claude's own default |
@@ -322,8 +433,8 @@ Environment variables (or `~/.config/pi-claude-supervisor/env`), all prefixed
322
433
  | `DECISION_SESSION_RETENTION_DAYS` | `30` | Prunes closed Decision Worker session records older than this; `0` keeps forever |
323
434
  | `EVIDENCE_MAX_BYTES` | `1048576` (1 MiB) | Maximum repository evidence bytes collected per task |
324
435
  | `EVIDENCE_MAX_UNTRACKED_FILES` | `512` | Maximum untracked files collected as evidence per task |
325
- | `REVIEW_TIMEOUT_MS` | `600000` (10 min) | Total independent Reviewer budget per round, including one retry on a provider error |
326
- | `DEADLINE_MS` | `4h` | Cumulative wall-clock budget per task (`8h`, `90m`, `2h30m` or ms; 5m–7d); `0` (or `0m`) disables it; `--deadline` overrides it per task |
436
+ | `REVIEW_TIMEOUT_MS` | `10m` (30s–1h) | Total independent Reviewer budget per round; a provider error is retried with a fresh session while budget remains |
437
+ | `DEADLINE_MS` | `4h` | Wall-clock budget per task, measured from its start — time Pi was down counts too, so `recover --extend` grants a fresh budget (`8h`, `90m`, `2h30m` or ms; 5m–7d); `0` (or `0m`) disables it; `--deadline` overrides it per task |
327
438
  | `DEADLINE_GRACE_MS` | `30m` | Close-out window after the deadline for automatic tasks: an idle Worker is verified instead of stopped; `0` restores the immediate stop |
328
439
  | `DEADLINE_WARNING_MS` | `15m` | How long before the deadline the Decision Worker is warned and re-asked; `0` disables the warning |
329
440
  | `NO_OUTPUT_TIMEOUT_MS` | `20m` | Stop a Worker that has produced no output for this long; `0` disables the check |
@@ -348,7 +459,11 @@ A task that stopped at its wall-clock deadline is listed with `deadline=expired
348
459
  <task-id>` grants that much budget from now (the recovered Supervisor persists
349
460
  the new deadline), and `--extend 0` opens the close-out at once, so the fresh
350
461
  Worker's first watchdog tick verifies and reviews the repository as it stands
351
- and any repair round tells it how long it has. A record nobody will recover is
462
+ and any repair round tells it how long it has. With `--extend` the recovered
463
+ task goes straight back to automation: a real extension sends the fresh Worker a
464
+ continuation of the original task (telling it to inspect the earlier work first),
465
+ and `--extend 0` needs none. A plain `recover` still leaves the Worker idle under
466
+ takeover — send it a continuation, then `resume-auto`. A record nobody will recover is
352
467
  dropped with `/supervise discard <task-id>` (its session file is kept until
353
468
  retention pruning).
354
469
 
@@ -143,7 +143,8 @@ start); credentials are not copied into a file, and credential-shaped command
143
143
  arguments are still rejected.
144
144
  `load-buffer`, bracketed `paste-buffer` and `send-keys Enter` provide the input
145
145
  boundary without interpolating a task into a shell command. C0/C1 terminal
146
- control bytes are rejected; CRLF is normalized to a newline. Automatic agents,
146
+ control bytes are neutralized (escape sequences removed, a lone CR becomes a newline,
147
+ other C0/C1 bytes become spaces); CRLF is normalized to a newline. Automatic agents,
147
148
  background tasks, plugins, MCP servers and nested Claude processes stay in the
148
149
  same cgroup and are cleaned with the Worker; they are intentionally not rejected
149
150
  or polled as a nested-process policy failure. The lexical Bash/file-tool policy
@@ -399,8 +400,11 @@ For Claude JSONL, the adapter tracks `activeRequests`, `lastInputAt` and
399
400
  `lastOutputAt`. A `result` record closes an active request; malformed output does
400
401
  not. JSONL sends are rejected while a request is active, and a valid terminal
401
402
  result moves the session to `waiting`; only then may the next turn be sent. A paused
402
- Worker does not consume its no-output budget; resume establishes a fresh no-output
403
- baseline while the cumulative wall-clock deadline remains active.
403
+ Worker does not consume its no-output budget; resume, and every message the
404
+ Supervisor sends, establishes a fresh no-output baseline while the cumulative
405
+ wall-clock deadline remains active. An idle automatic Worker that reaches the
406
+ no-output timeout is verified (`worker_idle_timeout`) rather than stopped, and a
407
+ Worker under human takeover is exempt.
404
408
 
405
409
  The wall-clock deadline is a budget, not a kill switch. The watchdog drives it
406
410
  through three phases, each recorded once per task: `worker_deadline_approaching`
@@ -440,6 +444,51 @@ derives from this task's cwd, which rejects a subagent transcript and any path
440
444
  naming another project. A root is honored before it exists (Claude creates the
441
445
  memory directory on first write) and through a symlinked ancestor.
442
446
 
447
+ When a task is granted remote authority (`autonomy.remoteAuthority`, default
448
+ `none`), verification does not end it. `#requestPublish` first checks that
449
+ HEAD *is* the verified tree — the evidence the Reviewer judged shows a clean
450
+ working tree and carries the same `head` — then issues a `RemoteGrant` naming
451
+ that commit, the candidate's own branch, the remote, the task directory and the
452
+ remote's repository (`host/owner/repo` from its fetch URL, an SSH alias
453
+ translated through `ssh -G`), and asks the Worker to publish: the Worker runs
454
+ the push and any `gh pr create`, the Supervisor never does. A dirty tree costs a
455
+ repair round first, the remote's resolved URL lists (`get-url --all`, both sides) must equal the baseline recorded at task start (`TaskContext.remoteBaseline`, persisted with the decision session, restored on recovery, never re-taken) — checked again by `#grantedRemoteChanged` at the moment a granted command is authorized, in both the PreToolUse and prompt-phase paths — the Git directory must be the task's own `.git` or a linked worktree's, and the reviewed evidence must carry a
456
+ HEAD (fail-closed); the grant is armed before the instruction is sent and
457
+ revoked only if the send failed before delivery (the turn counter tells). The instruction is built
458
+ by `publishCommand`/`pullRequestCommand` in `policy.ts`, beside the parser that
459
+ admits it, and a test round-trips one through the other. Under every permission
460
+ authority the granted command is answered by the policy (`PolicyResult.granted`)
461
+ rather than escalated to the Decision Worker. The returning turn skips
462
+ acceptance and the Reviewer when HEAD is unchanged — they already passed on that
463
+ tree — and `#settlePublish` confirms the result read-only (`#confirmPublish`:
464
+ both pinned remote URLs unchanged, `git ls-remote` carrying the verified commit,
465
+ plus `gh pr list` for `pr`) before completing, or blocks the candidate when it
466
+ cannot; that candidate keeps `deliverable: true`, since it passed and is intact
467
+ on its branch, and an unreachable remote is reported as *unconfirmed*
468
+ (`RemoteBranchLookup` tells `absent` from `unreachable`), never as a missing
469
+ commit or a repointed remote. A tree that changed during the publish turn — an uncommitted edit included — voids
470
+ the grant and is re-verified in full, with the same confirmation deciding whether the notice
471
+ says the verified commit landed first. The grant is cleared on every terminal
472
+ path, so it never outlives the turn it was issued for, and
473
+ `permittedRemoteCommand` admits a single literal shape —
474
+ `git -C '<task dir>' -c core.hooksPath=/dev/null -c push.followTags=false push <remote> <commit>:refs/heads/<branch>`
475
+ with no other option, and `gh pr create --repo <pinned URL> --head <branch> …`
476
+ — so no force, delete, mirror, tags, push-options, other `-c`, branch or `HEAD`
477
+ source, other remote, branch or repository, relative `-C`, shell wrapper,
478
+ dynamic word or second statement; the pinned hooks path keeps any installed
479
+ `pre-push` out of the granted command and the pinned `push.followTags=false` keeps any tag out of it. `git config` writes to
480
+ transport-affecting keys (including `include.*` and `init.*`), `git config
481
+ --edit`, `git init --template`, and any statement naming `.git/config` or
482
+ `.git/hooks` in any spelling (`namesGitMetadata` normalizes the path and matches a glob segment by segment, so only a
483
+ segment that could expand to `.git` counts — a project's own `src/hooks/` is ordinary work) that does not plainly only read are refused alongside `git remote`
484
+ mutations; git's own `--git-dir`/`--work-tree` options and `remote`'s own `-v`
485
+ cannot hide either, nor can `-C /proc/self/cwd` or `-C <cwd>/link/..` (the directory must be the granted one byte for byte; every realpath comparison elsewhere uses the native implementation, since Node's JavaScript `realpathSync` collapses `link/..` lexically) or `--separate-git-dir`. The publish
486
+ hint keys on `PolicyResult.boundary`, not on the reason text, and promises a publish turn
487
+ only where `#requestPublish` will start one. For an adopted tmux session the memory write root is
488
+ located under the *adopted process's* configuration directory, read from
489
+ `/proc/<pid>/environ` at adoption, so a Claude started with another
490
+ `CLAUDE_CONFIG_DIR` keeps its memory.
491
+
443
492
  A record left behind by the outright
444
493
  stop (`recoverable_failure`, so `active/interrupted`) is not a dead end either:
445
494
  `recover --extend <duration>` re-persists a deadline measured from now
@@ -542,19 +591,36 @@ When automatic supervision is enabled, a successful check set is passed to a
542
591
  fresh read-only Reviewer session. The Reviewer receives the task specification, repository status/diff evidence,
543
592
  check results and bounded Worker completion evidence, but not the Decision Worker
544
593
  conversation or control channel. It can inspect only `read`, `grep`, `find` and `ls`, and must return
545
- `pass`, `revise` or `human` with bounded structured findings. Invalid Reviewer
594
+ `pass`, `revise` or `human` with bounded structured findings. Its whole reply
595
+ must be that one JSON object (an optional ```json fence aside), carrying a
596
+ random `reviewId` that appears only in its own prompt, with no key repeated and
597
+ nothing beyond the schema (a string `summary`, and `findings` as flat objects of
598
+ the finding fields with scalar values). Each finding opens with `severity`
599
+ then a non-empty `message` (an `id` may lead) and closes with `evidence` if it
600
+ has one — the only field the prompt allows repository quotes in. Repository
601
+ text it quotes or copies therefore cannot stand in for the answer, change its
602
+ verdict, or drop a finding it wrote, and a quote in `evidence` that closes a
603
+ finding early can only add findings after it — never change the severity,
604
+ message, fix or location the Reviewer already wrote. A quote the Reviewer puts
605
+ in any other field against the prompt can still reach the rest of that
606
+ finding. Added findings cannot unblock a candidate (any P0/P1 blocks, even
607
+ under `pass`), and repair instructions list findings most severe first so
608
+ added lesser ones cannot crowd out a blocking one. A reply that breaks any of
609
+ these earns one corrective re-prompt. Invalid Reviewer
546
610
  output, incomplete evidence or a Reviewer API failure must prevent a candidate
547
611
  from crossing the remote/main boundary; the local system may retry, repair or
548
- park it without requiring a human to be online. The Reviewer retries a provider
549
- error once within a total review budget
612
+ park it without requiring a human to be online. The Reviewer retries provider
613
+ errors with a fresh session within a total review budget
550
614
  (`PI_CLAUDE_SUPERVISOR_REVIEW_TIMEOUT_MS`, default 10 minutes). Truncated
551
615
  (oversize) evidence requests a bounded repair before parking, while incomplete
552
616
  evidence still parks.
553
617
 
554
618
  A `revise` result produces an audited repair round and sends a bounded corrective
555
619
  instruction to a still-live `repairableSession` Worker. Checks and review then run again.
556
- The repair budget defaults to three rounds; repeated findings and P0/P1 findings
557
- stop automation and park a non-publishable candidate. A Worker that has already exited cannot be silently recreated
620
+ The repair budget defaults to three rounds. P0/P1 findings block a `pass` but are repair
621
+ inputs like any other concrete finding (a `pass` carrying one is treated as `revise`); a
622
+ `human` verdict, repeated findings or an exhausted budget stop automation and park a
623
+ non-publishable candidate. A Worker that has already exited cannot be silently recreated
558
624
  for repair; it remains failed/recoverable rather than replaying the original task. If a repair
559
625
  or candidate branch cannot continue, a single idempotent terminalizer records
560
626
  `verification_failed`, closes the Decision Worker and reports cleanup evidence; it never performs
@@ -106,7 +106,7 @@ stop or park safely and retain evidence; it must not silently grant remote or ma
106
106
  Automatic mode implements the local loop: policy decisions allow ordinary local development,
107
107
  `AskUserQuestion` is converted to a denied interactive permission, the Decision Worker can
108
108
  continue/redirect/answer/repair, acceptance and independent Review run without a human callback,
109
- and unresolved situations become `blocked` candidates. The default task autonomy is unattended, requires a local commit, and permits two bounded
109
+ and unresolved situations become `blocked` candidates. The default task autonomy is unattended, requires a local commit, and permits four bounded
110
110
  Decision Worker request retries. Automatic startup rejects non-Git/detached/bare/protected
111
111
  repository states, malformed baselines, startup-HEAD races, the unstructured
112
112
  process-pipe transport, Bash-preauthorizing Claude arguments/settings and non-Claude or untrusted
package/docs/testing.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Testing
2
2
 
3
- > Autonomy target: local editing, testing, repair and local commits run without a human being online. Invalid output, unavailable evidence, duplicate findings, P0/P1 findings and exhausted budgets become parked/non-publishable candidates rather than synchronous human gates. Remote push and main/integration merge remain independent-boundary tests. See [autonomy-target.md](autonomy-target.md).
3
+ > Autonomy target: local editing, testing, repair and local commits run without a human being online. Invalid output (after one corrective re-prompt), unavailable evidence, duplicate findings, a `human` Reviewer verdict and exhausted budgets become parked/non-publishable candidates rather than synchronous human gates. Remote push and main/integration merge remain independent-boundary tests. See [autonomy-target.md](autonomy-target.md).
4
4
 
5
5
  ## Local checks
6
6
 
@@ -146,7 +146,7 @@ Pi Decision Worker. Setting `PI_CLAUDE_SUPERVISOR_TRANSPORT=tmux` selects the
146
146
  Supervisor-owned live bridge, which carries the same structured records through
147
147
  private framing on the PTY rather than an independent JSONL sidecar. Task autonomy
148
148
  defaults to unattended local work, a required
149
- local commit on the task branch (any branch, anchored to the baseline commit) and two bounded Decision Worker retries. Configure
149
+ local commit on the task branch (any branch, anchored to the baseline commit) and four bounded Decision Worker retries. Configure
150
150
  `PI_CLAUDE_SUPERVISOR_REQUIRE_LOCAL_COMMIT=0` or task `autonomy.requireLocalCommit`
151
151
  only to disable the local-commit deliverability check; automatic mode still requires a Git
152
152
  baseline (any branch, including `main`; the candidate must descend from it). The tmux bridge
@@ -240,7 +240,7 @@ Deterministic tests must cover:
240
240
  - multiple required/optional checks with bounded output, timeout and exit-code evidence;
241
241
  - independent read-only Reviewer pass/revise/human results;
242
242
  - invalid Reviewer JSON and Reviewer API failure becoming a parked/non-publishable candidate without requiring a live callback;
243
- - repair rounds, repeated finding detection, P0/P1 parking and repair-budget exhaustion;
243
+ - repair rounds (P0/P1 findings are repaired, never passed), repeated finding detection and repair-budget exhaustion;
244
244
  - non-persistent JSONL verification failure without duplicate terminal transitions;
245
245
  - repairable-but-not-persistent JSONL multi-turn repair;
246
246
  - stop and Pi shutdown from `verifying`, including Decision Worker closure and cwd lease release;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-claude-supervisor",
3
- "version": "0.8.1",
3
+ "version": "0.9.1",
4
4
  "description": "A policy-gated Pi supervisor for observing and verifying Claude Code workers.",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
package/src/acceptance.ts CHANGED
@@ -1,10 +1,12 @@
1
+ import { DEFAULT_MAX_DECISION_RETRIES } from "./config.ts";
2
+ import { isPlainRemoteName } from "./policy.ts";
1
3
  import type { AcceptanceCheck, TaskSpec } from "./types.ts";
2
4
 
3
5
  const DEFAULT_TIMEOUT_MS = 120_000;
4
6
  const DEFAULT_MAX_REPAIR_ROUNDS = 3;
5
7
 
6
8
  /** Normalize legacy plain-text tasks into the structured acceptance model. */
7
- export function normalizeTaskSpec(value: unknown, fallbackGoal: string): TaskSpec {
9
+ export function normalizeTaskSpec(value: unknown, fallbackGoal: string, autonomyDefaults?: Partial<TaskSpec["autonomy"]>): TaskSpec {
8
10
  if (value !== undefined && (!value || typeof value !== "object" || Array.isArray(value))) {
9
11
  throw new Error("task spec must be a JSON object");
10
12
  }
@@ -18,7 +20,7 @@ export function normalizeTaskSpec(value: unknown, fallbackGoal: string): TaskSpe
18
20
  forbidden: stringList(source.forbidden, "forbidden"),
19
21
  acceptance: normalizeChecks(source.acceptance),
20
22
  maxRepairRounds: normalizeRepairRounds(source.maxRepairRounds),
21
- autonomy: normalizeAutonomy(source.autonomy),
23
+ autonomy: normalizeAutonomy(source.autonomy, autonomyDefaults),
22
24
  };
23
25
  }
24
26
 
@@ -84,23 +86,32 @@ function normalizeRepairRounds(value: unknown): number {
84
86
  return value;
85
87
  }
86
88
 
87
- function normalizeAutonomy(value: unknown): TaskSpec["autonomy"] {
88
- if (value === undefined) return { unattended: true, requireLocalCommit: true, maxDecisionRetries: 2, permissionAuthority: "hybrid" };
89
+ function normalizeAutonomy(value: unknown, defaults?: Partial<TaskSpec["autonomy"]>): TaskSpec["autonomy"] {
90
+ // A spec file that omits a key — or the whole block — must not silently
91
+ // override the operator's environment defaults with hardcoded ones;
92
+ // `defaults` carries them in and every key falls back to it.
93
+ if (value === undefined) return normalizeAutonomy({}, defaults);
89
94
  if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("task spec autonomy must be an object");
90
95
  const source = value as Record<string, unknown>;
91
96
  if (source.unattended !== undefined && typeof source.unattended !== "boolean") throw new Error("task spec autonomy.unattended must be boolean");
92
97
  if (source.requireLocalCommit !== undefined && typeof source.requireLocalCommit !== "boolean") throw new Error("task spec autonomy.requireLocalCommit must be boolean");
93
- const retries = source.maxDecisionRetries ?? 2;
98
+ const retries = source.maxDecisionRetries ?? defaults?.maxDecisionRetries ?? DEFAULT_MAX_DECISION_RETRIES;
94
99
  if (typeof retries !== "number" || !Number.isSafeInteger(retries) || retries < 0 || retries > 10) throw new Error("task spec autonomy.maxDecisionRetries must be between 0 and 10");
95
- const authority = source.permissionAuthority ?? "hybrid";
100
+ const authority = source.permissionAuthority ?? defaults?.permissionAuthority ?? "hybrid";
96
101
  if (authority !== "policy" && authority !== "hybrid" && authority !== "decision-worker") throw new Error("task spec autonomy.permissionAuthority must be policy, hybrid or decision-worker");
97
- const maxWorkerCostUsd = source.maxWorkerCostUsd;
102
+ const remoteAuthority = source.remoteAuthority ?? defaults?.remoteAuthority ?? "none";
103
+ if (remoteAuthority !== "none" && remoteAuthority !== "push" && remoteAuthority !== "pr") throw new Error("task spec autonomy.remoteAuthority must be none, push or pr");
104
+ const remoteName = source.remoteName ?? defaults?.remoteName ?? "origin";
105
+ if (!isPlainRemoteName(remoteName)) throw new Error("task spec autonomy.remoteName must be a plain remote name");
106
+ const maxWorkerCostUsd = source.maxWorkerCostUsd ?? defaults?.maxWorkerCostUsd;
98
107
  if (maxWorkerCostUsd !== undefined && (typeof maxWorkerCostUsd !== "number" || !Number.isFinite(maxWorkerCostUsd) || maxWorkerCostUsd <= 0)) throw new Error("task spec autonomy.maxWorkerCostUsd must be a positive number");
99
108
  return {
100
- unattended: source.unattended !== false,
101
- requireLocalCommit: source.requireLocalCommit !== false,
109
+ unattended: (source.unattended ?? defaults?.unattended) !== false,
110
+ requireLocalCommit: (source.requireLocalCommit ?? defaults?.requireLocalCommit) !== false,
102
111
  maxDecisionRetries: retries,
103
112
  permissionAuthority: authority,
113
+ remoteAuthority,
114
+ remoteName,
104
115
  ...(maxWorkerCostUsd !== undefined ? { maxWorkerCostUsd } : {}),
105
116
  };
106
117
  }