@garygentry/feature-forge 0.3.5 → 0.3.6

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.
Files changed (164) hide show
  1. package/README.md +1 -1
  2. package/adapters/claude/.feature-forge-bundle.json +1 -1
  3. package/adapters/claude/references/forge-config-schema.json +4 -4
  4. package/adapters/claude/references/ralph-loop-contract.md +12 -8
  5. package/adapters/claude/references/shared-conventions.md +1 -1
  6. package/adapters/claude/scripts/forge-session.py +30 -26
  7. package/adapters/claude/skills/forge/references/shared-conventions.md +1 -1
  8. package/adapters/claude/skills/forge-0-epic/references/shared-conventions.md +1 -1
  9. package/adapters/claude/skills/forge-1-prd/references/shared-conventions.md +1 -1
  10. package/adapters/claude/skills/forge-2-tech/references/shared-conventions.md +1 -1
  11. package/adapters/claude/skills/forge-3-specs/references/shared-conventions.md +1 -1
  12. package/adapters/claude/skills/forge-4-backlog/references/shared-conventions.md +1 -1
  13. package/adapters/claude/skills/forge-5-loop/SKILL.md +3 -3
  14. package/adapters/claude/skills/forge-5-loop/references/ralph-loop-contract.md +12 -8
  15. package/adapters/claude/skills/forge-5-loop/references/runner-contract.md +2 -2
  16. package/adapters/claude/skills/forge-5-loop/references/shared-conventions.md +1 -1
  17. package/adapters/claude/skills/forge-6-docs/references/shared-conventions.md +1 -1
  18. package/adapters/claude/skills/forge-fix/references/shared-conventions.md +1 -1
  19. package/adapters/claude/skills/forge-guide/references/forge-config-schema.json +4 -4
  20. package/adapters/claude/skills/forge-guide/references/ralph-loop-contract.md +12 -8
  21. package/adapters/claude/skills/forge-guide/references/shared-conventions.md +1 -1
  22. package/adapters/claude/skills/forge-verify/SKILL.md +5 -3
  23. package/adapters/claude/skills/forge-verify/references/findings-template.md +6 -6
  24. package/adapters/claude/skills/forge-verify/references/shared-conventions.md +1 -1
  25. package/adapters/codex/.feature-forge-bundle.json +1 -1
  26. package/adapters/codex/references/forge-config-schema.json +4 -4
  27. package/adapters/codex/references/ralph-loop-contract.md +12 -8
  28. package/adapters/codex/references/shared-conventions.md +1 -1
  29. package/adapters/codex/scripts/forge-session.py +30 -26
  30. package/adapters/codex/skills/forge/references/shared-conventions.md +1 -1
  31. package/adapters/codex/skills/forge-0-epic/references/shared-conventions.md +1 -1
  32. package/adapters/codex/skills/forge-1-prd/references/shared-conventions.md +1 -1
  33. package/adapters/codex/skills/forge-2-tech/references/shared-conventions.md +1 -1
  34. package/adapters/codex/skills/forge-3-specs/references/shared-conventions.md +1 -1
  35. package/adapters/codex/skills/forge-4-backlog/references/shared-conventions.md +1 -1
  36. package/adapters/codex/skills/forge-5-loop/SKILL.md +3 -3
  37. package/adapters/codex/skills/forge-5-loop/references/ralph-loop-contract.md +12 -8
  38. package/adapters/codex/skills/forge-5-loop/references/runner-contract.md +2 -2
  39. package/adapters/codex/skills/forge-5-loop/references/shared-conventions.md +1 -1
  40. package/adapters/codex/skills/forge-6-docs/references/shared-conventions.md +1 -1
  41. package/adapters/codex/skills/forge-fix/references/shared-conventions.md +1 -1
  42. package/adapters/codex/skills/forge-guide/references/forge-config-schema.json +4 -4
  43. package/adapters/codex/skills/forge-guide/references/ralph-loop-contract.md +12 -8
  44. package/adapters/codex/skills/forge-guide/references/shared-conventions.md +1 -1
  45. package/adapters/codex/skills/forge-verify/SKILL.md +5 -3
  46. package/adapters/codex/skills/forge-verify/references/findings-template.md +6 -6
  47. package/adapters/codex/skills/forge-verify/references/shared-conventions.md +1 -1
  48. package/adapters/copilot/.feature-forge-bundle.json +1 -1
  49. package/adapters/copilot/references/forge-config-schema.json +4 -4
  50. package/adapters/copilot/references/ralph-loop-contract.md +12 -8
  51. package/adapters/copilot/references/shared-conventions.md +1 -1
  52. package/adapters/copilot/scripts/forge-session.py +30 -26
  53. package/adapters/copilot/skills/forge/references/shared-conventions.md +1 -1
  54. package/adapters/copilot/skills/forge-0-epic/references/shared-conventions.md +1 -1
  55. package/adapters/copilot/skills/forge-1-prd/references/shared-conventions.md +1 -1
  56. package/adapters/copilot/skills/forge-2-tech/references/shared-conventions.md +1 -1
  57. package/adapters/copilot/skills/forge-3-specs/references/shared-conventions.md +1 -1
  58. package/adapters/copilot/skills/forge-4-backlog/references/shared-conventions.md +1 -1
  59. package/adapters/copilot/skills/forge-5-loop/forge-5-loop.md +3 -3
  60. package/adapters/copilot/skills/forge-5-loop/references/ralph-loop-contract.md +12 -8
  61. package/adapters/copilot/skills/forge-5-loop/references/runner-contract.md +2 -2
  62. package/adapters/copilot/skills/forge-5-loop/references/shared-conventions.md +1 -1
  63. package/adapters/copilot/skills/forge-6-docs/references/shared-conventions.md +1 -1
  64. package/adapters/copilot/skills/forge-fix/references/shared-conventions.md +1 -1
  65. package/adapters/copilot/skills/forge-guide/references/forge-config-schema.json +4 -4
  66. package/adapters/copilot/skills/forge-guide/references/ralph-loop-contract.md +12 -8
  67. package/adapters/copilot/skills/forge-guide/references/shared-conventions.md +1 -1
  68. package/adapters/copilot/skills/forge-verify/forge-verify.md +5 -3
  69. package/adapters/copilot/skills/forge-verify/references/findings-template.md +6 -6
  70. package/adapters/copilot/skills/forge-verify/references/shared-conventions.md +1 -1
  71. package/adapters/cursor/.feature-forge-bundle.json +1 -1
  72. package/adapters/cursor/references/forge-config-schema.json +4 -4
  73. package/adapters/cursor/references/ralph-loop-contract.md +12 -8
  74. package/adapters/cursor/references/shared-conventions.md +1 -1
  75. package/adapters/cursor/scripts/forge-session.py +30 -26
  76. package/adapters/cursor/skills/forge/references/shared-conventions.md +1 -1
  77. package/adapters/cursor/skills/forge-0-epic/references/shared-conventions.md +1 -1
  78. package/adapters/cursor/skills/forge-1-prd/references/shared-conventions.md +1 -1
  79. package/adapters/cursor/skills/forge-2-tech/references/shared-conventions.md +1 -1
  80. package/adapters/cursor/skills/forge-3-specs/references/shared-conventions.md +1 -1
  81. package/adapters/cursor/skills/forge-4-backlog/references/shared-conventions.md +1 -1
  82. package/adapters/cursor/skills/forge-5-loop/forge-5-loop.mdc +3 -3
  83. package/adapters/cursor/skills/forge-5-loop/references/ralph-loop-contract.md +12 -8
  84. package/adapters/cursor/skills/forge-5-loop/references/runner-contract.md +2 -2
  85. package/adapters/cursor/skills/forge-5-loop/references/shared-conventions.md +1 -1
  86. package/adapters/cursor/skills/forge-6-docs/references/shared-conventions.md +1 -1
  87. package/adapters/cursor/skills/forge-fix/references/shared-conventions.md +1 -1
  88. package/adapters/cursor/skills/forge-guide/references/forge-config-schema.json +4 -4
  89. package/adapters/cursor/skills/forge-guide/references/ralph-loop-contract.md +12 -8
  90. package/adapters/cursor/skills/forge-guide/references/shared-conventions.md +1 -1
  91. package/adapters/cursor/skills/forge-verify/forge-verify.mdc +5 -3
  92. package/adapters/cursor/skills/forge-verify/references/findings-template.md +6 -6
  93. package/adapters/cursor/skills/forge-verify/references/shared-conventions.md +1 -1
  94. package/adapters/gemini/.feature-forge-bundle.json +1 -1
  95. package/adapters/gemini/gemini-extension.json +1 -1
  96. package/adapters/gemini/references/forge-config-schema.json +4 -4
  97. package/adapters/gemini/references/ralph-loop-contract.md +12 -8
  98. package/adapters/gemini/references/shared-conventions.md +1 -1
  99. package/adapters/gemini/scripts/forge-session.py +30 -26
  100. package/adapters/gemini/skills/forge/references/shared-conventions.md +1 -1
  101. package/adapters/gemini/skills/forge-0-epic/references/shared-conventions.md +1 -1
  102. package/adapters/gemini/skills/forge-1-prd/references/shared-conventions.md +1 -1
  103. package/adapters/gemini/skills/forge-2-tech/references/shared-conventions.md +1 -1
  104. package/adapters/gemini/skills/forge-3-specs/references/shared-conventions.md +1 -1
  105. package/adapters/gemini/skills/forge-4-backlog/references/shared-conventions.md +1 -1
  106. package/adapters/gemini/skills/forge-5-loop/forge-5-loop.md +3 -3
  107. package/adapters/gemini/skills/forge-5-loop/references/ralph-loop-contract.md +12 -8
  108. package/adapters/gemini/skills/forge-5-loop/references/runner-contract.md +2 -2
  109. package/adapters/gemini/skills/forge-5-loop/references/shared-conventions.md +1 -1
  110. package/adapters/gemini/skills/forge-6-docs/references/shared-conventions.md +1 -1
  111. package/adapters/gemini/skills/forge-fix/references/shared-conventions.md +1 -1
  112. package/adapters/gemini/skills/forge-guide/references/forge-config-schema.json +4 -4
  113. package/adapters/gemini/skills/forge-guide/references/ralph-loop-contract.md +12 -8
  114. package/adapters/gemini/skills/forge-guide/references/shared-conventions.md +1 -1
  115. package/adapters/gemini/skills/forge-verify/forge-verify.md +5 -3
  116. package/adapters/gemini/skills/forge-verify/references/findings-template.md +6 -6
  117. package/adapters/gemini/skills/forge-verify/references/shared-conventions.md +1 -1
  118. package/adapters/pi/.feature-forge-bundle.json +1 -1
  119. package/adapters/pi/extensions/forge-loop-supervisor/events.ts +90 -0
  120. package/adapters/pi/extensions/forge-loop-supervisor/index.ts +114 -0
  121. package/adapters/pi/extensions/forge-loop-supervisor/registry.ts +137 -0
  122. package/adapters/pi/extensions/forge-loop-supervisor/supervisor.ts +185 -0
  123. package/adapters/pi/extensions/forge-loop-supervisor/tailer.ts +137 -0
  124. package/adapters/pi/extensions/forge-loop-supervisor/types.ts +87 -0
  125. package/adapters/pi/extensions/forge-loop-supervisor/wiring.ts +423 -0
  126. package/adapters/pi/package.json +2 -1
  127. package/adapters/pi/references/forge-config-schema.json +4 -4
  128. package/adapters/pi/references/ralph-loop-contract.md +12 -8
  129. package/adapters/pi/references/shared-conventions.md +1 -1
  130. package/adapters/pi/scripts/forge-session.py +30 -26
  131. package/adapters/pi/skills/forge/SKILL.md +4 -1
  132. package/adapters/pi/skills/forge/references/shared-conventions.md +1 -1
  133. package/adapters/pi/skills/forge-0-epic/SKILL.md +4 -1
  134. package/adapters/pi/skills/forge-0-epic/references/shared-conventions.md +1 -1
  135. package/adapters/pi/skills/forge-1-prd/SKILL.md +4 -1
  136. package/adapters/pi/skills/forge-1-prd/references/shared-conventions.md +1 -1
  137. package/adapters/pi/skills/forge-2-tech/SKILL.md +4 -1
  138. package/adapters/pi/skills/forge-2-tech/references/shared-conventions.md +1 -1
  139. package/adapters/pi/skills/forge-3-specs/SKILL.md +4 -1
  140. package/adapters/pi/skills/forge-3-specs/references/shared-conventions.md +1 -1
  141. package/adapters/pi/skills/forge-4-backlog/SKILL.md +4 -1
  142. package/adapters/pi/skills/forge-4-backlog/references/shared-conventions.md +1 -1
  143. package/adapters/pi/skills/forge-5-loop/SKILL.md +14 -8
  144. package/adapters/pi/skills/forge-5-loop/references/ralph-loop-contract.md +12 -8
  145. package/adapters/pi/skills/forge-5-loop/references/runner-contract.md +8 -5
  146. package/adapters/pi/skills/forge-5-loop/references/shared-conventions.md +1 -1
  147. package/adapters/pi/skills/forge-6-docs/SKILL.md +4 -1
  148. package/adapters/pi/skills/forge-6-docs/references/shared-conventions.md +1 -1
  149. package/adapters/pi/skills/forge-bootstrap/SKILL.md +4 -1
  150. package/adapters/pi/skills/forge-fix/SKILL.md +4 -1
  151. package/adapters/pi/skills/forge-fix/references/shared-conventions.md +1 -1
  152. package/adapters/pi/skills/forge-guide/SKILL.md +4 -1
  153. package/adapters/pi/skills/forge-guide/references/forge-config-schema.json +4 -4
  154. package/adapters/pi/skills/forge-guide/references/ralph-loop-contract.md +12 -8
  155. package/adapters/pi/skills/forge-guide/references/shared-conventions.md +1 -1
  156. package/adapters/pi/skills/forge-init/SKILL.md +4 -1
  157. package/adapters/pi/skills/forge-verify/SKILL.md +9 -4
  158. package/adapters/pi/skills/forge-verify/references/findings-template.md +6 -6
  159. package/adapters/pi/skills/forge-verify/references/shared-conventions.md +1 -1
  160. package/dist/manifest.d.ts +1 -1
  161. package/dist/rauf.d.ts +3 -3
  162. package/dist/rauf.js +2 -2
  163. package/dist/types.d.ts +1 -1
  164. package/package.json +8 -2
