@pi-unipi/unipi 3.0.0-alpha.8 → 3.0.0-alpha.9

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 (40) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/package.json +21 -21
  3. package/packages/ask-user/package.json +2 -2
  4. package/packages/autocomplete/package.json +1 -1
  5. package/packages/autocomplete/src/constants.ts +3 -1
  6. package/packages/background-tasks/package.json +2 -2
  7. package/packages/btw/package.json +2 -2
  8. package/packages/compactor/package.json +3 -3
  9. package/packages/core/package.json +1 -1
  10. package/packages/footer/package.json +3 -3
  11. package/packages/fusion/package.json +3 -3
  12. package/packages/info-screen/package.json +2 -2
  13. package/packages/input-shortcuts/package.json +2 -2
  14. package/packages/kanboard/index.ts +1 -0
  15. package/packages/kanboard/package.json +7 -7
  16. package/packages/kanboard/src/commands.ts +53 -19
  17. package/packages/kanboard/src/guard.ts +41 -18
  18. package/packages/kanboard/src/runner.ts +86 -11
  19. package/packages/kanboard/src/settings.ts +5 -0
  20. package/packages/long-horizon/index.ts +57 -4
  21. package/packages/long-horizon/package.json +2 -2
  22. package/packages/long-horizon/src/commands.ts +80 -5
  23. package/packages/long-horizon/src/engine/continuation.ts +64 -8
  24. package/packages/long-horizon/src/engine/goal-state.ts +62 -12
  25. package/packages/long-horizon/src/engine/runaway.ts +67 -10
  26. package/packages/long-horizon/src/gate.ts +16 -3
  27. package/packages/long-horizon/src/prompts/goal.ts +1 -1
  28. package/packages/long-horizon/src/runtime.ts +101 -6
  29. package/packages/long-horizon/src/tools/goal.ts +6 -0
  30. package/packages/mcp/package.json +2 -2
  31. package/packages/memory/package.json +3 -3
  32. package/packages/notify/package.json +2 -2
  33. package/packages/skill-registry/package.json +2 -2
  34. package/packages/subagents/package.json +2 -2
  35. package/packages/unipi/bundled.js +471 -82
  36. package/packages/updater/package.json +2 -2
  37. package/packages/utility/package.json +2 -2
  38. package/packages/watchdog/package.json +3 -3
  39. package/packages/web-api/package.json +2 -2
  40. package/packages/workflow/package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -6,6 +6,20 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [3.0.0-alpha.9] — 2026-09-29
10
+
11
+ ### Added
12
+ - `long-horizon`: `/unipi:goal|swarm|graph stop` — terminally ends the active owner (goal → `complete(user_requested)`; pending proposal discarded; no further continuation follow-up). `/unipi:clear` on an active owner now points at stop instead of silently doing nothing.
13
+ - `long-horizon`: `/unipi:regular` — stops any active owner (incl. ralph loops) and pins the session to plain mode; an explicit mode command unlatches it.
14
+ - `long-horizon`: command runners `unipi:goal-pause` / `unipi:goal-resume` / `unipi:goal-stop` for cross-module callers.
15
+ - `kanboard`: `/unipi:kanboard-do` grants session **write credits** (setting `kanboard.doCredits`, default 10) instead of a turn-scoped window — credits persist across turns, each board write costs 1, reads stay free, runner tasks keep unlimited access, `off` revokes.
16
+
17
+ ### Changed
18
+ - `long-horizon`: verifier resilience — evaluator failures retry once, then pause `paused(verifier_unavailable)`; two consecutive inconclusive verdicts pause `paused(verifier_inconclusive)`; five consecutive `not_met` claims pause unconditionally (stall-cap coupling removed). Paused goals park the owner so `/unipi:goal resume` works.
19
+ - `long-horizon`: tokens — provider usage now read in both pi shapes (`{input, output, …}` and `{inputTokens, outputTokens}`); usage-less turns estimate tokens (chars/4) and flag `tokens_estimated` in `get_goal` and the continuation hint.
20
+ - `long-horizon`: no-progress guard keys "identical arguments" on real tool arguments (captured per `toolCallId`), exempts search no-matches, `get_goal`/`update_goal`, and kanboard reads — no more false "ran 3 times with identical arguments" after three different commands.
21
+ - `kanboard`: an interrupted/released task parks its goal (id recorded in the release note) and resumes it on re-claim; a user-stopped goal releases the task to Todo ("goal stopped by user") instead of In Review; a task cancelled outside the runner stops its goal.
22
+ - `kanboard`: `-do` suggestions only for task-id-like tokens (no more task rows under free prose).
9
23
  ## [3.0.0-alpha.7] — 2026-09-28
