@indigoai-us/hq-cli 5.345.175 → 5.345.176

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/command-catalog.generated.d.ts +65 -0
  3. package/dist/command-catalog.generated.js +80 -0
  4. package/dist/commands/bot.d.ts +28 -2
  5. package/dist/commands/bot.js +29 -6
  6. package/dist/commands/daemon.js +16 -0
  7. package/dist/commands/lanes-loop.d.ts +4 -1
  8. package/dist/commands/lanes-loop.js +23 -4
  9. package/dist/commands/lanes-shared.d.ts +5 -0
  10. package/dist/commands/lanes.d.ts +5 -7
  11. package/dist/commands/lanes.js +65 -5
  12. package/dist/commands/sync-core.d.ts +17 -0
  13. package/dist/commands/sync-core.js +46 -5
  14. package/dist/commands/sync-manifest.d.ts +6 -0
  15. package/dist/commands/sync-manifest.js +13 -0
  16. package/dist/hq-cloud-sync-runner/bin/sync-runner.js +448 -226
  17. package/dist/hq-cloud-sync-runner/package.json +1 -1
  18. package/dist/lib/bot/cli-update.d.ts +36 -0
  19. package/dist/lib/bot/cli-update.js +160 -42
  20. package/dist/lib/bot/run.d.ts +21 -0
  21. package/dist/lib/bot/run.js +127 -10
  22. package/dist/lib/bot/runtime/claude.d.ts +7 -1
  23. package/dist/lib/bot/runtime/claude.js +29 -0
  24. package/dist/lib/bot/runtime/codex.d.ts +5 -0
  25. package/dist/lib/bot/runtime/codex.js +13 -0
  26. package/dist/lib/bot/runtime/types.d.ts +10 -0
  27. package/dist/lib/bot/runtime-sign-in-check.d.ts +32 -0
  28. package/dist/lib/bot/runtime-sign-in-check.js +53 -0
  29. package/dist/lib/bot/runtime-sign-in.d.ts +32 -3
  30. package/dist/lib/bot/runtime-sign-in.js +17 -2
  31. package/dist/lib/bot/status.d.ts +25 -1
  32. package/dist/lib/bot/status.js +28 -0
  33. package/dist/lib/daemon/catalog.d.ts +4 -0
  34. package/dist/lib/daemon/catalog.js +47 -19
  35. package/dist/lib/daemon/run.js +1 -0
  36. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  37. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  38. package/dist/lib/lanes/loop-disposition.d.ts +44 -0
  39. package/dist/lib/lanes/loop-disposition.js +302 -0
  40. package/dist/lib/lanes/loop.d.ts +16 -3
  41. package/dist/lib/lanes/loop.js +167 -60
  42. package/dist/lib/lanes/reconcile.d.ts +5 -0
  43. package/dist/lib/lanes/reconcile.js +79 -4
  44. package/dist/lib/mesh/live/daemon/work-subscribe.d.ts +3 -0
  45. package/dist/lib/mesh/live/daemon/work-subscribe.js +41 -12
  46. package/dist/lib/sync-core/engine-home.d.ts +11 -4
  47. package/dist/lib/sync-core/engine-home.js +22 -21
  48. package/dist/lib/sync-core/manifest-scopes.d.ts +15 -0
  49. package/dist/lib/sync-core/manifest-scopes.js +26 -0
  50. package/dist/lib/sync-core/reconcile.d.ts +2 -2
  51. package/dist/lib/sync-core/reconcile.js +70 -46
  52. package/dist/lib/sync-core/release.d.ts +2 -2
  53. package/dist/lib/sync-core/release.js +31 -6
  54. package/dist/lib/sync-core/rollout-state.d.ts +6 -3
  55. package/dist/lib/sync-core/rollout-state.js +26 -22
  56. package/package.json +8 -8
