@ordewell/core 0.5.5 → 0.6.0

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 (54) hide show
  1. package/dist/IFileSystem-CFYf4yR2.d.mts +76 -0
  2. package/dist/IFileSystem-D4dAl2r8.d.ts +76 -0
  3. package/dist/{ModeResolver-D-SUFRNF.d.mts → ModeResolver-DVYVRbJp.d.ts} +6 -2
  4. package/dist/{ModeResolver-D3XO0fT9.d.ts → ModeResolver-a4xJFlEp.d.mts} +6 -2
  5. package/dist/Task-CZJr6rmz.d.mts +2121 -0
  6. package/dist/Task-CZJr6rmz.d.ts +2121 -0
  7. package/dist/{chunk-KLN7ELXO.mjs → chunk-2SOVKO7P.mjs} +1561 -1269
  8. package/dist/chunk-2SOVKO7P.mjs.map +1 -0
  9. package/dist/{chunk-T2S5O36I.mjs → chunk-C44UWIAD.mjs} +6 -6
  10. package/dist/chunk-C44UWIAD.mjs.map +1 -0
  11. package/dist/{chunk-HD2FWPRV.mjs → chunk-EDGUFCIR.mjs} +6 -1
  12. package/dist/chunk-EDGUFCIR.mjs.map +1 -0
  13. package/dist/{chunk-UUBGVCGJ.mjs → chunk-OM4D274O.mjs} +20 -2
  14. package/dist/chunk-OM4D274O.mjs.map +1 -0
  15. package/dist/index.d.mts +1594 -803
  16. package/dist/index.d.ts +1594 -803
  17. package/dist/index.js +9540 -6040
  18. package/dist/index.js.map +1 -1
  19. package/dist/index.mjs +8167 -4996
  20. package/dist/index.mjs.map +1 -1
  21. package/dist/order-labels.d.mts +3 -1
  22. package/dist/order-labels.d.ts +3 -1
  23. package/dist/{parsing-BTP4bwkk.d.ts → parsing-Bd6-b644.d.ts} +2 -2
  24. package/dist/{parsing-CDtRSxBY.d.mts → parsing-DIaGZmiQ.d.mts} +2 -2
  25. package/dist/parsing.d.mts +5 -3
  26. package/dist/parsing.d.ts +5 -3
  27. package/dist/parsing.js.map +1 -1
  28. package/dist/parsing.mjs +2 -2
  29. package/dist/{plan-utils-pE4TBwxl.d.mts → plan-utils-Ba-l8EZS.d.mts} +212 -43
  30. package/dist/{plan-utils-BFaPo-IT.d.ts → plan-utils-_T5S7Mnk.d.ts} +212 -43
  31. package/dist/plan-utils.d.mts +5 -4
  32. package/dist/plan-utils.d.ts +5 -4
  33. package/dist/plan-utils.js +352 -64
  34. package/dist/plan-utils.js.map +1 -1
  35. package/dist/plan-utils.mjs +11 -5
  36. package/dist/testing.d.mts +54 -4
  37. package/dist/testing.d.ts +54 -4
  38. package/dist/testing.js +93 -2
  39. package/dist/testing.js.map +1 -1
  40. package/dist/testing.mjs +91 -2
  41. package/dist/testing.mjs.map +1 -1
  42. package/package.json +2 -1
  43. package/skills/grilling/SKILL.md +6 -16
  44. package/skills/improve-codebase-architecture/SKILL.md +1 -1
  45. package/dist/ApprovalPolicy-BVhGdECT.d.mts +0 -79
  46. package/dist/ApprovalPolicy-BVhGdECT.d.ts +0 -79
  47. package/dist/ITerminalRunner-BV9Rd2o9.d.ts +0 -563
  48. package/dist/ITerminalRunner-C77ZNZS9.d.mts +0 -563
  49. package/dist/Task-Vl5Zq_D-.d.mts +0 -825
  50. package/dist/Task-Vl5Zq_D-.d.ts +0 -825
  51. package/dist/chunk-HD2FWPRV.mjs.map +0 -1
  52. package/dist/chunk-KLN7ELXO.mjs.map +0 -1
  53. package/dist/chunk-T2S5O36I.mjs.map +0 -1
  54. package/dist/chunk-UUBGVCGJ.mjs.map +0 -1