10
24
 
11
25
  ### Changed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/unipi",
3
- "version": "3.0.0-alpha.8",
3
+ "version": "3.0.0-alpha.9",
4
4
  "description": "All-in-one extension suite for Pi coding agent",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -83,26 +83,26 @@
83
83
  "typebox": "^1.1.38"
84
84
  },
85
85
  "dependencies": {
86
- "@pi-unipi/ask-user": "3.0.0-alpha.8",
87
- "@pi-unipi/background-tasks": "3.0.0-alpha.8",
88
- "@pi-unipi/btw": "3.0.0-alpha.8",
89
- "@pi-unipi/command-enchantment": "3.0.0-alpha.8",
90
- "@pi-unipi/compactor": "3.0.0-alpha.8",
91
- "@pi-unipi/core": "3.0.0-alpha.8",
92
- "@pi-unipi/footer": "3.0.0-alpha.8",
93
- "@pi-unipi/fusion": "3.0.0-alpha.8",
94
- "@pi-unipi/info-screen": "3.0.0-alpha.8",
95
- "@pi-unipi/input-shortcuts": "3.0.0-alpha.8",
96
- "@pi-unipi/kanboard": "3.0.0-alpha.8",
97
- "@pi-unipi/mcp": "3.0.0-alpha.8",
98
- "@pi-unipi/memory": "3.0.0-alpha.8",
99
- "@pi-unipi/notify": "3.0.0-alpha.8",
100
- "@pi-unipi/skill-registry": "3.0.0-alpha.8",
101
- "@pi-unipi/subagents": "3.0.0-alpha.8",
102
- "@pi-unipi/updater": "3.0.0-alpha.8",
103
- "@pi-unipi/utility": "3.0.0-alpha.8",
104
- "@pi-unipi/web-api": "3.0.0-alpha.8",
105
- "@pi-unipi/workflow": "3.0.0-alpha.8"
86
+ "@pi-unipi/ask-user": "3.0.0-alpha.9",
87
+ "@pi-unipi/background-tasks": "3.0.0-alpha.9",
88
+ "@pi-unipi/btw": "3.0.0-alpha.9",
89
+ "@pi-unipi/command-enchantment": "3.0.0-alpha.9",
90
+ "@pi-unipi/compactor": "3.0.0-alpha.9",
91
+ "@pi-unipi/core": "3.0.0-alpha.9",
92
+ "@pi-unipi/footer": "3.0.0-alpha.9",
93
+ "@pi-unipi/fusion": "3.0.0-alpha.9",
94
+ "@pi-unipi/info-screen": "3.0.0-alpha.9",
95
+ "@pi-unipi/input-shortcuts": "3.0.0-alpha.9",
96
+ "@pi-unipi/kanboard": "3.0.0-alpha.9",
97
+ "@pi-unipi/mcp": "3.0.0-alpha.9",
98
+ "@pi-unipi/memory": "3.0.0-alpha.9",
99
+ "@pi-unipi/notify": "3.0.0-alpha.9",
100
+ "@pi-unipi/skill-registry": "3.0.0-alpha.9",
101
+ "@pi-unipi/subagents": "3.0.0-alpha.9",
102
+ "@pi-unipi/updater": "3.0.0-alpha.9",
103
+ "@pi-unipi/utility": "3.0.0-alpha.9",
104
+ "@pi-unipi/web-api": "3.0.0-alpha.9",
105
+ "@pi-unipi/workflow": "3.0.0-alpha.9"
106
106
  },
