@piagent/platform 1.8.0 → 1.9.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 (216) hide show
  1. package/AGENTS.md +7 -0
  2. package/CHANGELOG.md +97 -1
  3. package/README.md +45 -70
  4. package/README.vi.md +34 -61
  5. package/architecture/layers.json +16 -3
  6. package/catalog/capabilities.json +14 -7
  7. package/catalog/claude-model-additions.json +42 -0
  8. package/docs/auto-delegation-policy.md +2 -0
  9. package/docs/command-reference-vietnamese.md +68 -90
  10. package/docs/context-window-policy.md +5 -5
  11. package/docs/distribution-standard.md +13 -16
  12. package/docs/en/architecture.md +9 -1
  13. package/docs/herdr-workflow.md +2 -3
  14. package/docs/languages.json +4 -4
  15. package/docs/managed-local-acceptance.md +403 -0
  16. package/docs/mcp-and-tools.md +5 -7
  17. package/docs/memory-policy.md +3 -3
  18. package/docs/model-options.md +2 -2
  19. package/docs/oauth-providers.md +1 -1
  20. package/docs/operator-manual-vietnamese.md +82 -127
  21. package/docs/package-architecture-notes.md +4 -4
  22. package/docs/piagent-core-improvement-plan.md +3 -5
  23. package/docs/piagent-core-plan-checkpoint-2026-09-06.json +2 -13
  24. package/docs/piagent-runtime-benchmark-execution-plan.md +10 -8
  25. package/docs/project-adapters.md +3 -3
  26. package/docs/project-onboarding.md +15 -16
  27. package/docs/publishing-for-teams.md +2 -1
  28. package/docs/quality-benchmark.md +2 -0
  29. package/docs/quickstart-vietnamese.md +23 -24
  30. package/docs/release-install-policy.md +3 -2
  31. package/docs/research/harness-next-evidence-review.md +7 -7
  32. package/docs/runtime-harness-standard.md +2 -0
  33. package/docs/runtime-policy-design.md +2 -0
  34. package/docs/runtime-quality-baseline.md +2 -0
  35. package/docs/subagent-orchestration-capabilities.md +1 -1
  36. package/docs/subagents-and-multiagent.md +10 -8
  37. package/docs/task-implementation-contract.md +2 -0
  38. package/docs/task-lifecycle-vietnamese.md +2 -0
  39. package/docs/team-onboarding.md +28 -59
  40. package/docs/usage-observability.md +2 -2
  41. package/docs/vi/architecture.md +9 -1
  42. package/docs/workflow-recipes.md +6 -10
  43. package/docs-site/architecture.html +4 -3
  44. package/docs-site/assets/docs.css +80 -0
  45. package/docs-site/benchmark.html +7 -1
  46. package/docs-site/commands.html +37 -23
  47. package/docs-site/content/architecture.html +2 -2
  48. package/docs-site/content/benchmark.html +5 -0
  49. package/docs-site/content/commands.html +35 -22
  50. package/docs-site/content/ecosystem.html +144 -0
  51. package/docs-site/content/en/architecture.html +1 -1
  52. package/docs-site/content/en/benchmark.html +5 -0
  53. package/docs-site/content/en/commands.html +30 -13
  54. package/docs-site/content/en/ecosystem.html +144 -0
  55. package/docs-site/content/en/index.html +24 -9
  56. package/docs-site/content/en/quickstart.html +10 -12
  57. package/docs-site/content/en/security.html +1 -1
  58. package/docs-site/content/en/team.html +2 -2
  59. package/docs-site/content/en/whats-new.html +77 -1
  60. package/docs-site/content/en/workflows.html +29 -31
  61. package/docs-site/content/index.html +24 -9
  62. package/docs-site/content/quickstart.html +10 -12
  63. package/docs-site/content/security.html +2 -2
  64. package/docs-site/content/team.html +2 -2
  65. package/docs-site/content/whats-new.html +77 -1
  66. package/docs-site/content/workflows.html +36 -38
  67. package/docs-site/context-engine.html +2 -1
  68. package/docs-site/ecosystem.html +283 -0
  69. package/docs-site/en/architecture.html +3 -2
  70. package/docs-site/en/benchmark.html +7 -1
  71. package/docs-site/en/commands.html +32 -14
  72. package/docs-site/en/context-engine.html +2 -1
  73. package/docs-site/en/ecosystem.html +283 -0
  74. package/docs-site/en/index.html +30 -14
  75. package/docs-site/en/mcp-approval.html +2 -1
  76. package/docs-site/en/mcp-auth.html +2 -1
  77. package/docs-site/en/mcp-servers.html +2 -1
  78. package/docs-site/en/mcp.html +2 -1
  79. package/docs-site/en/permissions.html +2 -1
  80. package/docs-site/en/profiles.html +2 -1
  81. package/docs-site/en/quickstart.html +17 -18
  82. package/docs-site/en/runtime.html +2 -1
  83. package/docs-site/en/scope.html +2 -1
  84. package/docs-site/en/security.html +6 -5
  85. package/docs-site/en/team.html +4 -3
  86. package/docs-site/en/whats-new.html +85 -8
  87. package/docs-site/en/workflows.html +34 -35
  88. package/docs-site/index.html +30 -14
  89. package/docs-site/mcp-approval.html +2 -1
  90. package/docs-site/mcp-auth.html +2 -1
  91. package/docs-site/mcp-servers.html +2 -1
  92. package/docs-site/mcp.html +2 -1
  93. package/docs-site/permissions.html +2 -1
  94. package/docs-site/profiles.html +2 -1
  95. package/docs-site/quickstart.html +17 -18
  96. package/docs-site/runtime.html +2 -1
  97. package/docs-site/scope.html +2 -1
  98. package/docs-site/security.html +7 -6
  99. package/docs-site/team.html +4 -3
  100. package/docs-site/whats-new.html +85 -8
  101. package/docs-site/workflows.html +41 -42
  102. package/evals/fs5-causal-arm.v1.json +2 -2
  103. package/evals/private-holdout-v1/CURRENT_BOUNDARY.md +17 -0
  104. package/evals/private-holdout-v1/public-exposure.v2.json +2 -2
  105. package/package.json +8 -7
  106. package/packages/piagent-core/README.md +15 -23
  107. package/packages/piagent-core/docs/context-efficiency-improvement-plan.md +2 -0
  108. package/packages/piagent-core/extensions/experiment-loop-policy.ts +75 -0
  109. package/packages/piagent-core/extensions/local-state-path.js +15 -0
  110. package/packages/piagent-core/extensions/piagent-guard.ts +108 -960
  111. package/packages/piagent-core/extensions/policy-core.js +2 -2
  112. package/packages/piagent-core/extensions/source-checkout-paths.ts +54 -0
  113. package/packages/piagent-core/managed/broker-client.mjs +49 -0
  114. package/packages/piagent-core/managed/capabilities.mjs +28 -0
  115. package/packages/piagent-core/managed/compaction.mjs +63 -0
  116. package/packages/piagent-core/managed/doctor.mjs +107 -0
  117. package/packages/piagent-core/managed/native-catalog.mjs +42 -0
  118. package/packages/piagent-core/managed/patch-snapshot.mjs +48 -0
  119. package/packages/piagent-core/managed/repository-operation.mjs +84 -0
  120. package/packages/piagent-core/managed/request-stream.mjs +129 -0
  121. package/packages/piagent-core/managed/resource-loader.mjs +42 -0
  122. package/packages/piagent-core/managed/review-snapshot-worker.mjs +27 -0
  123. package/packages/piagent-core/managed/sandbox-diagnostic.mjs +12 -0
  124. package/packages/piagent-core/managed/search-fallback.mjs +93 -0
  125. package/packages/piagent-core/managed/session.mjs +472 -0
  126. package/packages/piagent-core/managed/store.d.mts +3 -0
  127. package/packages/piagent-core/managed/store.mjs +64 -0
  128. package/packages/piagent-core/managed/tool-boundary.mjs +243 -0
  129. package/packages/piagent-core/managed/tool-worker.mjs +36 -0
  130. package/packages/piagent-core/managed/toolchain.mjs +120 -0
  131. package/packages/piagent-core/managed/web-fetch.mjs +105 -0
  132. package/packages/piagent-core/managed/web-search.mjs +96 -0
  133. package/packages/piagent-core/managed/workflow.mjs +299 -0
  134. package/packages/piagent-core/package.json +2 -5
  135. package/packages/piagent-core/policies/base-policy.json +80 -26
  136. package/packages/piagent-core/runtime/hooks/agent-start-hook.ts +3 -3
  137. package/packages/piagent-core/runtime/hooks/input-hook.ts +35 -196
  138. package/packages/piagent-core/runtime/hooks/session-start-hook.ts +2 -0
  139. package/packages/piagent-core/runtime/inspection/approval-broker.ts +9 -1
  140. package/packages/piagent-core/runtime/managed-failure.d.mts +10 -0
  141. package/packages/piagent-core/runtime/managed-failure.mjs +164 -0
  142. package/packages/piagent-core/runtime/orchestration/minimal-delegation-policy.ts +8 -29
  143. package/packages/piagent-core/runtime/orchestration/owned-work-budget.ts +1 -1
  144. package/packages/piagent-core/runtime/policy/block-remedy.ts +5 -3
  145. package/packages/piagent-core/runtime/registration/context-commands.ts +1 -1
  146. package/packages/piagent-core/runtime/registration/knowledge-tools.ts +7 -7
  147. package/packages/piagent-core/runtime/registration/policy-tools.ts +2 -2
  148. package/packages/piagent-core/runtime/registration/session-commands.ts +11 -26
  149. package/packages/piagent-core/runtime/registration/task-evidence-tools.ts +1 -1
  150. package/packages/piagent-core/runtime/session/system-prompt.ts +6 -7
  151. package/packages/piagent-core/runtime/workflows/webui-runtime-command.ts +6 -6
  152. package/packages/piagent-core/runtime/workflows/webui-workflow.ts +6 -11
  153. package/packages/piagent-core/security/sensitive-text.js +9 -0
  154. package/packages/piagent-core/skills/piagent-ops/SKILL.md +8 -10
  155. package/packages/piagent-core/skills/test-audit/SKILL.md +101 -0
  156. package/packages/piagent-core/subagents/piagent-planner.md +1 -1
  157. package/packages/piagent-webui/dist/client/assets/{MarkdownMessage-DMU7Xe_O.js → MarkdownMessage-Dd9wPk-v.js} +1 -1
  158. package/packages/piagent-webui/dist/client/assets/index-7I-50Q2U.js +228 -0
  159. package/packages/piagent-webui/dist/client/assets/{index-DVahUVWy.css → index-DgSmcnP8.css} +1 -1
  160. package/packages/piagent-webui/dist/client/index.html +2 -2
  161. package/packages/piagent-webui/extension/piagent-webui.ts +11 -11
  162. package/packages/piagent-webui/extension/session-stream-adapter.ts +3 -1
  163. package/packages/piagent-webui/extension/terminal-session-adapter.ts +6 -1
  164. package/packages/piagent-webui/gateway/company-relay.ts +308 -0
  165. package/packages/piagent-webui/gateway/control-socket.ts +39 -12
  166. package/packages/piagent-webui/gateway/gateway-service.ts +109 -28
  167. package/packages/piagent-webui/gateway/gateway-session-stream.ts +58 -2
  168. package/packages/piagent-webui/gateway/managed-gateway.mjs +41 -0
  169. package/packages/piagent-webui/gateway/managed-launch.ts +145 -0
  170. package/packages/piagent-webui/gateway/managed-projection.ts +20 -0
  171. package/packages/piagent-webui/gateway/pi-host.ts +22 -4
  172. package/packages/piagent-webui/gateway/runtime-session-controls.ts +5 -2
  173. package/packages/piagent-webui/gateway/session-approval-authority.ts +28 -0
  174. package/packages/piagent-webui/gateway/session-catalog.ts +11 -3
  175. package/packages/piagent-webui/gateway/session-command-controller.ts +8 -2
  176. package/packages/piagent-webui/gateway/session-file-facts.ts +25 -0
  177. package/packages/piagent-webui/gateway/session-inspection-registry.ts +40 -9
  178. package/packages/piagent-webui/gateway/session-list-cache.ts +68 -0
  179. package/packages/piagent-webui/gateway/session-runtime-ownership.ts +8 -1
  180. package/packages/piagent-webui/gateway/session-runtime-supervisor.ts +20 -27
  181. package/packages/piagent-webui/ownership/managed-terminal-lease.ts +27 -0
  182. package/packages/piagent-webui/ownership/profile-state.ts +2 -0
  183. package/packages/piagent-webui/ownership/session-lease-store.ts +22 -1
  184. package/packages/piagent-webui/server/approval-projection.ts +9 -0
  185. package/packages/piagent-webui/server/core-inspection-provider.ts +4 -9
  186. package/packages/piagent-webui/server/loopback-server.ts +37 -1
  187. package/packages/piagent-webui/server/message-stop-reason.ts +11 -0
  188. package/packages/piagent-webui/server/tool-call-summary.ts +185 -0
  189. package/packages/piagent-webui/server/transcript-projection.ts +86 -3
  190. package/packs/engineering-base/pack.json +4 -0
  191. package/schemas/piagent-webui/runtime-command-v1.schema.json +1 -2
  192. package/schemas/piagent-webui/session-catalog-v1.schema.json +8 -0
  193. package/schemas/piagent-webui/transcript-v1.schema.json +94 -1
  194. package/scripts/build-docs-site.mjs +23 -12
  195. package/scripts/check-public-wording.mjs +5 -6
  196. package/scripts/check-runtime-advisories.mjs +23 -6
  197. package/scripts/claude-model-additions.mjs +33 -0
  198. package/scripts/configure-model-scope.sh +30 -6
  199. package/scripts/configure-subagents.sh +1 -1
  200. package/scripts/explain-command.mjs +5 -6
  201. package/scripts/model-scope-prune.mjs +75 -0
  202. package/scripts/phase-tools-evaluation.mjs +9 -4
  203. package/scripts/piagent-cli.mjs +51 -2
  204. package/scripts/piagent-dashboard.mjs +27 -4
  205. package/scripts/piagent-installations.mjs +26 -0
  206. package/scripts/piagent-studio.mjs +155 -0
  207. package/scripts/runtime-advisory-exceptions.json +29 -0
  208. package/scripts/team-doctor.sh +20 -1
  209. package/scripts/typescript-loader.mjs +6 -0
  210. package/scripts/verify-local.sh +2 -2
  211. package/templates/global/settings.json +3 -1
  212. package/templates/project/.pi/project-context.md +3 -3
  213. package/templates/project/AGENTS.md +4 -5
  214. package/docs/piagent-core-reasoning-research-2026-09-06.md +0 -225
  215. package/docs/piagent-core-research-sources-2026-09-06.json +0 -416
  216. package/packages/piagent-webui/dist/client/assets/index-Cui9AAV_.js +0 -176
