@tekmidian/pai 0.39.1 → 0.40.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/README.md +41 -0
  2. package/dist/{auto-route-BlWOWT4z.mjs → auto-route-BwzmlW3W.mjs} +2 -2
  3. package/dist/{auto-route-BlWOWT4z.mjs.map → auto-route-BwzmlW3W.mjs.map} +1 -1
  4. package/dist/{chain-CqlgL7uH.mjs → chain-DhVHVnmT.mjs} +263 -821
  5. package/dist/chain-DhVHVnmT.mjs.map +1 -0
  6. package/dist/cli/index.mjs +10 -8
  7. package/dist/cli/index.mjs.map +1 -1
  8. package/dist/cli/program.mjs +10 -8
  9. package/dist/{clusters-wlK0w41E.mjs → clusters-DRHhfli9.mjs} +1 -1
  10. package/dist/{clusters-wlK0w41E.mjs.map → clusters-DRHhfli9.mjs.map} +1 -1
  11. package/dist/{config-B64vFg14.mjs → config-D1G9IFpn.mjs} +50 -3
  12. package/dist/config-D1G9IFpn.mjs.map +1 -0
  13. package/dist/{config-C_ErGddD.mjs → config-DD52BPB-.mjs} +1 -1
  14. package/dist/config-Dl8lT4Lu.mjs +527 -0
  15. package/dist/config-Dl8lT4Lu.mjs.map +1 -0
  16. package/dist/{context-handover-cache-BpUojjsi.mjs → context-handover-cache-C6_rs7JT.mjs} +112 -24
  17. package/dist/context-handover-cache-C6_rs7JT.mjs.map +1 -0
  18. package/dist/daemon/index.mjs +12 -10
  19. package/dist/daemon/index.mjs.map +1 -1
  20. package/dist/daemon-BSL9sXxm.mjs +22 -0
  21. package/dist/{daemon-CTkN_Vn8.mjs → daemon-CpdVUnl_.mjs} +26 -26
  22. package/dist/{daemon-CTkN_Vn8.mjs.map → daemon-CpdVUnl_.mjs.map} +1 -1
  23. package/dist/daemon-mcp/index.mjs +76 -6
  24. package/dist/daemon-mcp/index.mjs.map +1 -1
  25. package/dist/detector-BLVOCiJ_.mjs +5 -0
  26. package/dist/{detector-Bwk_4Pk4.mjs → detector-smpCm7ku.mjs} +1 -1
  27. package/dist/{detector-Bwk_4Pk4.mjs.map → detector-smpCm7ku.mjs.map} +1 -1
  28. package/dist/{factory-A9x-T2Kg.mjs → factory-Bc5EyN54.mjs} +7 -7
  29. package/dist/{factory-A9x-T2Kg.mjs.map → factory-Bc5EyN54.mjs.map} +1 -1
  30. package/dist/factory-C7ua_3ik.mjs +3 -0
  31. package/dist/{fallback-DqYhiACU.mjs → fallback-CdXv-Np0.mjs} +139 -49
  32. package/dist/fallback-CdXv-Np0.mjs.map +1 -0
  33. package/dist/hooks/context-compression-hook.mjs +136 -22
  34. package/dist/hooks/context-compression-hook.mjs.map +4 -4
  35. package/dist/hooks/post-compact-inject.mjs +98 -7
  36. package/dist/hooks/post-compact-inject.mjs.map +4 -4
  37. package/dist/hooks/route-agents-to-worker.mjs +36 -2
  38. package/dist/hooks/route-agents-to-worker.mjs.map +2 -2
  39. package/dist/hooks/worker-proxy.mjs +36 -2
  40. package/dist/hooks/worker-proxy.mjs.map +2 -2
  41. package/dist/hooks/worker-status-line.mjs +110 -86
  42. package/dist/hooks/worker-status-line.mjs.map +4 -4
  43. package/dist/{indexer-backend-TG64CCQC.mjs → indexer-backend-rohVVKrA.mjs} +1 -1
  44. package/dist/{indexer-backend-TG64CCQC.mjs.map → indexer-backend-rohVVKrA.mjs.map} +1 -1
  45. package/dist/{ipc-client-BmypMNYk.mjs → ipc-client-C7AVpsyv.mjs} +2 -2
  46. package/dist/{ipc-client-BmypMNYk.mjs.map → ipc-client-C7AVpsyv.mjs.map} +1 -1
  47. package/dist/{kg-entity-r8duqhi9.mjs → kg-entity-DbOMPdF9.mjs} +1 -1
  48. package/dist/{kg-entity-r8duqhi9.mjs.map → kg-entity-DbOMPdF9.mjs.map} +1 -1
  49. package/dist/{latent-ideas-B7wq75Pt.mjs → latent-ideas-iNp5s55R.mjs} +2 -2
  50. package/dist/{latent-ideas-B7wq75Pt.mjs.map → latent-ideas-iNp5s55R.mjs.map} +1 -1
  51. package/dist/{link-boost-HkG7JWZR.mjs → link-boost-NNzADHX_.mjs} +1 -1
  52. package/dist/{link-boost-HkG7JWZR.mjs.map → link-boost-NNzADHX_.mjs.map} +1 -1
  53. package/dist/{main-resolver-DlaLOFBA.mjs → main-resolver-CM1IHbuu.mjs} +78 -75
  54. package/dist/main-resolver-CM1IHbuu.mjs.map +1 -0
  55. package/dist/main-resolver-E5WlilhM.mjs +6 -0
  56. package/dist/{neighborhood-lThN-MaQ.mjs → neighborhood-AGL7oXQ2.mjs} +1 -1
  57. package/dist/{neighborhood-lThN-MaQ.mjs.map → neighborhood-AGL7oXQ2.mjs.map} +1 -1
  58. package/dist/{note-context-b6k0mAKi.mjs → note-context-BHwYpSFf.mjs} +1 -1
  59. package/dist/{note-context-b6k0mAKi.mjs.map → note-context-BHwYpSFf.mjs.map} +1 -1
  60. package/dist/{planner-DGV8VFib.mjs → planner-Cm3g6fWH.mjs} +5 -3
  61. package/dist/{planner-DGV8VFib.mjs.map → planner-Cm3g6fWH.mjs.map} +1 -1
  62. package/dist/{postgres-mW1n7Vi1.mjs → postgres-Dwxe290H.mjs} +1 -1
  63. package/dist/{postgres-mW1n7Vi1.mjs.map → postgres-Dwxe290H.mjs.map} +1 -1
  64. package/dist/{program-B-QJXCRb.mjs → program-AR0qlYRg.mjs} +309 -78
  65. package/dist/program-AR0qlYRg.mjs.map +1 -0
  66. package/dist/query-feedback-CGcEiGtY.mjs +3 -0
  67. package/dist/{query-feedback-BV4CcxqS.mjs → query-feedback-DZfjI34w.mjs} +1 -1
  68. package/dist/{query-feedback-BV4CcxqS.mjs.map → query-feedback-DZfjI34w.mjs.map} +1 -1
  69. package/dist/router-CAtyVQud.mjs +3 -0
  70. package/dist/{router-DcHKnEPa.mjs → router-DHracTUC.mjs} +1 -1
  71. package/dist/{router-DcHKnEPa.mjs.map → router-DHracTUC.mjs.map} +1 -1
  72. package/dist/server-BOuAOj9b.mjs +386 -0
  73. package/dist/server-BOuAOj9b.mjs.map +1 -0
  74. package/dist/skills/Worker/SKILL.md +6 -1
  75. package/dist/{sources-kLnQsNrW.mjs → sources-ju6AA7lL.mjs} +1 -1
  76. package/dist/{sources-kLnQsNrW.mjs.map → sources-ju6AA7lL.mjs.map} +1 -1
  77. package/dist/{sqlite-BenGr3UP.mjs → sqlite-kFZeWCJ2.mjs} +1 -1
  78. package/dist/{sqlite-BenGr3UP.mjs.map → sqlite-kFZeWCJ2.mjs.map} +1 -1
  79. package/dist/{state-Ca9F_sZc.mjs → state-DH_mp5Xe.mjs} +1 -1
  80. package/dist/{state-CAeyOdfq.mjs → state-ahFAbD8b.mjs} +1 -1
  81. package/dist/{state-CAeyOdfq.mjs.map → state-ahFAbD8b.mjs.map} +1 -1
  82. package/dist/{themes-BI4GMRP9.mjs → themes-DqhNUUQk.mjs} +1 -1
  83. package/dist/{themes-BI4GMRP9.mjs.map → themes-DqhNUUQk.mjs.map} +1 -1
  84. package/dist/{tools-DGcB3o_S.mjs → tools-CMlHa9lZ.mjs} +14 -14
  85. package/dist/{tools-DGcB3o_S.mjs.map → tools-CMlHa9lZ.mjs.map} +1 -1
  86. package/dist/{tools-Bp7hj6OW.mjs → tools-COna9vZR.mjs} +1 -1
  87. package/dist/{trace-bobARFEX.mjs → trace-DTP6k4B_.mjs} +1 -1
  88. package/dist/{trace-bobARFEX.mjs.map → trace-DTP6k4B_.mjs.map} +1 -1
  89. package/dist/{vault-indexer-C3OfXTNF.mjs → vault-indexer-YMH22mLU.mjs} +1 -1
  90. package/dist/{vault-indexer-C3OfXTNF.mjs.map → vault-indexer-YMH22mLU.mjs.map} +1 -1
  91. package/dist/{work-queue-worker-R7UGIag_.mjs → work-queue-worker-B3nl8yyn.mjs} +23 -26
  92. package/dist/work-queue-worker-B3nl8yyn.mjs.map +1 -0
  93. package/dist/work-queue-worker-BM1GcAAq.mjs +14 -0
  94. package/dist/{zettelkasten-m5QPtb-L.mjs → zettelkasten-DDvKRm2V.mjs} +3 -3
  95. package/dist/{zettelkasten-m5QPtb-L.mjs.map → zettelkasten-DDvKRm2V.mjs.map} +1 -1
  96. package/docs/commands/README.md +4 -2
  97. package/docs/commands/worker.md +48 -5
  98. package/docs/images/provider-abstraction-live.png +0 -0
  99. package/docs/images/workers.png +0 -0
  100. package/docs/launcher-independence.md +70 -0
  101. package/docs/provider-abstraction.md +87 -0
  102. package/docs/provider-independence-details.md +330 -0
  103. package/docs/provider-independence.md +75 -0
  104. package/docs/worker.md +29 -11
  105. package/package.json +1 -1
  106. package/scripts/build-hooks.mjs +6 -1
  107. package/scripts/build-skill-stubs.mjs +17 -4
  108. package/src/hooks/ts/lib/context-fill.test.ts +158 -4
  109. package/src/hooks/ts/lib/context-fill.ts +102 -35
  110. package/statusline-command.sh +120 -21
  111. package/dist/chain-CqlgL7uH.mjs.map +0 -1
  112. package/dist/config-B64vFg14.mjs.map +0 -1
  113. package/dist/context-handover-cache-BpUojjsi.mjs.map +0 -1
  114. package/dist/daemon-CmHHmKpg.mjs +0 -20
  115. package/dist/detector-DO730Zq0.mjs +0 -5
  116. package/dist/factory-BD-np0Vd.mjs +0 -3
  117. package/dist/fallback-DqYhiACU.mjs.map +0 -1
  118. package/dist/main-resolver-D6IImXvF.mjs +0 -4
  119. package/dist/main-resolver-DlaLOFBA.mjs.map +0 -1
  120. package/dist/program-B-QJXCRb.mjs.map +0 -1
  121. package/dist/query-feedback-DSVyHtrG.mjs +0 -3
  122. package/dist/router-Bk77E7hj.mjs +0 -3
  123. package/dist/runtime-paths-rni52zHX.mjs +0 -50
  124. package/dist/runtime-paths-rni52zHX.mjs.map +0 -1
  125. package/dist/work-queue-worker-DW8lz-Oo.mjs +0 -11
  126. package/dist/work-queue-worker-R7UGIag_.mjs.map +0 -1
