@pi-unipi/unipi 3.0.0-alpha.14 → 3.0.0-alpha.16

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 (61) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/docs/prefix-cache-architecture.md +14 -0
  3. package/package.json +21 -21
  4. package/packages/ask-user/package.json +2 -2
  5. package/packages/autocomplete/package.json +1 -1
  6. package/packages/background-tasks/package.json +2 -2
  7. package/packages/background-tasks/src/index.ts +20 -1
  8. package/packages/btw/package.json +2 -2
  9. package/packages/compactor/package.json +3 -3
  10. package/packages/core/index.ts +4 -0
  11. package/packages/core/kanboard-status.ts +49 -0
  12. package/packages/core/long-horizon-owner-status.ts +46 -0
  13. package/packages/core/package.json +1 -1
  14. package/packages/core/src/evidence.ts +78 -0
  15. package/packages/core/src/turn/arbiter.ts +290 -0
  16. package/packages/footer/package.json +3 -3
  17. package/packages/footer/src/glance-editor.ts +11 -5
  18. package/packages/footer/src/index.ts +3 -1
  19. package/packages/fusion/package.json +3 -3
  20. package/packages/fusion/src/tools.ts +41 -0
  21. package/packages/info-screen/package.json +2 -2
  22. package/packages/input-shortcuts/package.json +2 -2
  23. package/packages/kanboard/README.md +51 -56
  24. package/packages/kanboard/index.ts +264 -73
  25. package/packages/kanboard/package.json +8 -8
  26. package/packages/kanboard/skills/kanboard/SKILL.md +35 -27
  27. package/packages/kanboard/src/badges.ts +93 -0
  28. package/packages/kanboard/src/commands.ts +61 -55
  29. package/packages/kanboard/src/debug.ts +20 -0
  30. package/packages/kanboard/src/guard.ts +233 -166
  31. package/packages/kanboard/src/monitor.ts +328 -0
  32. package/packages/kanboard/src/notice-buffer.ts +74 -0
  33. package/packages/kanboard/src/reminders.ts +17 -91
  34. package/packages/kanboard/src/settings.ts +54 -72
  35. package/packages/long-horizon/index.ts +71 -14
  36. package/packages/long-horizon/package.json +3 -3
  37. package/packages/long-horizon/src/engine/continuation.ts +16 -4
  38. package/packages/long-horizon/src/engine/nudge-stash.ts +52 -0
  39. package/packages/long-horizon/src/engine/ralph.ts +3 -2
  40. package/packages/long-horizon/src/engine/verifier.ts +19 -0
  41. package/packages/long-horizon/src/gate.ts +8 -1
  42. package/packages/long-horizon/src/judge/resolve.ts +1 -0
  43. package/packages/long-horizon/src/owner.ts +35 -2
  44. package/packages/long-horizon/src/runtime.ts +4 -5
  45. package/packages/mcp/package.json +2 -2
  46. package/packages/memory/package.json +3 -3
  47. package/packages/notify/package.json +2 -2
  48. package/packages/skill-registry/package.json +2 -2
  49. package/packages/subagents/package.json +2 -2
  50. package/packages/subagents/src/index.ts +13 -2
  51. package/packages/subagents/src/manager.ts +7 -0
  52. package/packages/unipi/bundled.js +1408 -1198
  53. package/packages/unipi/index.ts +8 -1
  54. package/packages/updater/package.json +2 -2
  55. package/packages/utility/package.json +2 -2
  56. package/packages/utility/src/render/reply-bg.ts +87 -6
  57. package/packages/utility/src/render/simple.ts +79 -3
  58. package/packages/watchdog/package.json +3 -3
  59. package/packages/web-api/package.json +2 -2
  60. package/packages/workflow/package.json +2 -2
  61. package/packages/kanboard/src/runner.ts +0 -1065