@@ -0,0 +1,423 @@
1
+ // GENERATED — DO NOT EDIT. Source: adapter-src/pi/extensions/forge-loop-supervisor/wiring.ts
2
+ // Regenerate with: python3 scripts/build-adapters.py
3
+ /**
4
+ * Pi wiring for the forge-loop-supervisor — builds the host, the three tools
5
+ * (launch / status / stop) and the session-lifecycle hooks on a given
6
+ * `ExtensionAPI`. Every side-effecting dependency (process spawn, file watch,
7
+ * clock) is injected so the whole extension is unit-testable with a fake pi and
8
+ * fake deps, mirroring how the vendored ask-user-question extension is driven
9
+ * headlessly in its own test.
10
+ *
11
+ * pi surface used (resolved against the installed pi types, stable across the
12
+ * pinned 0.81.x): `pi.registerTool`, `pi.on("session_start"|"session_shutdown")`,
13
+ * `pi.sendMessage(msg, { triggerTurn })` (wake the session — the file-trigger.ts
14
+ * pattern), `pi.appendEntry(type, data)` (durable task record), `pi.exec` (run
15
+ * `rauf status --json` / `rauf loop stop`), and `ctx.ui.notify` (quiet toasts).
16
+ * rauf's `--detached` loop is server-owned and outlives the session, so cleanup
17
+ * on shutdown tears down watchers only — never the runner.
18
+ */
19
+
20
+ import { existsSync, readFileSync } from "node:fs";
21
+ import { join } from "node:path";
22
+
23
+ import { Type } from "typebox";
24
+
25
+ import { clearMirror, discoverMirrors, readMirror, writeMirror } from "./registry.js";
26
+ import { LoopSupervisor, type TaskHandle } from "./supervisor.js";
27
+ import { NdjsonTailer } from "./tailer.js";
28
+ import type { SupervisorHost, SupervisorTask } from "./types.js";
29
+
30
+ /** The custom-entry type under which task identity is persisted into the pi
31
+ * session (read back on `session_start` for reattach). */
32
+ export const TASK_ENTRY_TYPE = "forge-loop-task";
33
+ /** The custom-message type used to wake the session on exception/terminal. */
34
+ export const WAKE_MESSAGE_TYPE = "forge-loop";
35
+
36
+ /** A live poll trigger for one watched file: `close()` stops it. The real
37
+ * factory wires `fs.watch` on the containing directory (so rotation-by-rename
38
+ * is caught) plus a low-frequency backstop; a test injects a manual trigger. */
39
+ export interface WatchHandle {
40
+ close(): void;
41
+ }
42
+
43
+ /** Injectable side effects. Production values live in index.ts. */
44
+ export interface Deps {
45
+ /** Launch the runner DETACHED so it outlives the session. Returns nothing —
46
+ * the loop is owned by the runner's server, not by a tracked child. `onError`
47
+ * reports an ASYNCHRONOUS spawn failure (e.g. ENOENT for a bad bin, which
48
+ * Node surfaces on the child's 'error' event, not as a throw). */
49
+ spawnDetached(bin: string, args: string[], cwd: string, onError?: (message: string) => void): void;
50
+ /** Watch `filePath` for changes, calling `onChange` on each (real: fs.watch
51
+ * on its directory + backstop interval). */
52
+ watch(filePath: string, onChange: () => void): WatchHandle;
53
+ /** Current time as ISO-8601. */
54
+ now(): string;
55
+ }
56
+
57
+ /** Minimal shape of the pi API this wiring needs (kept structural so the fake pi
58
+ * in tests satisfies it without importing pi). */
59
+ export interface PiLike {
60
+ registerTool(def: unknown): void;
61
+ on(event: string, handler: (event: unknown, ctx: unknown) => void | Promise<void>): void;
62
+ sendMessage(
63
+ message: { customType: string; content: string; display?: boolean },
64
+ options?: { triggerTurn?: boolean },
65
+ ): void;
66
+ appendEntry(customType: string, data?: unknown): void;
67
+ exec?(
68
+ command: string,
69
+ args: string[],
70
+ options?: { cwd?: string; timeout?: number },
71
+ ): Promise<{ stdout: string; stderr: string; code: number }>;
72
+ }
73
+
74
+ interface UiLike {
75
+ notify?(message: string, level?: "info" | "warning" | "error"): void;
76
+ }
77
+ interface CtxLike {
78
+ cwd?: string;
79
+ hasUI?: boolean;
80
+ ui?: UiLike;
81
+ sessionManager?: { getEntries?(): Array<{ type?: string; customType?: string; data?: unknown }> };
82
+ }
83
+
84
+ function textResult(text: string, details: Record<string, unknown>) {
85
+ return { content: [{ type: "text" as const, text }], details };
86
+ }
87
+
88
+ /** Best-effort count of backlog items for the `[N/M]` progress line. */
89
+ function countBacklog(cwd: string, backlogDir: string): number | undefined {
90
+ for (const candidate of [join(cwd, backlogDir, "backlog.json"), join(backlogDir, "backlog.json")]) {
91
+ try {
92
+ if (!existsSync(candidate)) continue;
93
+ const parsed = JSON.parse(readFileSync(candidate, "utf8")) as { items?: unknown[] };
94
+ if (Array.isArray(parsed.items)) return parsed.items.length;
95
+ } catch {
96
+ // unreadable/unparseable backlog → no total, not an error
97
+ }
98
+ }
99
+ return undefined;
100
+ }
101
+
102
+ /** Resolve `dir` against `cwd` unless it is already absolute. */
103
+ function resolveDir(cwd: string, dir: string): string {
104
+ return dir.startsWith("/") ? dir : join(cwd, dir);
105
+ }
106
+
107
+ /**
108
+ * Register the supervisor's tools and lifecycle hooks on `pi`. Returns a small
109
+ * control object (used by tests) exposing the live supervisor and watcher map.
110
+ */
111
+ export function createExtension(pi: PiLike, deps: Deps) {
112
+ const watchers = new Map<string, { handle: TaskHandle; watch: WatchHandle; backlogDir: string }>();
113
+ let ui: UiLike | null = null;
114
+
115
+ const host: SupervisorHost = {
116
+ notify(message, level) {
117
+ try {
118
+ ui?.notify?.(message, level);
119
+ } catch {
120
+ /* headless / no dialog UI */
121
+ }
122
+ },
123
+ wake(message) {
124
+ try {
125
+ pi.sendMessage({ customType: WAKE_MESSAGE_TYPE, content: message, display: true }, { triggerTurn: true });
126
+ } catch {
127
+ /* sendMessage unavailable — nothing else to do */
128
+ }
129
+ },
130
+ persist(task) {
131
+ try {
132
+ pi.appendEntry(TASK_ENTRY_TYPE, task);
133
+ } catch {
134
+ /* appendEntry best-effort */
135
+ }
136
+ writeMirror(task);
137
+ },
138
+ now: () => deps.now(),
139
+ };
140
+
141
+ const supervisor = new LoopSupervisor(host);
142
+
143
+ /** Keep the notify target current from whatever ctx we last saw (tool call or
144
+ * session_start). ctx can go stale across a session replacement, so we always
145
+ * prefer the most recent one. */
146
+ function refreshUi(ctx: CtxLike): void {
147
+ if (ctx?.ui) ui = ctx.ui;
148
+ }
149
+
150
+ /** Begin watching a task's event file. Idempotent per stateDir (the dedup
151
+ * guard against duplicate watchers). Does an initial poll to replay history. */
152
+ function startWatch(task: SupervisorTask): void {
153
+ if (watchers.has(task.stateDir)) return;
154
+ const handle = supervisor.attach(task, (onRecord, onRotate) =>
155
+ new NdjsonTailer(task.eventsFile, onRecord, undefined, onRotate, task.eventsIno),
156
+ );
157
+ // Poll, then tear the watcher down once a terminal event has closed the task
158
+ // (its wake was already sent inside poll). This stops the fs.watch + backstop
159
+ // interval for a finished loop and frees a relaunch on the same backlog — the
160
+ // detached runner is untouched, exactly as on session end.
161
+ const pump = () => {
162
+ handle.poll();
163
+ if (supervisor.progress(task.stateDir)?.closed) stopWatch(task.stateDir);
164
+ };
165
+ const watch = deps.watch(task.eventsFile, pump);
166
+ watchers.set(task.stateDir, { handle, watch, backlogDir: task.backlogDir });
167
+ pump();
168
+ }
169
+
170
+ /** Stop watching a task (teardown / stop) WITHOUT touching the runner. */
171
+ function stopWatch(stateDir: string): void {
172
+ const w = watchers.get(stateDir);
173
+ if (!w) return;
174
+ try {
175
+ w.watch.close();
176
+ } catch {
177
+ /* ignore */
178
+ }
179
+ w.handle.close();
180
+ watchers.delete(stateDir);
181
+ }
182
+
183
+ // ---- Tools -------------------------------------------------------------
184
+
185
+ pi.registerTool({
186
+ name: "forge_loop_launch",
187
+ label: "Launch forge loop",
188
+ description:
189
+ "Launch the forge autonomous coding loop (rauf) DETACHED and supervise it. " +
190
+ "The loop runs in rauf's server and outlives this session; this tool returns " +
191
+ "immediately. It then reports each completed backlog item as a quiet line and " +
192
+ "WAKES this session on needs-human, blocked, stuck, review-failed, error, or " +
193
+ "completion. Use this for forge-5-loop's loop run on Pi instead of running the " +
194
+ "runner in the foreground.",
195
+ promptSnippet:
196
+ "Start a forge/rauf loop without blocking the session and supervise its events.ndjson.",
197
+ parameters: Type.Object({
198
+ backlogDir: Type.String({ description: "Forge backlog directory passed to --backlog (e.g. specs/auth)." }),
199
+ bin: Type.Optional(Type.String({ description: "Runner binary. Default 'rauf'." })),
200
+ stateDir: Type.Optional(
201
+ Type.String({ description: "Runner state dir holding events.ndjson. Default '<backlogDir>/.rauf'." }),
202
+ ),
203
+ iterations: Type.Optional(Type.Number({ description: "Max iterations (--iterations). Omit for the runner default." })),
204
+ review: Type.Optional(Type.Boolean({ description: "Append --review to run the loop's review pass." })),
205
+ agent: Type.Optional(Type.String({ description: "Coding-agent id passed as --agent." })),
206
+ }),
207
+ async execute(_id: string, params: unknown, _signal: unknown, _onUpdate: unknown, ctx: CtxLike) {
208
+ const p = params as {
209
+ backlogDir: string;
210
+ bin?: string;
211
+ stateDir?: string;
212
+ iterations?: number;
213
+ review?: boolean;
214
+ agent?: string;
215
+ };
216
+ refreshUi(ctx);
217
+ const cwd = ctx.cwd ?? process.cwd();
218
+ const bin = p.bin || "rauf";
219
+ const stateDir = resolveDir(cwd, p.stateDir || join(p.backlogDir, ".rauf"));
220
+ const eventsFile = join(stateDir, "events.ndjson");
221
+
222
+ if (supervisor.isActive(stateDir)) {
223
+ return textResult(
224
+ `A forge loop is already being supervised for ${stateDir}. Use forge_loop_status to check it, or forge_loop_stop first.`,
225
+ { launched: false, stateDir },
226
+ );
227
+ }
228
+
229
+ const args = ["loop", "run", ".", "--backlog", p.backlogDir, "--detached"];
230
+ if (typeof p.iterations === "number") args.push("--iterations", String(p.iterations));
231
+ if (p.review) args.push("--review");
232
+ if (p.agent) args.push("--agent", p.agent);
233
+
234
+ try {
235
+ // A synchronous throw is a definite failure. An ASYNC spawn error
236
+ // (ENOENT for a bad bin) arrives after this returns, so surface it as
237
+ // a notification when it fires rather than silently claiming success.
238
+ deps.spawnDetached(bin, args, cwd, (msg) =>
239
+ host.notify(`forge loop: failed to launch ${bin} — ${msg}. The loop did not start.`, "error"),
240
+ );
241
+ } catch (e) {
242
+ const msg = e instanceof Error ? e.message : String(e);
243
+ return textResult(`Failed to launch ${bin}: ${msg}`, { launched: false, stateDir, error: msg });
244
+ }
245
+
246
+ const task: SupervisorTask = {
247
+ backlogDir: p.backlogDir,
248
+ stateDir,
249
+ eventsFile,
250
+ launchedAt: host.now(),
251
+ total: countBacklog(cwd, p.backlogDir),
252
+ lastSeq: -1,
253
+ closed: false,
254
+ };
255
+ host.persist(task);
256
+ startWatch(task);
257
+
258
+ return textResult(
259
+ `Launched \`${bin} ${args.join(" ")}\` detached; now supervising ${eventsFile}. ` +
260
+ "I'll report each completed item and wake this session on needs-human / blocked / " +
261
+ "stuck / review-failed / error / completion. Run forge_loop_status to confirm it started. " +
262
+ "Do NOT run the loop in the foreground.",
263
+ { launched: true, stateDir, eventsFile },
264
+ );
265
+ },
266
+ });
267
+
268
+ pi.registerTool({
269
+ name: "forge_loop_status",
270
+ label: "Forge loop status",
271
+ description:
272
+ "Report the status of a supervised forge/rauf loop: how many items are done, " +
273
+ "whether it has finished, and the runner's authoritative counts (via " +
274
+ "`rauf status --json`). Use to confirm a launch or check progress on demand.",
275
+ parameters: Type.Object({
276
+ backlogDir: Type.Optional(Type.String({ description: "Backlog dir of the loop to report. Omit for the only supervised loop." })),
277
+ stateDir: Type.Optional(Type.String({ description: "State dir of the loop. Overrides backlogDir when set." })),
278
+ bin: Type.Optional(Type.String({ description: "Runner binary for the status query. Default 'rauf'." })),
279
+ }),
280
+ async execute(_id: string, params: unknown, _signal: unknown, _onUpdate: unknown, ctx: CtxLike) {
281
+ const p = params as { backlogDir?: string; stateDir?: string; bin?: string };
282
+ refreshUi(ctx);
283
+ const cwd = ctx.cwd ?? process.cwd();
284
+ const stateDir = p.stateDir
285
+ ? resolveDir(cwd, p.stateDir)
286
+ : p.backlogDir
287
+ ? resolveDir(cwd, join(p.backlogDir, ".rauf"))
288
+ : [...watchers.keys()][0];
289
+
290
+ const progress = stateDir ? supervisor.progress(stateDir) : null;
291
+ // Scope the runner query to the supervised backlog so status reflects the
292
+ // right loop, not whatever sits at the default project root.
293
+ const backlogDir = p.backlogDir ?? (stateDir ? watchers.get(stateDir)?.backlogDir : undefined);
294
+ let runnerLine = "";
295
+ if (pi.exec) {
296
+ const statusArgs = ["status", "--json"];
297
+ if (backlogDir) statusArgs.push("--backlog", backlogDir);
298
+ try {
299
+ const res = await pi.exec(p.bin || "rauf", statusArgs, { cwd, timeout: 15000 });
300
+ runnerLine = res.code === 0 ? ` Runner: ${res.stdout.trim()}` : ` Runner status exited ${res.code}.`;
301
+ } catch {
302
+ runnerLine = " (could not query the runner for authoritative status)";
303
+ }
304
+ }
305
+
306
+ if (!progress) {
307
+ return textResult(
308
+ `No forge loop is being supervised in this session${stateDir ? ` for ${stateDir}` : ""}.${runnerLine}`,
309
+ { supervised: false, stateDir, runner: runnerLine.trim() },
310
+ );
311
+ }
312
+ const totalPart = typeof progress.total === "number" ? `/${progress.total}` : "";
313
+ return textResult(
314
+ `Forge loop ${progress.closed ? "finished" : "running"}: ${progress.done}${totalPart} items completed so far.${runnerLine}`,
315
+ { supervised: true, stateDir, done: progress.done, total: progress.total, closed: progress.closed },
316
+ );
317
+ },
318
+ });
319
+
320
+ pi.registerTool({
321
+ name: "forge_loop_stop",
322
+ label: "Stop forge loop",
323
+ description:
324
+ "Stop a supervised forge/rauf loop and stop watching it. This DELIBERATELY " +
325
+ "terminates the runner (via `rauf loop stop`) — unlike ending the session, " +
326
+ "which leaves the detached loop running. Use only when the user wants the loop " +
327
+ "to actually stop.",
328
+ parameters: Type.Object({
329
+ backlogDir: Type.Optional(Type.String({ description: "Backlog dir of the loop to stop. Omit for the only supervised loop." })),
330
+ stateDir: Type.Optional(Type.String({ description: "State dir of the loop. Overrides backlogDir when set." })),
331
+ bin: Type.Optional(Type.String({ description: "Runner binary. Default 'rauf'." })),
332
+ }),
333
+ async execute(_id: string, params: unknown, _signal: unknown, _onUpdate: unknown, ctx: CtxLike) {
334
+ const p = params as { backlogDir?: string; stateDir?: string; bin?: string };
335
+ refreshUi(ctx);
336
+ const cwd = ctx.cwd ?? process.cwd();
337
+ const stateDir = p.stateDir
338
+ ? resolveDir(cwd, p.stateDir)
339
+ : p.backlogDir
340
+ ? resolveDir(cwd, join(p.backlogDir, ".rauf"))
341
+ : [...watchers.keys()][0];
342
+
343
+ // Refuse a blind stop: with nothing supervised AND no explicit target we
344
+ // must NOT run `rauf loop stop`, which would kill whatever loop happens to
345
+ // be running in this project — one this extension never launched. Require
346
+ // a supervised task or an explicit backlogDir/stateDir.
347
+ const watcherRec = stateDir ? watchers.get(stateDir) : undefined;
348
+ if (!watcherRec && !p.backlogDir && !p.stateDir) {
349
+ return textResult(
350
+ "No forge loop is being supervised in this session, and no backlog/stateDir was given — nothing to stop. Ending the session already leaves any detached loop running by design; pass a backlogDir to stop a specific loop.",
351
+ { stopped: false },
352
+ );
353
+ }
354
+
355
+ // Scope the stop to the specific backlog so it targets the intended loop,
356
+ // not the default project root.
357
+ const backlogDir = p.backlogDir ?? watcherRec?.backlogDir;
358
+ let stopLine = "";
359
+ if (pi.exec) {
360
+ const stopArgs = ["loop", "stop"];
361
+ if (backlogDir) stopArgs.push("--backlog", backlogDir);
362
+ try {
363
+ const res = await pi.exec(p.bin || "rauf", stopArgs, { cwd, timeout: 30000 });
364
+ stopLine = res.code === 0 ? "Runner stop requested." : `Runner stop exited ${res.code}: ${res.stderr.trim()}`;
365
+ } catch (e) {
366
+ stopLine = `Could not run the stop command: ${e instanceof Error ? e.message : String(e)}`;
367
+ }
368
+ }
369
+ if (stateDir) {
370
+ stopWatch(stateDir);
371
+ supervisor.detach(stateDir);
372
+ clearMirror(stateDir);
373
+ }
374
+ return textResult(`Stopped supervising${stateDir ? ` ${stateDir}` : ""}. ${stopLine}`, {
375
+ stopped: true,
376
+ stateDir,
377
+ });
378
+ },
379
+ });
380
+
381
+ // ---- Lifecycle ---------------------------------------------------------
382
+
383
+ // Reattach on every session start (startup / reload / resume / fork): rebuild
384
+ // the watcher for any loop this session (or a previous one) launched, without
385
+ // duplicate reporting — the dedup cursor (lastSeq) makes replayed history silent.
386
+ pi.on("session_start", (_event: unknown, ctx: unknown) => {
387
+ const c = ctx as CtxLike;
388
+ ui = c.ui ?? null;
389
+ const seen = new Set<string>();
390
+ const consider = (task: SupervisorTask | null) => {
391
+ if (!task || seen.has(task.stateDir) || watchers.has(task.stateDir)) return;
392
+ seen.add(task.stateDir);
393
+ // Prefer the on-disk mirror (most current lastSeq/closed/eventsIno); fall
394
+ // back to the session entry's own copy.
395
+ const fresh = readMirror(task.stateDir) ?? task;
396
+ if (fresh.closed) return; // already surfaced its terminal — nothing to resume
397
+ startWatch(fresh);
398
+ };
399
+ // (1) Session entries carry tasks across reload/resume/fork within a session
400
+ // lineage (pi.appendEntry survives those).
401
+ const entries = c.sessionManager?.getEntries?.() ?? [];
402
+ for (const entry of entries) {
403
+ if (entry?.type === "custom" && entry?.customType === TASK_ENTRY_TYPE) {
404
+ consider(entry.data as SupervisorTask);
405
+ }
406
+ }
407
+ // (2) A BRAND-NEW session file has no such entry, so also discover the disk
408
+ // mirrors under the project root — this is what lets a fresh session reattach
409
+ // to a loop a previous session launched (issue #236's "reconcile after
410
+ // session restart"). Bounded scan; `consider` dedups and skips closed tasks.
411
+ const cwd = c.cwd ?? process.cwd();
412
+ for (const task of discoverMirrors(cwd)) consider(task);
413
+ });
414
+
415
+ // Cleanup on shutdown: close every watcher, but LEAVE the detached runner
416
+ // running (it is server-owned and meant to outlive the session) and leave the
417
+ // mirror in place so the next session reattaches.
418
+ pi.on("session_shutdown", () => {
419
+ for (const stateDir of [...watchers.keys()]) stopWatch(stateDir);
420
+ });
421
+
422
+ return { supervisor, watchers, startWatch, stopWatch, host };
423
+ }
@@ -10,7 +10,8 @@
10
10
  "./skills"