@@ -0,0 +1,87 @@
1
+ # Provider Abstraction
2
+
3
+ ## What this layer achieves
4
+
5
+ The entire assistant stack — interactive sessions, workers, follow panes, hooks, MCP tools and the statusline — runs on **any provider that speaks the Anthropic Messages protocol**, not just Anthropic. This has been demonstrated end-to-end on a third-party `glm` endpoint serving a 1M-context model: the interactive session, every delegated worker, the daemon-side background calls (summaries, context handovers, knowledge-graph extraction) and all instrumentation run against that provider, with nothing falling back to a vendor login.
6
+
7
+ What that buys:
8
+
9
+ - **No single-vendor lock-in.** Providers are entries in a registry (`src/workers/config.ts`): protocol (`anthropic`/`openai`), engine (`claude`/`codex`), model ids, key files, cost tiers, capability tags, quota probes. Switching is `pai worker providers use <name>`; disabling routing entirely is `pai worker off`.
10
+ - **Plan/quota-aware instrumentation.** The statusline reads the *active provider's* real plan utilization (5-hour rolling window and weekly quota with reset times), not a hardcoded vendor endpoint.
11
+ - **Cost routing per task class.** Work is dispatched by class (`draft`, `spotcheck`, `complex`, …); each class maps to a provider and model, so cheap work lands on cheap models without touching code.
12
+ - **Provider swaps are configuration, not surgery.** Every seam that used to assume one vendor — daemon LLM spawns, compaction-history family detection, context-window defaults, tier aliases — has been patched at protocol level (see below), so a new provider is a registry entry plus a key file.
13
+
14
+ OpenAI-protocol providers are reached through the translating proxy (`src/workers/proxy/`), and the Codex engine (`src/workers/codex.ts`) normalizes its events into the same shape — both are first-class in the registry.
15
+
16
+ ## Tools
17
+
18
+ ### Worker provider/model routing
19
+
20
+ `pai worker run --class <class>` picks the provider and model for a job by task class. The standard classes (`WORKER_CLASSES`, `src/workers/config.ts:126`): `draft`, `plan`, `implement`, `review`, `research`, `spotcheck`, `simple`, `complex`. `--provider` and `--model` override for a single call.
21
+
22
+ Model ids are configuration, not code. Two surfaces manage them:
23
+
24
+ - **CLI** — `pai worker model` (`src/cli/commands/worker/model.ts`): no args lists the active provider with its `default` and `fast` model ids; `model <id>` sets the default; `model fast <id>` sets the fast slot; `--provider` targets a non-active provider.
25
+ - **MCP** — the `worker_model` tool (`src/daemon-mcp/tools/worker-model.ts`): `action: get|set`, `slot: default|fast`, optional `provider`.
26
+
27
+ ```text
28
+ $ pai worker model
29
+ glm (active)
30
+ default: glm-5.3[1m]
31
+ fast: glm-5.3-flash
32
+
33
+ $ pai worker model glm-5.4
34
+ $ pai worker model fast glm-5.4-flash --provider glm
35
+ ```
36
+
37
+ The `fast` slot is what cheap classes resolve to; the runner also honors the platform's tier aliases by pinning `ANTHROPIC_DEFAULT_{HAIKU,SONNET,OPUS}_MODEL` to the provider's models in the spawn environment (`src/workers/run-env.ts`).
38
+
39
+ ### Provider-aware statusline
40
+
41
+ `statusline-command.sh` derives everything it can from the session's model id instead of assuming one vendor:
42
+
43
+ - **Plan utilization** — glm sessions fetch the provider's plan quota (5-hour window and weekly credit window, `nextResetTime` rendered as reset times); claude models keep the existing Anthropic OAuth path. Each source has its own 60-second cache. On fetch failure the provider windows show `?` rather than silently falling back to another plan's numbers. `PAI_ZAI_QUOTA_URL` overrides the endpoint (kill switch / testing).
44
+ - **Context window from the model id** — a `[1m]` variant suffix means a 1,000,000-token window (`statusline-command.sh:126`); ids with no marker keep the previous default. The same derivation lives in TypeScript as `contextWindowFromModelId()` (`src/utils/model-window.ts`), so the session hooks and the worker meter agree with the statusline.
45
+ - **`(N% left)`** — remaining context until auto-compact, computed against that derived window.
46
+ - **Worker row** — line 4 lists the workers this session launched, running ones with their current step (`pai worker status-line`, `statusline-command.sh:634`).
47
+
48
+ ### Seam patches
49
+
50
+ The seams that previously hardcoded one vendor, now patched:
51
+
52
+ - **Daemon LLM spawns route through the provider registry** — `src/workers/daemon-llm.ts` builds the spawn plan for session summaries (`src/daemon/session-summary-worker.ts`), context handovers (`src/daemon/context-handover-worker.ts`) and KG extraction (`src/memory/kg-extraction.ts`) via `buildRunEnv()` (`src/workers/run-env.ts`): provider base URL and token, the provider's concrete model id instead of a tier alias, and class-appropriate timeouts. With no provider configured it falls back to the historical bare-alias behaviour minus the vendor API key.
53
+ - **Compaction-history family detection accepts any provider model** — `modelFamily()` (`src/hooks/ts/lib/context-fill.ts:379`) derives a stable family key from the model id itself, stripping any bracketed variant suffix (`glm-5.3[1m]` → `glm-5.3`), instead of allowlisting `claude-*` and discarding everything else as "foreign". A provider-model session keeps its measured compaction history.
54
+ - **Context-window defaults derived from the model id** — `src/utils/model-window.ts` is the single helper; consumers are the session hooks (`context-fill.ts`) and the provider schema default (`src/workers/config.ts`). The statusline derives its window the same way, instead of writing a single assumed size into the state file for every model it does not recognize.
55
+ - **Tier aliases map provider models into the cheap/standard tiers** — `modelToClass()` (`src/workers/agents.ts`) resolves an agent's model through the provider registry (`fast` → cheap tier, `default` → standard) and logs once, then falls back to the standard class, instead of silently dropping the hint for unknown ids.
56
+
57
+ ### Delegation instruments
58
+
59
+ - **Agent-tool gate** — `src/hooks/ts/pre-tool-use/route-agents-to-worker.ts` (PreToolUse, Agent matcher) denies in-process subagents whenever worker providers are configured, so no delegated work runs on the vendor login; the deny reason tells the orchestrator to use `pai worker run` instead. `ALLOW_ANTHROPIC_AGENTS=1` bypasses it for one session. Every decision is appended to the routing ledger (`pai worker log`).
60
+ - **Worker exemption marker** — every worker spawn carries `PAI_WORKER=1` (`src/workers/run-env.ts:47`; `src/workers/codex.ts:50` for the Codex engine). `isWorkerSession()` (`src/hooks/ts/lib/worker-session.ts`) makes all per-session hooks (autosave, stop bookkeeping, compaction state) skip disposable workers, so the gate and the bookkeeping never fight the workers themselves.
61
+ - **Post-compaction reinjection** — `src/hooks/ts/session-start/post-compact-inject.ts` (SessionStart, matcher `compact`) replays the state the PreCompact hook saved, so a compacted orchestrator session resumes with its context intact. The live worker list stays visible in the statusline's worker row and via `pai worker ps` / `pai worker follow <id>`.
62
+
63
+ ### Session-level instruments (deployed config)
64
+
65
+ Two further instruments are deployed configuration in the PAI config dir rather than repo code, so a tree-only audit misses them:
66
+
67
+ - **Edit-delegation guard** — a PreToolUse hook (`route-edits-to-worker`) blocks `Edit`/`MultiEdit`/`Write` on files inside any git work tree for orchestrator sessions, while worker sessions are exempt via the `PAI_WORKER=1` marker set on every worker spawn. Code changes route through workers mechanically.
68
+ - **Compact recovery** — on `SessionStart(compact)` a hook injects the live `pai worker ps --all` output into the session context, so a session resuming from compaction immediately sees every running worker.
69
+
70
+ ### Live worker panes
71
+
72
+ `pai worker follow` (`src/cli/commands/worker/index.ts:165`) attaches a chat pane to a worker, rendered by `src/workers/viewer.ts`, `pane.ts`, `render.ts` and `chatui.ts`:
73
+
74
+ - **Transcript with stamped gutter** — every event line carries a dim `HH:MM:SS │ ` stamp from the event's own timestamp, with continuation padding for wrapped text (`render.ts:135`); the pane's bar runs unbroken down its full height (`render.ts:139`).
75
+ - **Prompt row** — `› ` with readline line editing. Plain text is *said* to a running worker; `/resume <text>` continues a finished one; `/status` prints a one-line status (`chatui.ts`).
76
+ - **Status ticker** — a fixed bottom row showing elapsed time, current intent and tool (`$ bun run test` …) plus the worker's provider/model and context meter (`tickerText()`, `render.ts:183`; meter data from `src/workers/status.ts`, whose window comes from the run's init event or the provider default). A finished worker freezes the row with ✓/✗.
77
+ - **Resize-safe redraw** — on terminal resize the viewer re-reads the geometry, clears the stale transcript and refills it from events (`viewer.ts:735`), so the fixed prompt/ticker rows and the scroll-region transcript stay consistent at any size.
78
+
79
+ ## In action
80
+
81
+ ![A live session on a third-party 1M-context model: the provider-aware statusline (plan quota windows with reset times, derived context window, worker row) above worker follow panes with stamped transcript, prompt row and status ticker.](images/provider-abstraction-live.png)
82
+
83
+ ## Current limits
84
+
85
+ The layer is proven against one non-Anthropic provider so far (the `glm` endpoint); other Anthropic-protocol providers are expected to work but have not been exercised end-to-end, and OpenAI-protocol providers additionally depend on the translating proxy. Instrumentation still special-cases per-provider quota endpoints as providers are added. The seam audit that drove these patches lives outside this repository; new seams are patched as they surface, and each patch carries a unit test pinning the provider-neutral behaviour (`src/workers/daemon-llm.test.ts`, `src/workers/model.test.ts`, `src/workers/agents.test.ts`, `src/hooks/ts/lib/context-fill.test.ts`).
86
+
87
+ For the full worker command reference see [worker.md](worker.md) and [commands/worker.md](commands/worker.md).
@@ -0,0 +1,330 @@
1
+ # Provider Independence — Details
2
+
3
+ Everything technical about provider independence: what switches, the tools,
4
+ the configuration, the internals. The human-facing page is
5
+ [provider-independence.md](provider-independence.md).
6
+
7
+ ## Switch what?
8
+
9
+ Two levels, one provider registry. Both switch by sentence or by command —
10
+ never by editing code:
11
+
12
+ | Level | What runs on the provider | Switch with |
13
+ | --- | --- | --- |
14
+ | The crew | delegated workers — research, drafting, implementation, review, spotchecks | `worker_providers` (use): "Use glm for the workers from now on." |
15
+ | The orchestrator | the session itself — the interactive chat you are typing into | start it with `glm` (the shim for `pai worker run`), or pick a project with `pai` — the picker follows the active provider |
16
+
17
+ The main session orchestrates, reviews and merges; workers do the work —
18
+ unless the session itself runs on a provider too, in which case everything is
19
+ off the Anthropic account.
20
+
21
+ ### The crew — `worker_providers` add / use
22
+
23
+ - **Add once.** "Add a worker provider named glm, base URL <url>, model
24
+ <model>, here is the key: <token>" → `worker_providers` (add). The key is
25
+ parked in `~/.config/pai/keys/<name>` (mode 0600); the config records only
26
+ the path. OpenAI-compatible endpoint? Say "upstream URL" — PAI routes it
27
+ through its local translating proxy. The first provider added also turns
28
+ worker routing on and seeds the nine classes (`addProvider`,
29
+ `src/workers/providers.ts:51`); from then a PreToolUse hook denies every
30
+ in-process subagent.
31
+ - **Use.** "Switch the fleet to glm." → `worker_providers` (use): sets
32
+ `workers.active` (`useProvider`, `src/workers/providers.ts:170`); classes
33
+ without an explicit target follow.
34
+ - **Test.** "Is glm alive?" → `worker_providers` (test): a one-word pong
35
+ probe, latency, OK/FAILED.
36
+ - **Off.** "Turn workers off." → `worker_toggle`: subagents run on Anthropic
37
+ again; the registry is kept.
38
+
39
+ ### The orchestrator — the session itself
40
+
41
+ There is no in-session switch: a session runs on a provider by being
42
+ **started that way**. The launcher is `glm` — a shim in `~/.local/bin`
43
+ written by `pai worker install` (`src/workers/install.ts:119` body
44
+ `exec pai worker run "$@"`; installed names `glm`, `glm-run`, `glm-ps`,
45
+ `glm-log`, `worker-say`, `:125`).
46
+
47
+ `pai worker run` without `-p` starts an **interactive** Claude Code session:
48
+ it spawns `claude` with the terminal inherited (`stdio: "inherit"`,
49
+ `src/workers/run.ts:452`) — that process *is* the chat pane, not a subagent
50
+ of it (`origin: "chat"`, `run.ts:394`). No MCP restriction applies.
51
+
52
+ The `pai` project picker follows the active provider the same way: its
53
+ launch keys start an interactive worker run in the chosen directory
54
+ (`w` forces it, `a` forces plain `claude`).
55
+
56
+ What actually puts it on the provider is the spawn environment
57
+ (`buildRunEnv`, `src/workers/run-env.ts`):
58
+
59
+ | Env var | Value | Where |
60
+ | --- | --- | --- |
61
+ | `ANTHROPIC_API_KEY` | deleted — the Anthropic key never reaches a provider session | `run-env.ts:19` |
62
+ | `ANTHROPIC_BASE_URL` | the provider's base URL; the local proxy's URL for OpenAI-protocol providers | `run-env.ts:25,37` |
63
+ | `ANTHROPIC_AUTH_TOKEN` | the token read from the provider's key file (proxy runs: placeholder) | `run-env.ts:40` |
64
+ | `ANTHROPIC_DEFAULT_HAIKU_MODEL` / `_SONNET_MODEL` / `_OPUS_MODEL` | the provider's fast/default models — every model slot maps to the provider | `run-env.ts:41-43` |
65
+ | `ENABLE_TOOL_SEARCH` | `true` in interactive runs; headless runs get `PAI_WORKER=1` instead | `run-env.ts:47-49` |
66
+
67
+ - **Model.** `--model <provider default>` is passed unless you name one
68
+ (`run.ts:438-439`). A `[1m]` suffix on the model id (`glm-5.3[1m]`) selects
69
+ the 1,000,000-token window variant: the suffix is read wherever a window is
70
+ needed (`contextWindowFromModelId()`, `src/utils/model-window.ts:33`) and
71
+ stripped for the family name (`stripModelVariant()`, `:19`).
72
+ - **Machine-wide instead of per-session.** `worker_fallback` (on) writes the
73
+ same env into the `env` block of `~/.claude/settings.json`
74
+ (`src/workers/fallback.ts:90-94`; path override `CLAUDE_SETTINGS_PATH` for
75
+ dry runs, `:46`), so every NEW Claude Code process — interactive,
76
+ task-bus, daemon summarizer — runs on the provider. `off` restores the
77
+ saved previous state. Running sessions keep their provider until
78
+ restarted; worker routing is unchanged while on.
79
+ - **Bypass.** `ALLOW_ANTHROPIC_AGENTS=1` in the environment re-enables the
80
+ Agent tool for one session.
81
+
82
+ ## Tools (MCP)
83
+
84
+ Everything here happens in chat: you say the sentence, the session calls the
85
+ tool — you never name the tool yourself. These are the **twelve `worker_*`
86
+ tools** the pai MCP server registers (`src/daemon-mcp/index.ts:804-1334`);
87
+ there is no worker surface outside them. Parameter names are the schema's
88
+ own.
89
+
90
+ ### At a glance
91
+
92
+ | Tool | You say | What it does |
93
+ | --- | --- | --- |
94
+ | `worker_status` | "How is the worker system doing?" | On/off, active provider, providers, today's run tally, running workers with waiting handoffs |
95
+ | `worker_providers` | "Switch the workers to glm." | Manage providers: list / add / update / remove / use / enable / disable / test |
96
+ | `worker_fallback` | "Run everything on glm." | Machine-wide: every NEW Claude Code process on a provider — or back on the Anthropic login |
97
+ | `worker_classes` | "Make spotchecks cheaper." | Map task classes to providers or routing constraints |
98
+ | `worker_model` | "Put glm on the 1M-context model." | Show or set a provider's model ids (default and fast slots) |
99
+ | `worker_run` | "Fix the black buttons bug." | Start a worker or chain; returns the id immediately |
100
+ | `worker_handoff` | (inside a worker) "Ask the parent whether …" | Send a proposal / question / blocker / result up to the parent |
101
+ | `worker_toggle` | "Turn workers off." | Route subagents to workers (on) or back to Anthropic (off) |
102
+ | `worker_ps` | "What are my workers doing?" | Running workers plus the last finished |
103
+ | `worker_replay` | "What did 3390 do?" | Transcript of one worker |
104
+ | `worker_say` | "Tell 3390 to skip the docs." | One message to a running worker, mid-run |
105
+ | `worker_resume` | "Have 3390 also update the docs." | Continue a finished worker, context intact |
106
+
107
+ ### Tool details
108
+
109
+ #### `worker_status` — the state page
110
+ No parameters. Workers on/off, active provider, provider list, today's tally
111
+ (started / ok / failed, hook denied / allowed / rerouted), running workers
112
+ with `◆N` waiting handoffs. Use it before delegating to see what routing
113
+ will choose.
114
+
115
+ #### `worker_providers` — manage providers
116
+ `action` (default `list`): **list** · **add** · **update** · **remove** ·
117
+ **use** · **enable** · **disable** · **test**. `name` is required for every
118
+ action but `list`.
119
+
120
+ - **add** — needs `name`, `model`, and `key_file` or `key`. `base_url` for an
121
+ Anthropic-protocol endpoint, or `upstream_url` with `protocol=openai`
122
+ (routed through the local proxy). Optional: `engine` (`claude`|`codex`),
123
+ `fast_model`, `context_window` (the meter; unset hides it unless the init
124
+ event announces one), `env`, `note`, `quota_probe`, `cost_tier` (1 cheapest
125
+ … 5), `tags` (`code`, `vision`, `image-gen`, `long-context`, `fast`,
126
+ `reasoning`). The first provider added also turns routing on and seeds the
127
+ classes.
128
+ - **update** — `cost_tier` and/or `tags`.
129
+ - **use** — make it active; unset classes follow it.
130
+ - **enable / disable** — in or out of auto-routing.
131
+ - **test** — one-word pong probe: latency, OK/FAILED.
132
+ - **remove** — drop it, and any classes pointing at it.
133
+
134
+ #### `worker_fallback` — the machine-wide switch
135
+ `action` (default `status`): **on** · **off** · **status**; `on` takes
136
+ `provider` (default: active). See the orchestrator section above for exactly
137
+ what it writes and restores.
138
+
139
+ #### `worker_classes` — map classes to providers
140
+ `action` (default `list`): **list** · **set** · **unset**. `set` takes
141
+ `class` plus either `target` (`<provider>` or `<provider>/fast`) or routing
142
+ constraints — `max_cost_tier` (1–5) and/or `require_tags` — with which
143
+ auto-routing picks any qualifying provider. `unset` frees a class back to
144
+ the active provider.
145
+
146
+ #### `worker_model` — model slots
147
+ `action` (default `get`): **get** — model ids of one provider (`provider`,
148
+ default active) or all when omitted; **set** — needs `model`, `slot` picks
149
+ `default` or `fast`. `[1m]` suffix = 1M window, derived from the id.
150
+
151
+ #### `worker_run` — start work
152
+ `prompt` (required — the task, self-contained). Optional: `chain`
153
+ (`draft,implement` or `draft,implement,review`), `class` (the nine below),
154
+ `label` (shown in `worker_ps` and the statusline — always give one), `cwd`,
155
+ `allowed_tools` (comma-separated allowlist), `mcp` (servers/sets the worker
156
+ may load). Returns the id at once; the run is a background task — the result
157
+ arrives later as a task notification (`result` plus a parsed `report`:
158
+ changed paths, ✓/✗ checks, open items). The orchestrator ends its turn
159
+ instead of waiting, and reviews the diff itself — workers never merge their
160
+ own work.
161
+
162
+ #### `worker_handoff` — from inside a worker
163
+ `kind` (required): `proposal` · `question` · `blocker` · `result`; `text`
164
+ (required, one paragraph); optional structured `data`. Lands in the parent's
165
+ inbox. Only works inside a worker — no sideways or downward path. Results
166
+ are sent automatically when you finish; send one yourself only for mid-run
167
+ findings.
168
+
169
+ #### `worker_toggle` — routing switch
170
+ `enabled` (required). `true`: the Agent-tool hook denies in-process
171
+ subagents and rewrites them to workers. `false`: they run on Anthropic
172
+ again. Registry kept either way.
173
+
174
+ #### `worker_ps` — the list
175
+ Optional `all`: `true` = every session's workers, default this session's.
176
+ Running workers show id, provider, age, turns, current tool; the last
177
+ finished is included.
178
+
179
+ #### `worker_replay` — one transcript
180
+ `id` (required), optional `tail` (last N rendered lines, 1–2000). Plain
181
+ text: tool calls, short outputs, result.
182
+
183
+ #### `worker_say` — talk to a running worker
184
+ `id`, `text` (both required). Lands on the worker's open stdin as a user
185
+ message, mid-run, without breaking its stream. Fails with an explanation
186
+ when the worker finished — then `worker_resume`.
187
+
188
+ #### `worker_resume` — continue a finished worker
189
+ `id`, `text` (both required). `claude --resume` on the same provider, same
190
+ session, context intact; returns the new worker id. Not for running workers.
191
+
192
+ ## Configure
193
+
194
+ Everything lives in the `workers` section of `~/.config/pai/config.json`
195
+ (`readWorkersSection` / `writeWorkersSection`, `src/workers/config.ts:619`);
196
+ every sentence and tool above writes it atomically — no hand-editing.
197
+
198
+ **Classes** (`WORKER_CLASSES`, `src/workers/config.ts:131`):
199
+
200
+ | Class | Work |
201
+ | --- | --- |
202
+ | `draft` · `plan` | spec writing, planning |
203
+ | `implement` · `complex` | code (get their own git worktree on `worker/<id>`) |
204
+ | `review` | reads the diff |
205
+ | `research` | web research (give it `WebSearch,WebFetch`) |
206
+ | `spotcheck` · `simple` | fast verification, small jobs |
207
+ | `image` | image generation |
208
+
209
+ A class maps to `<provider>`, `<provider>/fast`, or to constraints
210
+ (`maxCostTier` 1–5, `requireTags`, per-class `order`). Unset classes follow
211
+ the active provider; `active: "auto"` routes by `workers.routing.order`
212
+ instead.
213
+
214
+ **Provider fields** (registry entry, `parseProvider`,
215
+ `src/workers/config.ts:264`): protocol, base URL (or upstream URL for the
216
+ proxy), key file path, model slots (default / fast, `[1m]` suffix for the
217
+ 1M window), engine, `cost_tier`, `tags`, extra `env`, `context_window`.
218
+
219
+ **Defaults you inherit** (`src/workers/config.ts:209-226`):
220
+
221
+ | Area | Default |
222
+ | --- | --- |
223
+ | Routing | cooldown 30 min after a quota/rate failure, `retryOnQuota` true |
224
+ | Sub-worker tree | depth 2, 4 children per parent |
225
+ | Follow pane | font 13 pt, auto-exit 60 s |
226
+ | Logs | `~/.claude/logs/workers` |
227
+
228
+ **Environment variables:**
229
+
230
+ | Var | Effect |
231
+ | --- | --- |
232
+ | `PAI_WORKER=1` | carried by every headless spawn — marks the worker session, exempts it from orchestrator-only hooks (`src/workers/run-env.ts:47`) |
233
+ | `ALLOW_ANTHROPIC_AGENTS=1` | re-enables the Agent tool for one session |
234
+ | `ENABLE_TOOL_SEARCH=true` | set in interactive provider runs (`run-env.ts:49`) |
235
+ | `CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1` | every provider spawn (`run-env.ts:45`) |
236
+ | `CLAUDE_SETTINGS_PATH` | dry-run target for `worker_fallback` |
237
+
238
+ ## How it works
239
+
240
+ One orchestrator, many workers, split made mechanical by four hooks:
241
+
242
+ | Hook | Enforces | Where |
243
+ | --- | --- | --- |
244
+ | PreToolUse (Agent) | every in-process subagent denied while providers are configured; the deny reason prints the `pai worker run` line to use instead | `src/hooks/ts/pre-tool-use/route-agents-to-worker.ts`, deployed `${PAI_DIR}/Hooks/route-agents-to-worker.mjs` |
245
+ | PreToolUse (Edit/Write) | no code edits by the orchestrator inside a git work tree; `PAI_WORKER=1` sessions are exempt | `${PAI_DIR}/Hooks/route-edits-to-worker.mjs` (deployed config) |
246
+ | SessionStart (compact) | saved state replayed and the live worker list injected — a compacted orchestrator still sees every worker | `src/hooks/ts/session-start/post-compact-inject.ts`, `${PAI_DIR}/Hooks/post-compact-workers.mjs` |
247
+ | PreToolUse (TaskOutput) | blocking waits denied unless `block: false` — end the turn; the result arrives as a task notification | `${PAI_DIR}/Hooks/block-taskoutput-wait.mjs` |
248
+
249
+ Every routing decision lands in the routing ledger (`pai worker log`). A
250
+ chain `draft,implement[,review]` gives each stage its own worker — own id,
251
+ own follow pane, tree in the worker list; a failing stage stops the chain.
252
+ Writing classes work in their own git worktree on `worker/<id>`; merging or
253
+ discarding is a deliberate operator (or orchestrator) action.
254
+
255
+ The **statusline** carries the worker row (line 4):
256
+
257
+ ```
258
+ glm ▶1 #3390 unlabeled 30s · interactive ✓74 ✗4 today
259
+ ```
260
+
261
+ Segments: provider badge (`glm`; `workers` when mixed) · `▶N` this
262
+ terminal's running workers with a live pid · short id · label · age ·
263
+ current step · today's finished count. Full legend (`◆N` handoffs, `ctx`
264
+ meter, `↳` sub-workers): the *Status line* section of
265
+ [worker.md](worker.md).
266
+
267
+ What else follows the active provider — statusline plan windows,
268
+ daemon-side summarizer calls, compaction history, the translating proxy:
269
+ [provider-abstraction.md](provider-abstraction.md).
270
+
271
+ ### Code map
272
+
273
+ | What | Where |
274
+ | --- | --- |
275
+ | Provider registry, classes (`WORKER_CLASSES`) | `src/workers/config.ts:131` |
276
+ | Config read/write, `workers` section | `src/workers/config.ts:619` |
277
+ | Provider validation (`parseProvider`) | `src/workers/config.ts:264` |
278
+ | First-provider takeover (enabled + active + seeded classes) | `src/workers/providers.ts:51` (`addProvider`) |
279
+ | `providers use` (sets `workers.active`) | `src/workers/providers.ts:170` (`useProvider`) |
280
+ | `pai worker on/off` (`setWorkersEnabled`) | `src/workers/providers.ts:279` |
281
+ | Runner / chain / agent definitions | `src/workers/run.ts`, `src/workers/chain.ts`, `src/workers/agents.ts` |
282
+ | Spawn env — base URL, token, model pins | `src/workers/run-env.ts` |
283
+ | Machine-wide fallback (settings.json) | `src/workers/fallback.ts` |
284
+ | Shims (`glm`, `glm-run`, …) installer | `src/workers/install.ts` |
285
+ | Worktrees and merge | `src/workers/worktree.ts` |
286
+ | CLI surface (`run`, `ps`, `follow`, `replay`, `merge`, `discard`) | `src/cli/commands/worker/index.ts` |
287
+ | Providers / classes CLI | `src/cli/commands/worker/providers.ts:124,334` |
288
+ | Model slots CLI (`pai worker model`) | `src/cli/commands/worker/model.ts` |
289
+ | Statusline worker row | `src/workers/render.ts`, `statusline-command.sh` (line 4), built `${PAI_DIR}/worker-status-line.mjs` |
290
+ | Context window from model id (`[1m]`) | `src/utils/model-window.ts` |
291
+ | Agent-tool gate hook (repo) | `src/hooks/ts/pre-tool-use/route-agents-to-worker.ts` |
292
+ | Post-compact reinjection (repo) | `src/hooks/ts/session-start/post-compact-inject.ts`, `src/hooks/ts/lib/context-fill.ts` |
293
+ | Worker-session exemption | `src/hooks/ts/lib/worker-session.ts` |
294
+ | Deployed hooks (config, not repo) | `${PAI_DIR}/Hooks/route-edits-to-worker.mjs`, `block-taskoutput-wait.mjs`, `post-compact-workers.mjs` — registered in `${PAI_DIR}/settings.json` |
295
+ | MCP tool surface — all 12 `worker_*` tools | `src/daemon-mcp/index.ts:804-1334` (`worker_model` handler: `src/daemon-mcp/tools/worker-model.ts`) |
296
+ | Neighbour docs | [provider-independence.md](provider-independence.md), [provider-abstraction.md](provider-abstraction.md), [worker.md](worker.md), [commands/worker.md](commands/worker.md) |
297
+
298
+ ## CLI fallback
299
+
300
+ For scripts and terminals outside sessions — the twin of a tool above; flags
301
+ live in [commands/worker.md](commands/worker.md). (`worker_status` has no
302
+ single twin; `pai worker ps` + `pai worker log` show the same facts.)
303
+
304
+ | Command | Twin of |
305
+ | --- | --- |
306
+ | `pai worker run [claude-args…]` | `worker_run` — and the only place a one-call `--provider` override exists |
307
+ | `pai worker ps [--all]` | `worker_ps` |
308
+ | `pai worker replay <id> [--tail n]` | `worker_replay` |
309
+ | `pai worker say <id> <text>` | `worker_say` |
310
+ | `pai worker resume <id> <text>` | `worker_resume` |
311
+ | `pai worker handoff <json>` | `worker_handoff` |
312
+ | `pai worker providers <list\|add\|update\|remove\|use\|enable\|disable\|test>` | `worker_providers` — keys via `--key-file`, never pasted |
313
+ | `pai worker classes <list\|set\|unset>` | `worker_classes` |
314
+ | `pai worker model [what] [model]` | `worker_model` |
315
+ | `pai worker on` / `pai worker off` | `worker_toggle` |
316
+ | `pai worker fallback <on [provider]\|off\|status>` | `worker_fallback` |
317
+
318
+ Terminal-only (no tool equivalent):
319
+
320
+ | Command | What it does |
321
+ | --- | --- |
322
+ | `pai worker watch` | `ps` refreshed every 2 seconds |
323
+ | `pai worker follow [id]` / `pai worker pane [id]` | live transcript pane for one worker or this session's |
324
+ | `pai worker log [all\|tail\|<id>]` | the routing ledger, or one worker's raw event stream |
325
+ | `pai worker merge <id>` / `pai worker discard <id>` | land a worker's worktree branch in the original checkout, or drop it entirely |
326
+ | `pai worker controls <id> you\|me` | hand the desktop controls (clickr) to a worker, or take them back |
327
+ | `pai worker status-line [term] [cwd]` | one-line worker summary for a status bar (called by `statusline-command.sh`) |
328
+ | `pai worker proxy [stop]` | the local Anthropic↔OpenAI proxy |
329
+ | `pai worker mcp [list]` | MCP servers/sets workers may load |
330
+ | `pai worker install` | migration: Agent hook in settings.json, `~/.local/bin` shims (`glm`, …), old script cleanup |
@@ -0,0 +1,75 @@
1
+ # Provider Independence
2
+
3
+ How to free yourself from Claude — so a maxed-out subscription stops being your problem. How to run any cloud model and any local model inside your Claude Code (or OpenCode) harness. How to run models in parallel, each doing what it is best at — all of it purely local, no need for yet another Hermes-or-whatever harness. How to have workers run by default, visibly, each with its own chat you can talk to.
4
+
5
+ My journey into PAI and AIBroker started nine months ago, when OpenClawd burned through all my credits in four hours. I decided then and there that there had to be a better way: instead of buying API tokens, use the Claude Max subscription I was already paying for.
6
+
7
+ Nine months later, I hit the next wall: maxing out Claude Max itself. The next level of independence was required. I built it — in one day. Here is what happened.
8
+
9
+ I started my Thursday at 5am, which is normal, and at about 80%, which is not.
10
+
11
+ That 80% is not motivation — although that was certainly higher. It is the weekly usage of my Claude Max 200 subscription.
12
+
13
+ With two days to go until the Saturday 8am reset, I was getting worried. Last week I had already hit 100% — despite all my efficiency gains, including inventing my own "Agentish" language.
14
+
15
+ By 9am I was above 90%. There was no way I would make it to the reset.
16
+
17
+ So I decided to solve this problem instead of waiting it out. By 10pm I was at 97% — a number that has not moved since noon, because since noon, almost nothing has run on Claude.
18
+
19
+ This is how I did it.
20
+
21
+ I made myself independent of Claude and Fable. They are great — but I hate being locked in, and I suspect most of us do. I want to decide which model I use: Claude, GLM, Grok, whatever. I need to abstract away from that choice.
22
+
23
+ Fable was — and is — great, but it burns tokens. And Opus on its own is so unbelievably dumb that I had taken to letting Fable do everything, even forcing it to not use agents — burning tokens even faster.
24
+
25
+ So the requirements were:
26
+
27
+ 1. **Provider independence.** Switch to any model at any time — not only for agents, but for the main orchestrator session too.
28
+ 2. **Cloud and local.** Any cloud model, and local models.
29
+ 3. **Visibility.** Really see what each worker is doing — not Claude Code's click-into-a-worker-and-make-sense-of-it, but every worker in a side pane, automatically.
30
+ 4. **Direct chat.** Talk to any worker while it runs.
31
+ 5. **Hardening.** Each worker in its own worktree, so parallel work cannot destroy itself.
32
+ 6. **Forced orchestration.** Not just ask the orchestrator to delegate — deterministically enforce that it spawns workers.
33
+ 7. **Cost routing.** Automatically pick cheaper workers for simpler tasks.
34
+ 8. **Keep the harness.** Stay on Claude Code (I could have switched to OpenCode — turns out it was not even necessary).
35
+
36
+ The whole implementation cost 7 of my last 10 percentage points of Claude Max 200. Then I switched to GLM for everything else. And what can I say — it works beautifully.
37
+
38
+ ---
39
+
40
+ The rest of this page is the operator's guide: switching providers, starting sessions, and the three copy-paste steps to your own setup.
41
+
42
+ > My AI budget hit 91% before lunch. So I changed one rule: the expensive
43
+ > assistant keeps the thinking, a cheaper crew does the building. The feature
44
+ > shipped anyway.
45
+ >
46
+ > Your AI bill is not a fact of nature. It is a design decision.
47
+
48
+ Your assistant runs its helper crew on whichever AI provider you choose —
49
+ and so can the session itself.
50
+
51
+ ## Switching
52
+
53
+ - **The crew.** Once, at the start: "Add a worker provider named <provider> — here
54
+ is the key." Then: "Use <provider> for the workers from now on."
55
+ - **The session itself.** Start it with the `<provider>` command — the shim for
56
+ `pai worker run` — or just pick a project with `pai`: the picker follows
57
+ the active provider too, so the whole session, you included, runs on it.
58
+
59
+ Swapping back — or to any other provider — is one sentence for the crew, and
60
+ starting `claude` again for the session.
61
+
62
+ ## A day with the crew
63
+
64
+ - "Fix the login timeout bug." — a worker named *fix login timeout* starts; the status line shows its name and current step, live.
65
+ - "What are my workers doing?" — a short list, by name.
66
+ - "Tell fix login timeout to also check the retry path." — lands mid-run; the worker adapts.
67
+ - "Show me what it changed." — the diff comes back for your review.
68
+ - "Stop using workers for now." — the crew stands down.
69
+
70
+ ![The worker row live in the status line.](images/workers.png)
71
+
72
+ ## Further
73
+
74
+ - [How it works — full tool reference](provider-independence-details.md)
75
+ - [The provider layer, in depth](provider-abstraction.md)
package/docs/worker.md CHANGED
@@ -383,8 +383,8 @@ behaviour — no prompt row, no ticker, stdin still the operator channel.
383
383
  Any worker may start its own workers: the runner exports `PAI_WORKER_ID` in