package/CHANGELOG.md CHANGED
@@ -6,6 +6,36 @@ 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.16] — 2026-09-30
10
+
11
+ ### Added
12
+ - `core`: **turn arbiter** — one `agent_before_settle` decision point for the whole suite: nudge providers propose at most one continuation per settle (long-horizon owner 100 > kanboard claims 50 > autowork-next 40), wait sources (running bg wake, busy sidekick, background subagent) veto, outcome/pending/already-continuing short-circuit, providers are time-boxed and failure-proof, and everything lives behind a `Symbol.for` holder that is a no-op in child processes. Ships with `decideSettle`/`registerNudgeProvider`/`registerWaitSource`/`onSettleDecision` exports and tests.
13
+ - `core`: **evidence contributors** — `registerEvidenceContributor(name, fn)` + `gatherEvidence()` (each contributor time-boxed, a throw contributes nothing); `core` also gains `long-horizon-owner-status` and `kanboard-status` shared holders (same `Symbol.for` pattern as fusion status) for the footer and cross-module reads.
14
+ - `kanboard`: **monitor nudges** — a priority-50/40 provider that continues this session's own In Progress claims (`↻ UNI-30 still In Progress — continue, or finish/block it (n/5)`) and, in autowork, offers the next ready task; question heuristic (the agent asked something → notice only), stall guard after 2 idle nudged runs, hard caps (5 nudges/task, 3 offers/task), deference to an active long-horizon owner, notices when a goal stops paused/budget with claims open, and disarm on Esc. Notices are user-only custom entries plus a toast — never model context.
15
+ - `kanboard`: **transcript badges + footer `▣`** — successful lead `start`/`finish`/`move … blocked` append user-only entries (`▣ UNI-30 started`, `✓ UNI-30 → In Review`, `⊘ UNI-30 blocked: <comment≤80>`), and the glance title shows `▣ UNI-30`, `▣ UNI-30 +1`, `▣ autowork` or `▣ UNI-30 · autowork` from the shared status holder.
16
+ - `kanboard`: **session identity + resume notice + shutdown release** — the session id is now `pi-<pi session id>` (stable across `-c`/`-r`, children keep the inherited value); startup reaps claims whose pid died and notifies once (`UNI-30 was released when the last run ended — ask me to re-start it`); `session_shutdown` releases this session's open claims to Todo with `released: session ended mid-task (<session>)` (system actor — agents cannot release).
17
+ - `long-horizon`: **goal evidence contributors** — a goal completion claim is gathered against cross-module evidence first; any blocking item (e.g. an open kanboard claim) forces `not_met` with `missing` set, without paying for an evaluator call; non-blocking notes ride the evidence brief.
18
+
19
+ ### Changed
20
+ - `kanboard`: **the session works board tasks itself — the runner is gone.** `/unipi:kanboard-do` now grants a budget: `doTasks` task slots (each `start` uses one) and `doWrites` board writes (add, edit, link, order, move backlog↔todo, note on tasks you don't hold); reads, `finish`, `move <ID> blocked --comment` and `note` on tasks this session started are always free; budgets persist across turns, `-do` tops up without stacking, and the doText states the pre-flight rule (need more tasks than slots → start nothing and ask). Settings `doTasks` (5) / `doWrites` (10) replace `doCredits` (a stored value migrates); `queueMax` is gone.
21
+ - `kanboard`: **autowork is a session mode** — `/unipi:kanboard-autowork start` lifts the budgets and the monitor keeps offering the next ready task in this session (any mode the agent chooses); `stop` turns the offers off without aborting the current turn; the same stall rule and a 3-offers-per-task cap end it with an `autowork done · N in review · M blocked` notice.
22
+ - `kanboard`: **children are read-only** — fusion sidekicks and subagents may read the board but every write is refused with "board writes are the lead's job — report this to the lead"; reminders are silent there too. R2 (turn-end follow-up) is removed — continuation is the monitor's job; R1 (start-before-edit steer) stays.
23
+ - `long-horizon`: **no owners inside children** — the gate resolves mode `none` (source `child`) without consulting the judge, and owner activation is refused with "do the work and report back to the lead"; escape hatch `UNIPI_LH_ALLOW_CHILD=1`. Continuation and ralph messages ride a one-slot nudge stash delivered by the arbiter (kickoff append-only; timer wakes stay direct sends); owner transitions publish to the shared status holder kanboard reads.
24
+ - `background-tasks` / `fusion` / `subagents`: register arbiter wait sources, so a pending wake, a non-blocking sidekick handoff or a running background subagent defers nudges until the event lands.
25
+
26
+ ### Removed
27
+ - `kanboard`: the task **runner** and everything that served it — `queue`/`unqueue`/`claim-next`/`set-run` subcommands, per-task `--strategy`/`--plan` labels (jev routing included), the session queue and its drain, the runner compaction brief, and the `queueMax` setting. The refused subcommands answer "removed: the session works tasks itself (no runner/queue/strategy)"; the Rust binary still accepts them until the phase-2 binary release.
28
+
29
+ ### Fixed
30
+ - `utility` (simple render style): the reply panel has exactly one blank pad row between the `─ summary ─` rule and the reply text (and before the closing rule) — the edge-blank trim had left the `●` text flush under the rule.
31
+
32
+ ## [3.0.0-alpha.15] — 2026-09-30
33
+
34
+ ### Fixed
35
+ - `utility` (simple render style): a hidden-thinking run inside a thinking+text step no longer leaves blank rows — pi wraps the (empty) label in escapes that survive its own blank check, so a step showed up to three stray blank rows above its text; whitespace-only children and the spacer after each are now dropped before painting.
36
+ - `utility` (simple render style): resumed and re-rendered sessions (resume, boundary-compaction re-render) rebuild tool groups from the rendered transcript order — replayed calls never fired group-break events, so every row landed in one flat group and only the last component painted.
37
+ - `utility` (simple render style): compact spacing — pi's leading spacer row on every content-bearing assistant message is trimmed: no stray blank below custom badges (`▌ Memory recall …`), above mid-turn text, and no painted pad row under the `─ summary ─` rule inside the reply panel. Dropped edge rows keep their OSC-133 prompt-zone markers.
38
+
9
39
  ## [3.0.0-alpha.14] — 2026-09-30
10
40
 
11
41
  ### Added
@@ -86,3 +86,17 @@ UniPi's `/unipi:prefix-cache` diagnostic observes this provider-native boundary
86
86
  ## Compaction
87
87
 
88
88
  UniPi currently performs deterministic, zero-LLM compaction. Therefore it does not issue a second summarizer request whose prefix needs warming. UniPi's additional percentage trigger is disabled by default; Pi core's reserve-token safety trigger remains active unless the user changes Pi settings. With Pi 0.84.1 defaults, core compaction triggers above `contextWindow - 16,384` estimated tokens and retains approximately 20,000 recent tokens. Enabling UniPi's optional 80% trigger intentionally trades earlier epoch resets for more headroom, with cooldown and minimum-growth guards against repeat compaction. If LLM summarization is introduced, the summarization call must replay the current conversation's exact system prompt, ordered tools, and leading messages, then append the compaction instruction as the final user message. Replacing the system prompt with a special summarizer prompt would throw away the warm prefix at the most expensive point in the session.
89
+
90
+ ## Continuation nudges (turn arbiter)
91
+
92
+ Long-horizon and kanboard continuations ride **tail `custom_message` entries**
93
+ delivered by core's turn arbiter at `agent_before_settle`
94
+ (`packages/core/src/turn/arbiter.ts`): the previous request's prefix — system
95
+ prompt, tool definitions, prior messages — is untouched, and the nudge is
96
+ appended as one new tail entry before the continuation request. The nudge text
97
+ is **deterministic** for a given state (claim id, nudge counters, mode), so a
98
+ re-offer of the same nudge reproduces the same tail. Nudges never change the
99
+ system prompt and never reorder tools; providers that were mid-flight when the
100
+ nudge was proposed keep their exact surface. The long-horizon kickoff/continuation
101
+ messages live in a one-slot stash (overwritten, kickoff append-only) so at most
102
+ one deterministic tail message is added per settle.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/unipi",
3
- "version": "3.0.0-alpha.14",
3
+ "version": "3.0.0-alpha.16",
4
4
  "description": "All-in-one extension suite for Pi coding agent",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -82,26 +82,26 @@
82
82
  "typebox": "^1.1.38"
83
83
  },