11
11
  ],
12
12
  "extensions": [
13
- "./extensions/ask-user-question/index.ts"
13
+ "./extensions/ask-user-question/index.ts",
14
+ "./extensions/forge-loop-supervisor/index.ts"
14
15
  ]
15
16
  },
16
17
  "pi-subagents": {
@@ -235,8 +235,8 @@
235
235
  },
236
236
  "installHint": {
237
237
  "type": "string",
238
- "default": "Provision rauf for a multi-agent setup with the cross-agent installer: `npx @garygentry/feature-forge install` (records the pinned @garygentry/rauf@0.14.0 default). Or install/upgrade just the rauf CLI: `npx @garygentry/rauf@0.14.0 --version`, or `curl -fsSL https://raw.githubusercontent.com/garygentry/rauf/main/scripts/install-binary.sh | bash`.",
239
- "description": "Shown when the runner BINARY is missing or too old (version gate fails, minRunnerVersion floor) — how to obtain/upgrade the CLI itself. Names two distinct binary-provisioning paths: (1) the cross-agent installer (`npx @garygentry/feature-forge install`, the multi-agent provisioning path that pins @garygentry/rauf@0.14.0), and (2) the direct rauf-CLI install/upgrade one-liner. Distinct from setupHint (which installs per-project artifacts); a version-gate failure is ALWAYS this hint, never setupHint."
238
+ "default": "Provision rauf for a multi-agent setup with the cross-agent installer: `npx @garygentry/feature-forge install` (records the pinned @garygentry/rauf@0.15.0 default). Or install/upgrade just the rauf CLI: `npx @garygentry/rauf@0.15.0 --version`, or `curl -fsSL https://raw.githubusercontent.com/garygentry/rauf/main/scripts/install-binary.sh | bash`.",
239
+ "description": "Shown when the runner BINARY is missing or too old (version gate fails, minRunnerVersion floor) — how to obtain/upgrade the CLI itself. Names two distinct binary-provisioning paths: (1) the cross-agent installer (`npx @garygentry/feature-forge install`, the multi-agent provisioning path that pins @garygentry/rauf@0.15.0), and (2) the direct rauf-CLI install/upgrade one-liner. Distinct from setupHint (which installs per-project artifacts); a version-gate failure is ALWAYS this hint, never setupHint."
240
240
  },
