pi-anchorloop 2.4.1 → 2.6.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 +6 -6
- package/README.zh-TW.md +6 -6
- package/hooks/validate_tool_use.py +1 -1
- package/hooks/validators/constants.py +1 -0
- package/hooks/validators/durable_validator.py +4 -17
- package/hooks/validators/rule_validator.py +3 -5
- package/hooks/validators/skill_validator.py +7 -14
- package/package.json +1 -1
- package/pi/anchor-gate.ts +15 -2
- package/pi/book.ts +69 -76
- package/pi/decision-tool.ts +7 -9
- package/pi/second-thought.ts +56 -27
- package/pi/support/phases.ts +7 -12
- package/pi/support/snapshot.ts +22 -2
- package/pi/support/validators.ts +13 -44
- package/pi/ttsr.ts +7 -30
- package/pi/validate-tool-use.ts +1 -1
- package/skills/anchorloop/SKILL.md +5 -5
- package/skills/anchorloop/playbooks/autonomous-run.md +1 -1
- package/skills/coding/SKILL.md +1 -1
package/README.md
CHANGED
|
@@ -25,12 +25,12 @@ shared grilling · domain-modeling · human-feedback · agent-terse
|
|
|
25
25
|
|
|
26
26
|
| File | What it does |
|
|
27
27
|
| --- | --- |
|
|
28
|
-
| `pi/book.ts` | The loop book. The first `todo` create or `kind` call opens
|
|
29
|
-
| `pi/anchor-gate.ts` | Inside an open loop,
|
|
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. |
|
|
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 to `advance`, go `back`, or `ask-user`, plus `done`, the review tier still needed, and quality; phase completion comes from the program's evidence alone. The
|
|
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
|
|
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. |
|
|
28
|
+
| `pi/book.ts` | The loop book. The first `todo` create opens an `inline` loop (intent from the first todo's subject, anchor from a `Reality anchor:` line or the todo's `metadata.anchor`), and a `kind` call opens or changes one; task records need 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). The agent declares only what the extension cannot observe with `anchorloop_stage`: `kind` (to choose `quick`, `reviewed`, or `durable`, with an intent and the anchor command; changing a kind the agent chose takes a `reason`), `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 (a write or edit in any git repo, outside its `scratch/` and `docs/specs/`; a linked child's edits and a successful `git merge`, `cherry-pick`, or `pull` count too), the exit code of the bash call that ran the anchor command (an anchor that passed, failed, and passed again records its own friction entry, already reflected as `resolved`: the rerun command and the failed run's key line, at most 120 characters and never a stack trace; the agent is told nothing), 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 widget below the editor shows `⚓ <kind> · <current> ✓ → <next>`, with `· N friction` while friction waits for a `reflect`, after every tool call, on `session_start`, and when a `Reality anchor:` line is stated, and clears when the loop closes. An anchor run or declared verify of a `quick` or `inline` loop also measures the diff's reach: source files (not tests, lockfiles, specs, or prose), the top-level directories they sit in, and a README, changelog, or `*.schema.json` changed beside them. Files already changed when a repo joined the loop count only once their content changes, and a file at a repository root counts as a file, not a directory. At 3 source files, 2 directories, or one such file, the result carries one line asking to re-classify, and verify waits for a `kind` call with `reason` that answers each threshold crossed; a later threshold asks again. A phase that starts before an earlier one is complete warns once in one line; 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; empty project and `agents/` directories are removed), 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` answers `closed (landed). Clear the todo list.`, adding only the phases not shown complete and the unreflected friction, and 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, appends one warning line to the edit's tool result (where the agent reads it) 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. |
|
|
30
|
+
| `pi/decision-tool.ts` | The `anchorloop_decision` tool appends single-line, spreadsheet-safe rows to a decision trail TSV for forks, pivots, and blockers (completed units come from `git log`), or a `start` row when a run picks up another run's trail. |
|
|
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, the open todos by id and subject, the task list, and friction (a child's under its name), and judges only whether to `advance`, go `back`, or `ask-user`, plus `done`, the review tier still needed, and quality; phase completion comes from the program's evidence alone. The advisory is `next: <phase> (<missing>)` plus Jev's concerns, with no scores. It is sent only when Jev has a concern, todos are open without a followup, or the run continues, and stays silent when Jev agrees with nothing to add (the verdict is still recorded in the session); at close it carries concerns only. Away from close it also names the loop's open todos that have no valid `metadata.followup` (the first three, subjects cut to 60 characters, then `+N more`) and asks the agent to continue each, complete it, or set that followup. The run continues once, when Jev agrees to advance and the next phase is a verify, review, or fix, or when such todos are open and Jev's move is not `ask-user` (then the advisory tells the agent to ask the user instead); an advisory with the same next phase, concerns, and open todo ids as the last one is not repeated, and a `full` rating after a clean review that follows the last change is not a concern. 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. |
|
|
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` (at most three red-line advisories per edit), 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. |
|
|
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`. Each injected rule arrives as `<rule name="…">body</rule>`. 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. |
|
|
34
34
|
|
|
35
35
|
## Install
|
|
36
36
|
|
package/README.zh-TW.md
CHANGED
|
@@ -25,12 +25,12 @@ anchorloop 核心循環 · 原則 · playbook:autonomous-run、paus
|
|
|
25
25
|
|
|
26
26
|
| 檔案 | 作用 |
|
|
27
27
|
| --- | --- |
|
|
28
|
-
| `pi/book.ts` | Loop book。第一次 `todo` create
|
|
29
|
-
| `pi/anchor-gate.ts` | 只在開啟的 loop 內:`write` 或 `edit` 開始 `act` 階段、而這種 loop
|
|
30
|
-
| `pi/decision-tool.ts` | `anchorloop_decision` 工具把一筆決策寫進 TSV
|
|
31
|
-
| `pi/second-thought.ts` | 只在開啟的 loop 內:一次執行結束且 book 自上次判斷後多了條目時,以及宣告 `close` 時,透過 pi 的 classifier registry 請 Jev(TypeSafe 的型別化決策模型)複核階段機。下一個階段由程式從 book 算出;Jev 看到種類、intent、anchor、diff 摘要(≤1200 字元)、`phase`(目前、下一個、缺什麼)、verify 狀態、未完成 todo
|
|
32
|
-
| `pi/validate-tool-use.ts` | 每次 `write` 或 `edit` 後在行程內驗證該檔(`pi/support/validators.ts`,移植自 `hooks/validators/`),替 `SKILL.md
|
|
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
|
|
28
|
+
| `pi/book.ts` | Loop book。第一次 `todo` create 會以 `inline` 開啟 loop(intent 取第一個 todo 的標題,anchor 取 `Reality anchor:` 這一行或該 todo 的 `metadata.anchor`),`kind` 呼叫也能開啟或更改 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` 條目)。agent 只用 `anchorloop_stage` 宣告 extension 看不到的事:`kind`(用來選 `quick`、`reviewed` 或 `durable`,附 intent 和 anchor 指令;改掉 agent 自己選的種類時,必須附 `reason`)、`verify`(不是指令的檢查)、`fresh-read`、`friction`(`tried`、`found`、`led_by`)、`reflect`、`skip`、`back`、`close`,以及查詢用的 `read`。其餘都從證據讀出:production 的修改(任何 git repo 內、`scratch/` 與 `docs/specs/` 以外的 write 或 edit;已連結 child 的修改,以及成功的 `git merge`、`cherry-pick`、`pull` 也算)、執行過 anchor 指令的那次 bash 的 exit code(anchor 通過後失敗、又再通過時,extension 會自己記一筆 friction,並直接 reflect 為 `resolved`:重跑的指令,加上失敗那次最關鍵的一行輸出,最多 120 字元,不放 stack trace;不會通知 agent)、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 <目前> ✓ → <下一個>(缺:…)· 已跳過:…`。編輯器下方的 widget 會顯示 `⚓ <種類> · <目前> ✓ → <下一個>`,還有 friction 等著 `reflect` 時附 `· N friction`;每次工具呼叫後、`session_start` 時,以及 agent 寫出 `Reality anchor:` 這一行時更新,loop 結案後清掉。`quick` 或 `inline` loop 跑 anchor 或宣告 verify 時,也會量 diff 的影響範圍:原始碼檔案(不含測試、lockfile、spec 與文字檔)、它們所在的頂層目錄,以及跟原始碼一起改的 README、changelog 或 `*.schema.json`。repo 加入 loop 時就已經改過的檔案,要等內容再變動才算;repo 根目錄的檔案只算檔案數,不算一個目錄。達到 3 個原始碼檔案、2 個目錄或 1 個這類檔案時,結果會附一行請 agent 重新分類;verify 要等到以 `reason` 呼叫 `kind`、回應每一個跨過的門檻才算完成,之後再跨過新的門檻會再問一次。前一個階段還沒完成就開始下一個階段,會用一行警告一次;證據改變或用 `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` 回覆 `closed (landed). Clear the todo list.`,只多列沒有完成的階段和還沒 reflect 的 friction;它不會因為階段或 friction 而拒絕,只在還有 pending 或 in-progress 的 todo 沒有 `metadata.followup`(票號、`memory` 或 `drop — <原因>`)時拒絕。 |
|
|
29
|
+
| `pi/anchor-gate.ts` | 只在開啟的 loop 內:`write` 或 `edit` 開始 `act` 階段、而這種 loop 前面的階段還沒完成時,把一行警告附在該次編輯的 tool result 後面(agent 在那裡讀到它):沒有 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` 做的修改不檢查。 |
|
|
30
|
+
| `pi/decision-tool.ts` | `anchorloop_decision` 工具把一筆決策寫進 TSV 決策軌跡(記岔路、轉向與卡點;完成的工作單元看 `git log`),每筆只佔一行,用試算表開啟也安全;接手別人的軌跡時寫一筆 `start`。 |
|
|
31
|
+
| `pi/second-thought.ts` | 只在開啟的 loop 內:一次執行結束且 book 自上次判斷後多了條目時,以及宣告 `close` 時,透過 pi 的 classifier registry 請 Jev(TypeSafe 的型別化決策模型)複核階段機。下一個階段由程式從 book 算出;Jev 看到種類、intent、anchor、diff 摘要(≤1200 字元)、`phase`(目前、下一個、缺什麼)、verify 狀態、未完成 todo(id 與標題)、任務清單和 friction(子 agent 的以它的名字列出),只判斷要 `advance`、退回(`back`)還是 `ask-user`,另外還問 `done`、還需要哪一級 review 與品質;階段是否完成只由程式依證據判定。提醒是 `next: <階段>(<缺什麼>)` 加上 Jev 的疑慮,不附分數;只在 Jev 有疑慮、有 todo 未完成而沒有 followup、或這次會讓 agent 多跑一輪時才送出,Jev 同意且沒有補充時保持安靜(判斷結果仍記在 session 裡);結案時只附疑慮。離開 close 時,提醒也會點名 loop 中還沒有有效 `metadata.followup` 的未完成 todo(前三個,標題截成 60 字元,其餘以 `+N more` 帶過),請 agent 逐一繼續、完成,或設定該 followup。Jev 同意前進、而下一個階段是 verify、review 或 fix,或有這類 todo 未完成且 Jev 的判斷不是 `ask-user`(此時提醒改請 agent 去問使用者)時,才讓 agent 多跑一輪;下一個階段、疑慮和未完成 todo 的 id 都與上一則相同的提醒不重複;最後一次修改之後已有無 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 判斷結果。 |
|
|
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` 和對照測試使用。 |
|
|
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`。注入的規則以 `<rule name="…">內容</rule>` 送出。預設每條規則每個 session 只觸發一次。不支援 `astCondition`、`question`、`agents`,用到的規則會被跳過並回報。`/ttsr` 列出已載入的規則。 |
|
|
34
34
|
|
|
35
35
|
## 安裝
|
|
36
36
|
|
|
@@ -72,7 +72,7 @@ def main() -> None:
|
|
|
72
72
|
rel = target_path
|
|
73
73
|
|
|
74
74
|
lines = "\n".join(f" - {w}" for w in warnings)
|
|
75
|
-
msg = f"
|
|
75
|
+
msg = f"validator {rel}:\n{lines}"
|
|
76
76
|
print(json.dumps({"additionalContext": msg}))
|
|
77
77
|
else:
|
|
78
78
|
print(json.dumps({}))
|
|
@@ -29,6 +29,7 @@ RED_LINE_MARKERS = (
|
|
|
29
29
|
r"\b(never|must not|do not|don't|forbidden|barred|prohibited|avoid)\b"
|
|
30
30
|
r"|不要|禁止|不得|不准|嚴禁"
|
|
31
31
|
)
|
|
32
|
+
MAX_ADVISORIES = 3
|
|
32
33
|
RED_LINE_MIN_BUDGET = 3
|
|
33
34
|
RED_LINE_DENSITY = 0.10
|
|
34
35
|
# Emphatic capitals are matched case-sensitively and need a reason on the same or next line.
|
|
@@ -8,7 +8,7 @@ from .utils import find_defensive_phrases
|
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
def check_decision_md(path: Path) -> list[str]:
|
|
11
|
-
"""Check decision.md for
|
|
11
|
+
"""Check decision.md for its decision table."""
|
|
12
12
|
warnings: list[str] = []
|
|
13
13
|
text = path.read_text(encoding="utf-8")
|
|
14
14
|
|
|
@@ -16,18 +16,11 @@ def check_decision_md(path: Path) -> list[str]:
|
|
|
16
16
|
if not re.search(r"\|\s*Question\s*\|\s*Answer\s*\|\s*Basis\s*\|\s*Status\s*\|", text, re.IGNORECASE):
|
|
17
17
|
warnings.append("decision.md should include a Question | Answer | Basis | Status table (Status: grounded, confirmed, or open).")
|
|
18
18
|
|
|
19
|
-
# Check for pre-flight readiness (core rules)
|
|
20
|
-
lower = text.lower()
|
|
21
|
-
if "rule" not in lower and "readiness" not in lower and "prerequisite" not in lower and "core" not in lower:
|
|
22
|
-
warnings.append(
|
|
23
|
-
"confirm core rules readiness before implementation; note readiness in decision.md."
|
|
24
|
-
)
|
|
25
|
-
|
|
26
19
|
return warnings
|
|
27
20
|
|
|
28
21
|
|
|
29
22
|
def check_spec_md(path: Path) -> list[str]:
|
|
30
|
-
"""Check spec.md for approval status
|
|
23
|
+
"""Check spec.md for its approval status."""
|
|
31
24
|
warnings: list[str] = []
|
|
32
25
|
text = path.read_text(encoding="utf-8")
|
|
33
26
|
|
|
@@ -35,10 +28,6 @@ def check_spec_md(path: Path) -> list[str]:
|
|
|
35
28
|
if not re.search(r"Status:\s*(proposed|approved)", text, re.IGNORECASE):
|
|
36
29
|
warnings.append("spec.md must declare 'Status: proposed' or 'Status: approved' at the top.")
|
|
37
30
|
|
|
38
|
-
# Reality anchor declaration
|
|
39
|
-
if "reality anchor" not in text.lower() and "anchor" not in text.lower():
|
|
40
|
-
warnings.append("spec.md should name the chosen reality anchor and its checkpoint.")
|
|
41
|
-
|
|
42
31
|
return warnings
|
|
43
32
|
|
|
44
33
|
|
|
@@ -62,16 +51,14 @@ def check_verification_md(path: Path) -> list[str]:
|
|
|
62
51
|
]
|
|
63
52
|
if invalid_results:
|
|
64
53
|
warnings.append(
|
|
65
|
-
f"invalid verification Result values found: {', '.join(set(invalid_results))}
|
|
66
|
-
"Result must be one of: pass, fail, unknown."
|
|
54
|
+
f"invalid verification Result values found: {', '.join(set(invalid_results))} (use pass, fail, unknown)"
|
|
67
55
|
)
|
|
68
56
|
|
|
69
57
|
# 2. Review of newly added rules or skills (tuning vs defensive vocabulary)
|
|
70
58
|
found_defensive = find_defensive_phrases(text, DEFENSIVE_PATTERNS)
|
|
71
59
|
if found_defensive:
|
|
72
60
|
warnings.append(
|
|
73
|
-
f"detected defensive wording in verification/review: {', '.join(found_defensive)}
|
|
74
|
-
"Reviews of new rules and skills must focus on adjustment and tuning, not adding defensive red lines."
|
|
61
|
+
f"detected defensive wording in verification/review: {', '.join(found_defensive)}"
|
|
75
62
|
)
|
|
76
63
|
|
|
77
64
|
return warnings
|
|
@@ -18,16 +18,14 @@ def check_rules_file(path: Path) -> list[str]:
|
|
|
18
18
|
found_patterns = find_defensive_phrases(text, DEFENSIVE_PATTERNS)
|
|
19
19
|
if found_patterns:
|
|
20
20
|
warnings.append(
|
|
21
|
-
f"detected defensive wording: {', '.join(found_patterns)}
|
|
22
|
-
"State desired behavior directly; modern models do not need excessive red lines."
|
|
21
|
+
f"detected defensive wording: {', '.join(found_patterns)}"
|
|
23
22
|
)
|
|
24
23
|
|
|
25
24
|
# 2. Rule proliferation / bloat check
|
|
26
25
|
lines = [line.strip() for line in text.splitlines() if line.strip() and not line.strip().startswith("#")]
|
|
27
26
|
if len(lines) > 200:
|
|
28
27
|
warnings.append(
|
|
29
|
-
f"rule file has {len(lines)} non-empty lines
|
|
30
|
-
"or separating into specialized modules rather than accumulating micro-rules."
|
|
28
|
+
f"rule file has {len(lines)} non-empty lines (over 200)"
|
|
31
29
|
)
|
|
32
30
|
|
|
33
31
|
# 3. Check for Simplified Chinese common distinctive characters
|
|
@@ -36,7 +34,7 @@ def check_rules_file(path: Path) -> list[str]:
|
|
|
36
34
|
found_sc = [c for c in simplified_chinese_chars if c in text]
|
|
37
35
|
if found_sc:
|
|
38
36
|
warnings.append(
|
|
39
|
-
f"possible Simplified Chinese detected ({''.join(found_sc[:5])})
|
|
37
|
+
f"possible Simplified Chinese detected ({''.join(found_sc[:5])})"
|
|
40
38
|
)
|
|
41
39
|
|
|
42
40
|
return warnings
|
|
@@ -6,6 +6,7 @@ from pathlib import Path
|
|
|
6
6
|
|
|
7
7
|
from .constants import (
|
|
8
8
|
DEFENSIVE_PATTERNS,
|
|
9
|
+
MAX_ADVISORIES,
|
|
9
10
|
EMPHATIC_WORDS,
|
|
10
11
|
REASON_WORDS,
|
|
11
12
|
RED_LINE_DENSITY,
|
|
@@ -63,8 +64,7 @@ def check_skill_md(path: Path) -> list[str]:
|
|
|
63
64
|
has_references = (skill_dir / "references").is_dir()
|
|
64
65
|
if not has_references:
|
|
65
66
|
warnings.append(
|
|
66
|
-
f"skill has {len(lines)} lines (
|
|
67
|
-
"Move detailed guidelines or templates to references/ to keep SKILL.md streamlined."
|
|
67
|
+
f"skill has {len(lines)} lines (over {SKILL_MAX_RECOMMENDED_LINES}) and no references/"
|
|
68
68
|
)
|
|
69
69
|
|
|
70
70
|
# 3. Broken markdown links, followed through every Markdown file SKILL.md reaches
|
|
@@ -98,8 +98,7 @@ def check_skill_md(path: Path) -> list[str]:
|
|
|
98
98
|
found_patterns = find_defensive_phrases(text, DEFENSIVE_PATTERNS)
|
|
99
99
|
if found_patterns:
|
|
100
100
|
warnings.append(
|
|
101
|
-
f"detected defensive anti-pattern(s): {', '.join(found_patterns)}
|
|
102
|
-
"State expected behavior directly rather than constructing defensive red lines."
|
|
101
|
+
f"detected defensive anti-pattern(s): {', '.join(found_patterns)}"
|
|
103
102
|
)
|
|
104
103
|
|
|
105
104
|
return warnings
|
|
@@ -120,10 +119,7 @@ def check_skill_advisories(path: Path) -> list[str]:
|
|
|
120
119
|
markers = len(re.findall(RED_LINE_MARKERS, body, re.IGNORECASE))
|
|
121
120
|
budget = max(RED_LINE_MIN_BUDGET, int(sum(1 for line in lines if line.strip()) * RED_LINE_DENSITY))
|
|
122
121
|
if markers > budget:
|
|
123
|
-
advisories.append(
|
|
124
|
-
f"{markers} red-line markers exceed the budget of {budget}. Keep a red line only when an "
|
|
125
|
-
"observed failure backs it, and delete the rest."
|
|
126
|
-
)
|
|
122
|
+
advisories.append(f"{markers} red-line markers exceed the budget of {budget}")
|
|
127
123
|
|
|
128
124
|
# 2. Reasoned: an emphatic word carries its reason on the same or next line.
|
|
129
125
|
# 3. Once: an emphatic word is spent a single time.
|
|
@@ -133,12 +129,9 @@ def check_skill_advisories(path: Path) -> list[str]:
|
|
|
133
129
|
seen[word] += 1
|
|
134
130
|
window = " ".join(lines[number - 1 : number + 1])
|
|
135
131
|
if not re.search(REASON_WORDS, window, re.IGNORECASE):
|
|
136
|
-
advisories.append(
|
|
137
|
-
f"line {number}: emphatic '{word}' has no reason beside it. "
|
|
138
|
-
"State the consequence, or drop the emphasis."
|
|
139
|
-
)
|
|
132
|
+
advisories.append(f"line {number}: emphatic '{word}' has no reason beside it")
|
|
140
133
|
for word, count in seen.items():
|
|
141
134
|
if count > 1:
|
|
142
|
-
advisories.append(f"emphatic '{word}' appears {count} times
|
|
135
|
+
advisories.append(f"emphatic '{word}' appears {count} times")
|
|
143
136
|
|
|
144
|
-
return advisories
|
|
137
|
+
return advisories[:MAX_ADVISORIES]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-anchorloop",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.0",
|
|
4
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",
|
package/pi/anchor-gate.ts
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* pi extension: inside an open loop, warns once per jump when write or edit starts the `act` phase while an
|
|
3
3
|
* earlier phase of the loop kind is incomplete: no anchor, or in a durable loop a decision, spec, read,
|
|
4
4
|
* approval, or design still missing. The phase machine in support/phases.ts decides what is missing; the
|
|
5
|
-
* warning
|
|
5
|
+
* warning is one line appended to the edit's tool result, which is the only place the agent reads it.
|
|
6
|
+
* Outside a loop, or in a `quick` one, it stays silent.
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
@@ -11,6 +12,8 @@ import { JumpMemory, computePhases, exempt, incompleteBefore, jumpLine } from ".
|
|
|
11
12
|
|
|
12
13
|
export default function (pi: ExtensionAPI) {
|
|
13
14
|
const jumps = new JumpMemory();
|
|
15
|
+
// The warning each write or edit call earned, held until its result arrives.
|
|
16
|
+
const pending = new Map<string, string>();
|
|
14
17
|
|
|
15
18
|
pi.on(
|
|
16
19
|
"tool_call",
|
|
@@ -22,7 +25,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
22
25
|
if (!book || exempt(filePath, ctx.cwd) || currentKind(book.entries) === "quick") return;
|
|
23
26
|
const phases = computePhases(book, { cwd: ctx.cwd, todos: replayTodos(ctx.sessionManager.getBranch()) });
|
|
24
27
|
const missing = incompleteBefore(phases, "act");
|
|
25
|
-
if (jumps.fresh(String(book.header.id), "act", missing.map((status) => status.name)))
|
|
28
|
+
if (jumps.fresh(String(book.header.id), "act", missing.map((status) => status.name))) pending.set(event.toolCallId, jumpLine("act", missing));
|
|
29
|
+
}),
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
pi.on(
|
|
33
|
+
"tool_result",
|
|
34
|
+
guarded((event) => {
|
|
35
|
+
const line = pending.get(event.toolCallId);
|
|
36
|
+
if (line === undefined) return;
|
|
37
|
+
pending.delete(event.toolCallId);
|
|
38
|
+
return { content: [...event.content, { type: "text" as const, text: line }] };
|
|
26
39
|
}),
|
|
27
40
|
);
|
|
28
41
|
}
|
package/pi/book.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* pi extension: the loop book. The first `todo` create
|
|
3
|
-
*
|
|
2
|
+
* pi extension: the loop book. The first `todo` create opens a loop and its book as `inline`, its intent the first todo's subject
|
|
3
|
+
* and its anchor a stated `Reality anchor:` line or the todo's `metadata.anchor`; a `kind` declaration opens one too. The agent declares only
|
|
4
|
+
* what the program cannot observe (`kind` to choose `reviewed` or `durable`, a non-command `verify`,
|
|
4
5
|
* `fresh-read`, `friction`, `reflect`, `skip`, `back`, `close`); the extension derives the rest from
|
|
5
6
|
* evidence: production edits, the exit code of the anchor command, reviewer results, durable artifacts,
|
|
6
7
|
* and the fresh-reader subagent. A phase machine over that evidence warns once per jump and never blocks;
|
|
@@ -62,9 +63,6 @@ const REVIEWERS = new Set(["reviewer", "adversarial-reviewer"]);
|
|
|
62
63
|
const BLOCKING = /Blocking\s*:?\**\s*:?\s*P0\s*=\s*(\d+)\D{0,5}P1\s*=\s*(\d+)/gi;
|
|
63
64
|
const ANCHOR_LINE = /Reality anchor:[ \t]*(.*)/;
|
|
64
65
|
|
|
65
|
-
export const OPEN_LINE =
|
|
66
|
-
"anchorloop: a loop is open. Declare it with `anchorloop_stage` {stage: kind, kind: quick|inline|reviewed|durable, intent: <one line>, anchor: <check command>}.";
|
|
67
|
-
|
|
68
66
|
type Params = {
|
|
69
67
|
stage: (typeof STAGES)[number];
|
|
70
68
|
kind?: (typeof KINDS)[number];
|
|
@@ -330,7 +328,7 @@ function statedAnchor(message: { role?: string; content?: unknown } | undefined)
|
|
|
330
328
|
return found ? found[1].trim().slice(0, 300) || "(stated on the next line)" : undefined;
|
|
331
329
|
}
|
|
332
330
|
|
|
333
|
-
const
|
|
331
|
+
const WIDGET_KEY = "anchorloop";
|
|
334
332
|
|
|
335
333
|
const reply = (text: string) => ({ content: [{ type: "text" as const, text }], details: undefined });
|
|
336
334
|
|
|
@@ -402,6 +400,23 @@ export default function (pi: ExtensionAPI) {
|
|
|
402
400
|
}
|
|
403
401
|
}
|
|
404
402
|
|
|
403
|
+
/** True while no `kind` call has chosen the kind: the loop still holds the `inline` a todo create opened it with. */
|
|
404
|
+
function kindIsDefault(entries: Entry[]): boolean {
|
|
405
|
+
return !entries.some((entry) => entry.stage === "kind" && typeof entry.kind === "string" && entry.auto !== true);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/** Opens the loop at the first todo create as `inline`: the first todo's subject is the intent, a stated `Reality anchor:` line or its `metadata.anchor` the anchor. */
|
|
409
|
+
function openFromTodo(ctx: ExtensionContext, details: unknown): Book {
|
|
410
|
+
const book = createBook(ctx);
|
|
411
|
+
touchRepo(book, join(ctx.cwd, "_"));
|
|
412
|
+
const first = (taskDetails(details) ?? [])[0];
|
|
413
|
+
book.entries.push(appendEntry(book.path, "kind", { kind: "inline", auto: true, ...(first?.subject ? { intent: clip(first.subject.replace(/\s+/g, " ").trim(), 200) } : {}) }));
|
|
414
|
+
recordPriorAnchor(book, ctx);
|
|
415
|
+
const anchor = first?.metadata?.anchor;
|
|
416
|
+
if (typeof anchor === "string" && anchor.trim() && !anchorCommand(book.entries)) book.entries.push(appendEntry(book.path, "anchor", { check: anchor.trim() }));
|
|
417
|
+
return book;
|
|
418
|
+
}
|
|
419
|
+
|
|
405
420
|
function close(book: Book, ctx: ExtensionContext, verdict: string): { text: string; snapshot?: Snapshot } {
|
|
406
421
|
const todos = replayTodos(ctx.sessionManager.getBranch());
|
|
407
422
|
const undisposed = todos.filter((task) => isOpenTask(task) && !validFollowup(task.metadata?.followup));
|
|
@@ -415,7 +430,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
415
430
|
const kind = currentKind(book.entries);
|
|
416
431
|
const phases = phasesOf(book, ctx);
|
|
417
432
|
const missing = incompleteBefore(phases, "close");
|
|
418
|
-
const notice = verdict === "landed" && missing.length ? `anchorloop: closed with phases not shown complete: ${missing.map((status) => `${status.name} (${status.note})`).join("; ")}.` : undefined;
|
|
419
433
|
const diff = diffAgainstBases(book.entries);
|
|
420
434
|
const friction = allFriction(book.entries);
|
|
421
435
|
const phaseList = phases.statuses.map((status) => ({ phase: status.name, state: status.state === "skipped" && status.auto ? "auto-skipped" : status.state === "missing" ? "not shown complete" : status.state, ...(status.state === "skipped" ? { note: status.note } : {}) }));
|
|
@@ -434,25 +448,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
434
448
|
followups: todos.filter((task) => isOpenTask(task)).map((task) => ({ task: task.subject, followup: task.metadata?.followup })),
|
|
435
449
|
};
|
|
436
450
|
const unreflected = friction.filter((item) => !item.reflected);
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
const openNames = phaseList.filter((item) => item.state === "not shown complete").map((item) => item.phase);
|
|
444
|
-
const shown = [
|
|
445
|
-
`done: ${doneNames.join(", ") || "none"}`,
|
|
446
|
-
...(skippedNames.length ? [`skipped: ${skippedNames.join(", ")}`] : []),
|
|
447
|
-
...(openNames.length ? [`${verdict === "landed" ? "not complete" : "not reached"}: ${openNames.join(", ")}`] : []),
|
|
448
|
-
].join("; ");
|
|
449
|
-
const back = summary.back.length ? `; back ${summary.back.join(", ")}` : "";
|
|
450
|
-
const text = `Loop closed (${verdict}): phases ${shown}; ${summary.reviewTrend}; diff ${diff.summary.split("\n").join("; ")}${back}.${tail}`;
|
|
451
|
-
const after = notice ? `\n${notice}` : "";
|
|
451
|
+
// The agent has just done the finished phases, so the result names only what is left.
|
|
452
|
+
const text = [
|
|
453
|
+
`closed (${verdict}).`,
|
|
454
|
+
...(missing.length ? [`${verdict === "landed" ? "not complete" : "not reached"}: ${missing.map((status) => (verdict === "landed" ? `${status.name} (${status.note})` : status.name)).join("; ")}.`] : []),
|
|
455
|
+
...(unreflected.length ? [`unreflected friction: ${unreflected.map((item) => `${item.ref} (${item.found})`).join("; ")}.`] : []),
|
|
456
|
+
].join(" ");
|
|
452
457
|
if (kept) {
|
|
453
458
|
// A child's book stays for the main session to read and delete.
|
|
454
459
|
appendEntry(book.path, "close", { verdict, summary: text.slice(0, 500) });
|
|
455
|
-
return { text: `${text} The book stays for the main session
|
|
460
|
+
return { text: `${text} The book stays for the main session.` };
|
|
456
461
|
}
|
|
457
462
|
// Jev judges the loop as it stood at close; the book is gone by the time the result is judged.
|
|
458
463
|
const snap = kind === "quick" ? undefined : snapshot(book, ctx);
|
|
@@ -461,38 +466,33 @@ export default function (pi: ExtensionAPI) {
|
|
|
461
466
|
for (const { book: child } of findChildBooks(id)) deleteBook(child.path);
|
|
462
467
|
}
|
|
463
468
|
deleteBook(book.path);
|
|
464
|
-
return { text: `${text}
|
|
469
|
+
return { text: `${text} Clear the todo list.`, snapshot: snap };
|
|
465
470
|
}
|
|
466
471
|
|
|
467
472
|
pi.registerTool({
|
|
468
473
|
name: "anchorloop_stage",
|
|
469
474
|
label: "Loop stage",
|
|
470
475
|
description:
|
|
471
|
-
"
|
|
472
|
-
"`kind` (kind, intent, optional anchor command; call again to set the anchor, or with `reason` to change the kind or answer a re-classification): quick (act, check), inline (anchor, act, verify), reviewed (adds review), durable (adds decide, grill, spec, read, approve, design); every kind ends with reflect, close. " +
|
|
473
|
-
"When an anchor run or declared verify of a quick or inline loop finds the change reaching 3 source files, 2 modules, or a README, changelog, or *.schema.json beside code, its verify waits for a `kind` call with `reason`. " +
|
|
474
|
-
"`verify` (note; only for an anchor that is not a command), `fresh-read` (a fresh-context read of the spec is done), `friction` (tried, found, led_by), `reflect` (friction ids, class misdirection|gap|resolved, action), " +
|
|
475
|
-
"`skip` / `back` (phase, reason), `close` (verdict), `read` (view the book). Edits, anchor runs, reviews, and spec files are read from evidence. " +
|
|
476
|
-
"A loop opens with the first `todo` create; `close` refuses while a pending or in-progress todo lacks metadata.followup.",
|
|
476
|
+
"Loop book; the first `todo` create opens a loop as inline. stage: kind (kind, intent, anchor; reason to change the kind or answer a reach notice), verify (note; only for a check not run in bash), fresh-read, friction (tried, found, led_by), reflect (friction ids, class, action), skip/back (phase, reason), close (verdict; refuses while an open todo lacks metadata.followup), read. Edits, anchor runs, reviews, and specs are read from evidence.",
|
|
477
477
|
parameters: {
|
|
478
478
|
type: "object",
|
|
479
479
|
properties: {
|
|
480
480
|
stage: { type: "string", enum: [...STAGES] },
|
|
481
|
-
kind: { type: "string", enum: [...KINDS]
|
|
482
|
-
intent: { type: "string"
|
|
483
|
-
anchor: { type: "string", description: "
|
|
484
|
-
note: { type: "string"
|
|
485
|
-
result: { type: "string", enum: ["pass", "fail"]
|
|
486
|
-
tried: { type: "string"
|
|
487
|
-
found: { type: "string"
|
|
488
|
-
led_by: { type: "string", description: "
|
|
489
|
-
friction: { type: "array", items: { type: ["string", "number"] }, description: "
|
|
490
|
-
class: { type: "string", enum: ["misdirection", "gap", "resolved"]
|
|
491
|
-
action: { type: "string"
|
|
492
|
-
phase: { type: "string"
|
|
493
|
-
reason: { type: "string"
|
|
494
|
-
verdict: { type: "string", enum: ["landed", "blocked", "abandoned"]
|
|
495
|
-
view: { type: "string", enum: ["summary", "all"]
|
|
481
|
+
kind: { type: "string", enum: [...KINDS] },
|
|
482
|
+
intent: { type: "string" },
|
|
483
|
+
anchor: { type: "string", description: "the check command" },
|
|
484
|
+
note: { type: "string" },
|
|
485
|
+
result: { type: "string", enum: ["pass", "fail"] },
|
|
486
|
+
tried: { type: "string" },
|
|
487
|
+
found: { type: "string" },
|
|
488
|
+
led_by: { type: "string", description: "the instruction that misled the attempt, or `none`" },
|
|
489
|
+
friction: { type: "array", items: { type: ["string", "number"] }, description: "ids: `3`, `worker#2`" },
|
|
490
|
+
class: { type: "string", enum: ["misdirection", "gap", "resolved"] },
|
|
491
|
+
action: { type: "string" },
|
|
492
|
+
phase: { type: "string" },
|
|
493
|
+
reason: { type: "string" },
|
|
494
|
+
verdict: { type: "string", enum: ["landed", "blocked", "abandoned"] },
|
|
495
|
+
view: { type: "string", enum: ["summary", "all"] },
|
|
496
496
|
},
|
|
497
497
|
required: ["stage"],
|
|
498
498
|
additionalProperties: false,
|
|
@@ -533,22 +533,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
533
533
|
if (stage === "kind") {
|
|
534
534
|
if (!params.kind && !params.intent && !params.anchor && !params.reason) throw new Error("kind needs `kind`, `intent`, `anchor`, or `reason`");
|
|
535
535
|
if (!had && (!params.kind || !params.intent)) throw new Error("the first kind declaration needs `kind` and a one-line `intent`");
|
|
536
|
+
// The kind a todo create defaulted to is not a choice yet, so choosing another needs no reason.
|
|
536
537
|
const changed = had !== undefined && params.kind !== undefined && had !== params.kind;
|
|
537
|
-
if (changed && !params.reason) throw new Error(`changing the kind from ${had} to ${params.kind} needs \`reason\`: the reach grew, the user added scope, a user-owned decision appeared, or the change turned out smaller`);
|
|
538
|
+
if (changed && !params.reason && !kindIsDefault(book.entries)) throw new Error(`changing the kind from ${had} to ${params.kind} needs \`reason\`: the reach grew, the user added scope, a user-owned decision appeared, or the change turned out smaller`);
|
|
538
539
|
touchRepo(book, join(ctx.cwd, "_"));
|
|
539
|
-
const notes: string[] = [];
|
|
540
540
|
if (params.kind || params.intent || params.reason) {
|
|
541
541
|
// A reason answers the reach measured now, so a later threshold asks again.
|
|
542
542
|
const answer = params.reason ? { reason: params.reason, crossed: reachOf(book.entries, ctx.cwd).crossed } : {};
|
|
543
543
|
appendEntry(book.path, "kind", { kind: params.kind ?? had, ...(params.intent ? { intent: params.intent } : {}), ...answer });
|
|
544
|
-
const why = params.reason ? ` (${params.reason})` : "";
|
|
545
|
-
notes.push(changed ? `kind changed: ${had} → ${params.kind}${why}` : `kind: ${params.kind ?? had}${why}`);
|
|
546
544
|
}
|
|
547
|
-
if (params.anchor) {
|
|
548
|
-
|
|
549
|
-
notes.push(`anchor: ${params.anchor}`);
|
|
550
|
-
}
|
|
551
|
-
return reply(notes.join("\n"));
|
|
545
|
+
if (params.anchor) appendEntry(book.path, "anchor", { check: params.anchor });
|
|
546
|
+
return reply("ok");
|
|
552
547
|
}
|
|
553
548
|
if (stage === "verify") {
|
|
554
549
|
if (!params.note) throw new Error("verify needs a `note` saying what was observed");
|
|
@@ -557,17 +552,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
557
552
|
const entry = appendEntry(book.path, recorded, { declared: true, note: params.note, result, exit: result === "pass" ? 0 : 1, ...reachField(book, ctx.cwd) });
|
|
558
553
|
book.entries.push(entry);
|
|
559
554
|
const notice = reachNotice(book, entry);
|
|
560
|
-
return reply(
|
|
555
|
+
return reply(notice ? `ok\n${notice}` : "ok");
|
|
561
556
|
}
|
|
562
557
|
if (stage === "fresh-read") {
|
|
563
558
|
appendEntry(book.path, "goldfish", { source: "declared", ...(params.note ? { note: params.note } : {}) });
|
|
564
|
-
return reply("
|
|
559
|
+
return reply("ok");
|
|
565
560
|
}
|
|
566
561
|
if (stage === "friction") {
|
|
567
562
|
if (!params.tried || !params.found) throw new Error("friction needs `tried` and `found`");
|
|
568
563
|
const id = frictionOf(book.entries).length + 1;
|
|
569
564
|
appendEntry(book.path, "friction", { id, tried: params.tried, found: params.found, led_by: params.led_by || "none" });
|
|
570
|
-
return reply(`friction #${id}
|
|
565
|
+
return reply(`friction #${id}`);
|
|
571
566
|
}
|
|
572
567
|
if (stage === "reflect") {
|
|
573
568
|
const known = allFriction(book.entries).map((item) => item.ref);
|
|
@@ -576,7 +571,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
576
571
|
const unknown = ids.filter((id) => !known.includes(id));
|
|
577
572
|
if (unknown.length) throw new Error(`unknown friction ${unknown.join(", ")}; known: ${known.join(", ") || "none"}`);
|
|
578
573
|
appendEntry(book.path, "reflect", { friction: ids, class: params.class, ...(params.action ? { action: params.action } : {}) });
|
|
579
|
-
return reply(
|
|
574
|
+
return reply("ok");
|
|
580
575
|
}
|
|
581
576
|
// skip and back
|
|
582
577
|
const phase = params.phase;
|
|
@@ -584,7 +579,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
584
579
|
if (!phase || !known.includes(phase)) throw new Error(`${stage} needs \`phase\`, one of ${known.join(", ")} for a ${had ?? "undeclared"} loop`);
|
|
585
580
|
if (!params.reason) throw new Error(`${stage} needs a \`reason\``);
|
|
586
581
|
appendEntry(book.path, stage, { phase, reason: params.reason });
|
|
587
|
-
return reply(
|
|
582
|
+
return reply("ok");
|
|
588
583
|
}
|
|
589
584
|
|
|
590
585
|
pi.on(
|
|
@@ -648,7 +643,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
648
643
|
const code = anchorExit(command, task.anchor, exit);
|
|
649
644
|
book.entries.push(appendEntry(book.path, "taskrun", { id: task.id, command: task.anchor.slice(0, 200), exit: code, last, ...keyOf(code) }));
|
|
650
645
|
}
|
|
651
|
-
|
|
646
|
+
autoFriction(book);
|
|
652
647
|
const merged = mergeRun(command);
|
|
653
648
|
if (exit === 0 && merged && !/Already up[ -]to[ -]date/i.test(last ?? "") && book.entries.at(-1)?.stage !== "edit") {
|
|
654
649
|
touchRepoBeforeMerge(book, resolve(String(book.header.cwd ?? process.cwd()), merged.dir), merged.verb);
|
|
@@ -664,10 +659,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
664
659
|
/**
|
|
665
660
|
* Records one friction line for each anchor that passed again after it had passed and then failed. A pair is recorded
|
|
666
661
|
* once, and not at all when the agent recorded a friction entry itself after the failure. The pairs one command
|
|
667
|
-
* completes in one bash call (it is both the loop and a task anchor) share one entry.
|
|
662
|
+
* completes in one bash call (it is both the loop and a task anchor) share one entry. No instruction owns such a slip, so
|
|
663
|
+
* the entry is reflected as `resolved` at once and the agent is told nothing.
|
|
668
664
|
*/
|
|
669
|
-
function autoFriction(book: Book):
|
|
670
|
-
const lines: string[] = [];
|
|
665
|
+
function autoFriction(book: Book): void {
|
|
671
666
|
const open = new Map<string, string[]>();
|
|
672
667
|
for (const pair of retryPairs(book.entries)) {
|
|
673
668
|
const id = `${pair.key}#${pair.fail}`;
|
|
@@ -688,24 +683,23 @@ export default function (pi: ExtensionAPI) {
|
|
|
688
683
|
const found = clip(String(failed.key ?? keyLine(String(failed.last ?? ""))) || "failed with no output");
|
|
689
684
|
const number = frictionOf(book.entries).length + 1;
|
|
690
685
|
book.entries.push(appendEntry(book.path, "friction", { id: number, tried, found, led_by: "none", auto: true, pairs }));
|
|
691
|
-
|
|
686
|
+
book.entries.push(appendEntry(book.path, "reflect", { friction: [String(number)], class: "resolved", action: "auto-recorded rerun" }));
|
|
692
687
|
}
|
|
693
|
-
return lines;
|
|
694
688
|
}
|
|
695
689
|
|
|
696
|
-
/** The
|
|
690
|
+
/** The line below the editor: the loop's kind, its last complete phase, the next one, and the friction still to reflect; cleared when no loop is open. */
|
|
697
691
|
function showStatus(ctx: ExtensionContext): void {
|
|
698
|
-
if (typeof ctx.ui?.
|
|
692
|
+
if (typeof ctx.ui?.setWidget !== "function") return;
|
|
699
693
|
const book = openLoop(ctx);
|
|
700
694
|
if (!book) {
|
|
701
|
-
ctx.ui.
|
|
695
|
+
ctx.ui.setWidget(WIDGET_KEY, undefined);
|
|
702
696
|
return;
|
|
703
697
|
}
|
|
704
698
|
const phases = phasesOf(book, ctx);
|
|
705
699
|
const open = allFriction(book.entries).filter((item) => !item.reflected).length;
|
|
706
700
|
const kind = phases.kind === "undeclared" ? "no kind" : phases.kind;
|
|
707
701
|
const at = `${phases.current}${phases.current === "start" ? "" : " ✓"} → ${phases.next}`;
|
|
708
|
-
ctx.ui.
|
|
702
|
+
ctx.ui.setWidget(WIDGET_KEY, [`⚓ ${kind} · ${at}${open ? ` · ${open} friction` : ""}`], { placement: "belowEditor" });
|
|
709
703
|
}
|
|
710
704
|
|
|
711
705
|
pi.on(
|
|
@@ -721,14 +715,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
721
715
|
if (event.toolName === "todo") {
|
|
722
716
|
const input = event.input as { action?: unknown; id?: unknown };
|
|
723
717
|
let open = openLoop(ctx);
|
|
724
|
-
|
|
725
|
-
if (!open && opens) {
|
|
726
|
-
open = createBook(ctx);
|
|
727
|
-
recordPriorAnchor(open, ctx);
|
|
728
|
-
}
|
|
718
|
+
if (!open && input.action === "create") open = openFromTodo(ctx, event.details);
|
|
729
719
|
if (!open) return;
|
|
730
720
|
recordTasks(open, event.details, input, ctx.cwd);
|
|
731
|
-
return
|
|
721
|
+
return;
|
|
732
722
|
}
|
|
733
723
|
const book = openLoop(ctx);
|
|
734
724
|
if (!book) return;
|
|
@@ -833,7 +823,10 @@ function headAndTail(text: string): string {
|
|
|
833
823
|
const book = openLoop(ctx);
|
|
834
824
|
if (!book) return;
|
|
835
825
|
const stated = statedAnchor(message);
|
|
836
|
-
if (stated && !book.entries.some((entry) => entry.stage === "anchor" && entry.check === stated))
|
|
826
|
+
if (stated && !book.entries.some((entry) => entry.stage === "anchor" && entry.check === stated)) {
|
|
827
|
+
appendEntry(book.path, "anchor", { check: stated, line: true });
|
|
828
|
+
showStatus(ctx);
|
|
829
|
+
}
|
|
837
830
|
if (message.role !== "custom" || message.customType !== "subagent_result" || subagent) return;
|
|
838
831
|
const agent = String(message.details?.agent ?? "");
|
|
839
832
|
if (REVIEWERS.has(agent)) {
|
package/pi/decision-tool.ts
CHANGED
|
@@ -55,18 +55,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
55
55
|
pi.registerTool({
|
|
56
56
|
name: "anchorloop_decision",
|
|
57
57
|
label: "Decision trail",
|
|
58
|
-
description:
|
|
59
|
-
"Append one row to a decision trail TSV (scratch/decisions-<task-slug>.tsv): a fork chosen, a unit completed, a pivot, or a blocker. " +
|
|
60
|
-
"Pass `start` with your run id instead of the row fields when picking up a trail another run wrote.",
|
|
58
|
+
description: "Append one row to scratch/decisions-<slug>.tsv for a fork, pivot, or blocker (git log holds completed units). `start` = your run id when picking up a trail.",
|
|
61
59
|
parameters: {
|
|
62
60
|
type: "object",
|
|
63
61
|
properties: {
|
|
64
|
-
log: { type: "string"
|
|
65
|
-
start: { type: "string"
|
|
66
|
-
decision: { type: "string"
|
|
67
|
-
why: { type: "string"
|
|
68
|
-
evidence: { type: "string", description: "
|
|
69
|
-
result: { type: "string"
|
|
62
|
+
log: { type: "string" },
|
|
63
|
+
start: { type: "string" },
|
|
64
|
+
decision: { type: "string" },
|
|
65
|
+
why: { type: "string" },
|
|
66
|
+
evidence: { type: "string", description: "commit, file:line, or path" },
|
|
67
|
+
result: { type: "string" },
|
|
70
68
|
},
|
|
71
69
|
required: ["log"],
|
|
72
70
|
additionalProperties: false,
|
package/pi/second-thought.ts
CHANGED
|
@@ -4,8 +4,13 @@
|
|
|
4
4
|
* summary, phase (current, next, missing), verify state, open todos, tasks, and friction, and judges only
|
|
5
5
|
* whether the current phase's completion condition truly holds and whether to advance, go back, or ask
|
|
6
6
|
* the user. It runs when a run settles and the book gained an entry since the last judgment, and at a
|
|
7
|
-
* `close` declaration. The advisory is
|
|
8
|
-
*
|
|
7
|
+
* `close` declaration. The advisory is `next: <phase> (<missing>)` plus Jev's concerns, and is sent only when it
|
|
8
|
+
* changes the agent's next action: Jev has a concern, todos are still open without a followup, or the run
|
|
9
|
+
* continues. It continues the run once when Jev agrees to advance and the next phase is a verify, review, or
|
|
10
|
+
* fix, or when todos are still open without a followup (unless Jev says to ask the user). Away from close the
|
|
11
|
+
* advisory names up to three of those todos and asks the agent to continue, complete, or settle each with
|
|
12
|
+
* `metadata.followup`. An advisory with the same next phase, concerns, and open todo ids is sent once. At
|
|
13
|
+
* close only concerns are sent. A run that arranged a wake
|
|
9
14
|
* (a monitor, a loop, a subagent) is waiting, not finished, and is not judged. Outside a loop nothing
|
|
10
15
|
* runs, nor in a `quick` loop, whose only prompt is the close reminder; a subagent session registers
|
|
11
16
|
* nothing, since the agent that spawned it judges its result.
|
|
@@ -14,7 +19,7 @@
|
|
|
14
19
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
15
20
|
import { currentKind, guarded, openLoop } from "./support/book.ts";
|
|
16
21
|
import { type Answers, classify } from "./support/jev.ts";
|
|
17
|
-
import { type Snapshot, snapshot } from "./support/snapshot.ts";
|
|
22
|
+
import { type Snapshot, listTodos, snapshot } from "./support/snapshot.ts";
|
|
18
23
|
import { isSubagent } from "./support/subagent.ts";
|
|
19
24
|
|
|
20
25
|
export { isSubagent, snapshot };
|
|
@@ -87,32 +92,52 @@ export function readVerdict(answers: Answers): Verdict | undefined {
|
|
|
87
92
|
};
|
|
88
93
|
}
|
|
89
94
|
|
|
90
|
-
export function assess(verdict: Verdict, snap: Snapshot): { found: string[]
|
|
95
|
+
export function assess(verdict: Verdict, snap: Snapshot): { found: string[] } {
|
|
91
96
|
const found: string[] = [];
|
|
92
|
-
const notes: string[] = [];
|
|
93
97
|
if (verdict.done < THRESHOLDS.doneBelow) found.push("evidence not shown to cover the intent");
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
else found.push("needs full independent review");
|
|
97
|
-
}
|
|
98
|
+
// A review recorded after the last change with no blocking finding already covers the full-review rating.
|
|
99
|
+
if (verdict.review === THRESHOLDS.reviewTier && !snap.reviewClear) found.push("needs full independent review");
|
|
98
100
|
if (verdict.quality < THRESHOLDS.qualityBelow) found.push("result below the intent");
|
|
99
|
-
return { found
|
|
101
|
+
return { found };
|
|
100
102
|
}
|
|
101
103
|
|
|
102
|
-
/** True when Jev agrees to advance and the next phase is one the agent can act on now. */
|
|
104
|
+
/** True when Jev agrees to advance and the next phase is one the agent can act on now, or open todos wait and the user is not needed. */
|
|
103
105
|
export function continues(verdict: Verdict, snap: Snapshot): boolean {
|
|
106
|
+
if (snap.unsettled.length && verdict.move !== "ask-user") return true;
|
|
104
107
|
return verdict.move === "advance" && ACTIONABLE.has(snap.next);
|
|
105
108
|
}
|
|
106
109
|
|
|
107
|
-
/**
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
110
|
+
/** What Jev's move adds to the program's reading when it is not "advance". */
|
|
111
|
+
const MOVE_CONCERN: Record<Move, string | undefined> = { advance: undefined, back: "earlier evidence does not hold", "ask-user": "needs the user's decision" };
|
|
112
|
+
|
|
113
|
+
/** Jev's concerns: a move against the program's reading, then the assessment findings. */
|
|
114
|
+
export function concerns(verdict: Verdict, found: string[]): string[] {
|
|
115
|
+
const move = MOVE_CONCERN[verdict.move];
|
|
116
|
+
return move ? [move, ...found] : found;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** The open todos the agent must answer, or the call to put them to the user. */
|
|
120
|
+
function todoCall(verdict: Verdict, snap: Snapshot): string {
|
|
121
|
+
const list = `open: ${listTodos(snap.unsettled)}`;
|
|
122
|
+
return verdict.move === "ask-user"
|
|
123
|
+
? `${list} — ask the user about them with ask_user_question before ending the turn`
|
|
124
|
+
: `${list} — continue each, complete it, or set metadata.followup (a ticket id, \`memory\`, or \`drop — <reason>\`)`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** The advisory: the program's next phase and its gap, Jev's concerns, and, away from close, the open todos. Empty at close without concerns. */
|
|
128
|
+
export function message(verdict: Verdict, snap: Snapshot, found: string[], atClose = false): string {
|
|
129
|
+
const list = concerns(verdict, found);
|
|
130
|
+
if (atClose) return list.length ? `Jev: ${list.join("; ")}.` : "";
|
|
131
|
+
return [
|
|
132
|
+
`next: ${snap.next}${snap.missing ? ` (${snap.missing})` : ""}`,
|
|
133
|
+
...(list.length ? [`concerns: ${list.join("; ")}`] : []),
|
|
134
|
+
...(snap.unsettled.length ? [todoCall(verdict, snap)] : []),
|
|
135
|
+
].join("\n");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** What makes two advisories the same: the next phase, the concerns, and the open todo ids; the wording of the gap may change. */
|
|
139
|
+
function adviceKey(verdict: Verdict, snap: Snapshot, found: string[]): string {
|
|
140
|
+
return JSON.stringify([snap.next, concerns(verdict, found), snap.unsettled.map((task) => task.id)]);
|
|
116
141
|
}
|
|
117
142
|
|
|
118
143
|
export default function (pi: ExtensionAPI) {
|
|
@@ -163,9 +188,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
163
188
|
if (!snap) return;
|
|
164
189
|
const judgment = await judge(ctx, snap);
|
|
165
190
|
if (!judgment?.verdict) return;
|
|
166
|
-
const { found
|
|
191
|
+
const { found } = assess(judgment.verdict, snap);
|
|
167
192
|
pi.appendEntry(ENTRY_TYPE, { ...judgment.verdict, at: "close", concerns: found });
|
|
168
|
-
|
|
193
|
+
const text = message(judgment.verdict, snap, found, true);
|
|
194
|
+
return text ? { content: [...event.content, { type: "text" as const, text: `\n${text}` }] } : undefined;
|
|
169
195
|
}),
|
|
170
196
|
);
|
|
171
197
|
|
|
@@ -186,14 +212,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
186
212
|
// A later handler's boundary result replaces the entries an earlier one returned, so carry them along.
|
|
187
213
|
const earlier = event.entries ?? [];
|
|
188
214
|
if (!judgment.verdict) return { entries: [...earlier, record({ verdict: "unknown", reason: judgment.reason })] };
|
|
189
|
-
const { found
|
|
190
|
-
const
|
|
191
|
-
if (told.get(id) === text) return;
|
|
192
|
-
told.set(id, text);
|
|
215
|
+
const { found } = assess(judgment.verdict, snap);
|
|
216
|
+
const verdictEntry = record({ ...judgment.verdict, concerns: found });
|
|
193
217
|
const proceed = !continued && continues(judgment.verdict, snap);
|
|
218
|
+
// Jev agrees and nothing needs the agent: the verdict is recorded and nothing is said.
|
|
219
|
+
const speaks = proceed || snap.unsettled.length > 0 || concerns(judgment.verdict, found).length > 0;
|
|
220
|
+
const key = adviceKey(judgment.verdict, snap, found);
|
|
221
|
+
if (!speaks || told.get(id) === key) return { entries: [...earlier, verdictEntry] };
|
|
222
|
+
told.set(id, key);
|
|
194
223
|
if (proceed) continued = true;
|
|
195
224
|
return {
|
|
196
|
-
entries: [...earlier,
|
|
225
|
+
entries: [...earlier, verdictEntry, { type: "custom_message" as const, customType: ENTRY_TYPE, content: message(judgment.verdict, snap, found), display: true }],
|
|
197
226
|
...(proceed ? { continue: true } : {}),
|
|
198
227
|
};
|
|
199
228
|
}),
|
package/pi/support/phases.ts
CHANGED
|
@@ -171,13 +171,10 @@ export function unansweredReach(entries: Entry[], reach: Reach | undefined): str
|
|
|
171
171
|
|
|
172
172
|
/** The one line that asks an unreviewed loop to re-classify; an undeclared loop is asked to declare its kind. */
|
|
173
173
|
export function reachLine(reach: Reach, kind: string | undefined): string {
|
|
174
|
-
const
|
|
175
|
-
const
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
? `call \`kind\` with \`reason\`: upgrade to reviewed, or say why ${kind} still fits`
|
|
179
|
-
: "declare `kind` with `intent` and `reason`: reviewed when it reaches past its files, or inline and why it still fits";
|
|
180
|
-
return `the change reaches ${files} across ${modules}${told}; ${ask}`;
|
|
174
|
+
const modules = reach.modules.length ? ` / ${reach.modules.length} module${reach.modules.length === 1 ? "" : "s"} (${reach.modules.slice(0, 4).join(", ")}${reach.modules.length > 4 ? ", …" : ""})` : "";
|
|
175
|
+
const told = reach.behavior.length ? ` / ${reach.behavior.slice(0, 3).join(", ")}${reach.behavior.length > 3 ? ", …" : ""}` : "";
|
|
176
|
+
const ask = kind ? `kind with reason: reviewed, or why ${kind} fits` : "declare kind with intent and reason: reviewed, or why inline fits";
|
|
177
|
+
return `reach ${reach.source.length} file${reach.source.length === 1 ? "" : "s"}${modules}${told}; ${ask}`;
|
|
181
178
|
}
|
|
182
179
|
|
|
183
180
|
// --- evidence ------------------------------------------------------------------------------------
|
|
@@ -284,7 +281,7 @@ function evaluate(name: string, ctx: Ctx): Status {
|
|
|
284
281
|
}
|
|
285
282
|
case "anchor": {
|
|
286
283
|
const index = lastWhere(entries, from, (entry) => entry.stage === "anchor");
|
|
287
|
-
return index >= 0 ? done(name, String(entries[index].check ?? "")) : missing(name, "a `Reality anchor:` line, or `anchor` on the kind call");
|
|
284
|
+
return index >= 0 ? done(name, String(entries[index].check ?? "")) : missing(name, "a `Reality anchor:` line, `metadata.anchor` on the first todo, or `anchor` on the kind call");
|
|
288
285
|
}
|
|
289
286
|
case "act":
|
|
290
287
|
return lastWhere(entries, from, (entry) => entry.stage === "edit") >= 0 ? done(name) : missing(name, "a change to the code");
|
|
@@ -362,11 +359,9 @@ export function incompleteBefore(phases: Phases, entering: string): Status[] {
|
|
|
362
359
|
return phases.statuses.filter((status, index) => index < at && status.state === "missing");
|
|
363
360
|
}
|
|
364
361
|
|
|
365
|
-
/** One line: the
|
|
362
|
+
/** One line: the phases still missing before the one entered. */
|
|
366
363
|
export function jumpLine(entering: string, missingBefore: Status[]): string {
|
|
367
|
-
|
|
368
|
-
const skip = missingBefore[0]?.name ?? "<phase>";
|
|
369
|
-
return `anchorloop: ${entering} comes before ${list}; skip with \`anchorloop_stage skip ${skip} <reason>\`.`;
|
|
364
|
+
return `anchorloop: before ${entering}, still missing: ${missingBefore.map((status) => `${status.name} (${status.note})`).join("; ")}.`;
|
|
370
365
|
}
|
|
371
366
|
|
|
372
367
|
/** The phase line `read` and the close summary show. */
|
package/pi/support/snapshot.ts
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
stagesDone,
|
|
17
17
|
TASK_LEGEND,
|
|
18
18
|
taskLines,
|
|
19
|
+
validFollowup,
|
|
19
20
|
verifyAfterLastAct,
|
|
20
21
|
} from "./book.ts";
|
|
21
22
|
import { computePhases } from "./phases.ts";
|
|
@@ -29,12 +30,30 @@ export type Snapshot = {
|
|
|
29
30
|
current: string;
|
|
30
31
|
/** A review recorded after the last change with no blocking finding on it. */
|
|
31
32
|
reviewClear: boolean;
|
|
33
|
+
/** Open todos that no valid `metadata.followup` settles yet. */
|
|
34
|
+
unsettled: { id: number; subject: string }[];
|
|
32
35
|
};
|
|
33
36
|
|
|
37
|
+
const LISTED_TODOS = 3;
|
|
38
|
+
const SUBJECT_LIMIT = 60;
|
|
39
|
+
|
|
40
|
+
const clipped = (subject: string, limit: number): string => {
|
|
41
|
+
const flat = subject.replace(/\s+/g, " ").trim();
|
|
42
|
+
return flat.length <= limit ? flat : `${flat.slice(0, limit - 1)}…`;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/** The first few todos as `#id subject` (subject cut to 60 characters) joined by `; `, then `+N more`. */
|
|
46
|
+
export function listTodos(todos: { id: number; subject: string }[], limit = LISTED_TODOS): string {
|
|
47
|
+
const lines = todos.slice(0, limit).map((task) => `#${task.id} ${clipped(task.subject, SUBJECT_LIMIT)}`);
|
|
48
|
+
return [...lines, ...(todos.length > limit ? [`+${todos.length - limit} more`] : [])].join("; ");
|
|
49
|
+
}
|
|
50
|
+
|
|
34
51
|
export function snapshot(book: Book, ctx: { cwd: string; sessionManager: { getBranch(): unknown[] } }): Snapshot {
|
|
35
52
|
const { entries } = book;
|
|
36
53
|
const todos = replayTodos(ctx.sessionManager.getBranch());
|
|
37
|
-
const
|
|
54
|
+
const openTasks = todos.filter(isOpenTask);
|
|
55
|
+
const unsettled = openTasks.filter((task) => !validFollowup(task.metadata?.followup));
|
|
56
|
+
const settled = openTasks.length - unsettled.length;
|
|
38
57
|
const phases = computePhases(book, { cwd: ctx.cwd, todos });
|
|
39
58
|
const check = verifyAfterLastAct(entries);
|
|
40
59
|
const verify = [
|
|
@@ -56,7 +75,7 @@ export function snapshot(book: Book, ctx: { cwd: string; sessionManager: { getBr
|
|
|
56
75
|
diff: diffText(diffAgainstBases(entries)),
|
|
57
76
|
phase: `current: ${phases.current}; next: ${phases.next}${phases.missing ? `; missing: ${phases.missing}` : ""}${phases.skipped.length ? `; skipped: ${phases.skipped.join(", ")}` : ""}`.slice(0, 600),
|
|
58
77
|
verify: verify.slice(0, 1200),
|
|
59
|
-
todos: `${
|
|
78
|
+
todos: `${openTasks.length} open of ${todos.length}${unsettled.length ? `: ${listTodos(unsettled, 5)}` : ""}${settled ? ` (${settled} with a followup)` : ""}`.slice(0, 700),
|
|
60
79
|
tasks: [`legend: ${TASK_LEGEND}`, ...taskLines(entries, todos, ctx.cwd)].join("\n").slice(0, 1500),
|
|
61
80
|
friction: (friction.join("\n") || "none").slice(0, 800),
|
|
62
81
|
},
|
|
@@ -64,5 +83,6 @@ export function snapshot(book: Book, ctx: { cwd: string; sessionManager: { getBr
|
|
|
64
83
|
missing: phases.missing,
|
|
65
84
|
current: phases.current,
|
|
66
85
|
reviewClear: reviewClearAfterLastAct(entries),
|
|
86
|
+
unsettled: unsettled.map((task) => ({ id: task.id, subject: task.subject })),
|
|
67
87
|
};
|
|
68
88
|
}
|
package/pi/support/validators.ts
CHANGED
|
@@ -37,6 +37,7 @@ const RED_LINE_MARKERS = new RegExp(
|
|
|
37
37
|
`${wordAround("never|must not|do not|don't|forbidden|barred|prohibited|avoid")}|不要|禁止|不得|不准|嚴禁`,
|
|
38
38
|
"giu",
|
|
39
39
|
);
|
|
40
|
+
const MAX_ADVISORIES = 3;
|
|
40
41
|
const RED_LINE_MIN_BUDGET = 3;
|
|
41
42
|
const RED_LINE_DENSITY = 0.1;
|
|
42
43
|
const EMPHATIC_WORDS = new RegExp(wordAround("(NEVER|ALWAYS|MUST NOT|MUST|FORBIDDEN|DO NOT)"), "gu");
|
|
@@ -151,27 +152,19 @@ function checkRulesFile(path: string): string[] {
|
|
|
151
152
|
|
|
152
153
|
const found = findDefensivePhrases(text, DEFENSIVE_PATTERNS);
|
|
153
154
|
if (found.length) {
|
|
154
|
-
warnings.push(
|
|
155
|
-
`detected defensive wording: ${found.join(", ")}. ` +
|
|
156
|
-
"State desired behavior directly; modern models do not need excessive red lines.",
|
|
157
|
-
);
|
|
155
|
+
warnings.push(`detected defensive wording: ${found.join(", ")}`);
|
|
158
156
|
}
|
|
159
157
|
|
|
160
158
|
const lines = splitlines(text)
|
|
161
159
|
.map((line) => line.trim())
|
|
162
160
|
.filter((line) => line && !line.startsWith("#"));
|
|
163
161
|
if (lines.length > 200) {
|
|
164
|
-
warnings.push(
|
|
165
|
-
`rule file has ${lines.length} non-empty lines. Consider consolidating into higher-level principles ` +
|
|
166
|
-
"or separating into specialized modules rather than accumulating micro-rules.",
|
|
167
|
-
);
|
|
162
|
+
warnings.push(`rule file has ${lines.length} non-empty lines (over 200)`);
|
|
168
163
|
}
|
|
169
164
|
|
|
170
165
|
const simplified = [...SIMPLIFIED_CHINESE_CHARS].filter((char) => text.includes(char));
|
|
171
166
|
if (simplified.length) {
|
|
172
|
-
warnings.push(
|
|
173
|
-
`possible Simplified Chinese detected (${simplified.slice(0, 5).join("")}). Always use Traditional Chinese.`,
|
|
174
|
-
);
|
|
167
|
+
warnings.push(`possible Simplified Chinese detected (${simplified.slice(0, 5).join("")})`);
|
|
175
168
|
}
|
|
176
169
|
return warnings;
|
|
177
170
|
}
|
|
@@ -216,10 +209,7 @@ function checkSkillMd(path: string): string[] {
|
|
|
216
209
|
}
|
|
217
210
|
|
|
218
211
|
if (lines.length > SKILL_MAX_RECOMMENDED_LINES && !isDirectory(join(skillDir, "references"))) {
|
|
219
|
-
warnings.push(
|
|
220
|
-
`skill has ${lines.length} lines (exceeds recommended ${SKILL_MAX_RECOMMENDED_LINES}). ` +
|
|
221
|
-
"Move detailed guidelines or templates to references/ to keep SKILL.md streamlined.",
|
|
222
|
-
);
|
|
212
|
+
warnings.push(`skill has ${lines.length} lines (over ${SKILL_MAX_RECOMMENDED_LINES}) and no references/`);
|
|
223
213
|
}
|
|
224
214
|
|
|
225
215
|
// Broken markdown links, followed through every Markdown file SKILL.md reaches.
|
|
@@ -262,10 +252,7 @@ function checkSkillMd(path: string): string[] {
|
|
|
262
252
|
|
|
263
253
|
const defensive = findDefensivePhrases(text, DEFENSIVE_PATTERNS);
|
|
264
254
|
if (defensive.length) {
|
|
265
|
-
warnings.push(
|
|
266
|
-
`detected defensive anti-pattern(s): ${defensive.join(", ")}. ` +
|
|
267
|
-
"State expected behavior directly rather than constructing defensive red lines.",
|
|
268
|
-
);
|
|
255
|
+
warnings.push(`detected defensive anti-pattern(s): ${defensive.join(", ")}`);
|
|
269
256
|
}
|
|
270
257
|
return warnings;
|
|
271
258
|
}
|
|
@@ -286,10 +273,7 @@ function checkSkillAdvisories(path: string): string[] {
|
|
|
286
273
|
const markers = (body.match(RED_LINE_MARKERS) ?? []).length;
|
|
287
274
|
const budget = Math.max(RED_LINE_MIN_BUDGET, Math.floor(lines.filter((line) => line.trim()).length * RED_LINE_DENSITY));
|
|
288
275
|
if (markers > budget) {
|
|
289
|
-
advisories.push(
|
|
290
|
-
`${markers} red-line markers exceed the budget of ${budget}. Keep a red line only when an ` +
|
|
291
|
-
"observed failure backs it, and delete the rest.",
|
|
292
|
-
);
|
|
276
|
+
advisories.push(`${markers} red-line markers exceed the budget of ${budget}`);
|
|
293
277
|
}
|
|
294
278
|
|
|
295
279
|
const seen = new Map<string, number>();
|
|
@@ -299,19 +283,16 @@ function checkSkillAdvisories(path: string): string[] {
|
|
|
299
283
|
seen.set(word, (seen.get(word) ?? 0) + 1);
|
|
300
284
|
const window = lines.slice(index, index + 2).join(" ");
|
|
301
285
|
if (!REASON_WORDS.test(window)) {
|
|
302
|
-
advisories.push(
|
|
303
|
-
`line ${index + 1}: emphatic '${word}' has no reason beside it. ` +
|
|
304
|
-
"State the consequence, or drop the emphasis.",
|
|
305
|
-
);
|
|
286
|
+
advisories.push(`line ${index + 1}: emphatic '${word}' has no reason beside it`);
|
|
306
287
|
}
|
|
307
288
|
}
|
|
308
289
|
});
|
|
309
290
|
for (const [word, count] of seen) {
|
|
310
291
|
if (count > 1) {
|
|
311
|
-
advisories.push(`emphatic '${word}' appears ${count} times
|
|
292
|
+
advisories.push(`emphatic '${word}' appears ${count} times`);
|
|
312
293
|
}
|
|
313
294
|
}
|
|
314
|
-
return advisories;
|
|
295
|
+
return advisories.slice(0, MAX_ADVISORIES);
|
|
315
296
|
}
|
|
316
297
|
|
|
317
298
|
// ---- durable specs (durable_validator.py) ----
|
|
@@ -322,10 +303,6 @@ function checkDecisionMd(path: string): string[] {
|
|
|
322
303
|
if (!/\|\s*Question\s*\|\s*Answer\s*\|\s*Basis\s*\|\s*Status\s*\|/i.test(text)) {
|
|
323
304
|
warnings.push("decision.md should include a Question | Answer | Basis | Status table (Status: grounded, confirmed, or open).");
|
|
324
305
|
}
|
|
325
|
-
const lower = text.toLowerCase();
|
|
326
|
-
if (!lower.includes("rule") && !lower.includes("readiness") && !lower.includes("prerequisite") && !lower.includes("core")) {
|
|
327
|
-
warnings.push("confirm core rules readiness before implementation; note readiness in decision.md.");
|
|
328
|
-
}
|
|
329
306
|
return warnings;
|
|
330
307
|
}
|
|
331
308
|
|
|
@@ -335,9 +312,6 @@ function checkSpecMd(path: string): string[] {
|
|
|
335
312
|
if (!/Status:\s*(proposed|approved)/i.test(text)) {
|
|
336
313
|
warnings.push("spec.md must declare 'Status: proposed' or 'Status: approved' at the top.");
|
|
337
314
|
}
|
|
338
|
-
if (!text.toLowerCase().includes("reality anchor") && !text.toLowerCase().includes("anchor")) {
|
|
339
|
-
warnings.push("spec.md should name the chosen reality anchor and its checkpoint.");
|
|
340
|
-
}
|
|
341
315
|
return warnings;
|
|
342
316
|
}
|
|
343
317
|
|
|
@@ -351,18 +325,13 @@ function checkVerificationMd(path: string): string[] {
|
|
|
351
325
|
const results = [...text.matchAll(/\|\s*[^|]+\s*\|\s*[^|]+\s*\|\s*([a-zA-Z]+)\s*\|/g)].map((match) => match[1].toLowerCase());
|
|
352
326
|
const invalid = [...new Set(results.filter((result) => !VALID_VERIFICATION_RESULTS.has(result) && result !== "result"))];
|
|
353
327
|
if (invalid.length) {
|
|
354
|
-
warnings.push(
|
|
355
|
-
`invalid verification Result values found: ${invalid.join(", ")}. Result must be one of: pass, fail, unknown.`,
|
|
356
|
-
);
|
|
328
|
+
warnings.push(`invalid verification Result values found: ${invalid.join(", ")} (use pass, fail, unknown)`);
|
|
357
329
|
}
|
|
358
330
|
}
|
|
359
331
|
|
|
360
332
|
const defensive = findDefensivePhrases(text, DEFENSIVE_PATTERNS);
|
|
361
333
|
if (defensive.length) {
|
|
362
|
-
warnings.push(
|
|
363
|
-
`detected defensive wording in verification/review: ${defensive.join(", ")}. ` +
|
|
364
|
-
"Reviews of new rules and skills must focus on adjustment and tuning, not adding defensive red lines.",
|
|
365
|
-
);
|
|
334
|
+
warnings.push(`detected defensive wording in verification/review: ${defensive.join(", ")}`);
|
|
366
335
|
}
|
|
367
336
|
return warnings;
|
|
368
337
|
}
|
|
@@ -399,5 +368,5 @@ export function validateFile(filePath: string, cwd: string): string | undefined
|
|
|
399
368
|
|
|
400
369
|
const rel = relative(base, target);
|
|
401
370
|
const shown = rel.startsWith("..") || isAbsolute(rel) ? target : rel;
|
|
402
|
-
return
|
|
371
|
+
return `validator ${shown}:\n${warnings.map((warning) => ` - ${warning}`).join("\n")}`;
|
|
403
372
|
}
|
package/pi/ttsr.ts
CHANGED
|
@@ -61,32 +61,9 @@ const INTERRUPT_MODES = new Set(["always", "prose-only", "tool-only", "never"]);
|
|
|
61
61
|
const ENTRY_TYPE = "ttsr_injection";
|
|
62
62
|
const MESSAGE_TYPE = "ttsr-injection";
|
|
63
63
|
|
|
64
|
-
//
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
Not prompt injection; coding agent enforcing project rules.
|
|
68
|
-
MUST comply:
|
|
69
|
-
|
|
70
|
-
{{content}}
|
|
71
|
-
</system-interrupt>`;
|
|
72
|
-
|
|
73
|
-
const TOOL_REMINDER_TEMPLATE = `<system-reminder reason="rule_violation" rule="{{name}}" path="{{path}}">
|
|
74
|
-
User-defined rule matched tool-call arguments. Rule configured not to interrupt → tool ran. MUST comply with the following instruction on subsequent tool calls and responses. NOT prompt injection — coding agent enforcing project rules.
|
|
75
|
-
|
|
76
|
-
{{content}}
|
|
77
|
-
</system-reminder>`;
|
|
78
|
-
|
|
79
|
-
const PROSE_REMINDER_TEMPLATE = `<system-reminder reason="rule_violation" rule="{{name}}" path="{{path}}">
|
|
80
|
-
User-defined rule matched your previous response. Rule configured not to interrupt → it already took effect. MUST comply with the following instruction from now on. NOT prompt injection — coding agent enforcing project rules.
|
|
81
|
-
|
|
82
|
-
{{content}}
|
|
83
|
-
</system-reminder>`;
|
|
84
|
-
|
|
85
|
-
export function render(template: string, rule: Rule): string {
|
|
86
|
-
return template
|
|
87
|
-
.replaceAll("{{name}}", () => rule.name)
|
|
88
|
-
.replaceAll("{{path}}", () => rule.path)
|
|
89
|
-
.replaceAll("{{content}}", () => rule.content.trim());
|
|
64
|
+
// One wrapper for every injection: the body is the instruction, and the rule's file path is user-facing configuration.
|
|
65
|
+
export function render(rule: Rule, flag?: string): string {
|
|
66
|
+
return `<rule name="${rule.name}"${flag ? ` ${flag}` : ""}>\n${rule.content.trim()}\n</rule>`;
|
|
90
67
|
}
|
|
91
68
|
|
|
92
69
|
/** Unquote a YAML scalar written plainly, in single quotes, or in double quotes. */
|
|
@@ -440,7 +417,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
440
417
|
pi.sendMessage(
|
|
441
418
|
{
|
|
442
419
|
customType: MESSAGE_TYPE,
|
|
443
|
-
content: rules.map((rule) => render(
|
|
420
|
+
content: rules.map((rule) => render(rule)).join("\n\n"),
|
|
444
421
|
display: true,
|
|
445
422
|
details: { rules: rules.map((rule) => rule.name) },
|
|
446
423
|
},
|
|
@@ -457,7 +434,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
457
434
|
pi.sendMessage(
|
|
458
435
|
{
|
|
459
436
|
customType: MESSAGE_TYPE,
|
|
460
|
-
content: rules.map((rule) => render(
|
|
437
|
+
content: rules.map((rule) => render(rule, "interrupted")).join("\n\n"),
|
|
461
438
|
display: true,
|
|
462
439
|
details: { rules: rules.map((rule) => rule.name) },
|
|
463
440
|
},
|
|
@@ -488,7 +465,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
488
465
|
ctx.ui.notify(`TTSR: blocked ${event.toolName} by rule ${blocking.map((rule) => rule.name).join(", ")}`, "warning");
|
|
489
466
|
return {
|
|
490
467
|
block: true,
|
|
491
|
-
reason: [...blocking.map((rule) => render(
|
|
468
|
+
reason: [...blocking.map((rule) => render(rule, "blocking")), ...passive.map((rule) => render(rule, "passive"))].join("\n\n"),
|
|
492
469
|
};
|
|
493
470
|
}
|
|
494
471
|
toolReminders.set(event.toolCallId, passive);
|
|
@@ -500,7 +477,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
500
477
|
toolReminders.delete(event.toolCallId);
|
|
501
478
|
pi.appendEntry(ENTRY_TYPE, { rules: rules.map((rule) => rule.name) });
|
|
502
479
|
return {
|
|
503
|
-
content: [...event.content, { type: "text", text: rules.map((rule) => render(
|
|
480
|
+
content: [...event.content, { type: "text", text: rules.map((rule) => render(rule)).join("\n\n") }],
|
|
504
481
|
};
|
|
505
482
|
});
|
|
506
483
|
|
package/pi/validate-tool-use.ts
CHANGED
|
@@ -11,7 +11,7 @@ function validate(filePath: string, cwd: string): string | undefined {
|
|
|
11
11
|
try {
|
|
12
12
|
return validateFile(filePath, cwd);
|
|
13
13
|
} catch (error) {
|
|
14
|
-
return
|
|
14
|
+
return `validator did not run: ${error instanceof Error ? error.message : String(error)}`;
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
17
|
|
|
@@ -9,11 +9,11 @@ Every entry skill (`answering`, `investigating`, `inspecting`, `coding`, `writin
|
|
|
9
9
|
|
|
10
10
|
## Align
|
|
11
11
|
|
|
12
|
-
Pick the entry's playbook from its index; with none fitting, run the loop bare. `todo` holds real work items only, optionally with `metadata.anchor` (a task's own check) and `metadata.ref` (`<path>#<heading>`). The first `todo` create opens
|
|
12
|
+
Pick the entry's playbook from its index; with none fitting, run the loop bare. `todo` holds real work items only, optionally with `metadata.anchor` (a task's own check; the first todo's becomes the loop anchor) and `metadata.ref` (`<path>#<heading>`). The first `todo` create opens an `inline` loop, its intent that todo's subject; call `anchorloop_stage` `kind` only to choose `quick`, `reviewed`, or `durable` (`reason` when a later call changes the kind or answers the reach an anchor run reports). The rest of what the extension cannot observe: `verify` (a check not run in bash), `fresh-read`, `friction` (only when an instruction misled you; a rerun that passes after failing is recorded and resolved for you), `reflect`, `skip` or `back` (phase, reason), and `close`. Edits, anchor runs, reviews, and spec files are read from evidence; a phase that starts before an earlier one is complete warns once (`read` if you disagree), and `close` refuses only while an open todo lacks `metadata.followup` (a ticket id, `memory`, or `drop — <reason>`).
|
|
13
13
|
|
|
14
14
|
## Anchor
|
|
15
15
|
|
|
16
|
-
Before the first change the loop has an anchor:
|
|
16
|
+
Before the first change the loop has an anchor: a `Reality anchor: <the exact check that observes the result, and its checkpoint>` line, `metadata.anchor` on the first todo, or `anchor` on a `kind` call. The first edit without one gets a one-line reminder in its result.
|
|
17
17
|
|
|
18
18
|
Size the anchor to the change's reach and pick one method: an executable check, a user operation, human judgment, or a focused review. A coordinator's anchor governs. Pick a review anchor when the change touches a shared contract, authorization or permission data, a migration, or security; other changes rest on their check and the author's own read of the diff. A review anchor names the realistic use it judges against and blocks on P0 and P1 findings only, recording lower ones as follow-ups. Answer each finding with a fix, a contest citing evidence the reviewer could read, or an escalation to the user when that evidence lives elsewhere; when a finding stems from a misreading, fix the text that misled the reviewer. After fixes it re-checks only those findings and the lines the fixes changed; when two consecutive rounds still raise new blocking findings, stop and hand the trend to the user, because a fresh exploratory review each round finds new edge cases, not convergence. The checkpoint is normally once, after the last increment.
|
|
19
19
|
|
|
@@ -29,11 +29,11 @@ The reply names a [principle](PRINCIPLES.md) only with the decision it changed.
|
|
|
29
29
|
|
|
30
30
|
### Friction Notes
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
An anchor that passed, failed, and passed again is recorded and reflected as resolved by the extension. Record `anchorloop_stage friction` yourself only for a failure whose cause a later attempt revealed, or when an instruction misled an attempt (`led_by`).
|
|
33
33
|
|
|
34
34
|
### Decision Trail
|
|
35
35
|
|
|
36
|
-
A run the user reviews after stepping away records one row per fork,
|
|
36
|
+
A run the user reviews after stepping away records one row per fork, pivot, or blocker in `scratch/decisions-<task-slug>.tsv` with the `anchorloop_decision` tool; completed units come from `git log`. Evidence is a pointer (commit, `file:line`, artifact path), never prose, and a wrong call gets a superseding row instead of an edit. Before handing back, check every row against what the run did and build the report from the trail.
|
|
37
37
|
|
|
38
38
|
## Verify
|
|
39
39
|
|
|
@@ -41,7 +41,7 @@ Exercise the anchor and report what it observed. A requirement the anchor did no
|
|
|
41
41
|
|
|
42
42
|
A review anchor stays open until an independent reviewer returns its verdict. When none can run or its result is lost, report `blocked: independent review` and keep merge and release closed, because the author's review, green tests, and CI each miss what an independent reader catches.
|
|
43
43
|
|
|
44
|
-
Write the reply the playbook's `Reply:` line names, with the anchor's evidence. The Jev second thought
|
|
44
|
+
Write the reply the playbook's `Reply:` line names, with the anchor's evidence. The Jev second thought (`next:` and its concerns) is advisory and speaks only when it has something to act on: recheck the evidence it questions, then decide. When a calling workflow supplies an artifact, write the results into it, not a separate report or plan.
|
|
45
45
|
|
|
46
46
|
### Reflexive Pass
|
|
47
47
|
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
1. Align: state the exit condition as a predicate a command or artifact can pass or fail; a duration is not one.
|
|
6
6
|
2. Anchor: the predicate is the anchor. Set the wake mechanism: `MonitorCreate` with `onDone` or `onSignal` for an event (CI, a merge, a job), a `LoopCreate` heartbeat otherwise, plus one safety deadline.
|
|
7
7
|
3. Act: each iteration makes the smallest change the evidence justifies, checks it against the predicate, commits when it advanced, and discards it when it did not.
|
|
8
|
-
4. Act: record one decision-trail row per
|
|
8
|
+
4. Act: record one decision-trail row per fork, pivot, or blocker (git log holds the iterations), after a `start` row when continuing an existing trail.
|
|
9
9
|
5. Act: fix reversible mid-run discoveries yourself in their own commits, then return to the predicate. Bring only irreversible actions, user-owned decisions, or a real dead end to the user, and leave them in the trail when the user is asleep.
|
|
10
10
|
6. Verify: stop when the predicate holds. On a plateau, change approach; never relax the predicate to declare victory. A genuine dead end ends the run with a written reason.
|
|
11
11
|
|
package/skills/coding/SKILL.md
CHANGED
|
@@ -11,7 +11,7 @@ Run the loop in [../anchorloop/SKILL.md](../anchorloop/SKILL.md) with one of the
|
|
|
11
11
|
|
|
12
12
|
Before the first production edit, classify the change by what it reaches, not by how settled the approach feels:
|
|
13
13
|
|
|
14
|
-
- **Inline:** the change stays inside the files it edits: no export another file uses, no behavior its users are told about, and no behavior the requirement leaves open. The
|
|
14
|
+
- **Inline:** the change stays inside the files it edits: no export another file uses, no behavior its users are told about, and no behavior the requirement leaves open. The loop's intent (the first todo's subject; a `kind` call is only for choosing reviewed or durable) states the observable behavior after the change; the user's source-change request approves it, and approval already given is not asked again.
|
|
15
15
|
- **Reviewed:** the change reaches past its files without a decision the user owns: it changes an export other files use, a behavior users see (config keys, output, notices, command flags), or a behavior the requirement leaves open, which the reply names; or it touches a shared contract, authorization or permission data, a migration, or security. Before the first edit, write its premises in the todo's `metadata.premises`: three to five lines on what the change assumes holds, such as where its results land, what names may contain, and how paths are spelled. It needs no spec and no further approval.
|
|
16
16
|
- **Durable:** the work needs a persistent design record because of material ambiguity, a trade-off the user owns, cross-component coordination, migration risk, or a user-requested spec. Read [MINI-SDD.md](MINI-SDD.md) and create or resume its folder, which keeps no `tasks.md`; Spec waits for the user's approval before any production edit.
|
|
17
17
|
|