pi-anchorloop 2.1.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -3
- package/README.zh-TW.md +4 -3
- package/package.json +3 -2
- package/pi/anchor-gate.ts +21 -77
- package/pi/book.ts +500 -0
- package/pi/second-thought.ts +203 -172
- package/pi/support/book.ts +698 -0
- package/pi/support/jev.ts +28 -0
- package/pi/support/phases.ts +290 -0
- package/pi/support/subagent.ts +5 -0
- package/skills/anchorloop/SKILL.md +14 -28
- package/skills/anchorloop/playbooks/autonomous-run.md +1 -1
- package/skills/coding/MINI-SDD.md +6 -6
- package/skills/coding/SKILL.md +2 -2
- package/skills/coding/playbooks/feature.md +1 -1
|
@@ -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,290 @@
|
|
|
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
|
+
retryWithoutFriction,
|
|
23
|
+
specFolder,
|
|
24
|
+
} from "./book.ts";
|
|
25
|
+
|
|
26
|
+
export const SEQUENCES: Record<Kind, readonly string[]> = {
|
|
27
|
+
quick: ["act", "check", "reflect", "close"],
|
|
28
|
+
inline: ["anchor", "act", "verify", "reflect", "close"],
|
|
29
|
+
reviewed: ["anchor", "act", "verify", "review", "reflect", "close"],
|
|
30
|
+
durable: ["decide", "grill", "spec", "read", "approve", "design", "anchor", "act", "verify", "review", "reflect", "close"],
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type Status = { name: string; state: "done" | "skipped" | "missing"; note: string };
|
|
34
|
+
|
|
35
|
+
export type Phases = {
|
|
36
|
+
kind: Kind | "undeclared";
|
|
37
|
+
statuses: Status[];
|
|
38
|
+
/** The last phase of the complete prefix, or `start`. */
|
|
39
|
+
current: string;
|
|
40
|
+
/** The first incomplete phase, `fix` when a check or review failed after the last change, else `close`. */
|
|
41
|
+
next: string;
|
|
42
|
+
missing: string;
|
|
43
|
+
skipped: string[];
|
|
44
|
+
backs: string[];
|
|
45
|
+
/** Advisory hints that are not phases. */
|
|
46
|
+
hints: string[];
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export type Inputs = { entries: Entry[]; cwd: string; todos: Task[]; env?: NodeJS.ProcessEnv };
|
|
50
|
+
|
|
51
|
+
// --- paths ---------------------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
const EXEMPT_IN_PROJECT = ["scratch", "docs/specs"];
|
|
54
|
+
const EXEMPT_ROOTS = [tmpdir(), "/tmp", "/var/folders"];
|
|
55
|
+
const EXEMPT_EXTENSIONS = [".md", ".txt", ".html", ".htm"];
|
|
56
|
+
|
|
57
|
+
/** The real path of `path`, resolving symlinks such as /var → /private/var through its nearest existing parent. */
|
|
58
|
+
function real(path: string): string {
|
|
59
|
+
if (existsSync(path)) return realpathSync(path);
|
|
60
|
+
const parent = dirname(path);
|
|
61
|
+
return parent === path ? path : join(real(parent), basename(path));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function within(root: string, target: string): boolean {
|
|
65
|
+
const rel = relative(root, target);
|
|
66
|
+
return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Scratch and spec folders in the project and temp files outside it; a project that lives in a temp dir is still counted. */
|
|
70
|
+
export function outsideWork(filePath: string, cwd: string): boolean {
|
|
71
|
+
const root = real(cwd);
|
|
72
|
+
const target = real(resolve(cwd, filePath));
|
|
73
|
+
if (within(root, target)) return EXEMPT_IN_PROJECT.some((dir) => within(join(root, dir), target));
|
|
74
|
+
return EXEMPT_ROOTS.some((tmp) => within(real(tmp), target));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Markdown, text, and HTML files and everything `outsideWork` names: an edit the anchor gate does not warn about. */
|
|
78
|
+
export function exempt(filePath: string, cwd: string): boolean {
|
|
79
|
+
return EXEMPT_EXTENSIONS.includes(extname(filePath).toLowerCase()) || outsideWork(filePath, cwd);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// --- evidence ------------------------------------------------------------------------------------
|
|
83
|
+
|
|
84
|
+
type Ctx = Inputs & { seq: readonly string[]; folder?: string; lastAct: number };
|
|
85
|
+
|
|
86
|
+
const done = (name: string, note = ""): Status => ({ name, state: "done", note });
|
|
87
|
+
const missing = (name: string, note: string): Status => ({ name, state: "missing", note });
|
|
88
|
+
|
|
89
|
+
function same(a: string, b: string): boolean {
|
|
90
|
+
const group = (name: string) => (name === "check" ? "verify" : name);
|
|
91
|
+
return group(a) === group(b);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Entries before this index do not count for `name`: the latest `back` to this phase or an earlier one. */
|
|
95
|
+
function floor(entries: Entry[], seq: readonly string[], name: string): number {
|
|
96
|
+
const at = seq.indexOf(name);
|
|
97
|
+
let from = 0;
|
|
98
|
+
entries.forEach((entry, index) => {
|
|
99
|
+
if (entry.stage !== "back" || typeof entry.phase !== "string") return;
|
|
100
|
+
const target = seq.findIndex((candidate) => same(candidate, entry.phase as string));
|
|
101
|
+
if (target >= 0 && at >= target) from = index + 1;
|
|
102
|
+
});
|
|
103
|
+
return from;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function lastWhere(entries: Entry[], from: number, test: (entry: Entry) => boolean): number {
|
|
107
|
+
for (let i = entries.length - 1; i >= from; i--) if (test(entries[i])) return i;
|
|
108
|
+
return -1;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function skipEntry(entries: Entry[], from: number, name: string): Entry | undefined {
|
|
112
|
+
const index = lastWhere(entries, from, (entry) => entry.stage === "skip" && typeof entry.phase === "string" && same(entry.phase, name));
|
|
113
|
+
return index >= 0 ? entries[index] : undefined;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** The latest recorded summary of an artifact since `from`; a loop that never wrote it falls back to the file on disk. */
|
|
117
|
+
function artifact(ctx: Ctx, name: string, from: number): Entry | undefined {
|
|
118
|
+
const index = lastWhere(ctx.entries, from, (entry) => entry.stage === name && entry.auto === true);
|
|
119
|
+
if (index >= 0) return ctx.entries[index];
|
|
120
|
+
const wrote = ctx.entries.some((entry) => entry.stage === name && entry.auto === true);
|
|
121
|
+
return from === 0 && !wrote && ctx.folder ? artifactOnDisk(ctx.folder, name, ctx.cwd) : undefined;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const total = (counts: unknown): number => Object.values((counts ?? {}) as Record<string, number>).reduce((sum, n) => sum + Number(n), 0);
|
|
125
|
+
|
|
126
|
+
/** 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. */
|
|
127
|
+
function proposedSince(ctx: Ctx): number | undefined {
|
|
128
|
+
let start: number | undefined;
|
|
129
|
+
let seen = false;
|
|
130
|
+
ctx.entries.forEach((entry, index) => {
|
|
131
|
+
if (entry.stage !== "spec" || entry.auto !== true) return;
|
|
132
|
+
seen = true;
|
|
133
|
+
const live = entry.status === "proposed" || entry.status === "approved";
|
|
134
|
+
if (!live) start = undefined;
|
|
135
|
+
else if (start === undefined) start = index;
|
|
136
|
+
});
|
|
137
|
+
if (seen) return start;
|
|
138
|
+
const disk = ctx.folder ? artifactOnDisk(ctx.folder, "spec", ctx.cwd) : undefined;
|
|
139
|
+
return disk && (disk.status === "proposed" || disk.status === "approved") ? -1 : undefined;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function evaluate(name: string, ctx: Ctx): Status {
|
|
143
|
+
const { entries } = ctx;
|
|
144
|
+
const from = floor(entries, ctx.seq, name);
|
|
145
|
+
const skip = skipEntry(entries, from, name);
|
|
146
|
+
if (skip) return { name, state: "skipped", note: String(skip.reason ?? skip.skip ?? "") };
|
|
147
|
+
switch (name) {
|
|
148
|
+
case "decide": {
|
|
149
|
+
const decision = artifact(ctx, "decision", from);
|
|
150
|
+
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)");
|
|
151
|
+
}
|
|
152
|
+
case "grill": {
|
|
153
|
+
const decision = artifact(ctx, "decision", from);
|
|
154
|
+
if (!decision || total(decision.counts) === 0) return missing(name, "decision rows to grill");
|
|
155
|
+
const open = Number((decision.counts as Record<string, number>).open ?? 0);
|
|
156
|
+
if (open > 0) return missing(name, `${open} open decision row${open === 1 ? "" : "s"}`);
|
|
157
|
+
const first = entries.find((entry) => entry.stage === "decision" && entry.auto === true);
|
|
158
|
+
return first && Number((first.counts as Record<string, number>).open ?? 0) === 0 ? { name, state: "skipped", note: "no open rows" } : done(name);
|
|
159
|
+
}
|
|
160
|
+
case "spec": {
|
|
161
|
+
const spec = artifact(ctx, "spec", from);
|
|
162
|
+
return spec?.status === "proposed" || spec?.status === "approved" ? done(name) : missing(name, "spec.md with Status: proposed");
|
|
163
|
+
}
|
|
164
|
+
case "read": {
|
|
165
|
+
const start = proposedSince(ctx);
|
|
166
|
+
if (start === undefined) return missing(name, "a proposed spec to read");
|
|
167
|
+
const index = lastWhere(entries, Math.max(from, start + 1), (entry) => entry.stage === "goldfish" && entry.ok !== false);
|
|
168
|
+
if (index >= 0) return done(name, String(entries[index].source ?? ""));
|
|
169
|
+
const wrote = entries.some((entry) => entry.stage === "spec" && entry.auto === true);
|
|
170
|
+
const specs = entries.filter((entry) => entry.stage === "spec" && entry.auto === true);
|
|
171
|
+
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");
|
|
172
|
+
if (!wrote && from === 0 && artifact(ctx, "spec", 0)?.status === "approved") return done(name, "spec approved before this loop");
|
|
173
|
+
return missing(name, "a fresh-context read of the spec (dispatch a subagent, or `anchorloop_stage fresh-read`)");
|
|
174
|
+
}
|
|
175
|
+
case "approve": {
|
|
176
|
+
const spec = artifact(ctx, "spec", from);
|
|
177
|
+
return spec?.status === "approved" && spec.approvedFrom === true ? done(name) : missing(name, `spec.md Status: approved with Approved from${spec?.status ? ` (now ${spec.status})` : ""}`);
|
|
178
|
+
}
|
|
179
|
+
case "design": {
|
|
180
|
+
const design = artifact(ctx, "design", from);
|
|
181
|
+
return design?.approach === true ? done(name) : missing(name, "design.md with its approach section filled");
|
|
182
|
+
}
|
|
183
|
+
case "anchor": {
|
|
184
|
+
const index = lastWhere(entries, from, (entry) => entry.stage === "anchor");
|
|
185
|
+
return index >= 0 ? done(name, String(entries[index].check ?? "")) : missing(name, "a `Reality anchor:` line, or `anchor` on the kind call");
|
|
186
|
+
}
|
|
187
|
+
case "act":
|
|
188
|
+
return lastWhere(entries, from, (entry) => entry.stage === "edit") >= 0 ? done(name) : missing(name, "a change to the code");
|
|
189
|
+
case "check":
|
|
190
|
+
case "verify": {
|
|
191
|
+
const index = lastWhere(entries, Math.max(from, ctx.lastAct + 1), (entry) => entry.stage === "verify" || entry.stage === "check");
|
|
192
|
+
if (index < 0) {
|
|
193
|
+
const command = anchorCommand(entries);
|
|
194
|
+
return missing(name, command ? `a passing run of \`${command}\` after the last change` : "a verify declaration or a run of the anchor");
|
|
195
|
+
}
|
|
196
|
+
const entry = entries[index];
|
|
197
|
+
if (entry.declared === true) return entry.result === "fail" ? missing(name, "the declared verify failed") : done(name, "declared");
|
|
198
|
+
return entry.exit === 0 ? done(name) : missing(name, `the anchor exited ${entry.exit ?? "unknown"} after the last change`);
|
|
199
|
+
}
|
|
200
|
+
case "review": {
|
|
201
|
+
const index = lastWhere(entries, Math.max(from, ctx.lastAct + 1), (entry) => entry.stage === "review");
|
|
202
|
+
if (index < 0) return missing(name, "a review round after the last change with Blocking: P0=0 P1=0");
|
|
203
|
+
const round = entries[index];
|
|
204
|
+
if (round.blocking === false) return done(name);
|
|
205
|
+
const rounds = entries.filter((entry) => entry.stage === "review");
|
|
206
|
+
const stalled = rounds.length >= 2 && rounds.at(-1)?.blocking === true && rounds.at(-2)?.blocking === true;
|
|
207
|
+
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");
|
|
208
|
+
}
|
|
209
|
+
case "reflect": {
|
|
210
|
+
const items = allFriction(entries, ctx.env);
|
|
211
|
+
if (items.length === 0) return { name, state: "skipped", note: "no friction" };
|
|
212
|
+
const open = items.filter((item) => !item.reflected).length;
|
|
213
|
+
return open === 0 ? done(name) : missing(name, `${open} friction entr${open === 1 ? "y" : "ies"} without a reflect`);
|
|
214
|
+
}
|
|
215
|
+
default:
|
|
216
|
+
return missing(name, name);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** The phase state of the loop kind recorded in the book. */
|
|
221
|
+
export function computePhases(book: Pick<Book, "entries">, inputs: Omit<Inputs, "entries">): Phases {
|
|
222
|
+
const kind = currentKind(book.entries);
|
|
223
|
+
const seq = SEQUENCES[kind ?? "inline"];
|
|
224
|
+
const ctx: Ctx = { ...inputs, entries: book.entries, seq, folder: specFolder(book.entries, inputs.todos, inputs.cwd), lastAct: lastActIndex(book.entries) };
|
|
225
|
+
const statuses = seq.filter((name) => name !== "close").map((name) => evaluate(name, ctx));
|
|
226
|
+
let prefix = 0;
|
|
227
|
+
while (prefix < statuses.length && statuses[prefix].state !== "missing") prefix++;
|
|
228
|
+
const first = statuses[prefix];
|
|
229
|
+
const reached = statuses.slice(0, prefix).filter((status) => status.state === "done").at(-1);
|
|
230
|
+
let next = first ? first.name : "close";
|
|
231
|
+
let note = first ? first.note : "";
|
|
232
|
+
const last = ctx.lastAct;
|
|
233
|
+
const verify = book.entries.map((entry, index) => ({ entry, index })).filter(({ entry, index }) => index > last && (entry.stage === "verify" || entry.stage === "check")).at(-1)?.entry;
|
|
234
|
+
const review = book.entries.map((entry, index) => ({ entry, index })).filter(({ entry, index }) => index > last && entry.stage === "review").at(-1)?.entry;
|
|
235
|
+
if ((next === "verify" || next === "check") && last >= 0 && verify && verify.exit !== 0 && verify.declared !== true) next = "fix";
|
|
236
|
+
else if (next === "review" && last >= 0 && review?.blocking === true) next = "fix";
|
|
237
|
+
if (next === "fix") note = first?.note ?? "";
|
|
238
|
+
const skipped = statuses.filter((status) => status.state === "skipped" && status.note !== "no friction").map((status) => `${status.name} (${status.note})`);
|
|
239
|
+
const backs = book.entries.filter((entry) => entry.stage === "back").map((entry) => `${entry.phase} (${entry.reason})`);
|
|
240
|
+
const hints = retryWithoutFriction(book.entries) ? ["friction: a failed run was retried and passed, with no friction entry after it"] : [];
|
|
241
|
+
return {
|
|
242
|
+
kind: kind ?? "undeclared",
|
|
243
|
+
statuses,
|
|
244
|
+
current: reached?.name ?? "start",
|
|
245
|
+
next,
|
|
246
|
+
missing: note,
|
|
247
|
+
skipped,
|
|
248
|
+
backs,
|
|
249
|
+
hints,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** The statuses of phases before `entering` that are incomplete. */
|
|
254
|
+
export function incompleteBefore(phases: Phases, entering: string): Status[] {
|
|
255
|
+
const order = phases.statuses.map((status) => status.name);
|
|
256
|
+
const at = entering === "close" ? order.length : order.findIndex((name) => same(name, entering));
|
|
257
|
+
return phases.statuses.filter((status, index) => index < at && status.state === "missing");
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** One line: the phase entered, what is missing before it, and the skip escape. */
|
|
261
|
+
export function jumpLine(entering: string, missingBefore: Status[]): string {
|
|
262
|
+
const list = missingBefore.map((status) => `${status.name} (${status.note})`).join("; ");
|
|
263
|
+
const skip = missingBefore[0]?.name ?? "<phase>";
|
|
264
|
+
return `anchorloop: ${entering} comes before ${list}; skip with \`anchorloop_stage skip ${skip} <reason>\`.`;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** The phase line `read` and the close summary show. */
|
|
268
|
+
export function describe(phases: Phases): string {
|
|
269
|
+
const parts = [`${phases.kind} · phase ${phases.current}${phases.current === "start" ? "" : " ✓"} → ${phases.next}${phases.missing ? ` (missing: ${phases.missing})` : ""}`];
|
|
270
|
+
if (phases.skipped.length) parts.push(`skipped: ${phases.skipped.join(", ")}`);
|
|
271
|
+
if (phases.backs.length) parts.push(`back: ${phases.backs.join(", ")}`);
|
|
272
|
+
return parts.join(" · ");
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** Remembers which incomplete phases a jump already warned about; a regression adds one and warns again. */
|
|
276
|
+
export class JumpMemory {
|
|
277
|
+
private warned = new Map<string, Set<string>>();
|
|
278
|
+
|
|
279
|
+
/** True when `missingNames` holds a phase not yet warned about for this book and entering phase. */
|
|
280
|
+
fresh(bookId: string, entering: string, missingNames: string[]): boolean {
|
|
281
|
+
const key = `${bookId}|${entering}`;
|
|
282
|
+
const before = this.warned.get(key) ?? new Set<string>();
|
|
283
|
+
const news = missingNames.some((name) => !before.has(name));
|
|
284
|
+
if (missingNames.length === 0) this.warned.delete(key);
|
|
285
|
+
else this.warned.set(key, new Set(missingNames));
|
|
286
|
+
return news;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export { lastIndex };
|
|
@@ -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
|
-
|
|
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,
|
|
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
|
-
|
|
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; fill any gap a fresh reader hit. 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
|
-
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
58
|
+
Lifecycle playbooks wrap the entry playbook.
|
|
73
59
|
|
|
74
60
|
---
|
|
75
61
|
|
|
@@ -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 iteration.
|
|
8
|
+
4. Act: record one decision-trail row per iteration, 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
|
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
The Durable threshold lives in [SKILL.md](SKILL.md).
|
|
4
4
|
|
|
5
|
-
Artifacts live in `docs/specs/YYYY-MM-DD-<slug>/`. Create each file when its phase is reached, not up front. Link to
|
|
5
|
+
Artifacts live in `docs/specs/YYYY-MM-DD-<slug>/`. Create each file when its phase is reached, not up front. Link to instructions, ADRs, code, and tickets rather than copying them.
|
|
6
6
|
|
|
7
7
|
## Decision → `decision.md`
|
|
8
8
|
|
|
9
|
-
Resolve facts from their source; use `investigating` or `inspecting` when search is wide. Invoke `grilling` with `decision.md` only when it has an `open` row.
|
|
9
|
+
Resolve facts from their source; use `investigating` or `inspecting` when search is wide. Invoke `grilling` with `decision.md` only when it has an `open` row. Before implementation, note core-rule readiness in `decision.md`.
|
|
10
10
|
|
|
11
11
|
The file holds outcome and actors; in and out of scope; concrete scenarios; confirmed decisions; open consequential decisions. Explore every consequential branch in:
|
|
12
12
|
|
|
@@ -22,7 +22,7 @@ The file holds outcome and actors; in and out of scope; concrete scenarios; conf
|
|
|
22
22
|
|
|
23
23
|
## Spec → `spec.md`
|
|
24
24
|
|
|
25
|
-
Persist the spec in `spec.md` under `Status: proposed`, then have a fresh-context subagent
|
|
25
|
+
Persist the spec in `spec.md` under `Status: proposed`, then have a fresh-context subagent read it and its links, restate the contract, and list each point it would have to guess to build it. Fold each guess into the spec or add it to `decision.md` as an `open` row, because the author cannot see assumptions it never wrote; rerun the reader once when a fold changes observable behavior. Then present it for explicit user confirmation. `proposed` keeps production source untouched while artifacts and read-only work stay open. Only the user's approving reply sets `Status: approved`, `Approved at`, and `Approved from`.
|
|
26
26
|
|
|
27
27
|
The file opens with the approval header, then observable behavior and edge cases; compatibility constraints and non-goals; applied standards with their impact; evidence and precedent links; the selected reality anchor and checkpoint.
|
|
28
28
|
|
|
@@ -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.
|
|
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
|
|
|
@@ -51,10 +51,10 @@ Review the diff against project standards, the approved spec, and the confirmed
|
|
|
51
51
|
```
|
|
52
52
|
|
|
53
53
|
- **pass**: The anchor ran against the actual implementation and observed the expected behavior.
|
|
54
|
-
- **fail**: The anchor
|
|
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
|
|
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
|
|
package/skills/coding/SKILL.md
CHANGED
|
@@ -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.
|
|
15
|
-
- **Durable:** the work needs a persistent design record because of material ambiguity, consequential trade-offs, cross-component coordination, migration risk,
|
|
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:
|
|
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
|
|