241
241
  "schemaVersion": {
242
242
  "type": "string",
@@ -245,8 +245,8 @@
245
245
  },
246
246
  "minRunnerVersion": {
247
247
  "type": "string",
248
- "default": "0.6.0",
249
- "description": "Minimum runner version (semver). 0.6.0 is the AGENT-SURFACE FLOOR: the rauf version that ships the coding-agent selection surface (the --agent flag, the `agents` availability probe, and the preset agent registry) that this config's agentArgument/agentsProbeCommand consume. Flooring here guarantees a successful gate implies those surfaces exist. (0.5.0 was the prior grammar/contract-flip floor — unified exit codes, `loop run --detached`, explicit `review` signal, versioned events.ndjson — which predates the agent surface and so could not guarantee it.) forge-5 enforces this via versionCommand before any loop side-effects."
248
+ "default": "0.14.0",
249
+ "description": "Minimum runner version (semver). 0.14.0 is the CAPABILITY FLOOR the shipped stage actually depends on — it is the version needed for full post-run needs-human recovery: rauf 0.14's `backlog answer` injects a recorded answer into the next iteration (RECOVERY_MIN_RUNNER_VERSION in forge-session.py). Below it, recovery silently degrades to `backlog unblock` (answer not injected) and large Codex prompts retain the pre-0.14 argv-size (E2BIG) failure mode. Earlier surfaces are subsumed: 0.6.0 first shipped the coding-agent selection surface (--agent flag, `agents` availability probe, preset registry consumed by agentArgument/agentsProbeCommand); 0.5.0 added unified exit codes, `loop run --detached`, the explicit `review` signal, and versioned events.ndjson. Flooring at 0.14.0 guarantees a successful gate implies the recovery + agent surfaces both exist. The floor is distinct from the installer's `RAUF_PIN` (installHint) — the pin may sit ahead of the floor when a newer rauf ships nothing this floor's dependents need; it only rises when rauf ships a surface a shipped stage actually depends on. forge-5 enforces the floor via versionCommand before any loop side-effects."
250
250
  }
