npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@alphacoder-v0/pi-loops

v0.22.5

Published

An automation layer for pi: cron + stateful loops with a triage inbox, maker/checker verification, dynamic triggers, MCP notifications and tools, lifecycle hooks, session archives. A non-invasive pi extension.

Downloads

2,647

Readme

pi-loops

English · 中文

给 pi 的自动化层,做成一个 extension:cron 和有记忆的 loop、一个分诊用的 inbox、动态 trigger 与 MCP 推送通知、生命周期 hooks。pi 的代码一行没动。

浏览器窗口里同一个 loop 的两次运行:第一次「0 findings,没什么可报的」,第二次报出一条新增的 TODO;右边是等着分诊的 inbox

一个 loop 醒来时带着上次留下的笔记,在干净上下文的子代理里干完活,把 finding 归档;你想看的时候去看,值得认真处理的那条再 claim 成真正的一轮。加上 --verify,第二个对抗式子代理会先把每条 finding 挑一遍,留下的才到你面前。关掉 pi 之后,这些照样跑。

为什么需要它

有些活本来就该在没人看着的时候干完:夜里新开的 issue、某个依赖冒出来的 CVE、main 上突然开始挂的那个测试。这些 agent 都做得了,但得你坐下来开口要——于是「开口要」本身成了那份活。

而顺手能想到的两种自动化,产出都落错了地方:定时把 prompt 塞进你正在进行的会话,等于拿你此刻不想看的东西打断你;写进日志,则是写进一个没人翻的文件。

"Stop prompting the agent. Build loops that prompt the agent for you." — Addy Osmani, Loop Engineering

所以这些活挪进一个永远不碰你对话的子代理里跑,跑出来的东西进 inbox。

你要敲的命令

pi-loops                              # 开一个会话——本地开浏览器,ssh 里开终端
pi-loops --tui                        # 猜错时强制终端
pi-loops --continue                   # 接着这个目录里最新的那个会话
pi-loops sessions [--all]             # 这个目录(或整台机器)记下的会话
pi-loops inspect <file>               # 别人发来的 .pisession,导入前先看清里面是什么
pi-loops upgrade                      # 装最新的发布版,当初从哪装就从哪装
pi-loops host status                  # 看一眼没有 pi 开着时在跑的自动化

上面这几条都得先有 pi-loops 这个命令,它在 上手 里装。其余都是会话里的斜杠命令(/cron、/inbox、/triggers、/goal),装上扩展重启 pi 就能用。

上手

1. 先确认前提

Node ≥ 22.6(pi 直接加载 TypeScript 源码),pi ≥ 0.84.3(实际在 0.85 上测;更老的 pi 会在加载时被明确拒绝,报一句写明所需版本的话,而不是一个缺导出的链接错误),以及一个真的能说上话的 provider——先跑一次 pi,发一句,确认有回答。pi-loops 会在你不看着的时候替你跑子代理;凭据没配好,别等到第二天早上看见空 inbox 才知道。

pi-loops 自己没有任何运行时依赖。

2. 装上

pi install npm:@alphacoder-v0/pi-loops                      # 从 npm 装,跟着新版本走
pi install git:github.com/alphacoder-v0/[email protected]    # 或者从 GitHub 装,钉住一个 tag
pi install /path/to/pi-loops                                # 或者本地检出;本仓库里就是 pi install .

几种装法挑一种,不要装两份。 两份副本注册同名工具,pi 会拒绝加载第二份并直接退出(Tool "cron_create" conflicts with …)。如果你在改这份代码,留本地检出那份。

重启 pi,装到这里就够了——那四个斜杠命令现在就能用。下面这段只跟 pi-loops 这个命令有关:浏览器窗口和几个 shell 子命令靠它,不要浏览器窗口可以先跳过。

pi install 把包放在 pi 自己的托管目录里、不进 PATH,所以此刻 pi-loops 这个命令还不存在。这一步不用你自己动手:pi install 之后的第一个 pi 会话会问你要不要把 pi-loops 放进 PATH,写下之后也一直替你维护着——包被 pi 挪到另一个位置,下一个会话会把启动器改回能用的样子,并用一行告诉你改了什么。答 no 就什么都不写,也不会再问第二次;改了主意,/pi-loops install-launcher 随时给你写一个:

/pi-loops install-launcher                         # 在 pi 里面,扩展本来就加载着
# 或者在 shell 里用绝对路径跑同一个命令——给那种根本不开 pi 的机器留的路
cd ~/.pi/agent/npm/node_modules/@alphacoder-v0/pi-loops    # 从 npm 装的
cd ~/.pi/agent/git/github.com/alphacoder-v0/pi-loops       # 从 GitHub 装的
node src/cli-entry.mjs install-launcher

几条路都会往 ~/.local/bin(或其它已在 PATH 里的目录,用 --dir 指定)写一个启动器。之后 pi-loops 在任何目录都能用。

3. 开一个会话

pi-loops                                           # 开会话,窗口按环境自己选

本地终端里开浏览器前端;ssh 里或者根本没有终端时,开 pi 本身——在对面机器上弹浏览器对谁都没用。猜错了就用 --web / --tui 指定,--continue 接着上次,其余参数原样交给 pi:

pi-loops --tui                                     # 终端版
pi-loops --continue                                # 这个目录里最新的那个会话
pi-loops --model anthropic/claude-opus-5 -e .

两个窗口都是完整的 pi 会话(浏览器那个是 pi --mode rpc 加一个网页),会话文件、--resume、模型、工具、扩展完全一样。细节见 docs/cli.md,浏览器前端必须保住的能力清单见 docs/web-ui-parity.md。

上次选的模型和思考等级它记着(存在 ui.json),下一个会话直接从它开始——--continue / --resume 除外,那种会话自己带着模型。

重新开始不用回终端:clear 开一个新会话,resume 回到这个项目里早先的某个会话,compact 压缩当前上下文并告诉你压成了多少。三个都是按钮,也都可以在输入框里直接敲(/clear 或 /new、/resume、/compact 保住 API 的形状)。谁都不删东西:离开的那个会话就是磁盘上的一个文件,resume 里列的就是它,标题是当时说的第一句话。

浏览器那个地址默认是 http://127.0.0.1:4173/(--port 可以换):端口不随机,token 存在文件里,所以这个地址明天还是它,可以直接收藏。第一次访问会留一个 cookie,之后再也看不到 token。已经开着一个时再敲 pi-loops,它会把已经在跑的那个打开给你。只有你自己用的机器上,--no-auth 可以把这层也去掉。

