@garygentry/feature-forge 0.3.4 → 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 +57 -32
- 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 +57 -32
- 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 +57 -32
- 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 +57 -32
- 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 +57 -32
- 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 +57 -32
- 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
|
@@ -131,10 +131,10 @@ violate REQ-STATE-02; per-feature status is always derived live from each member
|
|
|
131
131
|
`.pipeline-state.json`).
|
|
132
132
|
|
|
133
133
|
Set `stages.forge-verify-epic.status` to `findings-reported` when the report lists at
|
|
134
|
-
least one **blocking** finding (`error`/`gap`), else `passed
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
`
|
|
134
|
+
least one **blocking** finding (`error`/`gap`), else `passed`. Always attach the report
|
|
135
|
+
and its total count, including count `0` for a clean report, exactly as in feature mode
|
|
136
|
+
(the severity floor in `skills/forge-verify/SKILL.md`) — recording `findingsFile`,
|
|
137
|
+
`findingsCount`, and `verifiedAt`.
|
|
138
138
|
|
|
139
139
|
**Write it with `state-verify`, never by hand.** `--stage forge-0-epic` is the sanctioned
|
|
140
140
|
epic writer: it creates the file lazily, mutates only `stages.forge-verify-epic` plus the
|
|
@@ -149,8 +149,8 @@ R="$(bash -c 'for d in "${FEATURE_FORGE_ROOT:-}" "$HOME"/.claude/skills/feature-
|
|
|
149
149
|
[ -n "$R" ] || { echo "feature-forge: cannot locate plugin root" >&2; exit 1; }
|
|
150
150
|
python3 "$R/scripts/forge-session.py" state-verify \
|
|
151
151
|
--feature "{epic}" --stage forge-0-epic \
|
|
152
|
-
--status "{
|
|
153
|
-
--findings-file "{relative findings path}" --findings-count {
|
|
152
|
+
--status "{outcome-specific status}" \
|
|
153
|
+
--findings-file "{relative findings path}" --findings-count {outcome-specific count} \
|
|
154
154
|
--verified-stage-version {manifest revision} --specs-dir "{specsDir}"
|
|
155
155
|
```
|
|
156
156
|
|
|
@@ -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
|
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// GENERATED — DO NOT EDIT. Source: adapter-src/pi/extensions/forge-loop-supervisor/events.ts
|
|
2
|
+
// Regenerate with: python3 scripts/build-adapters.py
|
|
3
|
+
/**
|
|
4
|
+
* Event classification + display formatting — pure functions over one rauf event.
|
|
5
|
+
*
|
|
6
|
+
* The classification mirrors the coverage-complete filter the generic
|
|
7
|
+
* forge-5-loop contract arms on Claude (`Monitor` on events.ndjson): every
|
|
8
|
+
* terminal and exception state is surfaced (silence is never success), while
|
|
9
|
+
* routine milestones stay quiet. See rauf's event union in
|
|
10
|
+
* `packages/core/src/schemas.ts` for the authoritative field list.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { EventClass, RaufEvent } from "./types.js";
|
|
14
|
+
|
|
15
|
+
/** Events that mean "the run is over" — the supervisor wakes the session so the
|
|
16
|
+
* loop stage runs its post-run close-out (Steps 4–7), then stops watching. */
|
|
17
|
+
const TERMINAL = new Set(["loop_completed", "loop_error", "loop_cancelled"]);
|
|
18
|
+
|
|
19
|
+
/** Events a human must see immediately. `loop_paused` carries `reason` and only
|
|
20
|
+
* the needs-human pause is surfaced here (the runner sets the item aside and
|
|
21
|
+
* keeps going — it is not a full stop). Everything else in this set is an
|
|
22
|
+
* unconditional exception. */
|
|
23
|
+
const EXCEPTION = new Set([
|
|
24
|
+
"needs_human",
|
|
25
|
+
"item_blocked",
|
|
26
|
+
"llm_stuck_warning",
|
|
27
|
+
"review_failed",
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
/** The single routine milestone that earns a quiet progress line. */
|
|
31
|
+
const PROGRESS = new Set(["item_completed"]);
|
|
32
|
+
|
|
33
|
+
/** Classify one event. Unknown/interior/firehose types (token updates, tool
|
|
34
|
+
* activity, iteration boundaries, review start, usage-limit chatter, …) are
|
|
35
|
+
* `ignore` — the supervisor never surfaces them. */
|
|
36
|
+
export function classifyEvent(evt: RaufEvent): EventClass {
|
|
37
|
+
const type = evt?.type;
|
|
38
|
+
if (typeof type !== "string") return "ignore";
|
|
39
|
+
if (TERMINAL.has(type)) return "terminal";
|
|
40
|
+
if (EXCEPTION.has(type)) return "exception";
|
|
41
|
+
// `loop_paused` is an exception ONLY when it is the needs-human pause.
|
|
42
|
+
if (type === "loop_paused" && evt.reason === "needs_human") return "exception";
|
|
43
|
+
if (PROGRESS.has(type)) return "progress";
|
|
44
|
+
return "ignore";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** A concise, deterministic one-line report for a routine `item_completed`.
|
|
48
|
+
* `done`/`total` are threaded in when known so the line reads `[3/10] …`. */
|
|
49
|
+
export function formatProgress(
|
|
50
|
+
evt: RaufEvent,
|
|
51
|
+
done?: number,
|
|
52
|
+
total?: number,
|
|
53
|
+
): string {
|
|
54
|
+
const count =
|
|
55
|
+
typeof done === "number" && typeof total === "number" ? `[${done}/${total}] ` : "";
|
|
56
|
+
const title = typeof evt.title === "string" && evt.title ? evt.title : evt.itemId ?? "item";
|
|
57
|
+
return `${count}forge loop: completed ${title}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** A human-facing line for an exception or terminal event, used both for the
|
|
61
|
+
* toast and for the wake message that triggers the session's next turn. */
|
|
62
|
+
export function formatSignal(evt: RaufEvent): string {
|
|
63
|
+
switch (evt.type) {
|
|
64
|
+
case "needs_human":
|
|
65
|
+
return `forge loop: item ${evt.itemId ?? "?"} needs a human — ${evt.reason ?? "no reason given"}`;
|
|
66
|
+
case "loop_paused":
|
|
67
|
+
return `forge loop: paused for human input on ${evt.itemId ?? "an item"}`;
|
|
68
|
+
case "item_blocked":
|
|
69
|
+
return `forge loop: item ${evt.itemId ?? "?"} blocked — ${evt.reason ?? "no reason given"}`;
|
|
70
|
+
case "llm_stuck_warning":
|
|
71
|
+
return `forge loop: item ${evt.itemId ?? "?"} looks stuck (no output for ${Math.round((evt.silentMs ?? 0) / 1000)}s)`;
|
|
72
|
+
case "review_failed":
|
|
73
|
+
return `forge loop: review failed — ${evt.reason ?? "no reason given"}`;
|
|
74
|
+
case "loop_error":
|
|
75
|
+
return `forge loop: the run errored — ${evt.reason ?? "see the runner log"}`;
|
|
76
|
+
case "loop_cancelled":
|
|
77
|
+
return "forge loop: the run was cancelled";
|
|
78
|
+
case "loop_completed": {
|
|
79
|
+
const done = evt.completedCount ?? 0;
|
|
80
|
+
const blocked = evt.blockedCount ?? 0;
|
|
81
|
+
const needsHuman = evt.needsHumanCount ?? 0;
|
|
82
|
+
const parts = [`${done} done`];
|
|
83
|
+
if (blocked) parts.push(`${blocked} blocked`);
|
|
84
|
+
if (needsHuman) parts.push(`${needsHuman} need a human`);
|
|
85
|
+
return `forge loop: run complete — ${parts.join(", ")}. Read the authoritative counts (status --json) and run the post-run close-out.`;
|
|
86
|
+
}
|
|
87
|
+
default:
|
|
88
|
+
return `forge loop: ${evt.type}`;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// GENERATED — DO NOT EDIT. Source: adapter-src/pi/extensions/forge-loop-supervisor/index.ts
|
|
2
|
+
// Regenerate with: python3 scripts/build-adapters.py
|
|
3
|
+
/**
|
|
4
|
+
* forge-loop-supervisor — a first-party Pi extension that lets forge-5-loop run
|
|
5
|
+
* the rauf autonomous coding loop WITHOUT blocking the Pi session, and supervises
|
|
6
|
+
* its native event stream.
|
|
7
|
+
*
|
|
8
|
+
* Pi has no built-in background bash, persistent monitor, or push-notification
|
|
9
|
+
* surface, so the generic Claude-first forge-5-loop contract (background the
|
|
10
|
+
* process, arm a `Monitor` on events.ndjson, `PushNotification` on exceptions)
|
|
11
|
+
* cannot be followed literally on Pi. This extension provides the real mechanism:
|
|
12
|
+
* `forge_loop_launch` starts the runner detached (it runs in rauf's server and
|
|
13
|
+
* outlives the session), then a rotation-aware NDJSON watcher turns each
|
|
14
|
+
* `item_completed` into a quiet progress line and wakes the session — via
|
|
15
|
+
* `pi.sendMessage(..., { triggerTurn: true })` — only on needs-human / blocked /
|
|
16
|
+
* stuck / review-failed / error / completion. `forge_loop_status` and
|
|
17
|
+
* `forge_loop_stop` round out the launch/attach/status/stop surface. Task
|
|
18
|
+
* identity is persisted (session entry + a file mirror beside the runner state)
|
|
19
|
+
* so a restarted session reattaches without duplicate reporting, and shutdown
|
|
20
|
+
* tears down watchers only — never the runner.
|
|
21
|
+
*
|
|
22
|
+
* The pi-facing glue is thin: all logic lives in the injectable {@link
|
|
23
|
+
* createExtension} factory (see wiring.ts), so the extension is unit-tested with
|
|
24
|
+
* a fake pi and fake deps. This file supplies the production dependencies.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
28
|
+
import { spawn } from "node:child_process";
|
|
29
|
+
import { watch as fsWatch } from "node:fs";
|
|
30
|
+
import { basename, dirname } from "node:path";
|
|
31
|
+
|
|
32
|
+
import { createExtension, type Deps, type PiLike, type WatchHandle } from "./wiring.js";
|
|
33
|
+
|
|
34
|
+
/** How often the backstop poll fires (ms). fs.watch can miss events or drop on
|
|
35
|
+
* some platforms; a low-frequency interval guarantees the tail keeps up without
|
|
36
|
+
* busy-waiting. */
|
|
37
|
+
const BACKSTOP_MS = 2000;
|
|
38
|
+
/** Debounce window (ms) coalescing a burst of fs.watch events into one poll. */
|
|
39
|
+
const DEBOUNCE_MS = 120;
|
|
40
|
+
|
|
41
|
+
const productionDeps: Deps = {
|
|
42
|
+
spawnDetached(bin, args, cwd, onError) {
|
|
43
|
+
// detached + unref + ignored stdio: the child is fully decoupled from the
|
|
44
|
+
// Pi process and survives session shutdown. rauf hands the loop to its
|
|
45
|
+
// server and this launcher exits quickly; a non-self-detaching runner still
|
|
46
|
+
// stays off the session.
|
|
47
|
+
const child = spawn(bin, args, { cwd, detached: true, stdio: "ignore" });
|
|
48
|
+
// ENOENT (bad bin) and other spawn failures arrive here asynchronously —
|
|
49
|
+
// report them so a launch that never happened does not look successful.
|
|
50
|
+
child.on("error", (err) => onError?.(err instanceof Error ? err.message : String(err)));
|
|
51
|
+
child.unref();
|
|
52
|
+
},
|
|
53
|
+
watch(filePath, onChange): WatchHandle {
|
|
54
|
+
const dir = dirname(filePath);
|
|
55
|
+
const base = basename(filePath);
|
|
56
|
+
let debounce: NodeJS.Timeout | null = null;
|
|
57
|
+
const fire = () => {
|
|
58
|
+
if (debounce) return;
|
|
59
|
+
debounce = setTimeout(() => {
|
|
60
|
+
debounce = null;
|
|
61
|
+
onChange();
|
|
62
|
+
}, DEBOUNCE_MS);
|
|
63
|
+
};
|
|
64
|
+
let fsw: ReturnType<typeof fsWatch> | null = null;
|
|
65
|
+
try {
|
|
66
|
+
// Watch the DIRECTORY (not the file) so rotation-by-rename — rauf moves
|
|
67
|
+
// events.ndjson into archive/ and recreates it each run — keeps firing.
|
|
68
|
+
fsw = fsWatch(dir, (_evt, name) => {
|
|
69
|
+
if (!name || name === base) fire();
|
|
70
|
+
});
|
|
71
|
+
fsw.on?.("error", () => {
|
|
72
|
+
/* directory vanished; the backstop interval keeps polling */
|
|
73
|
+
});
|
|
74
|
+
} catch {
|
|
75
|
+
fsw = null;
|
|
76
|
+
}
|
|
77
|
+
const interval = setInterval(onChange, BACKSTOP_MS);
|
|
78
|
+
return {
|
|
79
|
+
close() {
|
|
80
|
+
try {
|
|
81
|
+
fsw?.close();
|
|
82
|
+
} catch {
|
|
83
|
+
/* ignore */
|
|
84
|
+
}
|
|
85
|
+
if (debounce) clearTimeout(debounce);
|
|
86
|
+
clearInterval(interval);
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
},
|
|
90
|
+
now: () => new Date().toISOString(),
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export default function (pi: ExtensionAPI) {
|
|
94
|
+
// Adapt the real ExtensionAPI to the structural PiLike the wiring consumes.
|
|
95
|
+
// Each member is accessed by name off the concrete `pi`, so a method renamed
|
|
96
|
+
// or removed in a future pi is a COMPILE error here (not a silent runtime
|
|
97
|
+
// failure hidden behind a blanket cast) — while PiLike stays loose enough for
|
|
98
|
+
// the fake pi in tests. Argument casts bridge PiLike's minimal shapes to pi's
|
|
99
|
+
// stricter generic signatures; the pi-API contract this was built against
|
|
100
|
+
// pins the exact shapes.
|
|
101
|
+
const piLike: PiLike = {
|
|
102
|
+
registerTool: (def) => (pi.registerTool as (d: unknown) => void)(def),
|
|
103
|
+
on: (event, handler) =>
|
|
104
|
+
(pi.on as unknown as (e: string, h: (ev: unknown, ctx: unknown) => void | Promise<void>) => void)(event, handler),
|
|
105
|
+
sendMessage: (message, options) =>
|
|
106
|
+
pi.sendMessage(message as Parameters<typeof pi.sendMessage>[0], options),
|
|
107
|
+
appendEntry: (customType, data) => pi.appendEntry(customType, data),
|
|
108
|
+
exec:
|
|
109
|
+
typeof pi.exec === "function"
|
|
110
|
+
? (command, args, opts) => pi.exec(command, args, opts)
|
|
111
|
+
: undefined,
|
|
112
|
+
};
|
|
113
|
+
createExtension(piLike, productionDeps);
|
|
114
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// GENERATED — DO NOT EDIT. Source: adapter-src/pi/extensions/forge-loop-supervisor/registry.ts
|
|
2
|
+
// Regenerate with: python3 scripts/build-adapters.py
|
|
3
|
+
/**
|
|
4
|
+
* Durable task registry — a small JSON mirror of the supervised task, written
|
|
5
|
+
* beside the runner's own state so a brand-new Pi session (or a full pi restart,
|
|
6
|
+
* where the session file itself differs) can rediscover a loop it did not launch.
|
|
7
|
+
*
|
|
8
|
+
* Why a file and not only `pi.appendEntry`: appendEntry records survive session
|
|
9
|
+
* reload/branch within pi, but a cold restart may open a different session file.
|
|
10
|
+
* The mirror lives at `<stateDir>/.forge-supervisor.json`, next to rauf's own
|
|
11
|
+
* `state.json` / `events.ndjson`, so it is discoverable from the backlog dir
|
|
12
|
+
* alone. rauf's detached loop is server-managed (no single child pid to signal),
|
|
13
|
+
* so liveness is judged from the event stream and `rauf status --json`, never a
|
|
14
|
+
* tracked pid — the mirror deliberately stores no pid.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
existsSync,
|
|
19
|
+
mkdirSync,
|
|
20
|
+
readdirSync,
|
|
21
|
+
readFileSync,
|
|
22
|
+
renameSync,
|
|
23
|
+
rmSync,
|
|
24
|
+
statSync,
|
|
25
|
+
writeFileSync,
|
|
26
|
+
} from "node:fs";
|
|
27
|
+
import { dirname, join } from "node:path";
|
|
28
|
+
|
|
29
|
+
import type { SupervisorTask } from "./types.js";
|
|
30
|
+
|
|
31
|
+
/** The mirror filename, discoverable by scanning (see {@link discoverMirrors}). */
|
|
32
|
+
export const MIRROR_NAME = ".forge-supervisor.json";
|
|
33
|
+
|
|
34
|
+
/** Directory names never worth descending into during a mirror scan. */
|
|
35
|
+
const SKIP_DIRS = new Set(["node_modules", ".git", "dist", "archive", ".pi", ".claude"]);
|
|
36
|
+
/** How deep the scan descends from the project root — mirrors live at
|
|
37
|
+
* `<backlogDir>/.rauf/.forge-supervisor.json`, so a shallow bound reaches the
|
|
38
|
+
* common `specs/<feature>/.rauf/` layout without walking the whole tree. */
|
|
39
|
+
const SCAN_MAX_DEPTH = 5;
|
|
40
|
+
|
|
41
|
+
/** Path of the registry mirror for a runner state directory. */
|
|
42
|
+
export function mirrorPath(stateDir: string): string {
|
|
43
|
+
return join(stateDir, MIRROR_NAME);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Read the mirrored task for a state dir, or null if absent/unreadable/corrupt.
|
|
47
|
+
* A corrupt mirror is treated as "no task" rather than throwing — a half-written
|
|
48
|
+
* file must never wedge session startup. */
|
|
49
|
+
export function readMirror(stateDir: string): SupervisorTask | null {
|
|
50
|
+
const path = mirrorPath(stateDir);
|
|
51
|
+
if (!existsSync(path)) return null;
|
|
52
|
+
try {
|
|
53
|
+
const parsed = JSON.parse(readFileSync(path, "utf8")) as Partial<SupervisorTask>;
|
|
54
|
+
if (
|
|
55
|
+
parsed &&
|
|
56
|
+
typeof parsed.stateDir === "string" &&
|
|
57
|
+
typeof parsed.eventsFile === "string" &&
|
|
58
|
+
typeof parsed.backlogDir === "string"
|
|
59
|
+
) {
|
|
60
|
+
return {
|
|
61
|
+
backlogDir: parsed.backlogDir,
|
|
62
|
+
stateDir: parsed.stateDir,
|
|
63
|
+
eventsFile: parsed.eventsFile,
|
|
64
|
+
launchedAt: typeof parsed.launchedAt === "string" ? parsed.launchedAt : "",
|
|
65
|
+
total: typeof parsed.total === "number" ? parsed.total : undefined,
|
|
66
|
+
eventsIno: typeof parsed.eventsIno === "number" ? parsed.eventsIno : undefined,
|
|
67
|
+
lastSeq: typeof parsed.lastSeq === "number" ? parsed.lastSeq : -1,
|
|
68
|
+
closed: parsed.closed === true,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
} catch {
|
|
72
|
+
// fall through — corrupt mirror is "no task"
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Persist the task mirror atomically (write-temp + rename), creating the state
|
|
78
|
+
* dir if the launcher raced ahead of the runner. */
|
|
79
|
+
export function writeMirror(task: SupervisorTask): void {
|
|
80
|
+
const path = mirrorPath(task.stateDir);
|
|
81
|
+
try {
|
|
82
|
+
// Stamp the CURRENT events-file inode so a later session can tell whether the
|
|
83
|
+
// file was rotated (a new run) while it was away — see SupervisorTask.eventsIno.
|
|
84
|
+
let eventsIno = task.eventsIno;
|
|
85
|
+
try {
|
|
86
|
+
eventsIno = statSync(task.eventsFile).ino;
|
|
87
|
+
} catch {
|
|
88
|
+
// events file not present yet — keep whatever the task carried (if any)
|
|
89
|
+
}
|
|
90
|
+
const record: SupervisorTask = { ...task, eventsIno };
|
|
91
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
92
|
+
const tmp = `${path}.tmp-${process.pid}`;
|
|
93
|
+
writeFileSync(tmp, `${JSON.stringify(record, null, 2)}\n`, "utf8");
|
|
94
|
+
renameSync(tmp, path);
|
|
95
|
+
} catch {
|
|
96
|
+
// Best-effort durability; a failed mirror write must not break the tool.
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Remove the mirror (on an explicit stop of a task the session owns). */
|
|
101
|
+
export function clearMirror(stateDir: string): void {
|
|
102
|
+
try {
|
|
103
|
+
rmSync(mirrorPath(stateDir), { force: true });
|
|
104
|
+
} catch {
|
|
105
|
+
// best-effort
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Find every supervised-task mirror under `root`, so a BRAND-NEW Pi session (a
|
|
111
|
+
* fresh session file with no `forge-loop-task` entry of its own) can still
|
|
112
|
+
* rediscover and reattach to a loop a previous session launched. Bounded, cheap,
|
|
113
|
+
* and failure-tolerant: skips heavy directories, caps depth, and treats any
|
|
114
|
+
* unreadable dir or corrupt mirror as absent rather than throwing.
|
|
115
|
+
*/
|
|
116
|
+
export function discoverMirrors(root: string): SupervisorTask[] {
|
|
117
|
+
const found: SupervisorTask[] = [];
|
|
118
|
+
const walk = (dir: string, depth: number): void => {
|
|
119
|
+
if (depth > SCAN_MAX_DEPTH) return;
|
|
120
|
+
let entries: import("node:fs").Dirent[];
|
|
121
|
+
try {
|
|
122
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
123
|
+
} catch {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
for (const entry of entries) {
|
|
127
|
+
if (entry.isFile() && entry.name === MIRROR_NAME) {
|
|
128
|
+
const task = readMirror(dir); // mirror lives AT <stateDir>/<MIRROR_NAME>
|
|
129
|
+
if (task) found.push(task);
|
|
130
|
+
} else if (entry.isDirectory() && !SKIP_DIRS.has(entry.name)) {
|
|
131
|
+
walk(join(dir, entry.name), depth + 1);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
walk(root, 0);
|
|
136
|
+
return found;
|
|
137
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
// GENERATED — DO NOT EDIT. Source: adapter-src/pi/extensions/forge-loop-supervisor/supervisor.ts
|
|
2
|
+
// Regenerate with: python3 scripts/build-adapters.py
|
|
3
|
+
/**
|
|
4
|
+
* LoopSupervisor — the host-agnostic core that turns a stream of rauf events
|
|
5
|
+
* into the right user-facing behavior, with exactly-once reporting across
|
|
6
|
+
* session restarts.
|
|
7
|
+
*
|
|
8
|
+
* Reporting rule (issue #236):
|
|
9
|
+
* - routine `item_completed` → one quiet deterministic line, no model turn;
|
|
10
|
+
* - exception (`needs_human` / block / stuck / review-failed / error /
|
|
11
|
+
* cancellation) → notify AND wake the session;
|
|
12
|
+
* - terminal (`loop_completed` / error / cancelled) → wake the session so the
|
|
13
|
+
* loop stage runs its post-run close-out, then stop watching.
|
|
14
|
+
*
|
|
15
|
+
* Dedup + reattach: every rauf record carries a monotonic per-run `seq`. A task
|
|
16
|
+
* persists the highest `seq` it has already surfaced (`lastSeq`). When a fresh
|
|
17
|
+
* Pi session reattaches, its tailer re-reads events.ndjson from the top; records
|
|
18
|
+
* with `seq <= lastSeq` are REPLAYED SILENTLY — they still rebuild the in-memory
|
|
19
|
+
* `done` counter, but they are never re-notified — while records past `lastSeq`
|
|
20
|
+
* are surfaced live. That is what lets a restart reconcile a running loop without
|
|
21
|
+
* duplicate reports.
|
|
22
|
+
*
|
|
23
|
+
* Duplicate watchers are prevented by keying active tasks on their stateDir: a
|
|
24
|
+
* second `attach` for the same stateDir returns the existing handle.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { classifyEvent, formatProgress, formatSignal } from "./events.js";
|
|
28
|
+
import type { RaufEvent, SupervisorHost, SupervisorTask } from "./types.js";
|
|
29
|
+
|
|
30
|
+
/** A live watch handle the host drives: `poll()` on file change, `close()` on
|
|
31
|
+
* teardown. Reattach-safe and idempotent. */
|
|
32
|
+
export interface TaskHandle {
|
|
33
|
+
poll(): void;
|
|
34
|
+
close(): void;
|
|
35
|
+
readonly stateDir: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface ActiveTask {
|
|
39
|
+
task: SupervisorTask;
|
|
40
|
+
/** Running count of completed items, rebuilt from replayed history. */
|
|
41
|
+
done: number;
|
|
42
|
+
/** Highest item_completed seq already counted into `done` — guards the counter
|
|
43
|
+
* against a re-read double-count independently of the notify cursor. */
|
|
44
|
+
doneCursor: number;
|
|
45
|
+
/** Total backlog items, when the launcher recorded it (`task.total`). */
|
|
46
|
+
total?: number;
|
|
47
|
+
closed: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export class LoopSupervisor {
|
|
51
|
+
private readonly active = new Map<string, ActiveTask>();
|
|
52
|
+
|
|
53
|
+
constructor(private readonly host: SupervisorHost) {}
|
|
54
|
+
|
|
55
|
+
/** Whether a task for this stateDir is already being supervised. */
|
|
56
|
+
isActive(stateDir: string): boolean {
|
|
57
|
+
return this.active.has(stateDir);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Snapshot of the current progress for a task (for the status tool). */
|
|
61
|
+
progress(stateDir: string): { done: number; total?: number; closed: boolean } | null {
|
|
62
|
+
const a = this.active.get(stateDir);
|
|
63
|
+
return a ? { done: a.done, total: a.total, closed: a.closed } : null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Begin (or reattach) supervision of one task. The returned handle's `poll`
|
|
68
|
+
* feeds new NDJSON records through {@link handleRecord}; the host is expected
|
|
69
|
+
* to call it on file-change and once immediately (to replay history). A
|
|
70
|
+
* second attach for the same stateDir is a no-op that returns the live handle
|
|
71
|
+
* — the dedup guard against duplicate watchers.
|
|
72
|
+
*
|
|
73
|
+
* @param makeReader Builds a reader (typically an NdjsonTailer bound to
|
|
74
|
+
* `task.eventsFile`) whose `poll` dispatches each parsed record to `onRecord`.
|
|
75
|
+
*/
|
|
76
|
+
attach(
|
|
77
|
+
task: SupervisorTask,
|
|
78
|
+
makeReader: (
|
|
79
|
+
onRecord: (rec: RaufEvent) => void,
|
|
80
|
+
onRotate: () => void,
|
|
81
|
+
) => { poll(): void },
|
|
82
|
+
): TaskHandle {
|
|
83
|
+
const existing = this.active.get(task.stateDir);
|
|
84
|
+
if (existing) return this.handleFor(task.stateDir);
|
|
85
|
+
|
|
86
|
+
const entry: ActiveTask = {
|
|
87
|
+
task: { ...task },
|
|
88
|
+
done: 0,
|
|
89
|
+
doneCursor: -1,
|
|
90
|
+
total: task.total,
|
|
91
|
+
closed: task.closed,
|
|
92
|
+
};
|
|
93
|
+
this.active.set(task.stateDir, entry);
|
|
94
|
+
const reader = makeReader(
|
|
95
|
+
(rec) => this.handleRecord(task.stateDir, rec),
|
|
96
|
+
() => this.handleRotate(task.stateDir),
|
|
97
|
+
);
|
|
98
|
+
return {
|
|
99
|
+
stateDir: task.stateDir,
|
|
100
|
+
poll: () => reader.poll(),
|
|
101
|
+
close: () => this.detach(task.stateDir),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Stop tracking a task in memory (watcher teardown / stop). Does NOT touch
|
|
106
|
+
* the detached runner — the loop is server-owned and outlives the session. */
|
|
107
|
+
detach(stateDir: string): void {
|
|
108
|
+
this.active.delete(stateDir);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Process one parsed rauf record for a task: dedup, classify, dispatch. */
|
|
112
|
+
private handleRecord(stateDir: string, rec: RaufEvent): void {
|
|
113
|
+
const entry = this.active.get(stateDir);
|
|
114
|
+
if (!entry) return;
|
|
115
|
+
const seq = typeof rec.seq === "number" ? rec.seq : null;
|
|
116
|
+
const alreadySeen = seq !== null && seq <= entry.task.lastSeq;
|
|
117
|
+
const cls = classifyEvent(rec);
|
|
118
|
+
|
|
119
|
+
// Count completed items for the [N/M] line. Guard against double-counting a
|
|
120
|
+
// replayed item: only count when its seq is past the counter's cursor. On a
|
|
121
|
+
// fresh attach the cursor is -1 so the whole history counts; on a re-read of
|
|
122
|
+
// the same run (a spurious re-poll) already-counted items are skipped. A
|
|
123
|
+
// genuine new run arrives via handleRotate, which resets the cursor first.
|
|
124
|
+
if (rec.type === "item_completed") {
|
|
125
|
+
if (seq === null || seq > entry.doneCursor) {
|
|
126
|
+
entry.done += 1;
|
|
127
|
+
if (seq !== null) entry.doneCursor = seq;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (alreadySeen) return; // replayed history — rebuilt state, never re-notify
|
|
132
|
+
|
|
133
|
+
let surfaced = true;
|
|
134
|
+
switch (cls) {
|
|
135
|
+
case "progress":
|
|
136
|
+
this.host.notify(formatProgress(rec, entry.done, entry.total), "info");
|
|
137
|
+
break;
|
|
138
|
+
case "exception":
|
|
139
|
+
this.host.notify(formatSignal(rec), "warning");
|
|
140
|
+
this.host.wake(formatSignal(rec));
|
|
141
|
+
break;
|
|
142
|
+
case "terminal":
|
|
143
|
+
this.host.notify(formatSignal(rec), "info");
|
|
144
|
+
this.host.wake(formatSignal(rec));
|
|
145
|
+
entry.closed = true;
|
|
146
|
+
entry.task.closed = true;
|
|
147
|
+
break;
|
|
148
|
+
case "ignore":
|
|
149
|
+
surfaced = false;
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Advance the cursor and persist ONLY for a surfaced event. A firehose
|
|
154
|
+
// (`ignore`) event must not trigger a disk write + session entry per record
|
|
155
|
+
// (rauf emits many per second); ignored events re-classify as `ignore` on
|
|
156
|
+
// any later replay, so not persisting their seq is harmless. A terminal
|
|
157
|
+
// event persists `closed`, letting a later session know the run is done.
|
|
158
|
+
if (surfaced && seq !== null && seq > entry.task.lastSeq) {
|
|
159
|
+
entry.task.lastSeq = seq;
|
|
160
|
+
this.host.persist({ ...entry.task });
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** A rotation was detected (rauf started a new run — event `seq` restarts at
|
|
165
|
+
* 0). Reset the per-run cursor and counters so the new run is surfaced from
|
|
166
|
+
* its start instead of being swallowed by the previous run's high-water seq. */
|
|
167
|
+
private handleRotate(stateDir: string): void {
|
|
168
|
+
const entry = this.active.get(stateDir);
|
|
169
|
+
if (!entry) return;
|
|
170
|
+
entry.task.lastSeq = -1;
|
|
171
|
+
entry.doneCursor = -1;
|
|
172
|
+
entry.done = 0;
|
|
173
|
+
entry.closed = false;
|
|
174
|
+
entry.task.closed = false;
|
|
175
|
+
this.host.persist({ ...entry.task });
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private handleFor(stateDir: string): TaskHandle {
|
|
179
|
+
return {
|
|
180
|
+
stateDir,
|
|
181
|
+
poll: () => {},
|
|
182
|
+
close: () => this.detach(stateDir),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
}
|