@@ -0,0 +1,44 @@
1
+ import type { LaneRecord } from "./types.js";
2
+ export interface RecoveryIdentity {
3
+ isAlive(pid: number): boolean;
4
+ readStarttime(pid: number): number | null;
5
+ }
6
+ interface Entry {
7
+ directory: "active" | "claimed";
8
+ name: string;
9
+ sha256: string;
10
+ envelopeId: string | null;
11
+ }
12
+ export interface LoopDispositionSnapshot {
13
+ laneId: string;
14
+ version: number;
15
+ authority: Pick<LaneRecord, "senior" | "company" | "mesh_company" | "host">;
16
+ fingerprint: string;
17
+ leaseFingerprint: string;
18
+ unknownLeaseOwner: boolean;
19
+ envelopes: Entry[];
20
+ pending: Array<{
21
+ name: string;
22
+ sha256: string;
23
+ }>;
24
+ statusFingerprint: string;
25
+ }
26
+ export declare function loopDispositionSnapshot(hqRoot: string, lane: LaneRecord): LoopDispositionSnapshot;
27
+ export declare function applyLoopDisposition(input: {
28
+ hqRoot: string;
29
+ laneId: string;
30
+ expectedVersion: number;
31
+ fingerprint: string;
32
+ disposition: "fail" | "abandon";
33
+ reason: string;
34
+ ownershipAttested?: boolean;
35
+ authorize(lane: LaneRecord): void;
36
+ identity?: RecoveryIdentity;
37
+ now?: () => Date;
38
+ }): Promise<{
39
+ lane: LaneRecord;
40
+ decision: string;
41
+ externalEffects: "not-certified";
42
+ }>;
43
+ export {};
44
+ //# sourceMappingURL=loop-disposition.d.ts.map
@@ -0,0 +1,302 @@
1
+ /** Explicit local effects disposition. This module never executes a phase. */
2
+ import { createHash, randomUUID } from "node:crypto";
3
+ import * as fs from "node:fs";
4
+ import * as path from "node:path";
5
+ import { withAllLaneAdmissionLocks } from "./admission.js";
6
+ import { listLoopQueue, loopControllerIsAlive, resetLoopAfterDisposition } from "./loop.js";
7
+ import { isPidRecordAlive, laneRunDir, readPidRecord, readProcStarttime } from "./spawn.js";
8
+ import { listQuestionIds, readLane, readQuestion, updateLane } from "./store.js";
9
+ const digest = (value) => createHash("sha256").update(value).digest("hex");
10
+ const defaultIdentity = {
11
+ isAlive(pid) {
12
+ try {
13
+ process.kill(pid, 0);
14
+ return true;
15
+ }
16
+ catch (error) {
17
+ if (error.code === "ESRCH")
18
+ return false;
19
+ if (error.code === "EPERM")
20
+ return true;
21
+ throw error;
22
+ }
23
+ },
24
+ readStarttime: readProcStarttime,
25
+ };
26
+ /** Refuse linked directory components before looking at any recovery input. */
27
+ function assertDirectoryChain(root, directory) {
28
+ const relative = path.relative(root, directory);
29
+ if (relative.startsWith("..") || path.isAbsolute(relative))
30
+ throw new Error("Recovery directory is outside its root");
31
+ let current = root;
32
+ for (const component of ["", ...relative.split(path.sep).filter(Boolean)]) {
33
+ if (component)
34
+ current = path.join(current, component);
35
+ let stat;
36
+ try {
37
+ stat = fs.lstatSync(current);
38
+ }
39
+ catch (error) {
40
+ if (error.code === "ENOENT")
41
+ return;
42
+ throw error;
43
+ }
44
+ if (!stat.isDirectory() || stat.isSymbolicLink())
45
+ throw new Error("Recovery directory must be a real directory, not a symbolic link");
46
+ }
47
+ }
48
+ function recoveryRoot(runDir) {
49
+ return path.dirname(path.dirname(path.dirname(runDir)));
50
+ }
51
+ function assertRecoveryDirectories(runDir) {
52
+ const root = recoveryRoot(runDir);
53
+ for (const directory of ["", "active", "claimed", "pending", "recovery"]) {
54
+ assertDirectoryChain(root, path.join(runDir, "loop", directory));
55
+ }
56
+ }
57
+ function readRegularFile(file) {
58
+ const before = fs.lstatSync(file);
59
+ if (!before.isFile())
60
+ throw new Error("Recovery inputs must be regular files");
61
+ const fd = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0));
62
+ try {
63
+ const opened = fs.fstatSync(fd);
64
+ if (!opened.isFile() || opened.dev !== before.dev || opened.ino !== before.ino)
65
+ throw new Error("Recovery input changed while opening");
66
+ return fs.readFileSync(fd);
67
+ }
68
+ finally {
69
+ fs.closeSync(fd);
70
+ }
71
+ }
72
+ function fileHash(file) {
73
+ return digest(readRegularFile(file));
74
+ }
75
+ function leaseView(runDir) {
76
+ const lock = path.join(runDir, "loop", "runner.lock");
77
+ let stat;
78
+ try {
79
+ stat = fs.lstatSync(lock);
80
+ }
81
+ catch (error) {
82
+ if (error.code === "ENOENT")
83
+ return { fingerprint: "absent" };
84
+ throw error;
85
+ }
86
+ // Legacy format only. A new-format/linked/unknown lock is not silently removed.
87
+ if (!stat.isDirectory())
88
+ throw new Error("Unsupported runner lease format; do not convert a live root");
89
+ const entries = fs.readdirSync(lock).sort().map((name) => {
90
+ const file = path.join(lock, name);
91
+ return { name, hash: fileHash(file) };
92
+ });
93
+ const fingerprint = digest(JSON.stringify({ dev: stat.dev, ino: stat.ino, entries }));
94
+ let owner;
95
+ try {
96
+ owner = JSON.parse(readRegularFile(path.join(lock, "owner.json")).toString("utf8"));
97
+ }
98
+ catch (error) {
99
+ if (error.code !== "ENOENT" && !(error instanceof SyntaxError))
100
+ throw error;
101
+ }
102
+ if (owner && typeof owner === "object" && Number.isSafeInteger(owner.pid) && Number(owner.pid) > 0) {
103
+ const value = owner;
104
+ return { fingerprint, owner: { pid: value.pid, ...(typeof value.starttime === "number" ? { starttime: value.starttime } : {}) } };
105
+ }
106
+ return { fingerprint };
107
+ }
108
+ function entries(runDir) {
109
+ const out = [];
110
+ for (const directory of ["active", "claimed"]) {
111
+ const dir = path.join(runDir, "loop", directory);
112
+ if (!fs.existsSync(dir))
113
+ continue;
114
+ for (const name of fs.readdirSync(dir).sort().filter((name) => !name.startsWith("."))) {
115
+ const file = path.join(dir, name);
116
+ const sha256 = fileHash(file);
117
+ let record = null;
118
+ try {
119
+ const parsed = JSON.parse(readRegularFile(file).toString("utf8"));
120
+ if (parsed !== null && typeof parsed === "object")
121
+ record = parsed;
122
+ }
123
+ catch (error) {
124
+ if (!(error instanceof SyntaxError))
125
+ throw error;
126
+ }
127
+ if (record?.envelope?.kind === "stop")
128
+ throw new Error("An intentional stop requires its ordinary stop path");
129
+ // Corrupt records remain opaque and bound to their exact bytes;
130
+ // explicit disposition archives them without interpreting/replaying them.
131
+ out.push({ directory, name, sha256, envelopeId: typeof record?.envelope_id === "string" ? record.envelope_id : null });
132
+ }
133
+ }
134
+ return out;
135
+ }
136
+ function assertNoLiveProcess(runDir, identity) {
137
+ if (loopControllerIsAlive(runDir, identity.isAlive, identity.readStarttime))
138
+ throw new Error("A loop controller is still alive");
139
+ for (const name of ["worker.pid", "watcher.pid"]) {
140
+ const record = readPidRecord(path.join(runDir, name));
141
+ if (record && isPidRecordAlive(record, identity.isAlive, identity.readStarttime))
142
+ throw new Error("A worker or watcher is still alive");
143
+ }
144
+ const lease = leaseView(runDir);
145
+ if (lease.owner && isPidRecordAlive(lease.owner, identity.isAlive, identity.readStarttime))
146
+ throw new Error("The lease owner is still alive");
147
+ }
148
+ export function loopDispositionSnapshot(hqRoot, lane) {
149
+ const runDir = laneRunDir(hqRoot, lane.lane_id);
150
+ assertRecoveryDirectories(runDir);
151
+ const lease = leaseView(runDir);
152
+ const status = path.join(runDir, "loop", "loop.json");
153
+ const pendingDir = path.join(runDir, "loop", "pending");
154
+ const value = {
155
+ laneId: lane.lane_id,
156
+ version: lane.version,
157
+ authority: { senior: lane.senior, company: lane.company, mesh_company: lane.mesh_company, host: lane.host },
158
+ leaseFingerprint: lease.fingerprint,
159
+ unknownLeaseOwner: lease.fingerprint !== "absent" && !lease.owner,
160
+ envelopes: entries(runDir),
161
+ pending: fs.existsSync(pendingDir) ? fs.readdirSync(pendingDir).sort().filter((name) => !name.startsWith(".")).map((name) => ({ name, sha256: fileHash(path.join(pendingDir, name)) })) : [],
162
+ statusFingerprint: fs.existsSync(status) ? fileHash(status) : "absent",
163
+ };
164
+ return { ...value, fingerprint: digest(JSON.stringify(value)) };
165
+ }
166
+ function atomicRecord(file, value, runDir) {
167
+ assertRecoveryDirectories(runDir);
168
+ assertDirectoryChain(recoveryRoot(runDir), path.dirname(file));
169
+ const temporary = `${file}.${randomUUID()}.tmp`;
170
+ const fd = fs.openSync(temporary, "wx", 0o600);
171
+ try {
172
+ fs.writeFileSync(fd, `${JSON.stringify(value, null, 2)}\n`);
173
+ fs.fsyncSync(fd);
174
+ }
175
+ finally {
176
+ fs.closeSync(fd);
177
+ }
178
+ fs.renameSync(temporary, file);
179
+ const directory = fs.openSync(path.dirname(file), "r");
180
+ try {
181
+ fs.fsyncSync(directory);
182
+ }
183
+ finally {
184
+ fs.closeSync(directory);
185
+ }
186
+ }
187
+ export async function applyLoopDisposition(input) {
188
+ if (!/^[a-f0-9]{64}$/.test(input.fingerprint))
189
+ throw new Error("A preview fingerprint is required");
190
+ if (!input.reason.trim() || input.reason.length > 2000)
191
+ throw new Error("A bounded disposition reason is required");
192
+ const identity = input.identity ?? defaultIdentity;
193
+ return withAllLaneAdmissionLocks(input.hqRoot, async () => {
194
+ const lane = readLane(input.hqRoot, input.laneId);
195
+ input.authorize(lane);
196
+ if (!lane.loop_mode)
197
+ throw new Error("Not a loop lane");
198
+ if (listQuestionIds(input.hqRoot).some((id) => {
199
+ const q = readQuestion(input.hqRoot, id);
200
+ return q.lane_id === lane.lane_id && q.status === "pending";
201
+ }))
202
+ throw new Error("Pending questions must be resolved first");
203
+ const runDir = laneRunDir(input.hqRoot, lane.lane_id);
204
+ assertRecoveryDirectories(runDir);
205
+ const audit = path.join(runDir, "loop", "recovery", input.fingerprint);
206
+ assertDirectoryChain(input.hqRoot, audit);
207
+ const decisionFile = path.join(audit, "decision.json");
208
+ let decision;
209
+ if (fs.existsSync(decisionFile))
210
+ decision = JSON.parse(readRegularFile(decisionFile).toString("utf8"));
211
+ if (decision && (decision.disposition !== input.disposition || decision.reason !== input.reason || decision.snapshot.version !== input.expectedVersion))
212
+ throw new Error("Disposition does not match the recorded decision");
213
+ if (lane.terminal_details?.recovery_decision === input.fingerprint) {
214
+ if (!decision)
215
+ throw new Error("The recorded recovery decision is missing");
216
+ const complete = path.join(audit, "complete.json");
217
+ if (!fs.existsSync(complete))
218
+ atomicRecord(complete, { laneVersion: lane.version, at: (input.now ?? (() => new Date()))().toISOString() }, runDir);
219
+ return { lane, decision: input.fingerprint, externalEffects: "not-certified" };
220
+ }
221
+ if (lane.version !== input.expectedVersion)
222
+ throw new Error("Lane version changed; inspect again");
223
+ if (lane.terminal_reason?.startsWith("operator_") || ["story_closed", "loop_stopped"].includes(lane.terminal_reason ?? "")) {
224
+ throw new Error("Intentionally protected lane must use its ordinary lifecycle path");
225
+ }
226
+ if (lane.state !== "interrupted" && lane.state !== "done")
227
+ throw new Error("Only held or historical done lanes may be explicitly recovered");
228
+ const statusFile = path.join(runDir, "loop", "loop.json");
229
+ const status = fs.existsSync(statusFile) ? JSON.parse(readRegularFile(statusFile).toString("utf8")) : {};
230
+ if (["parked", "stopped", "failed"].includes(String(status.state)) && !(decision && status.recovery_decision === input.fingerprint))
231
+ throw new Error("Protected loop state cannot be recovered automatically");
232
+ assertNoLiveProcess(runDir, identity);
233
+ if (!decision) {
234
+ const snapshot = loopDispositionSnapshot(input.hqRoot, lane);
235
+ if (snapshot.fingerprint !== input.fingerprint)
236
+ throw new Error("Envelope or lease fingerprint changed; inspect again");
237
+ if (snapshot.unknownLeaseOwner && !input.ownershipAttested)
238
+ throw new Error("Unknown lease ownership requires explicit investigated-ownership attestation");
239
+ decision = { snapshot, disposition: input.disposition, reason: input.reason, ownershipAttested: input.ownershipAttested === true, at: (input.now ?? (() => new Date()))().toISOString(), externalEffects: "not-certified" };
240
+ assertDirectoryChain(input.hqRoot, audit);
241
+ fs.mkdirSync(audit, { recursive: true, mode: 0o700 });
242
+ assertDirectoryChain(input.hqRoot, audit);
243
+ // Persist the human/senior decision before any file is archived or lane CAS.
244
+ atomicRecord(decisionFile, decision, runDir);
245
+ }
246
+ const snapshot = decision.snapshot;
247
+ // Retry may resume partial archival, but must not silently discard new work.
248
+ const currentSnapshot = loopDispositionSnapshot(input.hqRoot, lane);
249
+ if (JSON.stringify(currentSnapshot.pending) !== JSON.stringify(snapshot.pending))
250
+ throw new Error("Pending queue changed during disposition");
251
+ if (currentSnapshot.statusFingerprint !== snapshot.statusFingerprint) {
252
+ const status = JSON.parse(readRegularFile(path.join(runDir, "loop", "loop.json")).toString("utf8"));
253
+ if (status.recovery_decision !== input.fingerprint)
254
+ throw new Error("Controller status changed during disposition");
255
+ }
256
+ const expected = new Set(snapshot.envelopes.map((entry) => `${entry.directory}/${entry.name}/${entry.sha256}`));
257
+ if (currentSnapshot.envelopes.some((entry) => !expected.has(`${entry.directory}/${entry.name}/${entry.sha256}`)))
258
+ throw new Error("New recovery input appeared during disposition");
259
+ for (const entry of snapshot.envelopes) {
260
+ assertRecoveryDirectories(runDir);
261
+ const source = path.join(runDir, "loop", entry.directory, entry.name);
262
+ const destination = path.join(audit, entry.directory, entry.name);
263
+ if (fs.existsSync(source)) {
264
+ if (fileHash(source) !== entry.sha256 || fs.existsSync(destination))
265
+ throw new Error("Recovery envelope changed during disposition");
266
+ assertDirectoryChain(input.hqRoot, path.dirname(destination));
267
+ fs.mkdirSync(path.dirname(destination), { recursive: true, mode: 0o700 });
268
+ assertDirectoryChain(input.hqRoot, path.dirname(destination));
269
+ assertRecoveryDirectories(runDir);
270
+ fs.renameSync(source, destination);
271
+ }
272
+ else if (!fs.existsSync(destination) || fileHash(destination) !== entry.sha256)
273
+ throw new Error("Recovery archive is incomplete");
274
+ }
275
+ assertRecoveryDirectories(runDir);
276
+ const lease = leaseView(runDir);
277
+ const quarantine = path.join(audit, "runner.lock");
278
+ if (snapshot.leaseFingerprint !== "absent") {
279
+ if (lease.fingerprint !== "absent") {
280
+ if (lease.fingerprint !== snapshot.leaseFingerprint || fs.existsSync(quarantine))
281
+ throw new Error("Runner lease changed during disposition");
282
+ fs.renameSync(path.join(runDir, "loop", "runner.lock"), quarantine);
283
+ }
284
+ else if (!fs.existsSync(quarantine))
285
+ throw new Error("Lease quarantine is incomplete");
286
+ }
287
+ else if (lease.fingerprint !== "absent")
288
+ throw new Error("A runner acquired the lease during disposition");
289
+ const pending = input.disposition === "abandon" && lane.state !== "done" && listLoopQueue(runDir).length > 0;
290
+ const nextState = lane.state === "done" ? "done" : pending ? "queued" : "failed";
291
+ resetLoopAfterDisposition(runDir, lane.lane_id, input.fingerprint, pending);
292
+ const updated = updateLane(input.hqRoot, lane.lane_id, input.expectedVersion, "command", (current) => ({
293
+ ...current, state: nextState, queued_reason: undefined, queued_details: undefined,
294
+ admission_reserved_at: undefined, pending_resume: undefined,
295
+ terminal_reason: lane.state === "done" ? lane.terminal_reason : pending ? undefined : "loop_recovery_disposed",
296
+ terminal_details: { recovery_decision: input.fingerprint, external_effects: "not-certified", disposition: input.disposition, ...(pending ? { loop_recovery: true } : {}) },
297
+ }));
298
+ atomicRecord(path.join(audit, "complete.json"), { laneVersion: updated.version, at: (input.now ?? (() => new Date()))().toISOString() }, runDir);
299
+ return { lane: updated, decision: input.fingerprint, externalEffects: "not-certified" };
300
+ });
301
+ }
302
+ //# sourceMappingURL=loop-disposition.js.map
@@ -47,6 +47,8 @@ export interface LoopStatus {
47
47
  active_phase: string | null;
48
48
  stall_counts: Record<string, number>;
49
49
  pid?: number;
50
+ starttime?: number;
51
+ recovery_decision?: string;
50
52
  updated_at: string;
51
53
  handoff_hold?: {
52
54
  envelope_id: string;
@@ -124,6 +126,10 @@ export interface LoopRunnerOptions {
124
126
  idlePollMs?: number;
125
127
  signal?: AbortSignal;
126
128
  repairInvalidHandoff?: boolean;
129
+ processIdentity?: {
130
+ isAlive(pid: number): boolean;
131
+ readStarttime(pid: number): number | null;
132
+ };
127
133
  }
128
134
  export interface LoopRunSummary {
129
135
  state: "stopped" | "parked" | "aborted";
@@ -203,14 +209,21 @@ export declare function loopStatus(runDir: string, laneId: string): LoopStatus;
203
209
  * and changes nothing when the queue holds anything ahead of the stop.
204
210
  */
205
211
  export declare function commitIdleLoopStop(runDir: string, laneId: string, now?: () => Date): boolean;
206
- export declare function setLoopControllerPid(runDir: string, laneId: string, pid: number | undefined): void;
212
+ export declare function setLoopControllerPid(runDir: string, laneId: string, pid: number | undefined, starttime?: number): void;
207
213
  export declare function clearLoopControllerPid(runDir: string, laneId: string, pid: number): void;
208
214
  /**
209
215
  * True when a loop lane's controller is alive with no phase active: the lane
210
216
  * finished its last phase and is waiting for the next envelope.
211
217
  */
212
- export declare function loopLaneIsIdle(runDir: string, laneId: string): boolean;
213
- export declare function loopControllerIsAlive(runDir: string): boolean;
218
+ export declare function loopLaneIsIdle(runDir: string, laneId: string, isAlive?: (pid: number) => boolean, readStarttime?: (pid: number) => number | null): boolean;
219
+ export declare function loopControllerIsAlive(runDir: string, isAlive?: (pid: number) => boolean, readStarttime?: (pid: number) => number | null): boolean;
220
+ /** Read durable work without changing queue files or assuming effects are replayable. */
221
+ export declare function inspectLoopRecovery(runDir: string): {
222
+ kind: "pending" | "hold" | "protected" | "empty";
223
+ reason?: string;
224
+ };
225
+ /** Reset only after an explicit, durable disposition; this never starts work. */
226
+ export declare function resetLoopAfterDisposition(runDir: string, laneId: string, decision: string, hasPending: boolean): void;
214
227
  /** Run queued phases serially. Sessions are isolated by story and provider. */
215
228
  export declare function runLoopLane(options: LoopRunnerOptions): Promise<LoopRunSummary>;
216
229
  export {};