pi-anchorloop 2.1.1 → 2.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,28 @@
1
+ /** Shared access to Jev (TypeSafe's typed-decision model) through pi's classifier registry. */
2
+
3
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
4
+
5
+ export const TIMEOUT_MS = 15_000;
6
+
7
+ export type Question = { type: "bool" | "choice" | "score"; instructions: string; criteria: unknown };
8
+ export type Answers = Record<string, { type: string; probability?: number; choice?: string; probabilities?: Record<string, number>; score?: number }>;
9
+
10
+ /** The classification answers, or the reason none came: no key, a timeout, or a failure. */
11
+ export async function classify(ctx: ExtensionContext, state: Record<string, string>, questions: Record<string, Question>): Promise<{ answers?: Answers; reason: string }> {
12
+ const jev = ctx.modelRegistry.findOfType("classifier", "typesafe", "jev-latest");
13
+ type AuthModel = Parameters<typeof ctx.modelRegistry.hasConfiguredAuth>[0];
14
+ if (!jev) return { reason: "no-auth" };
15
+ // SAFETY: hasConfiguredAuth is typed for chat models but reads only `provider`, which a classifier has too.
16
+ const authModel = jev as unknown as AuthModel;
17
+ if (!ctx.modelRegistry.hasConfiguredAuth(authModel)) return { reason: "no-auth" };
18
+ try {
19
+ const result = await ctx.modelRegistry.classify(
20
+ jev,
21
+ { state, questions } as Parameters<typeof ctx.modelRegistry.classify>[1],
22
+ { signal: AbortSignal.any([AbortSignal.timeout(TIMEOUT_MS), ...(ctx.signal ? [ctx.signal] : [])]) },
23
+ );
24
+ return result.stopReason === "stop" ? { answers: result.answers as Answers, reason: "stop" } : { reason: result.stopReason };
25
+ } catch (error) {
26
+ return { reason: error instanceof Error ? error.message : String(error) };
27
+ }
28
+ }
@@ -0,0 +1,301 @@
1
+ /**
2
+ * The phase machine: each loop kind runs a fixed sequence of phases, and the book's evidence says which
3
+ * phases are complete. Nothing here blocks; a phase entered while an earlier one is incomplete yields
4
+ * one warning line that names what is missing and the `skip` escape. Phases regress by themselves when
5
+ * evidence changes (a diff after verify, a spec edited back to proposed) and by `back` on request.
6
+ */
7
+
8
+ import { existsSync, realpathSync } from "node:fs";
9
+ import { tmpdir } from "node:os";
10
+ import { basename, dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
11
+ import {
12
+ type Book,
13
+ type Entry,
14
+ type Kind,
15
+ type Task,
16
+ allFriction,
17
+ anchorCommand,
18
+ artifactOnDisk,
19
+ currentKind,
20
+ lastActIndex,
21
+ lastIndex,
22
+ repoRootOf,
23
+ retryWithoutFriction,
24
+ specFolder,
25
+ withChildActs,
26
+ } from "./book.ts";
27
+
28
+ export const SEQUENCES: Record<Kind, readonly string[]> = {
29
+ quick: ["act", "check", "reflect", "close"],
30
+ inline: ["anchor", "act", "verify", "reflect", "close"],
31
+ reviewed: ["anchor", "act", "verify", "review", "reflect", "close"],
32
+ durable: ["decide", "grill", "spec", "read", "approve", "design", "anchor", "act", "verify", "review", "reflect", "close"],
33
+ };
34
+
35
+ export type Status = { name: string; state: "done" | "skipped" | "missing"; note: string; /** Skipped by the evidence itself (no open rows, no friction), not by a `skip` declaration. */ auto?: boolean };
36
+
37
+ export type Phases = {
38
+ kind: Kind | "undeclared";
39
+ statuses: Status[];
40
+ /** The last phase of the complete prefix, or `start`. */
41
+ current: string;
42
+ /** The first incomplete phase, `fix` when a check or review failed after the last change, else `close`. */
43
+ next: string;
44
+ missing: string;
45
+ skipped: string[];
46
+ backs: string[];
47
+ /** Advisory hints that are not phases. */
48
+ hints: string[];
49
+ };
50
+
51
+ export type Inputs = { entries: Entry[]; cwd: string; todos: Task[]; env?: NodeJS.ProcessEnv };
52
+
53
+ // --- paths ---------------------------------------------------------------------------------------
54
+
55
+ const EXEMPT_IN_PROJECT = ["scratch", "docs/specs"];
56
+ const EXEMPT_ROOTS = [tmpdir(), "/tmp", "/var/folders"];
57
+ const EXEMPT_EXTENSIONS = [".md", ".txt", ".html", ".htm"];
58
+
59
+ /** The real path of `path`, resolving symlinks such as /var → /private/var through its nearest existing parent. */
60
+ function real(path: string): string {
61
+ if (existsSync(path)) return realpathSync(path);
62
+ const parent = dirname(path);
63
+ return parent === path ? path : join(real(parent), basename(path));
64
+ }
65
+
66
+ function within(root: string, target: string): boolean {
67
+ const rel = relative(root, target);
68
+ return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
69
+ }
70
+
71
+ /**
72
+ * True for a file that is not code work: scratch and spec folders of the project or of any git repo the file lives in, and
73
+ * temp files outside every repo. A file inside a git repo counts as work wherever the repo is, even under a temp dir.
74
+ */
75
+ export function outsideWork(filePath: string, cwd: string): boolean {
76
+ const root = real(cwd);
77
+ const target = real(resolve(cwd, filePath));
78
+ const inFolders = (base: string) => EXEMPT_IN_PROJECT.some((dir) => within(join(base, dir), target));
79
+ if (within(root, target)) return inFolders(root);
80
+ const repo = repoRootOf(target);
81
+ if (repo) return inFolders(repo);
82
+ return EXEMPT_ROOTS.some((tmp) => within(real(tmp), target));
83
+ }
84
+
85
+ /** Markdown, text, and HTML files and everything `outsideWork` names: an edit the anchor gate does not warn about. */
86
+ export function exempt(filePath: string, cwd: string): boolean {
87
+ return EXEMPT_EXTENSIONS.includes(extname(filePath).toLowerCase()) || outsideWork(filePath, cwd);
88
+ }
89
+
90
+ // --- evidence ------------------------------------------------------------------------------------
91
+
92
+ type Ctx = Inputs & { seq: readonly string[]; folder?: string; lastAct: number };
93
+
94
+ const done = (name: string, note = ""): Status => ({ name, state: "done", note });
95
+ const missing = (name: string, note: string): Status => ({ name, state: "missing", note });
96
+
97
+ function same(a: string, b: string): boolean {
98
+ const group = (name: string) => (name === "check" ? "verify" : name);
99
+ return group(a) === group(b);
100
+ }
101
+
102
+ /** Entries before this index do not count for `name`: the latest `back` to this phase or an earlier one. */
103
+ function floor(entries: Entry[], seq: readonly string[], name: string): number {
104
+ const at = seq.indexOf(name);
105
+ let from = 0;
106
+ entries.forEach((entry, index) => {
107
+ if (entry.stage !== "back" || typeof entry.phase !== "string") return;
108
+ const target = seq.findIndex((candidate) => same(candidate, entry.phase as string));
109
+ if (target >= 0 && at >= target) from = index + 1;
110
+ });
111
+ return from;
112
+ }
113
+
114
+ function lastWhere(entries: Entry[], from: number, test: (entry: Entry) => boolean): number {
115
+ for (let i = entries.length - 1; i >= from; i--) if (test(entries[i])) return i;
116
+ return -1;
117
+ }
118
+
119
+ function skipEntry(entries: Entry[], from: number, name: string): Entry | undefined {
120
+ const index = lastWhere(entries, from, (entry) => entry.stage === "skip" && typeof entry.phase === "string" && same(entry.phase, name));
121
+ return index >= 0 ? entries[index] : undefined;
122
+ }
123
+
124
+ /** The latest recorded summary of an artifact since `from`; a loop that never wrote it falls back to the file on disk. */
125
+ function artifact(ctx: Ctx, name: string, from: number): Entry | undefined {
126
+ const index = lastWhere(ctx.entries, from, (entry) => entry.stage === name && entry.auto === true);
127
+ if (index >= 0) return ctx.entries[index];
128
+ const wrote = ctx.entries.some((entry) => entry.stage === name && entry.auto === true);
129
+ return from === 0 && !wrote && ctx.folder ? artifactOnDisk(ctx.folder, name, ctx.cwd) : undefined;
130
+ }
131
+
132
+ const total = (counts: unknown): number => Object.values((counts ?? {}) as Record<string, number>).reduce((sum, n) => sum + Number(n), 0);
133
+
134
+ /** The index of the first spec entry in the latest run of proposed-or-approved spec entries, -1 when there is no entry for it, or undefined when the spec is not proposed. */
135
+ function proposedSince(ctx: Ctx): number | undefined {
136
+ let start: number | undefined;
137
+ let seen = false;
138
+ ctx.entries.forEach((entry, index) => {
139
+ if (entry.stage !== "spec" || entry.auto !== true) return;
140
+ seen = true;
141
+ const live = entry.status === "proposed" || entry.status === "approved";
142
+ if (!live) start = undefined;
143
+ else if (start === undefined) start = index;
144
+ });
145
+ if (seen) return start;
146
+ const disk = ctx.folder ? artifactOnDisk(ctx.folder, "spec", ctx.cwd) : undefined;
147
+ return disk && (disk.status === "proposed" || disk.status === "approved") ? -1 : undefined;
148
+ }
149
+
150
+ function evaluate(name: string, ctx: Ctx): Status {
151
+ const { entries } = ctx;
152
+ const from = floor(entries, ctx.seq, name);
153
+ const skip = skipEntry(entries, from, name);
154
+ if (skip) return { name, state: "skipped", note: String(skip.reason ?? skip.skip ?? "") };
155
+ switch (name) {
156
+ case "decide": {
157
+ const decision = artifact(ctx, "decision", from);
158
+ return total(decision?.counts) > 0 ? done(name) : missing(name, ctx.folder ? `${ctx.folder}/decision.md with decision rows` : "decision.md with decision rows (no spec folder: name it in `intent` by calling `kind` again)");
159
+ }
160
+ case "grill": {
161
+ const decision = artifact(ctx, "decision", from);
162
+ if (!decision || total(decision.counts) === 0) return missing(name, "decision rows to grill");
163
+ const open = Number((decision.counts as Record<string, number>).open ?? 0);
164
+ if (open > 0) return missing(name, `${open} open decision row${open === 1 ? "" : "s"}`);
165
+ const first = entries.find((entry) => entry.stage === "decision" && entry.auto === true);
166
+ return first && Number((first.counts as Record<string, number>).open ?? 0) === 0 ? { name, state: "skipped", note: "no open rows", auto: true } : done(name);
167
+ }
168
+ case "spec": {
169
+ const spec = artifact(ctx, "spec", from);
170
+ return spec?.status === "proposed" || spec?.status === "approved" ? done(name) : missing(name, "spec.md with Status: proposed");
171
+ }
172
+ case "read": {
173
+ const start = proposedSince(ctx);
174
+ if (start === undefined) return missing(name, "a proposed spec to read");
175
+ const index = lastWhere(entries, Math.max(from, start + 1), (entry) => entry.stage === "goldfish" && entry.ok !== false);
176
+ if (index >= 0) return done(name, String(entries[index].source ?? ""));
177
+ const wrote = entries.some((entry) => entry.stage === "spec" && entry.auto === true);
178
+ const specs = entries.filter((entry) => entry.stage === "spec" && entry.auto === true);
179
+ if (specs.length > 0 && start >= 0 && entries[start].status === "approved" && !entries.slice(0, start).some((entry) => entry.stage === "spec" && entry.status === "proposed")) return done(name, "spec approved");
180
+ if (!wrote && from === 0 && artifact(ctx, "spec", 0)?.status === "approved") return done(name, "spec approved before this loop");
181
+ return missing(name, "a fresh-context read of the spec (dispatch a subagent, or `anchorloop_stage fresh-read`)");
182
+ }
183
+ case "approve": {
184
+ const spec = artifact(ctx, "spec", from);
185
+ return spec?.status === "approved" && spec.approvedFrom === true ? done(name) : missing(name, `spec.md Status: approved with Approved from${spec?.status ? ` (now ${spec.status})` : ""}`);
186
+ }
187
+ case "design": {
188
+ const design = artifact(ctx, "design", from);
189
+ return design?.approach === true ? done(name) : missing(name, "design.md with its approach section filled");
190
+ }
191
+ case "anchor": {
192
+ const index = lastWhere(entries, from, (entry) => entry.stage === "anchor");
193
+ return index >= 0 ? done(name, String(entries[index].check ?? "")) : missing(name, "a `Reality anchor:` line, or `anchor` on the kind call");
194
+ }
195
+ case "act":
196
+ return lastWhere(entries, from, (entry) => entry.stage === "edit") >= 0 ? done(name) : missing(name, "a change to the code");
197
+ case "check":
198
+ case "verify": {
199
+ const index = lastWhere(entries, Math.max(from, ctx.lastAct + 1), (entry) => entry.stage === "verify" || entry.stage === "check");
200
+ if (index < 0) {
201
+ const command = anchorCommand(entries);
202
+ return missing(name, command ? `a passing run of \`${command}\` after the last change` : "a verify declaration or a run of the anchor");
203
+ }
204
+ const entry = entries[index];
205
+ if (entry.declared === true && entry.result === "fail") return missing(name, "the declared verify failed");
206
+ if (entry.declared !== true && entry.exit !== 0) return missing(name, `the anchor exited ${entry.exit ?? "unknown"} after the last change`);
207
+ if (ctx.seq.includes("decide") && !artifact(ctx, "verification", from)) return missing(name, `${ctx.folder ?? "docs/specs/<folder>"}/verification.md (a durable loop's verify also needs it)`);
208
+ return done(name, entry.declared === true ? "declared" : "");
209
+ }
210
+ case "review": {
211
+ const index = lastWhere(entries, Math.max(from, ctx.lastAct + 1), (entry) => entry.stage === "review");
212
+ if (index < 0) return missing(name, "a review round after the last change with Blocking: P0=0 P1=0");
213
+ const round = entries[index];
214
+ if (round.blocking === false) return done(name);
215
+ const rounds = entries.filter((entry) => entry.stage === "review");
216
+ const stalled = rounds.length >= 2 && rounds.at(-1)?.blocking === true && rounds.at(-2)?.blocking === true;
217
+ return missing(name, stalled ? "two consecutive review rounds still blocking: hand the trend to the user" : round.blocking === true ? "blocking review findings are open" : "the review verdict is unknown");
218
+ }
219
+ case "reflect": {
220
+ const items = allFriction(entries, ctx.env);
221
+ if (items.length === 0) return { name, state: "skipped", note: "no friction", auto: true };
222
+ const open = items.filter((item) => !item.reflected).length;
223
+ return open === 0 ? done(name) : missing(name, `${open} friction entr${open === 1 ? "y" : "ies"} without a reflect`);
224
+ }
225
+ default:
226
+ return missing(name, name);
227
+ }
228
+ }
229
+
230
+ /** The phase state of the loop kind recorded in the book. */
231
+ export function computePhases(book: Pick<Book, "entries">, inputs: Omit<Inputs, "entries">): Phases {
232
+ const kind = currentKind(book.entries);
233
+ const seq = SEQUENCES[kind ?? "inline"];
234
+ const entries = withChildActs(book.entries);
235
+ const ctx: Ctx = { ...inputs, entries, seq, folder: specFolder(entries, inputs.todos, inputs.cwd), lastAct: lastActIndex(entries) };
236
+ const statuses = seq.filter((name) => name !== "close").map((name) => evaluate(name, ctx));
237
+ let prefix = 0;
238
+ while (prefix < statuses.length && statuses[prefix].state !== "missing") prefix++;
239
+ const first = statuses[prefix];
240
+ const reached = statuses.slice(0, prefix).filter((status) => status.state === "done").at(-1);
241
+ let next = first ? first.name : "close";
242
+ let note = first ? first.note : "";
243
+ const last = ctx.lastAct;
244
+ const verify = entries.map((entry, index) => ({ entry, index })).filter(({ entry, index }) => index > last && (entry.stage === "verify" || entry.stage === "check")).at(-1)?.entry;
245
+ const review = entries.map((entry, index) => ({ entry, index })).filter(({ entry, index }) => index > last && entry.stage === "review").at(-1)?.entry;
246
+ if ((next === "verify" || next === "check") && last >= 0 && verify && verify.exit !== 0 && verify.declared !== true) next = "fix";
247
+ else if (next === "review" && last >= 0 && review?.blocking === true) next = "fix";
248
+ if (next === "fix") note = first?.note ?? "";
249
+ const skipped = statuses.filter((status) => status.state === "skipped" && status.note !== "no friction").map((status) => `${status.name} (${status.note})`);
250
+ const backs = book.entries.filter((entry) => entry.stage === "back").map((entry) => `${entry.phase} (${entry.reason})`);
251
+ const hints = retryWithoutFriction(book.entries) ? ["friction: a failed run was retried and passed, with no friction entry after it"] : [];
252
+ return {
253
+ kind: kind ?? "undeclared",
254
+ statuses,
255
+ current: reached?.name ?? "start",
256
+ next,
257
+ missing: note,
258
+ skipped,
259
+ backs,
260
+ hints,
261
+ };
262
+ }
263
+
264
+ /** The statuses of phases before `entering` that are incomplete. */
265
+ export function incompleteBefore(phases: Phases, entering: string): Status[] {
266
+ const order = phases.statuses.map((status) => status.name);
267
+ const at = entering === "close" ? order.length : order.findIndex((name) => same(name, entering));
268
+ return phases.statuses.filter((status, index) => index < at && status.state === "missing");
269
+ }
270
+
271
+ /** One line: the phase entered, what is missing before it, and the skip escape. */
272
+ export function jumpLine(entering: string, missingBefore: Status[]): string {
273
+ const list = missingBefore.map((status) => `${status.name} (${status.note})`).join("; ");
274
+ const skip = missingBefore[0]?.name ?? "<phase>";
275
+ return `anchorloop: ${entering} comes before ${list}; skip with \`anchorloop_stage skip ${skip} <reason>\`.`;
276
+ }
277
+
278
+ /** The phase line `read` and the close summary show. */
279
+ export function describe(phases: Phases): string {
280
+ const parts = [`${phases.kind} · phase ${phases.current}${phases.current === "start" ? "" : " ✓"} → ${phases.next}${phases.missing ? ` (missing: ${phases.missing})` : ""}`];
281
+ if (phases.skipped.length) parts.push(`skipped: ${phases.skipped.join(", ")}`);
282
+ if (phases.backs.length) parts.push(`back: ${phases.backs.join(", ")}`);
283
+ return parts.join(" · ");
284
+ }
285
+
286
+ /** Remembers which incomplete phases a jump already warned about; a regression adds one and warns again. */
287
+ export class JumpMemory {
288
+ private warned = new Map<string, Set<string>>();
289
+
290
+ /** True when `missingNames` holds a phase not yet warned about for this book and entering phase. */
291
+ fresh(bookId: string, entering: string, missingNames: string[]): boolean {
292
+ const key = `${bookId}|${entering}`;
293
+ const before = this.warned.get(key) ?? new Set<string>();
294
+ const news = missingNames.some((name) => !before.has(name));
295
+ if (missingNames.length === 0) this.warned.delete(key);
296
+ else this.warned.set(key, new Set(missingNames));
297
+ return news;
298
+ }
299
+ }
300
+
301
+ export { lastIndex };
@@ -0,0 +1,71 @@
1
+ /** The loop state Jev judges, read from the book, the todo list, and the phase machine. */
2
+
3
+ import {
4
+ type Book,
5
+ allFriction,
6
+ childLines,
7
+ currentIntent,
8
+ currentKind,
9
+ diffAgainstBases,
10
+ diffText,
11
+ isOpenTask,
12
+ latestAnchor,
13
+ replayTodos,
14
+ reviewClearAfterLastAct,
15
+ reviewTrend,
16
+ stagesDone,
17
+ TASK_LEGEND,
18
+ taskLines,
19
+ verifyAfterLastAct,
20
+ } from "./book.ts";
21
+ import { computePhases } from "./phases.ts";
22
+
23
+ export type Snapshot = {
24
+ state: Record<string, string>;
25
+ /** The program's next phase and what it lacks, in a few words. */
26
+ next: string;
27
+ missing: string;
28
+ /** Advisory hints that are not phases, such as an unrecorded friction. */
29
+ hints: string[];
30
+ /** The last phase the book shows complete. */
31
+ current: string;
32
+ /** A review recorded after the last change with no blocking finding on it. */
33
+ reviewClear: boolean;
34
+ };
35
+
36
+ export function snapshot(book: Book, ctx: { cwd: string; sessionManager: { getBranch(): unknown[] } }): Snapshot {
37
+ const { entries } = book;
38
+ const todos = replayTodos(ctx.sessionManager.getBranch());
39
+ const open = todos.filter(isOpenTask).length;
40
+ const phases = computePhases(book, { cwd: ctx.cwd, todos });
41
+ const check = verifyAfterLastAct(entries);
42
+ const verify = [
43
+ `stages: ${stagesDone(entries).join(", ") || "none"}`,
44
+ `last check: ${check ? `${check.declared ? `declared ${check.result}` : `exit ${check.exit ?? "unknown"}`}${check.last ? ` — ${check.last}` : ""}` : "none after the last change"}`,
45
+ `reviews: ${reviewTrend(entries)}`,
46
+ ...childLines(entries).map((line) => `child ${line}`),
47
+ ].join("; ");
48
+ const items = allFriction(entries);
49
+ const friction = [
50
+ ...(items.length ? [`${items.length} recorded, ${items.filter((item) => item.reflected).length} reflected`] : []),
51
+ ...items.map((item) => `${item.ref}: ${item.tried} → ${item.found} [${item.reflected ? `${item.class}${item.action ? `: ${item.action}` : ""}` : "unreflected"}]`),
52
+ ];
53
+ return {
54
+ state: {
55
+ kind: currentKind(entries) ?? "not declared",
56
+ intent: (currentIntent(entries) ?? "not declared").slice(0, 500),
57
+ anchor: (latestAnchor(entries) ?? "none").slice(0, 500),
58
+ diff: diffText(diffAgainstBases(entries)),
59
+ phase: `current: ${phases.current}; next: ${phases.next}${phases.missing ? `; missing: ${phases.missing}` : ""}${phases.skipped.length ? `; skipped: ${phases.skipped.join(", ")}` : ""}`.slice(0, 600),
60
+ verify: verify.slice(0, 1200),
61
+ todos: `${open} open of ${todos.length}`,
62
+ tasks: [`legend: ${TASK_LEGEND}`, ...taskLines(entries, todos, ctx.cwd)].join("\n").slice(0, 1500),
63
+ friction: (friction.join("\n") || "none").slice(0, 800),
64
+ },
65
+ next: phases.next,
66
+ missing: phases.missing,
67
+ hints: phases.hints,
68
+ current: phases.current,
69
+ reviewClear: reviewClearAfterLastAct(entries),
70
+ };
71
+ }
@@ -0,0 +1,5 @@
1
+ // pi-herdr-agents marks its children with PI_SUBAGENT_ID, pi-subagents with PI_SUBAGENT_CHILD,
2
+ // and avtc's pi-subagent with PI_SUBAGENT_CHILD_AGENT plus PI_SUBAGENT_PARENT_PID.
3
+ export function isSubagent(env: NodeJS.ProcessEnv = process.env): boolean {
4
+ return Boolean(env.PI_SUBAGENT_ID) || env.PI_SUBAGENT_CHILD === "1" || Boolean(env.PI_SUBAGENT_CHILD_AGENT && env.PI_SUBAGENT_PARENT_PID);
5
+ }
@@ -9,59 +9,45 @@ Every entry skill (`answering`, `investigating`, `inspecting`, `coding`, `writin
9
9
 
10
10
  ## Align
11
11
 
12
- Open with one working interpretation in your own words:
13
-
14
- Alignment: <the requested task and intended outcome>
15
-
16
- Pick the entry's playbook from its index; with none fitting, run the loop bare. For work longer than one increment, copy the playbook's steps verbatim into `todo`, keeping each step you will not run as `skip: <reason>`, so the plan shows what was left out and why.
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 a loop. Declare with `anchorloop_stage` only what the extension cannot observe: `kind` (`quick`, `inline`, `reviewed`, `durable`; `intent`, the task and its outcome in one line; `anchor`, the check command), `verify` (a check not run in bash), `fresh-read`, `friction`, `reflect`, `skip` or `back` (phase, reason), `close`, and the query `read`. Edits, anchor runs, reviews, and spec files are read from evidence; a phase that starts before an earlier one is complete warns once, and `close` refuses only while an open todo lacks `metadata.followup` (a ticket id, `memory`, or `drop — <reason>`).
17
13
 
18
14
  ## Anchor
19
15
 
20
- Before the first change, state:
21
-
22
- Reality anchor: <the exact check that observes the result, and its checkpoint>
16
+ Before the first change the loop has an anchor: `anchor` on the `kind` call, or a `Reality anchor: <the exact check that observes the result, and its checkpoint>` line.
23
17
 
24
- The anchorloop extension holds `write` and `edit` outside `scratch/` and `docs/specs/` until this line appears after the user's latest message.
25
-
26
- Size the anchor to the change's actual reach and pick one method: an executable check, a user operation, human judgment, or a focused review. An anchor assigned by a coordinator governs. Pick a review anchor when the change touches a shared contract, authorization or permission data, a migration, or security, or when the Jev second thought rates its review `full`; 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, and when two consecutive rounds still raise new blocking findings the run stops and hands the trend to the user, because a fresh exploratory review each round finds new edge cases instead of converging. The checkpoint is normally once, after the last increment.
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.
27
19
 
28
20
  ## Act
29
21
 
30
22
  Work in the smallest useful increments through the project's native practices, deleting before adding (**Subtract before you add**).
31
23
 
32
- Give follow-up work (a fix round, a retry, the next item) to a fresh subagent carrying the original brief, every later directive, and the prior report and branch, because a resumed chain silently drops directives; resume an existing subagent only for state it alone holds, such as uncommitted changes or a running process. Write briefs, follow-ups, and messages to agents by [agent-terse](../agent-terse/SKILL.md); read each subagent's output and write its summary yourself.
24
+ Make a small fix you hold full context for yourself; give larger follow-up work to a fresh subagent carrying the original brief, every later directive, and the prior report and branch, because a resumed chain silently drops directives. Resume a subagent only for state it alone holds, such as uncommitted changes or a running process. Write briefs, follow-ups, and messages to agents by [agent-terse](../agent-terse/SKILL.md); read each subagent's output and write its summary yourself.
33
25
 
34
- Report problems outside the contract as follow-ups rather than fixing them in this change, because each extra fix reopens review on code the round never meant to touch.
26
+ Report problems outside the contract as follow-ups, because each extra fix reopens review on code the round never touched.
35
27
 
36
- The reply names a [principle](PRINCIPLES.md) only when it changed a decision, together with that decision.
28
+ The reply names a [principle](PRINCIPLES.md) only with the decision it changed.
37
29
 
38
30
  ### Friction Notes
39
31
 
40
- Each time an attempt fails and a later attempt reveals why, append a note before continuing:
41
-
42
- - Tried: <the attempt that failed>
43
- Found: <what the failure revealed>
44
- Led by: <the skill or instruction line that prompted the attempt, or `none`>
45
-
46
- Keep them in `scratch/friction.md` unless the entry names another home.
32
+ Each time an attempt fails and a later attempt reveals why, record it with `anchorloop_stage friction` before continuing.
47
33
 
48
34
  ### Decision Trail
49
35
 
50
- A run the user reviews after stepping away records one row per fork, completed unit, pivot, or blocker in `scratch/decisions-<task-slug>.tsv` with the `anchorloop_decision` tool. 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 actually did and build the report from the trail.
36
+ A run the user reviews after stepping away records one row per fork, completed unit, pivot, or blocker in `scratch/decisions-<task-slug>.tsv` with the `anchorloop_decision` tool. 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.
51
37
 
52
38
  ## Verify
53
39
 
54
- Exercise the anchor and report what it observed. A requirement the anchor did not exercise is `unknown`, whatever unrelated checks show. Report local tests, builds, interactive runs, commit, push, CI, and deployment as separate claims; one never stands in for another. A criterion that needs human judgment is a human checkpoint: propose a `human-feedback` pass when it is still `unknown`, and record its verdict apart from executable or review evidence.
40
+ Exercise the anchor and report what it observed. A requirement the anchor did not exercise is `unknown`, whatever other checks show. Report local tests, builds, interactive runs, commit, push, CI, and deployment as separate claims; one never stands in for another. A criterion that needs human judgment is a human checkpoint: propose a `human-feedback` pass when it is still `unknown`, and record its verdict apart from executable or review evidence.
55
41
 
56
- 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 own review, green tests, and CI each miss what an independent reader catches.
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.
57
43
 
58
- Write the reply the playbook's `Reply:` line names, with the anchor's evidence. A Jev second thought the extension may add after a changing run is advisory: recheck the evidence it questions, then decide. When a calling workflow supplies an artifact, write the results into it instead of a separate report or plan.
44
+ Write the reply the playbook's `Reply:` line names, with the anchor's evidence. The Jev second thought reports the next phase and is advisory: 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.
59
45
 
60
46
  ### Reflexive Pass
61
47
 
62
- Run only when friction notes exist, and say nothing about it otherwise. Classify each note: **misdirection** when the `Led by` line states or implies something `Found` contradicts; **gap** when no instruction in use states the `Found` fact. A gap the change itself resolved, or a tool slip no instruction owns, is recorded as resolved. Invoke `solid-loop` with the rest.
48
+ Run only when the book holds friction (`read` lists it); otherwise say nothing. Classify each entry with `reflect`: **misdirection** when the `led_by` line states or implies something `found` contradicts; **gap** when no instruction in use states the `found` fact. A gap the change itself resolved, or a tool slip no instruction owns, is `resolved`. Invoke `solid-loop` with the rest.
63
49
 
64
- **Complete when:** every requirement has evidence from its anchor or is reported `unknown`, and every friction note is applied through `solid-loop` or recorded as resolved.
50
+ **Complete when:** every requirement has evidence from its anchor or is reported `unknown`, and every friction entry is applied through `solid-loop` or reflected as resolved.
65
51
 
66
52
  ## Playbooks
67
53
 
@@ -69,7 +55,7 @@ Run only when friction notes exist, and say nothing about it otherwise. Classify
69
55
  - [pause](playbooks/pause.md): an explicit pause, going offline, a restart, or imminent compaction.
70
56
  - [pickup](playbooks/pickup.md): resuming or taking over a prior run's work.
71
57
 
72
- Lifecycle playbooks wrap the entry playbook that does the work.
58
+ Lifecycle playbooks wrap the entry playbook.
73
59
 
74
60
  ---
75
61
 
@@ -36,7 +36,7 @@ Approved from: <the user reply that approved it, empty while proposed>
36
36
 
37
37
  Choose the smallest approach that fits the approved spec and architecture. Follow the matching playbook's design steps ([feature](playbooks/feature.md) for interfaces and seams, [prototype](playbooks/prototype.md) when a named question is cheaper to settle by experiment) and the project's exact skill.
38
38
 
39
- The file holds the chosen approach; interfaces and data flow affected; existing precedent; decisions, trade-offs, and risks; the method selected inside the reality anchor. Friction notes recorded during Act go under `## Friction Notes`.
39
+ The file holds the chosen approach; interfaces and data flow affected; existing precedent; decisions, trade-offs, and risks; the method selected inside the reality anchor.
40
40
 
41
41
  **Complete when:** implementation invents no product behavior or architecture.
42
42
 
@@ -54,7 +54,7 @@ Review the diff against project standards, the approved spec, and the confirmed
54
54
  - **fail**: The anchor observed a deviation from the spec or an error.
55
55
  - **unknown**: No anchor exercised the requirement in this run.
56
56
 
57
- Then human appropriateness verdicts; deviations from the approved spec or design; unresolved verification gaps and their impact. When friction notes exist, include a Reflexive section listing each with its classification (`misdirection` or `gap`) and its action.
57
+ Then human appropriateness verdicts; deviations from the approved spec or design; unresolved verification gaps and their impact. When the loop book holds friction, include a Reflexive section listing each entry with its class (`misdirection`, `gap`, or `resolved`) and its action.
58
58
 
59
59
  ## Updates
60
60
 
@@ -11,8 +11,8 @@ Run the loop in [../anchorloop/SKILL.md](../anchorloop/SKILL.md) with one of the
11
11
 
12
12
  Before the first production edit, declare the change **Inline** or **Durable**:
13
13
 
14
- - **Inline:** requirements and approach are clear, impact is bounded, and implementation and verification fit this run; a lasting behavior or public contract can still be Inline when the change is simple. Follow Alignment with `Inline — Contract: <one sentence of the observable behavior after the change>`; the user's source-change request approves it, and approval already given is not asked again.
15
- - **Durable:** the work needs a persistent design record because of material ambiguity, consequential trade-offs, cross-component coordination, migration risk, continuity across sessions, 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.
14
+ - **Inline:** requirements and approach are clear, impact is bounded, and implementation and verification fit this run; a lasting behavior or public contract can still be Inline when the change is simple. The `intent` on the `kind` call states the observable behavior after the change; the user's source-change request approves it, and approval already given is not asked again.
15
+ - **Durable:** the work needs a persistent design record because of material ambiguity, consequential trade-offs, 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.
16
16
 
17
17
  ## Anchor
18
18
 
@@ -4,7 +4,7 @@
4
4
 
5
5
  1. Align: learn how the affected subsystem works now, and name the data shape and its organizing structure (**Model the domain**, **Foundational thinking**) before any logic.
6
6
  2. Act: when the change crosses a module, settle its interface first. Place the seam at an existing boundary; add one only for real variation, because one adapter is hypothetical and two demonstrate variation. Hide invariants and ordering behind the interface, and make it the test surface. A module whose removal makes complexity vanish rather than move to callers is a pass-through; delete it. With no precedent and a costly-to-reverse choice, compare two interface shapes on depth, locality, compatibility, and testability, or settle it with the `prototype` playbook; a user-owned trade-off goes to the user.
7
- 3. Act: for more than one unit, write the throughput checkpoint as four `todo` items, each `n/a: <reason>` when it does not apply: blocking first steps; independent workstreams; shared mutable state, split before serializing; the smallest safe decomposition.
7
+ 3. Act: when dispatching parallel workers, write the throughput checkpoint as four `todo` items, each `n/a: <reason>` when it does not apply: blocking first steps; independent workstreams; shared mutable state, split before serializing; the smallest safe decomposition.
8
8
  4. Act: build in units that each end in a check (**Sequence verifiable units**).
9
9
  5. Verify: on the surface the user will use; an inconclusive or wrong-surface result is not a pass.
10
10