107
107
  "devDependencies": {
108
108
  "@earendil-works/pi-agent-core": "^0.87.1",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/ask-user",
3
- "version": "3.0.0-alpha.8",
3
+ "version": "3.0.0-alpha.9",
4
4
  "description": "Structured user input tool for Pi coding agent — single-select, multi-select, freeform",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -40,7 +40,7 @@
40
40
  "access": "public"
41
41
  },
42
42
  "dependencies": {
43
- "@pi-unipi/core": "3.0.0-alpha.8"
43
+ "@pi-unipi/core": "3.0.0-alpha.9"
44
44
  },
45
45
  "peerDependencies": {
46
46
  "@earendil-works/pi-coding-agent": "^0.87.1",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/command-enchantment",
3
- "version": "3.0.0-alpha.8",
3
+ "version": "3.0.0-alpha.9",
4
4
  "description": "Enhanced TUI autocomplete for /unipi:* commands — colored, sorted, and grouped by package",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -42,11 +42,12 @@ export const COMMAND_REGISTRY: Record<string, string> = {
42
42
  "unipi:plan": "workflow",
43
43
  "unipi:permission": "workflow",
44
44
 
45
- // long-horizon (4 commands)
45
+ // long-horizon (5 commands)
46
46
  "unipi:goal": "long-horizon",
47
47
  "unipi:ralph": "long-horizon",
48
48
  "unipi:swarm": "long-horizon",
49
49
  "unipi:graph": "long-horizon",
50
+ "unipi:regular": "long-horizon",
50
51
 
51
52
  // memory (7 commands)
52
53
  "unipi:memory": "memory",
@@ -140,6 +141,7 @@ export const COMMAND_DESCRIPTIONS: Record<string, string> = {
140
141
  "unipi:ralph": "Checklist grind over iterations · enumerable chores · low cost, solid success",
141
142
  "unipi:swarm": "Parallel fan-out + synthesis · complex decomposable · higher cost, high coverage",
142
143
  "unipi:graph": "Dependent multi-step work · later steps need earlier results · highest cost",
144
+ "unipi:regular": "Regular mode — stop the active owner and run prompts without long-horizon routing",
143
145
 
144
146
  "unipi:memory": "Memory palace — status, migrate, recall/write toggles",
145
147
  "unipi:memory-process": "Analyze text and store extracted memories",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/background-tasks",
3
- "version": "3.0.0-alpha.8",
3
+ "version": "3.0.0-alpha.9",
4
4
  "description": "Background tasks for UniPi — durable shell jobs and delegated background agents with a footer dock",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -10,7 +10,7 @@
10
10
  },
11
11
  "dependencies": {
12
12
  "@earendil-works/pi-ai": "^0.87.1",
13
- "@pi-unipi/core": "3.0.0-alpha.8",
13
+ "@pi-unipi/core": "3.0.0-alpha.9",
14
14
  "turndown": "^7.2.4"
15
15
  },
16
16
  "devDependencies": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/btw",
3
- "version": "3.0.0-alpha.8",
3
+ "version": "3.0.0-alpha.9",
4
4
  "description": "A pi extension for parallel side conversations with /unipi:btw — part of the Unipi suite",
5
5
  "type": "module",
6
6
  "main": "extensions/btw.ts",
@@ -37,7 +37,7 @@
37
37
  "@earendil-works/pi-tui": "^0.87.1"
38
38
  },
39
39
  "dependencies": {
40
- "@pi-unipi/core": "3.0.0-alpha.8"
40
+ "@pi-unipi/core": "3.0.0-alpha.9"
41
41
  },
42
42
  "pi": {
43
43
  "extensions": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/compactor",
3
- "version": "3.0.0-alpha.8",
3
+ "version": "3.0.0-alpha.9",
4
4
  "description": "Compaction for Pi — lossless zero-LLM or model summaries, rebuilt from full history, with session recall",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -33,8 +33,8 @@
33
33
  "access": "public"
34
34
  },
35
35
  "dependencies": {
36
- "@pi-unipi/core": "3.0.0-alpha.8",
37
- "@pi-unipi/info-screen": "3.0.0-alpha.8",
36
+ "@pi-unipi/core": "3.0.0-alpha.9",
37
+ "@pi-unipi/info-screen": "3.0.0-alpha.9",
38
38
  "@earendil-works/pi-agent-core": "^0.87.1"
39
39
  },
40
40
  "peerDependencies": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/core",
3
- "version": "3.0.0-alpha.8",
3
+ "version": "3.0.0-alpha.9",
4
4
  "description": "Shared utilities, event types, and constants for Unipi extension suite",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/footer",
3
- "version": "3.0.0-alpha.8",
3
+ "version": "3.0.0-alpha.9",
4
4
  "description": "Persistent status bar for Unipi — subscribes to UNIPI_EVENTS and renders key stats from all unipi packages",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -32,8 +32,8 @@
32
32
  "access": "public"
33
33
  },