@@ -2,11 +2,13 @@ import {
2
2
  CHECKPOINT_TRUNCATE_LENGTH,
3
3
  EMPTY_CONVERSATION,
4
4
  EMPTY_HOLD,
5
+ EMPTY_TASK_LOG,
5
6
  NO_TURN,
6
7
  aheadOfDraft,
7
8
  canMergeTasks,
8
9
  canSetDependencies,
9
10
  canSplitTask,
11
+ capConflictFiles,
10
12
  dependencyCandidates,
11
13
  dependentsOf,
12
14
  drainNext,
@@ -17,6 +19,9 @@ import {
17
19
  outputLines,
18
20
  outputPreview,
19
21
  reduceConversation,
22
+ reduceTaskLog,
23
+ replayTaskLog,
24
+ runnerToolSubject,
20
25
  stopTurn,
21
26
  summarizeToolCall,
22
27
  taskStartedNotice,
@@ -24,16 +29,14 @@ import {
24
29
  truncateCheckpointSummary,
25
30
  unsendAll,
26
31
  unsendLatest
27
- } from "./chunk-KLN7ELXO.mjs";
28
- import "./chunk-HD2FWPRV.mjs";
32
+ } from "./chunk-2SOVKO7P.mjs";
33
+ import "./chunk-EDGUFCIR.mjs";
29
34
  import "./chunk-HPNUFBIT.mjs";
30
- import {
31
- capConflictFiles
32
- } from "./chunk-T2S5O36I.mjs";
33
35
  export {
34
36
  CHECKPOINT_TRUNCATE_LENGTH,
35
37
  EMPTY_CONVERSATION,
36
38
  EMPTY_HOLD,
39
+ EMPTY_TASK_LOG,
37
40
  NO_TURN,
38
41
  aheadOfDraft,
39
42
  canMergeTasks,
@@ -50,6 +53,9 @@ export {
50
53
  outputLines,
51
54
  outputPreview,
52
55
  reduceConversation,
56
+ reduceTaskLog,
57
+ replayTaskLog,
58
+ runnerToolSubject,
53
59
  stopTurn,
54
60
  summarizeToolCall,
55
61
  taskStartedNotice,
@@ -1,7 +1,14 @@
1
- import { i as ITerminalSession, I as IConfig, f as IFileSystem } from './ITerminalRunner-C77ZNZS9.mjs';
2
- import { b as IntegrationDisposal, I as IWorktreeIsolation, c as IsolationAvailability, k as IsolationMergeResult, m as IsolationPruneResult, l as IsolationOutcome, R as RepairEvidence, o as IsolationRun, T as Task, x as PreparedTask, d as IsolationHandoff } from './Task-Vl5Zq_D-.mjs';
3
- import './ApprovalPolicy-BVhGdECT.mjs';
1
+ import { O as IntegrationDisposal, M as ITerminalSession, aQ as StructuredSessionCapability, ax as QueuedTaskMessage, aR as StructuredTurnEnd, aP as StructuredEvent, k as ApprovalDecision, N as IWorktreeIsolation, P as IsolationAvailability, X as IsolationMergeResult, Z as IsolationPruneResult, Y as IsolationOutcome, ay as RepairEvidence, $ as IsolationRun, aU as Task, aq as PreparedTask, Q as IsolationHandoff, J as IConfig } from './Task-CZJr6rmz.mjs';
2
+ import { I as IFileSystem } from './IFileSystem-CFYf4yR2.mjs';
3
+ import 'child_process';
4
+ import 'events';
4
5
 
6
+ /**
7
+ * Let an already-queued promise chain run to its next await before asserting,
8
+ * without a wall-clock sleep. For a chain whose length is not known, drain the
9
+ * microtask queue repeatedly. Only helps a chain that does not wait on a timer.
10
+ */
11
+ declare function flushMicrotasks(rounds?: number): Promise<void>;
5
12
  declare function fakeConfig(overrides?: Partial<IConfig>): IConfig;
6
13
  /**
7
14
  * A complete {@link IFileSystem} stub. Lives here rather than in each package's
@@ -25,6 +32,49 @@ declare class FakeTerminalSession implements ITerminalSession {
25
32
  emitOutput(text: string): void;
26
33
  emitExit(code: number): void;
27
34
  }
35
+ /**
36
+ * A {@link FakeTerminalSession} that is feature-detected as structured
37
+ * (ADR-0018): the turn, queue and native-session calls a test drives, with
38
+ * no protocol behind them. It queues and delivers as `StructuredSession`
39
+ * does — a message to an idle session starts a turn at once, one sent
40
+ * mid-turn goes out as that turn ends, without passing through idle.
41
+ */
42
+ declare class FakeStructuredSession extends FakeTerminalSession implements StructuredSessionCapability {
43
+ sessionId: string | null;
44
+ readonly transport: "structured";
45
+ state: 'working' | 'idle';
46
+ messages: QueuedTaskMessage[];
47
+ /** Every message a turn was started with, in order. */
48
+ delivered: string[];
49
+ interrupts: number;
50
+ private messageCount;
51
+ private turnEndCbs;
52
+ private eventCbs;
53
+ constructor(id?: string, taskId?: string, sessionId?: string | null);
54
+ write(text: string): void;
55
+ turnState(): 'working' | 'idle';
56
+ onTurnEnd(cb: (reason: StructuredTurnEnd) => void): void;
57
+ onEvent(cb: (event: StructuredEvent) => void): void;
58
+ sendMessage(text: string): string;
59
+ removeQueued(id: string): boolean;
60
+ queued(): QueuedTaskMessage[];
61
+ interrupt(): Promise<void>;
62
+ nativeSessionId(): string | null;
63
+ /** Every answer this session was given, in order. */
64
+ answers: Array<{
65
+ id: string;
66
+ decision: ApprovalDecision;
67
+ }>;
68
+ private readonly openPermissions;
69
+ /** The runner asks to use a tool, as `StructuredSession` announces it. */
70
+ requestPermission(id: string, name: string, input: Record<string, unknown>, suggestions?: unknown[]): void;
71
+ /** The runner gives up on a request, as an interrupt makes it. */
72
+ withdrawPermission(id: string): void;
73
+ answerPermission(id: string, decision: ApprovalDecision): boolean;
74
+ emitEvent(event: StructuredEvent): void;
75
+ emitTurnEnd(reason: StructuredTurnEnd): void;
76
+ private deliver;
77
+ }
28
78
  type FakeIsolationCall = {
29
79
  op: 'isActive';
30
80
  workspaceRoot: string;
@@ -118,4 +168,4 @@ declare class FakeWorktreeIsolation implements IWorktreeIsolation {
118
168
  sweep(): Promise<void>;
119
169
  }
120
170
 
121
- export { type FakeIsolationCall, FakeTerminalSession, FakeWorktreeIsolation, fakeConfig, fakeFileSystem };
171
+ export { type FakeIsolationCall, FakeStructuredSession, FakeTerminalSession, FakeWorktreeIsolation, fakeConfig, fakeFileSystem, flushMicrotasks };
package/dist/testing.d.ts CHANGED
@@ -1,7 +1,14 @@
1
- import { i as ITerminalSession, I as IConfig, f as IFileSystem } from './ITerminalRunner-BV9Rd2o9.js';
2
- import { b as IntegrationDisposal, I as IWorktreeIsolation, c as IsolationAvailability, k as IsolationMergeResult, m as IsolationPruneResult, l as IsolationOutcome, R as RepairEvidence, o as IsolationRun, T as Task, x as PreparedTask, d as IsolationHandoff } from './Task-Vl5Zq_D-.js';
3
- import './ApprovalPolicy-BVhGdECT.js';
1
+ import { O as IntegrationDisposal, M as ITerminalSession, aQ as StructuredSessionCapability, ax as QueuedTaskMessage, aR as StructuredTurnEnd, aP as StructuredEvent, k as ApprovalDecision, N as IWorktreeIsolation, P as IsolationAvailability, X as IsolationMergeResult, Z as IsolationPruneResult, Y as IsolationOutcome, ay as RepairEvidence, $ as IsolationRun, aU as Task, aq as PreparedTask, Q as IsolationHandoff, J as IConfig } from './Task-CZJr6rmz.js';
2
+ import { I as IFileSystem } from './IFileSystem-D4dAl2r8.js';
3
+ import 'child_process';
4
+ import 'events';
4
5
 
6
+ /**
7
+ * Let an already-queued promise chain run to its next await before asserting,
8
+ * without a wall-clock sleep. For a chain whose length is not known, drain the
9
+ * microtask queue repeatedly. Only helps a chain that does not wait on a timer.
10
+ */
11
+ declare function flushMicrotasks(rounds?: number): Promise<void>;
5
12
  declare function fakeConfig(overrides?: Partial<IConfig>): IConfig;
6
13
  /**
7
14
  * A complete {@link IFileSystem} stub. Lives here rather than in each package's
@@ -25,6 +32,49 @@ declare class FakeTerminalSession implements ITerminalSession {
25
32
  emitOutput(text: string): void;
26
33
  emitExit(code: number): void;
27
34
  }
35
+ /**
36
+ * A {@link FakeTerminalSession} that is feature-detected as structured
37
+ * (ADR-0018): the turn, queue and native-session calls a test drives, with
38
+ * no protocol behind them. It queues and delivers as `StructuredSession`
39
+ * does — a message to an idle session starts a turn at once, one sent
40
+ * mid-turn goes out as that turn ends, without passing through idle.
41
+ */
42
+ declare class FakeStructuredSession extends FakeTerminalSession implements StructuredSessionCapability {
43
+ sessionId: string | null;
44
+ readonly transport: "structured";
45
+ state: 'working' | 'idle';
46
+ messages: QueuedTaskMessage[];
47
+ /** Every message a turn was started with, in order. */
48
+ delivered: string[];
49
+ interrupts: number;
50
+ private messageCount;
51
+ private turnEndCbs;
52
+ private eventCbs;
53
+ constructor(id?: string, taskId?: string, sessionId?: string | null);
54
+ write(text: string): void;
55
+ turnState(): 'working' | 'idle';
56
+ onTurnEnd(cb: (reason: StructuredTurnEnd) => void): void;
57
+ onEvent(cb: (event: StructuredEvent) => void): void;
58
+ sendMessage(text: string): string;
59
+ removeQueued(id: string): boolean;
60
+ queued(): QueuedTaskMessage[];
61
+ interrupt(): Promise<void>;
62
+ nativeSessionId(): string | null;
63
+ /** Every answer this session was given, in order. */
64
+ answers: Array<{
65
+ id: string;
66
+ decision: ApprovalDecision;
67
+ }>;
68
+ private readonly openPermissions;
69
+ /** The runner asks to use a tool, as `StructuredSession` announces it. */
70
+ requestPermission(id: string, name: string, input: Record<string, unknown>, suggestions?: unknown[]): void;
71
+ /** The runner gives up on a request, as an interrupt makes it. */
72
+ withdrawPermission(id: string): void;
73
+ answerPermission(id: string, decision: ApprovalDecision): boolean;
74
+ emitEvent(event: StructuredEvent): void;
75
+ emitTurnEnd(reason: StructuredTurnEnd): void;
76
+ private deliver;
77
+ }
28
78
  type FakeIsolationCall = {
29
79
  op: 'isActive';
30
80
  workspaceRoot: string;
@@ -118,4 +168,4 @@ declare class FakeWorktreeIsolation implements IWorktreeIsolation {
118
168
  sweep(): Promise<void>;
119
169
  }
120
170
 
121
- export { type FakeIsolationCall, FakeTerminalSession, FakeWorktreeIsolation, fakeConfig, fakeFileSystem };
171
+ export { type FakeIsolationCall, FakeStructuredSession, FakeTerminalSession, FakeWorktreeIsolation, fakeConfig, fakeFileSystem, flushMicrotasks };
package/dist/testing.js CHANGED
@@ -20,10 +20,12 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/testing.ts
21
21
  var testing_exports = {};
22
22
  __export(testing_exports, {
23
+ FakeStructuredSession: () => FakeStructuredSession,
23
24
  FakeTerminalSession: () => FakeTerminalSession,
24
25
  FakeWorktreeIsolation: () => FakeWorktreeIsolation,
25
26
  fakeConfig: () => fakeConfig,
26
- fakeFileSystem: () => fakeFileSystem
27
+ fakeFileSystem: () => fakeFileSystem,
28
+ flushMicrotasks: () => flushMicrotasks
27
29
  });
28
30
  module.exports = __toCommonJS(testing_exports);
29
31
 
@@ -52,6 +54,9 @@ function handoffOf(run) {
52
54
  }
53
55
 
54
56
  // src/testing.ts
57
+ async function flushMicrotasks(rounds = 20) {
58
+ for (let i = 0; i < rounds; i += 1) await Promise.resolve();
59
+ }
55
60
  function fakeConfig(overrides = {}) {
56
61
  return {
57
62
  aiProvider: "openrouter",
@@ -138,6 +143,90 @@ var FakeTerminalSession = class {
138
143
  for (const cb of this.exitCbs) cb(code);
139
144
  }
140
145
  };
146
+ var FakeStructuredSession = class extends FakeTerminalSession {
147
+ constructor(id = "s1", taskId = "t1", sessionId = "native-1") {
148
+ super(id, taskId);
149
+ this.sessionId = sessionId;
150
+ }
151
+ sessionId;
152
+ transport = "structured";
153
+ state = "working";
154
+ messages = [];
155
+ /** Every message a turn was started with, in order. */
156
+ delivered = [];
157
+ interrupts = 0;
158
+ messageCount = 0;
159
+ turnEndCbs = [];
160
+ eventCbs = [];
161
+ write(text) {
162
+ super.write(text);
163
+ const message = text.trim();
164
+ if (message) this.sendMessage(message);
165
+ }
166
+ turnState() {
167
+ return this.state;
168
+ }
169
+ onTurnEnd(cb) {
170
+ this.turnEndCbs.push(cb);
171
+ }
172
+ onEvent(cb) {
173
+ this.eventCbs.push(cb);
174
+ }
175
+ sendMessage(text) {
176
+ this.messageCount += 1;
177
+ const id = `msg-${this.messageCount}`;
178
+ if (this.state === "idle") this.deliver(text);
179
+ else this.messages.push({ id, text });
180
+ return id;
181
+ }
182
+ removeQueued(id) {
183
+ const before = this.messages.length;
184
+ this.messages = this.messages.filter((m) => m.id !== id);
185
+ return this.messages.length < before;
186
+ }
187
+ queued() {
188
+ return this.messages.map((m) => ({ ...m }));
189
+ }
190
+ async interrupt() {
191
+ this.interrupts += 1;
192
+ if (this.state === "working") this.emitTurnEnd("interrupted");
193
+ }
194
+ nativeSessionId() {
195
+ return this.sessionId;
196
+ }
197
+ /** Every answer this session was given, in order. */
198
+ answers = [];
199
+ openPermissions = /* @__PURE__ */ new Set();
200
+ /** The runner asks to use a tool, as `StructuredSession` announces it. */
201
+ requestPermission(id, name, input, suggestions = []) {
202
+ this.openPermissions.add(id);
203
+ this.emitEvent({ type: "permission_request", id, name, detail: JSON.stringify(input), input, suggestions });
204
+ }
205
+ /** The runner gives up on a request, as an interrupt makes it. */
206
+ withdrawPermission(id) {
207
+ if (this.openPermissions.delete(id)) this.emitEvent({ type: "permission_withdrawn", id });
208
+ }
209
+ answerPermission(id, decision) {
210
+ if (!this.openPermissions.delete(id)) return false;
211
+ this.answers.push({ id, decision });
212
+ this.emitEvent({ type: "permission_decided", id, decision });
213
+ return true;
214
+ }
215
+ emitEvent(event) {
216
+ for (const cb of this.eventCbs) cb(event);
217
+ }
218
+ emitTurnEnd(reason) {
219
+ const next = this.messages.shift();
220
+ if (!next) this.state = "idle";
221
+ for (const cb of this.turnEndCbs) cb(reason);
222
+ if (next) this.deliver(next.text);
223
+ }
224
+ deliver(text) {
225
+ this.state = "working";
226
+ this.delivered.push(text);
227
+ this.emitEvent({ type: "turn_start", text });
228
+ }
229
+ };
141
230
  var FakeWorktreeIsolation = class {
142
231
  availability = { active: true };
143
232
  /** The repo paths `startRun` groups: a group of one at `.` unless set. */
@@ -308,9 +397,11 @@ var FakeWorktreeIsolation = class {
308
397
  };
309
398
  // Annotate the CommonJS export names for ESM import in node:
310
399
  0 && (module.exports = {
400
+ FakeStructuredSession,
311
401
  FakeTerminalSession,
312
402
  FakeWorktreeIsolation,
313
403
  fakeConfig,
314
- fakeFileSystem
404
+ fakeFileSystem,
405
+ flushMicrotasks
315
406
  });
316
407
  //# sourceMappingURL=testing.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/testing.ts","../src/services/isolationRecord.ts"],"sourcesContent":["import type { IConfig } from './interfaces/IConfig';\nimport type { IFileSystem, ToolOutcome } from './interfaces/IFileSystem';\nimport type { ITerminalSession } from './interfaces/ITerminalRunner';\nimport type {\n IsolationAvailability,\n IsolationHandoff,\n IsolationMergeResult,\n IsolationOutcome,\n IsolationPruneResult,\n IsolationRun,\n IntegrationDisposal,\n PreparedTask,\n IWorktreeIsolation,\n RepairEvidence,\n} from './interfaces/IWorktreeIsolation';\nimport type { Task } from './models/Task';\nimport { handoffOf, integrationBranchFor, SELF_REPO } from './services/isolationRecord';\n\nexport function fakeConfig(overrides: Partial<IConfig> = {}): IConfig {\n return {\n aiProvider: 'openrouter',\n apiKey: 'sk-test',\n planningModel: 'test-model',\n enabledRunners: ['claude-code'],\n maxParallelSessions: 3,\n researchEnabled: false,\n researchMaxSteps: 10,\n researchMaxFileSize: 100_000,\n openAiBaseUrl: 'https://api.openai.com/v1',\n openAiApiKey: '',\n openrouterKey: '',\n geminiKey: '',\n openaiCompatibleBaseUrl: '',\n openaiCompatibleApiKey: '',\n orchestratorModel: '',\n researchSubagentModel: '',\n geminiModel: '',\n planMapEnabled: true,\n autonomousMode: true,\n // Off so an orchestrator built without an injected isolation never runs\n // real git against whatever repository the tests happen to run in.\n worktreeIsolation: false,\n workspaceRepos: [],\n worktreeLinks: [],\n // Off so a scripted conflict stays a conflict unless a test opts into repairing it.\n conflictRepairAttempts: 0,\n approvalMode: 'ask',\n approvalPreApproved: [],\n setProviderModelLists: () => {},\n getProviderBaseUrl: () => '',\n getProviderApiKey: () => '',\n ...overrides,\n };\n}\n\nconst EMPTY_OUTCOME: ToolOutcome = { success: false, output: '', truncated: false };\n\n/**\n * A complete {@link IFileSystem} stub. Lives here rather than in each package's\n * test folder so adding a tool to the interface is one edit, not one per suite.\n */\nexport function fakeFileSystem(overrides: Partial<IFileSystem> = {}): IFileSystem {\n return {\n readFile: async () => EMPTY_OUTCOME,\n readFiles: async () => EMPTY_OUTCOME,\n glob: async () => EMPTY_OUTCOME,\n grep: async () => EMPTY_OUTCOME,\n findSymbol: async () => EMPTY_OUTCOME,\n listDir: async () => EMPTY_OUTCOME,\n bash: async () => EMPTY_OUTCOME,\n getWorkspaceRoot: () => '/workspace',\n ...overrides,\n };\n}\n\nexport class FakeTerminalSession implements ITerminalSession {\n private outputCbs: Array<(text: string) => void> = [];\n private exitCbs: Array<(code: number) => void> = [];\n output = '';\n written: string[] = [];\n killed = false;\n\n constructor(public id = 's1', public taskId = 't1') {}\n\n onOutput(cb: (text: string) => void): void { this.outputCbs.push(cb); }\n onExit(cb: (code: number) => void): void { this.exitCbs.push(cb); }\n kill(): void { this.killed = true; }\n getOutput(): string { return this.output; }\n write(text: string): void { this.written.push(text); }\n\n emitOutput(text: string): void {\n this.output += text;\n for (const cb of this.outputCbs) cb(text);\n }\n emitExit(code: number): void {\n for (const cb of this.exitCbs) cb(code);\n }\n}\n\nexport type FakeIsolationCall =\n | { op: 'isActive'; workspaceRoot: string }\n | { op: 'stash'; workspaceRoot: string }\n | { op: 'startRun'; workspaceRoot: string }\n | { op: 'prepare' | 'reopen' | 'verifyRepair'; taskId: string }\n | { op: 'integrate'; taskId: string }\n | { op: 'release'; taskId: string; keep: boolean }\n | { op: 'handoff' | 'pruneOrphans' | 'reviewDiff' | 'mergeIntoCheckedOut' | 'sweep' }\n | { op: 'discard'; integration: IntegrationDisposal };\n\n/**\n * An in-memory {@link IWorktreeIsolation} for scheduling tests: no git, no\n * filesystem. Every call is logged in `calls`; `prepare` hands back a\n * deterministic fake cwd. Set `availability` to exercise the fallbacks, `outcomes`\n * to script a conflict, and `holdIntegration` to keep a task un-integrated so a\n * test can observe that its dependents wait. `repos` makes the run a group of\n * several; `changes` and `stopsIn` say which of them a task changes and where\n * its landing stops. `repairEvidence` scripts what a conflict repair's\n * evidence check finds.\n */\nexport class FakeWorktreeIsolation implements IWorktreeIsolation {\n availability: IsolationAvailability = { active: true };\n /** The repo paths `startRun` groups: a group of one at `.` unless set. */\n repos: string[] = [SELF_REPO];\n /** Per task id, the repos it changes; every repo of the group when not listed. */\n changes = new Map<string, string[]>();\n /** Per task id, the repo its landing stops in when its outcome is not `merged`; its first changed repo when not listed. */\n stopsIn = new Map<string, string>();\n /** Per task id, the files a `conflict` outcome names; empty when not listed. */\n conflictFiles = new Map<string, string[]>();\n /** What `mergeIntoCheckedOut` answers. */\n mergeResult: IsolationMergeResult = { outcome: 'merged' };\n /** What `startRun` shares and `prepare` copies, to exercise their notices. */\n shared: string[] = [];\n sharedRepos: string[] = [];\n copied: string[] = [];\n /** Set to make `startRun` throw, as git does when no repo of the group can be isolated. */\n startRunError: Error | null = null;\n /** Set to make `discard` / `sweep` throw after logging the call. */\n discardError: Error | null = null;\n sweepError: Error | null = null;\n /** What `pruneOrphans` reports as kept back from the sweep. */\n keptOnPrune: IsolationPruneResult['kept'] = [];\n /** Per task id; a task not listed integrates as `merged`. */\n outcomes = new Map<string, IsolationOutcome>();\n /** Per task id, what `verifyRepair` finds; a task not listed passes. */\n repairEvidence = new Map<string, RepairEvidence>();\n calls: FakeIsolationCall[] = [];\n private holds = new Map<string, Promise<void>>();\n private runCount = 0;\n\n private log(call: FakeIsolationCall): void { this.calls.push(call); }\n\n /** Task ids in the order `op` was called for them. */\n taskIdsFor(op: 'prepare' | 'reopen' | 'verifyRepair' | 'integrate' | 'release'): string[] {\n return this.calls.flatMap((c) => (c.op === op && 'taskId' in c ? [c.taskId] : []));\n }\n\n /** Make `integrate` for this task wait until the returned function is called. */\n holdIntegration(taskId: string): () => void {\n let open!: () => void;\n this.holds.set(taskId, new Promise<void>((resolve) => { open = resolve; }));\n return open;\n }\n\n async isActive(workspaceRoot: string): Promise<IsolationAvailability> {\n this.log({ op: 'isActive', workspaceRoot });\n return this.availability;\n }\n\n /** Like git: once the tracked changes are stashed, the tree is no longer dirty. */\n async stash(workspaceRoot: string): Promise<void> {\n this.log({ op: 'stash', workspaceRoot });\n if (!this.availability.active && this.availability.reason === 'dirty') this.availability = { active: true };\n }\n\n async startRun(workspaceRoot: string): Promise<IsolationRun> {\n this.log({ op: 'startRun', workspaceRoot });\n if (this.startRunError) throw this.startRunError;\n const id = `run${++this.runCount}`;\n return {\n id,\n workspaceRoot,\n repos: this.repos.map((repo) => ({\n path: repo, root: `${workspaceRoot}/${repo}`.replace(/\\/\\.$/, ''), baseRef: 'base0000', baseBranch: 'main', integrationBranch: integrationBranchFor(id),\n })),\n shared: [...this.shared],\n sharedRepos: [...this.sharedRepos],\n tasks: {},\n };\n }\n\n async prepare(task: Task, run: IsolationRun): Promise<PreparedTask> {\n this.log({ op: 'prepare', taskId: task.id });\n const name = `${task.order}-${task.id}`;\n const cwd = `/fake-worktrees/${run.id}/${name}`;\n const branch = `ordewell/${run.id}/${name}`;\n run.tasks[task.id] = {\n taskId: task.id, order: task.order, title: task.title, branch, workspace: cwd, status: 'active',\n repos: Object.fromEntries(run.repos.map((r) => [r.path, { worktree: r.path === SELF_REPO ? cwd : `${cwd}/${r.path}`, linked: [] }])),\n };\n return { cwd, branch, copied: [...this.copied] };\n }\n\n /** Like git: the same cwd, each changed repo's tip recorded, the repair counted. */\n async reopen(task: Task, run: IsolationRun): Promise<PreparedTask> {\n this.log({ op: 'reopen', taskId: task.id });\n const record = run.tasks[task.id];\n if (record?.status !== 'conflict') throw new Error(`Task ${task.order} has no conflict to repair`);\n const changed = Object.entries(record.repos).filter(([repo, entry]) => entry.changed || repo === record.conflictRepo).map(([repo]) => repo);\n const files = (record.conflictFiles ?? []).map((file) => (record.conflictRepo && record.conflictRepo !== SELF_REPO ? `${record.conflictRepo}/${file}` : file));\n record.repairs = (record.repairs ?? 0) + 1;\n record.repairBase = Object.fromEntries(changed.map((repo) => [repo, `tip-${repo}`]));\n record.repairedFiles = [...new Set([...(record.repairedFiles ?? []), ...files])];\n record.status = 'repairing';\n return { cwd: record.workspace, branch: record.branch, copied: [] };\n }\n\n async verifyRepair(task: Task, _run: IsolationRun): Promise<RepairEvidence> {\n this.log({ op: 'verifyRepair', taskId: task.id });\n return this.repairEvidence.get(task.id) ?? { ok: true };\n }\n\n /** Like git: the landing is recorded and persisted before the (held) merge, and cleared once it settles. */\n async integrate(task: Task, run: IsolationRun, persist: () => void = () => undefined): Promise<IsolationOutcome> {\n this.log({ op: 'integrate', taskId: task.id });\n const record = run.tasks[task.id];\n if (!record) return 'failed';\n const changed = this.changes.get(task.id) ?? run.repos.map((r) => r.path);\n for (const [repo, entry] of Object.entries(record.repos)) entry.changed = changed.includes(repo) || (entry.changed ?? false);\n if (changed.length > 0) {\n run.landing = { taskId: task.id, tips: Object.fromEntries(changed.map((repo) => [repo, `tip-${repo}`])) };\n persist();\n }\n await this.holds.get(task.id);\n delete run.landing;\n const outcome = this.outcomes.get(task.id) ?? 'merged';\n record.status = outcome;\n delete record.repairBase;\n if (outcome === 'merged') {\n delete record.conflictRepo;\n delete record.conflictFiles;\n } else {\n record.conflictRepo = this.stopsIn.get(task.id) ?? changed[0] ?? SELF_REPO;\n const files = this.conflictFiles.get(task.id);\n if (files) record.conflictFiles = files;\n else delete record.conflictFiles;\n }\n return outcome;\n }\n\n async release(run: IsolationRun, taskId: string, opts: { keep: boolean }): Promise<void> {\n this.log({ op: 'release', taskId, keep: opts.keep });\n const record = run.tasks[taskId];\n if (!opts.keep) delete run.tasks[taskId];\n else if (record?.status === 'active') record.status = 'kept';\n else if (record?.status === 'repairing') {\n record.status = 'conflict';\n delete record.repairBase;\n }\n }\n\n async handoff(run: IsolationRun): Promise<IsolationHandoff> {\n this.log({ op: 'handoff' });\n return handoffOf(run);\n }\n\n async pruneOrphans(): Promise<IsolationPruneResult> {\n this.log({ op: 'pruneOrphans' });\n return { kept: [...this.keptOnPrune] };\n }\n async reviewDiff(): Promise<string> { this.log({ op: 'reviewDiff' }); return ''; }\n async mergeIntoCheckedOut(): Promise<IsolationMergeResult> { this.log({ op: 'mergeIntoCheckedOut' }); return this.mergeResult; }\n async discard(_run: IsolationRun, opts: { integration: IntegrationDisposal }): Promise<void> {\n this.log({ op: 'discard', integration: opts.integration });\n if (this.discardError) throw this.discardError;\n }\n async sweep(): Promise<void> {\n this.log({ op: 'sweep' });\n if (this.sweepError) throw this.sweepError;\n }\n}\n","import * as path from 'path';\nimport type {\n IsolationHandoff,\n IsolationLandedTask,\n IsolationRepo,\n IsolationRun,\n IsolationTaskRecord,\n IsolationTaskStatus,\n PlanIsolation,\n RepoGroupLayout,\n TaskIsolation,\n} from '../interfaces/IWorktreeIsolation';\n\n/** The path of the repo in a workspace that is itself the repository. */\nexport const SELF_REPO = '.';\n\nexport function integrationBranchFor(runId: string): string {\n return `ordewell/${runId}/integration`;\n}\n\nexport function repoRootOf(workspaceRoot: string, repoPath: string): string {\n return path.join(workspaceRoot, repoPath);\n}\n\n/** The group a run isolates, as the planner is told it. */\nexport function layoutOf(run: IsolationRun): RepoGroupLayout {\n return { repos: run.repos.map((r) => r.path), shared: [...run.shared] };\n}\n\n/** The run's integration branch for a one-line notice: every repo's has the same name. */\nexport function integrationBranchNameOf(run: IsolationRun): string {\n return [...new Set(run.repos.map((r) => r.integrationBranch))].join(', ');\n}\n\nconst ISOLATION_STATE: Record<IsolationTaskStatus, Exclude<TaskIsolation['state'], 'none'>> = {\n active: 'active',\n merged: 'integrated',\n conflict: 'conflict',\n repairing: 'repairing',\n kept: 'kept',\n failed: 'kept',\n};\n\n/** `repairLimit` is `conflictRepairAttempts` as it stands now, which is what the next repair will be held to. */\nexport function taskIsolationOf(record: IsolationTaskRecord, repairLimit: number): TaskIsolation {\n return {\n state: ISOLATION_STATE[record.status],\n branch: record.branch,\n worktree: record.workspace,\n repos: Object.entries(record.repos).filter(([, r]) => r.changed).map(([repoPath]) => repoPath),\n ...(record.conflictRepo ? { conflictRepo: record.conflictRepo } : {}),\n ...(record.conflictFiles?.length ? { conflictFiles: record.conflictFiles } : {}),\n ...(record.repairs ? { repair: { attempt: record.repairs, limit: repairLimit } } : {}),\n ...(record.repairedFiles?.length ? { repairedFiles: record.repairedFiles } : {}),\n };\n}\n\n/** A conflict's files as one surface shows them: every one, up to `max`, then how many more. */\nexport function capConflictFiles(files: string[], max = 5): string {\n return files.length <= max ? files.join(', ') : `${files.slice(0, max).join(', ')}, +${files.length - max} more`;\n}\n\n/** What a run hands over: each repo's integration branch and base, and what landed, in plan order. */\nexport function handoffOf(run: IsolationRun): IsolationHandoff {\n const merged = Object.values(run.tasks)\n .filter((r) => r.status === 'merged')\n .sort((a, b) => a.order - b.order);\n const entry = (r: IsolationTaskRecord): IsolationLandedTask => ({\n taskId: r.taskId, order: r.order, title: r.title,\n ...(r.repairedFiles?.length ? { repairedFiles: r.repairedFiles } : {}),\n });\n return {\n repos: run.repos.map((repo) => ({\n path: repo.path,\n integrationBranch: repo.integrationBranch,\n baseRef: repo.baseRef,\n landed: merged.filter((r) => r.repos[repo.path]?.changed).map(entry),\n })),\n landed: merged.map(entry),\n };\n}\n\n/** A task record as ADR-0013 persisted it, for one repository. */\nexport interface Adr0013TaskRecord {\n taskId: string;\n order: number;\n title: string;\n branch: string;\n worktree: string;\n status: IsolationTaskStatus;\n linked: string[];\n}\n\n/** A run as ADR-0013 persisted it (0.4.23): one repository, its refs on the run itself. */\nexport interface Adr0013IsolationRun {\n id: string;\n workspaceRoot: string;\n baseRef: string;\n baseBranch?: string;\n integrationBranch: string;\n tasks: Record<string, Adr0013TaskRecord>;\n}\n\nexport interface Adr0013PlanIsolation {\n run: Adr0013IsolationRun;\n resolvers: Record<string, string>;\n}\n\nfunction isAdr0013(run: IsolationRun | Adr0013IsolationRun): run is Adr0013IsolationRun {\n return 'integrationBranch' in run && typeof run.integrationBranch === 'string';\n}\n\nfunction fromAdr0013Task(r: Adr0013TaskRecord): IsolationTaskRecord {\n return {\n taskId: r.taskId,\n order: r.order,\n title: r.title,\n branch: r.branch,\n workspace: r.worktree,\n status: r.status,\n // A merged task's branch was merged into the one repo there was; what it\n // changed was never recorded, and landing is what `changed` stands for.\n repos: { [SELF_REPO]: { worktree: r.worktree, linked: r.linked ?? [], ...(r.status === 'merged' ? { changed: true } : {}) } },\n ...(r.status === 'conflict' ? { conflictRepo: SELF_REPO } : {}),\n };\n}\n\n/**\n * A persisted plan isolation in today's shape. A run saved in the ADR-0013\n * format — recognised by its own `integrationBranch` — becomes a group of one\n * at `.`, keeping its branches, so a session saved by 0.4.23 resumes and hands\n * off exactly as it would have.\n */\nexport function migratePlanIsolation(state: PlanIsolation | Adr0013PlanIsolation): PlanIsolation {\n const { run, resolvers } = state;\n if (!isAdr0013(run)) return { run, resolvers: resolvers ?? {} };\n const repo: IsolationRepo = {\n path: SELF_REPO,\n root: repoRootOf(run.workspaceRoot, SELF_REPO),\n baseRef: run.baseRef,\n ...(run.baseBranch ? { baseBranch: run.baseBranch } : {}),\n integrationBranch: run.integrationBranch,\n };\n const tasks = Object.fromEntries(Object.entries(run.tasks ?? {}).map(([id, r]) => [id, fromAdr0013Task(r)]));\n return { run: { id: run.id, workspaceRoot: run.workspaceRoot, repos: [repo], shared: [], sharedRepos: [], tasks }, resolvers: resolvers ?? {} };\n}\n\n/** Bring a loaded plan's isolation record, if any, to today's shape in place. */\nexport function migratePlanStateIsolation(plan: { isolation?: PlanIsolation | Adr0013PlanIsolation }): void {\n if (plan.isolation?.run) plan.isolation = migratePlanIsolation(plan.isolation);\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/testing.ts","../src/services/isolationRecord.ts"],"sourcesContent":["import type { IConfig } from './interfaces/IConfig';\nimport type { IFileSystem, ToolOutcome } from './interfaces/IFileSystem';\nimport type { ITerminalSession, QueuedTaskMessage, StructuredEvent, StructuredSessionCapability, StructuredTurnEnd } from './interfaces/ITerminalRunner';\nimport type { ApprovalDecision } from './interfaces/IApproval';\nimport type {\n IsolationAvailability,\n IsolationHandoff,\n IsolationMergeResult,\n IsolationOutcome,\n IsolationPruneResult,\n IsolationRun,\n IntegrationDisposal,\n PreparedTask,\n IWorktreeIsolation,\n RepairEvidence,\n} from './interfaces/IWorktreeIsolation';\nimport type { Task } from './models/Task';\nimport { handoffOf, integrationBranchFor, SELF_REPO } from './services/isolationRecord';\n\n/**\n * Let an already-queued promise chain run to its next await before asserting,\n * without a wall-clock sleep. For a chain whose length is not known, drain the\n * microtask queue repeatedly. Only helps a chain that does not wait on a timer.\n */\nexport async function flushMicrotasks(rounds = 20): Promise<void> {\n for (let i = 0; i < rounds; i += 1) await Promise.resolve();\n}\n\nexport function fakeConfig(overrides: Partial<IConfig> = {}): IConfig {\n return {\n aiProvider: 'openrouter',\n apiKey: 'sk-test',\n planningModel: 'test-model',\n enabledRunners: ['claude-code'],\n maxParallelSessions: 3,\n researchEnabled: false,\n researchMaxSteps: 10,\n researchMaxFileSize: 100_000,\n openAiBaseUrl: 'https://api.openai.com/v1',\n openAiApiKey: '',\n openrouterKey: '',\n geminiKey: '',\n openaiCompatibleBaseUrl: '',\n openaiCompatibleApiKey: '',\n orchestratorModel: '',\n researchSubagentModel: '',\n geminiModel: '',\n planMapEnabled: true,\n autonomousMode: true,\n // Off so an orchestrator built without an injected isolation never runs\n // real git against whatever repository the tests happen to run in.\n worktreeIsolation: false,\n workspaceRepos: [],\n worktreeLinks: [],\n // Off so a scripted conflict stays a conflict unless a test opts into repairing it.\n conflictRepairAttempts: 0,\n approvalMode: 'ask',\n approvalPreApproved: [],\n setProviderModelLists: () => {},\n getProviderBaseUrl: () => '',\n getProviderApiKey: () => '',\n ...overrides,\n };\n}\n\nconst EMPTY_OUTCOME: ToolOutcome = { success: false, output: '', truncated: false };\n\n/**\n * A complete {@link IFileSystem} stub. Lives here rather than in each package's\n * test folder so adding a tool to the interface is one edit, not one per suite.\n */\nexport function fakeFileSystem(overrides: Partial<IFileSystem> = {}): IFileSystem {\n return {\n readFile: async () => EMPTY_OUTCOME,\n readFiles: async () => EMPTY_OUTCOME,\n glob: async () => EMPTY_OUTCOME,\n grep: async () => EMPTY_OUTCOME,\n findSymbol: async () => EMPTY_OUTCOME,\n listDir: async () => EMPTY_OUTCOME,\n bash: async () => EMPTY_OUTCOME,\n getWorkspaceRoot: () => '/workspace',\n ...overrides,\n };\n}\n\nexport class FakeTerminalSession implements ITerminalSession {\n private outputCbs: Array<(text: string) => void> = [];\n private exitCbs: Array<(code: number) => void> = [];\n output = '';\n written: string[] = [];\n killed = false;\n\n constructor(public id = 's1', public taskId = 't1') {}\n\n onOutput(cb: (text: string) => void): void { this.outputCbs.push(cb); }\n onExit(cb: (code: number) => void): void { this.exitCbs.push(cb); }\n kill(): void { this.killed = true; }\n getOutput(): string { return this.output; }\n write(text: string): void { this.written.push(text); }\n\n emitOutput(text: string): void {\n this.output += text;\n for (const cb of this.outputCbs) cb(text);\n }\n emitExit(code: number): void {\n for (const cb of this.exitCbs) cb(code);\n }\n}\n\n/**\n * A {@link FakeTerminalSession} that is feature-detected as structured\n * (ADR-0018): the turn, queue and native-session calls a test drives, with\n * no protocol behind them. It queues and delivers as `StructuredSession`\n * does — a message to an idle session starts a turn at once, one sent\n * mid-turn goes out as that turn ends, without passing through idle.\n */\nexport class FakeStructuredSession extends FakeTerminalSession implements StructuredSessionCapability {\n readonly transport = 'structured' as const;\n state: 'working' | 'idle' = 'working';\n messages: QueuedTaskMessage[] = [];\n /** Every message a turn was started with, in order. */\n delivered: string[] = [];\n interrupts = 0;\n private messageCount = 0;\n private turnEndCbs: Array<(reason: StructuredTurnEnd) => void> = [];\n private eventCbs: Array<(event: StructuredEvent) => void> = [];\n\n constructor(id = 's1', taskId = 't1', public sessionId: string | null = 'native-1') {\n super(id, taskId);\n }\n\n override write(text: string): void {\n super.write(text);\n const message = text.trim();\n if (message) this.sendMessage(message);\n }\n\n turnState(): 'working' | 'idle' { return this.state; }\n onTurnEnd(cb: (reason: StructuredTurnEnd) => void): void { this.turnEndCbs.push(cb); }\n onEvent(cb: (event: StructuredEvent) => void): void { this.eventCbs.push(cb); }\n sendMessage(text: string): string {\n this.messageCount += 1;\n const id = `msg-${this.messageCount}`;\n if (this.state === 'idle') this.deliver(text);\n else this.messages.push({ id, text });\n return id;\n }\n removeQueued(id: string): boolean {\n const before = this.messages.length;\n this.messages = this.messages.filter((m) => m.id !== id);\n return this.messages.length < before;\n }\n queued(): QueuedTaskMessage[] { return this.messages.map((m) => ({ ...m })); }\n async interrupt(): Promise<void> {\n this.interrupts += 1;\n if (this.state === 'working') this.emitTurnEnd('interrupted');\n }\n nativeSessionId(): string | null { return this.sessionId; }\n\n /** Every answer this session was given, in order. */\n answers: Array<{ id: string; decision: ApprovalDecision }> = [];\n private readonly openPermissions = new Set<string>();\n\n /** The runner asks to use a tool, as `StructuredSession` announces it. */\n requestPermission(id: string, name: string, input: Record<string, unknown>, suggestions: unknown[] = []): void {\n this.openPermissions.add(id);\n this.emitEvent({ type: 'permission_request', id, name, detail: JSON.stringify(input), input, suggestions });\n }\n /** The runner gives up on a request, as an interrupt makes it. */\n withdrawPermission(id: string): void {\n if (this.openPermissions.delete(id)) this.emitEvent({ type: 'permission_withdrawn', id });\n }\n answerPermission(id: string, decision: ApprovalDecision): boolean {\n if (!this.openPermissions.delete(id)) return false;\n this.answers.push({ id, decision });\n this.emitEvent({ type: 'permission_decided', id, decision });\n return true;\n }\n\n emitEvent(event: StructuredEvent): void {\n for (const cb of this.eventCbs) cb(event);\n }\n emitTurnEnd(reason: StructuredTurnEnd): void {\n const next = this.messages.shift();\n if (!next) this.state = 'idle';\n for (const cb of this.turnEndCbs) cb(reason);\n if (next) this.deliver(next.text);\n }\n\n private deliver(text: string): void {\n this.state = 'working';\n this.delivered.push(text);\n this.emitEvent({ type: 'turn_start', text });\n }\n}\n\nexport type FakeIsolationCall =\n | { op: 'isActive'; workspaceRoot: string }\n | { op: 'stash'; workspaceRoot: string }\n | { op: 'startRun'; workspaceRoot: string }\n | { op: 'prepare' | 'reopen' | 'verifyRepair'; taskId: string }\n | { op: 'integrate'; taskId: string }\n | { op: 'release'; taskId: string; keep: boolean }\n | { op: 'handoff' | 'pruneOrphans' | 'reviewDiff' | 'mergeIntoCheckedOut' | 'sweep' }\n | { op: 'discard'; integration: IntegrationDisposal };\n\n/**\n * An in-memory {@link IWorktreeIsolation} for scheduling tests: no git, no\n * filesystem. Every call is logged in `calls`; `prepare` hands back a\n * deterministic fake cwd. Set `availability` to exercise the fallbacks, `outcomes`\n * to script a conflict, and `holdIntegration` to keep a task un-integrated so a\n * test can observe that its dependents wait. `repos` makes the run a group of\n * several; `changes` and `stopsIn` say which of them a task changes and where\n * its landing stops. `repairEvidence` scripts what a conflict repair's\n * evidence check finds.\n */\nexport class FakeWorktreeIsolation implements IWorktreeIsolation {\n availability: IsolationAvailability = { active: true };\n /** The repo paths `startRun` groups: a group of one at `.` unless set. */\n repos: string[] = [SELF_REPO];\n /** Per task id, the repos it changes; every repo of the group when not listed. */\n changes = new Map<string, string[]>();\n /** Per task id, the repo its landing stops in when its outcome is not `merged`; its first changed repo when not listed. */\n stopsIn = new Map<string, string>();\n /** Per task id, the files a `conflict` outcome names; empty when not listed. */\n conflictFiles = new Map<string, string[]>();\n /** What `mergeIntoCheckedOut` answers. */\n mergeResult: IsolationMergeResult = { outcome: 'merged' };\n /** What `startRun` shares and `prepare` copies, to exercise their notices. */\n shared: string[] = [];\n sharedRepos: string[] = [];\n copied: string[] = [];\n /** Set to make `startRun` throw, as git does when no repo of the group can be isolated. */\n startRunError: Error | null = null;\n /** Set to make `discard` / `sweep` throw after logging the call. */\n discardError: Error | null = null;\n sweepError: Error | null = null;\n /** What `pruneOrphans` reports as kept back from the sweep. */\n keptOnPrune: IsolationPruneResult['kept'] = [];\n /** Per task id; a task not listed integrates as `merged`. */\n outcomes = new Map<string, IsolationOutcome>();\n /** Per task id, what `verifyRepair` finds; a task not listed passes. */\n repairEvidence = new Map<string, RepairEvidence>();\n calls: FakeIsolationCall[] = [];\n private holds = new Map<string, Promise<void>>();\n private runCount = 0;\n\n private log(call: FakeIsolationCall): void { this.calls.push(call); }\n\n /** Task ids in the order `op` was called for them. */\n taskIdsFor(op: 'prepare' | 'reopen' | 'verifyRepair' | 'integrate' | 'release'): string[] {\n return this.calls.flatMap((c) => (c.op === op && 'taskId' in c ? [c.taskId] : []));\n }\n\n /** Make `integrate` for this task wait until the returned function is called. */\n holdIntegration(taskId: string): () => void {\n let open!: () => void;\n this.holds.set(taskId, new Promise<void>((resolve) => { open = resolve; }));\n return open;\n }\n\n async isActive(workspaceRoot: string): Promise<IsolationAvailability> {\n this.log({ op: 'isActive', workspaceRoot });\n return this.availability;\n }\n\n /** Like git: once the tracked changes are stashed, the tree is no longer dirty. */\n async stash(workspaceRoot: string): Promise<void> {\n this.log({ op: 'stash', workspaceRoot });\n if (!this.availability.active && this.availability.reason === 'dirty') this.availability = { active: true };\n }\n\n async startRun(workspaceRoot: string): Promise<IsolationRun> {\n this.log({ op: 'startRun', workspaceRoot });\n if (this.startRunError) throw this.startRunError;\n const id = `run${++this.runCount}`;\n return {\n id,\n workspaceRoot,\n repos: this.repos.map((repo) => ({\n path: repo, root: `${workspaceRoot}/${repo}`.replace(/\\/\\.$/, ''), baseRef: 'base0000', baseBranch: 'main', integrationBranch: integrationBranchFor(id),\n })),\n shared: [...this.shared],\n sharedRepos: [...this.sharedRepos],\n tasks: {},\n };\n }\n\n async prepare(task: Task, run: IsolationRun): Promise<PreparedTask> {\n this.log({ op: 'prepare', taskId: task.id });\n const name = `${task.order}-${task.id}`;\n const cwd = `/fake-worktrees/${run.id}/${name}`;\n const branch = `ordewell/${run.id}/${name}`;\n run.tasks[task.id] = {\n taskId: task.id, order: task.order, title: task.title, branch, workspace: cwd, status: 'active',\n repos: Object.fromEntries(run.repos.map((r) => [r.path, { worktree: r.path === SELF_REPO ? cwd : `${cwd}/${r.path}`, linked: [] }])),\n };\n return { cwd, branch, copied: [...this.copied] };\n }\n\n /** Like git: the same cwd, each changed repo's tip recorded, the repair counted. */\n async reopen(task: Task, run: IsolationRun): Promise<PreparedTask> {\n this.log({ op: 'reopen', taskId: task.id });\n const record = run.tasks[task.id];\n if (record?.status !== 'conflict') throw new Error(`Task ${task.order} has no conflict to repair`);\n const changed = Object.entries(record.repos).filter(([repo, entry]) => entry.changed || repo === record.conflictRepo).map(([repo]) => repo);\n const files = (record.conflictFiles ?? []).map((file) => (record.conflictRepo && record.conflictRepo !== SELF_REPO ? `${record.conflictRepo}/${file}` : file));\n record.repairs = (record.repairs ?? 0) + 1;\n record.repairBase = Object.fromEntries(changed.map((repo) => [repo, `tip-${repo}`]));\n record.repairedFiles = [...new Set([...(record.repairedFiles ?? []), ...files])];\n record.status = 'repairing';\n return { cwd: record.workspace, branch: record.branch, copied: [] };\n }\n\n async verifyRepair(task: Task, _run: IsolationRun): Promise<RepairEvidence> {\n this.log({ op: 'verifyRepair', taskId: task.id });\n return this.repairEvidence.get(task.id) ?? { ok: true };\n }\n\n /** Like git: the landing is recorded and persisted before the (held) merge, and cleared once it settles. */\n async integrate(task: Task, run: IsolationRun, persist: () => void = () => undefined): Promise<IsolationOutcome> {\n this.log({ op: 'integrate', taskId: task.id });\n const record = run.tasks[task.id];\n if (!record) return 'failed';\n const changed = this.changes.get(task.id) ?? run.repos.map((r) => r.path);\n for (const [repo, entry] of Object.entries(record.repos)) entry.changed = changed.includes(repo) || (entry.changed ?? false);\n if (changed.length > 0) {\n run.landing = { taskId: task.id, tips: Object.fromEntries(changed.map((repo) => [repo, `tip-${repo}`])) };\n persist();\n }\n await this.holds.get(task.id);\n delete run.landing;\n const outcome = this.outcomes.get(task.id) ?? 'merged';\n record.status = outcome;\n delete record.repairBase;\n if (outcome === 'merged') {\n delete record.conflictRepo;\n delete record.conflictFiles;\n } else {\n record.conflictRepo = this.stopsIn.get(task.id) ?? changed[0] ?? SELF_REPO;\n const files = this.conflictFiles.get(task.id);\n if (files) record.conflictFiles = files;\n else delete record.conflictFiles;\n }\n return outcome;\n }\n\n async release(run: IsolationRun, taskId: string, opts: { keep: boolean }): Promise<void> {\n this.log({ op: 'release', taskId, keep: opts.keep });\n const record = run.tasks[taskId];\n if (!opts.keep) delete run.tasks[taskId];\n else if (record?.status === 'active') record.status = 'kept';\n else if (record?.status === 'repairing') {\n record.status = 'conflict';\n delete record.repairBase;\n }\n }\n\n async handoff(run: IsolationRun): Promise<IsolationHandoff> {\n this.log({ op: 'handoff' });\n return handoffOf(run);\n }\n\n async pruneOrphans(): Promise<IsolationPruneResult> {\n this.log({ op: 'pruneOrphans' });\n return { kept: [...this.keptOnPrune] };\n }\n async reviewDiff(): Promise<string> { this.log({ op: 'reviewDiff' }); return ''; }\n async mergeIntoCheckedOut(): Promise<IsolationMergeResult> { this.log({ op: 'mergeIntoCheckedOut' }); return this.mergeResult; }\n async discard(_run: IsolationRun, opts: { integration: IntegrationDisposal }): Promise<void> {\n this.log({ op: 'discard', integration: opts.integration });\n if (this.discardError) throw this.discardError;\n }\n async sweep(): Promise<void> {\n this.log({ op: 'sweep' });\n if (this.sweepError) throw this.sweepError;\n }\n}\n","import * as path from 'path';\nimport type {\n IsolationHandoff,\n IsolationLandedTask,\n IsolationRepo,\n IsolationRun,\n IsolationTaskRecord,\n IsolationTaskStatus,\n PlanIsolation,\n RepoGroupLayout,\n TaskIsolation,\n} from '../interfaces/IWorktreeIsolation';\n\n/** The path of the repo in a workspace that is itself the repository. */\nexport const SELF_REPO = '.';\n\nexport function integrationBranchFor(runId: string): string {\n return `ordewell/${runId}/integration`;\n}\n\nexport function repoRootOf(workspaceRoot: string, repoPath: string): string {\n return path.join(workspaceRoot, repoPath);\n}\n\n/** The group a run isolates, as the planner is told it. */\nexport function layoutOf(run: IsolationRun): RepoGroupLayout {\n return { repos: run.repos.map((r) => r.path), shared: [...run.shared] };\n}\n\n/** The run's integration branch for a one-line notice: every repo's has the same name. */\nexport function integrationBranchNameOf(run: IsolationRun): string {\n return [...new Set(run.repos.map((r) => r.integrationBranch))].join(', ');\n}\n\nconst ISOLATION_STATE: Record<IsolationTaskStatus, Exclude<TaskIsolation['state'], 'none'>> = {\n active: 'active',\n merged: 'integrated',\n conflict: 'conflict',\n repairing: 'repairing',\n kept: 'kept',\n failed: 'kept',\n};\n\n/** Paths of the repos a task brought commits to. */\nexport function changedReposOf(record: IsolationTaskRecord): string[] {\n return Object.entries(record.repos).filter(([, r]) => r.changed).map(([repoPath]) => repoPath);\n}\n\n/** `repairLimit` is `conflictRepairAttempts` as it stands now, which is what the next repair will be held to. */\nexport function taskIsolationOf(record: IsolationTaskRecord, repairLimit: number): TaskIsolation {\n return {\n state: ISOLATION_STATE[record.status],\n branch: record.branch,\n worktree: record.workspace,\n repos: changedReposOf(record),\n ...(record.conflictRepo ? { conflictRepo: record.conflictRepo } : {}),\n ...(record.conflictFiles?.length ? { conflictFiles: record.conflictFiles } : {}),\n ...(record.repairs ? { repair: { attempt: record.repairs, limit: repairLimit } } : {}),\n ...(record.repairedFiles?.length ? { repairedFiles: record.repairedFiles } : {}),\n };\n}\n\n/** What a run hands over: each repo's integration branch and base, and what landed, in plan order. */\nexport function handoffOf(run: IsolationRun): IsolationHandoff {\n const merged = Object.values(run.tasks)\n .filter((r) => r.status === 'merged')\n .sort((a, b) => a.order - b.order);\n const entry = (r: IsolationTaskRecord): IsolationLandedTask => ({\n taskId: r.taskId, order: r.order, title: r.title,\n ...(r.repairedFiles?.length ? { repairedFiles: r.repairedFiles } : {}),\n });\n return {\n repos: run.repos.map((repo) => ({\n path: repo.path,\n integrationBranch: repo.integrationBranch,\n baseRef: repo.baseRef,\n landed: merged.filter((r) => r.repos[repo.path]?.changed).map(entry),\n })),\n landed: merged.map(entry),\n };\n}\n\n/** A task record as ADR-0013 persisted it, for one repository. */\nexport interface Adr0013TaskRecord {\n taskId: string;\n order: number;\n title: string;\n branch: string;\n worktree: string;\n status: IsolationTaskStatus;\n linked: string[];\n}\n\n/** A run as ADR-0013 persisted it (0.4.23): one repository, its refs on the run itself. */\nexport interface Adr0013IsolationRun {\n id: string;\n workspaceRoot: string;\n baseRef: string;\n baseBranch?: string;\n integrationBranch: string;\n tasks: Record<string, Adr0013TaskRecord>;\n}\n\nexport interface Adr0013PlanIsolation {\n run: Adr0013IsolationRun;\n resolvers: Record<string, string>;\n}\n\nfunction isAdr0013(run: IsolationRun | Adr0013IsolationRun): run is Adr0013IsolationRun {\n return 'integrationBranch' in run && typeof run.integrationBranch === 'string';\n}\n\nfunction fromAdr0013Task(r: Adr0013TaskRecord): IsolationTaskRecord {\n return {\n taskId: r.taskId,\n order: r.order,\n title: r.title,\n branch: r.branch,\n workspace: r.worktree,\n status: r.status,\n // A merged task's branch was merged into the one repo there was; what it\n // changed was never recorded, and landing is what `changed` stands for.\n repos: { [SELF_REPO]: { worktree: r.worktree, linked: r.linked ?? [], ...(r.status === 'merged' ? { changed: true } : {}) } },\n ...(r.status === 'conflict' ? { conflictRepo: SELF_REPO } : {}),\n };\n}\n\n/**\n * A persisted plan isolation in today's shape. A run saved in the ADR-0013\n * format — recognised by its own `integrationBranch` — becomes a group of one\n * at `.`, keeping its branches, so a session saved by 0.4.23 resumes and hands\n * off exactly as it would have.\n */\nexport function migratePlanIsolation(state: PlanIsolation | Adr0013PlanIsolation): PlanIsolation {\n const { run, resolvers } = state;\n if (!isAdr0013(run)) return { run, resolvers: resolvers ?? {} };\n const repo: IsolationRepo = {\n path: SELF_REPO,\n root: repoRootOf(run.workspaceRoot, SELF_REPO),\n baseRef: run.baseRef,\n ...(run.baseBranch ? { baseBranch: run.baseBranch } : {}),\n integrationBranch: run.integrationBranch,\n };\n const tasks = Object.fromEntries(Object.entries(run.tasks ?? {}).map(([id, r]) => [id, fromAdr0013Task(r)]));\n return { run: { id: run.id, workspaceRoot: run.workspaceRoot, repos: [repo], shared: [], sharedRepos: [], tasks }, resolvers: resolvers ?? {} };\n}\n\n/** Bring a loaded plan's isolation record, if any, to today's shape in place. */\nexport function migratePlanStateIsolation(plan: { isolation?: PlanIsolation | Adr0013PlanIsolation }): void {\n if (plan.isolation?.run) plan.isolation = migratePlanIsolation(plan.isolation);\n}\n"],"mappings":"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","names":[]}
package/dist/testing.mjs CHANGED
@@ -2,9 +2,12 @@ import {
2
2
  SELF_REPO,
3
3
  handoffOf,
4
4
  integrationBranchFor
5
- } from "./chunk-T2S5O36I.mjs";
5
+ } from "./chunk-C44UWIAD.mjs";
6
6
 
7
7
  // src/testing.ts
8
+ async function flushMicrotasks(rounds = 20) {
9
+ for (let i = 0; i < rounds; i += 1) await Promise.resolve();
10
+ }
8
11
  function fakeConfig(overrides = {}) {
9
12
  return {
10
13
  aiProvider: "openrouter",
@@ -91,6 +94,90 @@ var FakeTerminalSession = class {
91
94
  for (const cb of this.exitCbs) cb(code);
92
95
  }
93
96
  };
97
+ var FakeStructuredSession = class extends FakeTerminalSession {
98
+ constructor(id = "s1", taskId = "t1", sessionId = "native-1") {
99
+ super(id, taskId);
100
+ this.sessionId = sessionId;
101
+ }
102
+ sessionId;
103
+ transport = "structured";
104
+ state = "working";
105
+ messages = [];
106
+ /** Every message a turn was started with, in order. */
107
+ delivered = [];
108
+ interrupts = 0;
109
+ messageCount = 0;
110
+ turnEndCbs = [];
111
+ eventCbs = [];
112
+ write(text) {
113
+ super.write(text);
114
+ const message = text.trim();
115
+ if (message) this.sendMessage(message);
116
+ }
117
+ turnState() {
118
+ return this.state;
119
+ }
120
+ onTurnEnd(cb) {
121
+ this.turnEndCbs.push(cb);
122
+ }
123
+ onEvent(cb) {
124
+ this.eventCbs.push(cb);
125
+ }
126
+ sendMessage(text) {
127
+ this.messageCount += 1;
128
+ const id = `msg-${this.messageCount}`;
129
+ if (this.state === "idle") this.deliver(text);
130
+ else this.messages.push({ id, text });
131
+ return id;
132
+ }
133
+ removeQueued(id) {
134
+ const before = this.messages.length;
135
+ this.messages = this.messages.filter((m) => m.id !== id);
136
+ return this.messages.length < before;
137
+ }
138
+ queued() {
139
+ return this.messages.map((m) => ({ ...m }));
140
+ }
141
+ async interrupt() {
142
+ this.interrupts += 1;
143
+ if (this.state === "working") this.emitTurnEnd("interrupted");
144
+ }
145
+ nativeSessionId() {
146
+ return this.sessionId;
147
+ }
148
+ /** Every answer this session was given, in order. */
149
+ answers = [];
150
+ openPermissions = /* @__PURE__ */ new Set();
151
+ /** The runner asks to use a tool, as `StructuredSession` announces it. */
152
+ requestPermission(id, name, input, suggestions = []) {
153
+ this.openPermissions.add(id);
154
+ this.emitEvent({ type: "permission_request", id, name, detail: JSON.stringify(input), input, suggestions });
155
+ }
156
+ /** The runner gives up on a request, as an interrupt makes it. */
157
+ withdrawPermission(id) {
158
+ if (this.openPermissions.delete(id)) this.emitEvent({ type: "permission_withdrawn", id });
159
+ }
160
+ answerPermission(id, decision) {
161
+ if (!this.openPermissions.delete(id)) return false;
162
+ this.answers.push({ id, decision });
163
+ this.emitEvent({ type: "permission_decided", id, decision });
164
+ return true;
165
+ }
166
+ emitEvent(event) {
167
+ for (const cb of this.eventCbs) cb(event);
168
+ }
169
+ emitTurnEnd(reason) {
170
+ const next = this.messages.shift();
171
+ if (!next) this.state = "idle";
172
+ for (const cb of this.turnEndCbs) cb(reason);
173
+ if (next) this.deliver(next.text);
174
+ }
175
+ deliver(text) {
176
+ this.state = "working";
177
+ this.delivered.push(text);
178
+ this.emitEvent({ type: "turn_start", text });
179
+ }
180
+ };
94
181
  var FakeWorktreeIsolation = class {
95
182
  availability = { active: true };
96
183
  /** The repo paths `startRun` groups: a group of one at `.` unless set. */
@@ -260,9 +347,11 @@ var FakeWorktreeIsolation = class {
260
347
  }
261
348
  };
262
349
  export {
350
+ FakeStructuredSession,
263
351
  FakeTerminalSession,
264
352
  FakeWorktreeIsolation,
265
353
  fakeConfig,
266
- fakeFileSystem
354
+ fakeFileSystem,
355
+ flushMicrotasks
267
356
  };
268
357
  //# sourceMappingURL=testing.mjs.map