251
251
  }
252
252
  }
@@ -156,11 +156,13 @@ to today. Degradation is **silent, not an error**, keeping alternate (non-rauf)
156
156
  runners first-class. The gate condition is owned by
157
157
  `02-config-schema-and-gating.md`.
158
158
 
159
- Independently, the **version gate** floors at the runner version that ships the
160
- agent surface. For rauf that is **0.6.0** (`loopRunner.minRunnerVersion`): the
161
- `--agent` flag, the `agents` probe, and the preset agent registry are present in
162
- rauf source at 0.6.0. A successful gate therefore guarantees those surfaces
163
- exist before any run. See `## Version gating` and
159
+ Independently, the **version gate** floors at the runner version the stage
160
+ depends on. For rauf that is **0.14.0** (`loopRunner.minRunnerVersion`): the
161
+ version the package pins and the floor for full needs-human recovery (0.14's
162
+ `backlog answer`). The agent surface — the `--agent` flag, the `agents` probe,
163
+ and the preset agent registry — has been present since rauf 0.6.0 and is
164
+ subsumed by the higher floor, so a successful gate guarantees both the recovery
165
+ and agent surfaces exist before any run. See `## Version gating` and
164
166
  `05-runner-discovery-version-gate.md`.
165
167
 
166
168
  **This document — the `## Agent selection` section, the `## Per-stage agent
@@ -210,9 +212,11 @@ belongs to execution only. If you find yourself adding `--agent` near a
210
212
 
211
213
  feature-forge requires a runner exposing `backlog validate` + backlog
212
214
  `schemaVersion`, and the unified exit-code/status contract it reads. The floor is
213
- now the **agent-surface floor**: the runner version that ships the `--agent`
214
- flag, the `agents` probe, and the preset agent registry. For rauf that is
215
- **0.6.0** (`loopRunner.minRunnerVersion`).
215
+ now the **capability floor** the stage actually depends on: the version the
216
+ package pins and the floor for full needs-human recovery (0.14's `backlog
217
+ answer`). It subsumes the older agent-surface floor (the `--agent` flag, the
218
+ `agents` probe, and the preset agent registry, present since 0.6.0). For rauf
219
+ that is **0.14.0** (`loopRunner.minRunnerVersion`).
216
220
  `forge-5-loop` runs `{bin} version --json`, semver-compares the reported version
217
221
  against `minRunnerVersion`, and on a missing-or-too-old runner stops with
218
222
  `loopRunner.installHint` (the CLI install/upgrade command) — **before** invoking
@@ -206,7 +206,7 @@ If a `state-*` verb exits 2, surface the plain `Error:` line from stderr verbati
206
206
 
207
207
  ### `state-verify` — verification results and provenance
208
208
 
209
- `state-verify` writes exactly one `stages.forge-verify-{token}` entry — the verification result for the production stage named by `--stage` — plus the top-level `updatedAt`, and nothing else. `--stage` takes the **served production stage** (`forge-0-epic` through `forge-5-loop`; `forge-6-docs` has no verification token and is rejected). Add `--epic "{epic}"` when the feature is an epic member — required, per the member rule above. Result mode passes `--status` (`auto-verify-pending`, `passed`, `findings-reported`, `findings-applied`, or `skipped`) with whatever `--findings-file`, `--findings-count`, and `--verified-stage-version` that status requires; contradictory metadata is refused before any write. `passed` may additionally carry `--findings-file` + `--findings-count` together for an **advisory-only** report (`inconsistency`/`improvement` findings only, per forge-verify's severity floor) — the stage resolves without a fix round and the report stays attached:
209
+ `state-verify` writes exactly one `stages.forge-verify-{token}` entry — the verification result for the production stage named by `--stage` — plus the top-level `updatedAt`, and nothing else. `--stage` takes the **served production stage** (`forge-0-epic` through `forge-5-loop`; `forge-6-docs` has no verification token and is rejected). Add `--epic "{epic}"` when the feature is an epic member — required, per the member rule above. Result mode passes `--status` (`auto-verify-pending`, `passed`, `findings-reported`, `findings-applied`, or `skipped`) with whatever `--findings-file`, `--findings-count`, and `--verified-stage-version` that status requires; contradictory metadata is refused before any write. `passed` may carry `--findings-file` + `--findings-count` for a clean report (count `0`), an **advisory-only** report (`inconsistency`/`improvement` findings only), or accepted residual findings. The stage resolves without a fix round and the report stays attached, making a completed verification round directly resumable if provenance recording is interrupted. A bare `passed` remains accepted for backward compatibility:
210
210
 
211
211
  **`auto-verify-pending` is not a skill-facing status.** It is written by `stage-exit`'s scheduling boundary, which records the debt automatically when auto-verify is effective for a stage. The value is accepted on this CLI so the entry stays inspectable and repairable, not so a skill can hand-schedule verification: no skill body and no reference passes it, and none should. Every other status in the list is the recorded *result* of a verification that ran (or was explicitly skipped); this one records that one was *owed*.
212
212
 
@@ -461,8 +461,12 @@ TOPOLOGY_DEPTH_WARN_RATIO: Final[float] = 0.5
461
461
  #: The forge-side capability threshold for the runner's `backlog answer` apply
462
462
  #: surface: at or above this rauf version the recovery procedure applies answers
463
463
  #: via `rauf backlog answer`; below it, it degrades to `rauf backlog unblock`.
464
- #: It never hard-fails recovery, and it is NOT ``loopRunner.minRunnerVersion``
465
- #: (the install floor in references/forge-config-schema.json, which stays 0.6.0).
464
+ #: It never hard-fails recovery. As of the 0.14.0 floor bump it now COINCIDES with
465
+ #: ``loopRunner.minRunnerVersion`` (the install floor in
466
+ #: references/forge-config-schema.json, also 0.14.0) — the launch gate was raised so
467
+ #: it no longer green-lights a runner too old to inject a recovered answer — but the
468
+ #: two remain conceptually distinct: this one selects the recovery apply surface and
469
+ #: only degrades, while the install floor hard-stops the launch.
466
470
  RECOVERY_MIN_RUNNER_VERSION: Final[str] = "0.14.0"
467
471
  #: The fixed final line of the NEXT-STEPS block. The stamp instructs the skill
468
472
  #: to print the block verbatim as its absolute last output — nothing after this.
@@ -5694,8 +5698,9 @@ def _verify_result_entry(
5694
5698
  the one status that carries prior state forward — the report metadata — and it
5695
5699
  deliberately writes no ``verifiedStageVersion``: fixes landed, nothing
5696
5700
  re-verified them, so freshness stays unresolved until a later ``passed``.
5697
- ``passed`` may record NEW attached-report metadata of its own (the
5698
- advisory-only and escalation-acceptance rules in ``cmd_state_verify``).
5701
+ ``passed`` may record NEW attached-report metadata of its own (a clean report,
5702
+ an advisory-only report, or the escalation-acceptance rules in
5703
+ ``cmd_state_verify``).
5699
5704
 
5700
5705
  Args:
5701
5706
  status: The validated result status.
@@ -5719,11 +5724,11 @@ def _verify_result_entry(
5719
5724
  if status == "passed":
5720
5725
  entry: dict = {"status": status}
5721
5726
  if findings_file is not None:
5722
- # An attached report — advisory-only, or residual findings the user
5723
- # explicitly accepted at the escalation gate — resolves as `passed`
5724
- # so it never routes to forge-fix, while the report stays attached
5725
- # for later pickup. A bare zero count records no report keys, keeping
5726
- # the plain "verified clean" shape byte-identical to before.
5727
+ # An attached clean/advisory report, or residual findings the user
5728
+ # explicitly accepted at the escalation gate, resolves as `passed`
5729
+ # so it never routes to forge-fix while the audit artifact stays
5730
+ # attached. A bare zero count records no report keys, preserving the
5731
+ # legacy plain "verified clean" shape.
5727
5732
  entry["findingsFile"] = findings_file
5728
5733
  entry["findingsCount"] = findings_count
5729
5734
  entry["verifiedAt"] = now
@@ -5779,15 +5784,19 @@ def cmd_state_verify(
5779
5784
  write, so a contradictory call never lands a partial entry:
5780
5785
 
5781
5786
  - `passed` — REQUIRES `verified_stage_version`. MAY carry an attached
5782
- report (`findings_file` + `findings_count` together, count >= 1) in
5783
- two protocol cases: an ADVISORY-ONLY report (no blocking
5784
- `error`/`gap` findings), and residual findings the user explicitly
5785
- ACCEPTED at the round-ledger escalation (recorded first as a
5786
- `state-decision`; see "Escalation" in stage-exit-protocol.md).
5787
- Either way the stage resolves without routing to forge-fix and the
5788
- report stays attached. Half a pairing is refused: a file without a
5789
- count, a positive count without a file, or a file with a zero
5790
- count. Unaccepted blocking findings belong to `findings-reported`.
5787
+ report (`findings_file` + `findings_count` together, count >= 0) in
5788
+ three protocol cases: a CLEAN zero-finding round report (a fix
5789
+ pass's re-verify — valid audit evidence that lets the stage advance
5790
+ instead of stranding at `findings-applied`, #237), an ADVISORY-ONLY
5791
+ report (no blocking `error`/`gap` findings, count >= 1), and
5792
+ residual findings the user explicitly ACCEPTED at the round-ledger
5793
+ escalation (recorded first as a `state-decision`; see "Escalation"
5794
+ in stage-exit-protocol.md). In every case the stage resolves
5795
+ without routing to forge-fix and the report stays attached. Half a
5796
+ pairing is refused: a file without a count, or a positive count
5797
+ without a file. A bare `passed` (neither flag) is also accepted and
5798
+ records the report-free clean shape. Unaccepted blocking findings
5799
+ belong to `findings-reported`.
5791
5800
  - `findings-reported` — REQUIRES all three of `verified_stage_version`,
5792
5801
  `findings_file`, and a non-negative `findings_count`.
5793
5802
  - `findings-applied` — REFUSES `verified_stage_version`. Applying fixes
@@ -5914,8 +5923,9 @@ def cmd_state_verify(
5914
5923
  elif status == "passed":
5915
5924
  if findings_file is not None and findings_count is None:
5916
5925
  raise UsageError(
5917
- "--status passed with an advisory --findings-file requires "
5918
- "--findings-count N (the number of advisory findings it lists)"
5926
+ "--status passed with --findings-file requires --findings-count N "
5927
+ "(zero for a clean report, or the number of advisory/residual "
5928
+ "findings it lists)"
5919
5929
  )
5920
5930
  if findings_count is not None:
5921
5931
  if findings_count < 0:
@@ -5929,12 +5939,6 @@ def cmd_state_verify(
5929
5939
  f"report to read is unrecoverable. Blocking findings belong to "
5930
5940
  f"--status findings-reported instead."
5931
5941
  )
5932
- if findings_count == 0 and findings_file is not None:
5933
- raise UsageError(
5934
- "--status passed with --findings-file requires --findings-count "
5935
- ">= 1: an attached report claiming zero findings is "
5936
- "self-contradictory — omit both for a clean pass"
5937
- )
5938
5942
  if verified_stage_version is None:
5939
5943
  raise UsageError(
5940
5944
  "--status passed requires --verified-stage-version <current version>"
@@ -247,4 +247,7 @@ This Pi bundle preserves Claude's `AskUserQuestion` references because it ships
247
247
  - **User input:** use `AskUserQuestion` for genuine user decisions. It supports multiple questions, option descriptions, recommended ordering, multi-select, previews, and free-form Other/custom answers.
248
248
  - **Skill dispatch:** Pi uses `/skill:<name>` commands. If you cannot invoke a skill directly, print the exact `/skill:<name> ...` command for the user to run.
249
249
  - **Subagents:** this bundle declares its custom agents (`forge-researcher`, `forge-spec-writer`, `forge-verifier`) as package agents. If a `subagent` tool is registered, dispatch one with `{ agent: "forge-verifier", task: "..." }`, or fan several out concurrently with `{ tasks: [{ agent: "forge-spec-writer", task: "..." }, ...] }`. If no `subagent` tool is available, run that step inline yourself.
250
- - **Background / monitoring:** run long-lived commands in the foreground and report progress as it arrives.
250
+ - **Background / monitoring (forge-5-loop):** Pi has no built-in background bash, persistent monitor, or push-notification, so do **not** run the loop runner in the foreground and do **not** try to arm one. This bundle registers a **forge-loop-supervisor** extension that IS the "background-execution mechanism" and "monitoring mechanism" Steps 3b–3f refer to. Concretely:
251
+ - **Launch (Step 3b):** call **`forge_loop_launch`** with the backlog dir (and `review` / `agent` / `iterations` as resolved from config). It starts the loop **detached** — it runs in rauf's server and outlives this session — and returns immediately; you do not build or redirect a command yourself.
252
+ - **Supervise (Steps 3d–3f):** the extension then watches the runner's `events.ndjson` for you. It reports each completed item as one quiet line and **wakes this session automatically** on needs-human, blocked, stuck, review-failed, error, and completion — so do **not** arm a monitor, set a continuous tail, send a notification, poll, or foreground-sleep, and do not treat any manual stop as the terminal signal. When completion wakes you, go straight to Step 4 and read the authoritative counts with the status/list command. Use **`forge_loop_status`** to check progress on demand.
253
+ - **Stop / session end:** **`forge_loop_stop`** deliberately stops the runner; use it only when the user wants the loop to actually stop. Ending the Pi session does **not** stop the loop (it is detached), and the next session **reattaches automatically** without re-reporting what you already saw.
@@ -206,7 +206,7 @@ If a `state-*` verb exits 2, surface the plain `Error:` line from stderr verbati
206
206
 
207
207
  ### `state-verify` — verification results and provenance
208
208
 
209
- `state-verify` writes exactly one `stages.forge-verify-{token}` entry — the verification result for the production stage named by `--stage` — plus the top-level `updatedAt`, and nothing else. `--stage` takes the **served production stage** (`forge-0-epic` through `forge-5-loop`; `forge-6-docs` has no verification token and is rejected). Add `--epic "{epic}"` when the feature is an epic member — required, per the member rule above. Result mode passes `--status` (`auto-verify-pending`, `passed`, `findings-reported`, `findings-applied`, or `skipped`) with whatever `--findings-file`, `--findings-count`, and `--verified-stage-version` that status requires; contradictory metadata is refused before any write. `passed` may additionally carry `--findings-file` + `--findings-count` together for an **advisory-only** report (`inconsistency`/`improvement` findings only, per forge-verify's severity floor) — the stage resolves without a fix round and the report stays attached:
209
+ `state-verify` writes exactly one `stages.forge-verify-{token}` entry — the verification result for the production stage named by `--stage` — plus the top-level `updatedAt`, and nothing else. `--stage` takes the **served production stage** (`forge-0-epic` through `forge-5-loop`; `forge-6-docs` has no verification token and is rejected). Add `--epic "{epic}"` when the feature is an epic member — required, per the member rule above. Result mode passes `--status` (`auto-verify-pending`, `passed`, `findings-reported`, `findings-applied`, or `skipped`) with whatever `--findings-file`, `--findings-count`, and `--verified-stage-version` that status requires; contradictory metadata is refused before any write. `passed` may carry `--findings-file` + `--findings-count` for a clean report (count `0`), an **advisory-only** report (`inconsistency`/`improvement` findings only), or accepted residual findings. The stage resolves without a fix round and the report stays attached, making a completed verification round directly resumable if provenance recording is interrupted. A bare `passed` remains accepted for backward compatibility:
210
210
 
211
211
  **`auto-verify-pending` is not a skill-facing status.** It is written by `stage-exit`'s scheduling boundary, which records the debt automatically when auto-verify is effective for a stage. The value is accepted on this CLI so the entry stays inspectable and repairable, not so a skill can hand-schedule verification: no skill body and no reference passes it, and none should. Every other status in the list is the recorded *result* of a verification that ran (or was explicitly skipped); this one records that one was *owed*.
212
212