@@ -55,19 +55,25 @@ const NAV = [
55
55
  href: "/",
56
56
  nav: "Overview",
57
57
  title: "Pi Agent Platform",
58
- lead: "Nền tảng dùng lại cho nhiều dự án: cài đặt, profile, quyền chạy, MCP, bảo mật và workflow trong một tài liệu."
58
+ lead: "Coding agent cho cả team: một dashboard cho Terminal và trình duyệt, model cá nhân hoặc của công ty, guard kiểm tra mọi tool call."
59
+ },
60
+ {
61
+ slug: "ecosystem",
62
+ nav: "Hệ sinh thái",
63
+ title: "Piagent, Agent Watch và Agent Studio",
64
+ lead: "Một dashboard, hai cách dùng model: tài khoản của bạn, hoặc model của công ty qua Agent Watch và Agent Studio, với Harness tự kiểm tra từng lượt."
59
65
  },
60
66
  {
61
67
  slug: "quickstart",
62
68
  nav: "Quickstart",
63
69
  title: "Step by step cho thành viên mới",
64
- lead: "Hai lệnh để cài, rồi mở repo, chọn profile, bật quyền phù hợp, chạy task, và verify bằng bằng chứng thật trước khi handoff."
70
+ lead: "Hai lệnh để cài, mở repo, chọn profile và quyền, rồi giao việc bằng lời thường; guard kiểm tra từng tool call."
65
71
  },
