@tt-a1i/openpi 0.1.1 → 0.2.0

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.
Files changed (43) hide show
  1. package/README.md +37 -22
  2. package/SETUP.md +8 -6
  3. package/extensions/ask-user/handoff.ts +5 -1
  4. package/extensions/ask-user/index.ts +44 -0
  5. package/extensions/background-terminals/index.ts +118 -29
  6. package/extensions/background-terminals/src/domain.ts +5 -1
  7. package/extensions/background-terminals/src/manager.ts +2 -1
  8. package/extensions/background-terminals/src/prompt.ts +35 -0
  9. package/extensions/background-terminals/src/result-delivery.ts +76 -3
  10. package/extensions/background-terminals/src/ui/tool-result.ts +52 -1
  11. package/extensions/capabilities/index.ts +198 -0
  12. package/extensions/context-pivot/index.ts +21 -0
  13. package/extensions/cron/index.ts +42 -15
  14. package/extensions/execution-convergence/active-evidence.ts +129 -0
  15. package/extensions/execution-convergence/index.ts +442 -0
  16. package/extensions/execution-convergence/workspace-provenance.ts +338 -0
  17. package/extensions/file-search/index.ts +8 -1
  18. package/extensions/file-search/src/binaries.ts +2 -1
  19. package/extensions/git-info/src/runtime.ts +1 -1
  20. package/extensions/goal/controller.ts +2 -1
  21. package/extensions/goal/index.ts +20 -1
  22. package/extensions/plan-mode/index.ts +12 -0
  23. package/extensions/setup/index.ts +93 -7
  24. package/extensions/shared/child-session.ts +40 -4
  25. package/extensions/shared/setup-config.ts +22 -0
  26. package/extensions/shared/setup-episode-state.ts +7 -0
  27. package/extensions/shared/tool-surface.ts +435 -0
  28. package/extensions/subagents/index.ts +15 -0
  29. package/extensions/subagents/src/manager.ts +13 -11
  30. package/extensions/subagents/src/prompt.ts +1 -1
  31. package/extensions/tasks/index.ts +39 -12
  32. package/extensions/ui-customization/footer.ts +6 -1
  33. package/extensions/workflows/graph-projection.ts +6 -4
  34. package/extensions/workflows/index.ts +16 -1
  35. package/extensions/workflows/invocation-ledger.ts +8 -2
  36. package/extensions/workflows/model.ts +5 -1
  37. package/extensions/workflows/prompt.ts +10 -40
  38. package/extensions/workflows/replay-safety.ts +9 -8
  39. package/package.json +10 -10
  40. package/skills/subagents/SKILL.md +6 -0
  41. package/skills/workflows/EXAMPLES.md +58 -0
  42. package/skills/workflows/REFERENCE.md +44 -0
  43. package/skills/workflows/SKILL.md +39 -0
package/README.md CHANGED
@@ -54,7 +54,7 @@ pi install npm:@tt-a1i/openpi
54
54
  OpenPI 会把长期进程放到后台,把独立任务交给隔离 Context 的 Pi Subagent,把多阶段依赖组织成 Workflow。状态会持续显示;完整运行可从 `/ps`、`/subagents` 和 `/workflows` 检查或终止。
55
55
 
56
56
  > [!IMPORTANT]
57
- > 默认安装是安静的:不改主题、不绑定 Provider 或模型、不开启下一步预测,也不执行 post-edit 命令。OpenPI 自有偏好统一通过 `/openpi-setup` 显式配置。
57
+ > 默认安装是安静的:不改主题、不绑定 Provider 或模型、不开启下一步预测,也不执行 post-edit 命令。Capability discovery 默认 `explicit`;只有用户通过 `/openpi-setup` 选择 `adaptive` 后,模型才会常驻看到一个小型发现网关并可自主加载额外能力。
58
58
 
59
59
  ```text
60
60
  /openpi-setup
@@ -144,6 +144,8 @@ bg_start({
144
144
 
145
145
  后台进程没有 stdin。需要交互输入的命令应由用户直接运行,而不是放进后台。
146
146
 
147
+ 前台执行沿用 Pi 的 Bash 合同:`timeout` 可选且没有统一默认值,是否设置以及设置多长由模型或用户按命令语义决定。OpenPI 不再通过正则改写测试命令 timeout;确实需要有界执行时应显式传入 timeout,长期运行的 build、test、migration 或 server 可使用 Background Terminal 的生命周期能力。
148
+
147
149
  ### Pi-native Subagent:隔离 Context,不另起系统
148
150
 
149
151
  ```text
@@ -284,7 +286,7 @@ Workflow 在清理隔离 checkout 前原子保存有界 Handoff Manifest:track
284
286
 
285
287
  | 能力 | 使用方式 | 它负责什么 |
286
288
  | ------------- | --------------------------------------- | ------------------------------------------------------------------- |
287
- | Tasks | `tasks_add` / `tasks_update` / `/tasks` | 跨 Agent Run 与用户回合记录当前批次工作意图;不执行工作 |
289
+ | Tasks | `tasks_add` / `tasks_update` / `/tasks` | 逐项同步当前批次工作意图并刷新完整快照;不推断完成、不执行工作 |
288
290
  | Goal | `/goal <目标>` | 驱动一个持续到终态的自主目标;完成前要求证据审计 |
289
291
  | Plan Mode | `/plan [目标]` | 只读调研;`plan_ready` 后才准备可编辑的实施 Prompt,不自动执行 |
290
292
  | Context Pivot | `/context-pivot <下一阶段>` | Context 超过约 30K Tokens 且任务换阶段时,用自包含 Brief 替换旧噪音 |
@@ -333,7 +335,7 @@ macOS/Linux arm64 与 x64 缺少二进制时,OpenPI 会从官方 Release 下
333
335
  | 终端输出 | 控制字符、方向格式符与超长内容在 ingress / render 边界清洗、限长 |
334
336
  | Shutdown | Terminal、Subagent、Workflow 都有有界取消、清理与唯一终态 |
335
337
  | 用户配置 | 单一受限 typed tool 写入;不散落扩展私有配置入口 |
