@oisin-ee/pi-todo 0.7.2 → 0.9.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.
@@ -1,5 +1,33 @@
1
1
  /* @ts-self-types="./todo_core.d.ts" */
2
2
 
3
+ /**
4
+ * Reads validated current-session activation.
5
+ *
6
+ * # Errors
7
+ * Rejects invalid persisted state.
8
+ * @param {TodoList} list
9
+ * @param {string} session_id
10
+ * @returns {ActiveUnit | undefined}
11
+ */
12
+ export function active(list, session_id) {
13
+ try {
14
+ const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
15
+ const ptr0 = passStringToWasm0(session_id, wasm.__wbindgen_export, wasm.__wbindgen_export2);
16
+ const len0 = WASM_VECTOR_LEN;
17
+ wasm.active(retptr, addBorrowedObject(list), ptr0, len0);
18
+ var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
19
+ var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
20
+ var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true);
21
+ if (r2) {
22
+ throw takeObject(r1);
23
+ }
24
+ return takeObject(r0);
25
+ } finally {
26
+ wasm.__wbindgen_add_to_stack_pointer(16);
27
+ heap[stack_pointer++] = undefined;
28
+ }
29
+ }
30
+
3
31
  /**
4
32
  * Applies one validated operation.
5
33
  *
@@ -7,13 +35,14 @@
7
35
  *
8
36
  * Throws for malformed input or a rejected operation.
9
37
  * @param {TodoList} list
10
- * @param {Request} request
38
+ * @param {unknown} request
39
+ * @param {MutationContext} context
11
40
  * @returns {Change}
12
41
  */
