@ordewell/core 0.11.3 → 0.12.1

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 (49) hide show
  1. package/AUTHORS.md +3 -1
  2. package/dist/{ModeResolver-DyXeZ9ib.d.ts → ModeResolver-B64aUZXJ.d.ts} +1 -1
  3. package/dist/{ModeResolver-BDAgouuE.d.mts → ModeResolver-CA2fUU9h.d.mts} +1 -1
  4. package/dist/{Task-BL4Idm9r.d.mts → Task-CxGgN6I0.d.mts} +801 -39
  5. package/dist/{Task-BL4Idm9r.d.ts → Task-CxGgN6I0.d.ts} +801 -39
  6. package/dist/{chunk-YAFNYNFX.mjs → chunk-2IFKPYPO.mjs} +453 -17
  7. package/dist/chunk-2IFKPYPO.mjs.map +1 -0
  8. package/dist/{chunk-MYXMIVWA.mjs → chunk-FX33UKAD.mjs} +8 -6
  9. package/dist/chunk-FX33UKAD.mjs.map +1 -0
  10. package/dist/{chunk-QTZ5XTBY.mjs → chunk-PKJTN2PD.mjs} +458 -13
  11. package/dist/chunk-PKJTN2PD.mjs.map +1 -0
  12. package/dist/{chunk-7WNLN63J.mjs → chunk-RZ23BMVR.mjs} +599 -124
  13. package/dist/chunk-RZ23BMVR.mjs.map +1 -0
  14. package/dist/chunk-UDUATSCJ.mjs +48 -0
  15. package/dist/chunk-UDUATSCJ.mjs.map +1 -0
  16. package/dist/{IRunner-DOMtDTVt.d.ts → eventStore-CTCSKS9E.d.ts} +976 -145
  17. package/dist/{IRunner-ihalYfTo.d.mts → eventStore-CcB8yELZ.d.mts} +976 -145
  18. package/dist/index.d.mts +905 -164
  19. package/dist/index.d.ts +905 -164
  20. package/dist/index.js +12496 -3308
  21. package/dist/index.js.map +1 -1
  22. package/dist/index.mjs +10830 -2919
  23. package/dist/index.mjs.map +1 -1
  24. package/dist/parsing.d.mts +2 -2
  25. package/dist/parsing.d.ts +2 -2
  26. package/dist/parsing.js +277 -21
  27. package/dist/parsing.js.map +1 -1
  28. package/dist/parsing.mjs +3 -3
  29. package/dist/plan-utils.d.mts +8 -5
  30. package/dist/plan-utils.d.ts +8 -5
  31. package/dist/plan-utils.js +262 -33
  32. package/dist/plan-utils.js.map +1 -1
  33. package/dist/plan-utils.mjs +39 -3
  34. package/dist/{runSettingsText-DVOd7vKC.d.ts → runSettingsText-DFVpe6aU.d.mts} +1996 -895
  35. package/dist/{runSettingsText-BuLVBlXS.d.mts → runSettingsText-lKLDsyli.d.ts} +1996 -895
  36. package/dist/testing.d.mts +99 -4
  37. package/dist/testing.d.ts +99 -4
  38. package/dist/testing.js +213 -8
  39. package/dist/testing.js.map +1 -1
  40. package/dist/testing.mjs +193 -8
  41. package/dist/testing.mjs.map +1 -1
  42. package/docker/task.Dockerfile +22 -0
  43. package/package.json +3 -1
  44. package/skills/diagnose/SKILL.md +2 -2
  45. package/skills/explore/SKILL.md +1 -1
  46. package/dist/chunk-7WNLN63J.mjs.map +0 -1
  47. package/dist/chunk-MYXMIVWA.mjs.map +0 -1
  48. package/dist/chunk-QTZ5XTBY.mjs.map +0 -1
  49. package/dist/chunk-YAFNYNFX.mjs.map +0 -1
@@ -1,5 +1,5 @@
1
- import { R as IRunnerSession, ar as QueuedTaskMessage, b6 as StructuredTurnEnd, b5 as StructuredEvent, bc as TaskCompleteArgs, t as CheckpointAnswer, aX as ScriptExit, be as TaskMessageFrom, aE as RunnerCommand, S as InvokedCommand, k as ApprovalDecision, O as IConfig, P as IFileSystem } from './IRunner-ihalYfTo.mjs';
2
- import { y as IntegrationDisposal, aA as RunnerSessionCost, x as IWorktreeIsolation, J as IsolationAvailability, R as IsolationMergeResult, b4 as TreeSnapshot, aF as SetupFailure, U as IsolationPruneResult, V as IsolationRemoval, S as IsolationOutcome, ao as RepairEvidence, X as IsolationRun, aS as Task, ah as PrepareIntent, ai as PreparedTask, q as HandoffCommit, K as IsolationHandoff } from './Task-BL4Idm9r.mjs';
1
+ import { J as EventStore, bP as TreeEvent, bQ as TreeFact, aj as NodePath, K as ExecutionBackend, a4 as LeaseRequest, a5 as LeaseSpec, S as Guarantee, a3 as LeaseRepo, b5 as RunnerPlacement, a2 as LeaseRecord, T as GuaranteeReport, aR as ReservedLease, _ as IRunnerSession, aK as QueuedTaskMessage, bs as StructuredTurnEnd, br as StructuredEvent, by as TaskCompleteArgs, x as CheckpointAnswer, be as ScriptExit, bA as TaskMessageFrom, aX as RunnerCommand, $ as InvokedCommand, k as ApprovalDecision, V as IConfig, W as IFileSystem } from './eventStore-CcB8yELZ.mjs';
2
+ import { U as IntegrationDisposal, b4 as RunnerSessionCost, T as IWorktreeIsolation, W as IsolationAvailability, a2 as IsolationMergeResult, bE as TreeSnapshot, bb as SetupFailure, a5 as IsolationPruneResult, a6 as IsolationRemoval, a3 as IsolationOutcome, a9 as IsolationRevertResult, a$ as RevertCommand, aP as RepairEvidence, ab as IsolationRun, bp as Task, aH as PrepareIntent, aI as PreparedTask, J as HandoffCommit, ai as LandedWork, b0 as RevertWatch, X as IsolationHandoff } from './Task-CxGgN6I0.mjs';
3
3
  import 'child_process';