336
- | 模型消费 | Suggestion 默认关闭;Subagent / Workflow 只在工具调用或用户 `/btw` 后运行 |
338
+ | 模型消费 | Suggestion 默认关闭;adaptive 仅在显式开启后允许模型自主加载能力 |
337
339
 
338
340
  可选的 [pi-intercom](https://github.com/nicobailon/pi-intercom) 只在顶层 Pi Session 加载。它使用进程级身份,而 OpenPI Child 是同一进程内的并发 Session;Child Resource Loader 会移除 pi-intercom 扩展与 Skill,避免身份串线。Replay 也不会复用其调用。
339
341
 
@@ -352,6 +354,7 @@ macOS/Linux arm64 与 x64 缺少二进制时,OpenPI 会从官方 Release 下
352
354
 
353
355
  ```text
354
356
  /openpi-setup 开启下一步预测,选择 Registry 里的轻量模型,minimal 推理
357
+ /openpi-setup 让模型在合适时自主发现并采用 OpenPI 能力
355
358
  /openpi-setup workflow 同时跑 16 个 agent,总调用最多 256
356
359
  /openpi-setup Footer 两行:cwd flex model / context cost flex git
357
360
  /openpi-setup Bash 展开,Write/Edit 保持紧凑
@@ -361,11 +364,14 @@ macOS/Linux arm64 与 x64 缺少二进制时,OpenPI 会从官方 Release 下
361
364
 
362
365
  配置保存在 `~/.pi/agent/my-pi-setup.json`,与包代码分离,升级不会覆盖。
363
366
 
367
+ 一次 `/openpi-setup` episode 最多成功写入一次;成功后配置工具立即隐藏。若随后还要修改另一项,请重新执行 `/openpi-setup <自然语言请求>`,不要让模型重调已隐藏工具,也不要绕过入口直接编辑配置文件。
368
+
364
369
  <details>
365
370
  <summary><strong>默认值</strong></summary>
366
371
 
367
372
  | 配置 | 默认值 |
368
373
  | ---------------------------- | ---------------------------------------------- |
374
+ | Capability discovery | `explicit`;`adaptive` 必须显式开启 |
369
375
  | Next-action Suggestion | 关闭;启用时显式选择 Registry 模型与 reasoning |
370
376
  | Workflow 并发 / 总调用 | 8 / 128;硬上限 64 / 1024 |
371
377
  | 大型 Header | 关闭 |
@@ -390,7 +396,7 @@ macOS/Linux arm64 与 x64 缺少二进制时,OpenPI 会从官方 Release 下
390
396
  ```bash
391
397
  git clone https://github.com/tt-a1i/openpi.git ~/work/openpi
392
398
  cd ~/work/openpi
393
- npm install
399
+ bun install --frozen-lockfile
394
400
  pi install ~/work/openpi
395
401
  ```
396
402
 
@@ -425,18 +431,23 @@ pi install npm:pi-intercom
425
431
  <details>
426
432
  <summary><strong>模型工具速查</strong></summary>
427
433
 
428
- | 工具 | 用途 |
429
- | -------------------------------------------------------------------------------------------------------- | ------------------------------ |
430
- | `bg_start`, `bg_status`, `bg_list`, `bg_watch`, `bg_kill` | 后台进程生命周期 |
431
- | `subagent_spawn`, `subagent_check`, `subagent_list`, `subagent_wait`, `subagent_send`, `subagent_cancel` | 独立子 Agent |
432
- | `workflow`, `workflow_status`, `workflow_stop` | 动态多阶段编排与运行管理 |
433
- | `tasks_add`, `tasks_update`, `tasks_list` | Session 工作项 |
434
- | `get_goal`, `create_goal`, `update_goal` | Session Goal |
435
- | `context_pivot` | Context 阶段切换 |
436
- | `ask_user`, `human_handoff` | 经复核的用户决策与用户专属操作 |
437
- | `plan_ready` | 显式完成计划,不自动开始实施 |
438
- | `fd`, `rg` | 文件发现与内容搜索 |
439
- | `configure_my_pi_setup` | 受限配置写入 |
434
+ Capability discovery 默认是 `explicit`:普通父 Session 不常驻任何 OpenPI 模型工具,首轮保持 Pi 原生 `read`、`bash`、`edit`、`write`。用户明确要求结构化搜索、Subagent、Workflow、后台进程或 Session Goal/Tasks 时,OpenPI 在 `before_agent_start` 直接加载对应能力组;明确询问 OpenPI capabilities/tools/features 时显示 `openpi_load_tools`。可通过 `/openpi-setup` 显式选择 `adaptive`:此时只让小型 `openpi_load_tools` 网关常驻,模型可在判断任务确实受益时自主加载一个能力组。该选择也授权模型启动该组内的昂贵工作,因此不作为默认值。条件句(例如 “If you delegate…”)不会被当成显式委派意图。能力组在当前 Session 内单调保持,避免反复增删工具破坏缓存。组内管理工具仍只在资源成功创建或状态确实存在后出现。Mode / Setup / Context 工具独立跟随实时状态显示和隐藏。Background、Subagent 与 Workflow 的 Skill 文件仍随包发布,但只在对应能力触发后提示读取,不常驻普通系统 Prompt。
435
+
436
+ 普通产品默认采用 Pi-native execution:保留 Pi 原生完整历史、工具输出上限、Session compaction、显式 Bash timeout 与 provider loop,不再额外做固定事务投影、成功 Bash 二次裁剪、测试 timeout 改写、重复失败硬拦或恢复/轨迹提示。OpenPI 只保留独立的工作区安全边界:阻止未授权删除 pre-existing 路径,并从实际文件状态识别本轮通过原生写入、文字重定向或 literal `mkdir -p` 创建的 scratch,避免误拦其清理。旧执行策略仅保留为受 benchmark root 门控的实验 profile,不会进入普通 Session。
437
+
438
+ | 工具 | 用途 | 可见时机 |
439
+ | -------------------------------------------------------------------------------------------------------- | ------------------------------ | -------------------------------- |
440
+ | `openpi_load_tools` | 列出或加载可选工具组 | 明确询问;或启用 `adaptive` |
441
+ | `bg_start`, `bg_status`, `bg_list`, `bg_watch`, `bg_kill` | 后台进程生命周期 | 明确意图或 adaptive;启动后展开 |
442
+ | `subagent_spawn`, `subagent_check`, `subagent_list`, `subagent_wait`, `subagent_send`, `subagent_cancel` | 独立子 Agent | 明确意图或 adaptive;创建后展开 |
443
+ | `workflow`, `workflow_status`, `workflow_stop` | 动态多阶段编排与运行管理 | 明确意图或 adaptive;运行后展开 |
444
+ | `tasks_add`, `tasks_update`, `tasks_list` | Session 工作项 | 明确意图或 adaptive;存在后展开 |
445
+ | `get_goal`, `create_goal`, `update_goal` | Session Goal | 明确意图或 adaptive;存在后展开 |
446
+ | `context_pivot` | Context 阶段切换 | Context 达到阈值时 |
447
+ | `ask_user`, `human_handoff` | 经复核的用户决策与用户专属操作 | Plan 或 Setup 进行中 |
448
+ | `plan_ready` | 显式完成计划,不自动开始实施 | Plan 调研阶段 |
449
+ | `fd`, `rg` | 文件发现与内容搜索 | 明确意图或 adaptive 加载 search |
450
+ | `configure_my_pi_setup` | 受限配置写入 | `/openpi-setup` 进行中 |
440
451
 
441
452
  </details>
442
453
 
@@ -451,7 +462,7 @@ pi install npm:pi-intercom
451
462
  <details>
452
463
  <summary><strong>安装后会自动调用额外模型吗?</strong></summary>
453
464
 
454
- 不会。Suggestion 默认关闭;只有用户显式选择模型后,完整主 Agent Run 结束时才可能增加一次小型预测调用。Subagent Workflow 也只在任务实际触发时运行。
465
+ 默认不会。Suggestion 默认关闭;Capability discovery 默认 `explicit`。如果用户显式开启 `adaptive`,网关本身不发模型请求,但主模型可以自主加载 Subagent Workflow 并启动额外模型调用;并发和 Workflow 总调用上限仍然生效。
455
466
 
456
467
  </details>
457
468
 
@@ -497,6 +508,7 @@ Plan Mode 不猜“任意 Shell 是否只读”,只放行由已知安全零件
497
508
  ```text
498
509
  extensions/
499
510
  ├── setup/ # /openpi-setup 与受限配置工具
511
+ ├── capabilities/ # 最小能力发现入口与 Session 工具面加载
500
512
  ├── background-terminals/ # 长进程、日志、/ps
501
513
  ├── subagents/ # Pi-native Backend、角色、/subagents
502
514
  ├── workflows/ # DSL、Ledger、Graph、Replay、Artifacts
@@ -510,17 +522,20 @@ extensions/
510
522
  ├── ui-customization/ # Header、Footer、Terminal title
511
523
  └── shared/ # Child policy、配置、Worktree、终端清洗
512
524
 
513
- skills/ # Background terminal 与 Subagent 指南
525
+ skills/ # Background terminal、SubagentWorkflow 指南
514
526
  themes/ # github-dark-default
515
527
  ```
516
528
 
529
+ 开发工具链使用 Bun `1.3.14` 管理依赖和脚本,Biome 负责 TypeScript / JavaScript / JSON 格式与基础 lint;产品运行时仍是 Node,测试仍由 `node:test` 与 Vitest 执行:
530
+
517
531
  ```bash
518
- npm install
519
- npm run format:check
520
- npm run check
521
- npm test
532
+ bun install --frozen-lockfile
533
+ bun run check
534
+ bun run test
522
535
  ```
523
536
 
537
+ npm 仍用于发布包的 `pack` / clean-install 验证,因为用户通过 npm Registry 安装 OpenPI。
538
+
524
539
  测试覆盖进程树终止与竞态、Subagent 生命周期与工具边界、Workflow Sandbox / Ledger / Graph / Replay / Acceptance、Worktree 数据保全、Session 状态恢复、配置迁移和 TUI 渲染。设计记录见 [`docs/design/`](docs/design/),问题请提交到 [GitHub Issues](https://github.com/tt-a1i/openpi/issues)。
525
540
 
526
541
  ---
package/SETUP.md CHANGED
@@ -16,7 +16,7 @@ Pi installs the package dependencies automatically. Restart Pi or run `/reload`
16
16
 
17
17
  ## fd and rg tools
18
18
 
19
- The `file-search` extension registers `fd` and `rg` as model tools. No setup is normally needed: at startup it silently uses a system-installed `fd` (or `fdfind` on Debian/Ubuntu) and `rg` when available, or an existing fallback in this package's private `bin/` directory. Only when neither exists does it download an official release binary (macOS/Linux, arm64/x64, over HTTPS) into that package-local directory and show a one-time notification. If your platform is unsupported, install `fd` and `rg` with your package manager and restart Pi.
19
+ The `file-search` extension registers `fd` and `rg` as model tools. They stay outside an ordinary parent turn until the user explicitly asks to use `fd`/`rg` or structured file search, or the model loads the `search` group through `openpi_load_tools`. The gateway is shown after an explicit OpenPI-capability request, or remains visible when the user opts into adaptive discovery; child sessions may still receive `fd` and `rg` through the reviewed child-safe allowlist. No setup is normally needed: at startup it silently uses a system-installed `fd` (or `fdfind` on Debian/Ubuntu) and `rg` when available, or an existing fallback in this package's private `bin/` directory. Only when neither exists does it download an official release binary (macOS/Linux, arm64/x64, over HTTPS) into that package-local directory and show a one-time notification. If your platform is unsupported, install `fd` and `rg` with your package manager and restart Pi.
20
20
 
21
21
  ## Theme
22
22
 
@@ -28,14 +28,16 @@ Add the included theme to `~/.pi/agent/settings.json` while keeping your existin
28
28
  }
29
29
  ```
30
30
 
31
- Pi will load the extensions, skills, and theme from their directories the next time it starts.
31
+ Pi will load the extensions and theme the next time it starts. OpenPI's Background, Subagent, and Workflow Skill files remain in the package, but ordinary turns do not advertise them in the system prompt; the matching path is disclosed only after explicit capability intent or after the model loads that group through the opt-in adaptive gateway.
32
32
 
33
33
  ## Configure this package
34
34
 
35
- Use the single canonical package-owned command. `/my-pi-setup` remains a compatibility alias. With no arguments, when optional pi-intercom is absent, the interactive TUI first offers a reviewed global installation; declining changes nothing. Acceptance installs the fixed `npm:pi-intercom` source through Pi's package manager. A new private config receives `confirmSend: true` and `inboundTrigger: "replies"`; an existing preference file is never rewritten and must already define both fields. Package download failure writes no config, while uncertain activation retains the safe new config. Setup asks for `/reload` instead of loading a new broker into the running Session. The current model then explains the remaining configurable areas and uses `ask_user`: first run initializes them; later runs explain the saved state and ask whether to keep it, change one area, or review everything. With arguments, it treats the rest as a targeted natural-language request:
35
+ Use the single canonical package-owned command. `/my-pi-setup` remains a compatibility alias. With no arguments, when optional pi-intercom is absent, the interactive TUI first offers a reviewed global installation; declining changes nothing. Acceptance installs the fixed `npm:pi-intercom` source through Pi's package manager. A new private config receives `confirmSend: true` and `inboundTrigger: "replies"`; an existing preference file is never rewritten and must already define both fields. Package download failure writes no config, while uncertain activation retains the safe new config. Setup asks for `/reload` instead of loading a new broker into the running Session. The current model then explains the remaining configurable areas and uses `ask_user`: first run initializes them; later runs explain the saved state and ask whether to keep it, change one area, or review everything. With arguments, it treats the rest as a targeted natural-language request. Persist still goes through the typed `configure_my_pi_setup` tool, which is exposed only while that `/openpi-setup` episode is in flight and is hidden again afterward. One successful apply completes the episode; a later configuration change starts a new one with `/openpi-setup <request>` rather than reusing the hidden tool:
36
36
 
37
37
  ```text
38
38
  /openpi-setup
39
+ /openpi-setup 让模型在合适时自主发现并采用 OpenPI 能力
40
+ /openpi-setup 只在我明确要求时加载 OpenPI 能力
39
41
  /openpi-setup 开启下一步预测,使用 seal/deepseek-v4-flash,关闭推理
40
42
  /openpi-setup 关闭下一步预测
41
43
  /openpi-setup workflow 同时跑 16 个 agent,总任务最多 256 个
@@ -52,7 +54,7 @@ Use the single canonical package-owned command. `/my-pi-setup` remains a compati
52
54
  /openpi-setup 清除 explorer 的模型,让它继承父模型
53
55
  ```
54
56
 
55
- Next-action suggestions default to off. Run `/openpi-setup` to explicitly choose an available model and reasoning level. After a fully settled main-agent run, one suggestion may appear as dim inline text on the first row of an empty editor; reserved cells at the row end keep CJK IME preedit from overwriting it. `Right` accepts it into the editor without submitting, while any other editor input dismisses it. Suggestions are ephemeral and never enter session history or model context. Workflows default to 8 concurrent agents and 128 total agent calls per run; configurable hard maxima are 64 and 1024. The large decorative header defaults off and the custom dashboard footer defaults on with a one-line Powerline layout (`cwd model thinking context cache cost throughput |flex| git pr`). Footer presets are `powerline`, `powerline-mono`, and `compact`; style can also be set independently to `plain`, `powerline`, or `powerline-mono`. Custom layouts use a 2D `footerLines` array with at most one `flex` per row for left/right alignment. Nerd Font only affects powerline separator glyphs (``); metric text stays readable without it. Footer changes apply immediately in the active TUI session. Subagent results default to the existing full display; users who do not usually inspect implementation detail can select compact previews. Bash defaults to a folded one-line command with bounded output and a hidden-line count. Write/Edit defaults to an extra-short folded preview capped at three rendered lines including the operation header; its hidden-line hint remains inside the operation's status background. Select full independently for any category to keep it expanded. Compact views temporarily expand with `app.tools.expand` (`Ctrl+O` by default). An optional post-edit command is off by default: set one (for example `npm run format`, maximum 500 characters) and it runs once in the background after each interactive-TUI turn with successful Write/Edit operations, with failures reported as a notification. It deliberately does not guess whether arbitrary Bash commands changed files. Built-in Agent roles `explorer`, `implementer`, `reviewer`, and `advisor` are shared by `subagent_spawn.agent_type` and Workflow `agent(..., { agent_type })`; all inherit the parent model by default. `/openpi-setup` may assign a currently available Registry model to any subset; clearing one returns it to inheritance and omitted roles stay unchanged. Model precedence is explicit call > selected role-file model > setup assignment > parent inheritance; effort is explicit call > selected role > parent. A trusted project `.pi/agents/<role>.md` overrides global `~/.pi/agent/agents/<role>.md`, which overrides the complete built-in role definition; overrides are diagnosed. Role-model changes apply to the next spawn or Workflow agent call without reload. Configuration is stored privately at `~/.pi/agent/my-pi-setup.json`.
57
+ Capability discovery defaults to `explicit`, preserving the zero-resident OpenPI tool surface until the user asks for a capability. `adaptive` is an explicit opt-in that keeps only `openpi_load_tools` visible and allows the model to load a useful group on its own; because this can start Subagents, Workflows, or background processes, normal permission and configured concurrency/call limits still apply. Changing the setting updates the current Session immediately, while already loaded groups remain stable for that Session. Next-action suggestions default to off. Run `/openpi-setup` to explicitly choose an available model and reasoning level. After a fully settled main-agent run, one suggestion may appear as dim inline text on the first row of an empty editor; reserved cells at the row end keep CJK IME preedit from overwriting it. `Right` accepts it into the editor without submitting, while any other editor input dismisses it. Suggestions are ephemeral and never enter session history or model context. Workflows default to 8 concurrent agents and 128 total agent calls per run; configurable hard maxima are 64 and 1024. The large decorative header defaults off and the custom dashboard footer defaults on with a one-line Powerline layout (`cwd model thinking context cache cost throughput |flex| git pr`). Footer presets are `powerline`, `powerline-mono`, and `compact`; style can also be set independently to `plain`, `powerline`, or `powerline-mono`. Custom layouts use a 2D `footerLines` array with at most one `flex` per row for left/right alignment. Nerd Font only affects powerline separator glyphs (``); metric text stays readable without it. Footer changes apply immediately in the active TUI session. Subagent results default to the existing full display; users who do not usually inspect implementation detail can select compact previews. Bash defaults to a folded one-line command with bounded output and a hidden-line count. Write/Edit defaults to an extra-short folded preview capped at three rendered lines including the operation header; its hidden-line hint remains inside the operation's status background. Select full independently for any category to keep it expanded. Compact views temporarily expand with `app.tools.expand` (`Ctrl+O` by default). An optional post-edit command is off by default: set one (for example `npm run format`, maximum 500 characters) and it runs once in the background after each interactive-TUI turn with successful Write/Edit operations, with failures reported as a notification. It deliberately does not guess whether arbitrary Bash commands changed files. Built-in Agent roles `explorer`, `implementer`, `reviewer`, and `advisor` are shared by `subagent_spawn.agent_type` and Workflow `agent(..., { agent_type })`; all inherit the parent model by default. `/openpi-setup` may assign a currently available Registry model to any subset; clearing one returns it to inheritance and omitted roles stay unchanged. Model precedence is explicit call > selected role-file model > setup assignment > parent inheritance; effort is explicit call > selected role > parent. A trusted project `.pi/agents/<role>.md` overrides global `~/.pi/agent/agents/<role>.md`, which overrides the complete built-in role definition; overrides are diagnosed. Role-model changes apply to the next spawn or Workflow agent call without reload. Configuration is stored privately at `~/.pi/agent/my-pi-setup.json`.
56
58
 
57
59
  ## Session Goal and Tasks
58
60
 
@@ -62,7 +64,7 @@ Model callers use `get_goal`, `create_goal`, and `update_goal`. `create_goal` is
62
64
 
63
65
  There are no normal user-facing Turn, no-progress, or wall-clock caps; a hidden 1000-continuation circuit breaker exists only to stop runaway automation. An optional `token_budget` must only be positive. Goal non-cached Assistant input-plus-output Token and elapsed-time usage are persisted; crossing the budget marks `budget_limited` and queues one wrap-up Turn. Active goals continue after reload/resume. Fork and tree navigation defer inherited active continuation until the first explicit user input; paused, blocked, and usage-limited goals remain stopped and can prompt for Resume. A v1 active/waiting goal migrates once to paused. Assistant aborts pause an active goal and Assistant errors block it. Print/json automation is inert. Footer text mirrors Codex (`Pursuing goal (…)`, resume hints, `Goal unmet`, `Goal achieved`) without showing the objective or legacy Turn counts. An achieved Footer remains visible until the next explicit interactive/RPC input, then a branch-persisted acknowledgement hides only the Footer while `/goal` retains the completed record.
64
66
 
65
- Session Tasks remain advisory multi-item work intent and do not determine Goal completion. They are scoped to the current request batch: once every item is done or dropped, the batch closes and the next `tasks_add` starts again at T1. Active items persist in a polished Claude Code-style panel above the editor; `Ctrl+Shift+T` or `/tasks hide|show|toggle` controls visibility, while `/tasks` opens the full list. No `/openpi-setup` setting or secondary judge model is required.
67
+ Session Tasks remain advisory multi-item work intent and do not determine Goal completion. They are scoped to the current request batch: once every item is done or dropped, the batch closes and the next `tasks_add` starts again at T1. The model marks a tracked item `in_progress` before starting it, records `done`, `blocked`, or `dropped` immediately after that item reaches a real outcome, and reconciles touched items before its final answer. Every add/update result returns the complete bounded current snapshot so the next item is explicit and the panel refreshes on each persisted transition. Commit, test, and authorization signals are only task-scoped evidence candidates; OpenPI never infers completion or mutates a task from those signals. Active items persist in a polished Claude Code-style panel above the editor; `Ctrl+Shift+T` or `/tasks hide|show|toggle` controls visibility, while `/tasks` opens the full list. No `/openpi-setup` setting or secondary judge model is required.
66
68
 
67
69
  ## Other commands added by this fork
68
70
 
@@ -70,5 +72,5 @@ Session Tasks remain advisory multi-item work intent and do not determine Goal c
70
72
  - `/tasks` inspects branch-scoped advisory work items.
71
73
  - `/goal ...` controls the persistent autonomous session objective.
72
74
  - `/context-pivot <next phase>` deliberately compacts a long current session into a next-phase brief. It requires at least 30,000 context tokens and is rejected below that; use the separate `/handoff` skill when work should move to a genuinely new session.
73
- - `/cron every <5m> <prompt>`, `/cron in <30s> <prompt>`, `/cron list`, and `/cron remove <id>` schedule a prompt for this session. Jobs are in-memory and session-scoped (cleared on shutdown), fire only while the session is idle, and use a duration grammar (`30s`/`5m`/`2h`, minimum 30s) rather than crontab fields, because the scheduler polls about every 30 seconds.
75
+ - `/cron every <5m> <prompt>`, `/cron in <30s> <prompt>`, `/cron list`, and `/cron remove <id>` schedule a prompt for this session. Jobs are in-memory and session-scoped (cleared on shutdown), fire only while the session is idle, and use a duration grammar (`30s`/`5m`/`2h`, minimum 30s) rather than crontab fields, because the scheduler polls about every 30 seconds. Jobs due in the same poll are delivered as one triggered turn while retaining each job's id and recurrence metadata; if that atomic delivery fails, every due job remains pending for retry.
74
76
  - `/plan [objective]` explores read-only before changing anything: while armed it blocks `edit`, `write`, mutating Bash, `subagent_send`, `workflow`, and `bg_start`, while read/grep/find/ls/fd/rg and verified read-only Git/GitHub commands stay available. It permits `subagent_spawn`, but the harness narrows every newly spawned planning child to investigation-only tools; agent types can narrow that list further, never widen it. The model must submit the complete plan through parent-only `plan_ready`; the write gate stays closed until `/plan` prepares an editable implementation prompt for the current or a fresh Session. `/plan off` cancels.
@@ -33,7 +33,11 @@ const HumanHandoffParams = Type.Object({
33
33
 
34
34
  export type HumanHandoffInput = Static<typeof HumanHandoffParams>;
35
35
  export type HumanHandoffStatus =
36
- "completed" | "unable" | "dismissed" | "cancelled" | "unavailable";
36
+ | "completed"
37
+ | "unable"
38
+ | "dismissed"
39
+ | "cancelled"
40
+ | "unavailable";
37
41
 
38
42
  export interface HumanHandoffDetails {
39
43
  status: HumanHandoffStatus;
@@ -25,6 +25,18 @@ import {
25
25
  import { Cause, Effect, Exit } from "effect";
26
26
  import { Type, type Static } from "typebox";
27
27
  import { sanitizeTerminalText } from "../shared/terminal-text.ts";
28
+ import {
29
+ PLAN_MODE_CHANNEL,
30
+ type PlanModeState,
31
+ } from "../shared/plan-mode-state.ts";
32
+ import {
33
+ OPENPI_SETUP_EPISODE_CHANNEL,
34
+ type OpenPiSetupEpisodeState,
35
+ } from "../shared/setup-episode-state.ts";
36
+ import {
37
+ OPENPI_TOOL_SURFACE,
38
+ patchOwnedTools,
39
+ } from "../shared/tool-surface.ts";
28
40
  import { createHumanHandoffToolDefinition } from "./handoff.ts";
29
41
  import {
30
42
  BRACKETED_PASTE_END,
@@ -355,6 +367,38 @@ export async function showQuestionsWithDialogs(
355
367
  }
356
368
 
357
369
  export default function askUser(pi: ExtensionAPI) {
370
+ let planning = false;
371
+ let setupActive = false;
372
+ const syncInteractionTools = () =>
373
+ patchOwnedTools(pi, "interaction", {
374
+ ...(planning || setupActive
375
+ ? { enable: OPENPI_TOOL_SURFACE.interaction.deferred }
376
+ : { disable: OPENPI_TOOL_SURFACE.interaction.deferred }),
377
+ });
378
+
379
+ pi.events.on(PLAN_MODE_CHANNEL, (state: unknown) => {
380
+ planning =
381
+ typeof state === "object" &&
382
+ state !== null &&
383
+ (state as PlanModeState).planning === true;
384
+ syncInteractionTools();
385
+ });
386
+ pi.events.on(OPENPI_SETUP_EPISODE_CHANNEL, (state: unknown) => {
387
+ setupActive =
388
+ typeof state === "object" &&
389
+ state !== null &&
390
+ (state as OpenPiSetupEpisodeState).active === true;
391
+ syncInteractionTools();
392
+ });
393
+ pi.on("session_shutdown", () => {
394
+ planning = false;
395
+ setupActive = false;
396
+ syncInteractionTools();
397
+ });
398
+ pi.on("session_start", () => {
399
+ syncInteractionTools();
400
+ });
401
+
358
402
  pi.registerTool({
359
403
  name: "ask_user",
360
404
  label: "Ask User",
@@ -28,8 +28,16 @@ import type {
28
28
  import { Type } from "typebox";
29
29
  import { loadSetupConfig } from "../shared/setup-config.ts";
30
30
  import { sanitizeTerminalText } from "../shared/terminal-text.ts";
31
+ import {
32
+ OPENPI_TOOL_SURFACE,
33
+ patchOwnedTools,
34
+ } from "../shared/tool-surface.ts";
31
35
  import type { TerminalSnapshot } from "./src/domain.ts";
32
- import { TerminalManager, type TerminalManagerShape } from "./src/manager.ts";
36
+ import {
37
+ MAX_RUNNING,
38
+ TerminalManager,
39
+ type TerminalManagerShape,
40
+ } from "./src/manager.ts";
33
41
  import {
34
42
  BG_KILL_PARAMETER_DESCRIPTIONS,
35
43
  BG_KILL_TOOL_DESCRIPTION,
@@ -45,6 +53,7 @@ import {
45
53
  buildKillReport,
46
54
  buildStartResult,
47
55
  buildStatusResult,
56
+ buildTerminalBatchResultMessage,
48
57
  buildTerminalResultMessage,
49
58
  buildWatchArmedResult,
50
59
  buildWatchMatchMessage,
@@ -52,23 +61,29 @@ import {
52
61
  } from "./src/prompt.ts";
53
62
  import {
54
63
  createDeferredResultDelivery,
64
+ createIdleResultBatcher,
65
+ hasTerminalCapacity,
55
66
  resultDeliveryOptions,
56
67
  } from "./src/result-delivery.ts";
57
- import {
58
- assertWatchableOutput,
59
- compileWatchPattern,
60
- createChunkMatcher,
61
- matchCapturedOutput,
62
- } from "./src/watch.ts";
63
68
  import {
64
69
  createTerminalRuntime,
65
70
  runTool,
66
71
  type TerminalRuntime,
67
72
  } from "./src/runtime.ts";
68
73
  import { openTerminalPicker } from "./src/ui/ps.ts";
69
- import { renderTerminalResult } from "./src/ui/tool-result.ts";
74
+ import {
75
+ renderTerminalBatchResult,
76
+ renderTerminalResult,
77
+ } from "./src/ui/tool-result.ts";
78
+ import {
79
+ assertWatchableOutput,
80
+ compileWatchPattern,
81
+ createChunkMatcher,
82
+ matchCapturedOutput,
83
+ } from "./src/watch.ts";
70
84
 
71
85
  const WIDGET_KEY = "background-terminals";
86
+ const IDLE_RESULT_BATCH_MS = 200;
72
87
 
73
88
  interface WatchToolDetails {
74
89
  id: string;
@@ -83,7 +98,16 @@ export default function (pi: ExtensionAPI) {
83
98
  let sessionContext: ExtensionContext | undefined;
84
99
  let ui: ExtensionUIContext | undefined;
85
100
  let unsubStatus: (() => void) | undefined;
101
+ let startReservations = 0;
86
102
  const resultDelivery = createDeferredResultDelivery<TerminalSnapshot>();
103
+ const hideLifecycleTools = () =>
104
+ patchOwnedTools(pi, "background", {
105
+ disable: OPENPI_TOOL_SURFACE.background.deferred,
106
+ });
107
+ const showLifecycleTools = () =>
108
+ patchOwnedTools(pi, "background", {
109
+ enable: OPENPI_TOOL_SURFACE.background.deferred,
110
+ });
87
111
  /** Active bg_watch disarm callbacks, keyed by terminal id (one per id). */
88
112
  const watchers = new Map<string, () => void>();
89
113
 
@@ -160,7 +184,9 @@ export default function (pi: ExtensionAPI) {
160
184
  customType: "background-terminal-result",
161
185
  // One message per flush, not per terminal: five processes exiting
162
186
  // together are one event to react to, not five.
163
- content: snaps.map(buildTerminalResultMessage).join("\n\n"),
187
+ content: buildTerminalBatchResultMessage(
188
+ snaps.map(buildTerminalResultMessage),
189
+ ),
164
190
  display: true,
165
191
  details:
166
192
  snaps.length === 1
@@ -198,12 +224,18 @@ export default function (pi: ExtensionAPI) {
198
224
  };
199
225
 
200
226
  const flushResults = (wake: boolean) => {
201
- const snaps = resultDelivery.drain();
202
- if (!deliverResults(snaps, wake)) {
203
- for (const snap of snaps) resultDelivery.defer(snap);
204
- }
227
+ const snaps = resultDelivery.drain(MAX_RUNNING);
228
+ if (!deliverResults(snaps, wake)) resultDelivery.restore(snaps);
205
229
  };
206
230
 
231
+ const idleResultBatcher = createIdleResultBatcher({
232
+ delayMs: IDLE_RESULT_BATCH_MS,
233
+ isIdle: () => sessionContext?.isIdle() === true,
234
+ flush: flushResults,
235
+ startTimer: (callback, delayMs) => setTimeout(callback, delayMs),
236
+ clearTimer: (timer) => clearTimeout(timer),
237
+ });
238
+
207
239
  const onSettled = (snap: TerminalSnapshot, consumed: boolean) => {
208
240
  // A settled terminal has delivered its final result and will emit no more
209
241
  // output, so any watch armed on it can never match — disarm it now instead
@@ -216,17 +248,27 @@ export default function (pi: ExtensionAPI) {
216
248
  }
217
249
  // Defer a deep-enough copy: the live snapshot's output views keep
218
250
  // mutating (late flushes) after settle.
219
- resultDelivery.defer({
251
+ const pending = resultDelivery.defer({
220
252
  ...snap,
221
253
  stdout: { ...snap.stdout },
222
254
  stderr: { ...snap.stderr },
223
255
  });
224
- // Settled while the model sits idle: it has nothing else in flight, so
225
- // this is the result it is waiting on wake it.
226
- if (sessionContext?.isIdle()) flushResults(true);
256
+ // Pending settlements remain retractable while busy, so bg_status/bg_kill
257
+ // can consume them before they are committed to context. bg_start applies
258
+ // backpressure across running + pending + reserved work, keeping this map
259
+ // bounded without dropping or prematurely queueing any result.
260
+ if (pending >= MAX_RUNNING && sessionContext?.isIdle()) {
261
+ idleResultBatcher.flushNow();
262
+ return;
263
+ }
264
+ // Give near-simultaneous idle settlements one fixed, bounded window to
265
+ // join this result. This costs one model turn for a batch instead of one
266
+ // turn per process, while preserving the immediate path after 200 ms.
267
+ if (sessionContext?.isIdle()) idleResultBatcher.schedule();
227
268
  };
228
269
 
229
270
  pi.on("session_start", (_event, ctx) => {
271
+ hideLifecycleTools();
230
272
  sessionContext = ctx;
231
273
  if (ctx.hasUI) ui = ctx.ui;
232
274
  });
@@ -236,7 +278,14 @@ export default function (pi: ExtensionAPI) {
236
278
  // double delivery is structurally impossible — whoever drains first wins.
237
279
  // These finished while the model was working on something else, so they go
238
280
  // into context without forcing a turn per stale process.
239
- pi.on("agent_settled", () => flushResults(false));
281
+ pi.on("agent_settled", () => {
282
+ idleResultBatcher.flushWithoutWake();
283
+ // A failed send is restored exactly. Drain it in bounded chunks on later
284
+ // settled turns rather than growing a single unbounded message.
285
+ if (sessionContext?.isIdle() && resultDelivery.size() > 0) {
286
+ idleResultBatcher.schedule();
287
+ }
288
+ });
240
289
 
241
290
  // /new, /resume, /fork, /reload, and quit all emit session_shutdown for
242
291
  // the old extension instance. Processes never survive a session
@@ -245,6 +294,7 @@ export default function (pi: ExtensionAPI) {
245
294
  // bounded so a wedged process cannot hang shutdown.
246
295
  pi.on("session_shutdown", async () => {
247
296
  sessionContext = undefined;
297
+ idleResultBatcher.clear();
248
298
  resultDelivery.clear();
249
299
  for (const disarm of [...watchers.values()]) disarm();
250
300
  watchers.clear();
@@ -256,6 +306,7 @@ export default function (pi: ExtensionAPI) {
256
306
  // UI may already be gone.
257
307
  }
258
308
  widgetRunning = 0;
309
+ startReservations = 0;
259
310
  ui = undefined;
260
311
  const closing = runtime;
261
312
  runtime = undefined;
@@ -309,15 +360,38 @@ export default function (pi: ExtensionAPI) {
309
360
  .replace(/\s+/g, " ")
310
361
  .trim()
311
362
  .slice(0, 80) || "terminal";
312
- const snap = await runTool(
313
- getRuntime(),
314
- manager.start({
315
- command,
316
- title,
317
- cwd,
318
- timeoutSeconds: params.timeout_seconds,
319
- }),
320
- );
363
+ const running = manager.view
364
+ .list()
365
+ .filter((entry) => entry.status === "running").length;
366
+ if (
367
+ !hasTerminalCapacity({
368
+ running,
369
+ pending: resultDelivery.size(),
370
+ reserved: startReservations,
371
+ maximum: MAX_RUNNING,
372
+ })
373
+ ) {
374
+ throw new Error(
375
+ `Max ${MAX_RUNNING} background terminals may be running or awaiting delivery. Let the current turn settle, or inspect a finished terminal with bg_status before starting another.`,
376
+ );
377
+ }
378
+ startReservations++;
379
+ let snap: TerminalSnapshot;
380
+ try {
381
+ snap = await runTool(
382
+ getRuntime(),
383
+ manager.start({
384
+ command,
385
+ title,
386
+ cwd,
387
+ timeoutSeconds: params.timeout_seconds,
388
+ }),
389
+ );
390
+ } finally {
391
+ startReservations--;
392
+ }
393
+
394
+ showLifecycleTools();
321
395
 
322
396
  return {
323
397
  content: [{ type: "text", text: buildStartResult(snap) }],
@@ -591,7 +665,24 @@ export default function (pi: ExtensionAPI) {
591
665
  status?: string;
592
666
  exitCode?: number;
593
667
  signal?: string;
668
+ results?: Array<{
669
+ id: string;
670
+ title: string;
671
+ status: string;
672
+ exitCode?: number;
673
+ signal?: string;
674
+ }>;
594
675
  };
676
+ const content =
677
+ typeof message.content === "string" ? message.content : "";
678
+ if (details.results && details.results.length > 1) {
679
+ return renderTerminalBatchResult(
680
+ content,
681
+ expanded || loadSetupConfig().ui.bashToolDisplay === "full",
682
+ theme,
683
+ details.results,
684
+ );
685
+ }
595
686
  const failed = details.status === "failed";
596
687
  const killed = details.status === "killed";
597
688
  const timedOut = details.status === "timed_out";
@@ -611,8 +702,6 @@ export default function (pi: ExtensionAPI) {
611
702
  theme.fg("accent", theme.bold(`terminal ${details.id ?? "?"}`)) +
612
703
  theme.fg("muted", ` · ${details.title ?? ""} · ${how}`);
613
704
 
614
- const content =
615
- typeof message.content === "string" ? message.content : "";
616
705
  return renderTerminalResult(
617
706
  content,
618
707
  expanded || loadSetupConfig().ui.bashToolDisplay === "full",
@@ -9,7 +9,11 @@
9
9
  import { Data } from "effect";
10
10
 
11
11
  export type TerminalStatus =
12
- "running" | "done" | "failed" | "killed" | "timed_out";
12
+ | "running"
13
+ | "done"
14
+ | "failed"
15
+ | "killed"
16
+ | "timed_out";
13
17
  // "done" = exited with code 0
14
18
  // "failed" = exited non-zero, or a spawn-level runtime error after start
15
19
  // "killed" = terminated by bg_kill, the /ps UI, or session teardown
@@ -313,7 +313,8 @@ const makeManager = Effect.gen(function* () {
313
313
  let disposed = false;
314
314
  let spillDir: string | undefined | null;
315
315
  let onSettled:
316
- ((snap: TerminalSnapshot, consumed: boolean) => void) | undefined;
316
+ | ((snap: TerminalSnapshot, consumed: boolean) => void)
317
+ | undefined;
317
318
 
318
319
  const notify = (id?: string) => {
319
320
  for (const listener of [...listeners]) {
@@ -22,6 +22,8 @@ export const STATUS_STDERR_MAX = 8 * 1024;
22
22
  export const RESULT_STDOUT_MAX = 8 * 1024;
23
23
  /** Completion follow-up stderr tail. Keep this concise; /ps has the detailed view. */
24
24
  export const RESULT_STDERR_MAX = 4 * 1024;
25
+ /** Global model-facing cap for one batched completion message. */
26
+ export const RESULT_BATCH_MAX = 48 * 1024;
25
27
  const STATUS_STDOUT_MAX_LINES = 400;
26
28
  const STATUS_STDERR_MAX_LINES = 200;
27
29
  const RESULT_STDOUT_MAX_LINES = 40;
@@ -179,6 +181,39 @@ export function buildTerminalResultMessage(snap: TerminalSnapshot) {
179
181
  return text;
180
182
  }
181
183
 
184
+ /** Preserve every retained terminal identity while globally bounding batch logs. */
185
+ export function buildTerminalBatchResultMessage(
186
+ messages: readonly string[],
187
+ omitted = 0,
188
+ ) {
189
+ if (messages.length === 1 && omitted === 0) return messages[0]!;
190
+ const summaries = messages.map(
191
+ (message) => message.split("\n", 1)[0] || "Background terminal result",
192
+ );
193
+ const header = [
194
+ `${messages.length} background terminal result${messages.length === 1 ? "" : "s"}:`,
195
+ ...summaries.map((summary) => `- ${summary}`),
196
+ omitted > 0
197
+ ? `- ${omitted} older result${omitted === 1 ? "" : "s"} omitted from this bounded batch; use bg_list/bg_status for retained details.`
198
+ : "",
199
+ ]
200
+ .filter(Boolean)
201
+ .join("\n");
202
+ const logsHeader = "\n\nLogs (newest tail):\n";
203
+ const truncationMarker =
204
+ "\n[batch logs truncated at the 48 KiB model-context limit; use bg_status or /ps for retained details.]";
205
+ const fixedBytes = Buffer.byteLength(
206
+ `${header}${logsHeader}${truncationMarker}`,
207
+ "utf8",
208
+ );
209
+ const logs = truncateTail(messages.join("\n\n"), {
210
+ maxBytes: Math.max(1, RESULT_BATCH_MAX - fixedBytes),
211
+ maxLines: DEFAULT_MAX_LINES,
212
+ });
213
+ const text = `${header}${logsHeader}${logs.content}${logs.truncated ? truncationMarker : ""}`;
214
+ return text;
215
+ }
216
+
182
217
  export function buildKillReport(results: ReadonlyArray<KillResult>) {
183
218
  return results
184
219
  .map((entry) => {