13
- export function apply(list, request) {
42
+ export function apply(list, request, context) {
14
43
  try {
15
44
  const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
16
- wasm.apply(retptr, addBorrowedObject(list), addBorrowedObject(request));
45
+ wasm.apply(retptr, addBorrowedObject(list), addHeapObject(request), addBorrowedObject(context));
17
46
  var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
18
47
  var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
19
48
  var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true);
@@ -28,6 +57,37 @@ export function apply(list, request) {
28
57
  }
29
58
  }
30
59
 
60
+ /**
61
+ * Validates and atomically freezes a worker contract.
62
+ *
63
+ * # Errors
64
+ * Rejects malformed requests, stale origins and competing claims.
65
+ * @param {TodoList} list
66
+ * @param {string} input
67
+ * @param {string} session_id
68
+ * @returns {ClaimChange}
69
+ */
70
+ export function claim(list, input, session_id) {
71
+ try {
72
+ const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
73
+ const ptr0 = passStringToWasm0(input, wasm.__wbindgen_export, wasm.__wbindgen_export2);
74
+ const len0 = WASM_VECTOR_LEN;
75
+ const ptr1 = passStringToWasm0(session_id, wasm.__wbindgen_export, wasm.__wbindgen_export2);
76
+ const len1 = WASM_VECTOR_LEN;
77
+ wasm.claim(retptr, addBorrowedObject(list), ptr0, len0, ptr1, len1);
78
+ var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
79
+ var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
80
+ var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true);
81
+ if (r2) {
82
+ throw takeObject(r1);
83
+ }
84
+ return takeObject(r0);
85
+ } finally {
86
+ wasm.__wbindgen_add_to_stack_pointer(16);
87
+ heap[stack_pointer++] = undefined;
88
+ }
89
+ }
90
+
31
91
  /**
32
92
  * Computes completion policy from branch facts.
33
93
  *
@@ -60,6 +120,68 @@ export function completion_reminder(facts) {
60
120
  }
61
121
  }
62
122
 
123
+ /**
124
+ * Imports one validated seed into a fresh worker session.
125
+ *
126
+ * # Errors
127
+ * Rejects invalid seed, repository identity or existing work.
128
+ * @param {TodoList} list
129
+ * @param {string} input
130
+ * @param {string} session_id
131
+ * @param {string} repository_id
132
+ * @returns {Change}
133
+ */
134
+ export function import_unit(list, input, session_id, repository_id) {
135
+ try {
136
+ const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
137
+ const ptr0 = passStringToWasm0(input, wasm.__wbindgen_export, wasm.__wbindgen_export2);
138
+ const len0 = WASM_VECTOR_LEN;
139
+ const ptr1 = passStringToWasm0(session_id, wasm.__wbindgen_export, wasm.__wbindgen_export2);
140
+ const len1 = WASM_VECTOR_LEN;
141
+ const ptr2 = passStringToWasm0(repository_id, wasm.__wbindgen_export, wasm.__wbindgen_export2);
142
+ const len2 = WASM_VECTOR_LEN;
143
+ wasm.import_unit(retptr, addBorrowedObject(list), ptr0, len0, ptr1, len1, ptr2, len2);
144
+ var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
145
+ var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
146
+ var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true);
147
+ if (r2) {
148
+ throw takeObject(r1);
149
+ }
150
+ return takeObject(r0);
151
+ } finally {
152
+ wasm.__wbindgen_add_to_stack_pointer(16);
153
+ heap[stack_pointer++] = undefined;
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Reads immutable import acknowledgement and closure evidence.
159
+ *
160
+ * # Errors
161
+ * Rejects invalid persisted state.
162
+ * @param {TodoList} list
163
+ * @param {string} session_id
164
+ * @returns {ImportReceipt | undefined}
165
+ */
166
+ export function imported(list, session_id) {
167
+ try {
168
+ const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
169
+ const ptr0 = passStringToWasm0(session_id, wasm.__wbindgen_export, wasm.__wbindgen_export2);
170
+ const len0 = WASM_VECTOR_LEN;
171
+ wasm.imported(retptr, addBorrowedObject(list), ptr0, len0);
172
+ var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
173
+ var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
174
+ var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true);
175
+ if (r2) {
176
+ throw takeObject(r1);
177
+ }
178
+ return takeObject(r0);
179
+ } finally {
180
+ wasm.__wbindgen_add_to_stack_pointer(16);
181
+ heap[stack_pointer++] = undefined;
182
+ }
183
+ }
184
+
63
185
  /**
64
186
  * Replaces the whole list with `items`.
65
187
  *
@@ -87,6 +209,34 @@ export function init(items) {
87
209
  }
88
210
  }
89
211
 
212
+ /**
213
+ * Reads a complete task contract.
214
+ *
215
+ * # Errors
216
+ * Rejects unknown IDs or incomplete contracts.
217
+ * @param {TodoList} list
218
+ * @param {string} id
219
+ * @returns {UnitContract}
220
+ */
221
+ export function inspect(list, id) {
222
+ try {
223
+ const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
224
+ const ptr0 = passStringToWasm0(id, wasm.__wbindgen_export, wasm.__wbindgen_export2);
225
+ const len0 = WASM_VECTOR_LEN;
226
+ wasm.inspect(retptr, addBorrowedObject(list), ptr0, len0);
227
+ var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
228
+ var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
229
+ var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true);
230
+ if (r2) {
231
+ throw takeObject(r1);
232
+ }
233
+ return takeObject(r0);
234
+ } finally {
235
+ wasm.__wbindgen_add_to_stack_pointer(16);
236
+ heap[stack_pointer++] = undefined;
237
+ }
238
+ }
239
+
90
240
  /**
91
241
  * Validates a stored snapshot. It comes from the session file, so it is typed
92
242
  * `unknown` and checked here, by the type that owns its shape.
@@ -113,6 +263,64 @@ export function parse(snapshot) {
113
263
  }
114
264
  }
115
265
 
266
+ /**
267
+ * Prevents mutations of revisions with an in-flight tool call.
268
+ *
269
+ * # Errors
270
+ * Rejects closure, replacement or alteration of any leased revision.
271
+ * @param {TodoList} before
272
+ * @param {TodoList} after
273
+ * @param {UnitRef[]} leases
274
+ */
275
+ export function protect(before, after, leases) {
276
+ try {
277
+ const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
278
+ const ptr0 = passArrayJsValueToWasm0(leases, wasm.__wbindgen_export);
279
+ const len0 = WASM_VECTOR_LEN;
280
+ wasm.protect(retptr, addBorrowedObject(before), addBorrowedObject(after), ptr0, len0);
281
+ var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
282
+ var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
283
+ if (r1) {
284
+ throw takeObject(r0);
285
+ }
286
+ } finally {
287
+ wasm.__wbindgen_add_to_stack_pointer(16);
288
+ heap[stack_pointer++] = undefined;
289
+ heap[stack_pointer++] = undefined;
290
+ }
291
+ }
292
+
293
+ /**
294
+ * Records an executor's exact release evidence, never acceptance.
295
+ *
296
+ * # Errors
297
+ * Rejects malformed or mismatched settlements.
298
+ * @param {TodoList} list
299
+ * @param {string} input
300
+ * @param {string} session_id
301
+ * @returns {SettlementChange}
302
+ */
303
+ export function settle(list, input, session_id) {
304
+ try {
305
+ const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
306
+ const ptr0 = passStringToWasm0(input, wasm.__wbindgen_export, wasm.__wbindgen_export2);
307
+ const len0 = WASM_VECTOR_LEN;
308
+ const ptr1 = passStringToWasm0(session_id, wasm.__wbindgen_export, wasm.__wbindgen_export2);
309
+ const len1 = WASM_VECTOR_LEN;
310
+ wasm.settle(retptr, addBorrowedObject(list), ptr0, len0, ptr1, len1);
311
+ var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
312
+ var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
313
+ var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true);
314
+ if (r2) {
315
+ throw takeObject(r1);
316
+ }
317
+ return takeObject(r0);
318
+ } finally {
319
+ wasm.__wbindgen_add_to_stack_pointer(16);
320
+ heap[stack_pointer++] = undefined;
321
+ }
322
+ }
323
+
116
324
  /**
117
325
  * The model-facing summary of `list`.
118
326
  *
@@ -187,6 +395,10 @@ function __wbg_get_imports() {
187
395
  const ret = Error(getStringFromWasm0(arg0, arg1));
188
396
  return addHeapObject(ret);
189
397
  },
398
+ __wbg_Number_14af1003b8dd5ead: function(arg0) {
399
+ const ret = Number(getObject(arg0));
400
+ return ret;
401
+ },
190
402
  __wbg_String_8564e559799eccda: function(arg0, arg1) {
191
403
  const ret = String(getObject(arg1));
192
404
  const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_export, wasm.__wbindgen_export2);
@@ -374,16 +586,21 @@ function __wbg_get_imports() {
374
586
  return addHeapObject(ret);
375
587
  },
376
588
  __wbindgen_generic_0000000000000001: function(arg0) {
589
+ // Cast intrinsic for `F64 -> Externref`.
590
+ const ret = arg0;
591
+ return addHeapObject(ret);
592
+ },
593
+ __wbindgen_generic_0000000000000002: function(arg0) {
377
594
  // Cast intrinsic for `I64 -> Externref`.
378
595
  const ret = arg0;
379
596
  return addHeapObject(ret);
380
597
  },
381
- __wbindgen_generic_0000000000000002: function(arg0, arg1) {
598
+ __wbindgen_generic_0000000000000003: function(arg0, arg1) {
382
599
  // Cast intrinsic for `Ref(String) -> Externref`.
383
600
  const ret = getStringFromWasm0(arg0, arg1);
384
601
  return addHeapObject(ret);
385
602
  },
386
- __wbindgen_generic_0000000000000003: function(arg0) {
603
+ __wbindgen_generic_0000000000000004: function(arg0) {
387
604
  // Cast intrinsic for `U64 -> Externref`.
388
605
  const ret = BigInt.asUintN(64, arg0);
389
606
  return addHeapObject(ret);
Binary file
@@ -1,10 +1,17 @@
1
1
  /* tslint:disable */