66
72
  {
67
73
  slug: "whats-new",
68
74
  nav: "Có gì mới",
69
- title: "Luồng task an toàn hơn, ít thao tác tay hơn",
70
- lead: "Product stabilization bổ sung runtime truth, trajectory, recovery, helper policy, activity inspector và completion receipt fail-closed mà không phá workflow cũ."
75
+ title: "Bản 1.9.0: nói việc bằng lời, Harness của team kiểm tra",
76
+ lead: "Bỏ task contract và lệnh workflow. Chế độ công ty tự yêu cầu check và review; nút Dừng, lệnh có mạng theo lần duyệt và thẻ duyệt dễ đọc hơn."
71
77
  },
72
78
  {
73
79
  slug: "scope",
@@ -102,7 +108,7 @@ const NAV = [
102
108
  slug: "workflows",
103
109
  nav: "Workflows",
104
110
  title: "Workflow thay đổi thế nào sau update",
105
- lead: "Workflow launcher cũ vẫn giữ nguyên. Runtime lifecycle và live inspector chạy tự động; command mới chỉ giúp quan sát preflight, diff, tiến độ, receipt, hiệu suất và orchestration policy."
111
+ lead: "Từ bản 1.9.0 không còn launcher workflow: nói việc cần làm bằng lời thường. Các command xem thêm chỉ để quan sát và không bao giờ mở lượt model."
106
112
  }
107
113
  ]
108
114
  },
@@ -142,7 +148,7 @@ const NAV = [
142
148
  slug: "security",
143
149
  nav: "Security model",
144
150
  title: "Policy enforcement layer, không phải OS sandbox",
145
- lead: "Platform ưu tiên fail-closed trong bề mặt tool được kiểm soát: khi lock sai, profile sai hoặc bằng chứng thiếu thì chặn, không đoán."
151
+ lead: "Platform ưu tiên fail-closed trong bề mặt tool được kiểm soát: khi lock sai, profile sai hoặc quyền không đủ thì chặn, không đoán."
146
152
  },