384
384
  every worker's environment, and a `pai worker run` launched from inside one
385
385
  records `parent` in its status — so the forest is visible in `ps` (children
386
- indented under their parent, `├`/`└` connectors), the status line (`↳` under
387
- the parent) and each child gets its own follow pane. Handoffs travel **up
386
+ indented under their parent, `├`/`└` connectors) and each child gets its own
387
+ follow pane. Handoffs travel **up
388
388
  only**, from a child to its parent:
389
389
 
390
390
  ```
@@ -395,8 +395,8 @@ pai worker handoff '{"kind":"proposal","text":"run this on a cheap provider","da
395
395
  `result` is sent automatically when a child finishes). The handoff is appended
396
396
  to `<logDir>/<parent>.inbox.jsonl` (durable, ordered) and, when the parent is
397
397
  running, also delivered as an operator message `[handoff from <child id>]`. The
398
- parent sees it in its pane (`◆ from <id> · kind: text`, magenta), `ps` and the
399
- status line show `◆N` for an inbox with N handoffs, and `replay`/`follow`
398
+ parent sees it in its pane (`◆ from <id> · kind: text`, magenta), `ps` shows
399
+ `◆N` for an inbox with N handoffs, and `replay`/`follow`
400
400
  merge them into the transcript by timestamp. There is no sideways channel:
401
401
  siblings never see each other, everything goes up.
402
402
 
@@ -468,9 +468,9 @@ drive the desktop until it is handed over.
468
468
 
469
469
  Status files carry `contextTokens` (input + cache read + cache creation +
470
470
  output of the last assistant turn) and `contextWindow` (from the init event,
471
- else the provider's `contextWindow`, else 200 000). The `ps` table and the
472
- status line show `ctx 84k/200k (42%)` once it passes 60 % — yellow past
473
- 70 %, red past 85 % — and the pane's liveness line always shows it.
471
+ else the provider's `contextWindow`, else 200 000). The `ps` table shows
472
+ `ctx 84k/200k (42%)` once it passes 60 % — yellow past 70 %, red past
473
+ 85 % — and the pane's liveness line always shows it.
474
474
 
475
475
  ### MCP for workers
476
476
 
@@ -559,7 +559,25 @@ the CLI calls.
559
559
 
560
560
  ## Status line
561
561
 
562
- Line 4 of the statusline lists this session's running workers (provider,
563
- label, age, current tool, context meter past 60 %) plus today's ✓/✗ tally.
564
- It prefers the standalone `~/.claude/worker-status-line.mjs` (plain node,
565
- built by `bun run build`) and falls back to `pai worker status-line`.
562
+ Line 4 of the statusline is the worker bar: the provider and running-worker
563
+ count, one row per running spawned worker, and today's ✓/✗ tally. It prefers
564
+ the standalone `~/.claude/worker-status-line.mjs` (plain node, built by
565
+ `bun run build`) and falls back to `pai worker status-line`.
566
+
567
+ Example: `glm ▶2 | fix black buttons · 6m · grep … | spotcheck login · 1m · Edit: button.ts ✓75 ✗4 today`
568
+
569
+ Segment by segment:
570
+
571
+ - provider tag (`glm`) — the chat pane's provider; `workers` when there is no tracked chat pane and the running workers are mixed
572
+ - `▶2` — this terminal's running spawned workers: `state=running` **and** a live pid **and** not the chat pane; nothing at zero, so an idle terminal shows just `glm`
573
+ - row `name · age · step` — the worker's `--label` (else the first 70 chars of the prompt, `unlabeled` when the run had neither), time since it started, and the verb of what it is doing; `#id` (the short form `pai worker ps` shows) joins the name only when two running workers share a label
574
+ - step (`grep …`) — the current tool trimmed to its verb: no `Bash:` prefix, no flags, no quotes, ≤24 chars; file tools keep their `Edit: button.ts` shape
575
+ - `✓75 ✗4 today` — today's finished workers: done vs failed
576
+
577
+ The chat pane itself contributes only its provider — its age, state and
578
+ inbox live in `pai worker ps`, never in the bar. Rows are flat and
579
+ oldest-first; the worker forest (`↳` depth) and `◆N` inbox marks are `ps`
580
+ territory too. A status file from before the `origin` flag (a running entry
581
+ with no origin, the unlabeled placeholder and zero turns) is treated as the
582
+ chat pane so it does not render as a phantom worker — that shim goes away
583
+ once every pane runs code that writes `origin`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tekmidian/pai",
3
- "version": "0.39.1",
3
+ "version": "0.40.0",
4
4
  "description": "PAI Knowledge OS — Personal AI Infrastructure with federated memory and project management",