2
2
  /* eslint-disable */
3
3
  export const memory: WebAssembly.Memory;
4
- export const apply: (a: number, b: number, c: number) => void;
4
+ export const active: (a: number, b: number, c: number, d: number) => void;
5
+ export const apply: (a: number, b: number, c: number, d: number) => void;
6
+ export const claim: (a: number, b: number, c: number, d: number, e: number, f: number) => void;
5
7
  export const completion_reminder: (a: number, b: number) => void;
8
+ export const import_unit: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number) => void;
9
+ export const imported: (a: number, b: number, c: number, d: number) => void;
6
10
  export const init: (a: number, b: number, c: number) => void;
11
+ export const inspect: (a: number, b: number, c: number, d: number) => void;
7
12
  export const parse: (a: number, b: number) => void;
13
+ export const protect: (a: number, b: number, c: number, d: number, e: number) => void;
14
+ export const settle: (a: number, b: number, c: number, d: number, e: number, f: number) => void;
8
15
  export const view: (a: number, b: number) => void;
9
16
  export const widget: (a: number, b: number, c: number) => void;
10
17
  export const __wbindgen_export: (a: number, b: number) => number;
package/src/guidance.ts CHANGED
@@ -3,6 +3,10 @@ const workUnitGuidance =
3
3
  "Size each executable unit for one agent session, with one deliverable, clear scope, and an acceptance condition. " +