34
34
  "dependencies": {
35
- "@pi-unipi/core": "3.0.0-alpha.8",
36
- "@pi-unipi/background-tasks": "3.0.0-alpha.8"
35
+ "@pi-unipi/core": "3.0.0-alpha.9",
36
+ "@pi-unipi/background-tasks": "3.0.0-alpha.9"
37
37
  },
38
38
  "peerDependencies": {
39
39
  "@earendil-works/pi-coding-agent": "^0.87.1",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/fusion",
3
- "version": "3.0.0-alpha.8",
3
+ "version": "3.0.0-alpha.9",
4
4
  "description": "Devin-style model picker, fusion presets (lead + sidekick), and Local Fusion runtime for UniPi",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -32,8 +32,8 @@
32
32
  "access": "public"
33
33
  },
34
34
  "dependencies": {
35
- "@pi-unipi/core": "3.0.0-alpha.8",
36
- "@pi-unipi/subagents": "3.0.0-alpha.8"
35
+ "@pi-unipi/core": "3.0.0-alpha.9",
36
+ "@pi-unipi/subagents": "3.0.0-alpha.9"
37
37
  },
38
38
  "peerDependencies": {
39
39
  "@earendil-works/pi-ai": "^0.87.1",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/info-screen",
3
- "version": "3.0.0-alpha.8",
3
+ "version": "3.0.0-alpha.9",
4
4
  "description": "Dashboard and module registry for Unipi — configurable info overlay with tabbed groups",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -32,7 +32,7 @@
32
32
  "access": "public"
33
33
  },
34
34
  "dependencies": {
35
- "@pi-unipi/core": "3.0.0-alpha.8"
35
+ "@pi-unipi/core": "3.0.0-alpha.9"
36
36
  },
37
37
  "peerDependencies": {
38
38
  "@earendil-works/pi-coding-agent": "^0.87.1",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/input-shortcuts",
3
- "version": "3.0.0-alpha.8",
3
+ "version": "3.0.0-alpha.9",
4
4
  "description": "Keyboard shortcuts for stash/restore, undo/redo, clipboard, and thinking toggle — chord-based overlay system",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -33,7 +33,7 @@
33
33
  "access": "public"
34
34
  },
35
35
  "dependencies": {
36
- "@pi-unipi/core": "3.0.0-alpha.8"
36
+ "@pi-unipi/core": "3.0.0-alpha.9"
37
37
  },
38
38
  "peerDependencies": {
39
39
  "@earendil-works/pi-coding-agent": "^0.87.1",
@@ -71,6 +71,7 @@ export default function (pi: ExtensionAPI) {
71
71
  return status?.phase === "running" ? status.taskId : null;
72
72
  },
73
73
  () => readKanboardSettings().turnAddLimit,
74
+ () => readKanboardSettings().doCredits,
74
75
  );
