@piagent/platform 1.8.0 → 1.9.1

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 (221) hide show
  1. package/AGENTS.md +7 -0
  2. package/CHANGELOG.md +132 -2
  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 +3 -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 +92 -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 +92 -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 +100 -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 +100 -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/client-agent.mjs +6 -0
  116. package/packages/piagent-core/managed/compaction.mjs +63 -0
  117. package/packages/piagent-core/managed/doctor.mjs +107 -0
  118. package/packages/piagent-core/managed/helper-roles.mjs +57 -0
  119. package/packages/piagent-core/managed/native-catalog.mjs +42 -0
  120. package/packages/piagent-core/managed/patch-snapshot.mjs +48 -0
  121. package/packages/piagent-core/managed/repository-operation.mjs +84 -0
  122. package/packages/piagent-core/managed/request-stream.mjs +130 -0
  123. package/packages/piagent-core/managed/resource-loader.mjs +42 -0
  124. package/packages/piagent-core/managed/review-snapshot-worker.mjs +27 -0
  125. package/packages/piagent-core/managed/sandbox-diagnostic.mjs +12 -0
  126. package/packages/piagent-core/managed/search-fallback.mjs +93 -0
  127. package/packages/piagent-core/managed/session.mjs +493 -0
  128. package/packages/piagent-core/managed/store.d.mts +3 -0
  129. package/packages/piagent-core/managed/store.mjs +64 -0
  130. package/packages/piagent-core/managed/tool-boundary.mjs +275 -0
  131. package/packages/piagent-core/managed/tool-worker.mjs +36 -0
  132. package/packages/piagent-core/managed/toolchain.mjs +120 -0
  133. package/packages/piagent-core/managed/web-fetch.mjs +105 -0
  134. package/packages/piagent-core/managed/web-search.mjs +131 -0
  135. package/packages/piagent-core/managed/workflow.mjs +326 -0
  136. package/packages/piagent-core/package.json +2 -5
  137. package/packages/piagent-core/policies/base-policy.json +80 -26
  138. package/packages/piagent-core/runtime/hooks/agent-start-hook.ts +3 -3
  139. package/packages/piagent-core/runtime/hooks/input-hook.ts +35 -196
  140. package/packages/piagent-core/runtime/hooks/session-start-hook.ts +2 -0
  141. package/packages/piagent-core/runtime/inspection/approval-broker.ts +9 -1
  142. package/packages/piagent-core/runtime/managed-failure.d.mts +10 -0
  143. package/packages/piagent-core/runtime/managed-failure.mjs +166 -0
  144. package/packages/piagent-core/runtime/orchestration/minimal-delegation-policy.ts +8 -29
  145. package/packages/piagent-core/runtime/orchestration/owned-work-budget.ts +1 -1
  146. package/packages/piagent-core/runtime/policy/block-remedy.ts +5 -3
  147. package/packages/piagent-core/runtime/registration/context-commands.ts +1 -1
  148. package/packages/piagent-core/runtime/registration/knowledge-tools.ts +7 -7
  149. package/packages/piagent-core/runtime/registration/policy-tools.ts +2 -2
  150. package/packages/piagent-core/runtime/registration/session-commands.ts +11 -26
  151. package/packages/piagent-core/runtime/registration/task-evidence-tools.ts +1 -1
  152. package/packages/piagent-core/runtime/session/system-prompt.ts +6 -7
  153. package/packages/piagent-core/runtime/workflows/webui-runtime-command.ts +6 -6
  154. package/packages/piagent-core/runtime/workflows/webui-workflow.ts +6 -11
  155. package/packages/piagent-core/security/sensitive-text.js +9 -0
  156. package/packages/piagent-core/skills/piagent-ops/SKILL.md +8 -10
  157. package/packages/piagent-core/skills/test-audit/SKILL.md +101 -0
  158. package/packages/piagent-core/subagents/piagent-planner.md +1 -1
  159. package/packages/piagent-webui/dist/client/assets/{MarkdownMessage-DMU7Xe_O.js → MarkdownMessage-BRPIMCRh.js} +1 -1
  160. package/packages/piagent-webui/dist/client/assets/index-DNJ1vH5d.js +228 -0
  161. package/packages/piagent-webui/dist/client/assets/{index-DVahUVWy.css → index-DgSmcnP8.css} +1 -1
  162. package/packages/piagent-webui/dist/client/index.html +2 -2
  163. package/packages/piagent-webui/extension/piagent-webui.ts +11 -11
  164. package/packages/piagent-webui/extension/session-stream-adapter.ts +3 -1
  165. package/packages/piagent-webui/extension/terminal-session-adapter.ts +6 -1
  166. package/packages/piagent-webui/gateway/company-relay.ts +308 -0
  167. package/packages/piagent-webui/gateway/control-socket.ts +39 -12
  168. package/packages/piagent-webui/gateway/gateway-service.ts +109 -28
  169. package/packages/piagent-webui/gateway/gateway-session-stream.ts +58 -2
  170. package/packages/piagent-webui/gateway/managed-gateway.mjs +41 -0
  171. package/packages/piagent-webui/gateway/managed-launch.ts +145 -0
  172. package/packages/piagent-webui/gateway/managed-projection.ts +20 -0
  173. package/packages/piagent-webui/gateway/pi-host.ts +22 -4
  174. package/packages/piagent-webui/gateway/runtime-session-controls.ts +5 -2
  175. package/packages/piagent-webui/gateway/session-approval-authority.ts +28 -0
  176. package/packages/piagent-webui/gateway/session-catalog.ts +11 -3
  177. package/packages/piagent-webui/gateway/session-command-controller.ts +8 -2
  178. package/packages/piagent-webui/gateway/session-file-facts.ts +25 -0
  179. package/packages/piagent-webui/gateway/session-inspection-registry.ts +40 -9
  180. package/packages/piagent-webui/gateway/session-list-cache.ts +68 -0
  181. package/packages/piagent-webui/gateway/session-runtime-ownership.ts +8 -1
  182. package/packages/piagent-webui/gateway/session-runtime-supervisor.ts +20 -27
  183. package/packages/piagent-webui/ownership/managed-terminal-lease.ts +27 -0
  184. package/packages/piagent-webui/ownership/profile-state.ts +2 -0
  185. package/packages/piagent-webui/ownership/session-lease-store.ts +22 -1
  186. package/packages/piagent-webui/server/approval-projection.ts +9 -0
  187. package/packages/piagent-webui/server/core-inspection-provider.ts +4 -9
  188. package/packages/piagent-webui/server/loopback-server.ts +37 -1
  189. package/packages/piagent-webui/server/message-stop-reason.ts +11 -0
  190. package/packages/piagent-webui/server/tool-call-summary.ts +185 -0
  191. package/packages/piagent-webui/server/transcript-projection.ts +86 -3
  192. package/packs/engineering-base/pack.json +4 -0
  193. package/schemas/piagent-webui/runtime-command-v1.schema.json +1 -2
  194. package/schemas/piagent-webui/session-catalog-v1.schema.json +8 -0
  195. package/schemas/piagent-webui/transcript-v1.schema.json +94 -1
  196. package/scripts/build-docs-site.mjs +23 -12
  197. package/scripts/check-public-wording.mjs +5 -6
  198. package/scripts/check-runtime-advisories.mjs +23 -6
  199. package/scripts/claude-model-additions.mjs +33 -0
  200. package/scripts/configure-model-scope.sh +30 -6
  201. package/scripts/configure-subagents.sh +1 -1
  202. package/scripts/explain-command.mjs +5 -6
  203. package/scripts/init-project.sh +0 -2
  204. package/scripts/install-global.sh +3 -2
  205. package/scripts/model-scope-prune.mjs +75 -0
  206. package/scripts/phase-tools-evaluation.mjs +9 -4
  207. package/scripts/piagent-cli.mjs +51 -2
  208. package/scripts/piagent-dashboard.mjs +27 -4
  209. package/scripts/piagent-installations.mjs +26 -0
  210. package/scripts/piagent-studio.mjs +155 -0
  211. package/scripts/runtime-advisory-exceptions.json +29 -0
  212. package/scripts/setup.sh +10 -6
  213. package/scripts/team-doctor.sh +20 -1
  214. package/scripts/typescript-loader.mjs +6 -0
  215. package/scripts/verify-local.sh +2 -2
  216. package/templates/global/settings.json +3 -1
  217. package/templates/project/.pi/project-context.md +3 -3
  218. package/templates/project/AGENTS.md +4 -5
  219. package/docs/piagent-core-reasoning-research-2026-09-06.md +0 -225
  220. package/docs/piagent-core-research-sources-2026-09-06.json +0 -416
  221. package/packages/piagent-webui/dist/client/assets/index-Cui9AAV_.js +0 -176