4
4
  "Split independent outcomes or work that needs another session. Combine trivial steps with their deliverable and tests. " +
5
5
  "Delegate independent units separately; the lead can execute one unit. Keep dependent units ordered. " +
6
- "Do not delegate an oversized parent as one assignment. Close children before explicit parent integration review.";
6
+ "Do not delegate an oversized parent as one assignment. Close children before explicit parent integration review. " +
7
+ "Supply deliverable, repository-relative scope, acceptance, prerequisite IDs, and self or worker/profile assignment. " +
8
+ "Use local keys for new prerequisites and the optional start selector for atomic creation and activation. " +
9
+ "Use returned IDs after creation. Update drafts before start; only one complete self leaf executes locally. " +
10
+ "Done requires concrete acceptance evidence; worker exit alone is not acceptance.";
7
11
 
8
12
  export { workUnitGuidance };
package/src/handoff.ts ADDED
@@ -0,0 +1,84 @@
1
+ import type {
2
+ ExtensionAPI,
3
+ ExtensionContext,
4
+ SessionStartEvent,
5
+ } from "@earendil-works/pi-coding-agent";
6
+ import type { TaskProvider } from "./provider.ts";
7
+ import { ownsTodo } from "./lifecycle.ts";
8
+ import { seedPath } from "./seed-transport.ts";
9
+ import { seedSource } from "./source.ts";
10
+
11
+ type ImportHost = Readonly<Pick<ExtensionAPI, "exec" | "getAllTools">> & {
12
+ readonly events: Readonly<ExtensionAPI["events"]>;
13
+ };
14
+
15
+ /** Startup transport is read once; branch progress always wins on resume. */
16
+ class WorkerImport {
17
+ readonly #pi: ImportHost;
18
+ readonly #provider: Readonly<TaskProvider>;
19
+
20
+ public constructor(pi: ImportHost, provider: Readonly<TaskProvider>) {
21
+ this.#pi = pi;
22
+ this.#provider = provider;
23
+ }
24
+
25
+ /**
26
+ * Activates a seed before the worker's initial request, without an LLM call.
27
+ * @param {SessionStartEvent} event Runtime startup reason.
28
+ * @param {ExtensionContext} ctx Current session authority.
29
+ */
30
+ public async start(event: SessionStartEvent, ctx: ExtensionContext): Promise<void> {
31
+ if (!ownsTodo(this.#pi)) {
32
+ return;
33
+ }
34
+ try {
35
+ const file = seedPath(this.#pi.events);
36
+ if (file !== undefined && this.#requiresImport(event, ctx)) {
37
+ await this.#load(file, ctx);
38
+ }
39
+ } catch (error: unknown) {
40
+ ctx.abort();
41
+ ctx.ui.notify(
42
+ `IMPORT_FAILED: ${error instanceof Error ? error.message : String(error)}`,
43
+ "error",
44
+ );
45
+ }
46
+ }
47
+
48
+ #requiresImport(event: SessionStartEvent, ctx: ExtensionContext): boolean {
49
+ const receipt = this.#provider.capability().imported();
50
+ if (!receipt.ok) {
51
+ throw new Error(receipt.error.message);
52
+ }
53
+ if (receipt.value !== null) {
54
+ return false;
55
+ }
56
+ if (
57
+ event.reason !== "startup" ||
58
+ ctx.sessionManager.getEntries().some((entry) => entry.type === "message")
59
+ ) {
60
+ throw new Error("IMPORT_FAILED: cannot seed an existing session");
61
+ }
62
+ return true;
63
+ }
64
+
65
+ async #load(file: string, ctx: ExtensionContext): Promise<void> {
66
+ const sessionId = ctx.sessionManager.getSessionId(),
67
+ leaf = ctx.sessionManager.getLeafId(),
68
+ source = seedSource(file),
69
+ origin = await this.#pi.exec("git", ["remote", "get-url", "origin"], { cwd: ctx.cwd }),
70
+ success = 0;
71
+ if (origin.code !== success) {
72
+ throw new Error("IMPORT_FAILED: checkout has no origin");
73
+ }
74
+ if (
75
+ ctx.sessionManager.getSessionId() !== sessionId ||
76
+ ctx.sessionManager.getLeafId() !== leaf
77
+ ) {
78
+ throw new Error("STALE_ORIGIN: session changed while reading repository identity");
79
+ }
80
+ this.#provider.import(source, origin.stdout, ctx);
81
+ }
82
+ }
83
+
84
+ export { WorkerImport };
@@ -0,0 +1,63 @@
1
+ import { mkdtemp, rm, writeFile } from "node:fs/promises";
2
+ import { createBashTool } from "@earendil-works/pi-coding-agent";
3
+ import { tmpdir } from "node:os";
4
+ import { worker } from "./provider-fixture.ts";
5
+
6
+ /**
7
+ * Runs Git with argument boundaries and explicit exit failure.
8
+ * @param {string} cwd Temporary owned checkout.
9
+ * @returns {Promise<void>} Command completion.
10
+ */
11
+ const repository = async (cwd: string): Promise<void> => {
12
+ await createBashTool(cwd).execute("repository", {
13
+ command: "git init && git remote add origin git@github.com:oisin-ee/oisinpi.git",
14
+ });
15
+ },
16
+ /**
17
+ * Makes an owned repository and a seed from the real provider, without network I/O.
18
+ * @param {string} repositoryId Logical identity authored by the lead.
19
+ * @returns {Promise<object>} Temporary checkout and exact claim.
20
+ */
21
+ importFixture = async (
22
+ repositoryId = "github.com/oisin-ee/oisinpi",
23
+ ): Promise<{
24
+ cwd: string;
25
+ seedPath: string;
26
+ origin: Awaited<ReturnType<typeof worker>>;
27
+ removeSeed: () => Promise<void>;
28
+ close: () => Promise<void>;
29
+ }> => {
30
+ const cwd = await mkdtemp(`${tmpdir()}/todo-import-`),
31
+ seedPath = `${cwd}/seed.json`,
32
+ origin = await worker(repositoryId);
33
+ try {
34
+ await repository(cwd);
35
+ await writeFile(
36
+ seedPath,
37
+ JSON.stringify({
38
+ binding: origin.claimed.binding,
39
+ execution: origin.claimed.binding.execution,
40
+ exported: origin.claimed.exported,
41
+ version: 1,
42
+ }),
43
+ );
44
+ return {
45
+ close: async (): Promise<void> => {
46
+ origin.test.session.dispose();
47
+ await rm(cwd, { force: true, recursive: true });
48
+ },
49
+ cwd,
50
+ origin,
51
+ removeSeed: async (): Promise<void> => {
52
+ await rm(seedPath);
53
+ },
54
+ seedPath,
55
+ };
56
+ } catch (error: unknown) {
57
+ origin.test.session.dispose();
58
+ await rm(cwd, { force: true, recursive: true });
59
+ throw error;
60
+ }
61
+ };
62
+
63
+ export { importFixture };
package/src/index.ts CHANGED
@@ -1,12 +1,13 @@
1
1
  import { ownsTodo, settle } from "./lifecycle.ts";
