@oisin-ee/oisinpi 0.7.1 → 0.8.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 (41) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +4 -4
  3. package/node_modules/@oisin-ee/pi-guardrails/README.md +1 -1
  4. package/node_modules/@oisin-ee/pi-guardrails/dist/policies/shell.js +9 -9
  5. package/node_modules/@oisin-ee/pi-guardrails/package.json +1 -1
  6. package/node_modules/@oisin-ee/pi-guardrails/src/pi/tool-timeouts.ts +4 -4
  7. package/node_modules/@oisin-ee/pi-guardrails/src/policies/shell.ts +9 -9
  8. package/node_modules/@oisin-ee/pi-prewalk/README.md +3 -2
  9. package/node_modules/@oisin-ee/pi-prewalk/package.json +1 -1
  10. package/node_modules/@oisin-ee/pi-prewalk/src/adapter.ts +2 -0
  11. package/node_modules/@oisin-ee/pi-prewalk/src/generated/prewalk_core.d.ts +4 -0
  12. package/node_modules/@oisin-ee/pi-prewalk/src/generated/prewalk_core_bg.wasm +0 -0
  13. package/node_modules/@oisin-ee/pi-prewalk/src/ports.ts +1 -1
  14. package/node_modules/@oisin-ee/pi-prewalk/src/tasks.ts +48 -0
  15. package/node_modules/@oisin-ee/pi-todo/README.md +4 -4
  16. package/node_modules/@oisin-ee/pi-todo/package.json +1 -1
  17. package/node_modules/@oisin-ee/pi-todo/src/contract-fixture.ts +103 -0
  18. package/node_modules/@oisin-ee/pi-todo/src/display.ts +8 -2
  19. package/node_modules/@oisin-ee/pi-todo/src/gate.ts +79 -0
  20. package/node_modules/@oisin-ee/pi-todo/src/generated/todo_core.d.ts +342 -5
  21. package/node_modules/@oisin-ee/pi-todo/src/generated/todo_core.js +222 -5
  22. package/node_modules/@oisin-ee/pi-todo/src/generated/todo_core_bg.wasm +0 -0
  23. package/node_modules/@oisin-ee/pi-todo/src/generated/todo_core_bg.wasm.d.ts +8 -1
  24. package/node_modules/@oisin-ee/pi-todo/src/guidance.ts +5 -1
  25. package/node_modules/@oisin-ee/pi-todo/src/handoff.ts +84 -0
  26. package/node_modules/@oisin-ee/pi-todo/src/import-fixture.ts +63 -0
  27. package/node_modules/@oisin-ee/pi-todo/src/index.ts +17 -43
  28. package/node_modules/@oisin-ee/pi-todo/src/lease-fixture.ts +95 -0
  29. package/node_modules/@oisin-ee/pi-todo/src/provider-fixture.ts +50 -0
  30. package/node_modules/@oisin-ee/pi-todo/src/provider.ts +289 -0
  31. package/node_modules/@oisin-ee/pi-todo/src/registration-fixture.ts +126 -0
  32. package/node_modules/@oisin-ee/pi-todo/src/repository.ts +31 -0
  33. package/node_modules/@oisin-ee/pi-todo/src/runtime-fixture.ts +209 -0
  34. package/node_modules/@oisin-ee/pi-todo/src/schema.ts +86 -0
  35. package/node_modules/@oisin-ee/pi-todo/src/seed-fixture.ts +35 -0
  36. package/node_modules/@oisin-ee/pi-todo/src/seed-transport.ts +52 -0
  37. package/node_modules/@oisin-ee/pi-todo/src/source.ts +33 -3
  38. package/node_modules/@oisin-ee/pi-todo/src/state.ts +78 -1
  39. package/node_modules/@oisin-ee/pi-todo/src/tool.ts +15 -14
  40. package/node_modules/@oisin-ee/pi-todo/src/wasm.ts +26 -0
  41. package/package.json +4 -4
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # @oisin-ee/oisinpi
2
2
 