手机上用:最好的路是 tailscale serve --bg 4173——服务器仍然只绑 127.0.0.1,TLS 和身份都交给 tailnet,手机打开 https://<机器>.<tailnet>.ts.net/。同一个 wifi 里也可以 pi-loops --host 0.0.0.0(这时候再加 --no-auth,它当场拒绝)。手机第一次进什么都不用输:在已经登录的浏览器里点 add device,拿手机扫那个二维码就进去了,之后一直是登录状态(不想扫也可以输下面那六位数)。加到主屏幕会以独立窗口打开(有 manifest,没有 service worker)。

4. 第一个 loop

/cron add --stateful "0 9 * * *" 看一下这个仓库的 GitHub issues,报告自上次以来新开的和新关的

每天早上一个干净上下文的子代理带着上次写的笔记跑一遍,回复末尾给出 <loop-state>…</loop-state>(留给明天的笔记)和 <inbox>一行 finding</inbox>:状态写进一个 Markdown 文件,finding 进 inbox,你的对话一个字都不动。下面是一个盯着 TODO 文件的 loop 相隔半小时的两次运行,第一次:

cron todo · 5s · $0.000 · 0 findings · state updated
MD5 unchanged (`06ff2ec8af3668bb89ecc6580110ecad`), git rev still `15b6562`. No new unchecked items — nothing to report.
/cron trace todo · /inbox

中间提交了一次之后,第二次:

cron todo · 4s · $0.000 · 1 finding · state updated
md5 changed (06ff2ec8… → fe00aaad…). One new unchecked item appeared.
• TODO.md: new unchecked item — cache the /search results for 60s (commit 395a124 "todo: cache search results")
/cron trace todo · /inbox

第一次什么都没发现,于是什么都没说;第二次只注意到一处变化,也只报了那一处。这就是「两次运行之间留笔记」换来的东西——没有它,每天早上报的都是整份文件,你周四就不看了。它们归档的东西在 inbox 里等着,每行都写明项目、来自哪个 loop、什么时候:

/inbox
Inbox (acme-api, 3 new, times +00:00):
  1. [inb-aae794d5] TODO: rate-limit the /search endpoint  (acme-api, cron:todo, 2026-09-12 05:25)
  2. [inb-74b73c72] TODO.md: new unchecked item — cache the /search results for 60s  (acme-api, cron:todo, 2026-09-12 05:28)
claim with /inbox claim <n>, dismiss with /inbox dismiss <n>
/cron                                              # 这个项目里有什么、下次什么时候跑
/cron run 1                                        # 不用等到早上九点,现在就跑一次看看
/inbox claim 1                                     # 把第 1 条作为真实的一轮交给 agent
/inbox dismiss 2                                   # 不感兴趣
/inbox dismiss 3 那个文件是生成的,别管它             # 理由会放进这个 loop 下一次 run 的 prompt 里

5. 让它整夜跑之前

/cron cost                                         # 今天自动化花了多少

在 ~/.pi/agent/loops/config.toml 里先设个上限再依赖它:

[limits]
daily_budget_usd = 5.0

最后一个 pi 退出时,无头宿主会接手时钟,所以早上九点那次照跑(/cron host、pi-loops host status)。不想要就在同一个文件里写 [host] auto = false。

升级

pi-loops upgrade                                   # 装最新的发布版
pi-loops upgrade --check                           # 只看看有没有新的

它从这份副本的来源仓库读 release tag,和你正在跑的版本比,然后按这份副本当初的装法装最新那个。

从 GitHub 装的,只能靠它换版本:pi update --extensions 有意不做这件事,pi 钉住你写的那个 ref,并且只把克隆对齐到那个 ref。换版本是另一个决定,而做这个决定得先知道哪个 tag 最新——本该命令替你查,不是你查完再手打回来。从 npm 装、没写版本号的,pi update --extensions 本来就会升,upgrade 升完也保持不钉版本;钉了精确版本的,升完仍然钉着,钉到新版本上。

装完重启 pi(或者再跑一次 pi-loops)就生效。启动器不用重装:pi 放包的路径不随版本变——~/.pi/agent/npm/node_modules/<包名> 或 ~/.pi/agent/git/<host>/<owner>/<repo>。

版本就是 GitHub 上的 tag,同一个版本号也发到 npm,每个 tag 里有什么见 CHANGELOG.md。

卸载

pi remove npm:@alphacoder-v0/pi-loops              # 当初怎么装的就怎么删:npm: 包名、git: ref 或检出路径
pi remove git:github.com/alphacoder-v0/pi-loops
pi -e /path/to/pi-loops                            # 或者:只在这次启动试用,什么都不装
pi update --extensions                             # 对齐已安装的包

数据留在 ~/.pi/agent/loops,想清就删目录。

包里附带一个 skill(skills/pi-loops),让 agent 知道什么时候该用 cron_create、new_trigger 和 inbox。

docs/ 里分成 loops、triggers、goal、mcp、hooks、session-archive、cli、configuration、design、troubleshooting、web-ui-parity 几篇;变更记录在 CHANGELOG.md,贡献者说明在 AGENTS.md。

值得偷的 loop

/cron add --stateful --name main-watch "0 9 * * *" 读一下 main 上自笔记里那个 revision 之后的提交,动到公开 API 的报出来,并把新的 head revision 记进笔记

笔记里存一个 revision,下次运行拿它当起点,只有差出来的那一截值得占一条 inbox。

/cron add --stateful --name deps "0 8 * * 1" 跑 npm audit,只报笔记里还没有的 advisory id;报过的 id 追加进那个列表

每周一早上跑一次。watermark 不在别处,就在 loop 自己的笔记里——一份 id 列表,纯 Markdown,/cron state deps 能看能改。同一条 advisory 报过一次就不再报。

/cron add --verify --name ci every 30m 跑测试套件,只报相对笔记状态发生变化的测试

--verify 隐含 --stateful,在 findings 和你之间加一层子代理。flaky 测试正该拦在这一层。剔掉它的理由在 /cron trace ci 1 checker 里。

/new-trigger 当 ~/build.done 出现的时候,跑 cargo test 并把结果给我看

不看钟,看条件。子代理每 [triggers] poll_interval_secs(默认 600 秒)回来看一眼,条件成立就动手——默认只触发一次,要重复得明说。