75
76
  const sessionId = (): string => process.env.UNIPI_KANBOARD_SESSION ?? `pi-${process.pid}`;
76
77
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/kanboard",
3
- "version": "3.0.0-alpha.8",
3
+ "version": "3.0.0-alpha.9",
4
4
  "description": "Kanboard v3 — deferred-work board: CLI/daemon bridge, task runner and skill",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -31,7 +31,7 @@
31
31
  "access": "public"
32
32
  },
33
33
  "dependencies": {
34
- "@pi-unipi/core": "3.0.0-alpha.8"
34
+ "@pi-unipi/core": "3.0.0-alpha.9"
35
35
  },
36
36
  "peerDependencies": {
37
37
  "@earendil-works/pi-coding-agent": "^0.87.1",
@@ -48,10 +48,10 @@
48
48
  "themes": []
49
49
  },
50
50
  "optionalDependencies": {
51
- "@pi-unipi/kanboard-linux-x64": "3.0.0-alpha.8",
52
- "@pi-unipi/kanboard-linux-arm64": "3.0.0-alpha.8",
53
- "@pi-unipi/kanboard-darwin-x64": "3.0.0-alpha.8",
54
- "@pi-unipi/kanboard-darwin-arm64": "3.0.0-alpha.8",
55
- "@pi-unipi/kanboard-win32-x64": "3.0.0-alpha.8"
51
+ "@pi-unipi/kanboard-linux-x64": "3.0.0-alpha.9",
52
+ "@pi-unipi/kanboard-linux-arm64": "3.0.0-alpha.9",
53
+ "@pi-unipi/kanboard-darwin-x64": "3.0.0-alpha.9",
54
+ "@pi-unipi/kanboard-darwin-arm64": "3.0.0-alpha.9",
55
+ "@pi-unipi/kanboard-win32-x64": "3.0.0-alpha.9"
56
56
  }
57
57
  }
@@ -40,9 +40,9 @@ export const HELP = `Kanboard commands
40
40
  /unipi:kanboard-autowork start|stop work ready tasks chain by chain; stop finishes the current one first
41
41
  Priority -p: 1 none · 2 low · 3 medium · 4 high · 5 urgent`;
42
42
 