84
84
  "dependencies": {
85
- "@pi-unipi/ask-user": "3.0.0-alpha.14",
86
- "@pi-unipi/background-tasks": "3.0.0-alpha.14",
87
- "@pi-unipi/btw": "3.0.0-alpha.14",
88
- "@pi-unipi/command-enchantment": "3.0.0-alpha.14",
89
- "@pi-unipi/compactor": "3.0.0-alpha.14",
90
- "@pi-unipi/core": "3.0.0-alpha.14",
91
- "@pi-unipi/footer": "3.0.0-alpha.14",
92
- "@pi-unipi/fusion": "3.0.0-alpha.14",
93
- "@pi-unipi/info-screen": "3.0.0-alpha.14",
94
- "@pi-unipi/input-shortcuts": "3.0.0-alpha.14",
95
- "@pi-unipi/kanboard": "3.0.0-alpha.14",
96
- "@pi-unipi/mcp": "3.0.0-alpha.14",
97
- "@pi-unipi/memory": "3.0.0-alpha.14",
98
- "@pi-unipi/notify": "3.0.0-alpha.14",
99
- "@pi-unipi/skill-registry": "3.0.0-alpha.14",
100
- "@pi-unipi/subagents": "3.0.0-alpha.14",
101
- "@pi-unipi/updater": "3.0.0-alpha.14",
102
- "@pi-unipi/utility": "3.0.0-alpha.14",
103
- "@pi-unipi/web-api": "3.0.0-alpha.14",
104
- "@pi-unipi/workflow": "3.0.0-alpha.14"
85
+ "@pi-unipi/ask-user": "3.0.0-alpha.16",
86
+ "@pi-unipi/background-tasks": "3.0.0-alpha.16",
87
+ "@pi-unipi/btw": "3.0.0-alpha.16",
88
+ "@pi-unipi/command-enchantment": "3.0.0-alpha.16",
89
+ "@pi-unipi/compactor": "3.0.0-alpha.16",
90
+ "@pi-unipi/core": "3.0.0-alpha.16",
91
+ "@pi-unipi/footer": "3.0.0-alpha.16",
92
+ "@pi-unipi/fusion": "3.0.0-alpha.16",
93
+ "@pi-unipi/info-screen": "3.0.0-alpha.16",
94
+ "@pi-unipi/input-shortcuts": "3.0.0-alpha.16",
95
+ "@pi-unipi/kanboard": "3.0.0-alpha.16",
96
+ "@pi-unipi/mcp": "3.0.0-alpha.16",
97
+ "@pi-unipi/memory": "3.0.0-alpha.16",
98
+ "@pi-unipi/notify": "3.0.0-alpha.16",
99
+ "@pi-unipi/skill-registry": "3.0.0-alpha.16",
100
+ "@pi-unipi/subagents": "3.0.0-alpha.16",
101
+ "@pi-unipi/updater": "3.0.0-alpha.16",
102
+ "@pi-unipi/utility": "3.0.0-alpha.16",
103
+ "@pi-unipi/web-api": "3.0.0-alpha.16",
104
+ "@pi-unipi/workflow": "3.0.0-alpha.16"
105
105
  },
