@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.
- package/README.md +1 -1
- package/adapters/claude/.feature-forge-bundle.json +1 -1
- package/adapters/claude/references/forge-config-schema.json +4 -4
- package/adapters/claude/references/ralph-loop-contract.md +12 -8
- package/adapters/claude/references/shared-conventions.md +1 -1
- package/adapters/claude/scripts/forge-session.py +30 -26
- package/adapters/claude/skills/forge/references/shared-conventions.md +1 -1
- package/adapters/claude/skills/forge-0-epic/references/shared-conventions.md +1 -1
- package/adapters/claude/skills/forge-1-prd/references/shared-conventions.md +1 -1
- package/adapters/claude/skills/forge-2-tech/references/shared-conventions.md +1 -1
- package/adapters/claude/skills/forge-3-specs/references/shared-conventions.md +1 -1
- package/adapters/claude/skills/forge-4-backlog/references/shared-conventions.md +1 -1
- package/adapters/claude/skills/forge-5-loop/SKILL.md +3 -3
- package/adapters/claude/skills/forge-5-loop/references/ralph-loop-contract.md +12 -8
- package/adapters/claude/skills/forge-5-loop/references/runner-contract.md +2 -2
- package/adapters/claude/skills/forge-5-loop/references/shared-conventions.md +1 -1
- package/adapters/claude/skills/forge-6-docs/references/shared-conventions.md +1 -1
- package/adapters/claude/skills/forge-fix/references/shared-conventions.md +1 -1
- package/adapters/claude/skills/forge-guide/references/forge-config-schema.json +4 -4
- package/adapters/claude/skills/forge-guide/references/ralph-loop-contract.md +12 -8
- package/adapters/claude/skills/forge-guide/references/shared-conventions.md +1 -1
- package/adapters/claude/skills/forge-verify/SKILL.md +5 -3
- package/adapters/claude/skills/forge-verify/references/findings-template.md +6 -6
- package/adapters/claude/skills/forge-verify/references/shared-conventions.md +1 -1
- package/adapters/codex/.feature-forge-bundle.json +1 -1
- package/adapters/codex/references/forge-config-schema.json +4 -4
- package/adapters/codex/references/ralph-loop-contract.md +12 -8
- package/adapters/codex/references/shared-conventions.md +1 -1
- package/adapters/codex/scripts/forge-session.py +30 -26
- package/adapters/codex/skills/forge/references/shared-conventions.md +1 -1
- package/adapters/codex/skills/forge-0-epic/references/shared-conventions.md +1 -1
- package/adapters/codex/skills/forge-1-prd/references/shared-conventions.md +1 -1
- package/adapters/codex/skills/forge-2-tech/references/shared-conventions.md +1 -1
- package/adapters/codex/skills/forge-3-specs/references/shared-conventions.md +1 -1
- package/adapters/codex/skills/forge-4-backlog/references/shared-conventions.md +1 -1
- package/adapters/codex/skills/forge-5-loop/SKILL.md +3 -3
- package/adapters/codex/skills/forge-5-loop/references/ralph-loop-contract.md +12 -8
- package/adapters/codex/skills/forge-5-loop/references/runner-contract.md +2 -2
- package/adapters/codex/skills/forge-5-loop/references/shared-conventions.md +1 -1
- package/adapters/codex/skills/forge-6-docs/references/shared-conventions.md +1 -1
- package/adapters/codex/skills/forge-fix/references/shared-conventions.md +1 -1
- package/adapters/codex/skills/forge-guide/references/forge-config-schema.json +4 -4
- package/adapters/codex/skills/forge-guide/references/ralph-loop-contract.md +12 -8
- package/adapters/codex/skills/forge-guide/references/shared-conventions.md +1 -1
- package/adapters/codex/skills/forge-verify/SKILL.md +5 -3
- package/adapters/codex/skills/forge-verify/references/findings-template.md +6 -6
- package/adapters/codex/skills/forge-verify/references/shared-conventions.md +1 -1
- package/adapters/copilot/.feature-forge-bundle.json +1 -1
- package/adapters/copilot/references/forge-config-schema.json +4 -4
- package/adapters/copilot/references/ralph-loop-contract.md +12 -8
- package/adapters/copilot/references/shared-conventions.md +1 -1
- package/adapters/copilot/scripts/forge-session.py +30 -26
- package/adapters/copilot/skills/forge/references/shared-conventions.md +1 -1
- package/adapters/copilot/skills/forge-0-epic/references/shared-conventions.md +1 -1
- package/adapters/copilot/skills/forge-1-prd/references/shared-conventions.md +1 -1
- package/adapters/copilot/skills/forge-2-tech/references/shared-conventions.md +1 -1
- package/adapters/copilot/skills/forge-3-specs/references/shared-conventions.md +1 -1
- package/adapters/copilot/skills/forge-4-backlog/references/shared-conventions.md +1 -1
- package/adapters/copilot/skills/forge-5-loop/forge-5-loop.md +3 -3
- package/adapters/copilot/skills/forge-5-loop/references/ralph-loop-contract.md +12 -8
- package/adapters/copilot/skills/forge-5-loop/references/runner-contract.md +2 -2
- package/adapters/copilot/skills/forge-5-loop/references/shared-conventions.md +1 -1
- package/adapters/copilot/skills/forge-6-docs/references/shared-conventions.md +1 -1
- package/adapters/copilot/skills/forge-fix/references/shared-conventions.md +1 -1
- package/adapters/copilot/skills/forge-guide/references/forge-config-schema.json +4 -4
- package/adapters/copilot/skills/forge-guide/references/ralph-loop-contract.md +12 -8
- package/adapters/copilot/skills/forge-guide/references/shared-conventions.md +1 -1
- package/adapters/copilot/skills/forge-verify/forge-verify.md +5 -3
- package/adapters/copilot/skills/forge-verify/references/findings-template.md +6 -6
- package/adapters/copilot/skills/forge-verify/references/shared-conventions.md +1 -1
- package/adapters/cursor/.feature-forge-bundle.json +1 -1
- package/adapters/cursor/references/forge-config-schema.json +4 -4
- package/adapters/cursor/references/ralph-loop-contract.md +12 -8
- package/adapters/cursor/references/shared-conventions.md +1 -1
- package/adapters/cursor/scripts/forge-session.py +30 -26
- package/adapters/cursor/skills/forge/references/shared-conventions.md +1 -1
- package/adapters/cursor/skills/forge-0-epic/references/shared-conventions.md +1 -1
- package/adapters/cursor/skills/forge-1-prd/references/shared-conventions.md +1 -1
- package/adapters/cursor/skills/forge-2-tech/references/shared-conventions.md +1 -1
- package/adapters/cursor/skills/forge-3-specs/references/shared-conventions.md +1 -1
- package/adapters/cursor/skills/forge-4-backlog/references/shared-conventions.md +1 -1
- package/adapters/cursor/skills/forge-5-loop/forge-5-loop.mdc +3 -3
- package/adapters/cursor/skills/forge-5-loop/references/ralph-loop-contract.md +12 -8
- package/adapters/cursor/skills/forge-5-loop/references/runner-contract.md +2 -2
- package/adapters/cursor/skills/forge-5-loop/references/shared-conventions.md +1 -1
- package/adapters/cursor/skills/forge-6-docs/references/shared-conventions.md +1 -1
- package/adapters/cursor/skills/forge-fix/references/shared-conventions.md +1 -1
- package/adapters/cursor/skills/forge-guide/references/forge-config-schema.json +4 -4
- package/adapters/cursor/skills/forge-guide/references/ralph-loop-contract.md +12 -8
- package/adapters/cursor/skills/forge-guide/references/shared-conventions.md +1 -1
- package/adapters/cursor/skills/forge-verify/forge-verify.mdc +5 -3
- package/adapters/cursor/skills/forge-verify/references/findings-template.md +6 -6
- package/adapters/cursor/skills/forge-verify/references/shared-conventions.md +1 -1
- package/adapters/gemini/.feature-forge-bundle.json +1 -1
- package/adapters/gemini/gemini-extension.json +1 -1
- package/adapters/gemini/references/forge-config-schema.json +4 -4
- package/adapters/gemini/references/ralph-loop-contract.md +12 -8
- package/adapters/gemini/references/shared-conventions.md +1 -1
- package/adapters/gemini/scripts/forge-session.py +30 -26
- package/adapters/gemini/skills/forge/references/shared-conventions.md +1 -1
- package/adapters/gemini/skills/forge-0-epic/references/shared-conventions.md +1 -1
- package/adapters/gemini/skills/forge-1-prd/references/shared-conventions.md +1 -1
- package/adapters/gemini/skills/forge-2-tech/references/shared-conventions.md +1 -1
- package/adapters/gemini/skills/forge-3-specs/references/shared-conventions.md +1 -1
- package/adapters/gemini/skills/forge-4-backlog/references/shared-conventions.md +1 -1
- package/adapters/gemini/skills/forge-5-loop/forge-5-loop.md +3 -3
- package/adapters/gemini/skills/forge-5-loop/references/ralph-loop-contract.md +12 -8
- package/adapters/gemini/skills/forge-5-loop/references/runner-contract.md +2 -2
- package/adapters/gemini/skills/forge-5-loop/references/shared-conventions.md +1 -1
- package/adapters/gemini/skills/forge-6-docs/references/shared-conventions.md +1 -1
- package/adapters/gemini/skills/forge-fix/references/shared-conventions.md +1 -1
- package/adapters/gemini/skills/forge-guide/references/forge-config-schema.json +4 -4
- package/adapters/gemini/skills/forge-guide/references/ralph-loop-contract.md +12 -8
- package/adapters/gemini/skills/forge-guide/references/shared-conventions.md +1 -1
- package/adapters/gemini/skills/forge-verify/forge-verify.md +5 -3
- package/adapters/gemini/skills/forge-verify/references/findings-template.md +6 -6
- package/adapters/gemini/skills/forge-verify/references/shared-conventions.md +1 -1
- package/adapters/pi/.feature-forge-bundle.json +1 -1
- package/adapters/pi/extensions/forge-loop-supervisor/events.ts +90 -0
- package/adapters/pi/extensions/forge-loop-supervisor/index.ts +114 -0
- package/adapters/pi/extensions/forge-loop-supervisor/registry.ts +137 -0
- package/adapters/pi/extensions/forge-loop-supervisor/supervisor.ts +185 -0
- package/adapters/pi/extensions/forge-loop-supervisor/tailer.ts +137 -0
- package/adapters/pi/extensions/forge-loop-supervisor/types.ts +87 -0
- package/adapters/pi/extensions/forge-loop-supervisor/wiring.ts +423 -0
- package/adapters/pi/package.json +2 -1
- package/adapters/pi/references/forge-config-schema.json +4 -4
- package/adapters/pi/references/ralph-loop-contract.md +12 -8
- package/adapters/pi/references/shared-conventions.md +1 -1
- package/adapters/pi/scripts/forge-session.py +30 -26
- package/adapters/pi/skills/forge/SKILL.md +4 -1
- package/adapters/pi/skills/forge/references/shared-conventions.md +1 -1
- package/adapters/pi/skills/forge-0-epic/SKILL.md +4 -1
- package/adapters/pi/skills/forge-0-epic/references/shared-conventions.md +1 -1
- package/adapters/pi/skills/forge-1-prd/SKILL.md +4 -1
- package/adapters/pi/skills/forge-1-prd/references/shared-conventions.md +1 -1
- package/adapters/pi/skills/forge-2-tech/SKILL.md +4 -1
- package/adapters/pi/skills/forge-2-tech/references/shared-conventions.md +1 -1
- package/adapters/pi/skills/forge-3-specs/SKILL.md +4 -1
- package/adapters/pi/skills/forge-3-specs/references/shared-conventions.md +1 -1
- package/adapters/pi/skills/forge-4-backlog/SKILL.md +4 -1
- package/adapters/pi/skills/forge-4-backlog/references/shared-conventions.md +1 -1
- package/adapters/pi/skills/forge-5-loop/SKILL.md +14 -8
- package/adapters/pi/skills/forge-5-loop/references/ralph-loop-contract.md +12 -8
- package/adapters/pi/skills/forge-5-loop/references/runner-contract.md +8 -5
- package/adapters/pi/skills/forge-5-loop/references/shared-conventions.md +1 -1
- package/adapters/pi/skills/forge-6-docs/SKILL.md +4 -1
- package/adapters/pi/skills/forge-6-docs/references/shared-conventions.md +1 -1
- package/adapters/pi/skills/forge-bootstrap/SKILL.md +4 -1
- package/adapters/pi/skills/forge-fix/SKILL.md +4 -1
- package/adapters/pi/skills/forge-fix/references/shared-conventions.md +1 -1
- package/adapters/pi/skills/forge-guide/SKILL.md +4 -1
- package/adapters/pi/skills/forge-guide/references/forge-config-schema.json +4 -4
- package/adapters/pi/skills/forge-guide/references/ralph-loop-contract.md +12 -8
- package/adapters/pi/skills/forge-guide/references/shared-conventions.md +1 -1
- package/adapters/pi/skills/forge-init/SKILL.md +4 -1
- package/adapters/pi/skills/forge-verify/SKILL.md +9 -4
- package/adapters/pi/skills/forge-verify/references/findings-template.md +6 -6
- package/adapters/pi/skills/forge-verify/references/shared-conventions.md +1 -1
- package/dist/manifest.d.ts +1 -1
- package/dist/rauf.d.ts +3 -3
- package/dist/rauf.js +2 -2
- package/dist/types.d.ts +1 -1
- 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
|
+
}
|
package/adapters/pi/package.json
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
249
|
-
"description": "Minimum runner version (semver). 0.
|
|
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
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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 **
|
|
214
|
-
|
|
215
|
-
|
|
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
|
|
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
|
|
465
|
-
#: (the install floor in
|
|
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 (
|
|
5698
|
-
advisory-only
|
|
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
|
|
5723
|
-
# explicitly accepted at the escalation gate
|
|
5724
|
-
# so it never routes to forge-fix
|
|
5725
|
-
#
|
|
5726
|
-
#
|
|
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 >=
|
|
5783
|
-
|
|
5784
|
-
|
|
5785
|
-
|
|
5786
|
-
`
|
|
5787
|
-
|
|
5788
|
-
|
|
5789
|
-
|
|
5790
|
-
|
|
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
|
|
5918
|
-
"
|
|
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:**
|
|
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
|
|
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
|
|