/cron add in 45m 提醒我看一下这次部署

这条没有 --stateful,所以它是个普通任务,不是 loop。45 分钟后 prompt 直接落进当前这个对话,agent 就在这儿回你,不进 inbox。提醒该在对话里,夜里的报告不该。

/cron add --stateful --cwd /srv/acme-api --model openai/gpt-5.5 "0 7 * * *" 总结这个仓库自笔记以来的变化

任务在创建时就记下自己的目录和模型,所以它不绑在敲出它的那个窗口上。--cwd 让它在另一个检出里跑——绝对路径,或相对当前项目;这里没有 shell,~ 不会展开。--model 把模型钉死,/cron set <ref> --model - 解钉。

不用自己写的:recipe

/recipe                    有哪些打包好的 recipe,这个项目装了哪些
/recipe add issue-loop     把 issue tracker 跑成状态机:分诊成 agent brief、在 worktree 里实现、开 PR;推进和合并留给人
/recipe add autoresearch   按你写的研究合同每次跑一个实验,账本记下每次尝试,只凭 held-out 数据提议晋升
/recipe add deps-sweeper   每周一跑 audit 和 outdated:新的安全通告和落后一个大版本的依赖各报一条;propose 级在 worktree 里做小版本升级并开 PR

一个 recipe 是一组 loop 加上它们各自每次 run 都会读的 playbook。安装只问一个问题(自治级别 report / propose / act)、确认一次(每个要写的文件、setup 脚本全文、每一行 /cron add), playbook 拷到 .agents/skills/<name>/,通过 .git/info/exclude 排除在仓库之外——你随时可以改它, 下一次 run 就照改后的做。见 docs/recipes.md。

用法

会话里的命令分几摊:/cron 管任务,/inbox 管分诊,/goal 管「做到什么算完」,/triggers 管不看钟的那些规则,/session-share 把这次会话脱敏后发出去。

/cron add "*/30 * * * *" summarize the repo state               # 普通任务:到点结果出现在当前对话
/cron add --stateful "0 9 * * *" check the GitHub issues of this repo and report anything new or newly closed since the last run
                                                                # loop:子代理 + 跨轮笔记 + findings 进 /inbox
/cron add --stateful --name ci every 30m run the test suite; only report tests that changed status since your notes
/cron add in 10m 提醒我看一下测试结果                           # 会话级闹钟
/cron  ·  /cron list|ls|status      本项目的任务,[stateful] 标记;/cron all 看整台机器
/cron enable|resume|disable|pause|remove <n|id|name>
                                    remove 只认本项目的名字和编号,别的项目的任务要给完整 id(删除撤不回来)
/cron set <ref> …                   改已有任务而不换 id(笔记因此留着):--prompt、--schedule,
                                    以及 --model、--thinking、--timeout、--name 这几个钉子(`-` 解掉)
/cron run 1                         立刻跑一次(once 跑完即删,every 的间隔从现在重算,cron 的下次不变;停用的普通任务会被拒绝)
/cron state ci                      loop 的笔记(状态脊柱)
/cron runs [ci]                     最近运行,最新在前
/cron trace ci 2                    第 2 新那次运行的 transcript:prompt、工具调用、结果、回复
/cron scheduler                     谁在走时间、有哪些 run 在跑(`/cron status` 是 `/cron` 的别名)
/cron cost [today|7d|all]           自动化花了多少,按任务分,对照 [limits] daily_budget_usd
/cron disable --all                 本项目全停(--all-projects 停整台机器);/cron clear <ref> 清掉死进程留下的 running 标记
/cron gc                            清掉会话已消失的任务(本项目;--all 才跨项目)
/cron host [start|stop]             最后一个 pi 退出后接手时钟的无头宿主
/cron snapshot                      把「只有这个进程知道的状态」写进会话:哪些 MCP 连上了、暴露了什么工具、
                                    当前 active tools、hooks、谁拥有时钟。给非终端的前端读的

/goal 测试全部通过并且改动已提交                                # 每轮结束由评估器判断是否达成,未达成就送回去继续做(最多 8 次)
/goal  ·  /goal pause|resume|clear                              # 看状态 / 暂停 / 恢复 / 清除

/triggers run <id>                  立刻检查某条动态规则,不等它的轮询时隙
/session-share [--public]           把这次会话的 transcript 脱敏后传成 GitHub gist(走 gh),
                                    上传前先告诉你里面有什么、遮掉了几处、本地副本在哪

/inbox                              本项目的新 findings(每行标出项目;`--all` 看全部项目,与 /cron、/triggers 同一套作用域)
/inbox claim 1                      标记 claimed,并把它作为一个真实 user turn 交给主会话的 agent
/inbox dismiss 2 [理由]  ·  /inbox clear  ·  /inbox all     写了理由,报这条的 loop 下次 run 会看到;不写则沉默

/crontab 和 /loop 是 /cron 的别名。

调度表达式有四种写法:

  • 5 段 cron:本地时间,支持 */n、范围、列表、mon-fri、jan。
  • 别名:hourly / every hour / daily / once a day / weekly / 每小时 / 每天 / 每周,daily 与 weekly 落在本地 09:00。crontab 那套 @hourly / @daily / @weekly / @monthly 也认——注意 @daily 是午夜,不是 09:00。
  • 间隔:every 30m、every 24h。
  • 只跑一次:in 10m、at 2026-09-08T18:00。

/cron add 的额外 flags:--name、--cwd、--model provider/id、--thinking level、--tools a,b、--timeout 20m、--catchup / --no-catchup、--verify、--checker-model provider/id,外加 --loop(--stateful 的同义词)和 --inject(反过来,明确要一个普通注入任务)。

自然语言也行:注册了 cron_create(带 stateful/verify 参数)、cron_list、cron_remove(两步:先 confirm=false 预览,用户确认后再 confirm=true)、set_cron_job_state(启用会弹确认;禁用本项目的任务直接生效,禁用别的项目的也要确认)四个工具,「每小时看一下 CI,记住上次看到的,只报变化」会让 agent 自己建一个 stateful 任务。

时间用的是哪个钟

全都是这台机器的系统时间。 cron 表达式按本地时区匹配——0 9 * * * 是机器所在地的早上九点,不是 09:00 UTC——没有 per-job 时区。换机器、改 TZ,任务跟着走。

写进文件的时间戳也是同一个钟,后面缀着偏移,读的人不用猜:

{"startedAt": "2026-09-11T20:37:59.405+08:00", "finishedAt": "2026-09-11T20:38:12.880+08:00"}

(这是 +08:00 的机器写出来的;你那台写你那台的。)它和 2026-09-11T12:37:59.405Z 是同一时刻,解析得了一个的就解析得了另一个——包括旧版本写的,所以不需要迁移。差别在于:打开 runs.jsonl 看到的是你当时坐在电脑前的那个钟点,而且和 /cron 那行的 next 对得上。/cron 与 /inbox 在标题里写明偏移;离开这块屏幕的时间戳(进子代理 prompt、进模型读的工具结果)自己带着偏移。

只有两样仍然是 UTC,因为它们都不是给人读的时间:session 文件的文件名(装不下偏移里的 + 和 :),以及 pi 自己的 session header(格式由 pi 定)。

夏令时(实测,America/New_York,2026):

| | 会发生什么 | |---|---| | 3 月 8 日前跳,0 2 * * * | 那天没有 02:00,当天不跑,第二天照常 | | 11 月 1 日回拨,0 1 * * * | 01:00 出现两次,跑两次 |

Vixie cron 对这两种都有特例处理,pi-loops 没有:它匹配墙上的钟,钟是什么就是什么。必须一天一次且不能两次的任务,用 every 24h——它算流逝时间,不看日历。

at 的坑(是 JavaScript 的规则,不是我们的选择):

at 2026-09-08T18:00    → 本地 18:00
at 2026-09-08T18:00Z   → UTC 18:00
at 2026-09-08          → UTC 零点   ← 只给日期按 UTC,在北京就是当天 08:00

要表示时间就把时间写出来。另外 in 10m / at 在创建那一刻就被算成一个绝对时刻存下来,之后改时区不影响它;every 30m 是纯间隔,和时区、夏令时都无关。

一台机器:pi-loops 在运行 pi 的那台机器上跑,没有跨机器的概念(0.19.0 去掉了任务上的主机名戳;两台机器同步一个 $HOME 会各跑一遍,不支持)。要在别处看它,走 tailnet 开浏览器前端。stateful loop 的笔记是模型自己写的自由文本,里面常有 watermark(「看到这里为止」),而机器的时区会变(设了 TZ、带着笔记本出差、容器重建成 UTC),所以 prompt 会要求模型写时间时带上偏移。0.14.1 之前写下的笔记没有这个保障,时区变过又带 watermark 的 loop 值得用 /cron state <id> 看一眼。

Maker/checker:先质疑每条 finding,再给你看

/cron add --stateful --verify "0 9 * * *" check the repo issues and report anything new since the last run
/cron add --verify --checker-model openai/gpt-5.5 every 30m …        # --verify 隐含 --stateful;checker 可换模型
/cron trace ci 1 checker                                              # 看最近一轮 checker 的 transcript 与被剔除的条目

maker 子代理照常跑,产出 <inbox> findings。任务带 verify 时,这些 findings 不直接进 inbox,先交给第二个干净上下文的 checker 子代理。

checker 拿到任务目标、maker 的笔记和编号好的 findings,prompt 明着要它对抗式地核:假定每条都可能错、过期、重复或者根本不值得看,自己动工具验,再对每条给 <verdict n="i">keep|drop — 理由</verdict>,措辞不对就用 <rewrite n="i">…</rewrite> 改。

  • checker 自己挂了怎么办? 失败或超时就 fail-open:findings 全部进 inbox,标记未核实,卡片里注明。坏掉的 checker 不能让 loop 失声。
  • 只有 keep 的进 inbox,/inbox 里带 ✓,claim 时把 checker 的理由一并交给主 agent。
  • drop 的连理由记进 run log,运行卡片里以 ✗ 列出,/cron runs 显示 checker kept k/n。
  • 没给判定的条目也进 inbox,只是不带 ✓(unreviewed)。
  • 笔记是 maker 的,checker 只裁决输出——状态脊柱它一个字都不碰。
  • 两份 transcript 一起留在 sessions/<id>/。

真机验证:让 maker 故意夹一条「src/does-not-exist.ts 存在且有 900 行」,checker 用 ls 核实后把它剔除,理由写 deliberately false per loop goal,另外两条真实 finding 带 ✓ 进了 inbox。

Session 归档:连同自动化一起带走

pi 内置的 /export 只导 HTML/JSONL 对话,/import 只导回对话——一个会话真正的一半留在原地。这里做成 /session-export 与 /session-import(pi 已占用 /session),格式是 .pisession:对话、这个项目的任务与规则、以及每个 loop 攒下的笔记,一起打包带走。

/session-export [path] [--exclude-triggers]          默认 ./pi-session-<id前16位>.pisession
/session-import <path> [--activate-triggers=off|ask|on] [--cwd <dir>] [--resume]
backup.pisession                 无压缩 ustar,0600,拒绝覆盖已有文件
  manifest.json                  schema / 时间 / pi 与 pi-loops 版本 / 来源 / session.jsonl 的 sha256 / 敏感性声明
  session.jsonl                  pi 的 session 文件原样
  sidecars/cron.json             本项目的 cron 任务
  sidecars/triggers.json         本项目的 trigger 规则(--exclude-triggers 可去掉)
  loops/<job-id>.md              每个 stateful 任务的笔记(状态脊柱)

导入的时候会重写这些:

  • session 换新 id,cwd 改成目标目录,header 里记下 importedFrom 来源。
  • 任务和规则默认全部 disabled。--activate-triggers 默认 ask:导入完之后弹一次确认,问你要不要把源会话里原本 enabled 的那些重新打开;off 不问,on 直接打开。
  • 运行标记、错误、重叠计数清零。
  • id 撞上本机已有的就重新生成,loop 状态文件跟着新 id 走。
  • 非 stateful 任务重新绑定到导入的 session。

--resume 直接切到导入的会话,否则给出 pi --session <path>。校验有四道:manifest schema、session.jsonl 校验和、路径穿越、各部分大小上限(session 50 MiB、sidecar 2 MiB)。