106
106
  "devDependencies": {
107
107
  "@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.14",
3
+ "version": "3.0.0-alpha.16",
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.14"
43
+ "@pi-unipi/core": "3.0.0-alpha.16"
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.14",
3
+ "version": "3.0.0-alpha.16",
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",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/background-tasks",
3
- "version": "3.0.0-alpha.14",
3
+ "version": "3.0.0-alpha.16",
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.14",
13
+ "@pi-unipi/core": "3.0.0-alpha.16",
14
14
  "turndown": "^7.2.4"
15
15
  },
16
16
  "devDependencies": {
@@ -10,7 +10,7 @@
10
10
  */
11
11
 
12
12
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
13
- import { createSpinnerLine, setHerdrWorking } from "@pi-unipi/core";
13
+ import { createSpinnerLine, isChildProcess, registerWaitSource, setHerdrWorking } from "@pi-unipi/core";
14
14
  import { loadBackgroundTasksConfig } from "./config.js";
15
15
  import { BackgroundTaskRegistry } from "./registry.js";
16
16
  import {
@@ -43,6 +43,15 @@ function pendingWakeText(registry: BackgroundTaskRegistry, isIdle: () => boolean
43
43
  return `waiting on ${count}${detail} — agent resumes automatically when done`;
44
44
  }
45
45
 
46
+ /** Arbiter wait-source reason (lead only): what a running wake-task is, or null. */
47
+ export function pendingWakeReason(tasks: readonly { status: string; triggerOnCompletion: boolean; command?: string; name?: string; description?: string; id?: string }[]): string | null {
48
+ const pendingWake = tasks.filter((task) => task.status === "running" && task.triggerOnCompletion);
49
+ if (pendingWake.length === 0) return null;
50
+ const first = pendingWake[0];
51
+ const more = pendingWake.length > 1 ? ` +${String(pendingWake.length - 1)} more` : "";
52
+ return `bg: ${first === undefined ? "task" : taskDisplayName(first)}${more}`;
53
+ }
54
+
46
55
  export default function backgroundTasksExtension(pi: ExtensionAPI): void {
47
56
  const { config, warnings } = loadBackgroundTasksConfig(process.cwd());
48
57
 
@@ -76,6 +85,16 @@ export default function backgroundTasksExtension(pi: ExtensionAPI): void {
76
85
  });
77
86
  setSharedTaskRegistry(registry);
78
87
 
88
+ // Turn-arbiter wait source (lead only): while a task that will wake the
89
+ // agent is still running, a nudge would race the wake — defer to it.
90
+ if (!isChildProcess()) {
91
+ try {
92
+ registerWaitSource("background-tasks", () => pendingWakeReason(registry.allTasks()));
93
+ } catch {
94
+ // Registration must never block module load.
95
+ }
96
+ }
97
+
79
98
  const eventService: BackgroundTaskExtensionService = installBackgroundTaskExtensionApi({
80
99
  events: pi.events,
81
100
  registry,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/btw",
3
- "version": "3.0.0-alpha.14",
3
+ "version": "3.0.0-alpha.16",
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.14"
40
+ "@pi-unipi/core": "3.0.0-alpha.16"
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.14",
3
+ "version": "3.0.0-alpha.16",
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.14",
37
- "@pi-unipi/info-screen": "3.0.0-alpha.14",
36
+ "@pi-unipi/core": "3.0.0-alpha.16",
37
+ "@pi-unipi/info-screen": "3.0.0-alpha.16",
38
38
  "@earendil-works/pi-agent-core": "^0.87.1"
39
39
  },
40
40
  "peerDependencies": {
@@ -14,8 +14,12 @@ export * from "./bounded-output.js";
14
14
  export * from "./spinner-line.js";
15
15
  export * from "./fusion-status.js";
16
16
  export * from "./long-horizon-status.js";
17
+ export * from "./long-horizon-owner-status.js";
18
+ export * from "./kanboard-status.js";
17
19
  export * from "./plan-permission-status.js";
18
20
  export * from "./command-echo.js";
21
+ export * from "./src/turn/arbiter.js";
22
+ export * from "./src/evidence.js";
19
23
 
20
24
  // v3 workspace identity + state layout (marker-file id, per-workspace roots)
21
25
  export * from "./src/workspace/identity.js";
@@ -0,0 +1,49 @@
1
+ /**
2
+ * @pi-unipi/core — shared kanboard status
3
+ *
4
+ * The kanboard package owns the session's board claims and autowork flag;
5
+ * the footer package owns the glance title segment that should display them
6
+ * (`▣ UNI-30`, `▣ UNI-30 +1`, `▣ autowork`). Same Symbol.for global pattern
7
+ * as fusion-status: published by kanboard after board changes and at each
8
+ * settle, pulled by the footer every render.
9
+ */
10
+
11
+ export interface SharedKanboardStatus {
12
+ /** Ids this session holds In Progress (board order). */
13
+ claims: string[];
14
+ /** Autowork is on: the arbiter keeps offering the next ready task. */
15
+ autowork: boolean;
16
+ }
17
+
18
+ const KEY = Symbol.for("unipi.kanboard.status");
19
+
20
+ type Holder = { status?: SharedKanboardStatus | undefined };
21
+
22
+ function holder(): Holder {
23
+ const g = globalThis as { [KEY]?: Holder };
24
+ g[KEY] ??= {};
25
+ return g[KEY] as Holder;
26
+ }
27
+
28
+ /** Publish the current claims/autowork snapshot (undefined clears it). */
29
+ export function setSharedKanboardStatus(status: SharedKanboardStatus | undefined): void {
30
+ holder().status = status;
31
+ }
32
+
33
+ /** Read the current snapshot, or undefined when kanboard never published. */
34
+ export function getSharedKanboardStatus(): SharedKanboardStatus | undefined {
35
+ return holder().status;
36
+ }
37
+
38
+ /** Glance label for a snapshot: `▣ UNI-30`, `▣ UNI-30 +1`, `▣ autowork`,
39
+ * `▣ UNI-30 · autowork`. Null when there is nothing to show. */
40
+ export function kanboardGlanceLabel(status: SharedKanboardStatus | null | undefined): string | null {
41
+ if (!status) return null;
42
+ const { claims, autowork } = status;
43
+ if (claims.length === 0 && !autowork) return null;
44
+ const claimPart =
45
+ claims.length === 0 ? null : claims.length === 1 ? claims[0]! : `${claims[0]} +${String(claims.length - 1)}`;
46
+ const autoworkPart = autowork ? "autowork" : null;
47
+ const body = [claimPart, autoworkPart].filter(Boolean).join(" · ");
48
+ return `▣ ${body}`;
49
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * @pi-unipi/core — shared long-horizon owner status
3
+ *
4
+ * The long-horizon package owns the automation owner lifecycle; kanboard's
5
+ * monitor (step 4) needs to defer to an active owner and to notice one that
6
+ * STOPPED this run ("goal paused — UNI-30 left In Progress"). Same
7
+ * Symbol.for global pattern as long-horizon-status: published on every owner
8
+ * transition, pulled by readers whenever they settle.
9
+ */
10
+
11
+ export interface SharedOwnerStatus {
12
+ /** Current owner, when one exists (active, or parked in the single slot). */
13
+ owner?: { kind: string; status: "active" | "parked" };
14
+ /** Last owner stop. Kanboard compares `at` against its turn-start marker. */
15
+ lastStop?: { kind: "complete" | "paused" | "budget" | "other"; at: number };
16
+ }
17
+
18
+ const KEY = Symbol.for("unipi.longHorizon.ownerStatus");
19
+
20
+ type Holder = { status?: SharedOwnerStatus | undefined };
21
+
22
+ function holder(): Holder {
23
+ const g = globalThis as { [KEY]?: Holder };
24
+ g[KEY] ??= {};
25
+ return g[KEY] as Holder;
26
+ }
27
+
28
+ /** Publish the current owner (undefined clears it — no owner active or parked). */
29
+ export function setSharedOwner(owner: SharedOwnerStatus["owner"]): void {
30
+ const h = holder();
31
+ h.status ??= {};
32
+ if (owner === undefined) delete h.status.owner;
33
+ else h.status.owner = owner;
34
+ }
35
+
36
+ /** Record that an owner stopped, with the mapped terminal kind. */
37
+ export function markSharedOwnerStopped(kind: "complete" | "paused" | "budget" | "other"): void {
38
+ const h = holder();
39
+ h.status ??= {};
40
+ h.status.lastStop = { kind, at: Date.now() };
41
+ }
42
+
43
+ /** Read the published status, or undefined before the first transition. */
44
+ export function getSharedOwnerStatus(): SharedOwnerStatus | undefined {
45
+ return holder().status;
46
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/core",
3
- "version": "3.0.0-alpha.14",
3
+ "version": "3.0.0-alpha.16",
4
4
  "description": "Shared utilities, event types, and constants for Unipi extension suite",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Evidence contributors — how other modules hand the long-horizon verifier
3
+ * hard signals about work outside the transcript.
4
+ *
5
+ * Kanboard (step 4) registers one so a goal cannot complete while this
6
+ * session still holds an In Progress claim. Contributors are time-boxed and
7
+ * a throw counts as no contribution — evidence must never break settlement.
8
+ */
9
+
10
+ export interface EvidenceContribution {
11
+ blocking: string[];
12
+ notes: string[];
13
+ }
14
+
15
+ export type EvidenceContributor = () => Promise<EvidenceContribution>;
16
+
17
+ const KEY = Symbol.for("unipi.evidence.contributors");
18
+
19
+ interface Holder {
20
+ contributors: Map<string, EvidenceContributor>;
21
+ }
22
+
23
+ function holder(): Holder {
24
+ const g = globalThis as { [KEY]?: Holder };
25
+ g[KEY] ??= { contributors: new Map() };
26
+ return g[KEY] as Holder;
27
+ }
28
+
29
+ /** Register a named contributor; re-registering a name replaces it. */
30
+ export function registerEvidenceContributor(
31
+ name: string,
32
+ contributor: () => Promise<EvidenceContribution>,
33
+ ): () => void {
34
+ const h = holder();
35
+ h.contributors.set(name, contributor);
36
+ return () => {
37
+ const current = h.contributors.get(name);
38
+ if (current === contributor) h.contributors.delete(name);
39
+ };
40
+ }
41
+
42
+ /** Test/holder introspection hook. */
43
+ export function resetEvidenceForTests(): void {
44
+ holder().contributors.clear();
45
+ }
46
+
47
+ /**
48
+ * Gather every contribution, each time-boxed; a throw or timeout contributes
49
+ * nothing. `blocking` items force a `not_met` verdict downstream; `notes`
50
+ * ride the evidence brief as context.
51
+ */
52
+ export async function gatherEvidence(timeoutMs = 2_000): Promise<EvidenceContribution> {
53
+ const contributors = [...holder().contributors.values()];
54
+ if (contributors.length === 0) return { blocking: [], notes: [] };
55
+ const blocking: string[] = [];
56
+ const notes: string[] = [];
57
+ await Promise.all(
58
+ contributors.map(async (contributor) => {
59
+ let timer: ReturnType<typeof setTimeout> | undefined;
60
+ try {
61
+ const contribution = await Promise.race([
62
+ Promise.resolve(contributor()),
63
+ new Promise<null>((resolve) => {
64
+ timer = setTimeout(() => resolve(null), timeoutMs);
65
+ }),
66
+ ]);
67
+ if (contribution === null) return;
68
+ blocking.push(...contribution.blocking);
69
+ notes.push(...contribution.notes);
70
+ } catch {
71
+ // A failed contributor is no contribution.
72
+ } finally {
73
+ if (timer !== undefined) clearTimeout(timer);
74
+ }
75
+ }),
76
+ );
77
+ return { blocking, notes };
78
+ }