2
2
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
3
3
  import { PlannotatorHandoff } from "./plannotator.ts";
4
- import { StringEnum } from "@earendil-works/pi-ai";
4
+ import { TaskProvider } from "./provider.ts";
5
5
  import { TodoPresentation } from "./presentation.ts";
6
6
  import { TodoWork } from "./work.ts";
7
- import { Type } from "typebox";
8
7
  import type { WorkHost } from "./work.ts";
9
- import { runTodo } from "./tool.ts";
8
+ import { WorkerImport } from "./handoff.ts";
9
+ import { parameters } from "./schema.ts";
10
+ import { registerGate } from "./gate.ts";
10
11
  import { workUnitGuidance } from "./guidance.ts";
11
12
 
12
13
  type TodoAPI = WorkHost &
@@ -15,6 +16,7 @@ type TodoAPI = WorkHost &
15
16
  ExtensionAPI,
16
17
  | "appendEntry"
17
18
  | "getAllTools"
19
+ | "exec"
18
20
  | "on"
19
21
  | "registerCommand"
20
22
  | "registerShortcut"
@@ -23,37 +25,6 @@ type TodoAPI = WorkHost &
23
25
  >
24
26
  >;
25
27
 
26
- const subtasks = Type.Immutable(Type.Array(Type.String())),
27
- outcome = Type.Immutable(
28
- Type.Object({ content: Type.String(), subtasks }, { additionalProperties: false }),
29
- ),
30
- item = Type.Union([Type.String(), outcome]),
31
- items = Type.Immutable(
32
- Type.Array(item, {
33
- description:
34
- "Pending outcomes for init/append. With parent, append accepts string work units only. Two levels only.",
35
- }),
36
- ),
37
- parameters = Type.Object({
38
- items: Type.Optional(items),
39
- op: StringEnum(["init", "start", "done", "drop", "append", "view"] as const, {
40
- description:
41
- "init replaces; start explicitly activates without demoting others; done completes; drop abandons open work; append adds pending work; view reads.",
42
- }),
43
- parent: Type.Optional(
44
- Type.String({
45
- description:
46
- "Exact root content to target a child. Child start/done/drop also require task.",
47
- }),
48
- ),
49
- task: Type.Optional(
50
- Type.String({
51
- description:
52
- "Exact task content within parent, or at root. Omit for done/drop to close open roots. Complete children before parents.",
53
- }),
54
- ),
55
- });
56
-
57
28
  /**
58
29
  * Registers the branch-backed `todo` tool and its completion reminder.
59
30
  * @param {TodoAPI} pi The extension API methods used here.
@@ -61,17 +32,16 @@ const subtasks = Type.Immutable(Type.Array(Type.String())),
61
32
  export default function piTodo(pi: TodoAPI): void {
62
33
  const handoff = new PlannotatorHandoff(pi),
63
34
  presentation = new TodoPresentation(),
64
- work = new TodoWork(pi, presentation);
35
+ work = new TodoWork(pi, presentation),
36
+ provider = new TaskProvider(pi),
37
+ workerImport = new WorkerImport(pi, provider);
65
38
  work.register(pi);
39
+ provider.register();
40
+ registerGate(pi, provider);
66
41
  pi.registerTool({
67
- description: `Keep branch-local outcomes and optional bounded subtasks. Address exact content plus optional parent. Creation is pending. Start explicitly; parallel starts preserve other active work. Parents require explicit integration review after children close. ${workUnitGuidance}`,
42
+ description: `Keep branch-local outcomes and executable contracts. Use returned IDs, not labels. Draft creation is pending. Start one complete self-assigned leaf; workers require their assigned profile through the executor. Update replaces pending contract fields. Done requires acceptance evidence. Parents require explicit integration review after children close. ${workUnitGuidance}`,
68
43
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
69
- const result = runTodo(params, {
70
- append: (customType, list) => {
71
- pi.appendEntry(customType, list);
72
- },
73
- branch: () => ctx.sessionManager.getBranch(),
74
- });
44
+ const result = provider.run(params, ctx);
75
45
  work.publish(ctx);
76
46
  presentation.refresh(ctx);
77
47
  return result;
@@ -81,16 +51,20 @@ export default function piTodo(pi: TodoAPI): void {
81
51
  name: "todo",
82
52
  parameters,
83
53
  });
84
- pi.on("session_start", (_event, ctx) => {
54
+ pi.on("session_start", async (event, ctx) => {
55
+ provider.context(ctx);
56
+ await workerImport.start(event, ctx);
85
57
  presentation.start(pi, ctx);
86
58
  work.start(ctx);
87
59
  handoff.start(ctx);
88
60
  });
89
61
  pi.on("session_shutdown", () => {
62
+ provider.stop();
90
63
  handoff.stop();
91
64
  work.stop();
92
65
  });
93
66
  pi.on("session_tree", (_event, ctx) => {
67
+ provider.context(ctx);
94
68
  if (ownsTodo(pi)) {
95
69
  work.publish(ctx);
96
70
  presentation.reset(ctx);
@@ -0,0 +1,95 @@
1
+ import { contract, firstId } from "./contract-fixture.ts";
2
+ import { emptyParameters, emptyResult, runtimeFixture } from "./runtime-fixture.ts";
3
+ import { branchSnapshot } from "./state.ts";
4
+
5
+ /**
6
+ * Completes an unrelated unit on a fresh branch.
7
+ * @param {Readonly<object>} test Runtime task and branch operations.
8
+ */
9
+ const unrelated = async (test: {
10
+ readonly manager: {
11
+ readonly resetLeaf: () => void;
12
+ readonly getBranch: () => ReturnType<
13
+ Awaited<ReturnType<typeof runtimeFixture>>["manager"]["getBranch"]
14
+ >;
15
+ };
16
+ readonly todo: Awaited<ReturnType<typeof runtimeFixture>>["todo"];
17
+ }): Promise<void> => {
18
+ test.manager.resetLeaf();
19
+ await test.todo({
20
+ items: [{ content: "Other branch", contract: contract(), key: "other" }],
21
+ op: "init",
22
+ start: "other",
23
+ });
24
+ const list = branchSnapshot(test.manager.getBranch());
25
+ await test.todo({ evidence: "Reviewed unrelated work", op: "done", task: firstId(list) });
26
+ };
27
+
28
+ interface Leased {
29
+ readonly test: Awaited<ReturnType<typeof runtimeFixture>>;
30
+ readonly id: string;
31
+ readonly calls: readonly string[];
32
+ readonly revisit: () => void;
33
+ readonly unrelated: () => Promise<void>;
34
+ readonly finish: () => Promise<void>;
35
+ readonly close: () => void;
36
+ }
37
+
38
+ /**
39
+ * Owns the controlled execution boundary used by lease regressions.
40
+ * @param {boolean} nested Runs through real codemode when true.
41
+ * @param {boolean} fail Throws at the actual execute boundary when true.
42
+ * @returns {Promise<object>} Running tool and deterministic completion controls.
43
+ */
44
+ const leased = async (nested: boolean, fail: boolean): Promise<Leased> => {
45
+ const entered = Promise.withResolvers<boolean>(),
46
+ finished = Promise.withResolvers<boolean>(),
47
+ calls: string[] = [],
48
+ test = await runtimeFixture((pi) => {
49
+ pi.registerTool({
50
+ description: "Controlled mutation",
51
+ execute: async (id) => {
52
+ calls.push(id);
53
+ entered.resolve(true);
54
+ await finished.promise;
55
+ if (fail) {
56
+ throw new Error("Mutation failed");
57
+ }
58
+ return emptyResult();
59
+ },
60
+ label: "Mutation",
61
+ name: "mutation",
62
+ parameters: emptyParameters,
63
+ });
64
+ });
65
+ await test.create();
66
+ const id = firstId(branchSnapshot(test.manager.getBranch())),
67
+ running = nested
68
+ ? test.call("codemode", { code: "return await tools.mutation({})" })
69
+ : test.call("mutation", {});
70
+ await entered.promise;
71
+ const origin = test.manager.getLeafId();
72
+ if (origin === null) {
73
+ throw new Error("Expected leased origin branch leaf");
74
+ }
75
+ return {
76
+ calls,
77
+ close: (): void => {
78
+ finished.resolve(true);
79
+ test.session.dispose();
80
+ },
81
+ finish: async (): Promise<void> => {
82
+ finished.resolve(true);
83
+ await running;
84
+ },
85
+ id,
86
+ revisit: (): void => {
87
+ test.manager.resetLeaf();
88
+ test.manager.branch(origin);
89
+ },
90
+ test,
91
+ unrelated: async (): Promise<void> => unrelated(test),
92
+ };
93
+ };
94
+
95
+ export { leased };