3
+ ## 0.8.0
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [[`50877ec`](https://github.com/oisin-ee/oisinpi/commit/50877ec0f9bba71b7f70d8bf8d3754191ac73364)]:
8
+ - @oisin-ee/pi-todo@0.8.0
9
+ - @oisin-ee/pi-prewalk@0.8.0
10
+ - @oisin-ee/pi-guardrails@0.8.0
11
+
12
+ ## 0.7.2
13
+
14
+ ### Patch Changes
15
+
16
+ - Updated dependencies [[`f31e9a6`](https://github.com/oisin-ee/oisinpi/commit/f31e9a6043d5b7cbe351540b51f87eadf85c9af3)]:
17
+ - @oisin-ee/pi-guardrails@0.7.2
18
+ - @oisin-ee/pi-prewalk@0.7.2
19
+ - @oisin-ee/pi-todo@0.7.2
20
+
3
21
  ## 0.7.1
4
22
 
5
23
  ### Patch Changes
package/README.md CHANGED
@@ -15,11 +15,11 @@ pi install npm:@oisin-ee/oisinpi
15
15
  The collection bundles the three packages in its tarball and adds no code. No member imports another, so each one also
16
16
  installs by itself.
17
17
 
18
- pi-wrangle is not in the collection. Install it on its own, at the same version as the `wrangle` binary
19
- ([oisin-ee/wrangle](https://github.com/oisin-ee/wrangle)):
18
+ pi-dogsled is not in the collection. Install it on its own, at the same version as the `dogsled` binary
19
+ ([oisin-ee/dogsled](https://github.com/oisin-ee/dogsled)):
20
20
 
21
21
  ```bash
22
- pi install npm:@oisin-ee/pi-wrangle@<wrangle version>
22
+ pi install npm:@oisin-ee/pi-dogsled@<dogsled version>
23
23
  ```
24
24
 
25
- When pi-wrangle is active, its work widget hosts pi-todo's view. Without it, pi-todo keeps its own widget.
25
+ When pi-dogsled is active, its work widget hosts pi-todo's view. Without it, pi-todo keeps its own widget.
@@ -6,7 +6,7 @@ Agent guardrails for [pi](https://pi.dev), Claude Code, and Codex. One set of po
6
6
  under `.worktrees/`, no on-the-fly identity.
7
7
  - **Wait gate:** no `sleep`, `watch`, follow flags, `--wait`/`--watch`, or wait subcommands. Start long work in the
8
8
  background instead.
9
- - **Agent typing:** no `herdr agent prompt` or `pane send-*` into another agent's terminal. Use `wrangle send`.
9
+ - **Agent typing:** no `herdr agent prompt` or `pane send-*` into another agent's terminal. Use `dogsled send`. For prompt answers, only `herdr agent send-keys` accepts `# dogsled: answer-prompt`; each use is recorded.
10
10
  - **GitOps:** no imperative `kubectl`, `helm`, `argocd`, or `nix` mutations, and no `fsck`/`mkfs`.
11
11
  - **Generated paths:** no hand-edits of lockfiles or generated files.
12
12
 
@@ -27,7 +27,7 @@ export const SHELL_MSG = {
27
27
  branch: "git-gate: blocked creating or resetting a branch. Per rules/30-safety-and-delivery.md, create new branches with " +
28
28
  "git worktree add -b <branch> .worktrees/<name> [<commit-ish>], or with git branch/switch -c/checkout -b " +
29
29
  "inside an agent-owned linked worktree. For a delegated child's branch, run " +
30
- "`wrangle place --branch <branch> --base origin/main` instead (rules/20-delegation.md). " +
30
+ "`dogsled place --branch <branch> --base origin/main` instead (rules/20-delegation.md). " +
31
31
  "Forced branch resets (-B, -C, -f) are never allowed.",
32
32
  clean: 'git-gate: blocked "git clean -f" which permanently deletes untracked files. Per rules/30-safety-and-delivery.md, ' +
33
33
  'never discard work on your own — preview with "git clean -n" or ask the user.',
@@ -51,13 +51,13 @@ export const SHELL_MSG = {
51
51
  "unrelated working-tree changes can be lost or conflict on restore. Use a worktree or ask the user.",
52
52
  worktree: "git-gate: blocked creating a worktree outside repo-local .worktrees/. Per rules/30-safety-and-delivery.md, " +
53
53
  "worktrees must use git worktree add [-b <branch>] .worktrees/<name> [<commit-ish>]. For a delegated child's " +
54
- "worktree, run `wrangle place --branch <branch> --base origin/main` instead (rules/20-delegation.md).",
54
+ "worktree, run `dogsled place --branch <branch> --base origin/main` instead (rules/20-delegation.md).",
55
55
  };
56
56
  export const TYPING_MSG = "agent-typing: blocked typing into another agent's terminal. Typed text lands in the user's draft and mixes " +
57
- "with it. Message the agent through its wrangle inbox instead: the `send` tool (mcp__wrangle__send) or " +
58
- "`wrangle send <name-or-pane> --message <text>`. To answer a prompt dialog in another pane with keys, add " +
59
- "`# wrangle: answer-prompt` to `herdr agent send-keys`; each such use is recorded.";
60
- const ANSWER_PROMPT = /#\s*wrangle:\s*answer-prompt\b/u, HERDR_VALUE_OPTIONS = new Set(["--session", "--machine", "--remote"]), PANE_TYPING = new Set(["send-text", "send-keys", "send-input"]);
57
+ "with it. Message the agent through its dogsled inbox instead: the `send` tool (mcp__dogsled__send) or " +
58
+ "`dogsled send <name-or-pane> --message <text>`. To answer a prompt dialog in another pane with keys, add " +
59
+ "`# dogsled: answer-prompt` to `herdr agent send-keys`; each such use is recorded.";
60
+ const ANSWER_PROMPT = /#\s*dogsled:\s*answer-prompt\b/u, HERDR_VALUE_OPTIONS = new Set(["--session", "--machine", "--remote"]), PANE_TYPING = new Set(["send-text", "send-keys", "send-input"]);
61
61
  // The herdr subcommand words after global options, through env, timeout, and shell wrappers.
62
62
  function herdrWords(inputWords) {
63
63
  const words = [...inputWords];
@@ -124,7 +124,7 @@ function shellPieces(words) {
124
124
  }
125
125
  // Typing into another agent's terminal: `herdr agent prompt`, `herdr agent send-keys`, and
126
126
  // `herdr pane send-text|send-keys|send-input` aimed at a pane that runs an agent. `agent send-keys` with
127
- // `# wrangle: answer-prompt` on the command line is allowed and recorded. The caller's own pane is not
127
+ // `# dogsled: answer-prompt` on the command line is allowed and recorded. The caller's own pane is not
128
128
  // Another agent's. `isAgent` is the pane lookup; the second argument is the cwd every classifier receives.
129
129
  export function classifyAgentTyping(command, cwd = process.cwd(), isAgent = paneRunsAgent, self = process.env["HERDR_PANE_ID"]) {
130
130
  const marked = typeof command === "string" && ANSWER_PROMPT.test(command);
@@ -177,8 +177,8 @@ export const IMPERATIVE_MSG = {
177
177
  };
178
178
  export const WAIT_MSG = "wait-gate: blocked a command that waits instead of working (sleep, watch, a follow flag, a --watch or --wait " +
179
179
  "flag, or a wait subcommand). Never hold a tool call to wait, and never re-read a log to check progress. " +
180
- "For long work in pi, use Wrangle `run` (mcp__wrangle__run) and continue other work. " +
181
- "Use `read` (mcp__wrangle__read) for output and `stop` (mcp__wrangle__stop) to cancel the job. " +
180
+ "For long work in pi, use Dogsled `run` (mcp__dogsled__run) and continue other work. " +
181
+ "Use `read` (mcp__dogsled__read) for output and `stop` (mcp__dogsled__stop) to cancel the job. " +
182
182
  "Start long work so the host reports its exit: in Claude Code, `run_in_background` " +
183
183
  "or Monitor (the exit arrives as a message); in Codex, exec_command, then empty write_stdin calls, which " +
184
184
  "return when the process exits. " +
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oisin-ee/pi-guardrails",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
4
4
  "description": "Agent guardrails for pi, Claude Code, and Codex: Git safety, no waiting, no typing into other agents, GitOps-only clusters.",
5
5
  "keywords": [
6
6
  "claude-code",
@@ -41,8 +41,8 @@ export function codeWaitViolation(
41
41
  }
42
42
  return CODE_WAITS.some((pattern) => pattern.test(code))
43
43
  ? "wait-gate: blocked a sleep or timer wait in sandbox code. Never hold a tool call to wait. Start long work with " +
44
- "Wrangle `run` (mcp__wrangle__run) and continue other work. Use `read` (mcp__wrangle__read) for output " +
45
- "and `stop` (mcp__wrangle__stop) to cancel the job."
44
+ "Dogsled `run` (mcp__dogsled__run) and continue other work. Use `read` (mcp__dogsled__read) for output " +
45
+ "and `stop` (mcp__dogsled__stop) to cancel the job."
46
46
  : null;
47
47
  }
48
48
 
@@ -62,8 +62,8 @@ function limitMessage(
62
62
  return (
63
63
  `timeout-gate: blocked ${toolName} ${field}=${value}. The ${field} unit is ${unit === "s" ? "seconds" : "milliseconds"} ` +
64
64
  `and the limit is ${max} (${MAX_TIMEOUT_SECONDS / 60} minutes).${unitHint} ` +
65
- "For a longer job, start it with Wrangle `run` (mcp__wrangle__run) and continue other work. " +
66
- "Use `read` (mcp__wrangle__read) for output and `stop` (mcp__wrangle__stop) to cancel the job. " +
65
+ "For a longer job, start it with Dogsled `run` (mcp__dogsled__run) and continue other work. " +
66
+ "Use `read` (mcp__dogsled__read) for output and `stop` (mcp__dogsled__stop) to cancel the job. " +
67
67
  "For CI, use the ci_find and ci_watch tools."
68
68
  );
69
69
  }
@@ -42,7 +42,7 @@ export const SHELL_MSG = {
42
42
  "git-gate: blocked creating or resetting a branch. Per rules/30-safety-and-delivery.md, create new branches with " +
43
43
  "git worktree add -b <branch> .worktrees/<name> [<commit-ish>], or with git branch/switch -c/checkout -b " +
44
44
  "inside an agent-owned linked worktree. For a delegated child's branch, run " +
45
- "`wrangle place --branch <branch> --base origin/main` instead (rules/20-delegation.md). " +
45
+ "`dogsled place --branch <branch> --base origin/main` instead (rules/20-delegation.md). " +
46
46
  "Forced branch resets (-B, -C, -f) are never allowed.",
47
47
  clean:
48
48
  'git-gate: blocked "git clean -f" which permanently deletes untracked files. Per rules/30-safety-and-delivery.md, ' +
@@ -74,16 +74,16 @@ export const SHELL_MSG = {
74
74
  worktree:
75
75
  "git-gate: blocked creating a worktree outside repo-local .worktrees/. Per rules/30-safety-and-delivery.md, " +
76
76
  "worktrees must use git worktree add [-b <branch>] .worktrees/<name> [<commit-ish>]. For a delegated child's " +
77
- "worktree, run `wrangle place --branch <branch> --base origin/main` instead (rules/20-delegation.md).",
77
+ "worktree, run `dogsled place --branch <branch> --base origin/main` instead (rules/20-delegation.md).",
78
78
  };
79
79
 
80
80
  export const TYPING_MSG =
81
81
  "agent-typing: blocked typing into another agent's terminal. Typed text lands in the user's draft and mixes " +
82
- "with it. Message the agent through its wrangle inbox instead: the `send` tool (mcp__wrangle__send) or " +
83
- "`wrangle send <name-or-pane> --message <text>`. To answer a prompt dialog in another pane with keys, add " +
84
- "`# wrangle: answer-prompt` to `herdr agent send-keys`; each such use is recorded.";
82
+ "with it. Message the agent through its dogsled inbox instead: the `send` tool (mcp__dogsled__send) or " +
83
+ "`dogsled send <name-or-pane> --message <text>`. To answer a prompt dialog in another pane with keys, add " +
84
+ "`# dogsled: answer-prompt` to `herdr agent send-keys`; each such use is recorded.";
85
85
 
86
- const ANSWER_PROMPT = /#\s*wrangle:\s*answer-prompt\b/u,
86
+ const ANSWER_PROMPT = /#\s*dogsled:\s*answer-prompt\b/u,
87
87
  HERDR_VALUE_OPTIONS: ReadonlySet<string> = new Set(["--session", "--machine", "--remote"]),
88
88
  PANE_TYPING: ReadonlySet<string> = new Set(["send-text", "send-keys", "send-input"]);
89
89
 
@@ -161,7 +161,7 @@ function shellPieces(words: Words): string | null {
161
161
 
162
162
  // Typing into another agent's terminal: `herdr agent prompt`, `herdr agent send-keys`, and
163
163
  // `herdr pane send-text|send-keys|send-input` aimed at a pane that runs an agent. `agent send-keys` with
164
- // `# wrangle: answer-prompt` on the command line is allowed and recorded. The caller's own pane is not
164
+ // `# dogsled: answer-prompt` on the command line is allowed and recorded. The caller's own pane is not
165
165
  // Another agent's. `isAgent` is the pane lookup; the second argument is the cwd every classifier receives.
166
166
  export function classifyAgentTyping(
167
167
  command: Command,
@@ -229,8 +229,8 @@ export const IMPERATIVE_MSG = {
229
229
  export const WAIT_MSG =
230
230
  "wait-gate: blocked a command that waits instead of working (sleep, watch, a follow flag, a --watch or --wait " +
231
231
  "flag, or a wait subcommand). Never hold a tool call to wait, and never re-read a log to check progress. " +
232
- "For long work in pi, use Wrangle `run` (mcp__wrangle__run) and continue other work. " +
233
- "Use `read` (mcp__wrangle__read) for output and `stop` (mcp__wrangle__stop) to cancel the job. " +
232
+ "For long work in pi, use Dogsled `run` (mcp__dogsled__run) and continue other work. " +
233
+ "Use `read` (mcp__dogsled__read) for output and `stop` (mcp__dogsled__stop) to cancel the job. " +
234
234
  "Start long work so the host reports its exit: in Claude Code, `run_in_background` " +
235
235
  "or Monitor (the exit arrives as a message); in Codex, exec_command, then empty write_stdin calls, which " +
236
236
  "return when the process exits. " +
@@ -1,6 +1,7 @@
1
1
  # @oisin-ee/pi-prewalk
2
2
 
3
- Prewalk starts with the active planner model. After the planning nudge, a successful `todo` call opens the handoff gate.
3
+ Prewalk starts with the active planner model. After the planning nudge, validated local task activation opens the handoff gate.
4
+ Without the task-provider capability, a successful `todo` call opens the gate instead. Imported workers need no bookkeeping call.
4
5
  The first subsequent `edit` or `write` result switches the session to the target model, even if that result failed.
5
6
  Nested calls through codemode also count. Without a callable `todo` tool, the gate is open.
6
7
 
@@ -27,7 +28,7 @@ The startup flags are `--prewalk`, `--no-prewalk`, and `--prewalk-into provider/
27
28
  Do not combine `--no-prewalk` with either arming flag.
28
29
  Flags affect only the startup session. New sessions use `enabled` from config. Resumed sessions do not arm automatically.
29
30
 
30
- For a wrangle child, add `"pi_args": ["--prewalk-into", "alias/luna"]` to its profile.
31
+ For a dogsled child, add `"pi_args": ["--prewalk-into", "alias/luna"]` to its profile.
31
32
  To suppress configured prewalk for that child, use `"pi_args": ["--no-prewalk"]`.
32
33
 
33
34
  ## State and context
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oisin-ee/pi-prewalk",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
4
4
  "description": "A one-shot planner-to-executor handoff for pi, with branch-backed Rust rules.",
5
5
  "keywords": [
6
6
  "pi-extension",
@@ -4,6 +4,7 @@ import type { BoundaryResult, SessionBoundaryDraft } from "@earendil-works/pi-co
4
4
  import type { Context, Files, Host } from "./ports.ts";
5
5
  import { branchRecords, calls } from "./branch.ts";
6
6
  import type { CallEvidence } from "./branch.ts";
7
+ import { activeTask } from "./tasks.ts";
7
8
  import { decide } from "./wasm.ts";
8
9
 
9
10
  /**
@@ -117,6 +118,7 @@ class Adapter {
117
118
  todo.exposure === "deferred" ||
118
119
  this.#pi.getActiveTools().includes("todo"));
119
120
  return {
121
+ active_unit: activeTask(this.#pi.events),
120
122
  available: ctx.modelRegistry
121
123
  .getAvailable()
122
124
  .map((candidate) => `${candidate.provider}/${candidate.id}`),
@@ -55,6 +55,10 @@ export interface Facts {
55
55
  * Is todo callable (directly or through codemode)?
56
56
  */
57
57
  todo: boolean;
58
+ /**
59
+ * Current provider activation; absent preserves the legacy successful-todo rule.
60
+ */
61
+ active_unit?: boolean | undefined;
58
62
  /**
59
63
  * Todo source identity for status.
60
64
  */
@@ -12,7 +12,7 @@ type Host = Pick<
12
12
  | "getThinkingLevel"
13
13
  | "setThinkingLevel"
14
14
  | "setModel"
15
- >;
15
+ > & { readonly events: Readonly<ExtensionAPI["events"]> };
16
16
  /** Read-only pi facts. */
17
17
  interface Context {
18
18
  readonly cwd: string;
@@ -0,0 +1,48 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+
3
+ interface ActiveProvider {
4
+ readonly active: () => unknown;
5
+ }
6
+
7
+ /**
8
+ * Validates only the capability method this consumer needs.
9
+ * @param {unknown} value Capability response.
10
+ * @returns {boolean} Whether the active query is callable.
11
+ */
12
+ const isProvider = (value: unknown): value is ActiveProvider =>
13
+ typeof value === "object" &&
14
+ value !== null &&
15
+ "active" in value &&
16
+ typeof value.active === "function",
17
+ /**
18
+ * Queries current activation without loading todo or interpreting its snapshots.
19
+ * @param {Readonly<ExtensionAPI["events"]>} events Extension capability bus.
20
+ * @returns {boolean | undefined} Activation, or unavailable legacy capability.
21
+ */
22
+ activeTask = (events: Readonly<ExtensionAPI["events"]>): boolean | undefined => {
23
+ const replies: unknown[] = [];
24
+ events.emit("oisin.tasks.acquire.v1", {
25
+ reply: (provider: unknown): void => {
26
+ replies.push(provider);
27
+ },
28
+ version: 1,
29
+ });
30
+ const [provider] = replies,
31
+ one = 1;
32
+ if (replies.length !== one || !isProvider(provider)) {
33
+ return undefined;
34
+ }
35
+ const result: unknown = provider.active();
36
+ if (
37
+ typeof result !== "object" ||
38
+ result === null ||
39
+ !("ok" in result) ||
40
+ result.ok !== true ||
41
+ !("value" in result)
42
+ ) {
43
+ return undefined;
44
+ }
45
+ return result.value !== null && result.value !== undefined;
46
+ };
47
+
48
+ export { activeTask };
@@ -82,14 +82,14 @@ Outside the terminal UI, `/todos` prints the list instead.
82
82
 
83
83
  ## Shared work display
84
84
 
85
- When a compatible pi-wrangle extension is active, its work widget replaces the standalone todo widget.
86
- Tasks remain on the current Pi session branch. Wrangle receives a read-only presentation capability through `pi.events`.
87
- Pi-todo owns the renderer in both standalone and shared views. Wrangle hosts it above the editor without opening a popup.
85
+ When a compatible pi-dogsled extension is active, its work widget replaces the standalone todo widget.
86
+ Tasks remain on the current Pi session branch. Dogsled receives a read-only presentation capability through `pi.events`.
87
+ Pi-todo owns the renderer in both standalone and shared views. Dogsled hosts it above the editor without opening a popup.
88
88
  Alt+J/K scrolls the hosted rows. Alt+I toggles their expansion. Ordinary editor keys remain unchanged.
89
89
  `/todos` and Alt+T request the shared Todos view. `/todos list` still prints the full list.
90
90
  When shared presentation ends, the standalone widget returns without changing task state.
91
91
 
92
- The [version 2 display contract](https://github.com/oisin-ee/wrangle/blob/main/docs/pi-work.md)
92
+ The [version 2 display contract](https://github.com/oisin-ee/dogsled/blob/main/docs/pi-work.md)
93
93
  defines the snapshot request, acknowledgement, release, and read-only view events.
94
94
  Incompatible versions leave the standalone widget available. Rendering failures restore it without changing saved tasks.
95
95
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oisin-ee/pi-todo",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
4
4
  "description": "A todo tool for pi with a Rust core: one active task, state on the session branch.",
5
5
  "keywords": [
6
6
  "pi-extension",
@@ -0,0 +1,103 @@
1
+ import type { TodoParams, TodoSession } from "./tool.ts";
2
+ import { STATE_ENTRY } from "./state.ts";
3
+ import type { SessionEntry } from "@earendil-works/pi-coding-agent";
4
+ import type { TodoList } from "./wasm.ts";
5
+ import { runTodo } from "./tool.ts";
6
+
7
+ interface Fixture {
8
+ readonly entries: SessionEntry[];
9
+ readonly session: TodoSession;
10
+ readonly call: (params: TodoParams) => TodoList;
11
+ }
12
+
13
+ /**
14
+ * Creates a branch-backed caller using the real tool and WASM core.
15
+ * @param {readonly unknown[]} snapshots Existing stored snapshots.
16
+ * @returns {Fixture} The tool caller and Pi append seam.
17
+ */
18
+ const first = 0,
19
+ second = 1,
20
+ third = 2,
21
+ last = -1,
22
+ fixture = (snapshots: readonly unknown[] = []): Fixture => {
23
+ const entries = snapshots.map((data): SessionEntry => ({
24
+ customType: STATE_ENTRY,
25
+ data,
26
+ id: crypto.randomUUID(),
27
+ parentId: null,
28
+ timestamp: "2026-10-20T00:00:00Z",
29
+ type: "custom",
30
+ })),
31
+ session: TodoSession = {
32
+ append: (customType, data): void => {
33
+ entries.push({
34
+ customType,
35
+ data,
36
+ id: crypto.randomUUID(),
37
+ parentId: entries.at(last)?.id ?? null,
38
+ timestamp: "2026-10-20T00:00:00Z",
39
+ type: "custom",
40
+ });
41
+ },
42
+ branch: () => entries,
43
+ sessionId: "test-session",
44
+ };
45
+ return { call: (params) => runTodo(params, session).details, entries, session };
46
+ },
47
+ /**
48
+ * A complete self-assigned contract for deterministic behavior tests.
49
+ * @returns {NonNullable<TodoParams["contract"]>} Authored contract fields.
50
+ */
51
+ contract = (): NonNullable<TodoParams["contract"]> => ({
52
+ acceptance: {
53
+ argv: ["moon", "run", "todo-core:test"],
54
+ cwd: ".",
55
+ expected: "passes",
56
+ kind: "command",
57
+ },
58
+ assignment: { kind: "self" },
59
+ deliverable: "Executable task contracts",
60
+ dependsOn: [],
61
+ scope: { generates: [], owns: ["src/"], reads: ["README.md"], repositoryId: "oisinpi" },
62
+ }),
63
+ /**
64
+ * Returns the runtime ID from the first root in a tool result.
65
+ * @param {TodoList} list Real tool result.
66
+ * @returns {string} Runtime ID.
67
+ */
68
+ firstId = (list: TodoList): string => {
69
+ const id = list.tasks.at(first)?.unit?.reference.unitId;
70
+ if (id === undefined) {
71
+ throw new Error("Expected a runtime unit ID");
72
+ }
73
+ return id;
74
+ },
75
+ /**
76
+ * Requires a root identity at a known fixture position.
77
+ * @param {TodoList} list Tool result.
78
+ * @param {number} index Root position.
79
+ * @returns {string} Runtime ID.
80
+ */
81
+ rootId = (list: TodoList, index: number): string => {
82
+ const id = list.tasks.at(index)?.unit?.reference.unitId;
83
+ if (id === undefined) {
84
+ throw new Error("Expected root identity");
85
+ }
86
+ return id;
87
+ },
88
+ /**
89
+ * Requires a child identity from the first outcome.
90
+ * @param {TodoList} list Tool result.
91
+ * @param {number} index Child position.
92
+ * @returns {string} Runtime ID.
93
+ */
94
+ childId = (list: TodoList, index: number): string => {
95
+ const id = list.tasks.at(first)?.subtasks?.at(index)?.unit?.reference.unitId;
96
+ if (id === undefined) {
97
+ throw new Error("Expected child identity");
98
+ }
99
+ return id;
100
+ };
101
+
102
+ export { childId, contract, first, firstId, fixture, last, rootId, second, third };
103
+ export type { Fixture };
@@ -41,8 +41,14 @@ const emptyCount = 0,
41
41
  active = list.tasks.flatMap((task) => [
42
42
  ...(task.status === "in_progress" ? [task.content] : []),
43
43
  ...(task.subtasks ?? [])
44
- .filter((child: Readonly<Subtask>) => child.status === "in_progress")
45
- .map((child: Readonly<Subtask>) => `${task.content} → ${child.content}`),
44
+ .filter(
45
+ (child: Readonly<Pick<Subtask, "content" | "status">>) =>
46
+ child.status === "in_progress",
47
+ )
48
+ .map(
49
+ (child: Readonly<Pick<Subtask, "content" | "status">>) =>
50
+ `${task.content} → ${child.content}`,
51
+ ),
46
52
  ]),
47
53
  current =
48
54
  active.length > emptyCount
@@ -0,0 +1,79 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import type { TaskProvider } from "./provider.ts";
3
+ import { ownsTodo } from "./lifecycle.ts";
4
+
5
+ /** Exact structured reads and workflow controls, never command-text inference. */
6
+ const controls = new Set([
7
+ "ask",
8
+ "codemode",
9
+ "interview",
10
+ "todo",
11
+ "tool_search",
12
+ "read",
13
+ "ls",
14
+ "find",
15
+ "grep",
16
+ "hypa_read",
17
+ "hypa_ls",
18
+ "hypa_find",
19
+ "hypa_grep",
20
+ "web_search",
21
+ "web_fetch",
22
+ "lsp_diagnostics",
23
+ "ctx_search",
24
+ "ctx_stats",
25
+ "mcp__codebase_memory__search_graph",
26
+ "mcp__codebase_memory__get_code_snippet",
27
+ "mcp__codebase_memory__trace_path",
28
+ "mcp__codebase_memory__detect_changes",
29
+ "hindsight_search_knowledge_pages",
30
+ "hindsight_list_knowledge_pages",
31
+ "hindsight_read_knowledge_page",
32
+ "hindsight_reflect",
33
+ "hindsight_sync_status",
34
+ "hindsight_diagnose",
35
+ "hindsight_capture_initiative",
36
+ "mcp__dogsled__spawn",
37
+ "mcp__dogsled__send",
38
+ "mcp__dogsled__cancel",
39
+ "mcp__dogsled__close",
40
+ "mcp__dogsled__stop",
41
+ "mcp__dogsled__read",
42
+ "mcp__dogsled__status",
43
+ "mcp__dogsled__inbox",
44
+ ]),
45
+ /**
46
+ * Checks actual calls and leases execution-bearing calls.
47
+ * @param {Readonly<ExtensionAPI>} pi Runtime hooks and metadata.
48
+ * @param {Readonly<TaskProvider>} provider Task state and leases.
49
+ */
50
+ registerGate = (
51
+ pi: Readonly<Pick<ExtensionAPI, "on" | "getAllTools">>,
52
+ provider: Readonly<TaskProvider>,
53
+ ): void => {
54
+ pi.on("tool_call", (event: Readonly<{ toolName: string; toolCallId: string }>, ctx) => {
55
+ if (!ownsTodo(pi)) {
56
+ return { block: false };
57
+ }
58
+ provider.context(ctx);
59
+ if (controls.has(event.toolName)) {
60
+ return { block: false };
61
+ }
62
+ const active = provider.capability().active();
63
+ if (!active.ok || active.value === null) {
64
+ return {
65
+ block: true,
66
+ reason: active.ok
67
+ ? "Start a complete self-assigned leaf with todo before shell, code, mutation, or unknown tools."
68
+ : active.error.message,
69
+ };
70
+ }
71
+ provider.lease(event.toolCallId, active.value.ref);
72
+ return { block: false };
73
+ });
74
+ pi.on("tool_execution_end", (event: Readonly<{ toolCallId: string }>) => {
75
+ provider.release(event.toolCallId);
76
+ });
77
+ };
78
+
79
+ export { registerGate };