43
- export function doText(slug: string, cli: string, request: string, queueMax = 10): string {
43
+ export function doText(slug: string, cli: string, request: string, queueMax = 10, credits = 0): string {
44
44
  const limit = queueMax === 0 ? "Todo tasks" : `Todo tasks, at most ${queueMax}`;
45
- return `[kanboard] For this request you may use the kanboard skill on project ${slug} (CLI: \`${cli} --actor agent --project ${slug} …\`). Board writes are allowed until this turn ends. You can add tasks, move them between backlog and todo, link, order, note, and edit tasks you created. To have tasks worked, queue them in order with \`queue <IDs>\` (${limit}); the runner starts them one by one after this turn, so do not start the work yourself. Read a task with \`show <ID>\` before editing, linking or queueing it — \`list\` only shows titles and a one-line excerpt. To choose how a task is worked, label it with \`edit <ID> --strategy none|goal|ralph|swarm|graph\` and \`--plan yes|no\`; leave it unset to let the runner decide. If the request is unclear, ask me instead of guessing.
45
+ return `[kanboard] For this request you may use the kanboard skill on project ${slug} (CLI: \`${cli} --actor agent --project ${slug} …\`). You have ${credits} write credit${credits === 1 ? "" : "s"} remaining this session — each board write (add, move, edit, note, link, order, queue) costs 1, reads are free; /unipi:kanboard-do tops the credits back up. You can add tasks, move them between backlog and todo, link, order, note, and edit tasks you created. To have tasks worked, queue them in order with \`queue <IDs>\` (${limit}); the runner starts them one by one after this turn, so do not start the work yourself. Read a task with \`show <ID>\` before editing, linking or queueing it — \`list\` only shows titles and a one-line excerpt. To choose how a task is worked, label it with \`edit <ID> --strategy none|goal|ralph|swarm|graph\` and \`--plan yes|no\`; leave it unset to let the runner decide. If the request is unclear, ask me instead of guessing.
46
46
 
47
47
  Request: ${request}`;
48
48
  }
@@ -1093,7 +1093,21 @@ export function kanboardCompletions(prefix: string): CompletionItem[] | null {
1093
1093
  /** Live task list for `--after` / `-do` completions, cached ~5s per client. */
1094
1094
  const taskCache = new WeakMap<KanboardCli, { at: number; tasks: ShowTask[] }>();
1095
1095
 
1096
- export async function taskCompletions(deps: CommandDeps, prefix: string, before: string): Promise<CompletionItem[] | null> {
1096
+ export interface TaskCompletionOptions {
1097
+ /** Also match task TITLES (default true); false = id prefixes only. */
1098
+ titleSearch?: boolean;
1099
+ /** Row style: "status" = label id, description "<status> — <title>";
1100
+ * "id-title" = label "<id> <title>", description "<status>". */
1101
+ rowStyle?: "status" | "id-title";
1102
+ }
1103
+
1104
+ export async function taskCompletions(
1105
+ deps: CommandDeps,
1106
+ prefix: string,
1107
+ before: string,
1108
+ opts: TaskCompletionOptions = {},
1109
+ ): Promise<CompletionItem[] | null> {
1110
+ const { titleSearch = true, rowStyle = "status" } = opts;
1097
1111
  const client = deps.cli;
1098
1112
  if (!client) return null;
1099
1113
  let cache = taskCache.get(client);
@@ -1109,13 +1123,21 @@ export async function taskCompletions(deps: CommandDeps, prefix: string, before:
1109
1123
  const needle = prefix.toLowerCase();
1110
1124
  const items = cache.tasks
1111
1125
  .filter((task) => !["cancelled", "archived"].includes(task.status))
1112
- .filter((task) => task.id.toLowerCase().startsWith(needle) || task.title.toLowerCase().includes(needle))
1126
+ .filter((task) => task.id.toLowerCase().startsWith(needle) || (titleSearch && task.title.toLowerCase().includes(needle)))
1113
1127
  .slice(0, 12)
1114
- .map((task) => ({
1115
- value: fullArgs(before, task.id),
1116
- label: task.id,
1117
- description: `${task.status} — ${task.title}`,
1118
- }));
1128
+ .map((task) =>
1129
+ rowStyle === "id-title"
1130
+ ? {
1131
+ value: fullArgs(before, task.id),
1132
+ label: `${task.id} ${task.title.slice(0, 64)}`,
1133
+ description: task.status,
1134
+ }
1135
+ : {
1136
+ value: fullArgs(before, task.id),
1137
+ label: task.id,
1138
+ description: `${task.status} — ${task.title}`,
1139
+ },
1140
+ );
1119
1141
  return items.length > 0 ? items : null;
1120
1142
  }
1121
1143
 
@@ -1191,15 +1213,19 @@ export async function kanboardAddCompletions(deps: CommandDeps, prefix: string):
1191
1213
  return null;
1192
1214
  }
1193
1215
 
1194
- /** /unipi:kanboard-do … — complete a trailing task-id prefix. */
1216
+ /** /unipi:kanboard-do … — complete a trailing task-id token; plain prose never matches. */
1195
1217
  export async function kanboardDoCompletions(deps: CommandDeps, prefix: string): Promise<CompletionItem[] | null> {
1196
1218
  const raw = prefix ?? "";
1197
- const match = /^(.*?)(\S*)$/.exec(raw)!;
1198
- const before = match[1]!;
1199
- const token = match[2]!;
1200
- // Only when the last token looks like a task id prefix (ABC-…).
1201
- if (!/^[A-Za-z]{2,6}-?[0-9]*$/.test(token) || token.length < 2) return null;
1202
- return taskCompletions(deps, token, before);
1219
+ const at = raw.lastIndexOf(" ");
1220
+ const before = at === -1 ? "" : raw.slice(0, at + 1);
1221
+ const token = raw.slice(at + 1);
1222
+ // Only a task-id-like tail ("UNI-1", "uni", "UNI-") completes; words that
1223
+ // match no id shape or prefix ("note", "set") yield nothing.
1224
+ if (!/^[A-Za-z]{2,10}-?\d*$/.test(token)) return null;
1225
+ // Title search only when the token looks like a real id prefix (has a digit
1226
+ // or ends with the dash); otherwise id prefixes only.
1227
+ const idLike = /\d/.test(token) || token.endsWith("-");
1228
+ return taskCompletions(deps, token, before, { titleSearch: idLike, rowStyle: "id-title" });
1203
1229
  }
1204
1230
 
1205
1231
  export function registerKanboardCommands(pi: ExtensionAPI, deps: CommandDeps): void {
@@ -1308,10 +1334,15 @@ export function registerKanboardCommands(pi: ExtensionAPI, deps: CommandDeps): v
1308
1334
  });
1309
1335
 
1310
1336
  pi.registerCommand("unipi:kanboard-do", {
1311
- description: "Let the agent use the kanboard for this turn — writes are allowed until the turn ends",
1337
+ description: "Grant kanboard write credits to the agent (/unipi:kanboard-do <request>; off revokes)",
1312
1338
  getArgumentCompletions: (prefix: string) => kanboardDoCompletions(deps, prefix ?? ""),
1313
1339
  handler: async (args: string, ctx: ExtensionCommandContext) => {
1314
1340
  const request = (args ?? "").trim();
1341
+ if (request.toLowerCase() === "off") {
1342
+ deps.guard.revoke();
1343
+ ctx.ui.notify("kanboard: write credits revoked — reads stay free", "info");
1344
+ return;
1345
+ }
1315
1346
  if (!request) {
1316
1347
  ctx.ui.notify("kanboard: -do needs a request — /unipi:kanboard-do <what the agent may do on the board>", "warning");
1317
1348
  return;
@@ -1328,9 +1359,12 @@ export function registerKanboardCommands(pi: ExtensionAPI, deps: CommandDeps): v
1328
1359
  const busy = typeof (ctx as unknown as { isIdle?: () => boolean }).isIdle === "function"
1329
1360
  ? !(ctx as unknown as { isIdle: () => boolean }).isIdle!()
1330
1361
  : false;
1331
- pi.sendUserMessage(doText(slug, client.binary.path, request, deps.settings().queueMax), busy ? { deliverAs: "followUp" } : undefined);
1362
+ pi.sendUserMessage(
1363
+ doText(slug, client.binary.path, request, deps.settings().queueMax, deps.guard.remaining()),
1364
+ busy ? { deliverAs: "followUp" } : undefined,
1365
+ );
1332
1366
  deps.guard.noteSent();
1333
- deps.debug(`do window open (${slug})`);
1367
+ deps.debug(`do credits open (${slug}): ${deps.guard.remaining()} remaining`);
1334
1368
  },
1335
1369
  });
1336
1370
 
@@ -9,8 +9,8 @@
9
9
 
10
10
  import { tokenizeArgs } from "./commands.js";
11
11
 
12
- export const WRITE_BLOCK_REASON =
13
- "kanboard board writes are only allowed during /unipi:kanboard-do or a runner task";
12
+ export const DEFAULT_DO_CREDITS = 10;
13
+ export const WRITE_CREDITS_USED_UP = "kanboard write credits used up — run /unipi:kanboard-do to reload";
14
14
  export const addCapReason = (limit: number): string => `at most ${limit} new tasks per turn`;
15
15
  /** @deprecated tests should read the limit through the guard's getter instead. */
16
16
  export const ADD_CAP = 20;
@@ -80,35 +80,55 @@ export function isReadonly(invocation: KanboardInvocation): boolean {
80
80
  }
81
81
 
82
82
  export interface WriteGuard {
83
- /** Open the window for a `/unipi:kanboard-do` turn. */
83
+ /** Grant / top up the session's write credits to N (kanboard.doCredits). */
84
84
  open(): void;
85
+ /** Revoke remaining credits (/unipi:kanboard-do off). */
86
+ revoke(): void;
87
+ /** Credits left this session. */
88
+ remaining(): number;
85
89
  /** Arm the agent_end closer right after the -do prompt was sent. */
86
90
  noteSent(): void;
87
- /**
88
- * A window opened by -do closes on the first agent_end that isn't the
89
- * pre-send echo (>150ms after noteSent). Returns true when it just closed.
90
- */
91
+ /** Credits persist across turns; this only closes the -do window label. */
91
92
  onAgentEnd(): boolean;
92
93
  /** null when the command is allowed; otherwise the block reason. */
93
94
  check(command: string): string | null;
94
95
  }
95
96
 
96
97
  /**
97
- * The window is open during a -do turn (`doOpen`) or while the runner has a
98
- * task in phase `running`. `runnerTask` returns that task's id (null when the
99
- * runner is not running one) — the `add` counter resets whenever the running
100
- * task changes, so autowork/queue drains get a fresh 20 per task.
98
+ * Writes cost session credits (one per write subcommand in the command line);
99
+ * reads are free and never blocked. A runner task keeps unlimited access
100
+ * (still add-capped). Credits persist across turns and follow-up questions
101
+ * until spent; /unipi:kanboard-do tops up to N without stacking past N.
101
102
  */
102
- export function createWriteGuard(runnerTask: () => string | null, addLimit: () => number = () => ADD_CAP): WriteGuard {
103
+ export function createWriteGuard(
104
+ runnerTask: () => string | null,
105
+ addLimit: () => number = () => ADD_CAP,
106
+ doCredits: () => number = () => DEFAULT_DO_CREDITS,
107
+ ): WriteGuard {
108
+ let credits = 0;
103
109
  let doOpen = false;
104
110
  let sentAt = 0;
105
111
  let adds = 0;
106
112
  let lastTask: string | null = null;
113
+ const countAdd = (invocation: KanboardInvocation): string | null => {
114
+ if (invocation.sub !== "add") return null;
115
+ adds += 1;
116
+ const limit = addLimit();
117
+ return limit > 0 && adds > limit ? addCapReason(limit) : null;
118
+ };
107
119
  return {
108
120
  open() {
109
- doOpen = true;
121
+ credits = Math.max(credits, Math.max(0, doCredits()));
110
122
  adds = 0;
111
123
  lastTask = null;
124
+ doOpen = true;
125
+ },
126
+ revoke() {
127
+ credits = 0;
128
+ doOpen = false;
129
+ },
130
+ remaining() {
131
+ return credits;
112
132
  },
113
133
  noteSent() {
114
134
  sentAt = Date.now();
@@ -130,12 +150,15 @@ export function createWriteGuard(runnerTask: () => string | null, addLimit: () =
130
150
  }
131
151
  for (const invocation of invocations) {
132
152
  if (isReadonly(invocation)) continue;
133
- if (!(doOpen || task !== null)) return WRITE_BLOCK_REASON;
134
- if (invocation.sub === "add") {
135
- adds += 1;
136
- const limit = addLimit();
137
- if (limit > 0 && adds > limit) return addCapReason(limit);
153
+ if (task !== null) {
154
+ const cap = countAdd(invocation);
155
+ if (cap) return cap;
156
+ continue;
138
157
  }
158
+ if (credits <= 0) return WRITE_CREDITS_USED_UP;
159
+ credits -= 1;
160
+ const cap = countAdd(invocation);
161
+ if (cap) return cap;
139
162
  }
140
163
  return null;
141
164
  },