4
4
  import 'zod/v4';
5
5
 
@@ -107,6 +107,82 @@ declare class FakeRunnerSession implements IRunnerSession {
107
107
  emitTurnEnd(reason: StructuredTurnEnd): void;
108
108
  private deliver;
109
109
  }
110
+ /** An in-memory {@link EventStore}. `failing` makes the next appends reject, as a full disk would. */
111
+ declare class FakeEventStore implements EventStore {
112
+ readonly events: TreeEvent[];
113
+ failing: Error | null;
114
+ private closed;
115
+ append(events: readonly TreeFact[]): Promise<void>;
116
+ read(after: number, path?: NodePath): AsyncIterable<TreeEvent>;
117
+ close(): Promise<void>;
118
+ }
119
+ /**
120
+ * An in-memory {@link ExecutionBackend}, as {@link FakeWorktreeIsolation} fakes
121
+ * git: a count of slots and nothing else. `requests`, `opened` and `released`
122
+ * log what the seam was asked; `holdOpen` keeps an open waiting, `openError`
123
+ * fails one, `refuse` turns a role down, `provenGuarantees` is what its probe
124
+ * proves, and `freed` is what a release does, a pool's tick.
125
+ */
126
+ declare class FakeExecutionBackend implements ExecutionBackend {
127
+ /** Slots; a planner, a merge and a delegated task take none, as a flat run's do not. */
128
+ limit: number;
129
+ held: number;
130
+ requests: LeaseRequest[];
131
+ opened: {
132
+ request: LeaseRequest;
133
+ spec: LeaseSpec;
134
+ }[];
135
+ released: {
136
+ id: string;
137
+ reason: string;
138
+ }[];
139
+ letGoCalls: {
140
+ taskId: string;
141
+ keep: boolean;
142
+ reason: string;
143
+ }[];
144
+ openError: Error | null;
145
+ refuse: {
146
+ role: LeaseRequest['role'];
147
+ reason: string;
148
+ } | null;
149
+ provenGuarantees: readonly Guarantee[];
150
+ /** What exportResult reports of a session, in place of what it is told. */
151
+ sessionState: {
152
+ nativeSessionId: string;
153
+ } | null;
154
+ /** The repositories an open says its worktree holds. */
155
+ repos: readonly LeaseRepo[];
156
+ /** Where an open puts its runner, for a lease that does not run it on this host. */
157
+ placement: RunnerPlacement | null;
158
+ /** Brings a task's work back; absent, it is where the attempt ran. */
159
+ collect?: (taskId: string) => Promise<void>;
160
+ /** Waits on what it has running, as a closing Session does; absent, nothing outlives a runner. */
161
+ settle?: () => Promise<void>;
162
+ /** The workspace root's repositories; absent, none is known. */
163
+ checkoutRepos?: () => readonly LeaseRepo[];
164
+ records: LeaseRecord[];
165
+ freed: () => void;
166
+ private openHold;
167
+ private nextId;
168
+ /** Make the next open wait until the returned function is called. */
169
+ holdOpen(): () => void;
170
+ probe(): Promise<GuaranteeReport>;
171
+ private takesSlot;
172
+ refusal(request: LeaseRequest): string | null;
173
+ reserve(request: LeaseRequest): {
174
+ lease: ReservedLease;
175
+ } | {
176
+ refused: string;
177
+ };
178
+ private open;
179
+ letGo(taskId: string, { keep, landing, reason }: {
180
+ keep: boolean;
181
+ landing?: Promise<unknown> | null;
182
+ reason: string;
183
+ }): Promise<void>;
184
+ list(): Promise<readonly LeaseRecord[]>;
185
+ }
110
186
  type FakeIsolationCall = {
111
187
  op: 'isActive';
112
188
  workspaceRoot: string;
@@ -131,6 +207,9 @@ type FakeIsolationCall = {
131
207
  } | {
132
208
  op: 'discard';
133
209
  integration: IntegrationDisposal;
210
+ } | {
211
+ op: 'readLanded' | 'revert';
212
+ taskIds: readonly string[];
134
213
  };
135
214
  /**
136
215
  * An in-memory {@link IWorktreeIsolation} for scheduling tests: no git, no
@@ -144,6 +223,8 @@ type FakeIsolationCall = {
144
223
  */
145
224
  declare class FakeWorktreeIsolation implements IWorktreeIsolation {
146
225
  availability: IsolationAvailability;
226
+ /** What `gitShortfall` answers. */
227
+ gitShortfallText: string | null;
147
228
  /** The repo paths `startRun` groups: a group of one at `.` unless set. */
148
229
  repos: string[];
149
230
  /** Per task id, the repos it changes; every repo of the group when not listed. */
@@ -179,23 +260,33 @@ declare class FakeWorktreeIsolation implements IWorktreeIsolation {
179
260
  removal: IsolationRemoval;
180
261
  /** Per task id; a task not listed integrates as `merged`. */
181
262
  outcomes: Map<string, IsolationOutcome>;
263
+ /** What `revert` stops with; a revert that goes through when null. */
264
+ revertStop: Exclude<IsolationRevertResult, {
265
+ outcome: 'reverted';
266
+ }> | null;
267
+ /** What `readLanded` finds of a task's work in the user's branch, by task id. */
268
+ inBranch: Map<string, RevertCommand[]>;
182
269
  /** Per task id, what `verifyRepair` finds; a task not listed passes. */
183
270
  repairEvidence: Map<string, RepairEvidence>;
184
271
  calls: FakeIsolationCall[];
185
272
  private holds;
186
273
  private verifyHolds;
274
+ private revertHold;
187
275
  private runCount;
188
276
  private log;
189
277
  /** Task ids in the order `op` was called for them. */
190
278
  taskIdsFor(op: 'prepare' | 'reopen' | 'verifyRepair' | 'integrate' | 'release'): string[];
191
279
  /** Make `integrate` for this task wait until the returned function is called. */
192
280
  holdIntegration(taskId: string): () => void;
281
+ /** Make the next `revert` wait, past its `refusal`, until the returned function is called. */
282
+ holdRevert(): () => void;
193
283
  /** Make `verifyRepair` for this task wait until the returned function is called. */
194
284
  holdVerification(taskId: string): () => void;
285
+ gitShortfall(): Promise<string | null>;
195
286
  isActive(workspaceRoot: string): Promise<IsolationAvailability>;
196
287
  /** Like git: once the tracked changes are stashed, the tree is no longer dirty. */
197
288
  stash(workspaceRoot: string): Promise<void>;
198
- startRun(workspaceRoot: string): Promise<IsolationRun>;
289
+ startRun(workspaceRoot: string, stateRoot?: string): Promise<IsolationRun>;
199
290
  prepare(task: Task, run: IsolationRun, _intent?: PrepareIntent, opts?: {
200
291
  readOnly?: boolean;
201
292
  }): Promise<PreparedTask>;
@@ -204,6 +295,10 @@ declare class FakeWorktreeIsolation implements IWorktreeIsolation {
204
295
  verifyRepair(task: Task, _run: IsolationRun): Promise<RepairEvidence>;
205
296
  /** Like git: the landing is recorded and persisted before the (held) merge, and cleared once it settles. */
206
297
  integrate(task: Task, run: IsolationRun, persist?: () => void, commit?: HandoffCommit): Promise<IsolationOutcome>;
298
+ /** Newest landing first, read as the plan's highest order: scheduling tests land in plan order. */
299
+ readLanded(run: IsolationRun, taskIds: readonly string[]): Promise<LandedWork>;
300
+ /** Like git: the turn, then the judgement, then the records go in one step with the tasks back. */
301
+ revert(run: IsolationRun, taskIds: readonly string[], watch: RevertWatch): Promise<IsolationRevertResult>;
207
302
  release(run: IsolationRun, taskId: string, opts: {
208
303
  keep: boolean;
209
304
  }): Promise<IsolationRemoval>;
@@ -221,4 +316,4 @@ declare class FakeWorktreeIsolation implements IWorktreeIsolation {
221
316
  sweep(): Promise<void>;
222
317
  }
223
318
 
224
- export { type FakeIsolationCall, FakeRunnerSession, FakeWorktreeIsolation, fakeConfig, fakeFileSystem, flushMicrotasks };
319
+ export { FakeEventStore, FakeExecutionBackend, type FakeIsolationCall, FakeRunnerSession, FakeWorktreeIsolation, fakeConfig, fakeFileSystem, flushMicrotasks };
package/dist/testing.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { R as IRunnerSession, ar as QueuedTaskMessage, b6 as StructuredTurnEnd, b5 as StructuredEvent, bc as TaskCompleteArgs, t as CheckpointAnswer, aX as ScriptExit, be as TaskMessageFrom, aE as RunnerCommand, S as InvokedCommand, k as ApprovalDecision, O as IConfig, P as IFileSystem } from './IRunner-DOMtDTVt.js';
2
- import { y as IntegrationDisposal, aA as RunnerSessionCost, x as IWorktreeIsolation, J as IsolationAvailability, R as IsolationMergeResult, b4 as TreeSnapshot, aF as SetupFailure, U as IsolationPruneResult, V as IsolationRemoval, S as IsolationOutcome, ao as RepairEvidence, X as IsolationRun, aS as Task, ah as PrepareIntent, ai as PreparedTask, q as HandoffCommit, K as IsolationHandoff } from './Task-BL4Idm9r.js';
1
+ import { J as EventStore, bP as TreeEvent, bQ as TreeFact, aj as NodePath, K as ExecutionBackend, a4 as LeaseRequest, a5 as LeaseSpec, S as Guarantee, a3 as LeaseRepo, b5 as RunnerPlacement, a2 as LeaseRecord, T as GuaranteeReport, aR as ReservedLease, _ as IRunnerSession, aK as QueuedTaskMessage, bs as StructuredTurnEnd, br as StructuredEvent, by as TaskCompleteArgs, x as CheckpointAnswer, be as ScriptExit, bA as TaskMessageFrom, aX as RunnerCommand, $ as InvokedCommand, k as ApprovalDecision, V as IConfig, W as IFileSystem } from './eventStore-CTCSKS9E.js';
2
+ import { U as IntegrationDisposal, b4 as RunnerSessionCost, T as IWorktreeIsolation, W as IsolationAvailability, a2 as IsolationMergeResult, bE as TreeSnapshot, bb as SetupFailure, a5 as IsolationPruneResult, a6 as IsolationRemoval, a3 as IsolationOutcome, a9 as IsolationRevertResult, a$ as RevertCommand, aP as RepairEvidence, ab as IsolationRun, bp as Task, aH as PrepareIntent, aI as PreparedTask, J as HandoffCommit, ai as LandedWork, b0 as RevertWatch, X as IsolationHandoff } from './Task-CxGgN6I0.js';
3
3
  import 'child_process';
4
4
  import 'zod/v4';
5
5
 
@@ -107,6 +107,82 @@ declare class FakeRunnerSession implements IRunnerSession {
107
107
  emitTurnEnd(reason: StructuredTurnEnd): void;
108
108
  private deliver;
109
109
  }
110
+ /** An in-memory {@link EventStore}. `failing` makes the next appends reject, as a full disk would. */
111
+ declare class FakeEventStore implements EventStore {
112
+ readonly events: TreeEvent[];
113
+ failing: Error | null;
114
+ private closed;
115
+ append(events: readonly TreeFact[]): Promise<void>;
116
+ read(after: number, path?: NodePath): AsyncIterable<TreeEvent>;
117
+ close(): Promise<void>;
118
+ }
119
+ /**
120
+ * An in-memory {@link ExecutionBackend}, as {@link FakeWorktreeIsolation} fakes
121
+ * git: a count of slots and nothing else. `requests`, `opened` and `released`
122
+ * log what the seam was asked; `holdOpen` keeps an open waiting, `openError`
123
+ * fails one, `refuse` turns a role down, `provenGuarantees` is what its probe
124
+ * proves, and `freed` is what a release does, a pool's tick.
125
+ */
126
+ declare class FakeExecutionBackend implements ExecutionBackend {
127
+ /** Slots; a planner, a merge and a delegated task take none, as a flat run's do not. */
128
+ limit: number;
129
+ held: number;
130
+ requests: LeaseRequest[];
131
+ opened: {
132
+ request: LeaseRequest;
133
+ spec: LeaseSpec;
134
+ }[];
135
+ released: {
136
+ id: string;
137
+ reason: string;
138
+ }[];
139
+ letGoCalls: {
140
+ taskId: string;
141
+ keep: boolean;
142
+ reason: string;
143
+ }[];
144
+ openError: Error | null;
145
+ refuse: {
146
+ role: LeaseRequest['role'];
147
+ reason: string;
148
+ } | null;
149
+ provenGuarantees: readonly Guarantee[];
150
+ /** What exportResult reports of a session, in place of what it is told. */
151
+ sessionState: {
152
+ nativeSessionId: string;
153
+ } | null;
154
+ /** The repositories an open says its worktree holds. */
155
+ repos: readonly LeaseRepo[];
156
+ /** Where an open puts its runner, for a lease that does not run it on this host. */
157
+ placement: RunnerPlacement | null;
158
+ /** Brings a task's work back; absent, it is where the attempt ran. */
159
+ collect?: (taskId: string) => Promise<void>;
160
+ /** Waits on what it has running, as a closing Session does; absent, nothing outlives a runner. */
161
+ settle?: () => Promise<void>;
162
+ /** The workspace root's repositories; absent, none is known. */
163
+ checkoutRepos?: () => readonly LeaseRepo[];
164
+ records: LeaseRecord[];
165
+ freed: () => void;
166
+ private openHold;
167
+ private nextId;
168
+ /** Make the next open wait until the returned function is called. */
169
+ holdOpen(): () => void;
170
+ probe(): Promise<GuaranteeReport>;
171
+ private takesSlot;
172
+ refusal(request: LeaseRequest): string | null;
173
+ reserve(request: LeaseRequest): {
174
+ lease: ReservedLease;
175
+ } | {
176
+ refused: string;
177
+ };
178
+ private open;
179
+ letGo(taskId: string, { keep, landing, reason }: {
180
+ keep: boolean;
181
+ landing?: Promise<unknown> | null;
182
+ reason: string;
183
+ }): Promise<void>;
184
+ list(): Promise<readonly LeaseRecord[]>;
185
+ }
110
186
  type FakeIsolationCall = {
111
187
  op: 'isActive';
112
188
  workspaceRoot: string;
@@ -131,6 +207,9 @@ type FakeIsolationCall = {
131
207
  } | {
132
208
  op: 'discard';
133
209
  integration: IntegrationDisposal;
210
+ } | {
211
+ op: 'readLanded' | 'revert';
212
+ taskIds: readonly string[];
134
213
  };
135
214
  /**
136
215
  * An in-memory {@link IWorktreeIsolation} for scheduling tests: no git, no
@@ -144,6 +223,8 @@ type FakeIsolationCall = {
144
223
  */
145
224
  declare class FakeWorktreeIsolation implements IWorktreeIsolation {
146
225
  availability: IsolationAvailability;
226
+ /** What `gitShortfall` answers. */
227
+ gitShortfallText: string | null;
147
228
  /** The repo paths `startRun` groups: a group of one at `.` unless set. */
148
229
  repos: string[];
149
230
  /** Per task id, the repos it changes; every repo of the group when not listed. */
@@ -179,23 +260,33 @@ declare class FakeWorktreeIsolation implements IWorktreeIsolation {
179
260
  removal: IsolationRemoval;
180
261
  /** Per task id; a task not listed integrates as `merged`. */
181
262
  outcomes: Map<string, IsolationOutcome>;
263
+ /** What `revert` stops with; a revert that goes through when null. */
264
+ revertStop: Exclude<IsolationRevertResult, {
265
+ outcome: 'reverted';
266
+ }> | null;
267
+ /** What `readLanded` finds of a task's work in the user's branch, by task id. */
268
+ inBranch: Map<string, RevertCommand[]>;
182
269
  /** Per task id, what `verifyRepair` finds; a task not listed passes. */
183
270
  repairEvidence: Map<string, RepairEvidence>;
184
271
  calls: FakeIsolationCall[];
185
272
  private holds;
186
273
  private verifyHolds;
274
+ private revertHold;
187
275
  private runCount;
188
276
  private log;
189
277
  /** Task ids in the order `op` was called for them. */
190
278
  taskIdsFor(op: 'prepare' | 'reopen' | 'verifyRepair' | 'integrate' | 'release'): string[];
191
279
  /** Make `integrate` for this task wait until the returned function is called. */
192
280
  holdIntegration(taskId: string): () => void;
281
+ /** Make the next `revert` wait, past its `refusal`, until the returned function is called. */
282
+ holdRevert(): () => void;
193
283
  /** Make `verifyRepair` for this task wait until the returned function is called. */
194
284
  holdVerification(taskId: string): () => void;
285
+ gitShortfall(): Promise<string | null>;
195
286
  isActive(workspaceRoot: string): Promise<IsolationAvailability>;
196
287
  /** Like git: once the tracked changes are stashed, the tree is no longer dirty. */
197
288
  stash(workspaceRoot: string): Promise<void>;
198
- startRun(workspaceRoot: string): Promise<IsolationRun>;
289
+ startRun(workspaceRoot: string, stateRoot?: string): Promise<IsolationRun>;
199
290
  prepare(task: Task, run: IsolationRun, _intent?: PrepareIntent, opts?: {
200
291
  readOnly?: boolean;
201
292
  }): Promise<PreparedTask>;
@@ -204,6 +295,10 @@ declare class FakeWorktreeIsolation implements IWorktreeIsolation {
204
295
  verifyRepair(task: Task, _run: IsolationRun): Promise<RepairEvidence>;
205
296
  /** Like git: the landing is recorded and persisted before the (held) merge, and cleared once it settles. */
206
297
  integrate(task: Task, run: IsolationRun, persist?: () => void, commit?: HandoffCommit): Promise<IsolationOutcome>;
298
+ /** Newest landing first, read as the plan's highest order: scheduling tests land in plan order. */
299
+ readLanded(run: IsolationRun, taskIds: readonly string[]): Promise<LandedWork>;
300
+ /** Like git: the turn, then the judgement, then the records go in one step with the tasks back. */
301
+ revert(run: IsolationRun, taskIds: readonly string[], watch: RevertWatch): Promise<IsolationRevertResult>;
207
302
  release(run: IsolationRun, taskId: string, opts: {
208
303
  keep: boolean;
209
304
  }): Promise<IsolationRemoval>;
@@ -221,4 +316,4 @@ declare class FakeWorktreeIsolation implements IWorktreeIsolation {
221
316
  sweep(): Promise<void>;
222
317
  }
223
318
 
224
- export { type FakeIsolationCall, FakeRunnerSession, FakeWorktreeIsolation, fakeConfig, fakeFileSystem, flushMicrotasks };
319
+ export { FakeEventStore, FakeExecutionBackend, type FakeIsolationCall, FakeRunnerSession, FakeWorktreeIsolation, fakeConfig, fakeFileSystem, flushMicrotasks };
package/dist/testing.js CHANGED
@@ -20,6 +20,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/testing.ts
21
21
  var testing_exports = {};
22
22
  __export(testing_exports, {
23
+ FakeEventStore: () => FakeEventStore,
24
+ FakeExecutionBackend: () => FakeExecutionBackend,
23
25
  FakeRunnerSession: () => FakeRunnerSession,
24
26
  FakeWorktreeIsolation: () => FakeWorktreeIsolation,
25
27
  fakeConfig: () => fakeConfig,
@@ -45,6 +47,9 @@ function integrationBranchFor(runId) {
45
47
  function noRemoval() {
46
48
  return { preserved: [], refused: [] };
47
49
  }
50
+ function landedInRepo(run, record, repoPath) {
51
+ return !!record.repos[repoPath]?.changed || run.landings?.[record.taskId]?.[repoPath] !== void 0;
52
+ }
48
53
  function handoffOf(run) {
49
54
  const merged = Object.values(run.tasks).filter((r) => r.status === "merged" && !r.inHead).sort((a, b) => a.order - b.order);
50
55
  const entry = (r) => ({
@@ -60,7 +65,7 @@ function handoffOf(run) {
60
65
  baseRef: repo.baseRef,
61
66
  ...repo.baseBranch ? { baseBranch: repo.baseBranch } : {},
62
67
  ...repo.startedDetached ? { startedDetached: true } : {},
63
- landed: merged.filter((r) => r.repos[repo.path]?.changed).map(entry)
68
+ landed: merged.filter((r) => landedInRepo(run, r, repo.path)).map(entry)
64
69
  })),
65
70
  landed: merged.map(entry)
66
71
  };
@@ -70,6 +75,26 @@ function handoffOf(run) {
70
75
  var SKILL_NAME = new RegExp(`^${SKILL_NAME_PATTERN}$`);
71
76
  var DEFAULT_RUNNERS = ["claude-code"];
72
77
 
78
+ // src/services/executionLease.ts
79
+ var GUARANTEES = ["fs.hostHidden", "fs.writeScope", "net.allowlist", "net.noMetadata", "cred.unreadable", "kernel.separate", "limits.memory"];
80
+ var LeaseGuaranteeError = class extends Error {
81
+ constructor(unproven) {
82
+ super(`This session requires ${unproven.join(", ")}, which its execution backend could not prove.`);
83
+ this.unproven = unproven;
84
+ this.name = "LeaseGuaranteeError";
85
+ }
86
+ unproven;
87
+ };
88
+ function refuseUnproven(required, report) {
89
+ const unproven = required.filter((guarantee) => !report[guarantee].proven);
90
+ if (unproven.length > 0) throw new LeaseGuaranteeError(unproven);
91
+ }
92
+
93
+ // src/models/TreeEvent.ts
94
+ function isWithin(node, ancestor) {
95
+ return node === ancestor || node.startsWith(`${ancestor}/`);
96
+ }
97
+
73
98
  // src/testing.ts
74
99
  async function flushMicrotasks(rounds = 20) {
75
100
  for (let i = 0; i < rounds; i += 1) await Promise.resolve();
@@ -107,6 +132,7 @@ function fakeConfig(overrides = {}) {
107
132
  // Off so a test that is not about the allowlist sees every prompt it raises.
108
133
  approvalDefaults: false,
109
134
  effectAllowlist: [],
135
+ notifyCommand: [],
110
136
  webReadBudget: 40,
111
137
  plannerNativeShell: false,
112
138
  setProviderModelLists: () => {
@@ -340,8 +366,121 @@ var FakeRunnerSession = class {
340
366
  this.emitEvent({ type: "turn_start", text, ...from ? { from } : {} });
341
367
  }
342
368
  };
369
+ var FakeEventStore = class {
370
+ events = [];
371
+ failing = null;
372
+ closed = false;
373
+ async append(events) {
374
+ if (this.closed) throw new Error("This event log is closed.");
375
+ if (this.failing) throw this.failing;
376
+ for (const event of events) this.events.push({ ...event, seq: this.events.length + 1 });
377
+ }
378
+ async *read(after, path) {
379
+ for (const event of [...this.events]) {
380
+ if (event.seq > after && (path === void 0 || isWithin(event.node, path))) yield event;
381
+ }
382
+ }
383
+ async close() {
384
+ this.closed = true;
385
+ }
386
+ };
387
+ var FakeExecutionBackend = class {
388
+ /** Slots; a planner, a merge and a delegated task take none, as a flat run's do not. */
389
+ limit = Infinity;
390
+ held = 0;
391
+ requests = [];
392
+ opened = [];
393
+ released = [];
394
+ letGoCalls = [];
395
+ openError = null;
396
+ refuse = null;
397
+ provenGuarantees = [];
398
+ /** What exportResult reports of a session, in place of what it is told. */
399
+ sessionState = null;
400
+ /** The repositories an open says its worktree holds. */
401
+ repos = [];
402
+ /** Where an open puts its runner, for a lease that does not run it on this host. */
403
+ placement = null;
404
+ /** Brings a task's work back; absent, it is where the attempt ran. */
405
+ collect;
406
+ /** Waits on what it has running, as a closing Session does; absent, nothing outlives a runner. */
407
+ settle;
408
+ /** The workspace root's repositories; absent, none is known. */
409
+ checkoutRepos;
410
+ records = [];
411
+ freed = () => void 0;
412
+ openHold = null;
413
+ nextId = 0;
414
+ /** Make the next open wait until the returned function is called. */
415
+ holdOpen() {
416
+ let open;
417
+ this.openHold = new Promise((resolve) => {
418
+ open = resolve;
419
+ });
420
+ return open;
421
+ }
422
+ async probe() {
423
+ const at = (/* @__PURE__ */ new Date(0)).toISOString();
424
+ return Object.fromEntries(GUARANTEES.map((g) => [g, { proven: this.provenGuarantees.includes(g), by: "fake probe", at }]));
425
+ }
426
+ takesSlot(request) {
427
+ return request.role === "task" || request.role === "script";
428
+ }
429
+ refusal(request) {
430
+ if (this.refuse?.role === request.role) return this.refuse.reason;
431
+ return this.takesSlot(request) && this.held >= this.limit ? `no slot for ${request.node.taskId ?? request.role}` : null;
432
+ }
433
+ reserve(request) {
434
+ this.requests.push(request);
435
+ const refused = this.refusal(request);
436
+ if (refused) return { refused };
437
+ const holdsSlot = this.takesSlot(request);
438
+ if (holdsSlot) this.held += 1;
439
+ const id = `lease-${++this.nextId}`;
440
+ let live = true;
441
+ return {
442
+ lease: {
443
+ id,
444
+ holdsSlot,
445
+ open: (spec) => this.open(request, spec),
446
+ release: (reason) => {
447
+ if (!live) return;
448
+ live = false;
449
+ this.released.push({ id, reason });
450
+ if (holdsSlot) this.held -= 1;
451
+ this.freed();
452
+ }
453
+ }
454
+ };
455
+ }
456
+ async open(request, spec) {
457
+ this.opened.push({ request, spec });
458
+ const guarantees = await this.probe();
459
+ refuseUnproven(spec.required, guarantees);
460
+ const hold = this.openHold;
461
+ this.openHold = null;
462
+ await hold;
463
+ if (this.openError) throw this.openError;
464
+ const worktree = spec.workspace.kind === "worktree";
465
+ return {
466
+ workspace: { cwd: worktree ? `/fake-lease/${spec.task?.id}` : "/repo", worktree, ...this.repos.length ? { repos: this.repos } : {} },
467
+ guarantees,
468
+ exportResult: (session) => ({ refs: {}, sessionState: { ...session, ...this.sessionState } }),
469
+ ...this.placement ? { placement: this.placement } : {}
470
+ };
471
+ }
472
+ async letGo(taskId, { keep, landing, reason }) {
473
+ await landing;
474
+ this.letGoCalls.push({ taskId, keep, reason });
475
+ }
476
+ async list() {
477
+ return this.records;
478
+ }
479
+ };
343
480
  var FakeWorktreeIsolation = class {
344
481
  availability = { active: true };
482
+ /** What `gitShortfall` answers. */
483
+ gitShortfallText = null;
345
484
  /** The repo paths `startRun` groups: a group of one at `.` unless set. */
346
485
  repos = [SELF_REPO];
347
486
  /** Per task id, the repos it changes; every repo of the group when not listed. */
@@ -377,11 +516,16 @@ var FakeWorktreeIsolation = class {
377
516
  removal = noRemoval();
378
517
  /** Per task id; a task not listed integrates as `merged`. */
379
518
  outcomes = /* @__PURE__ */ new Map();
519
+ /** What `revert` stops with; a revert that goes through when null. */
520
+ revertStop = null;
521
+ /** What `readLanded` finds of a task's work in the user's branch, by task id. */
522
+ inBranch = /* @__PURE__ */ new Map();
380
523
  /** Per task id, what `verifyRepair` finds; a task not listed passes. */
381
524
  repairEvidence = /* @__PURE__ */ new Map();
382
525
  calls = [];
383
526
  holds = /* @__PURE__ */ new Map();
384
527
  verifyHolds = /* @__PURE__ */ new Map();
528
+ revertHold = null;
385
529
  runCount = 0;
386
530
  log(call) {
387
531
  this.calls.push(call);
@@ -398,6 +542,14 @@ var FakeWorktreeIsolation = class {
398
542
  }));
399
543
  return open;
400
544
  }
545
+ /** Make the next `revert` wait, past its `refusal`, until the returned function is called. */
546
+ holdRevert() {
547
+ let open;
548
+ this.revertHold = new Promise((resolve) => {
549
+ open = resolve;
550
+ });
551
+ return open;
552
+ }
401
553
  /** Make `verifyRepair` for this task wait until the returned function is called. */
402
554
  holdVerification(taskId) {
403
555
  let open;
@@ -406,6 +558,9 @@ var FakeWorktreeIsolation = class {
406
558
  }));
407
559
  return open;
408
560
  }
561
+ async gitShortfall() {
562
+ return this.gitShortfallText;
563
+ }
409
564
  async isActive(workspaceRoot) {
410
565
  this.log({ op: "isActive", workspaceRoot });
411
566
  return this.availability;
@@ -415,13 +570,14 @@ var FakeWorktreeIsolation = class {
415
570
  this.log({ op: "stash", workspaceRoot });
416
571
  if (!this.availability.active && this.availability.reason === "dirty") this.availability = { active: true };
417
572
  }
418
- async startRun(workspaceRoot) {
573
+ async startRun(workspaceRoot, stateRoot) {
419
574
  this.log({ op: "startRun", workspaceRoot });
420
575
  if (this.startRunError) throw this.startRunError;
421
576
  const id = `run${++this.runCount}`;
422
577
  return {
423
578
  id,
424
579
  workspaceRoot,
580
+ ...stateRoot !== void 0 ? { stateRoot } : {},
425
581
  repos: this.repos.map((repo) => ({
426
582
  path: repo,
427
583
  root: `${workspaceRoot}/${repo}`.replace(/\/\.$/, ""),
@@ -431,7 +587,9 @@ var FakeWorktreeIsolation = class {
431
587
  })),
432
588
  shared: [...this.shared],
433
589
  sharedRepos: [...this.sharedRepos],
434
- tasks: {}
590
+ tasks: {},
591
+ keepsEveryLanding: true,
592
+ carried: {}
435
593
  };
436
594
  }
437
595
  async prepare(task, run, _intent, opts = {}) {
@@ -494,6 +652,10 @@ var FakeWorktreeIsolation = class {
494
652
  if (outcome === "merged") {
495
653
  delete record.conflictRepo;
496
654
  delete record.conflictFiles;
655
+ for (const repo of changed) {
656
+ ((run.landings ??= {})[task.id] ??= {})[repo] = [...run.landings?.[task.id]?.[repo] ?? [], `tip-${repo}`];
657
+ (run.landedTasks ??= {})[task.id] = { taskId: task.id, order: task.order, title: task.title };
658
+ }
497
659
  } else {
498
660
  record.conflictRepo = this.stopsIn.get(task.id) ?? changed[0] ?? SELF_REPO;
499
661
  const files = this.conflictFiles.get(task.id);
@@ -502,6 +664,44 @@ var FakeWorktreeIsolation = class {
502
664
  }
503
665
  return outcome;
504
666
  }
667
+ /** Newest landing first, read as the plan's highest order: scheduling tests land in plan order. */
668
+ async readLanded(run, taskIds) {
669
+ this.log({ op: "readLanded", taskIds });
670
+ const records = taskIds.flatMap((id) => run.tasks[id] ?? run.landedTasks?.[id] ?? []).sort((a, b) => b.order - a.order);
671
+ const tasks = records.map((r) => ({
672
+ taskId: r.taskId,
673
+ order: r.order,
674
+ title: r.title,
675
+ repos: Object.keys(run.landings?.[r.taskId] ?? {})
676
+ }));
677
+ return { ok: true, tasks, inBranch: records.flatMap((r) => this.inBranch.get(r.taskId) ?? []) };
678
+ }
679
+ /** Like git: the turn, then the judgement, then the records go in one step with the tasks back. */
680
+ async revert(run, taskIds, watch) {
681
+ this.log({ op: "revert", taskIds });
682
+ const hold = this.revertHold;
683
+ this.revertHold = null;
684
+ await hold;
685
+ const reason = watch.refusal();
686
+ if (reason) return { outcome: "refused", reason };
687
+ if (this.revertStop) return this.revertStop;
688
+ const landed = await this.readLanded(run, taskIds);
689
+ if (!landed.ok) return { outcome: "failed", error: landed.error };
690
+ if (landed.inBranch.length > 0) return { outcome: "in-branch", commands: landed.inBranch };
691
+ run.landing = { taskId: taskIds[0], tips: {}, reverts: {} };
692
+ watch.persist();
693
+ delete run.landing;
694
+ for (const id of taskIds) {
695
+ if (run.tasks[id]?.status === "merged") delete run.tasks[id];
696
+ else if (run.tasks[id]?.status === "conflict") run.tasks[id].status = "kept";
697
+ delete run.landings?.[id];
698
+ delete run.landedTasks?.[id];
699
+ delete run.landedOn?.[id];
700
+ }
701
+ run.reverted = true;
702
+ watch.settled(taskIds);
703
+ return { outcome: "reverted", tasks: landed.tasks };
704
+ }
505
705
  async release(run, taskId, opts) {
506
706
  this.log({ op: "release", taskId, keep: opts.keep });
507
707
  const record = run.tasks[taskId];
@@ -543,13 +743,16 @@ var FakeWorktreeIsolation = class {
543
743
  }
544
744
  async findInHead(run) {
545
745
  this.log({ op: "findInHead" });
546
- const found = [];
746
+ const changed = [];
547
747
  for (const record of Object.values(run.tasks)) {
548
- if (record.status !== "merged" || record.inHead || !this.mergedByHand.has(record.taskId)) continue;
549
- record.inHead = true;
550
- found.push(record.taskId);
748
+ if (record.status !== "merged") continue;
749
+ const merged = this.mergedByHand.has(record.taskId);
750
+ if (merged === !!record.inHead) continue;
751
+ if (merged) record.inHead = true;
752
+ else delete record.inHead;
753
+ changed.push(record.taskId);
551
754
  }
552
- return found;
755
+ return changed;
553
756
  }
554
757
  async snapshotTree() {
555
758
  this.log({ op: "snapshotTree" });
@@ -571,6 +774,8 @@ var FakeWorktreeIsolation = class {
571
774
  };
572
775
  // Annotate the CommonJS export names for ESM import in node:
573
776
  0 && (module.exports = {
777
+ FakeEventStore,
778
+ FakeExecutionBackend,
574
779
  FakeRunnerSession,
575
780
  FakeWorktreeIsolation,
576
781
  fakeConfig,