147
153
  {
148
154
  slug: "architecture",
@@ -182,17 +188,22 @@ const EN_PAGE_COPY = {
182
188
  index: {
183
189
  nav: "Overview",
184
190
  title: "Pi Agent Platform",
185
- lead: "Reusable infrastructure for multiple projects: installation, profiles, permissions, MCP, security, and workflows in one place."
191
+ lead: "A coding agent for the whole team: one dashboard for Terminal and the browser, personal or company models, and a guard that checks every tool call."
192
+ },
193
+ ecosystem: {
194
+ nav: "Ecosystem",
195
+ title: "Piagent, Agent Watch and Agent Studio",
196
+ lead: "One dashboard, two ways to use models: your own account, or the company's models through Agent Watch and Agent Studio, with a Harness that checks every turn."
186
197
  },
187
198
  quickstart: {
188
199
  nav: "Quickstart",
189
200
  title: "Step by step for new team members",
190
- lead: "Install with two commands, open a repository, select its profile and permission mode, run a task, and verify real evidence before handoff."
201
+ lead: "Install with two commands, open a repository, choose its profile and permissions, then ask in plain language; the guard checks every tool call."
191
202
  },
192
203
  "whats-new": {
193
204
  nav: "What’s new",
194
- title: "Safer task flow with less manual coordination",
195
- lead: "Product stabilization adds runtime truth, trajectories, recovery, helper policy, an activity inspector, and fail-closed completion receipts without breaking existing workflows."
205
+ title: "1.9.0: ask in plain language, the team's Harness checks",
206
+ lead: "The task contract and workflow commands are gone. Company mode asks for checks and a review on its own; Stop, network commands on approval and clearer approval cards."
196
207
  },
197
208
  scope: {
198
209
  nav: "Platform scope",
@@ -217,7 +228,7 @@ const EN_PAGE_COPY = {
217
228
  workflows: {
218
229
  nav: "Workflows",
219
230
  title: "How workflows change after the update",
220
- lead: "Existing workflow launchers stay unchanged. The runtime lifecycle and live inspector update automatically, while commands expose preflight, diffs, progress, receipts, efficiency, and orchestration policy."
231
+ lead: "Since 1.9.0 there are no workflow launchers: say what you need in plain language. The optional inspection commands only observe and never start a model turn."
221
232
  },
222
233
  mcp: {
223
234
  nav: "MCP overview",
@@ -242,7 +253,7 @@ const EN_PAGE_COPY = {
242
253
  security: {
243
254
  nav: "Security model",
244
255
  title: "Policy enforcement, not an OS sandbox",
245
- lead: "Controlled tool surfaces fail closed when profiles, integrity locks, permissions, or verification evidence are invalid."
256
+ lead: "Controlled tool surfaces fail closed when profiles, integrity locks or permissions are invalid."
246
257
  },
247
258
  architecture: {
248
259
  nav: "Architecture",
@@ -7,10 +7,10 @@ const root = path.resolve(import.meta.dirname, "..");
7
7
  const surfaces = ["README.md", "docs", "docs-site/content", "scripts/build-docs-site.mjs",
8
8
  "packages/piagent-core/README.md", "packages/piagent-core/prompts", "packages/piagent-core/mcp",
9
9
  "packages/piagent-core/extensions", "packages/piagent-core/runtime", "templates/project/AGENTS.md"];
10
- // Explicit reviewed research records, never a marker supplied by arbitrary docs.
11
- const researchRecords = new Set(["docs/piagent-core-improvement-plan.md",
12
- "docs/piagent-core-reasoning-research-2026-09-06.md", "docs/piagent-core-research-sources-2026-09-06.json"]);
13
10
  const sourceNames = ["Codex CLI", "Claude CLI", "Claude Code"];
11
+ // Other agent products and projects. Public text describes a technique in its
12
+ // own words and names none of them; whole words only ("declined" is not one).
13
+ const otherAgents = /\b(?:Aider|SWE-agent|OpenHands|Oh My Pi|oh-my-pi|OpenClaw|OpenClaude|LangGraph|LangChain|Windsurf|Cline|Roo Code|Kilo Code|autoresearch)\b/g;
14
14
  const prohibited = ["platform-migration", "codex-migration", "codex-parity", "benchmark-parity",
15
15
  "harness-migration", "agent-stuff", "mitsuhiko", "claude mcp", "codex mcp",
16
16
  "Codex-inspired", "Codex-grade", "Pi vs Codex", "vs Claude", "reference repo",
@@ -18,9 +18,8 @@ const prohibited = ["platform-migration", "codex-migration", "codex-parity", "be
18
18
  const escaped = value => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
19
19
 
20
20
  export function publicWordingViolations(file, text) {
21
- const terms = researchRecords.has(file) ? prohibited : [...prohibited, ...sourceNames];
22
- const pattern = new RegExp(terms.map(escaped).join("|"), "gi");
23
- return [...text.matchAll(pattern)].map(match => ({ file,
21
+ const pattern = new RegExp([...prohibited, ...sourceNames].map(escaped).join("|"), "gi");
22
+ return [...text.matchAll(pattern), ...text.matchAll(otherAgents)].sort((a, b) => a.index - b.index).map(match => ({ file,
24
23
  line: text.slice(0, match.index).split("\n").length, keyword: match[0] }));
25
24
  }
26
25
 
@@ -4,9 +4,11 @@
4
4
  //
5
5
  // The rule is "no moderate, high, or critical advisory in the pinned Pi host
6
6
  // and add-on tree".
7
- // There is deliberately no advisory allowlist. A supported host release must
8
- // audit clean at these severities; changing that policy requires changing this
9
- // gate and its tests explicitly.
7
+ // The only exceptions are listed in runtime-advisory-exceptions.json, each an
8
+ // owner decision for one advisory at one package path in the tree, with a day
9
+ // it stops applying. The same advisory anywhere else, any other advisory, and
10
+ // an exception past its day all block. Its tests check every entry.
11
+ import fs from "node:fs";
10
12
 
11
13
  function readStdin() {
12
14
  return new Promise((resolve, reject) => {
@@ -43,7 +45,11 @@ if (report.auditReportVersion !== 2) {
43
45
  process.exit(1);
44
46
  }
45
47
 
46
- const blocking = [];
48
+ const { exceptions } = JSON.parse(fs.readFileSync(new URL("./runtime-advisory-exceptions.json", import.meta.url), "utf8"));
49
+ // A test seam for the day exceptions are judged on; the gate uses today (UTC).
50
+ const today = process.env.PIAGENT_ADVISORY_TODAY ?? new Date().toISOString().slice(0, 10);
51
+
52
+ const blocking = [], accepted = [];
47
53
  for (const [name, vulnerability] of Object.entries(report.vulnerabilities ?? {})) {
48
54
  if (!["moderate", "high", "critical"].includes(vulnerability.severity)) continue;
49
55
  const ids = advisoryIdsFor(vulnerability);
@@ -51,13 +57,22 @@ for (const [name, vulnerability] of Object.entries(report.vulnerabilities ?? {})
51
57
  blocking.push({ name, severity: vulnerability.severity, id: "(no advisory id)" });
52
58
  continue;
53
59
  }
60
+ const nodes = Array.isArray(vulnerability.nodes) ? vulnerability.nodes : [];
54
61
  for (const id of ids) {
55
- blocking.push({ name, severity: vulnerability.severity, id });
62
+ const exception = exceptions.find((entry) => entry.id === id && entry.package === name
63
+ && nodes.length > 0 && nodes.every((node) => node === entry.node));
64
+ if (exception && today <= exception.until) accepted.push({ name, severity: vulnerability.severity, id, exception });
65
+ else blocking.push({ name, severity: vulnerability.severity, id, expired: exception?.until });
56
66
  }
57
67
  }
58
68
 
69
+ for (const entry of accepted) {
70
+ console.log(`ACCEPTED: ${entry.severity} advisory ${entry.id} in ${entry.name} at ${entry.exception.node} until ${entry.exception.until} (owner decision ${entry.exception.decided})`);
71
+ }
72
+
59
73
  const failures = blocking.map(
60
74
  (entry) => `${entry.severity} advisory ${entry.id} in ${entry.name} blocks the supported runtime`
75
+ + (entry.expired ? ` (its exception ended ${entry.expired})` : "")
61
76
  );
62
77
 
63
78
  if (failures.length > 0) {
@@ -67,4 +82,6 @@ if (failures.length > 0) {
67
82
  }
68
83
 
69
84
  const counts = report.metadata?.vulnerabilities ?? {};
70
- console.log(`PASS: no moderate, high, or critical advisory (${counts.moderate ?? 0} moderate, ${counts.high ?? 0} high, ${counts.critical ?? 0} critical)`);
85
+ console.log(accepted.length
86
+ ? `PASS: no unaccepted moderate, high, or critical advisory (${accepted.length} accepted, each until its end day)`
87
+ : `PASS: no moderate, high, or critical advisory (${counts.moderate ?? 0} moderate, ${counts.high ?? 0} high, ${counts.critical ?? 0} critical)`);
@@ -0,0 +1,33 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ const additions = JSON.parse(fs.readFileSync(new URL("../catalog/claude-model-additions.json", import.meta.url), "utf8"));
5
+ const object = value => value !== null && typeof value === "object" && !Array.isArray(value);
6
+
7
+ // Explicit model-scope setup only. Keep existing models, endpoints and auth;
8
+ // this metadata is not evidence that an account has access to a model.
9
+ export function prepareClaudeModelAdditions(file) {
10
+ if (fs.existsSync(file) && (!fs.lstatSync(file).isFile() || fs.lstatSync(file).isSymbolicLink())) throw new Error("Unsafe model configuration path");
11
+ const before = fs.existsSync(file) ? fs.readFileSync(file, "utf8") : null;
12
+ const data = before === null ? {} : JSON.parse(before);
13
+ if (!object(data) || (data.providers !== undefined && !object(data.providers))) throw new Error("Unsupported model configuration");
14
+ data.providers ??= {};
15
+ const provider = data.providers.anthropic ?? {};
16
+ if (!object(provider) || (provider.models !== undefined && !Array.isArray(provider.models))) throw new Error("Unsupported Anthropic model configuration");
17
+ // A custom endpoint is controlled by its operator; Studio supplies its own catalog.
18
+ if (provider.baseUrl && provider.baseUrl !== "https://api.anthropic.com") return null;
19
+ const models = provider.models ?? [];
20
+ const missing = additions.models.filter(model => !models.some(existing => existing.id === model.id));
21
+ if (missing.length === 0) return null;
22
+ data.providers.anthropic = { ...provider, models: [...models, ...missing] };
23
+ return { apply() {
24
+ const current = fs.existsSync(file) ? fs.readFileSync(file, "utf8") : null;
25
+ if (current !== before) throw new Error("Model configuration changed during setup");
26
+ fs.mkdirSync(path.dirname(file), { recursive: true });
27
+ const temporary = `${file}.piagent-${process.pid}`;
28
+ try {
29
+ fs.writeFileSync(temporary, `${JSON.stringify(data, null, 2)}\n`, { mode: 0o600, flag: "wx" });
30
+ fs.renameSync(temporary, file);
31
+ } finally { if (fs.existsSync(temporary)) fs.unlinkSync(temporary); }
32
+ } };
33
+ }
@@ -12,6 +12,8 @@ Options:
12
12
  Default model (default: openai-codex/gpt-6-sol:high)
13
13
  --settings <path> Pi settings file (default: ~/.pi/agent/settings.json)
14
14
  --dry-run Print resulting settings JSON without writing
15
+ --prune Only remove enabledModels patterns that match no model
16
+ Pi knows (Pi warns about each on every start); keep the rest
15
17
  -h, --help
16
18
 
17
19
  Purpose:
@@ -33,6 +35,7 @@ PRESET="full"
33
35
  DEFAULT_MODEL="openai-codex/gpt-6-sol:high"
34
36
  SETTINGS_PATH="${PI_CODING_AGENT_DIR:-"${HOME}/.pi/agent"}/settings.json"
35
37
  DRY_RUN=false
38
+ PRUNE=false
36
39
 
37
40
  # A flag whose value is missing swallows the next flag instead. `--settings
38
41
  # --dry-run` used to set the settings path to the string "--dry-run", leave
@@ -68,6 +71,10 @@ while [[ $# -gt 0 ]]; do
68
71
  DRY_RUN=true
69
72
  shift
70
73
  ;;
74
+ --prune)
75
+ PRUNE=true
76
+ shift
77
+ ;;
71
78
  -h|--help)
72
79
  usage
73
80
  exit 0
@@ -80,6 +87,13 @@ while [[ $# -gt 0 ]]; do
80
87
  esac
81
88
  done
82
89
 
90
+ if [[ "$PRUNE" == true ]]; then
91
+ if [[ "$DRY_RUN" == true ]]; then
92
+ exec node "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/model-scope-prune.mjs" --settings "$SETTINGS_PATH"
93
+ fi
94
+ exec node "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/model-scope-prune.mjs" --settings "$SETTINGS_PATH" --fix
95
+ fi
96
+
83
97
  case "$PRESET" in
84
98
  full|codex|claude) ;;
85
99
  *)
@@ -88,17 +102,17 @@ case "$PRESET" in
88
102
  ;;
89
103
  esac
90
104
 
91
- node --input-type=module - "$SETTINGS_PATH" "$PRESET" "$DEFAULT_MODEL" "$DRY_RUN" <<'NODE'
105
+ PIAGENT_SCOPE_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" node --input-type=module - "$SETTINGS_PATH" "$PRESET" "$DEFAULT_MODEL" "$DRY_RUN" <<'NODE'
92
106
  import fs from "node:fs";
93
107
  import path from "node:path";
108
+ import { pathToFileURL } from "node:url";
109
+ const { prepareClaudeModelAdditions } = await import(pathToFileURL(process.env.PIAGENT_SCOPE_ROOT + "/scripts/claude-model-additions.mjs"));
94
110
 
95
111
  const [settingsPath, preset, defaultModelInput, dryRunRaw] = process.argv.slice(2);
96
112
  const dryRun = dryRunRaw === "true";
97
113
 
98
114
  const codexModels = [
99
115
  "openai-codex/gpt-5.3-codex-spark:minimal",
100
- "openai-codex/gpt-5.4-mini:minimal",
101
- "openai-codex/gpt-5.4:xhigh",
102
116
  "openai-codex/gpt-5.5:high",
103
117
  "openai-codex/gpt-5.6-luna:medium",
104
118
  "openai-codex/gpt-5.6-terra:high",
@@ -115,6 +129,8 @@ const claudeModels = [
115
129
  "anthropic/claude-sonnet-4-5:high",
116
130
  "anthropic/claude-sonnet-4-6:max",
117
131
  "anthropic/claude-sonnet-5:xhigh",
132
+ "anthropic/claude-sonnet-5-5:high",
133
+ "anthropic/claude-opus-5-5:high",
118
134
  "anthropic/claude-opus-4-5:xhigh",
119
135
  "anthropic/claude-opus-4-6:max",
120
136
  "anthropic/claude-opus-4-7:xhigh",
@@ -141,21 +157,29 @@ function parseDefaultModel(input) {
141
157
  }
142
158
 
143
159
  const parsedDefault = parseDefaultModel(defaultModelInput);
144
- const settings = fs.existsSync(settingsPath)
145
- ? JSON.parse(fs.readFileSync(settingsPath, "utf8"))
146
- : {};
160
+ const settingsBefore = fs.existsSync(settingsPath) ? fs.readFileSync(settingsPath, "utf8") : null;
161
+ const settings = settingsBefore === null ? {} : JSON.parse(settingsBefore);
147
162
 
148
163
  settings.defaultProvider = parsedDefault.provider;
149
164
  settings.defaultModel = parsedDefault.model;
150
165
  settings.defaultThinkingLevel = parsedDefault.thinking;
151
166
  settings.enabledModels = enabledModels;
152
167
 
168
+ const catalogEdit = preset === "codex" ? null : prepareClaudeModelAdditions(path.join(path.dirname(settingsPath), "models.json"));
153
169
  const output = `${JSON.stringify(settings, null, 2)}\n`;
154
170
  if (dryRun) {
155
171
  process.stdout.write(output);
156
172
  } else {
157
173
  fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
158
174
  fs.writeFileSync(settingsPath, output);
175
+ try { if (catalogEdit) catalogEdit.apply(); }
176
+ catch (error) {
177
+ if (fs.readFileSync(settingsPath, "utf8") === output) {
178
+ if (settingsBefore === null) fs.unlinkSync(settingsPath);
179
+ else fs.writeFileSync(settingsPath, settingsBefore);
180
+ }
181
+ throw error;
182
+ }
159
183
  console.log(`Configured Pi model scope: ${settingsPath}`);
160
184
  console.log(` preset: ${preset}`);
161
185
  console.log(` default: ${parsedDefault.provider}/${parsedDefault.model}:${parsedDefault.thinking}`);
@@ -27,7 +27,7 @@ After installing/configuring:
27
27
  pi
28
28
  /subagents-doctor
29
29
  /subagents-models
30
- # Piagent stays parent-direct unless runtime evidence proves >=30% net token saving.
30
+ # Piagent delegates useful bounded read-only work without a token-saving threshold.
31
31
  USAGE
32
32
  }
33
33
 
@@ -67,19 +67,18 @@ function parseArgs(argv) {
67
67
  // What the operator should do next. The guard states 74 refusals and only three
68
68
  // of them say this, which is why a denial so often ends the conversation.
69
69
  const REMEDY = {
70
- "protected-path": "Use an approved context projection if one is available. Direct access requires the operator to change the protected-path policy; task scope alone does not override it.",
70
+ "protected-path": "This path is protected because it can hold credentials. Open it yourself or share the non-secret part; only the operator can change the protected-path policy, and no request or scope overrides it.",
71
71
  "protected-glob": "Name the file you mean instead of a pattern; a pattern is refused when any name it can match is protected.",
72
72
  "resolved-protected": "The path resolves to a protected file through a link or a parent directory. Use the real path you intend.",
73
73
  "unresolved": "Write the path out, or put the expansion in its own argument, so the guard can see what is being opened.",
74
74
  "exec-policy": "This is refused by an exec-policy rule, not by a path. Change the command, or ask the operator to run it.",
75
- "runtime-required": "Run the command through the current Pi session. Only that runtime owns the Task Contract, scope, permission, lifecycle, approval, and budget facts needed for a final decision."
75
+ "runtime-required": "Run the command through the current Pi session. Only that runtime owns the permission, approval, and budget facts needed for a final decision."
76
76
  };
77
77
 
78
+ // Task contracts, task scope and task lifecycle control were retired on
79
+ // 2026-09-30; the live guard no longer applies them.
78
80
  const RUNTIME_GATES = [
79
- "task-contract",
80
- "task-scope",
81
81
  "permission-profile",
82
- "lifecycle-control",
83
82
  "approval-state",
84
83
  "context-budget"
85
84
  ];
@@ -144,7 +143,7 @@ function explain(command, protectedPaths, policy, cwd) {
144
143
  staticDecision,
145
144
  reason: staticDecision === "confirm"
146
145
  ? `Static preflight requires confirmation (${exec.reasons.join("; ")}), but the live runtime may still block it.`
147
- : "Static preflight found no refusal. A final decision requires live session and task state.",
146
+ : "Static preflight found no refusal. A final decision requires live session state.",
148
147
  remainingGates: RUNTIME_GATES,
149
148
  steps, exec
150
149
  };
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env node
2
+ // Patterns in Pi's enabledModels that match no model Pi knows. Pi prints
3
+ // "Warning: No models match pattern" for each one on every start, and a model
4
+ // that left the catalog never comes back by itself. Matching uses Pi's own
5
+ // resolver over every model Pi knows (built-in and models.json), signed in or
6
+ // not, so a provider that is only signed out keeps its patterns.
7
+ //
8
+ // Usage: model-scope-prune.mjs [--settings <path>] [--fix] [--json]
9
+ import fs from "node:fs";
10
+ import path from "node:path";
11
+ import { execFileSync } from "node:child_process";
12
+ import { pathToFileURL } from "node:url";
13
+
14
+ // The Pi install `pi` runs from.
15
+ export function piPackageRoot(environment = process.env) {
16
+ try {
17
+ const bin = execFileSync("/usr/bin/which", ["pi"], { encoding: "utf8", env: environment }).trim();
18
+ let dir = path.dirname(fs.realpathSync(bin));
19
+ for (let depth = 0; depth < 4; depth += 1, dir = path.dirname(dir)) {
20
+ const manifest = path.join(dir, "package.json");
21
+ if (fs.existsSync(manifest) && JSON.parse(fs.readFileSync(manifest, "utf8")).name === "@earendil-works/pi-coding-agent") return dir;
22
+ }
23
+ } catch { /* no pi on PATH */ }
24
+ return null;
25
+ }
26
+
27
+ // Every model Pi knows, without network or credentials.
28
+ export async function knownModels(piRoot, agentDir) {
29
+ const { ModelRuntime } = await import(pathToFileURL(path.join(piRoot, "dist/core/model-runtime.js")).href);
30
+ const { InMemoryCredentialStore } = await import(pathToFileURL(path.join(piRoot, "dist/index.js")).href).catch(() => ({}));
31
+ const previous = process.env.PI_OFFLINE; process.env.PI_OFFLINE = "1";
32
+ try {
33
+ const runtime = await ModelRuntime.create({ refreshOnCreate: false, modelsPath: path.join(agentDir, "models.json"),
34
+ ...(InMemoryCredentialStore ? { credentials: new InMemoryCredentialStore() } : {}) });
35
+ return runtime.getProviders().flatMap((provider) => runtime.getModels(provider.id ?? provider));
36
+ } finally { if (previous === undefined) delete process.env.PI_OFFLINE; else process.env.PI_OFFLINE = previous; }
37
+ }
38
+
39
+ export async function stalePatterns(patterns, models, piRoot) {
40
+ const { resolveModelScopeFromModels } = await import(pathToFileURL(path.join(piRoot, "dist/core/model-resolver.js")).href);
41
+ const { diagnostics } = resolveModelScopeFromModels(patterns, models);
42
+ return [...new Set(diagnostics.filter((item) => item.code === "no-match").map((item) => item.pattern))];
43
+ }
44
+
45
+ // Removes only the stale patterns; everything else in the file stays as written.
46
+ export function pruneSettings(settingsPath, stale) {
47
+ const text = fs.readFileSync(settingsPath, "utf8"), settings = JSON.parse(text);
48
+ const kept = settings.enabledModels.filter((pattern) => !stale.includes(pattern));
49
+ const next = `${JSON.stringify({ ...settings, enabledModels: kept }, null, 2)}\n`;
50
+ const temporary = `${settingsPath}.${process.pid}.tmp`;
51
+ fs.writeFileSync(temporary, next, { mode: fs.statSync(settingsPath).mode & 0o777 });
52
+ fs.renameSync(temporary, settingsPath);
53
+ return kept.length;
54
+ }
55
+
56
+ if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(new URL(import.meta.url).pathname)) {
57
+ const args = process.argv.slice(2), at = args.indexOf("--settings");
58
+ const agentDir = process.env.PI_CODING_AGENT_DIR || path.join(process.env.HOME ?? "", ".pi", "agent");
59
+ const settingsPath = at >= 0 && args[at + 1] ? path.resolve(args[at + 1]) : path.join(agentDir, "settings.json");
60
+ const json = args.includes("--json"), fix = args.includes("--fix");
61
+ const out = (value, text) => process.stdout.write(json ? `${JSON.stringify(value)}\n` : `${text}\n`);
62
+ const settings = fs.existsSync(settingsPath) ? JSON.parse(fs.readFileSync(settingsPath, "utf8")) : {};
63
+ const patterns = Array.isArray(settings.enabledModels) ? settings.enabledModels.filter((item) => typeof item === "string") : [];
64
+ const piRoot = piPackageRoot();
65
+ if (!piRoot) { out({ status: "unavailable", reason: "pi-not-found" }, "Pi is not on PATH; nothing checked."); process.exit(0); }
66
+ if (patterns.length === 0) { out({ status: "ok", stale: [] }, "enabledModels is empty; nothing to prune."); process.exit(0); }
67
+ const stale = await stalePatterns(patterns, await knownModels(piRoot, path.dirname(settingsPath)), piRoot);
68
+ if (stale.length === 0) { out({ status: "ok", stale: [] }, `All ${patterns.length} enabledModels patterns match a model Pi knows.`); process.exit(0); }
69
+ if (!fix) {
70
+ out({ status: "stale", stale }, `${stale.length} enabledModels pattern(s) match no model Pi knows:\n${stale.map((item) => ` - ${item}`).join("\n")}\nRun with --fix to remove only these.`);
71
+ process.exit(0);
72
+ }
73
+ const kept = pruneSettings(settingsPath, stale);
74
+ out({ status: "pruned", stale, kept }, `Removed ${stale.length} pattern(s) from ${settingsPath}; ${kept} kept.`);
75
+ }
@@ -106,13 +106,16 @@ for (const [name, tool] of tools) {
106
106
  }
107
107
  const duplicateDescriptions = [...descriptions.values()].filter((names) => names.length > 1);
108
108
  const replay = lanes.map(replayLane);
109
+ // The task contract was retired on 2026-09-30: these evidence tools are no longer
110
+ // registered, so no phase may offer them and the runtime keeps only its hooks.
111
+ const p2BaselineRetired = lanes.every((lane) => lane.p2.every((name) => !tools.has(name)));
109
112
  const runtimeOwnedEvidenceTools = ["piagent_context_record", "piagent_verify_record", "piagent_task_gate_check", "piagent_trace_record"];
110
113
  const checks = [
111
114
  { id: "read-only-no-direct-mutator", passed: phaseRows.filter((row) => row.lane === "read-only").every((row) => row.piagentTools.every((tool) => !PIAGENT_MUTATION_CAPABLE_TOOLS.has(tool)) && row.requiredHostTools.every((tool) => !["edit", "write", "apply_patch"].includes(tool))) },
112
115
  { id: "review-carrier-checked-shell-retained", passed: phaseRows.filter((row) => row.phase === "review").every((row) => row.piagentTools.every((tool) => !PIAGENT_MUTATION_CAPABLE_TOOLS.has(tool)) && row.requiredHostTools.includes("bash") && row.requiredHostTools.every((tool) => !["edit", "write", "apply_patch"].includes(tool))) },
113
116
  { id: "verify-surface-complete", passed: phaseRows.filter((row) => row.phase === "verify").every((row) => row.requiredHostTools.includes("bash") && runtimeOwnedEvidenceTools.every((tool) => !row.piagentTools.includes(tool))) },
114
117
  { id: "handoff-surface-complete", passed: phaseRows.filter((row) => row.phase === "handoff").every((row) => row.requiredHostTools.includes("read") && runtimeOwnedEvidenceTools.every((tool) => !row.piagentTools.includes(tool))) },
115
- { id: "runtime-evidence-surface-complete", passed: runtimeOwnedEvidenceTools.every((tool) => tools.has(tool)) && ["tool_result", "message_end"].every((hook) => hooks.has(hook)) },
118
+ { id: "retired-evidence-tools-absent", passed: runtimeOwnedEvidenceTools.every((tool) => !tools.has(tool)) && ["tool_result", "message_end"].every((hook) => hooks.has(hook)) },
116
119
  { id: "high-risk-review-retained", passed: lanes.find((lane) => lane.id === "high-risk").phases.includes("review") },
117
120
  { id: "duplicate-descriptions-zero", passed: duplicateDescriptions.length === 0 },
118
121
  { id: "trajectory-replay-100-percent", passed: replay.every((item) => item.deterministic && item.finalPhase === "terminal") }
@@ -202,9 +205,11 @@ const report = {
202
205
  evaluatedTurns: phaseRows.length,
203
206
  p2WeightedSchemaBytes: p2WeightedBytes,
204
207
  phaseWeightedSchemaBytes: phaseWeightedBytes,
205
- schemaReduction: Number(reduction.toFixed(4)),
206
- schemaReductionPercent: Number((reduction * 100).toFixed(2)),
207
- schemaReductionSemantics: "counterfactual-intended-surface-only",
208
+ // Every P2 baseline tool was retired with the task contract, so there is no
209
+ // baseline left to compare against; null says so instead of a meaningless ratio.
210
+ schemaReduction: p2BaselineRetired ? null : Number(reduction.toFixed(4)),
211
+ schemaReductionPercent: p2BaselineRetired ? null : Number((reduction * 100).toFixed(2)),
212
+ schemaReductionSemantics: p2BaselineRetired ? "counterfactual-baseline-retired" : "counterfactual-intended-surface-only",
208
213
  runtimeContract: { strict: strictContract, shadow: shadowContract },
209
214
  duplicateDescriptions,
210
215
  missingToolEvents: checks.filter((check) => !check.passed && check.id.includes("surface")).length,
@@ -36,7 +36,10 @@ let script = scriptByCommand[invokedAs];
36
36
 
37
37
  if (invokedAs === "piagent") {
38
38
  const subcommand = forwardedArgs[0];
39
- if (subcommand === "dashboard") {
39
+ if (subcommand === "studio") {
40
+ script = "scripts/piagent-studio.mjs";
41
+ forwardedArgs = forwardedArgs.slice(1);
42
+ } else if (subcommand === "dashboard") {
40
43
  script = "scripts/piagent-dashboard.mjs";
41
44
  forwardedArgs = forwardedArgs.slice(1);
42
45
  } else if (subcommand === "explain") {
@@ -56,6 +59,7 @@ if (invokedAs === "piagent") {
56
59
  console.log("");
57
60
  console.log("Commands:");
58
61
  console.log(" dashboard Open and manage the local session hub");
62
+ console.log(" studio Open the managed company agent imported by Agent Watch");
59
63
  console.log(" explain Say why the guard would allow or block a shell command");
60
64
  console.log(" approve-verification Preview or approve an independent verification plan");
61
65
  console.log(" select-verification Preview reusable contracts for exact task criteria");
@@ -87,6 +91,51 @@ function benchmarkSourceRoot() {
87
91
  process.exit(1);
88
92
  }
89
93
 
94
+ // The Pi host this member runs: the `pi` found on PATH, as its package root.
95
+ function piHost() {
96
+ for (const dir of (process.env.PATH ?? "").split(path.delimiter)) {
97
+ if (!path.isAbsolute(dir)) continue;
98
+ let root;
99
+ try { root = path.dirname(fs.realpathSync(path.join(dir, "pi"))); } catch { continue; }
100
+ for (let depth = 0; depth < 4; depth += 1, root = path.dirname(root)) {
101
+ try { if (JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8")).name === "@earendil-works/pi-coding-agent") return root; } catch { /* keep walking up */ }
102
+ }
103
+ }
104
+ return null;
105
+ }
106
+
107
+ // Agent Watch binds the company runtime to the Piagent this member actually
108
+ // runs (nvm, Volta, Homebrew, a custom npm prefix…): record the installed
109
+ // entrypoint, the Node running it and the Pi host. Source checkouts never
110
+ // register, and a failure here never blocks the command. Asking for help or the
111
+ // version changes nothing in the operator's agent directory.
112
+ function recordRuntime() {
113
+ try {
114
+ if (forwardedArgs.some((value) => ["--help", "-h", "--version", "-v"].includes(value))) return;
115
+ if (!packageRoot.split(path.sep).includes("node_modules")) return;
116
+ // An install under the OS temporary directory (a packaging test, a scratch
117
+ // prefix) is about to disappear; recording it would point Agent Watch at
118
+ // an entrypoint that no longer exists.
119
+ const osTemp = fs.realpathSync.native(os.tmpdir()), installRoot = fs.realpathSync.native(packageRoot);
120
+ if (installRoot === osTemp || installRoot.startsWith(`${osTemp}${path.sep}`)) return;
121
+ const agentDir = path.resolve(process.env.PI_CODING_AGENT_DIR || path.join(os.homedir(), ".pi", "agent"));
122
+ if (!fs.statSync(agentDir).isDirectory()) return;
123
+ const body = `${JSON.stringify({
124
+ schema_version: 1,
125
+ version: JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "utf8")).version,
126
+ entrypoint: fs.realpathSync(path.join(packageRoot, "scripts", "piagent-studio.mjs")),
127
+ node: fs.realpathSync(process.execPath),
128
+ pi_sdk_root: piHost()
129
+ }, null, 2)}\n`;
130
+ const file = path.join(agentDir, "piagent-runtime.json");
131
+ try { if (fs.readFileSync(file, "utf8") === body) return; } catch { /* first run */ }
132
+ const temporary = `${file}.${process.pid}.tmp`;
133
+ fs.writeFileSync(temporary, body, { mode: 0o600 });
134
+ fs.renameSync(temporary, file);
135
+ } catch { /* optional: Agent Watch falls back to known install locations */ }
136
+ }
137
+ recordRuntime();
138
+
90
139
  const sourceRoot = invokedAs === "piagent-benchmark" ? benchmarkSourceRoot() : packageRoot;
91
140
  const target = path.join(sourceRoot, script);
92
141
  const runner = target.endsWith(".mjs") ? process.execPath : "bash";
@@ -95,7 +144,7 @@ const runnerArgs = target.endsWith(".mjs")
95
144
  : [target];
96
145
  const child = spawn(runner, [...runnerArgs, ...forwardedArgs], {
97
146
  cwd: process.cwd(),
98
- env: process.env,
147
+ env: script === "scripts/piagent-studio.mjs" ? { PATH: "/usr/bin:/bin", HOME: os.homedir(), TERM: process.env.TERM || "xterm-256color", LANG: "en_US.UTF-8" } : process.env,
99
148
  stdio: "inherit"
100
149
  });
101
150
 
@@ -10,6 +10,7 @@ import { installedPiHostRoot } from "../packages/piagent-webui/gateway/pi-host.t
10
10
  import { gatewayProfileState, profileRef, readGatewayDescriptor, readOrCreateCatalogKey,
11
11
  removeGatewayDescriptor } from "../packages/piagent-webui/gateway/profile-state.ts";
12
12
  import { startPiagentGateway } from "../packages/piagent-webui/gateway/gateway-service.ts";
13
+ import { agentWatchCompanyConnector } from "../packages/piagent-webui/gateway/managed-launch.ts";
13
14
 
14
15
  const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
15
16
  const packageJson = JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "utf8"));
@@ -111,9 +112,9 @@ async function control(action, timeoutMs = 1_500) {
111
112
  return await requestGatewayControl(state.controlSocket, { action }, timeoutMs);
112
113
  }
113
114
 
114
- async function currentLaunchUrl() {
115
+ async function currentLaunchUrl(timeoutMs) {
115
116
  try {
116
- const response = await control("issue-launch-url");
117
+ const response = await control("issue-launch-url", timeoutMs);
117
118
  if (response.ok && response.value && typeof response.value === "object"
118
119
  && typeof response.value.launchUrl === "string") return response.value.launchUrl;
119
120
  } catch {
@@ -122,6 +123,26 @@ async function currentLaunchUrl() {
122
123
  return null;
123
124
  }
124
125
 
126
+ // A Gateway that is alive but busy answers late. Starting another one beside
127
+ // it left two Gateways on the same conversations, the first unreachable and
128
+ // still serving its pages, so the running one is waited for instead.
129
+ async function waitForRunningGateway(timeoutMs = 30_000) {
130
+ const running = readGatewayDescriptor(gatewayProfileState(agentDir));
131
+ if (!running || !processAlive(running.pid)) return null;
132
+ const deadline = Date.now() + timeoutMs;
133
+ while (Date.now() < deadline && processAlive(running.pid)) {
134
+ try {
135
+ const response = await control("issue-launch-url", 5_000);
136
+ if (response.ok && typeof response.value?.launchUrl === "string") return response.value.launchUrl;
137
+ } catch (error) {
138
+ // Nothing listens on its socket: the process is not a Gateway anymore.
139
+ if (!(error instanceof Error) || error.message !== "gateway-control-timeout") return null;
140
+ }
141
+ }
142
+ if (!processAlive(running.pid)) return null;
143
+ throw new Error("gateway-busy\nThe Gateway is running but did not answer within 30s. Try again, or run `piagent dashboard restart`.");
144
+ }
145
+
125
146
  async function waitForGatewayStopped(previous, timeoutMs = 8_000) {
126
147
  const deadline = Date.now() + timeoutMs;
127
148
  while (Date.now() < deadline) {
@@ -134,7 +155,7 @@ async function waitForGatewayStopped(previous, timeoutMs = 8_000) {
134
155
  }
135
156
 
136
157
  async function ensureStarted() {
137
- const existing = await currentLaunchUrl();
158
+ const existing = await currentLaunchUrl() ?? await waitForRunningGateway();
138
159
  if (existing) return existing;
139
160
  const effectiveAgentDir = path.resolve(agentDir ?? process.env.PI_CODING_AGENT_DIR ?? path.join(os.homedir(), ".pi", "agent"));
140
161
  const state = gatewayProfileState(effectiveAgentDir);
@@ -168,7 +189,9 @@ async function ensureStarted() {
168
189
  }
169
190
 
170
191
  async function serve() {
171
- const gateway = await startPiagentGateway({ packageRoot, expectedPiVersion, agentDir });
192
+ // Company sessions come from the Agent Watch binding in this Pi folder.
193
+ const piAgentDir = path.resolve(agentDir ?? process.env.PI_CODING_AGENT_DIR ?? path.join(os.homedir(), ".pi", "agent"));
194
+ const gateway = await startPiagentGateway({ packageRoot, expectedPiVersion, agentDir, company: agentWatchCompanyConnector(piAgentDir) });
172
195
  const shutdown = () => { void gateway.close(); };
173
196
  process.once("SIGINT", shutdown);
174
197
  process.once("SIGTERM", shutdown);