真机验证:导出后 tar tf 看到四个成员;同机导入时 job id 冲突被重生成,loops/*.md 内容出现在新 id 的状态文件里,确认后任务恢复 enabled。

Trigger 与 notification

/new-trigger when ~/build.done exists, run cargo test and show me the result   # 自然语言,agent 调 new_trigger 建规则
/new-trigger 当 $HOME/helloworld 存在的时候,打印它的内容                          # 中文分隔词同样支持
/triggers                    status:规则统计、轮询器归属、上次检查结果、推送源数量
/triggers rules [--all]      规则列表:id [enabled, fire_once|repeat, audit_only|promote_to_chat, fired_at] when … -> …
/triggers sources            trigger 源:本地轮询器 + 每个 MCP 服务器的连接状态、queued/dropped/deduped、最近错误
/triggers enable|disable|remove <id>  ·  /triggers remove --all(本项目)| --all-projects(本机全部)
/triggers running  ·  /triggers abort <trace>|--all
/triggers audit [N]          最近 N 条 audit:accepted / deduped / running / completed / failed / promoted

语义:

  • 规则默认 fire once,匹配后自动 disabled 并记 fired_at;/triggers enable 会复位。要重复触发得明确要求,agent 会传 fire_once=false。
  • 只要还有 enabled 的规则,每 poll_interval_secs(默认 600)就起一个干净上下文的子代理,把全部规则和事件 JSON 交给它。它自己用工具查文件、查命令输出、查时间,命中就执行那条 action,回复里带 matched dyn-…;没命中就回固定的一句 no dynamic trigger rule matched。
  • 结果默认只落在 TUI 卡片和 audit 里;规则带 promote_to_chat 时,它以 [Trigger <trace_id>] … 开头插进主对话,后面的 turn 看得见。
  • 5 分钟 dedup 窗口,listChanged 类通知用稳定 key 折叠成最新一条,自定义通知必须带 _meta.pi_dedup_key,否则在源头丢弃并计数。
  • 自然语言里出现「每小时 / daily / cron / 定时任务」之类时,new_trigger 会拒绝并让 agent 改用 cron_create。
  • 工具:new_trigger(condition / action / spec / fire_once / promote_to_chat)、list_triggers、remove_trigger(id | all)、set_trigger_state,描述与返回文本固定。建规则、删规则、重新启用自动化这三件事都要你点头:工具把原因摆出来等你确认,没有 UI 可点的场合(子会话、无头宿主)一律拒绝。
  • 规则是机器全局的(带 cwd),/triggers rules 默认只看本项目;子代理进程里不注册这些工具,所以一条 trigger 的动作造不出新的 trigger。
  • 轮询间隔:--trigger-poll-secs 60 或 config.toml 的 [triggers] poll_interval_secs。

MCP 推送作为 trigger 源

pi 没有内置 MCP 客户端,这里自带了一个只消费通知的最小实现。配置文件 ~/.pi/agent/loops/mcp.toml,项目级 <repo>/.pi/mcp.toml 同名覆盖(需要该项目已被 pi 信任):

[[server]]
name = "filesystem"                       # kind 默认 stdio
command = "npx"
args = ["-y", "@modelcontextprotocol/server-filesystem", "/path"]

[[server]]
name = "hub"
kind = "streamable_http"
endpoint = "https://example.com/mcp"      # 必须 https,127.0.0.1 除外
auth = { kind = "bearer", token_keychain_ref = "PI_MCP_TOKEN_HUB" }  # token 不写在文件里:查 pi 的凭据库,或 PI_MCP_TOKEN_* 前缀的环境变量
request_timeout_ms = 30000                # 默认 30s
sse_idle_timeout_ms = 60000               # 事件流静默超过此值就重连,默认 60s
body_cap_bytes = 1048576                  # 响应体上限,默认 1 MB
reconnect = { initial_ms = 500, max_ms = 30000, max_attempts = 10 }   # 500ms 起指数退避到 30s,这里写的是试 10 次;不写 max_attempts 则默认 20 次,之后放弃,等 pi 重启
inject_summary = true                     # 摘要直接进主对话,不起子代理、不花钱
# inject_and_run = true                   # 摘要进主对话并跑一个 turn,让主 agent 响应

语义:

  • 通知 → trigger 的映射:tools/resources/prompts listChanged 用稳定 key 折叠成最新一条;resources/updated 按 uri 分 key;自定义通知必须带 _meta.pi_dedup_key,否则在源头丢弃并计数,/triggers sources 显示 dropped custom notification "…": missing …。
  • 摘要里只有方法名,和几个脱敏过、长度封了顶的字段(notifications/resources/updated uri=…、自定义通知的 _meta.pi_summary 截到 200 字)。原始 params 绝不写进 audit 或对话。
  • 逐台服务器校验:一台配置错只报 mcp server '<name>' failed: …,其它照常连接。stdio 不得带 endpoint/auth;超时、上限、重连延迟必须为正;auth 只支持 bearer。
  • streamable_http:POST initialize 与 initialized,然后长连 GET 事件流;POST 响应本身是 SSE 时也解析;断流带 Last-Event-ID 续传。
  • 不加任何 inject 标记的服务器,其通知交给动态规则子代理评估。

stdio 服务器崩溃后自动重连;Mcp-Session-Id 会话头;服务器发来的 request 都回得上(ping 回 {},其它回 method-not-found);stdio 可加 env 表。

MCP 工具注册给 agent

mcp.toml 里每台服务器握完手就 tools/list,把工具逐个 pi.registerTool 给 agent:名字用服务器给的原名(和已有工具撞名就加 <server>_ 前缀),参数 schema 原样透传,tools/call 的 text / image / resource 内容映射成 pi 的工具结果,isError 变成工具错误,用户中断时给服务器发 notifications/cancelled。

连接是每个 pi 进程自己的事——工具得在手边才用得上,子代理也一样。推送通知则是交互式 pi 各自收各自的,机器级的 dedup.json 保证同一条只处理一次;子代理不注册通知钩子,收不到推送,万一还有触发跑到 hop ≥ 1,运行时把它记成 cycle_suppressed。

/triggers sources 显示每台服务器注册了哪些工具。

真机验证:agent 调假服务器的 echo,拿到了返回。

生命周期 hooks

~/.pi/agent/loops/hooks.toml:

  • 事件:agent_start/agent_end/turn_start/turn_end/message_start/message_update/message_end/tool_start/tool_update/tool_end/compaction,外加一对 run_start / run_end——定时运行发这一对,交互式 pi 和无头宿主都发。

    有人值守时,一次定时运行本身就是对话里的一轮,agent_* 够用。无人值守时没有对话可言,宿主里一个 agent_* 都不会发,于是最该让外部系统知道的那类活,反倒成了 hooks 看不见的活。硬把定时运行也算进 agent_*?那你为自己的轮次写的规则会突然替自动化触发。

    run_end 的 payload 带 run_ok / run_findings / run_error / run_cost_usd,所以「loop 挂了通知我」是 [ "$PI_RUN_OK" = false ],不是拿摘要做字符串匹配。

  • 字段:command、webhook(可同时用,先命令后 webhook)、timeout_ms(默认 5000)、enabled、cwd = project|loops|home、on_failure = warn|ignore、tool 过滤、[hook.headers]。

  • payload 去两个地方:webhook body,和 $PI_HOOK_PAYLOAD 指的那个文件。每次都是同一组键,这次用不上的写 null:event、session_id、cwd、model_provider、model_id、thinking_level、source,消息的 message_kind、message_summary、assistant_event,工具的 tool_call_id、tool_name、tool_is_error、tool_args、tool_result_summary,压缩的 compaction_trigger、compaction_tokens_before、compaction_summary、compaction_failed,定时运行的 run_job、run_id、run_ok、run_findings、run_error、run_cost_usd。摘要截到 2000 字符,thinking / tool call / image 用占位符。不脱敏——这是给你自己的脚本看的。

  • 环境变量用 PI_* 前缀,只在有值时设置。

  • 单条规则写错只跳过那条并提示,其它照常加载。同一事件的规则按文件顺序串行执行,不阻塞 agent。

  • 超时或 Ctrl-C 时杀掉 hook 的整棵进程树,不只是 sh。

  • 项目级 <repo>/.pi/hooks.toml 默认忽略;用户 hooks.toml 顶层写 allow_project_hooks = true、或 config.toml 同名键、或 PI_ALLOW_PROJECT_HOOKS=1(true 也算,其它值都算关)才启用。在无头宿主里还额外要求 pi 对那个任务的 cwd 有精确的信任记录——allow_project_hooks 是对「你会打开的项目」的表态,不是对「模型顺手指过去的目录」的表态。

  • hook 打到 stdout 的东西写进本进程日志(logs/pi-<pid>.log,宿主写 host.log),截到 4000 字符——「打印点东西再去看」这个最常用的调试手段现在是通的。

  • 子代理进程里不触发 hooks,只有你交互的那个 pi 触发。

子代理每次收到的 prompt

You are running the recurring loop "ci" (current run started <这次运行开始的时间,跑它那台机器的钟,带那台机器的偏移>; write any time in your notes with its offset, as that one has). This is a background run: nobody is watching, and your final reply is parsed by a program.

[loop-state] (your notes from the previous run of this recurring job)
<state 文件内容,或 (first run)>
[/loop-state]

<你的 prompt>

Output protocol (mandatory):
- End your reply with <loop-state>notes for the next run</loop-state> — it REPLACES the saved state; keep it under 2000 characters and make it the information your next run needs (baselines, ids already seen, watermarks).
- For each finding a human should act on, emit <inbox>one concise line</inbox>. No findings → no inbox tags; do not invent work.
- Keep everything after the last tool call short so the tags are not truncated.

它是怎么搭起来的

| 机制 | 怎么实现的 | 存放位置 | |---|---|---| | 状态脊柱 — 每个 loop 一份 ≤2000 字符的笔记,run N+1 读到 run N 写的 | <loop-state> 标签解析后写入 Markdown,下次运行拼进 prompt 头部 | ~/.pi/agent/loops/state/<id>.md | | maker/checker | --verify:第二个对抗式子代理逐条核实 findings,drop 的不进 inbox | runs.jsonl 的 checker 字段 | | 路由层 — 产出既不打断你也不沉进日志 | <inbox> 标签 → JSONL 追加,new → claimed/dismissed 生命周期,状态栏 Inbox: N new 角标;条目 id inb-<32hex>、来源 cron:<id或name>、/inbox 与 /inbox all 的行格式、错误措辞都固定下来,已核实的多一个 ✓ 标记 | ~/.pi/agent/loops/inbox.jsonl | | stateful job 走 SubAgent、永不碰主对话 | 同进程子会话(pi SDK),干净上下文,共享父会话的 MCP 客户端、扩展、model/thinking;完整 transcript 保留在 sessions/<id>/,/cron trace 可看 | ~/.pi/agent/loops/sessions/<id>/*.jsonl | | trigger 的产出看得见、可追溯 | 每次运行结束在 transcript 里落一张卡片(耗时、成本、findings、摘要),run log 记退出码与用量 | runs.jsonl | | 预览与 audit 一律脱敏 | src/redact.ts 一套 redactor,列表、卡片、run log、trace 全部过一遍 | — | | 普通 cron 走 inject-and-run | 不加 --stateful 的任务到点用 pi.sendUserMessage 注入创建它的会话,消息带 [Trigger <trace>] 前缀(空闲直接发,忙则 followUp 排队) | — | | 每次 add / enable / disable / remove 写 cron_control_plane audit(含 actor 是 slash 还是 tool) | 同名 custom entry 写进 session(不进 LLM 上下文) | session 文件 | | cron 运行也走 trigger runtime | /triggers running 看得见、/triggers abort 可中止、/triggers audit 有记录;运行 id 就是 trace id | triggers-audit.jsonl | | 输出协议是纯文本不是 API | 同一套 prompt 措辞,任何能听指令的模型都能跑 | src/protocol.ts | | 标签解析永不让 run 失败 | 标签缺失/截断 → 状态不动、inbox 不进,run 照样记完成 | — | | 一切有界 | state ≤ 2000 字符,finding ≤ 500 字符,每 run 最多 16 条,prompt ≤ 8 KB | — |

自动化要活过开它的那个窗口

定时任务值不值得信,取决于你关掉编辑器之后会发生什么。所以这里把「pi 重启过」当成常态:

  1. 任务是机器全局的,存在 ~/.pi/agent/loops/jobs.json,不绑会话、不绑目录(每个任务记住自己的 cwd,子代理在那里跑)。任何目录里打开的任何 pi 都能看到并执行 stateful 任务;普通任务因为要注入对话,只在创建它的那个会话里触发(--resume 回来就继续),会话没了,leader 会把它停用,/cron gc 清掉;子代理创建的普通任务归它所服务的那个父会话。

  2. loop 归本机唯一的 leader 跑;动态检查归「开在那个项目里的那个 pi」跑。 scheduler.json 里放 pid 加心跳,30 秒一 tick;leader 退出或者崩了,其它 pi 在下一 tick 接手。

    每个进程每 tick 往 presence/ 登记自己的 pid、会话和 cwd。一个项目的规则检查和推送评估,就交给开在那个项目里的 pi——优先创建规则的那个会话,其次 pid 最小的那个——所以 promote_to_chat 一定落在对的对话里。项目里一个 pi 都没开,才轮到 leader 代跑,结果进 inbox。

    轮询间隔看共享的 polls.json,全机一份,交接不会重复检查。

  3. loop 错过的 tick 默认补发一次(多次错过折叠成一次,就像 systemd Persistent=true),--no-catchup 关掉。普通注入任务反过来,默认不补,要补得写 --catchup:它是往对话里插一句话的,迟到的提醒不如不提醒。

  4. 没有过期时间。 任务只在你 /cron remove 时消失。

  5. 有 run log 和完整 transcript。 runs.jsonl 记每次运行的退出码、耗时、成本、finding 数、有没有更新状态;子代理的 session 文件每个 loop 保留最近 20 份,/cron trace <job> [k] 直接看它调了什么工具、看到了什么,pi --session <文件> 可以整个接管回放。

子代理在父进程内运行:共享父进程活着的 MCP 服务器实例(浏览器标签页、数据库会话都是同一份)、-e 扩展、system prompt、skill 标志与模型,同一项目下继承信任。「一个 pi 进程都没有」也解决了:本机最后一个交互式 pi 退出时,如果还有 loop、规则或 MCP 服务器,它会拉起一个无头宿主进程(src/host.ts,同一套存储、同样的进程内运行器、自己的 MCP 客户端)继续走时钟;本来要进对话的结果进 inbox;下一个打开的 pi 抢回时钟,宿主退出。/cron host [start|stop] 看和控制(start = 即使 [host] auto = false 也在本 pi 退出时交接),host.log 是它的日志,重启机器后要等下一个 pi 打开才会再交接。

对 pi 的无侵入性

  • 只用 pi 公开导出的扩展 API:ExtensionAPI 的事件、命令、工具、sendMessage/sendUserMessage/appendEntry/exec/registerFlag,以及 getAgentDir、readStoredCredential、@earendil-works/pi-tui 的 Box/Text、typebox。
  • pi 的安装目录一个文件都没改(find <pi包> -newer package.json 为空);~/.pi/agent 下只多了 settings.json 的一行 extensions 和运行时才会创建的 loops/ 目录。
  • 不 monkeypatch,不碰私有字段;子代理是用 pi 公开 SDK 在同进程里开的会话,不起子进程。卸载就是删掉 settings.json 里那一行。
  • 接进去的地方就这么几个:定时器在 session_start 起、session_shutdown 停,状态是磁盘上的 Markdown,findings 进全局 JSONL inbox,/inbox claim 用 pi.sendUserMessage() 把一条 finding 变成主会话里一个真正的 agent turn。

存储

| 路径 | 内容 | |---|---| | ~/.pi/agent/loops/jobs.json | 所有任务(全局;PI_LOOPS_DIR 可改根目录) | | ~/.pi/agent/loops/state/<id>.md | loop 状态,纯 Markdown,可以 cat、可以手改 | | ~/.pi/agent/loops/inbox.jsonl | 全局 inbox,追加式,坏行跳过不删;超 1 MB 丢掉最旧的已分诊条目(new 的永不丢) | | ~/.pi/agent/loops/runs.jsonl | run log,超 1 MB 自动保留后半 | | ~/.pi/agent/loops/spend.json | 轮转掉的那部分花费按天留一份,预算上限不会因为 run log 被截断而失效 | | ~/.pi/agent/loops/logs/pi-<pid>.log | 每个 pi 进程的自动化诊断,超 2 MB 保留后半,只留最近五个进程(进程还活着的那份不算) | | ~/.pi/agent/loops/sessions/<id>/*.jsonl | 子代理完整 transcript,每个 loop 保留最近 20 份 | | ~/.pi/agent/loops/scheduler.json | 当前 leader 的 pid / 心跳 | | ~/.pi/agent/loops/triggers.json | 动态 trigger 规则(全局,带 cwd) | | ~/.pi/agent/loops/triggers-audit.jsonl | trigger audit,超 2 MB 保留后半 | | ~/.pi/agent/loops/sessions/triggers-<项目>-<哈希>/*.jsonl | 动态检查子代理的 transcript,每个项目一个目录(目录名带项目路径的哈希,同名的两个项目不会共用),保留最近 40 份 | | ~/.pi/agent/loops/config.toml | [triggers] poll_interval_secs、allow_project_hooks | | ~/.pi/agent/loops/mcp.toml · hooks.toml | MCP 推送源、生命周期 hooks |

跨进程写入靠 mkdir 锁 + 原子改名,没有原生依赖。

代码结构

src/extension-entry.ts      pi 加载的入口:先查 pi 版本(src/pi-floor.ts),够了再导入 src/pi-loops.ts
src/pi-loops.ts             扩展本体:命令、工具、生命周期、状态栏角标、面板
src/cli.ts                  `pi-loops`:会话入口(网页或终端)与 sessions / inspect / export / import / host
src/cli-entry.mjs           bin
src/ts-entry.mjs            .mjs 入口怎么加载本包的 .ts:Node 自己剥类型,装在 node_modules 下就借 pi 的 jiti

流水线
src/scheduler.ts            tick 循环、leader 选举、到期判定、错过补发、并发/重叠控制、子会话执行、写回
src/trigger-runtime.ts      动态规则运行时:admit、投递(sub_agent / inject_summary / inject_and_run)、fire-once、promote、audit
src/triggers.ts             规则本身:解析、prompt、id 提取、存储、dedup 窗口
src/goal.ts                 /goal:停止条件状态机、评估器 prompt、续跑预算
src/tools.ts                cron / trigger 工具定义(交互会话、子会话、宿主三处共用)
src/protocol.ts             <loop-state>/<inbox>/<verdict> 协议与上限
src/schedule.ts             cron / every / once 解析与到期计算
src/job-edit.ts             /cron set 的决策:锚在哪个时间戳上、下一次什么时候跑
src/args.ts                 /cron add 参数解析
src/thinking.ts             pi 认得的思考等级,以及在每个输入口校验它
src/slots.ts                子代理并发池——两条流水线和 /goal 共用这一个
src/job-health.ts           哪些 loop 算在失败,以及说出这件事的那行角标 / 摘要

跑一个子代理
src/runner.ts               运行器接口、结果形状、父会话可继承的标志
src/sdk-runner.ts           用 pi SDK 在同进程里开会话的那个实现(每次运行一个 createAgentSession)
src/danger.ts               无人值守运行的危险命令策略
src/subagent-guard.ts       把该策略装进每个子会话的合成扩展
src/transcript.ts           把子代理 session 文件压成可读的几十行

没有 pi 开着的时候
src/host.ts                 最后一个 pi 退出后接手时钟的无头宿主
src/host-entry.mjs          宿主实际被拉起的那个进程,好让 host.ts 在 node_modules 下也能加载
src/host-control.ts         host.json、拉起/停止、交接判定
src/host-control-channel.ts 宿主的 unix socket:snapshot、abort、stop
src/host-runtime.ts         宿主里跑的东西(调度器 + triggers + 按请求服务的工具宿主)
src/presence.ts             每个活着的 pi 登记一份:谁开在哪个项目,结果该落进哪个对话
src/register-pi.mjs         在 pi 之外解析 pi 包的 node --import 钩子(宿主、测试用)
src/pi-resolver.mjs         这个钩子去哪里找

存储
src/store.ts                jobs.json、state/*.md、runs.jsonl、sessions/
src/inbox.ts                inbox.jsonl
src/archive.ts              .pisession 归档:无依赖 tar 读写、导出、导入改写
src/lock.ts                 文件锁、原子写、pid 存活
src/paths.ts                realpathish:解析到最深的那个存在的祖先,所以还不存在的路径按它父目录的方式解析
src/snapshot.ts             pi_loops_snapshot 的指纹,以及什么样的变化才值得写一条新条目
src/config.ts               config.toml 与环境变量覆盖
src/ui-prefs.ts             ui.json:会话之间记住的偏好,按键合并而不是整份覆写

连接与输出
src/mcp.ts                  最小 MCP 客户端(stdio / streamable_http)、通知→trigger 映射、工具注册
src/mcp-pool.ts             按需连接别的项目的 MCP 服务器,借给那个项目的运行
src/hooks.ts                hooks.toml 的加载与执行(命令 + webhook)
src/share.ts                /session-share:脱敏后的 Markdown transcript,交给 `gh gist create`
src/redact.ts               脱敏
src/log.ts                  logs/pi-<pid>.log:轮转,以及谁有资格写
src/trust.ts                pi 是否信任某个目录——读那个项目的东西之前先问它
src/toml.ts                 TOML 子集解析器(无依赖)
src/version.ts              归档、payload、`/pi-loops` 都报这一个版本号

src/web.mjs                 单文件、零依赖的浏览器前端:跑 `pi --mode rpc` 并把协议透传给网页
skills/pi-loops/            让 agent 知道什么时候该用 cron_create、new_trigger 和 inbox
examples/                   零依赖的 MCP 推送服务器,和指向它的 mcp.toml
test/                       node --test;test/fake-runner.ts 与 test/fake-mcp-server.mjs 替身模型和 MCP 服务器
scripts/                    typecheck.mjs 与 lint.mjs——都通过 npx 借 TypeScript,不引入依赖
npm run ci      # typecheck + lint + check:scripts + 全套单元/集成测试,与 .github/workflows/ci.yml 跑的一致

scripts/lint.mjs 只管两类错误:floating promise(pi 不装 unhandledRejection handler,没人 await 的 promise 一旦 reject 会直接杀掉整个会话,void x() 不算豁免)和没写注释的空 catch {}。两条都靠 TypeScript 的类型信息判断。

边界与已知取舍

  • 子会话继承父会话的 model/thinking(任务固定了模型则用固定的);子会话里不再加载这个扩展本身,不会递归。
  • 子会话没有 UI 就没有审批弹窗:需要确认的工具在子会话里 fail-closed 拒绝。没人能点「同意」的时候,默认答案只能是「不」。要收紧就用 --tools read,grep,ls。
  • 状态栏角标最多五段,按需出现,之间用 · 隔开:Inbox: N new、N job(s) failing (<name> ×K)、running: <loop>(多个用逗号连起来,trigger 检查在这儿显示成 trigger-check)、mcp: 1 source down / mcp: 2 sources down、loops standby。
  • 同一任务上一轮还在跑时新 tick 直接跳过并计数(overlap-skipped),不排队。
  • 同时最多 3 个子代理在跑([cron] max_concurrent_runs)——loop 运行、trigger 检查、/goal 评估器共用这一个池子(src/slots.ts),三条路加起来一共 3 个。/goal 评估器和 /cron run 占槽但永不被拒(你直接要的东西,机器悄悄不做和从没设过是一样的),所以 /triggers running 有可能显示 4 of 3 sub-agent slot(s) in use。
  • [limits] daily_budget_usd 不只挡派发,也会停掉正在跑的运行:算的是运行日志里已落账的花费加上本进程在飞的(并行的兄弟运行,以及 --verify 那对共用 runId 的 maker/checker)。被预算停掉记为 aborted 而不是 failed,所以时隙还欠着、失败连击不累加。
  • inbox 的状态改写是「最后写者赢」:两个进程同时处理同一条时,后写的那个说了算,不做冲突检测。
  • 常驻面板是编辑器上方的 widget:Triggers 规则最多 5 条 + Polling 最近一次检查、Inbox N new、Cron 启停统计与任务最多 5 条、MCP 各服务器连接状态与工具数——这几段各自没内容时不显示;Hooks(cli_hooks 规则数与事件,没有就写 none)和 Runtime(dedup · cycle suppress · fire-once rules · inject-and-run)总是画出来。规则、任务、新 findings、MCP 服务器全都没有时整个面板不显示;/cron panel off 或 /triggers panel off 关闭,偏好存在 ui.json。
  • 跨设备访问不放 broker 在中间:让你从别处够到那个还待在原地的前端——tailscale serve 在 tailnet 上终结 TLS、代理到本机 loopback,手机就能开,中间不经过任何第三方;同一网段则用 --host。手机第一次进用配对码和二维码(docs/cli.md)。真正没覆盖的是「手机两个网络都不在」,那种情况才需要中继。
  • 这是一个自动化层,不是一个 agent:写代码、读网页、管 skill 都是 pi 自己的本事,这里只决定什么时候、在哪里、带着什么上下文去跑它们。

致谢

灵感与重写来源:pie。

许可

MIT