@pi-unipi/unipi 3.0.0-alpha.2 → 3.0.0-alpha.21
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.
- package/CHANGELOG.md +292 -0
- package/README.md +8 -11
- package/docs/prefix-cache-architecture.md +14 -0
- package/package.json +22 -24
- package/packages/ask-user/README.md +55 -107
- package/packages/ask-user/ask-ui.ts +306 -732
- package/packages/ask-user/clipboard.ts +50 -0
- package/packages/ask-user/config.ts +74 -78
- package/packages/ask-user/package.json +2 -2
- package/packages/ask-user/questions.ts +151 -0
- package/packages/ask-user/skills/ask-user/SKILL.md +59 -133
- package/packages/ask-user/tools.ts +198 -432
- package/packages/autocomplete/package.json +1 -1
- package/packages/autocomplete/src/constants.ts +38 -43
- package/packages/autocomplete/src/sorting.ts +8 -0
- package/packages/background-tasks/package.json +2 -2
- package/packages/background-tasks/src/config.ts +0 -73
- package/packages/background-tasks/src/index.ts +20 -16
- package/packages/background-tasks/src/registry.ts +1 -171
- package/packages/background-tasks/src/types.ts +0 -32
- package/packages/btw/README.md +13 -56
- package/packages/btw/extensions/btw.ts +403 -1796
- package/packages/btw/package.json +5 -2
- package/packages/btw/skills/btw/SKILL.md +7 -143
- package/packages/compactor/README.md +56 -287
- package/packages/compactor/package.json +5 -9
- package/packages/compactor/skills/compactor/SKILL.md +13 -20
- package/packages/compactor/src/card.ts +90 -0
- package/packages/compactor/src/commands/index.ts +73 -130
- package/packages/compactor/src/compaction/compact-args.ts +5 -3
- package/packages/compactor/src/compaction/filter-noise.ts +1 -1
- package/packages/compactor/src/compaction/hooks.ts +487 -311
- package/packages/compactor/src/compaction/source.ts +178 -0
- package/packages/compactor/src/compaction/summarize.ts +586 -52
- package/packages/compactor/src/config/manager.ts +130 -127
- package/packages/compactor/src/config/schema.ts +32 -48
- package/packages/compactor/src/index.ts +40 -431
- package/packages/compactor/src/info-screen.ts +44 -190
- package/packages/compactor/src/session/recall-blocks.ts +1 -1
- package/packages/compactor/src/stats.ts +76 -0
- package/packages/compactor/src/tools/context-budget.ts +17 -34
- package/packages/compactor/src/tools/ctx-doctor.ts +45 -36
- package/packages/compactor/src/tools/register.ts +29 -269
- package/packages/compactor/src/types.ts +47 -186
- package/packages/core/README.md +25 -0
- package/packages/core/child-agent.ts +1 -0
- package/packages/core/command-echo.ts +63 -0
- package/packages/core/compaction-context.ts +41 -0
- package/packages/core/constants.ts +8 -38
- package/packages/core/events.ts +0 -53
- package/packages/core/fusion-status.ts +4 -0
- package/packages/core/global.d.ts +10 -0
- package/packages/core/harness-messages.ts +392 -0
- package/packages/core/index.ts +14 -0
- package/packages/core/kanboard-status.ts +49 -0
- package/packages/core/long-horizon-owner-status.ts +46 -0
- package/packages/core/model-cache.ts +81 -7
- package/packages/core/package.json +6 -3
- package/packages/core/src/attach/detect.ts +163 -0
- package/packages/core/src/child-agent/child-args.ts +31 -0
- package/packages/core/src/child-agent/delivery.ts +41 -0
- package/packages/core/src/child-agent/index.ts +5 -0
- package/packages/core/src/child-agent/readers.ts +203 -0
- package/packages/core/src/child-agent/runtime.ts +584 -0
- package/packages/core/src/evidence.ts +78 -0
- package/packages/core/src/hints/browser.ts +278 -0
- package/packages/core/src/hints/crab-data.ts +19 -0
- package/packages/core/src/hints/index.ts +865 -0
- package/packages/core/src/hints/lines.ts +607 -0
- package/packages/core/src/hints/store.ts +120 -0
- package/packages/core/src/jev/chatter.ts +24 -0
- package/packages/core/src/jev/settings.ts +177 -14
- package/packages/core/src/package-colors.ts +2 -0
- package/packages/core/src/settings/catalog.ts +39 -15
- package/packages/core/src/settings/engine.ts +48 -2
- package/packages/core/src/settings/hub.ts +83 -20
- package/packages/core/src/settings/open.ts +49 -0
- package/packages/core/src/settings/schema.ts +27 -7
- package/packages/core/src/tui/hub-kit.ts +18 -5
- package/packages/core/src/tui/kit.ts +198 -0
- package/packages/core/src/tui/progress.ts +73 -0
- package/packages/core/src/turn/arbiter.ts +290 -0
- package/packages/footer/README.md +4 -6
- package/packages/footer/package.json +3 -3
- package/packages/footer/src/glance-editor.ts +19 -11
- package/packages/footer/src/help.ts +1 -1
- package/packages/footer/src/index.ts +36 -3
- package/packages/footer/src/presets.ts +3 -4
- package/packages/footer/src/rendering/icons.ts +0 -20
- package/packages/footer/src/segments/compactor.ts +52 -142
- package/packages/footer/src/segments/status-ext.ts +0 -1
- package/packages/fusion/README.md +14 -4
- package/packages/fusion/package.json +4 -3
- package/packages/fusion/src/child-args.ts +1 -0
- package/packages/fusion/src/index.ts +161 -8
- package/packages/fusion/src/pi-settings.ts +57 -0
- package/packages/fusion/src/picker.ts +186 -51
- package/packages/fusion/src/preset.ts +56 -2
- package/packages/fusion/src/prompts.ts +1 -1
- package/packages/fusion/src/sidekick-runtime.ts +1 -420
- package/packages/fusion/src/tools.ts +107 -128
- package/packages/fusion/src/transcript.ts +27 -85
- package/packages/info-screen/package.json +2 -2
- package/packages/input-shortcuts/README.md +18 -30
- package/packages/input-shortcuts/package.json +2 -2
- package/packages/input-shortcuts/src/burst.ts +88 -0
- package/packages/input-shortcuts/src/chord-overlay.ts +20 -70
- package/packages/input-shortcuts/src/cleared-input.ts +97 -0
- package/packages/input-shortcuts/src/index.ts +99 -193
- package/packages/input-shortcuts/src/last-response.ts +40 -0
- package/packages/input-shortcuts/src/registers.ts +8 -23
- package/packages/input-shortcuts/src/types.ts +4 -16
- package/packages/input-shortcuts/src/undo-redo.ts +59 -47
- package/packages/kanboard/README.md +51 -47
- package/packages/kanboard/index.ts +293 -55
- package/packages/kanboard/package.json +8 -8
- package/packages/kanboard/skills/kanboard/SKILL.md +61 -27
- package/packages/kanboard/src/badges.ts +93 -0
- package/packages/kanboard/src/bin.ts +4 -2
- package/packages/kanboard/src/commands.ts +218 -84
- package/packages/kanboard/src/debug.ts +20 -0
- package/packages/kanboard/src/guard.ts +267 -109
- package/packages/kanboard/src/monitor.ts +330 -0
- package/packages/kanboard/src/notice-buffer.ts +74 -0
- package/packages/kanboard/src/progress.ts +27 -0
- package/packages/kanboard/src/reminders.ts +234 -0
- package/packages/kanboard/src/settings.ts +69 -15
- package/packages/kanboard/src/shapes.ts +5 -0
- package/packages/long-horizon/index.ts +190 -13
- package/packages/long-horizon/package.json +3 -3
- package/packages/long-horizon/src/commands.ts +103 -7
- package/packages/long-horizon/src/compaction-brief.ts +35 -0
- package/packages/long-horizon/src/engine/continuation.ts +84 -15
- package/packages/long-horizon/src/engine/goal-state.ts +64 -13
- package/packages/long-horizon/src/engine/nudge-stash.ts +71 -0
- package/packages/long-horizon/src/engine/ralph.ts +3 -2
- package/packages/long-horizon/src/engine/runaway.ts +68 -11
- package/packages/long-horizon/src/engine/verifier.ts +19 -0
- package/packages/long-horizon/src/gate.ts +37 -12
- package/packages/long-horizon/src/judge/resolve.ts +1 -0
- package/packages/long-horizon/src/owner.ts +35 -2
- package/packages/long-horizon/src/progress.ts +87 -0
- package/packages/long-horizon/src/prompts/goal.ts +1 -1
- package/packages/long-horizon/src/runtime.ts +172 -18
- package/packages/long-horizon/src/settings.ts +54 -39
- package/packages/long-horizon/src/tools/goal.ts +6 -0
- package/packages/long-horizon/src/tools/graph.ts +1 -1
- package/packages/long-horizon/src/tools/swarm.ts +3 -3
- package/packages/mcp/package.json +2 -2
- package/packages/memory/commands.ts +7 -2
- package/packages/memory/index.ts +210 -32
- package/packages/memory/package.json +3 -3
- package/packages/memory/save-session.ts +264 -0
- package/packages/memory/settings.ts +17 -0
- package/packages/memory/tools.ts +253 -203
- package/packages/notify/package.json +2 -2
- package/packages/skill-registry/README.md +53 -16
- package/packages/skill-registry/index.ts +230 -0
- package/packages/skill-registry/package.json +19 -8
- package/packages/skill-registry/src/editor.ts +206 -0
- package/packages/skill-registry/src/judge.ts +289 -0
- package/packages/skill-registry/src/registry.ts +87 -0
- package/packages/skill-registry/src/settings.ts +259 -0
- package/packages/skill-registry/src/vault.ts +80 -0
- package/packages/subagents/README.md +39 -119
- package/packages/subagents/package.json +6 -13
- package/packages/subagents/skills/subagents/SKILL.md +18 -42
- package/packages/subagents/src/agents.ts +213 -0
- package/packages/subagents/src/cards.ts +182 -0
- package/packages/subagents/src/index.ts +609 -1084
- package/packages/subagents/src/manager.ts +429 -0
- package/packages/subagents/src/profiles.ts +130 -0
- package/packages/subagents/src/transcript.ts +154 -0
- package/packages/subagents/src/ui.ts +389 -0
- package/packages/unipi/bundled.js +39756 -45569
- package/packages/unipi/index.ts +55 -10
- package/packages/updater/package.json +2 -2
- package/packages/utility/README.md +76 -69
- package/packages/utility/package.json +2 -2
- package/packages/utility/skills/image/SKILL.md +37 -0
- package/packages/utility/skills/utility/SKILL.md +17 -60
- package/packages/utility/src/answer/extract.ts +128 -0
- package/packages/utility/src/answer/index.ts +213 -0
- package/packages/utility/src/answer/reply.ts +133 -0
- package/packages/utility/src/answer/web.ts +167 -0
- package/packages/utility/src/attach/index.ts +205 -0
- package/packages/utility/src/commands.ts +38 -142
- package/packages/utility/src/diagnostics/engine.ts +50 -5
- package/packages/utility/src/image/index.ts +33 -0
- package/packages/utility/src/image/models.ts +221 -0
- package/packages/{image/src → utility/src/image}/recognize.ts +2 -2
- package/packages/utility/src/image/settings.ts +178 -0
- package/packages/{image/src/image-source.ts → utility/src/image/source.ts} +1 -1
- package/packages/utility/src/image/tools.ts +168 -0
- package/packages/utility/src/image/transports.ts +186 -0
- package/packages/utility/src/index.ts +68 -389
- package/packages/utility/src/lifecycle/cleanup.ts +116 -311
- package/packages/utility/src/rename/gate.ts +102 -0
- package/packages/utility/src/rename/index.ts +191 -0
- package/packages/utility/src/rename/session.ts +170 -0
- package/packages/utility/src/render/delegated.ts +285 -0
- package/packages/utility/src/render/harness.ts +683 -0
- package/packages/utility/src/render/parse.ts +156 -0
- package/packages/utility/src/render/pi-settings.ts +38 -0
- package/packages/utility/src/render/reply-bg.ts +307 -0
- package/packages/utility/src/render/simple.ts +724 -0
- package/packages/utility/src/render/spacing.ts +235 -0
- package/packages/utility/src/render/styled.ts +251 -0
- package/packages/utility/src/render/tools.ts +353 -0
- package/packages/utility/src/settings.ts +166 -233
- package/packages/utility/src/types.ts +1 -222
- package/packages/watchdog/index.ts +17 -6
- package/packages/watchdog/package.json +3 -3
- package/packages/watchdog/src/config.ts +3 -2
- package/packages/web-api/package.json +2 -2
- package/packages/workflow/README.md +3 -0
- package/packages/workflow/index.ts +0 -2
- package/packages/workflow/package.json +2 -2
- package/packages/workflow/src/permission/decide.ts +6 -4
- package/packages/workflow/src/permission/index.ts +17 -7
- package/packages/workflow/src/permission/settings.ts +39 -4
- package/packages/workflow/src/plan/index.ts +19 -4
- package/packages/background-tasks/extensions/delegate-child.ts +0 -1
- package/packages/background-tasks/src/context-parent-snapshot.ts +0 -142
- package/packages/background-tasks/src/context-token-budget.ts +0 -903
- package/packages/background-tasks/src/context-visible-conversation-v2.ts +0 -551
- package/packages/background-tasks/src/delegate/artifacts.ts +0 -487
- package/packages/background-tasks/src/delegate/budget.ts +0 -415
- package/packages/background-tasks/src/delegate/hook-contract-evidence.json +0 -18
- package/packages/background-tasks/src/delegate/hook-contract.ts +0 -154
- package/packages/background-tasks/src/delegate/launch.ts +0 -484
- package/packages/background-tasks/src/delegate/result-package.ts +0 -459
- package/packages/background-tasks/src/delegate/runner.ts +0 -430
- package/packages/background-tasks/src/delegate/seed.ts +0 -423
- package/packages/background-tasks/src/delegate/types.ts +0 -323
- package/packages/background-tasks/src/delegate-child-extension.ts +0 -978
- package/packages/background-tasks/src/delegate-extension.ts +0 -654
- package/packages/background-tasks/src/fixtures/delegate-context-incident.json +0 -17
- package/packages/compactor/skills/compactor-detail/SKILL.md +0 -116
- package/packages/compactor/skills/compactor-doctor/SKILL.md +0 -71
- package/packages/compactor/skills/compactor-stats/SKILL.md +0 -51
- package/packages/compactor/src/compaction/build-sections.ts +0 -76
- package/packages/compactor/src/compaction/extract/goals.ts +0 -36
- package/packages/compactor/src/compaction/extract/preferences.ts +0 -40
- package/packages/compactor/src/compaction/format.ts +0 -53
- package/packages/compactor/src/compaction/merge.ts +0 -143
- package/packages/compactor/src/config/presets.ts +0 -160
- package/packages/compactor/src/executor/executor.ts +0 -233
- package/packages/compactor/src/executor/runtime.ts +0 -115
- package/packages/compactor/src/security/evaluator.ts +0 -89
- package/packages/compactor/src/security/policy.ts +0 -96
- package/packages/compactor/src/security/scanner.ts +0 -65
- package/packages/compactor/src/session/auto-inject.ts +0 -60
- package/packages/compactor/src/session/db.ts +0 -325
- package/packages/compactor/src/session/extract.ts +0 -144
- package/packages/compactor/src/session/resume-inject.ts +0 -64
- package/packages/compactor/src/session/snapshot.ts +0 -327
- package/packages/compactor/src/tools/ctx-batch-execute.ts +0 -41
- package/packages/compactor/src/tools/ctx-execute-file.ts +0 -23
- package/packages/compactor/src/tools/ctx-execute.ts +0 -23
- package/packages/compactor/src/tools/ctx-stats.ts +0 -96
- package/packages/image/README.md +0 -99
- package/packages/image/package.json +0 -61
- package/packages/image/skills/image/SKILL.md +0 -76
- package/packages/image/src/generate.ts +0 -256
- package/packages/image/src/index.ts +0 -135
- package/packages/image/src/models.ts +0 -519
- package/packages/image/src/openai-images-api.ts +0 -282
- package/packages/image/src/register-providers.ts +0 -209
- package/packages/image/src/settings.ts +0 -173
- package/packages/image/src/tools.ts +0 -326
- package/packages/input-shortcuts/src/clipboard.ts +0 -119
- package/packages/kanboard/src/runner.ts +0 -817
- package/packages/skill-registry/skills/auto/SKILL.md +0 -465
- package/packages/skill-registry/skills/brainstorm/SKILL.md +0 -219
- package/packages/skill-registry/skills/chore-create/SKILL.md +0 -315
- package/packages/skill-registry/skills/chore-execute/SKILL.md +0 -314
- package/packages/skill-registry/skills/consolidate/SKILL.md +0 -142
- package/packages/skill-registry/skills/consultant/SKILL.md +0 -97
- package/packages/skill-registry/skills/debug/SKILL.md +0 -228
- package/packages/skill-registry/skills/document/SKILL.md +0 -120
- package/packages/skill-registry/skills/fix/SKILL.md +0 -212
- package/packages/skill-registry/skills/gather-context/SKILL.md +0 -145
- package/packages/skill-registry/skills/plan/SKILL.md +0 -231
- package/packages/skill-registry/skills/quick-fix/SKILL.md +0 -135
- package/packages/skill-registry/skills/quick-work/SKILL.md +0 -110
- package/packages/skill-registry/skills/research/SKILL.md +0 -251
- package/packages/skill-registry/skills/review-work/SKILL.md +0 -187
- package/packages/skill-registry/skills/scan-issues/SKILL.md +0 -195
- package/packages/skill-registry/skills/work/SKILL.md +0 -218
- package/packages/skill-registry/skills/worktree-create/SKILL.md +0 -69
- package/packages/skill-registry/skills/worktree-list/SKILL.md +0 -78
- package/packages/skill-registry/skills/worktree-merge/SKILL.md +0 -89
- package/packages/subagents/agents/delegate.md +0 -14
- package/packages/subagents/agents/oracle.md +0 -78
- package/packages/subagents/agents/researcher.md +0 -52
- package/packages/subagents/agents/reviewer.md +0 -80
- package/packages/subagents/agents/scout.md +0 -50
- package/packages/subagents/agents/worker.md +0 -59
- package/packages/subagents/prompts/council.md +0 -48
- package/packages/subagents/prompts/gather-context-and-clarify.md +0 -13
- package/packages/subagents/prompts/parallel-cleanup.md +0 -59
- package/packages/subagents/prompts/parallel-research.md +0 -50
- package/packages/subagents/prompts/parallel-review.md +0 -54
- package/packages/subagents/prompts/review-loop.md +0 -43
- package/packages/subagents/skills/council-mode/SKILL.md +0 -230
- package/packages/subagents/skills/subagents/references/constraints-and-recipes.md +0 -259
- package/packages/subagents/skills/subagents/references/execution-controls.md +0 -454
- package/packages/subagents/skills/subagents/references/management-authoring-rpc.md +0 -161
- package/packages/subagents/skills/subagents/references/multi-lane-orchestration.md +0 -39
- package/packages/subagents/skills/subagents/references/prompting-and-roles.md +0 -267
- package/packages/subagents/src/acceptance.ts +0 -331
- package/packages/subagents/src/agent-manager.ts +0 -405
- package/packages/subagents/src/agent-memory.ts +0 -300
- package/packages/subagents/src/agent-overrides.ts +0 -183
- package/packages/subagents/src/agent-runner.ts +0 -271
- package/packages/subagents/src/async-runner.ts +0 -422
- package/packages/subagents/src/authority-policy.ts +0 -47
- package/packages/subagents/src/budgets.ts +0 -285
- package/packages/subagents/src/child-safety.ts +0 -140
- package/packages/subagents/src/config.ts +0 -249
- package/packages/subagents/src/conversation-viewer.ts +0 -281
- package/packages/subagents/src/core-compat.ts +0 -94
- package/packages/subagents/src/custom-agents.ts +0 -343
- package/packages/subagents/src/file-system-retry.ts +0 -50
- package/packages/subagents/src/fleet-data.ts +0 -105
- package/packages/subagents/src/fleet-view.ts +0 -322
- package/packages/subagents/src/foreground-detach.ts +0 -59
- package/packages/subagents/src/fork-context.ts +0 -285
- package/packages/subagents/src/global.d.ts +0 -7
- package/packages/subagents/src/guide.ts +0 -129
- package/packages/subagents/src/mission-state.ts +0 -133
- package/packages/subagents/src/mission-store.ts +0 -436
- package/packages/subagents/src/output-limits.ts +0 -142
- package/packages/subagents/src/parity-types.ts +0 -315
- package/packages/subagents/src/pi-args.ts +0 -167
- package/packages/subagents/src/result-files.ts +0 -232
- package/packages/subagents/src/result-watcher.ts +0 -221
- package/packages/subagents/src/retained-children.ts +0 -176
- package/packages/subagents/src/run-fanout-budget.ts +0 -356
- package/packages/subagents/src/scheduled-runs.ts +0 -349
- package/packages/subagents/src/schemas.ts +0 -263
- package/packages/subagents/src/slash-commands.ts +0 -80
- package/packages/subagents/src/supervisor-channel.ts +0 -249
- package/packages/subagents/src/tool-handler.ts +0 -1144
- package/packages/subagents/src/types.ts +0 -238
- package/packages/subagents/src/widget.ts +0 -457
- package/packages/subagents/src/workflow-script.ts +0 -737
- package/packages/subagents/src/workflow-worker.ts +0 -384
- package/packages/subagents/src/worktree.ts +0 -614
- package/packages/utility/src/analytics/collector.ts +0 -293
- package/packages/utility/src/info-screen.ts +0 -80
- package/packages/utility/src/lifecycle/process.ts +0 -162
- package/packages/utility/src/prefix-cache.ts +0 -263
- package/packages/utility/src/skill-discovery.ts +0 -542
- package/packages/utility/src/tools/env.ts +0 -113
- package/packages/utility/src/tui/name-badge-state.ts +0 -315
- package/packages/utility/src/tui/name-badge.ts +0 -109
- /package/packages/{subagents/src → core/src/child-agent}/pi-spawn.ts +0 -0
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @unipi/core — harness message provenance.
|
|
3
|
+
*
|
|
4
|
+
* Harness-generated text that reaches the model as role USER (direct
|
|
5
|
+
* sendUserMessage) is annotated with a namespaced `unipiHarness` metadata field
|
|
6
|
+
* so the UI can style it — model roles, text, delivery and admission behavior
|
|
7
|
+
* stay untouched.
|
|
8
|
+
*
|
|
9
|
+
* Lifecycle (single-call synchronous arm — the SDK runs `input` handlers
|
|
10
|
+
* synchronously inside sendUserMessage):
|
|
11
|
+
* 1. `sendHarnessUserMessage` ensures the observers are installed, pushes a
|
|
12
|
+
* single-call ARM, calls the unchanged `pi.sendUserMessage`, and pops the
|
|
13
|
+
* arm in `finally` — nothing lingers across calls.
|
|
14
|
+
* 2. The input handler run DURING that call confirms the arm (source
|
|
15
|
+
* `extension`, exact text). Human/rpc inputs and extension inputs without
|
|
16
|
+
* an active arm become human/unattributed admission records.
|
|
17
|
+
* 3. Idle (non-queued) harness records are dispatch-eligible only after
|
|
18
|
+
* `before_agent_start` carries the exact prompt (auth/model passed).
|
|
19
|
+
* Queued records are eligible from their confirmed delivery while active.
|
|
20
|
+
* 4. `message_start` selects among ELIGIBLE harness records; human/
|
|
21
|
+
* unattributed records participate in conflict detection but never label.
|
|
22
|
+
* Identical texts with conflicting origins — across deliveries — fail
|
|
23
|
+
* CLOSED and clear the whole matching group. The selected message is
|
|
24
|
+
* marked processed; `message_end` never re-selects.
|
|
25
|
+
* 5. `message_end` attaches `unipiHarness` only when the finalized text still
|
|
26
|
+
* equals the selected text (later transforms fail closed). SessionManager
|
|
27
|
+
* persists the whole object, so provenance survives resume/branch.
|
|
28
|
+
*
|
|
29
|
+
* Irreducible SDK gap: input events carry no per-send ID. The next input
|
|
30
|
+
* purges ALL unmatched direct records (harness + human/rpc) and terminal
|
|
31
|
+
* boundaries purge everything outstanding — the failure mode is always native
|
|
32
|
+
* fallback, never a human message mislabelled.
|
|
33
|
+
*/
|
|
34
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
35
|
+
|
|
36
|
+
export type HarnessDelivery =
|
|
37
|
+
| "direct"
|
|
38
|
+
| "steer"
|
|
39
|
+
| "followUp"
|
|
40
|
+
| "nextTurn"
|
|
41
|
+
| "boundary"
|
|
42
|
+
| "before_agent_start";
|
|
43
|
+
|
|
44
|
+
export interface HarnessMessageMeta {
|
|
45
|
+
version: 1;
|
|
46
|
+
id: string;
|
|
47
|
+
source: string;
|
|
48
|
+
title: string;
|
|
49
|
+
synopsis?: string;
|
|
50
|
+
delivery: HarnessDelivery;
|
|
51
|
+
severity?: "warning";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export type HarnessMessageMetaInput = Omit<HarnessMessageMeta, "version" | "id" | "delivery">;
|
|
55
|
+
|
|
56
|
+
/** Canonical per-API state, carried on the API ROOT via a Symbol.for property.
|
|
57
|
+
* Proxies (withCommandEcho, the umbrella registerTool proxy) forward unknown
|
|
58
|
+
* property gets/sets to their target, so every wrapper observes one state. */
|
|
59
|
+
const STATE_SYM = Symbol.for("unipi.harnessMessages.state");
|
|
60
|
+
|
|
61
|
+
interface HarnessToken {
|
|
62
|
+
kind: "harness";
|
|
63
|
+
origin: string;
|
|
64
|
+
title: string;
|
|
65
|
+
synopsis?: string;
|
|
66
|
+
severity?: "warning";
|
|
67
|
+
text: string;
|
|
68
|
+
delivery: "direct" | "steer" | "followUp";
|
|
69
|
+
/** Direct records become eligible only at before_agent_start. */
|
|
70
|
+
eligible: boolean;
|
|
71
|
+
state: "admitted" | "consumed";
|
|
72
|
+
seq: number;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
interface HarnessHumanRecord {
|
|
76
|
+
kind: "human" | "unattributed";
|
|
77
|
+
origin: string;
|
|
78
|
+
text: string;
|
|
79
|
+
delivery: "direct" | "steer" | "followUp";
|
|
80
|
+
state: "admitted" | "consumed";
|
|
81
|
+
seq: number;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
type AdmissionRecord = HarnessToken | HarnessHumanRecord;
|
|
85
|
+
|
|
86
|
+
interface Arm {
|
|
87
|
+
origin: string;
|
|
88
|
+
title: string;
|
|
89
|
+
synopsis?: string;
|
|
90
|
+
severity?: "warning";
|
|
91
|
+
text: string;
|
|
92
|
+
delivery: "direct" | "steer" | "followUp";
|
|
93
|
+
seq: number;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface HarnessRegistryState {
|
|
97
|
+
records: AdmissionRecord[];
|
|
98
|
+
seq: number;
|
|
99
|
+
arms: Arm[];
|
|
100
|
+
/** message object → { meta, text } (set at message_start). */
|
|
101
|
+
pendingMeta: WeakMap<object, { meta: HarnessMessageMeta; text: string }>;
|
|
102
|
+
/** messages whose selection already happened — message_end never re-selects. */
|
|
103
|
+
processed: WeakSet<object>;
|
|
104
|
+
installed: boolean;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function stateFor(pi: object): HarnessRegistryState {
|
|
108
|
+
const existing = (pi as { [STATE_SYM]?: HarnessRegistryState })[STATE_SYM];
|
|
109
|
+
if (existing) return existing;
|
|
110
|
+
const state: HarnessRegistryState = {
|
|
111
|
+
records: [],
|
|
112
|
+
seq: 0,
|
|
113
|
+
arms: [],
|
|
114
|
+
pendingMeta: new WeakMap(),
|
|
115
|
+
processed: new WeakSet(),
|
|
116
|
+
installed: false,
|
|
117
|
+
};
|
|
118
|
+
try {
|
|
119
|
+
Object.defineProperty(pi, STATE_SYM, { value: state, enumerable: false, configurable: false, writable: false });
|
|
120
|
+
} catch {
|
|
121
|
+
// Frozen hosts: fall back to an instance-local property (single-proxy use).
|
|
122
|
+
try {
|
|
123
|
+
(pi as { [STATE_SYM]?: HarnessRegistryState })[STATE_SYM] = state;
|
|
124
|
+
} catch {
|
|
125
|
+
// Absolutely frozen: operate armless — sends stay UNTRACKED (native).
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return (pi as { [STATE_SYM]?: HarnessRegistryState })[STATE_SYM] ?? state;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
let idCounter = 0;
|
|
132
|
+
function nextId(): string {
|
|
133
|
+
idCounter += 1;
|
|
134
|
+
return `hm-${Date.now().toString(36)}-${idCounter.toString(36)}`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Build a stable namespaced meta (no date header — ids are monotonic). */
|
|
138
|
+
export function harnessMetadata(meta: HarnessMessageMetaInput, delivery: HarnessDelivery): HarnessMessageMeta {
|
|
139
|
+
return { version: 1, id: nextId(), delivery, ...meta };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Details payload for tool results: provenance header data for annotations
|
|
143
|
+
* appended to the result content (fusion nudges, kanboard R1, watchdog kill). */
|
|
144
|
+
export interface HarnessToolAnnotation {
|
|
145
|
+
meta: HarnessMessageMeta;
|
|
146
|
+
text: string;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function harnessToolResultDetails(
|
|
150
|
+
eventDetails: unknown,
|
|
151
|
+
meta: HarnessMessageMetaInput,
|
|
152
|
+
delivery: HarnessDelivery,
|
|
153
|
+
text: string,
|
|
154
|
+
): Record<string, unknown> {
|
|
155
|
+
const existing = (eventDetails as { unipiHarnessAnnotations?: unknown } | null | undefined)?.unipiHarnessAnnotations;
|
|
156
|
+
const list = Array.isArray(existing) ? existing : [];
|
|
157
|
+
return {
|
|
158
|
+
...((eventDetails as Record<string, unknown> | null | undefined) ?? {}),
|
|
159
|
+
unipiHarnessAnnotations: [...list, { meta: harnessMetadata(meta, delivery), text }],
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Namespaced read of harness metadata from a message or details object. */
|
|
164
|
+
export function readHarnessMeta(source: unknown): HarnessMessageMeta | undefined {
|
|
165
|
+
const meta = (source as { unipiHarness?: unknown } | null | undefined)?.unipiHarness ?? source;
|
|
166
|
+
if (!meta || typeof meta !== "object") return undefined;
|
|
167
|
+
const m = meta as Partial<HarnessMessageMeta>;
|
|
168
|
+
const ALLOWED_DELIVERY: readonly string[] = ["direct", "steer", "followUp", "nextTurn", "boundary", "before_agent_start"];
|
|
169
|
+
if (
|
|
170
|
+
m.version !== 1 || typeof m.id !== "string" || typeof m.source !== "string" ||
|
|
171
|
+
typeof m.title !== "string" || typeof m.delivery !== "string" || !ALLOWED_DELIVERY.includes(m.delivery)
|
|
172
|
+
) {
|
|
173
|
+
return undefined;
|
|
174
|
+
}
|
|
175
|
+
return m as HarnessMessageMeta;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Extract the exact text the SDK renders/queues for a user message. */
|
|
179
|
+
export function userMessageText(content: unknown): string {
|
|
180
|
+
if (typeof content === "string") return content;
|
|
181
|
+
if (!Array.isArray(content)) return "";
|
|
182
|
+
return content
|
|
183
|
+
.filter((c): c is { type: "text"; text: string } => (c as { type?: string }).type === "text")
|
|
184
|
+
.map((c) => (c as { text: string }).text)
|
|
185
|
+
.join("");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function userTextFromMessage(message: unknown): string {
|
|
189
|
+
if (!message || typeof message !== "object") return "";
|
|
190
|
+
const m = message as { role?: unknown; content?: unknown };
|
|
191
|
+
if (m.role !== "user") return "";
|
|
192
|
+
return userMessageText(m.content);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Send a harness-generated user message through the UNCHANGED
|
|
197
|
+
* `pi.sendUserMessage` with a synchronous single-call provenance arm. Returns
|
|
198
|
+
* the raw sender's value (callers may await; failures propagate unchanged).
|
|
199
|
+
* Provenance tracking is best-effort: if observers cannot install on this API,
|
|
200
|
+
* the raw send proceeds untracked (native fallback).
|
|
201
|
+
*/
|
|
202
|
+
export function sendHarnessUserMessage(
|
|
203
|
+
pi: ExtensionAPI,
|
|
204
|
+
content: string,
|
|
205
|
+
meta: HarnessMessageMetaInput,
|
|
206
|
+
options?: { deliverAs?: "steer" | "followUp" },
|
|
207
|
+
): ReturnType<ExtensionAPI["sendUserMessage"]> {
|
|
208
|
+
let arm: Arm | undefined;
|
|
209
|
+
try {
|
|
210
|
+
try {
|
|
211
|
+
installHarnessProvenance(pi);
|
|
212
|
+
const state = stateFor(pi);
|
|
213
|
+
state.seq += 1;
|
|
214
|
+
arm = {
|
|
215
|
+
origin: meta.source,
|
|
216
|
+
title: meta.title,
|
|
217
|
+
synopsis: meta.synopsis,
|
|
218
|
+
severity: meta.severity,
|
|
219
|
+
text: content,
|
|
220
|
+
delivery: options?.deliverAs ?? "direct",
|
|
221
|
+
seq: state.seq,
|
|
222
|
+
};
|
|
223
|
+
state.arms.push(arm);
|
|
224
|
+
} catch {
|
|
225
|
+
// Observers/state unavailable (frozen host): send untracked.
|
|
226
|
+
arm = undefined;
|
|
227
|
+
}
|
|
228
|
+
return pi.sendUserMessage(content, options);
|
|
229
|
+
} finally {
|
|
230
|
+
// The input handler ran synchronously inside the call (or never ran —
|
|
231
|
+
// compaction/auth failure). Either way the arm dies with the call: an
|
|
232
|
+
// unconfirmed arm leaves NO token, so nothing can mismatch later sends.
|
|
233
|
+
if (arm) {
|
|
234
|
+
const idx = stateFor(pi).arms.indexOf(arm);
|
|
235
|
+
if (idx !== -1) stateFor(pi).arms.splice(idx, 1);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const DELIVERY_ORDER: Record<string, number> = { direct: 0, steer: 1, followUp: 2 };
|
|
241
|
+
|
|
242
|
+
/** Prune consumed records immediately (bounded arrays over long runs). */
|
|
243
|
+
function prune(state: HarnessRegistryState): void {
|
|
244
|
+
if (state.records.some((r) => r.state === "consumed")) {
|
|
245
|
+
state.records = state.records.filter((r) => r.state !== "consumed");
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** Select the outstanding admission record for an arriving user message.
|
|
250
|
+
* Only ELIGIBLE harness records can label; human/unattributed records
|
|
251
|
+
* participate in conflict detection but never label. Identical texts with
|
|
252
|
+
* conflicting origins — across deliveries — fail closed and clear the whole
|
|
253
|
+
* matching group. */
|
|
254
|
+
function selectRecord(state: HarnessRegistryState, text: string): HarnessMessageMeta | undefined {
|
|
255
|
+
const candidates = state.records.filter(
|
|
256
|
+
(r) => r.state === "admitted" && r.text === text && (r.kind !== "harness" || r.eligible),
|
|
257
|
+
);
|
|
258
|
+
if (candidates.length === 0) return undefined;
|
|
259
|
+
const origins = new Set(candidates.map((r) => r.origin));
|
|
260
|
+
if (origins.size > 1) {
|
|
261
|
+
for (const r of candidates) r.state = "consumed";
|
|
262
|
+
prune(state);
|
|
263
|
+
return undefined;
|
|
264
|
+
}
|
|
265
|
+
candidates.sort((a, b) => (DELIVERY_ORDER[a.delivery] ?? 3) - (DELIVERY_ORDER[b.delivery] ?? 3) || a.seq - b.seq);
|
|
266
|
+
const chosen = candidates[0]!;
|
|
267
|
+
for (const r of candidates) r.state = "consumed";
|
|
268
|
+
prune(state); // consumed records never linger, even on normal success
|
|
269
|
+
if (chosen.kind !== "harness") return undefined;
|
|
270
|
+
return harnessMetadata(
|
|
271
|
+
{ source: chosen.origin, title: chosen.title, synopsis: chosen.synopsis, severity: chosen.severity },
|
|
272
|
+
chosen.delivery,
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Install the provenance observers. Idempotent per API ROOT (state travels with
|
|
278
|
+
* the root through proxies). Model roles/text/delivery/admission are untouched;
|
|
279
|
+
* the only behavior change is the namespaced `unipiHarness` field on harness
|
|
280
|
+
* user messages.
|
|
281
|
+
*/
|
|
282
|
+
export function installHarnessProvenance(pi: ExtensionAPI): void {
|
|
283
|
+
const state = stateFor(pi);
|
|
284
|
+
if (state.installed) return;
|
|
285
|
+
state.installed = true;
|
|
286
|
+
const onAny = pi.on as unknown as (event: string, handler: (event?: unknown, ctx?: unknown) => unknown) => void;
|
|
287
|
+
|
|
288
|
+
onAny("input", (event) => {
|
|
289
|
+
const ev = (event ?? {}) as { text?: unknown; source?: unknown; streamingBehavior?: unknown };
|
|
290
|
+
const text = typeof ev.text === "string" ? ev.text : "";
|
|
291
|
+
const source = typeof ev.source === "string" ? ev.source : "interactive";
|
|
292
|
+
const streaming = ev.streamingBehavior === "steer" || ev.streamingBehavior === "followUp";
|
|
293
|
+
// Effective delivery: the queued value ONLY while streaming (the real SDK
|
|
294
|
+
// omits streamingBehavior on idle events even when the caller passed one —
|
|
295
|
+
// an idle followUp option is ignored and dispatches direct).
|
|
296
|
+
const effective = streaming ? (ev.streamingBehavior as "steer" | "followUp") : "direct";
|
|
297
|
+
// The next input invalidates ALL unmatched direct records (harness and
|
|
298
|
+
// human/rpc alike) — bounds failed/aborted sends. Queued stays.
|
|
299
|
+
state.records = state.records.filter(
|
|
300
|
+
(r) => !(r.delivery === "direct" && r.state === "admitted"),
|
|
301
|
+
);
|
|
302
|
+
state.seq += 1;
|
|
303
|
+
const arm = state.arms[state.arms.length - 1];
|
|
304
|
+
if (arm && source === "extension" && text === arm.text) {
|
|
305
|
+
state.records.push({
|
|
306
|
+
kind: "harness",
|
|
307
|
+
origin: arm.origin,
|
|
308
|
+
title: arm.title,
|
|
309
|
+
synopsis: arm.synopsis,
|
|
310
|
+
severity: arm.severity,
|
|
311
|
+
text: arm.text,
|
|
312
|
+
delivery: effective,
|
|
313
|
+
// Queued is eligible immediately; idle direct only at before_agent_start.
|
|
314
|
+
eligible: streaming,
|
|
315
|
+
state: "admitted",
|
|
316
|
+
seq: arm.seq,
|
|
317
|
+
});
|
|
318
|
+
prune(state);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
state.records.push({
|
|
322
|
+
kind: source === "extension" ? "unattributed" : "human",
|
|
323
|
+
origin: source,
|
|
324
|
+
text,
|
|
325
|
+
delivery: effective,
|
|
326
|
+
state: "admitted",
|
|
327
|
+
seq: state.seq,
|
|
328
|
+
});
|
|
329
|
+
prune(state);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
onAny("before_agent_start", (event) => {
|
|
333
|
+
const prompt = (event as { prompt?: unknown } | undefined)?.prompt;
|
|
334
|
+
if (typeof prompt !== "string") return;
|
|
335
|
+
// Idle harness records matching THIS prompt become eligible; other
|
|
336
|
+
// unconfirmed direct records are stale and dropped. Queued untouched.
|
|
337
|
+
for (const r of state.records) {
|
|
338
|
+
if (r.kind === "harness" && r.delivery === "direct" && r.state === "admitted") {
|
|
339
|
+
if (r.text === prompt) r.eligible = true;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
state.records = state.records.filter(
|
|
343
|
+
(r) => !(r.kind === "harness" && r.delivery === "direct" && r.state === "admitted" && !r.eligible),
|
|
344
|
+
);
|
|
345
|
+
prune(state);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
onAny("message_start", (event) => {
|
|
349
|
+
const message = (event as { message?: unknown } | undefined)?.message;
|
|
350
|
+
if (!message || typeof message !== "object") return;
|
|
351
|
+
const text = userTextFromMessage(message);
|
|
352
|
+
state.processed.add(message as object);
|
|
353
|
+
if (!text) return;
|
|
354
|
+
const meta = selectRecord(state, text);
|
|
355
|
+
if (meta) state.pendingMeta.set(message as object, { meta, text });
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
onAny("message_end", (event) => {
|
|
359
|
+
const message = (event as { message?: unknown } | undefined)?.message;
|
|
360
|
+
if (!message || typeof message !== "object") return undefined;
|
|
361
|
+
if (!state.processed.has(message as object)) return undefined; // never re-select
|
|
362
|
+
const pending = state.pendingMeta.get(message as object);
|
|
363
|
+
if (!pending) return undefined;
|
|
364
|
+
state.pendingMeta.delete(message as object);
|
|
365
|
+
// Later transforms: attach only if the finalized text is still the
|
|
366
|
+
// selected text (original content identity preserved either way).
|
|
367
|
+
if (userTextFromMessage(message) !== pending.text) return undefined;
|
|
368
|
+
return { message: { ...(message as Record<string, unknown>), unipiHarness: pending.meta } };
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
// Terminal boundary: purge everything outstanding (queued records were
|
|
372
|
+
// consumed by their message_start before agent_end).
|
|
373
|
+
onAny("agent_settled", () => {
|
|
374
|
+
state.records = state.records.filter((r) => r.state === "consumed");
|
|
375
|
+
prune(state);
|
|
376
|
+
state.arms.length = 0;
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
// Fresh session context: pending state from the old tree is meaningless.
|
|
380
|
+
for (const evt of ["session_before_switch", "session_before_fork", "session_before_tree", "session_start", "session_shutdown"]) {
|
|
381
|
+
onAny(evt, () => {
|
|
382
|
+
state.records = [];
|
|
383
|
+
state.pendingMeta = new WeakMap();
|
|
384
|
+
state.processed = new WeakSet();
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/** Test hook: inspect the per-API state. */
|
|
390
|
+
export function harnessStateForTests(pi: object): HarnessRegistryState {
|
|
391
|
+
return stateFor(pi);
|
|
392
|
+
}
|
package/packages/core/index.ts
CHANGED
|
@@ -14,21 +14,35 @@ 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";
|
|
20
|
+
export * from "./command-echo.js";
|
|
21
|
+
export * from "./src/turn/arbiter.js";
|
|
22
|
+
export * from "./src/evidence.js";
|
|
18
23
|
|
|
19
24
|
// v3 workspace identity + state layout (marker-file id, per-workspace roots)
|
|
20
25
|
export * from "./src/workspace/identity.js";
|
|
21
26
|
export * from "./src/workspace/paths.js";
|
|
22
27
|
export * from "./src/package-colors.js";
|
|
23
28
|
export * from "./src/tui/hub-kit.js";
|
|
29
|
+
export * from "./src/tui/kit.js";
|
|
30
|
+
export * from "./src/tui/progress.js";
|
|
24
31
|
export * from "./src/jev/client.js";
|
|
25
32
|
export * from "./src/jev/settings.js";
|
|
33
|
+
export * from "./src/jev/chatter.js";
|
|
26
34
|
export * from "./src/workspace/state-migration.js";
|
|
27
35
|
// v3 settings engine + migration (canonical ~/.unipi/config layout)
|
|
28
36
|
export * from "./src/settings/paths.js";
|
|
29
37
|
export * from "./src/settings/engine.js";
|
|
30
38
|
export * from "./src/settings/schema.js";
|
|
31
39
|
export { SettingsHub, type SettingsHubDeps } from "./src/settings/hub.js";
|
|
40
|
+
export { openSettingsHub } from "./src/settings/open.js";
|
|
41
|
+
export * from "./src/attach/detect.js";
|
|
32
42
|
export * from "./src/settings/catalog.js";
|
|
33
43
|
export * from "./command-runner.js";
|
|
44
|
+
export * from "./compaction-context.js";
|
|
34
45
|
export * from "./src/settings/migrations.js";
|
|
46
|
+
export * from "./src/hints/index.js";
|
|
47
|
+
export * from "./src/hints/store.js";
|
|
48
|
+
export * from "./harness-messages.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,9 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @pi-unipi/core — Model Cache
|
|
3
3
|
*
|
|
4
|
-
* File-based model list cache at ~/.unipi/config/models-cache.json
|
|
5
|
-
*
|
|
6
|
-
* without needing ctx.modelRegistry
|
|
4
|
+
* File-based model list cache at ~/.unipi/config/models-cache.json — the one
|
|
5
|
+
* catalog every unipi surface reads (settings hub pickers, kanboard, image
|
|
6
|
+
* pickers) without needing ctx.modelRegistry. Written by utility on every
|
|
7
|
+
* session start from pi's live registry (models with credentials), plus the
|
|
8
|
+
* image-generation models pi-ai publishes separately.
|
|
7
9
|
*/
|
|
8
10
|
|
|
9
11
|
import * as fs from "node:fs";
|
|
@@ -18,10 +20,12 @@ function cacheDir(): string {
|
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
/** Resolve the model cache file path at call time. */
|
|
21
|
-
function
|
|
23
|
+
export function modelCachePath(): string {
|
|
22
24
|
return path.join(cacheDir(), "models-cache.json");
|
|
23
25
|
}
|
|
24
26
|
|
|
27
|
+
export type Modality = "text" | "image";
|
|
28
|
+
|
|
25
29
|
/** A single cached model entry */
|
|
26
30
|
export interface CachedModel {
|
|
27
31
|
/** Model provider (e.g. "openai", "anthropic") */
|
|
@@ -30,6 +34,14 @@ export interface CachedModel {
|
|
|
30
34
|
id: string;
|
|
31
35
|
/** Optional display name */
|
|
32
36
|
name?: string;
|
|
37
|
+
/** Input modalities. Absent in pre-v3 caches → treat as ["text"]. */
|
|
38
|
+
input?: Modality[];
|
|
39
|
+
/** Output modalities. Chat models → ["text"]; image generators include "image". */
|
|
40
|
+
output?: Modality[];
|
|
41
|
+
/** "chat" = pi chat registry, "images" = pi-ai images collection / generator. */
|
|
42
|
+
kind?: "chat" | "images";
|
|
43
|
+
contextWindow?: number;
|
|
44
|
+
reasoning?: boolean;
|
|
33
45
|
}
|
|
34
46
|
|
|
35
47
|
/** The full model cache structure */
|
|
@@ -40,13 +52,64 @@ export interface ModelCache {
|
|
|
40
52
|
models: CachedModel[];
|
|
41
53
|
}
|
|
42
54
|
|
|
55
|
+
/** `provider/id` reference for a cached model. */
|
|
56
|
+
export function modelRef(model: Pick<CachedModel, "provider" | "id">): string {
|
|
57
|
+
return `${model.provider}/${model.id}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function modelInputs(model: CachedModel): Modality[] {
|
|
61
|
+
return model.input?.length ? model.input : ["text"];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function modelOutputs(model: CachedModel): Modality[] {
|
|
65
|
+
return model.output?.length ? model.output : ["text"];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Filter by modality. `input` lists what the model must accept (all of them),
|
|
70
|
+
* `output` what it must produce (any of them).
|
|
71
|
+
*/
|
|
72
|
+
export function filterModels(
|
|
73
|
+
models: readonly CachedModel[],
|
|
74
|
+
want: { input?: Modality[]; output?: Modality[] },
|
|
75
|
+
): CachedModel[] {
|
|
76
|
+
return models.filter((m) => {
|
|
77
|
+
const inputs = modelInputs(m);
|
|
78
|
+
const outputs = modelOutputs(m);
|
|
79
|
+
if (want.input && !want.input.every((i) => inputs.includes(i))) return false;
|
|
80
|
+
if (want.output && !want.output.some((o) => outputs.includes(o))) return false;
|
|
81
|
+
return true;
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Map pi registry models (chat) to cache entries. Tolerates partial shapes. */
|
|
86
|
+
export function chatModelsToCache(models: readonly unknown[]): CachedModel[] {
|
|
87
|
+
const out: CachedModel[] = [];
|
|
88
|
+
for (const raw of models) {
|
|
89
|
+
const m = raw as { provider?: unknown; id?: unknown; name?: unknown; input?: unknown; contextWindow?: unknown; reasoning?: unknown };
|
|
90
|
+
if (typeof m?.provider !== "string" || typeof m?.id !== "string") continue;
|
|
91
|
+
const input = Array.isArray(m.input) ? (m.input.filter((x) => x === "text" || x === "image") as Modality[]) : ["text" as const];
|
|
92
|
+
out.push({
|
|
93
|
+
provider: m.provider,
|
|
94
|
+
id: m.id,
|
|
95
|
+
...(typeof m.name === "string" ? { name: m.name } : {}),
|
|
96
|
+
input: input.length ? input : ["text"],
|
|
97
|
+
output: ["text"],
|
|
98
|
+
kind: "chat",
|
|
99
|
+
...(typeof m.contextWindow === "number" ? { contextWindow: m.contextWindow } : {}),
|
|
100
|
+
...(typeof m.reasoning === "boolean" ? { reasoning: m.reasoning } : {}),
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
105
|
+
|
|
43
106
|
/**
|
|
44
107
|
* Read cached model list from disk.
|
|
45
108
|
* Returns empty array if no cache file exists or it's malformed.
|
|
46
109
|
*/
|
|
47
110
|
export function readModelCache(): CachedModel[] {
|
|
48
111
|
try {
|
|
49
|
-
const file =
|
|
112
|
+
const file = modelCachePath();
|
|
50
113
|
if (!fs.existsSync(file)) return [];
|
|
51
114
|
const parsed = JSON.parse(fs.readFileSync(file, "utf-8"));
|
|
52
115
|
return Array.isArray(parsed.models) ? parsed.models : [];
|
|
@@ -58,6 +121,8 @@ export function readModelCache(): CachedModel[] {
|
|
|
58
121
|
/**
|
|
59
122
|
* Write model list to cache file.
|
|
60
123
|
* Creates directory if needed. Best effort — silently ignores errors.
|
|
124
|
+
* Entries are de-duplicated by `provider/id` (first wins) and sorted, so the
|
|
125
|
+
* file is stable across sessions with the same catalog.
|
|
61
126
|
*/
|
|
62
127
|
export function writeModelCache(models: CachedModel[]): void {
|
|
63
128
|
try {
|
|
@@ -65,11 +130,20 @@ export function writeModelCache(models: CachedModel[]): void {
|
|
|
65
130
|
if (!fs.existsSync(dir)) {
|
|
66
131
|
fs.mkdirSync(dir, { recursive: true });
|
|
67
132
|
}
|
|
133
|
+
const seen = new Set<string>();
|
|
134
|
+
const unique = models.filter((m) => {
|
|
135
|
+
const key = modelRef(m);
|
|
136
|
+
if (seen.has(key)) return false;
|
|
137
|
+
seen.add(key);
|
|
138
|
+
return true;
|
|
139
|
+
}).sort((a, b) => modelRef(a).localeCompare(modelRef(b)));
|
|
68
140
|
const cache: ModelCache = {
|
|
69
141
|
updatedAt: new Date().toISOString(),
|
|
70
|
-
models,
|
|
142
|
+
models: unique,
|
|
71
143
|
};
|
|
72
|
-
|
|
144
|
+
const tmp = `${modelCachePath()}.${process.pid}.tmp`;
|
|
145
|
+
fs.writeFileSync(tmp, JSON.stringify(cache, null, 2) + "\n", "utf-8");
|
|
146
|
+
fs.renameSync(tmp, modelCachePath());
|
|
73
147
|
} catch {
|
|
74
148
|
// Best effort — cache is optional
|
|
75
149
|
}
|