@@ -13,6 +13,9 @@ const MAX_BOOTSTRAP_BODY_BYTES = 4_096;
13
13
  const MAX_CONTROL_BODY_BYTES = 70_000;
14
14
  const MAX_ATTACHMENT_BODY_BYTES = 11_250_000;
15
15
  const REQUESTS_PER_MINUTE = 120;
16
+ // An authenticated browser session refreshes its lists on every live event;
17
+ // several busy conversations exceed 120 reads a minute in normal use.
18
+ const SESSION_REQUESTS_PER_MINUTE = 600;
16
19
  const BOOTSTRAP_FAILURES_PER_MINUTE = 8;
17
20
  const CONTROLS_PER_MINUTE = 60;
18
21
  const MAX_JSON_RESPONSE_BYTES = 16 * 1024 * 1024;
@@ -69,6 +72,10 @@ export async function startLoopbackServer(options: {
69
72
  readProviderAuthJob?: (jobRef: string) => unknown | Promise<unknown>;
70
73
  executeProviderAuth?: (command: unknown) => unknown | Promise<unknown>;
71
74
  executeProjectImport?: () => unknown | Promise<unknown>;
75
+ readCompanyStatus?: () => unknown | Promise<unknown>;
76
+ executeCompanyConnect?: () => unknown | Promise<unknown>;
77
+ // Reads of a company session, answered by the company Gateway; null: local.
78
+ relaySessionRead?: (sessionRef: string, path: string) => Promise<{ status: number; value: unknown } | null>;
72
79
  gatewayProtocol?: GatewayProtocolHandler;
73
80
  readModel?: WebUiReadModelProvider;
74
81
  executeControl?: (command: unknown) => unknown | Promise<unknown>;
@@ -113,7 +120,7 @@ export async function startLoopbackServer(options: {
113
120
  if (now - rate.windowStart >= 60_000) Object.assign(rate, { windowStart: now, requests: 0, bootstrapFailures: 0 });
114
121
  rate.requests += 1; rates.set(rateKey, rate);
115
122
  while (rates.size > 128) rates.delete(rates.keys().next().value as string);
116
- if (rate.requests > REQUESTS_PER_MINUTE) return errorResponse(response, 429, "rate-limit");
123
+ if (rate.requests > (browserSession ? SESSION_REQUESTS_PER_MINUTE : REQUESTS_PER_MINUTE)) return errorResponse(response, 429, "rate-limit");
117
124
  let url: URL;
118
125
  try { url = new URL(request.url ?? "/", origin); }
119
126
  catch { return errorResponse(response, 400, "invalid-url"); }
@@ -176,6 +183,33 @@ export async function startLoopbackServer(options: {
176
183
  }
177
184
  }
178
185
 
186
+ // A gateway without company sessions (the company WebUI itself, or a
187
+ // dashboard with no relay) answers null: no company section, no failed load.
188
+ if (request.method === "GET" && url.pathname === "/api/v1/managed") {
189
+ if (!auth.authenticate(request)) return errorResponse(response, 401, "authentication-required");
190
+ if (url.search) return errorResponse(response, 400, "invalid-managed-query");
191
+ if (!options.readCompanyStatus) return jsonResponse(response, 200, null);
192
+ try { return jsonResponse(response, 200, await options.readCompanyStatus()); }
193
+ catch { return errorResponse(response, 503, "managed-status-unavailable"); }
194
+ }
195
+
196
+ // Starts (or reconnects to) the company Gateway; macOS may ask for Keychain access.
197
+ if (request.method === "POST" && url.pathname === "/api/v1/managed/connect" && options.executeCompanyConnect) {
198
+ if (requestOrigin !== origin) return errorResponse(response, 403, "origin-required");
199
+ const mutationSession = auth.authorizeMutation(request);
200
+ if (!mutationSession) return errorResponse(response, 403, "mutation-authority-rejected");
201
+ if (!consumeControl(mutationSession.id, now)) return errorResponse(response, 429, "control-rate-limit");
202
+ if (!String(request.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) return errorResponse(response, 415, "content-type");
203
+ let command: Record<string, unknown>;
204
+ try { command = JSON.parse((await requestBody(request, MAX_BOOTSTRAP_BODY_BYTES)).toString("utf8")); }
205
+ catch (error) { return errorResponse(response, (error as Error).message === "body-limit" ? 413 : 400, "invalid-managed-connect"); }
206
+ if (!command || typeof command !== "object" || Array.isArray(command) || Object.keys(command).length !== 1 || command.action !== "managed.connect") {
207
+ return errorResponse(response, 400, "invalid-managed-connect");
208
+ }
209
+ try { return jsonResponse(response, 200, await options.executeCompanyConnect()); }
210
+ catch { return errorResponse(response, 503, "managed-connect-failed"); }
211
+ }
212
+
179
213
  if (request.method === "POST" && url.pathname === "/api/v1/provider-auth" && options.executeProviderAuth) {
180
214
  if (requestOrigin !== origin) return errorResponse(response, 403, "origin-required");
181
215
  const mutationSession = auth.authorizeMutation(request);
@@ -353,6 +387,8 @@ export async function startLoopbackServer(options: {
353
387
  return errorResponse(response, 400, "invalid-session-inspection-ref");
354
388
  }
355
389
  try {
390
+ const relayed = await options.relaySessionRead?.(sessionRef, inspectionPath + url.search);
391
+ if (relayed) return jsonResponse(response, relayed.status, relayed.value);
356
392
  if (inspectionPath === "/connections") {
357
393
  if (url.search || !options.readSessionConnections) return errorResponse(response, 404, "inspection-route-not-found");
358
394
  const value = await options.readSessionConnections(sessionRef);
@@ -0,0 +1,11 @@
1
+ // Some providers end a turn the member stopped with `stopReason: "error"` and
2
+ // the AbortError text instead of `stopReason: "aborted"`. That is a stop, not a
3
+ // model failure: showing "the model returned an error" for it tells the member
4
+ // to resend or change model after they pressed Stop themselves.
5
+ const ABORT_ERROR = /\b(?:this operation|the operation|request) was aborted\b|\bAbortError\b|^aborted$/i;
6
+
7
+ export function effectiveStopReason(message: any): string {
8
+ const stopReason = String(message?.stopReason ?? "");
9
+ if (stopReason === "error" && ABORT_ERROR.test(String(message?.errorMessage ?? "").trim())) return "aborted";
10
+ return stopReason;
11
+ }
@@ -0,0 +1,185 @@
1
+ import os from "node:os";
2
+ import path from "node:path";
3
+
4
+ import { redactSensitiveText } from "../../piagent-core/extensions/redaction-core.js";
5
+
6
+ // What a coding-agent timeline shows for one tool call: the kind of action,
7
+ // its target (project-relative path, command, query or URL), an edit's diff
8
+ // and a bounded result preview. Every string is ANSI-stripped and redacted;
9
+ // raw arguments and results never leave the server.
10
+ export type ToolKind = "read" | "write" | "edit" | "command" | "search" | "list" | "web-search" | "web-fetch"
11
+ | "subagent" | "network" | "git" | "plan" | "other";
12
+ export type ToolSummary = { kind: ToolKind; target: string | null; detail: string | null };
13
+ export type ToolChange = { added: number; removed: number; preview: string; truncated: boolean };
14
+ export type ToolResult = { text: string; truncated: boolean; isError: boolean };
15
+ export type TurnUsage = { inputTokens: number; outputTokens: number; cacheReadTokens: number; cacheWriteTokens: number; totalTokens: number };
16
+
17
+ const ANSI = /\u001b(?:\[[0-?]*[ -/]*[@-~]|[@-_])/g;
18
+ const CONTROL = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g;
19
+ const TARGET_CHARS = 400, DETAIL_CHARS = 400, PREVIEW_LINES = 80, PREVIEW_CHARS = 6_000, RESULT_LINES = 60, RESULT_CHARS = 4_000;
20
+
21
+ function clean(value: unknown, max: number): string | null {
22
+ if (typeof value !== "string" || !value.trim()) return null;
23
+ const text = redactedWhole(value);
24
+ return text.length > max ? `${text.slice(0, max - 1)}…` : text;
25
+ }
26
+ // Redaction needs the whole text: a private key is recognised only while its
27
+ // lines are together, so file contents and edit texts are redacted before they
28
+ // are split into preview rows.
29
+ function redactedWhole(value: unknown): string {
30
+ return redactSensitiveText(String(value ?? "").replace(ANSI, "").replace(CONTROL, " ")).text;
31
+ }
32
+ function record(value: unknown): Record<string, unknown> {
33
+ return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {};
34
+ }
35
+ function pick(args: Record<string, unknown>, keys: readonly string[]): unknown {
36
+ for (const key of keys) if (args[key] !== undefined && args[key] !== null && args[key] !== "") return args[key];
37
+ return undefined;
38
+ }
39
+ // Paths inside the project are shown relative to it; anything else only by
40
+ // its file name, so folders outside the project never reach the browser.
41
+ export function displayPath(value: unknown, cwd?: string): string | null {
42
+ if (typeof value !== "string" || !value.trim()) return null;
43
+ const raw = value.trim();
44
+ if (!path.isAbsolute(raw)) return raw.split(/[\\/]/).includes("..") ? clean(path.basename(raw), TARGET_CHARS) : clean(raw, TARGET_CHARS);
45
+ const relative = cwd ? path.relative(cwd, raw) : "";
46
+ return cwd && !relative.startsWith("..") && !path.isAbsolute(relative) ? clean(relative || ".", TARGET_CHARS) : clean(path.basename(raw), TARGET_CHARS);
47
+ }
48
+ // A command as run in the project: the project folder reads "." and the home
49
+ // folder "~".
50
+ function displayCommand(value: unknown, cwd?: string): string | null {
51
+ if (typeof value !== "string") return null;
52
+ let shown = value;
53
+ if (cwd && cwd.length > 1) shown = shown.split(cwd).join(".");
54
+ const home = os.homedir();
55
+ if (home.length > 1) shown = shown.split(home).join("~");
56
+ return clean(shown, TARGET_CHARS);
57
+ }
58
+ // A multi-line command (a heredoc, a script) reads by its first line; the
59
+ // whole command, bounded, opens in the step's body.
60
+ function commandParts(value: unknown, cwd?: string): { target: string | null; script: string | null } {
61
+ const full = displayCommand(value, cwd), rows = full ? full.split("\n").filter((row) => row.trim()) : [];
62
+ return rows.length > 1 ? { target: `${rows[0].trim().slice(0, TARGET_CHARS - 2)} …`, script: full } : { target: full, script: null };
63
+ }
64
+ const lines = (text: string) => text === "" ? [] : text.replace(/\r\n/g, "\n").split("\n");
65
+
66
+ function bounded(rows: string[], removed: number, added: number): ToolChange {
67
+ let preview = "", count = 0, truncated = false;
68
+ for (const row of rows) {
69
+ const safe = (clean(row, 400) ?? row.slice(0, 1)).replace(/\n/g, " ");
70
+ if (count >= PREVIEW_LINES || preview.length + safe.length + 1 > PREVIEW_CHARS) { truncated = true; break; }
71
+ preview += (count ? "\n" : "") + safe; count += 1;
72
+ }
73
+ return { added, removed, preview, truncated };
74
+ }
75
+ // Old and new text of each edit, trimmed to the changed lines plus two lines
76
+ // of context: enough to read the change without reproducing the file.
77
+ function editChange(edits: Array<{ oldText?: unknown; newText?: unknown }>): ToolChange {
78
+ const rows: string[] = []; let added = 0, removed = 0;
79
+ for (const edit of edits.slice(0, 20)) {
80
+ const before = lines(redactedWhole(edit.oldText)), after = lines(redactedWhole(edit.newText));
81
+ let head = 0; while (head < before.length && head < after.length && before[head] === after[head]) head += 1;
82
+ let tail = 0; while (tail < before.length - head && tail < after.length - head && before[before.length - 1 - tail] === after[after.length - 1 - tail]) tail += 1;
83
+ const gone = before.slice(head, before.length - tail), come = after.slice(head, after.length - tail);
84
+ removed += gone.length; added += come.length;
85
+ if (rows.length) rows.push("@@");
86
+ for (const row of before.slice(Math.max(0, head - 2), head)) rows.push(` ${row}`);
87
+ for (const row of gone) rows.push(`-${row}`);
88
+ for (const row of come) rows.push(`+${row}`);
89
+ for (const row of before.slice(before.length - tail, Math.min(before.length, before.length - tail + 2))) rows.push(` ${row}`);
90
+ }
91
+ return bounded(rows, removed, added);
92
+ }
93
+
94
+ // A patch in the apply_patch format: the files it touches (from its
95
+ // "*** Add/Update/Delete File:" headers) and its +/- body lines, redacted as a
96
+ // whole like an edit. "write" when it only adds files.
97
+ function patchChange(value: unknown, cwd?: string): { summary: ToolSummary; change: ToolChange } {
98
+ const text = redactedWhole(value), rows: string[] = [], files: string[] = []; let added = 0, removed = 0, onlyAdds = true;
99
+ for (const row of lines(text)) {
100
+ const header = row.match(/^\*\*\* (Add|Update|Delete) File: (.+)$/);
101
+ if (header) { files.push(header[2].trim()); if (header[1] !== "Add") onlyAdds = false; if (rows.length) rows.push("@@"); continue; }
102
+ if (/^\*\*\* (Begin|End) Patch$|^\*\*\* Move to: /.test(row)) continue;
103
+ if (row.startsWith("@@")) { if (rows.length && rows.at(-1) !== "@@") rows.push("@@"); continue; }
104
+ if (row.startsWith("+")) added += 1; else if (row.startsWith("-")) removed += 1;
105
+ rows.push(row);
106
+ }
107
+ const first = displayPath(files[0], cwd), more = files.length - 1;
108
+ return { summary: { kind: files.length && onlyAdds ? "write" : "edit", target: first && more > 0 ? `${first} +${more}` : first, detail: null },
109
+ change: bounded(rows, removed, added) };
110
+ }
111
+
112
+ const ALIASES: Record<string, string> = { read_file: "read", view: "read", cat: "read", write_file: "write", create_file: "write",
113
+ edit_file: "edit", str_replace: "edit", str_replace_editor: "edit", shell: "bash", exec: "bash", run: "bash", terminal: "bash", run_experiment: "bash",
114
+ glob: "find", search: "grep", search_files: "grep", list: "ls", list_files: "ls", list_dir: "ls" };
115
+
116
+ export function summarizeToolCall(name: unknown, input: unknown, cwd?: string): { summary: ToolSummary; change?: ToolChange } {
117
+ const raw = String(name ?? "").toLowerCase(), tool = ALIASES[raw] ?? raw, args = record(input);
118
+ const file = pick(args, ["path", "file_path", "filePath", "file"]);
119
+ switch (tool) {
120
+ case "read": {
121
+ const offset = Number(args.offset), limit = Number(args.limit);
122
+ const detail = Number.isInteger(offset) && offset > 0 ? `${offset}${Number.isInteger(limit) && limit > 0 ? `–${offset + limit - 1}` : "+"}` : null;
123
+ return { summary: { kind: "read", target: displayPath(file, cwd), detail } };
124
+ }
125
+ case "write": {
126
+ const content = lines(redactedWhole(args.content));
127
+ return { summary: { kind: "write", target: displayPath(file, cwd), detail: null },
128
+ change: bounded(content.map((row) => `+${row}`), 0, content.length) };
129
+ }
130
+ case "edit": {
131
+ const edits = Array.isArray(args.edits) ? args.edits as Array<{ oldText?: unknown; newText?: unknown }>
132
+ : [{ oldText: args.oldText ?? args.old_string, newText: args.newText ?? args.new_string }];
133
+ return { summary: { kind: "edit", target: displayPath(file, cwd), detail: null }, change: editChange(edits) };
134
+ }
135
+ case "apply_patch": return patchChange(pick(args, ["patch", "input"]), cwd);
136
+ case "bash": {
137
+ const command = commandParts(args.command, cwd);
138
+ return { summary: { kind: "command", target: command.target, detail: command.script } };
139
+ }
140
+ case "run_with_network":
141
+ return { summary: { kind: "network", target: commandParts(args.command, cwd).target, detail: clean(args.reason, DETAIL_CHARS) } };
142
+ case "grep": case "find": {
143
+ const where = [displayPath(args.path, cwd), clean(args.glob, 120)].filter(Boolean).join(" · ");
144
+ return { summary: { kind: "search", target: clean(args.pattern, TARGET_CHARS), detail: where || null } };
145
+ }
146
+ case "ls": return { summary: { kind: "list", target: displayPath(file ?? ".", cwd), detail: null } };
147
+ case "web_search": return { summary: { kind: "web-search", target: clean(args.query, TARGET_CHARS), detail: null } };
148
+ case "web_fetch": return { summary: { kind: "web-fetch", target: clean(args.url, TARGET_CHARS), detail: null } };
149
+ case "delegate": case "subagent":
150
+ return { summary: { kind: "subagent", target: clean(pick(args, ["role", "agent", "name"]), 40), detail: clean(args.task, 200) } };
151
+ case "fetch_origin": return { summary: { kind: "git", target: clean(pick(args, ["remote", "branch", "ref"]) ?? "origin", 120), detail: null } };
152
+ case "update_plan": {
153
+ // The checklist's progress, and the step being worked on.
154
+ const steps = Array.isArray(args.plan) ? (args.plan as Array<{ step?: unknown; status?: unknown }>) : [];
155
+ const done = steps.filter((p) => p?.status === "completed").length, current = steps.find((p) => p?.status === "in_progress");
156
+ return { summary: { kind: "plan", target: `${done}/${steps.length}`, detail: clean(current?.step, 200) } };
157
+ }
158
+ default: {
159
+ const first = Object.values(args).find((value) => typeof value === "string" && value.trim()) as string | undefined;
160
+ const target = first && /[\\/]/.test(first) ? displayPath(first, cwd) : clean(first, 200);
161
+ return { summary: { kind: "other", target, detail: null } };
162
+ }
163
+ }
164
+ }
165
+
166
+ export function toolResultPreview(message: any): ToolResult {
167
+ const text = typeof message?.content === "string" ? message.content : Array.isArray(message?.content)
168
+ ? message.content.filter((part: any) => part?.type === "text" && typeof part.text === "string").map((part: any) => part.text).join("\n") : "";
169
+ const all = lines(clean(text, 1_000_000) ?? ""), kept = all.slice(0, RESULT_LINES).join("\n");
170
+ return { text: kept.slice(0, RESULT_CHARS), truncated: all.length > RESULT_LINES || kept.length > RESULT_CHARS, isError: Boolean(message?.isError) };
171
+ }
172
+
173
+ export function turnUsage(message: any): TurnUsage | null {
174
+ const usage = record(message?.usage);
175
+ const count = (value: unknown) => Number.isFinite(Number(value)) && Number(value) > 0 ? Math.min(10_000_000_000, Math.round(Number(value))) : 0;
176
+ const value = { inputTokens: count(usage.input), outputTokens: count(usage.output), cacheReadTokens: count(usage.cacheRead),
177
+ cacheWriteTokens: count(usage.cacheWrite), totalTokens: count(usage.totalTokens) };
178
+ if (!value.totalTokens) value.totalTokens = Math.min(10_000_000_000, value.inputTokens + value.outputTokens + value.cacheReadTokens + value.cacheWriteTokens);
179
+ return value.totalTokens ? value : null;
180
+ }
181
+
182
+ export function modelLabel(message: any): string | null {
183
+ const model = typeof message?.model === "string" ? message.model.replace(/[^A-Za-z0-9._:@/-]/g, "").slice(0, 120) : "";
184
+ return model || null;
185
+ }
@@ -5,9 +5,13 @@ import { looksLikeCompletionClaim } from "../../piagent-core/runtime/session/com
5
5
  import { isLightweightNonAuthorizingChangeLanguage } from "../../piagent-core/runtime/workflows/change-clarification.ts";
6
6
  import { workflowCommandPattern } from "../../piagent-core/runtime/workflows/webui-workflow.ts";
7
7
  import { hasVisibleText } from "../shared/text-visibility.ts";
8
+ import { effectiveStopReason } from "./message-stop-reason.ts";
8
9
  import { WEBUI_MESSAGE_CORRELATION_ENTRY_TYPE, webUiMessageCorrelationEntry,
9
10
  type WebUiMessageCorrelation } from "../shared/message-correlation.ts";
10
11
  import { terminalDeliveryPairs, type TerminalDeliveryPair, type TerminalDeliveryReceipt } from "./terminal-delivery-receipt.ts";
12
+ import { parseFailure } from "../../piagent-core/runtime/managed-failure.mjs";
13
+ import { modelLabel, summarizeToolCall, toolResultPreview, turnUsage, type ToolChange, type ToolResult, type ToolSummary,
14
+ type TurnUsage } from "./tool-call-summary.ts";
11
15
 
12
16
  const MAX_ENTRIES = 50_000;
13
17
  const MAX_ITEMS = 200;
@@ -28,7 +32,11 @@ type TranscriptAttachment = { displayName: string; kind: "file" | "image" | "doc
28
32
  type TranscriptItem = { messageRef: string; parentMessageRef: string | null; role: "user" | "assistant" | "tool-result" | "custom"; recordedAt: string;
29
33
  agentOperationId: string | null; messageRequestId?: string; turnIndex: number | null; content: TranscriptContent;
30
34
  attachments?: TranscriptAttachment[];
31
- toolCalls: Array<{ toolCallRef: string; toolName: string; state: "requested" | "completed" | "failed" | "unknown" }> };
35
+ toolCalls: Array<{ toolCallRef: string; toolName: string; state: "requested" | "completed" | "failed" | "unknown";
36
+ summary?: ToolSummary; change?: ToolChange; result?: ToolResult }>; usage?: TurnUsage; model?: string; process?: HarnessProcess };
37
+ type HarnessFinding = { severity: "blocking" | "major" | "minor"; file: string; line: number | null; issue: string };
38
+ type HarnessProcess = { phase: "plan" | "verify" | "review" | "final"; outcome?: string; loop?: number; maxLoops?: number; verified?: boolean; reviewed?: boolean;
39
+ blockingOpen?: number; planOpen?: number; planSkipped?: true; verifyPolicy?: string; reviewPolicy?: string; reviewUnavailable?: string; findings?: HarnessFinding[] };
32
40
  export type TranscriptDocument = { schemaVersion: 1; version: "piagent-webui-transcript-v1"; generatedAt: string; identity: TranscriptIdentity;
33
41
  revision: TranscriptRevision; eventCursor: string; state: "ready" | "unavailable"; items: TranscriptItem[];
34
42
  page: { beforeCursor: string | null; nextBeforeCursor: string | null; hasOlder: boolean; limit: number; truncated: boolean }; reasonCode: string | null };
@@ -44,6 +52,8 @@ export type TranscriptProjectionInput = {
44
52
  taskOutcome?: string | null;
45
53
  terminalDeliveryReceipt?: TerminalDeliveryReceipt;
46
54
  terminalDeliveryEntries?: unknown[];
55
+ // The session's project folder: tool paths are shown relative to it.
56
+ cwd?: string;
47
57
  };
48
58
 
49
59
  function hash(value: string): string { return createHash("sha256").update(value).digest("hex"); }
@@ -88,8 +98,18 @@ function userMessageProjection(message: any): { text: string; attachments: Trans
88
98
  // text the operator typed; the remaining recognized blocks become cards.
89
99
  return { text: attachments.length ? String(textParts[0]?.text ?? "") : messageText(message), attachments: attachments.slice(0, 4) };
90
100
  }
101
+ // A company request fails with Studio's (or Agent Watch's) own code: who
102
+ // failed, which kind of failure, and Studio's request id. Text is never shown.
103
+ export type CompanyFailure = { role: "main" | "scout" | "research" | "verify" | "review"; reasonCode: string; code: string; requestRef: string | null; local: boolean };
104
+ export function companyFailure(text: unknown): CompanyFailure | null {
105
+ const parsed = parseFailure(text);
106
+ if (!parsed || !/^[A-Za-z0-9][A-Za-z0-9_:.-]{0,79}$/.test(parsed.code)) return null;
107
+ return { role: parsed.role as CompanyFailure["role"], reasonCode: `company-${parsed.kind}`, code: parsed.code, requestRef: parsed.requestId, local: parsed.local };
108
+ }
109
+ const COMPANY = "agent_watch_managed";
91
110
  function assistantFailureReason(message: any): string | null {
92
111
  if (message?.role !== "assistant" || message?.stopReason !== "error") return null;
112
+ if (message.provider === COMPANY) return companyFailure(message.errorMessage)?.reasonCode ?? "company-failed";
93
113
  const detail = String(message?.errorMessage ?? "").toLowerCase();
94
114
  if (/(?:authentication|auth|token|credential).{0,48}expired|expired.{0,48}(?:authentication|auth|token|credential)/.test(detail)) {
95
115
  return "provider-auth-expired";
@@ -103,7 +123,7 @@ function assistantFailureReason(message: any): string | null {
103
123
  }
104
124
  function assistantUnavailableReason(message: any, text: string, projectedToolCalls: TranscriptItem["toolCalls"]): string | null {
105
125
  if (message?.role !== "assistant") return null;
106
- const stopReason = String(message?.stopReason ?? "");
126
+ const stopReason = effectiveStopReason(message);
107
127
  if (stopReason === "error") return assistantFailureReason(message) ?? "provider-response-failed";
108
128
  if (stopReason === "aborted") return "assistant-message-aborted";
109
129
  if (stopReason === "length") return "assistant-output-incomplete";
@@ -141,6 +161,31 @@ function toolCalls(message: any, sessionRef: string, role: TranscriptItem["role"
141
161
  return { toolCallRef: opaque("tool", [sessionRef, item.id ?? index]), toolName: safeToolName(item.name), state: item.state };
142
162
  });
143
163
  }
164
+ // The harness's process notes ("agent-watch-process"): what it asked of the
165
+ // agent before a code-changing turn ended, and how that turn ended.
166
+ const PROCESS_OUTCOMES = ["no_change", "interrupted", "blocking_open", "unverified", "review_unavailable", "unreviewed", "clean"];
167
+ const POLICY_MODES = ["off", "suggest", "require"];
168
+ function harnessProcess(details: any): HarnessProcess | null {
169
+ if (!["plan", "verify", "review", "final"].includes(details?.phase)) return null;
170
+ const loop = (n: unknown): n is number => Number.isInteger(n) && (n as number) >= 0 && (n as number) <= 3;
171
+ const out: HarnessProcess = { phase: details.phase };
172
+ if (PROCESS_OUTCOMES.includes(details.outcome)) out.outcome = details.outcome;
173
+ if (loop(details.loop)) out.loop = details.loop;
174
+ if (loop(details.maxLoops)) out.maxLoops = details.maxLoops;
175
+ if (typeof details.verified === "boolean") out.verified = details.verified;
176
+ if (typeof details.reviewed === "boolean") out.reviewed = details.reviewed;
177
+ if (Number.isInteger(details.blockingOpen) && details.blockingOpen >= 0) out.blockingOpen = Math.min(details.blockingOpen, 1000);
178
+ if (Number.isInteger(details.planOpen) && details.planOpen >= 1 && details.planOpen <= 30) out.planOpen = details.planOpen;
179
+ if (details.planSkipped === true) out.planSkipped = true;
180
+ if (POLICY_MODES.includes(details.policy?.verify)) out.verifyPolicy = details.policy.verify;
181
+ if (POLICY_MODES.includes(details.policy?.review)) out.reviewPolicy = details.policy.review;
182
+ if (["too_large", "not_git", "limit", "failed"].includes(details.reviewUnavailable)) out.reviewUnavailable = details.reviewUnavailable;
183
+ if (Array.isArray(details.findings)) out.findings = details.findings
184
+ .filter((f: any) => ["blocking", "major", "minor"].includes(f?.severity) && typeof f.issue === "string" && f.issue.trim()).slice(0, 10)
185
+ .map((f: any) => ({ severity: f.severity, file: safeText(String(f.file ?? "")).full.slice(0, 300), line: Number.isSafeInteger(f.line) && f.line > 0 ? f.line : null,
186
+ issue: safeText(f.issue).full.slice(0, 500) }));
187
+ return out;
188
+ }
144
189
  function role(message: any): TranscriptItem["role"] | null {
145
190
  return message?.role === "user" ? "user" : message?.role === "assistant" ? "assistant" : message?.role === "toolResult" ? "tool-result" : null;
146
191
  }
@@ -198,6 +243,13 @@ function correlatedTranscriptItems(entries: unknown[], identity: TranscriptIdent
198
243
  pending = webUiMessageCorrelationEntry(entry);
199
244
  continue;
200
245
  }
246
+ if (entry?.type === "custom_message" && entry.customType === "agent-watch-process") {
247
+ const process = harnessProcess(entry.details);
248
+ const projected = process && item({ ...entry, type: "message", message: { role: "custom", content: entry.content } }, identity, true);
249
+ if (projected) values.push({ entry, item: { ...projected, process, ...(turn ? { agentOperationId: turn.operationRef, messageRequestId: turn.messageRequestId } : {}) },
250
+ cursor: opaque("transcript", [identity.sessionRef, entry.id]) });
251
+ continue;
252
+ }
201
253
  if (role(entry?.message) === "user") { turn = pending; pending = null; }
202
254
  const projected = item(entry, identity);
203
255
  if (!projected) continue;
@@ -291,6 +343,37 @@ function boundedTurnPage(
291
343
  return { selected, start, compacted: false };
292
344
  }
293
345
 
346
+ // The agent timeline: each tool call of a shown answer carries its summary,
347
+ // an edit's diff and its result preview; answers carry usage and model.
348
+ function timelineItems(selected: ProjectedTranscriptItem[], all: ProjectedTranscriptItem[], cwd?: string): TranscriptItem[] {
349
+ const results = new Map<string, any>();
350
+ for (const value of all) if (value.item.role === "tool-result" && value.item.toolCalls[0]) results.set(value.item.toolCalls[0].toolCallRef, value.entry?.message);
351
+ return selected.map((value) => {
352
+ if (value.item.role !== "assistant") return value.item;
353
+ const message = value.entry?.message;
354
+ const blocks = Array.isArray(message?.content) ? message.content.filter((part: any) => part?.type === "toolCall") : [];
355
+ const toolCalls = value.item.toolCalls.map((call, index) => {
356
+ const block = blocks[index], result = results.get(call.toolCallRef);
357
+ if (!block) return call;
358
+ const { summary, change } = summarizeToolCall(block.name, block.arguments, cwd);
359
+ // A helper's answer can carry its delegated prompt and artifacts: only
360
+ // its outcome is shown, never its text.
361
+ const shown = result && summary.kind !== "subagent" ? { result: toolResultPreview(result) } : {};
362
+ // A failed helper or company search says why (its code), not what it wrote.
363
+ const failed = result?.isError && message?.provider === COMPANY && ["subagent", "web-search"].includes(summary.kind)
364
+ ? companyFailure(toolResultPreview(result).text) : null;
365
+ return { ...call, summary, ...(change ? { change } : {}), ...shown, ...(failed ? { failure: failed } : {}),
366
+ ...(result ? { state: result.isError ? "failed" as const : "completed" as const } : {}) };
367
+ });
368
+ // A company turn is answered by a harness role; which model sits behind it
369
+ // is the harness's business and stays out of the browser.
370
+ const company = message?.provider === COMPANY;
371
+ const usage = turnUsage(message), model = company ? null : modelLabel(message);
372
+ const failure = company && effectiveStopReason(message) === "error" ? companyFailure(message.errorMessage) : null;
373
+ return { ...value.item, toolCalls, ...(usage ? { usage } : {}), ...(model ? { model } : {}), ...(failure ? { failure } : {}) };
374
+ });
375
+ }
376
+
294
377
  function unavailable(input: TranscriptProjectionInput, reasonCode: string, limit: number): TranscriptDocument {
295
378
  return { schemaVersion: 1, version: "piagent-webui-transcript-v1", generatedAt: input.generatedAt ?? new Date().toISOString(),
296
379
  identity: structuredClone(input.identity), revision: structuredClone(input.revision), eventCursor: input.eventCursor,
@@ -314,7 +397,7 @@ export function projectTranscript(input: TranscriptProjectionInput): TranscriptD
314
397
  if (cursorIndex < 0) return unavailable(input, "transcript-cursor-gap", limit);
315
398
  end = cursorIndex;
316
399
  }
317
- const page = boundedTurnPage(projected, end, limit), selected = page.selected.map((value) => value.item), hasOlder = page.start > 0;
400
+ const page = boundedTurnPage(projected, end, limit), selected = timelineItems(page.selected, projected, input.cwd), hasOlder = page.start > 0;
318
401
  return { schemaVersion: 1, version: "piagent-webui-transcript-v1", generatedAt: input.generatedAt ?? new Date().toISOString(),
319
402
  identity: structuredClone(input.identity), revision: structuredClone(input.revision), eventCursor: input.eventCursor,
320
403
  state: "ready", items: selected, page: { beforeCursor: input.beforeCursor ?? null, nextBeforeCursor: hasOlder ? cursors[page.start] : null,
@@ -45,6 +45,10 @@
45
45
  {
46
46
  "id": "piagent-ops",
47
47
  "path": "packages/piagent-core/skills/piagent-ops/SKILL.md"
48
+ },
49
+ {
50
+ "id": "test-audit",
51
+ "path": "packages/piagent-core/skills/test-audit/SKILL.md"
48
52
  }
49
53
  ],
50
54
  "subagents": [
@@ -9,9 +9,8 @@
9
9
  "$defs": {
10
10
  "action": {
11
11
  "enum": [
12
- "runtime.status", "runtime.inspector", "runtime.commands", "runtime.fast-status", "runtime.fast-on", "runtime.fast-off", "orchestration.status",
12
+ "runtime.status", "runtime.inspector", "runtime.fast-status", "runtime.fast-on", "runtime.fast-off", "orchestration.status",
13
13
  "usage.live", "usage.history", "usage.logs", "usage.efficiency", "usage.preflight",
14
- "onboarding.status", "onboarding.profile", "onboarding.tech",
15
14
  "profile.status", "profile.options", "profile.tech-options", "profile.apply", "profile.auto",
16
15
  "context.status", "context.rebuild", "context.search", "context.pack", "context.impact", "context.efficiency", "context.preflight", "context.compact",
17
16
  "memory.status",
@@ -91,6 +91,14 @@
91
91
  "unread": { "type": "boolean" },
92
92
  "composerAvailable": { "type": "boolean" },
93
93
  "needsAttention": { "type": "boolean" },
94
+ "managedHelpers": { "type": "object", "additionalProperties": false, "required": ["active", "maximum"], "properties": {"active": {"type": ["integer", "null"], "minimum": 0, "maximum": 2}, "maximum": {"const": 2}} },
95
+ "managedThinkingLevels": {"type": "array", "uniqueItems": true, "maxItems": 7, "items": {"enum": ["off","minimal","low","medium","high","xhigh","max"]}},
96
+ "managedPlan": { "type": "object", "additionalProperties": false, "required": ["steps"], "properties": {
97
+ "steps": { "type": "array", "minItems": 1, "maxItems": 30, "items": { "type": "object", "additionalProperties": false, "required": ["step", "status"], "properties": {
98
+ "step": { "type": "string", "minLength": 1, "maxLength": 200 }, "status": { "enum": ["pending", "in_progress", "completed"] } } } } } },
99
+ "managedProcess": { "type": "object", "additionalProperties": false, "required": ["outcome", "verified", "reviewed", "blockingOpen"], "properties": {
100
+ "outcome": { "enum": ["no_change", "interrupted", "blocking_open", "unverified", "review_unavailable", "unreviewed", "clean"] },
101
+ "verified": { "type": "boolean" }, "reviewed": { "type": "boolean" }, "blockingOpen": { "type": "integer", "minimum": 0, "maximum": 1000 }, "planSkipped": { "const": true } } },
94
102
  "modelLabel": { "$ref": "common-v1.schema.json#/$defs/nullableDisplayText" },
95
103
  "thinkingLevel": { "enum": ["off", "minimal", "low", "medium", "high", "xhigh", "max", "unknown"] },
96
104
  "contextUsage": { "$ref": "#/$defs/contextUsage" },
@@ -152,6 +152,52 @@
152
152
  }
153
153
  ]
154
154
  },
155
+ "harnessProcess": {
156
+ "type": "object",
157
+ "additionalProperties": false,
158
+ "required": ["phase"],
159
+ "properties": {
160
+ "phase": { "enum": ["plan", "verify", "review", "final"] },
161
+ "outcome": { "enum": ["no_change", "interrupted", "blocking_open", "unverified", "review_unavailable", "unreviewed", "clean"] },
162
+ "loop": { "type": "integer", "minimum": 0, "maximum": 3 },
163
+ "maxLoops": { "type": "integer", "minimum": 0, "maximum": 3 },
164
+ "verified": { "type": "boolean" },
165
+ "reviewed": { "type": "boolean" },
166
+ "blockingOpen": { "type": "integer", "minimum": 0, "maximum": 1000 },
167
+ "planOpen": { "type": "integer", "minimum": 1, "maximum": 30 },
168
+ "planSkipped": { "const": true },
169
+ "verifyPolicy": { "enum": ["off", "suggest", "require"] },
170
+ "reviewPolicy": { "enum": ["off", "suggest", "require"] },
171
+ "reviewUnavailable": { "enum": ["too_large", "not_git", "limit", "failed"] },
172
+ "findings": {
173
+ "type": "array",
174
+ "maxItems": 10,
175
+ "items": {
176
+ "type": "object",
177
+ "additionalProperties": false,
178
+ "required": ["severity", "file", "line", "issue"],
179
+ "properties": {
180
+ "severity": { "enum": ["blocking", "major", "minor"] },
181
+ "file": { "type": "string", "maxLength": 300 },
182
+ "line": { "type": ["integer", "null"], "minimum": 1 },
183
+ "issue": { "type": "string", "maxLength": 500 }
184
+ }
185
+ }
186
+ }
187
+ }
188
+ },
189
+ "companyFailure": {
190
+ "type": "object",
191
+ "additionalProperties": false,
192
+ "required": ["role", "reasonCode", "code", "requestRef", "local"],
193
+ "properties": {
194
+ "role": { "enum": ["main", "scout", "research", "verify", "review"] },
195
+ "reasonCode": { "$ref": "common-v1.schema.json#/$defs/reasonCode" },
196
+ "code": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9_:.-]{0,79}$" },
197
+ "requestRef": { "anyOf": [{ "type": "string", "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" }, { "type": "null" }] },
198
+ "local": { "type": "boolean" }
199
+ }
200
+ },
155
201
  "toolCall": {
156
202
  "type": "object",
157
203
  "additionalProperties": false,
@@ -164,7 +210,39 @@
164
210
  "maxLength": 80,
165
211
  "pattern": "^[A-Za-z0-9][A-Za-z0-9._:@~-]{0,79}$"
166
212
  },
167
- "state": { "enum": ["requested", "completed", "failed", "unknown"] }
213
+ "state": { "enum": ["requested", "completed", "failed", "unknown"] },
214
+ "summary": {
215
+ "type": "object",
216
+ "additionalProperties": false,
217
+ "required": ["kind", "target", "detail"],
218
+ "properties": {
219
+ "kind": { "enum": ["read", "write", "edit", "command", "search", "list", "web-search", "web-fetch", "subagent", "network", "git", "plan", "other"] },
220
+ "target": { "anyOf": [{ "type": "string", "maxLength": 400 }, { "type": "null" }] },
221
+ "detail": { "anyOf": [{ "type": "string", "maxLength": 400 }, { "type": "null" }] }
222
+ }
223
+ },
224
+ "change": {
225
+ "type": "object",
226
+ "additionalProperties": false,
227
+ "required": ["added", "removed", "preview", "truncated"],
228
+ "properties": {
229
+ "added": { "type": "integer", "minimum": 0, "maximum": 1000000 },
230
+ "removed": { "type": "integer", "minimum": 0, "maximum": 1000000 },
231
+ "preview": { "type": "string", "maxLength": 6000 },
232
+ "truncated": { "type": "boolean" }
233
+ }
234
+ },
235
+ "result": {
236
+ "type": "object",
237
+ "additionalProperties": false,
238
+ "required": ["text", "truncated", "isError"],
239
+ "properties": {
240
+ "text": { "type": "string", "maxLength": 4000 },
241
+ "truncated": { "type": "boolean" },
242
+ "isError": { "type": "boolean" }
243
+ }
244
+ },
245
+ "failure": { "$ref": "#/$defs/companyFailure" }
168
246
  }
169
247
  },
170
248
  "transcriptItem": {
@@ -195,6 +273,21 @@
195
273
  "uniqueItems": true,
196
274
  "items": { "$ref": "#/$defs/attachmentSummary" }
197
275
  },
276
+ "usage": {
277
+ "type": "object",
278
+ "additionalProperties": false,
279
+ "required": ["inputTokens", "outputTokens", "cacheReadTokens", "cacheWriteTokens", "totalTokens"],
280
+ "properties": {
281
+ "inputTokens": { "type": "integer", "minimum": 0, "maximum": 10000000000 },
282
+ "outputTokens": { "type": "integer", "minimum": 0, "maximum": 10000000000 },
283
+ "cacheReadTokens": { "type": "integer", "minimum": 0, "maximum": 10000000000 },
284
+ "cacheWriteTokens": { "type": "integer", "minimum": 0, "maximum": 10000000000 },
285
+ "totalTokens": { "type": "integer", "minimum": 0, "maximum": 10000000000 }
286
+ }
287
+ },
288
+ "model": { "type": "string", "minLength": 1, "maxLength": 120, "pattern": "^[A-Za-z0-9._:@/-]{1,120}$" },
289
+ "failure": { "$ref": "#/$defs/companyFailure" },
290
+ "process": { "$ref": "#/$defs/harnessProcess" },
198
291
  "toolCalls": {
199
292
  "type": "array",
200
293
  "maxItems": 64,