pi-anchorloop 2.1.0 → 2.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.
- package/README.md +4 -3
- package/README.zh-TW.md +4 -3
- package/package.json +3 -2
- package/pi/anchor-gate.ts +21 -77
- package/pi/book.ts +500 -0
- package/pi/second-thought.ts +203 -172
- package/pi/support/book.ts +698 -0
- package/pi/support/jev.ts +28 -0
- package/pi/support/phases.ts +290 -0
- package/pi/support/subagent.ts +5 -0
- package/skills/anchorloop/SKILL.md +14 -28
- package/skills/anchorloop/playbooks/autonomous-run.md +1 -1
- package/skills/coding/MINI-SDD.md +6 -6
- package/skills/coding/SKILL.md +2 -2
- package/skills/coding/playbooks/feature.md +1 -1
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ anchorloop core loop · principles · playbooks: autonomous-run, paus
|
|
|
17
17
|
shared grilling · domain-modeling · human-feedback · agent-terse
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
- **`anchorloop`** holds what is true for every task type: restate the task, pick the entry's playbook
|
|
20
|
+
- **`anchorloop`** holds what is true for every task type: restate the task, pick the entry's playbook, keep real work items in `todo` (its first create opens a loop), declare the loop with `anchorloop_stage` (kind, intent, anchor command) before the first change, work in small increments deleting before adding, verify against the anchor, and record friction in the loop book. The Reflexive pass hands reflected friction to `solid-loop`. `PRINCIPLES.md` names the principles playbooks cite and the user can steer with. The lifecycle playbooks cover unattended runs, clean pauses, and picking up a prior run.
|
|
21
21
|
- **Entry skills** each name the anchor for their task type and index their playbooks. A playbook has an ownership line, numbered steps, and a `Reply:` line that sets what the final report contains.
|
|
22
22
|
- **Shared skills** are called by entries: `grilling` for user-owned decisions, `domain-modeling` for terms and ADRs, `human-feedback` for reactions to working output, `agent-terse` for text only agents read: briefs, reports, and agent messages.
|
|
23
23
|
|
|
@@ -25,9 +25,10 @@ shared grilling · domain-modeling · human-feedback · agent-terse
|
|
|
25
25
|
|
|
26
26
|
| File | What it does |
|
|
27
27
|
| --- | --- |
|
|
28
|
-
| `pi/
|
|
28
|
+
| `pi/book.ts` | The loop book. The first `todo` create opens a loop, so it needs the `todo` tool of [`@juicesharp/rpiv-todo`](https://github.com/juicesharp/rpiv-mono/tree/main/packages/rpiv-todo#readme) (its `tool_result` details `{tasks, nextId}` and `rpiv-todo-state` entries); without it no loop opens. The agent declares only what the extension cannot observe with `anchorloop_stage`: `kind` (`quick`, `inline`, `reviewed`, `durable`, with an intent and the anchor command), `verify` (a check not run in bash), `fresh-read`, `friction` (`tried`, `found`, `led_by`), `reflect`, `skip`, `back`, `close`, and the query `read`. Everything else is read from evidence: production edits, the exit code of the bash call that ran the anchor command (failed-then-passed runs become retry pairs), reviewer `Blocking: P0=<n> P1=<m>` counts, summaries of `docs/specs/*/` artifacts as they are edited, and a returned subagent that Jev judges to have been a fresh-context read of the spec. A phase machine over that evidence gives each kind a fixed sequence (`quick` act → check; `inline` anchor → act → verify; `reviewed` adds review; `durable` adds decide → grill → spec → read → approve → design; every kind ends with reflect → close), and `read` shows `<kind> · phase <current> ✓ → <next> (missing: …) · skipped: …`. A phase that starts before an earlier one is complete warns once in one line and names the `skip` escape; phases regress when evidence changes or on `back`. The book also records each todo with its `anchor`, `ref` (`<path>#<heading>`, resolved to line ranges), and `followup` metadata. One YAML book per open loop lives under `~/.pi/agent/anchorloop/books/` (`ANCHORLOOP_BOOKS_DIR` overrides it), subagent books under `agents/` (a child that closes keeps its book for the main session); `session_start` sweeps books whose session is gone or that have not changed in 7 days. `close` never refuses over phases or friction, only while a pending or in-progress todo lacks `metadata.followup` (a ticket id, `memory`, or `drop — <reason>`). |
|
|
29
|
+
| `pi/anchor-gate.ts` | Inside an open loop, shows one warning line when `write` or `edit` starts the `act` phase while an earlier phase of the loop kind is incomplete: no anchor (`anchor` on the `kind` call, a `Reality anchor:` line, or a skip), and in a `durable` loop a missing decision, spec, fresh read, approval, or design, read from the `docs/specs/<folder>/` files (found from the book, the intent, or a todo `ref`). It warns again only when a new phase goes missing; the call still goes through. Outside a loop and in `quick` loops it stays silent. `.md`, `.txt`, and `.html` files, `scratch/`, `docs/specs/`, and temp files outside the project pass silently. Edits made through `bash` are not checked. |
|
|
29
30
|
| `pi/decision-tool.ts` | The `anchorloop_decision` tool appends single-line, spreadsheet-safe rows to a decision trail TSV, or a `start` row when a run picks up another run's trail. |
|
|
30
|
-
| `pi/second-thought.ts` |
|
|
31
|
+
| `pi/second-thought.ts` | Inside an open loop, asks Jev (TypeSafe's typed-decision model, through pi's classifier registry) to check the phase machine when a run settles and the loop book gained an entry since the last judgment, and at a `close` declaration. The program computes the next phase from the book; Jev sees the kind, intent, anchor, diff summary (≤1200 chars), `phase` (current, next, missing), verify state, open todos, the task list, and friction (a child's under its name), and judges only whether the current phase's completion truly holds (`complete`) and whether to `advance`, go `back`, or `ask-user`, plus `done`, the review tier still needed, and quality. The one-line advisory leads with `next → <phase>`; a retry pair with no friction entry adds a `friction` hint. The run continues once, only when Jev agrees to advance and the next phase is a verify, review, or fix; an advisory identical to the last one is not repeated, and a `full` rating after a clean review that follows the last change shows a note instead of a recheck. A run that called `MonitorCreate`, `LoopCreate`, `goal_wait`, or a `subagent` tool is waiting and is not judged. Without a TypeSafe key it does nothing, and outside a loop it never runs. In a subagent session (`PI_SUBAGENT_ID` from pi-herdr-agents, or `PI_SUBAGENT_CHILD` from pi-subagents) it registers nothing, since the agent that spawned it judges the result. |
|
|
31
32
|
| `pi/validate-tool-use.ts` | After each `write` or `edit`, validates the file in-process (`pi/support/validators.ts`, a port of `hooks/validators/`) and appends suggestions for `SKILL.md`, instruction files (`AGENTS.md`, `CLAUDE.md`, `GEMINI.md`, `rules/`), and `docs/specs/` artifacts. Needs no Python; the Python validators in `hooks/` remain for `validate_all.py` and the parity test. |
|
|
32
33
|
| `pi/ttsr.ts` | A port of oh-my-pi's Time Traveling Stream Rules. A Markdown rule in `~/.pi/agent/rules/` or `<project>/.pi/rules/` whose frontmatter has a `condition` regex is watched during the run. A match in streamed assistant text aborts the response, drops it, and retries with the rule injected; a match in tool arguments (the command for `bash`, the new text for `write` and `edit`) blocks the call with the rule as its reason. Frontmatter fields follow oh-my-pi: `condition`, `scope` (`text`, `thinking`, `tool`, `tool:bash`, `tool:edit(*.ts)`), `globs`, and `interruptMode` (`always`, `prose-only`, `tool-only`, `never`), plus `repeatMode` (`once` or `after-gap`) and `repeatGap`. A rule fires once per session by default. `astCondition`, `question`, and `agents` are not supported; a rule that uses them is skipped and reported. `/ttsr` lists the loaded rules. |
|
|
33
34
|
|
package/README.zh-TW.md
CHANGED
|
@@ -17,7 +17,7 @@ anchorloop 核心循環 · 原則 · playbook:autonomous-run、paus
|
|
|
17
17
|
共用 grilling · domain-modeling · human-feedback · agent-terse
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
- **`anchorloop`** 放每一種任務都成立的做法:重述任務、選入口的 playbook
|
|
20
|
+
- **`anchorloop`** 放每一種任務都成立的做法:重述任務、選入口的 playbook、只把真正的工作項目放進 `todo`(第一次 create 會開啟一個 loop)、第一筆修改前用 `anchorloop_stage` 宣告 loop(種類、intent、anchor 指令)、以小步前進且先刪後加、依 anchor 驗證,並把 friction 記在 loop book。Reflexive pass 會把已 reflect 的 friction 交給 `solid-loop`。`PRINCIPLES.md` 列出 playbook 引用的原則,使用者也可以用這些名稱中途修正方向。三本生命週期 playbook 處理無人看管的執行、乾淨的暫停,以及接手前一輪的工作。
|
|
21
21
|
- **入口 skill** 各自寫出這類任務的 anchor,並列出自己的 playbook。每本 playbook 先用一句話說明它負責什麼,接著是編號步驟,最後一行 `Reply:` 規定回報要包含什麼。
|
|
22
22
|
- **共用 skill** 由入口呼叫:`grilling` 處理使用者要做的決定,`domain-modeling` 處理術語和 ADR,`human-feedback` 處理對成品的反應,`agent-terse` 處理只給 agent 讀的文字:任務 brief、回報、agent 之間的訊息。
|
|
23
23
|
|
|
@@ -25,9 +25,10 @@ anchorloop 核心循環 · 原則 · playbook:autonomous-run、paus
|
|
|
25
25
|
|
|
26
26
|
| 檔案 | 作用 |
|
|
27
27
|
| --- | --- |
|
|
28
|
-
| `pi/
|
|
28
|
+
| `pi/book.ts` | Loop book。第一次 `todo` create 會開啟一個 loop,所以需要 [`@juicesharp/rpiv-todo`](https://github.com/juicesharp/rpiv-mono/tree/main/packages/rpiv-todo#readme) 的 `todo` 工具(它的 `tool_result` details `{tasks, nextId}` 和 `rpiv-todo-state` 條目);沒有它就不會開啟 loop。agent 只用 `anchorloop_stage` 宣告 extension 看不到的事:`kind`(`quick`、`inline`、`reviewed`、`durable`,附 intent 和 anchor 指令)、`verify`(不是指令的檢查)、`fresh-read`、`friction`(`tried`、`found`、`led_by`)、`reflect`、`skip`、`back`、`close`,以及查詢用的 `read`。其餘都從證據讀出:production 的修改、執行過 anchor 指令的那次 bash 的 exit code(失敗後又通過的執行會成為 retry pair)、reviewer 的 `Blocking: P0=<n> P1=<m>` 數字、`docs/specs/*/` 產出物每次編輯後的摘要,以及 Jev 判定為「全新 context 讀過 spec」的 subagent 回報。階段機依這些證據給每種 loop 固定的階段順序(`quick` 為 act → check;`inline` 為 anchor → act → verify;`reviewed` 加 review;`durable` 再加 decide → grill → spec → read → approve → design;每一種都以 reflect → close 結尾),`read` 顯示 `<種類> · phase <目前> ✓ → <下一個>(缺:…)· 已跳過:…`。前一個階段還沒完成就開始下一個階段,會用一行警告一次並指出 `skip` 的出口;證據改變或用 `back` 時階段會退回。book 也記錄每個 todo 的 `anchor`、`ref`(`<path>#<heading>`,解析成行號範圍)和 `followup` metadata。每個開啟中的 loop 有一份 YAML book,放在 `~/.pi/agent/anchorloop/books/`(可用 `ANCHORLOOP_BOOKS_DIR` 覆寫),subagent 的 book 放在 `agents/`(subagent 自己 close 時 book 留給主 session);`session_start` 會清掉 session 已不存在或 7 天沒有更新的 book。`close` 不會因為階段或 friction 而拒絕,只在還有 pending 或 in-progress 的 todo 沒有 `metadata.followup`(票號、`memory` 或 `drop — <原因>`)時拒絕。 |
|
|
29
|
+
| `pi/anchor-gate.ts` | 只在開啟的 loop 內:`write` 或 `edit` 開始 `act` 階段、而這種 loop 前面的階段還沒完成時,顯示一行警告:沒有 anchor(kind 呼叫的 `anchor`、`Reality anchor:` 這一行,或已跳過),`durable` loop 則還缺決策、spec、全新 context 的閱讀、核准或設計(從 `docs/specs/<資料夾>/` 的檔案判斷,資料夾由 book、intent 或 todo 的 `ref` 找出)。只有新缺了一個階段時才會再警告;呼叫仍會放行。沒有 loop 或是 `quick` loop 時保持安靜。`.md`、`.txt`、`.html` 檔、`scratch/`、`docs/specs/` 和專案外的暫存檔不會觸發。透過 `bash` 做的修改不檢查。 |
|
|
29
30
|
| `pi/decision-tool.ts` | `anchorloop_decision` 工具把一筆決策寫進 TSV 決策軌跡,每筆只佔一行,用試算表開啟也安全;接手別人的軌跡時寫一筆 `start`。 |
|
|
30
|
-
| `pi/second-thought.ts` |
|
|
31
|
+
| `pi/second-thought.ts` | 只在開啟的 loop 內:一次執行結束且 book 自上次判斷後多了條目時,以及宣告 `close` 時,透過 pi 的 classifier registry 請 Jev(TypeSafe 的型別化決策模型)複核階段機。下一個階段由程式從 book 算出;Jev 看到種類、intent、anchor、diff 摘要(≤1200 字元)、`phase`(目前、下一個、缺什麼)、verify 狀態、未完成 todo、任務清單和 friction(子 agent 的以它的名字列出),只判斷目前階段的完成條件是否真的達到(`complete`),以及要 `advance`、退回(`back`)還是 `ask-user`,另外還問 `done`、還需要哪一級 review 與品質。一行提醒以 `next → <階段>` 開頭;有 retry pair 卻沒有 friction 條目時,附上 `friction` 提示。只有 Jev 同意前進、而下一個階段是 verify、review 或 fix 時,才讓 agent 多跑一輪;與上一則相同的提醒不重複,最後一次修改之後已有無 blocking 發現的 review 時,`full` 評等只顯示備註、不要求複查。呼叫過 `MonitorCreate`、`LoopCreate`、`goal_wait` 或 `subagent` 工具的執行在等待中,不被判斷。沒有 TypeSafe key 時什麼都不做,沒有 loop 時不會執行。subagent session(pi-herdr-agents 的 `PI_SUBAGENT_ID`,或 pi-subagents 的 `PI_SUBAGENT_CHILD`)不註冊任何東西,因為由派它的 agent 判斷結果。 |
|
|
31
32
|
| `pi/validate-tool-use.ts` | 每次 `write` 或 `edit` 後在行程內驗證該檔(`pi/support/validators.ts`,移植自 `hooks/validators/`),替 `SKILL.md`、指令檔(`AGENTS.md`、`CLAUDE.md`、`GEMINI.md`、`rules/`)和 `docs/specs/` 產出物附上建議。不需要 Python;`hooks/` 的 Python 驗證器仍供 `validate_all.py` 和對照測試使用。 |
|
|
32
33
|
| `pi/ttsr.ts` | 移植自 oh-my-pi 的 Time Traveling Stream Rules。`~/.pi/agent/rules/` 或 `<專案>/.pi/rules/` 裡 frontmatter 帶有 `condition` regex 的 Markdown 規則,會在執行過程中被監看。assistant 串流文字命中時,中斷回應、丟棄那段輸出,並注入規則後重試;工具參數命中時(`bash` 看指令,`write`/`edit` 看新寫入的文字),直接擋下呼叫,以規則內容作為理由。frontmatter 欄位沿用 oh-my-pi:`condition`、`scope`(`text`、`thinking`、`tool`、`tool:bash`、`tool:edit(*.ts)`)、`globs`、`interruptMode`(`always`、`prose-only`、`tool-only`、`never`),另加 `repeatMode`(`once` 或 `after-gap`)與 `repeatGap`。預設每條規則每個 session 只觸發一次。不支援 `astCondition`、`question`、`agents`,用到的規則會被跳過並回報。`/ttsr` 列出已載入的規則。 |
|
|
33
34
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-anchorloop",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "Agent workflow skills for pi: one entry skill per task type, each with playbooks, over a shared Align → Anchor → Act → Verify loop, plus loop extensions (anchor gate, decision trail tool, Jev second thought, skill validator, stream rules).",
|
|
3
|
+
"version": "2.2.0",
|
|
4
|
+
"description": "Agent workflow skills for pi: one entry skill per task type, each with playbooks, over a shared Align → Anchor → Act → Verify loop, plus loop extensions (loop book, anchor gate, decision trail tool, Jev second thought, skill validator, stream rules).",
|
|
5
5
|
"author": "Wei Hung",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"type": "module",
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"extensions": [
|
|
33
33
|
"./pi/validate-tool-use.ts",
|
|
34
34
|
"./pi/ttsr.ts",
|
|
35
|
+
"./pi/book.ts",
|
|
35
36
|
"./pi/anchor-gate.ts",
|
|
36
37
|
"./pi/decision-tool.ts",
|
|
37
38
|
"./pi/second-thought.ts"
|
package/pi/anchor-gate.ts
CHANGED
|
@@ -1,84 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* pi extension: warns once when write or edit
|
|
3
|
-
*
|
|
2
|
+
* pi extension: inside an open loop, warns once per jump when write or edit starts the `act` phase while an
|
|
3
|
+
* earlier phase of the loop kind is incomplete: no anchor, or in a durable loop a decision, spec, read,
|
|
4
|
+
* approval, or design still missing. The phase machine in support/phases.ts decides what is missing; the
|
|
5
|
+
* warning names it and the `skip` escape. Outside a loop, or in a `quick` one, it stays silent.
|
|
4
6
|
*/
|
|
5
7
|
|
|
6
|
-
import { existsSync, realpathSync } from "node:fs";
|
|
7
|
-
import { tmpdir } from "node:os";
|
|
8
|
-
import { basename, dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
|
|
9
8
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const EXEMPT_IN_PROJECT = ["scratch", "docs/specs"];
|
|
13
|
-
const EXEMPT_ROOTS = [tmpdir(), "/tmp", "/var/folders"];
|
|
14
|
-
const EXEMPT_EXTENSIONS = [".md", ".txt", ".html", ".htm"];
|
|
15
|
-
|
|
16
|
-
export const WARNING =
|
|
17
|
-
"anchorloop anchor gate: a change ran before the reply stated " +
|
|
18
|
-
"`Reality anchor: <the exact check that observes the result, and its checkpoint>`.";
|
|
19
|
-
|
|
20
|
-
type Block = { type: string; text?: string };
|
|
21
|
-
type Message = { role?: string; content?: string | Block[] };
|
|
22
|
-
|
|
23
|
-
function statesAnchor(message: Message): boolean {
|
|
24
|
-
if (message.role !== "assistant" || !Array.isArray(message.content)) return false;
|
|
25
|
-
return message.content.some((block) => block.type === "text" && ANCHOR.test(block.text ?? ""));
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/** The real path of `path`, resolving symlinks such as /var → /private/var through its nearest existing parent. */
|
|
29
|
-
function real(path: string): string {
|
|
30
|
-
if (existsSync(path)) return realpathSync(path);
|
|
31
|
-
const parent = dirname(path);
|
|
32
|
-
return parent === path ? path : join(real(parent), basename(path));
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function within(root: string, target: string): boolean {
|
|
36
|
-
const rel = relative(root, target);
|
|
37
|
-
return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** Markdown, text, and HTML files, scratch and spec folders in the project, and temp files outside it; a project that lives in a temp dir is still gated. */
|
|
41
|
-
export function exempt(filePath: string, cwd: string): boolean {
|
|
42
|
-
if (EXEMPT_EXTENSIONS.includes(extname(filePath).toLowerCase())) return true;
|
|
43
|
-
const root = real(cwd);
|
|
44
|
-
const target = real(resolve(cwd, filePath));
|
|
45
|
-
if (within(root, target)) return EXEMPT_IN_PROJECT.some((dir) => within(join(root, dir), target));
|
|
46
|
-
return EXEMPT_ROOTS.some((tmp) => within(real(tmp), target));
|
|
47
|
-
}
|
|
9
|
+
import { currentKind, guarded, openLoop, replayTodos } from "./support/book.ts";
|
|
10
|
+
import { JumpMemory, computePhases, exempt, incompleteBefore, jumpLine } from "./support/phases.ts";
|
|
48
11
|
|
|
49
12
|
export default function (pi: ExtensionAPI) {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
if (
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
// User messages that steer a running loop end here too, so the gate closes for each of them.
|
|
68
|
-
pi.on("message_end", (event) => {
|
|
69
|
-
const message = event.message as Message;
|
|
70
|
-
if (message.role === "user") {
|
|
71
|
-
anchored = false;
|
|
72
|
-
warned = false;
|
|
73
|
-
}
|
|
74
|
-
else if (statesAnchor(message)) anchored = true;
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
pi.on("tool_call", (event, ctx) => {
|
|
78
|
-
if (anchored || warned || (event.toolName !== "write" && event.toolName !== "edit")) return;
|
|
79
|
-
const filePath = (event.input as { path?: unknown }).path;
|
|
80
|
-
if (typeof filePath !== "string" || exempt(filePath, ctx.cwd)) return;
|
|
81
|
-
warned = true;
|
|
82
|
-
ctx.ui.notify(WARNING, "warning");
|
|
83
|
-
});
|
|
13
|
+
const jumps = new JumpMemory();
|
|
14
|
+
|
|
15
|
+
pi.on(
|
|
16
|
+
"tool_call",
|
|
17
|
+
guarded((event, ctx) => {
|
|
18
|
+
if (event.toolName !== "write" && event.toolName !== "edit") return;
|
|
19
|
+
const filePath = (event.input as { path?: unknown }).path;
|
|
20
|
+
if (typeof filePath !== "string") return;
|
|
21
|
+
const book = openLoop(ctx);
|
|
22
|
+
if (!book || exempt(filePath, ctx.cwd) || currentKind(book.entries) === "quick") return;
|
|
23
|
+
const phases = computePhases(book, { cwd: ctx.cwd, todos: replayTodos(ctx.sessionManager.getBranch()) });
|
|
24
|
+
const missing = incompleteBefore(phases, "act");
|
|
25
|
+
if (jumps.fresh(String(book.header.id), "act", missing.map((status) => status.name))) ctx.ui.notify(jumpLine("act", missing), "warning");
|
|
26
|
+
}),
|
|
27
|
+
);
|
|
84
28
|
}
|