5
5
  "type": "module",
6
6
  "main": "dist/index.mjs",
@@ -29,6 +29,7 @@ import {
29
29
  } from "fs";
30
30
  import { join, resolve, basename } from "path";
31
31
  import { homedir, platform } from "os";
32
+ import { isInsideWorkerWorktree } from "./lib/sync-guard.mjs";
32
33
 
33
34
  const HOOKS_SRC = "src/hooks/ts";
34
35
  const HOOKS_OUT = "dist/hooks";
@@ -143,7 +144,11 @@ console.log(
143
144
  // --sync: Symlink (or copy on Windows) all deployable files to ~/.claude/
144
145
  // ---------------------------------------------------------------------------
145
146
 
146
- if (doSync) {
147
+ if (doSync && isInsideWorkerWorktree(process.cwd())) {
148
+ // Never repoint the live ~/.claude symlinks at a worktree that `pai worker
149
+ // merge` will delete — see scripts/lib/sync-guard.mjs.
150
+ console.log("✔ Hook symlinks skipped: build runs inside a worker worktree");
151
+ } else if (doSync) {
147
152
  const useSymlinks = platform() !== "win32";
148
153
  const claudeDir = join(homedir(), ".claude");
149
154
  const hooksTarget = join(claudeDir, "Hooks");
@@ -32,6 +32,7 @@ import {
32
32
  } from "fs";
33
33
  import { join, resolve } from "path";
34
34
  import { homedir, platform } from "os";
35
+ import { isInsideWorkerWorktree } from "./lib/sync-guard.mjs";
35
36
 
36
37
  const PROMPTS_DIR = "src/daemon-mcp/prompts";
37
38
  const CUSTOM_DIR = join(PROMPTS_DIR, "custom");
@@ -107,13 +108,21 @@ function syncSymlinks(generatedNames) {
107
108
  const source = resolve(join(STUBS_OUT, name));
108
109
  const target = join(skillsDir, name);
109
110
 
111
+ // lstat once: dangling links must count as symlinks (existsSync misses them)
112
+ let entry;
113
+ try {
114
+ entry = lstatSync(target);
115
+ } catch {
116
+ // absent — fresh install
117
+ }
118
+
110
119
  // Never overwrite non-symlink directories (user's own skills)
111
- if (existsSync(target) && !lstatSync(target).isSymbolicLink()) {
120
+ if (entry && !entry.isSymbolicLink()) {
112
121
  continue;
113
122
  }
114
123
 
115
- // Check existing symlink
116
- if (existsSync(target) && lstatSync(target).isSymbolicLink()) {
124
+ // Check existing symlink (may be dangling)
125
+ if (entry) {
117
126
  if (resolve(readlinkSync(target)) === source) {
118
127
  current++;
119
128
  continue;
@@ -197,6 +206,10 @@ for (const [fileName, dir] of [
197
206
  const customLabel = customNames.length > 0 ? ` (${builtinNames.length} built-in + ${customNames.length} custom)` : "";
198
207
  console.log(`✔ ${generated} skill stubs generated in ${STUBS_OUT}/${customLabel}`);
199
208
 
200
- if (doSync) {
209
+ if (doSync && isInsideWorkerWorktree(process.cwd())) {
210
+ // Never repoint the live ~/.claude/skills symlinks at a worktree that
211
+ // `pai worker merge` will delete — see scripts/lib/sync-guard.mjs.
212
+ console.log("✔ Skill symlinks skipped: build runs inside a worker worktree");
213
+ } else if (doSync) {
201
214
  syncSymlinks(generatedDirNames);
202
215
  }