@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
@@ -5,34 +5,12 @@ import { matchesProtectedPath } from "../../extensions/policy-core.js";
5
5
  import type { TaskContract } from "../../extensions/guard-types.js";
6
6
  import { attachLocalImagesFromText, extractLocalImagePathCandidates } from "../input/chat-images.ts";
7
7
  import type { ChatImageAccessPolicy } from "../input/chat-images.ts";
8
- import { LONG_INPUT_CHARS } from "../runtime-limits.ts";
9
8
  import type { AuthorityResumeDecision } from "../policy/authority-resume-policy.ts";
10
- import { buildHandoffProjection, writeHandoffProjection } from "../recovery/handoff-projection.ts";
11
- import {
12
- isUncertainSendContinuation,
13
- terminalUncertainSendReceipt
14
- } from "../session/uncertain-send-continuation.ts";
15
- import { buildContextPreflight, buildUsageSnapshot } from "../session/usage.ts";
16
9
  import { registerAdaptiveContextGovernor } from "../session/adaptive-context-governor.ts";
17
- import { activeTaskToolGroups, toolGroupsForPrompt, PIAGENT_TOOL_NAMES, PIAGENT_TOOL_ORDER, PIAGENT_TOOL_GROUPS } from "../tools/tool-groups.ts";
10
+ import { toolGroupsForPrompt, PIAGENT_TOOL_NAMES, PIAGENT_TOOL_ORDER, PIAGENT_TOOL_GROUPS } from "../tools/tool-groups.ts";
18
11
  import type { PiagentToolGroup } from "../tools/tool-groups.ts";
19
12
  import type { RuntimeSessionState } from "../session/runtime-state.ts";
20
13
  import { observeHostWireInput } from "../session/runtime-state.ts";
21
- import {
22
- buildFreshCommand,
23
- agentStartTaskRequest,
24
- chooseFreshWorkflow,
25
- extractTaskRequest,
26
- isFreshOrUtilityInput,
27
- isPiagentWorkflowInput,
28
- looksLikeGovernedBoilerplate,
29
- trimTaskForInline
30
- } from "../workflows/input-routing.ts";
31
- import {
32
- automaticTaskIntakeMode,
33
- isLightweightNonAuthorizingChangeContinuation,
34
- manualTaskIntakeEligible
35
- } from "../workflows/task-intake.ts";
36
14
 
37
15
  type InputHookDependencies = {
38
16
  state: RuntimeSessionState;
@@ -54,191 +32,52 @@ export function projectPiagentWireInput(input: {
54
32
  const blocked = (reason: string) => ({ disposition: "blocked", reason });
55
33
  if (input.activeTask === undefined || !Array.isArray(input.readProtectedPaths)
56
34
  || typeof input.replacementIntake !== "boolean" || typeof input.dynamicToolsEnabled !== "boolean") return blocked("input-state-unfrozen");
57
- const text = input.text.trim();
35
+ const text = input.text;
58
36
  if (input.hasImages !== false || extractLocalImagePathCandidates(text, "/").length > 0) return blocked("input-images-unprojected");
59
- if (!text || isFreshOrUtilityInput(text) || /^\/piagent-workflow\b/i.test(text)) return blocked("input-transform-unprojected");
37
+ if (!text.trim()) return blocked("input-empty");
60
38
  if (!["rpc", "interactive", "extension"].includes(input.source)) return blocked("input-source-unfrozen");
61
- if (input.source !== "extension" && (looksLikeGovernedBoilerplate(text) || isPiagentWorkflowInput(text)
62
- || text.length >= LONG_INPUT_CHARS)) return blocked("input-preflight-unfrozen");
63
- const signal = classifyContextTask(text), activeTask = input.activeTask?.trace.outcome === "pending" ? input.activeTask : undefined;
64
- const onlyProtected = signal.paths.length > 0 && signal.paths.every((p) => matchesProtectedPath(p, input.readProtectedPaths));
65
- const runtimeMode = !activeTask ? automaticTaskIntakeMode(text, input.readProtectedPaths) : undefined;
66
- const manual = !activeTask && !runtimeMode && manualTaskIntakeEligible(text, input.readProtectedPaths);
67
- const groups = onlyProtected ? [] : toolGroupsForPrompt(text).filter((group) => !runtimeMode || group !== "intake" && group !== "task");
68
- if (manual && !groups.includes("intake")) groups.push("intake");
69
- const selectedGroups = activeTask ? [...groups.filter((group) => input.replacementIntake || group !== "intake"),
70
- ...(input.replacementIntake ? ["intake" as const] : activeTaskToolGroups(activeTask))] : groups;
39
+ const signal = classifyContextTask(text);
40
+ const protectedOnly = signal.paths.length > 0 && signal.paths.every((p) => matchesProtectedPath(p, input.readProtectedPaths));
41
+ const groups = protectedOnly ? [] : toolGroupsForPrompt(text).filter((g) => !["intake", "task", "recovery"].includes(g));
71
42
  const builtins = input.currentTools.filter((name) => !PIAGENT_TOOL_NAMES.has(name));
72
43
  if (builtins.includes("apply_patch")) {
73
- const firstWriter = builtins.findIndex((name) => name === "edit" || name === "write");
74
- if (firstWriter >= 0) { builtins.splice(builtins.indexOf("apply_patch"), 1); builtins.splice(builtins.findIndex((name) => name === "edit" || name === "write"), 0, "apply_patch"); }
44
+ const writer = builtins.findIndex((n) => n === "edit" || n === "write");
45
+ if (writer >= 0) { builtins.splice(builtins.indexOf("apply_patch"), 1); builtins.splice(builtins.findIndex((n) => n === "edit" || n === "write"), 0, "apply_patch"); }
75
46
  }
76
47
  const selectedTools = input.dynamicToolsEnabled ? [...builtins, ...PIAGENT_TOOL_ORDER.filter((name) =>
77
- selectedGroups.some((group) => PIAGENT_TOOL_GROUPS[group].includes(name as never)))] : [...input.currentTools];
48
+ groups.some((group) => PIAGENT_TOOL_GROUPS[group].includes(name as never)))] : [...input.currentTools];
78
49
  if (selectedTools.some((name) => !input.availableToolNames.includes(name))) return blocked("tool-definition-missing");
79
- const query = agentStartTaskRequest(input.expandedPrompt ?? text), querySignal = classifyContextTask(query);
80
- const protectedQuery = querySignal.paths.length > 0 && querySignal.paths.every((p) => matchesProtectedPath(p, input.readProtectedPaths));
81
- const agentStartRuntimeMode = !activeTask ? automaticTaskIntakeMode(query, input.readProtectedPaths) : undefined;
82
- if (runtimeMode || agentStartRuntimeMode || input.replacementIntake) return blocked("automatic-task-state-unprojected");
83
- if (activeTask && !input.phase) return blocked("current-task-phase-unfrozen");
84
- return { disposition: "known", reason: null, groups: selectedGroups, selectedTools,
85
- compactMode: protectedQuery ? "protected" : activeTask?.intakeMode === "runtime" ? "automatic" : null,
86
- taskPresence: activeTask ? "current" : "none", phase: activeTask ? input.phase : null,
50
+ return { disposition: "known", reason: null, groups, selectedTools,
51
+ compactMode: protectedOnly ? "protected" : null, taskPresence: "none", phase: null,
87
52
  inputHash: signal.promptHash, inputText: text };
53
+
88
54
  }
89
55
 
90
56
  export function registerInputHook(pi: ExtensionAPI, dependencies: InputHookDependencies): void {
91
- registerAdaptiveContextGovernor(pi, {
92
- activeTask: dependencies.activeTask,
93
- telemetry: dependencies.telemetry
94
- });
95
-
57
+ registerAdaptiveContextGovernor(pi, { activeTask: () => undefined, telemetry: dependencies.telemetry });
96
58
  pi.on("input", async (event, ctx) => {
97
- const text = event.text.trim();
98
- if (!text || isFreshOrUtilityInput(text)) return { action: "continue" };
99
- if (/^\/piagent-workflow\b/i.test(text)) {
100
- return { action: "transform", text: text.replace(/^\/piagent-workflow\b/i, "/workflow") };
101
- }
102
-
103
- const taskSignal = classifyContextTask(text);
104
- const sessionTask = dependencies.activeTask(ctx);
105
- const uncertainSendContinuation = (!Array.isArray(event.images) || event.images.length === 0)
106
- && isUncertainSendContinuation(text);
107
- const terminalReceipt = uncertainSendContinuation
108
- ? terminalUncertainSendReceipt(ctx.cwd, sessionTask)
109
- : undefined;
110
- if (terminalReceipt && event.source !== "extension") {
111
- dependencies.telemetry(ctx, {
112
- event: "uncertain_send_terminal_receipt_reemitted",
113
- source: event.source,
114
- promptHash: taskSignal.promptHash,
115
- taskId: terminalReceipt.details.taskId,
116
- taskRunId: terminalReceipt.details.taskRunId,
117
- outcome: terminalReceipt.details.outcome,
118
- completionApproved: terminalReceipt.details.completionApproved,
119
- replacementTaskStarted: false,
120
- replayed: false,
121
- modelTurnStarted: false
122
- });
123
- pi.sendMessage(
124
- {
125
- customType: terminalReceipt.customType,
126
- content: terminalReceipt.content,
127
- display: true,
128
- details: terminalReceipt.details
129
- },
130
- { triggerTurn: false }
131
- );
132
- return { action: "handled" };
133
- }
134
- if (!uncertainSendContinuation) {
135
- if (sessionTask && sessionTask.trace.outcome !== "pending") dependencies.state.clearTaskBoundary(ctx, sessionTask.taskRunId);
136
- else if (!sessionTask) {
137
- const cachedTask = dependencies.state.taskIdentity(ctx);
138
- if (cachedTask) dependencies.state.clearTaskBoundary(ctx, cachedTask.taskRunId);
139
- }
140
- }
141
- const activeTask = sessionTask?.trace.outcome === "pending" ? sessionTask : undefined;
142
- const turn = dependencies.state.beginTurn(ctx, taskSignal.promptHash, {
143
- lightweightNonAuthorizingChange: isLightweightNonAuthorizingChangeContinuation(text)
144
- });
145
- observeHostWireInput(ctx.cwd, ctx.sessionManager.getSessionId(), { turnId: turn.turnId,
146
- promptHash: taskSignal.promptHash, text, source: event.source,
147
- hasImages: Boolean(event.images?.length || extractLocalImagePathCandidates(text, ctx.cwd).length) });
148
- const authorityPolicy = activeTask?.trace.outcome === "pending" ? dependencies.authorityPolicy(ctx, activeTask) : undefined;
149
- let authorityHandoffReady = false;
150
- if (activeTask && authorityPolicy?.disposition === "new-attempt-required") {
151
- try {
152
- writeHandoffProjection(ctx.cwd, buildHandoffProjection(ctx.cwd, activeTask, {
153
- gate: { decision: "fail", missing: [`authority policy handoff: ${authorityPolicy.reason}`], missingVerifyCommands: [] },
154
- recovery: null
155
- }));
156
- authorityHandoffReady = true;
157
- } catch (error) {
158
- ctx.ui.notify(`Piagent authority-policy handoff could not be written: ${error instanceof Error ? error.message : String(error)}`, "warning");
159
- }
160
- }
161
- const replacementIntake = Boolean(activeTask && (
162
- activeTask.workingTreeDigestMigration?.status === "new-attempt-required" || authorityHandoffReady
163
- ));
164
- const readProtectedPaths = dependencies.readProtectedPaths(ctx);
165
- const protectedTarget = taskSignal.paths.some((candidate) => matchesProtectedPath(candidate, readProtectedPaths));
166
- const protectedOnlyTarget = taskSignal.paths.length > 0
167
- && taskSignal.paths.every((candidate) => matchesProtectedPath(candidate, readProtectedPaths));
168
- const runtimeIntakeMode = !activeTask ? automaticTaskIntakeMode(text, readProtectedPaths) : undefined;
169
- const runtimeIntake = Boolean(runtimeIntakeMode);
170
- const manualIntake = !activeTask && !runtimeIntake && manualTaskIntakeEligible(text, readProtectedPaths);
171
- const promptGroups = protectedOnlyTarget
172
- ? []
173
- : toolGroupsForPrompt(text).filter((group) => (
174
- runtimeIntake ? group !== "intake" && group !== "task" : true
175
- ));
176
- if (manualIntake && !promptGroups.includes("intake")) promptGroups.push("intake");
177
- dependencies.activateToolGroups(ctx, activeTask?.trace.outcome === "pending"
178
- ? [...promptGroups.filter((group) => replacementIntake || group !== "intake"), ...(replacementIntake ? ["intake" as const] : activeTaskToolGroups(activeTask))]
179
- : promptGroups);
180
- dependencies.telemetry(ctx, {
181
- event: "user_input",
182
- turnId: turn.turnId,
183
- source: event.source,
184
- promptHash: taskSignal.promptHash,
185
- promptChars: taskSignal.promptChars,
186
- workflow: taskSignal.workflow,
187
- riskLane: taskSignal.lane,
188
- intakeMode: runtimeIntake ? "runtime" : "model",
189
- taskMode: runtimeIntakeMode,
190
- protectedTarget,
191
- protectedOnlyTarget,
192
- manualIntake,
193
- explicitPathCount: taskSignal.paths.length,
194
- termCount: taskSignal.terms.length
59
+ // Preserve the actual message, including whitespace and slash-like text.
60
+ // Session/run lifecycle belongs to the host, not to prompt rewriting.
61
+ const text = event.text;
62
+ if (!text.trim()) return { action: "continue" };
63
+ const signal = classifyContextTask(text);
64
+ const cached = dependencies.state.taskIdentity(ctx);
65
+ if (cached) dependencies.state.clearTaskBoundary(ctx, cached.taskRunId);
66
+ const turn = dependencies.state.beginTurn(ctx, signal.promptHash);
67
+ observeHostWireInput(ctx.cwd, ctx.sessionManager.getSessionId(), {
68
+ turnId: turn.turnId, promptHash: signal.promptHash, text, source: event.source,
69
+ hasImages: Boolean(event.images?.length || extractLocalImagePathCandidates(text, ctx.cwd).length)
195
70
  });
196
-
197
- const imageAttachment = attachLocalImagesFromText(
198
- text,
199
- event.images,
200
- ctx.cwd,
201
- () => dependencies.imageAccess(ctx)
202
- );
203
- if (imageAttachment?.attached.length) {
204
- ctx.ui.notify(`Piagent image input: attached ${imageAttachment.attached.map((item) => item.marker).join(", ")}`, "info");
205
- } else if (imageAttachment?.skipped.length) {
206
- ctx.ui.notify(`Piagent image input: skipped ${imageAttachment.skipped.length} local image path(s)`, "warning");
207
- }
208
-
209
- const inputText = imageAttachment?.text ?? text;
210
- const canRewriteWorkflow = event.source !== "extension";
211
- const snapshot = buildUsageSnapshot(ctx, String(pi.getThinkingLevel()));
212
- const preflight = buildContextPreflight(snapshot, chooseFreshWorkflow(inputText, inputText), inputText.length);
213
- const hasBoilerplate = looksLikeGovernedBoilerplate(inputText);
214
- const shouldFreshen = canRewriteWorkflow
215
- && preflight.recommendation === "fresh-session"
216
- && (hasBoilerplate || isPiagentWorkflowInput(inputText) || inputText.length >= LONG_INPUT_CHARS);
217
- const shouldCollapseBoilerplate = canRewriteWorkflow
218
- && hasBoilerplate
219
- && inputText.length >= dependencies.boilerplateCollapseChars;
220
- const outgoingImages = [
221
- ...(Array.isArray(event.images) ? event.images : []),
222
- ...(imageAttachment?.images ?? [])
223
- ];
224
-
225
- if (!shouldFreshen && !shouldCollapseBoilerplate) {
226
- if (imageAttachment?.attached.length) return { action: "transform", text: inputText, images: outgoingImages };
227
- return { action: "continue" };
228
- }
229
-
230
- const task = extractTaskRequest(inputText);
231
- const workflow = chooseFreshWorkflow(inputText, task);
232
- const reason = shouldFreshen
233
- ? "Current session is near context limits; use a fresh governed session."
234
- : "Mandatory flow boilerplate is already part of the platform; collapse it to the task request.";
235
- const command = shouldFreshen
236
- ? buildFreshCommand(ctx.cwd, workflow, inputText, reason)
237
- : `/${workflow} ${trimTaskForInline(task)}`;
238
-
239
- ctx.ui.notify(`Piagent preflight: ${reason}`, "warning");
240
- return outgoingImages.length > 0
241
- ? { action: "transform", text: command, images: outgoingImages }
242
- : { action: "transform", text: command };
71
+ const protectedPaths = dependencies.readProtectedPaths(ctx);
72
+ const protectedOnly = signal.paths.length > 0 && signal.paths.every((p) => matchesProtectedPath(p, protectedPaths));
73
+ dependencies.activateToolGroups(ctx, protectedOnly ? [] : toolGroupsForPrompt(text)
74
+ .filter((group) => !["intake", "task", "recovery"].includes(group)));
75
+ dependencies.telemetry(ctx, { event: "user_input", turnId: turn.turnId, source: event.source,
76
+ promptHash: signal.promptHash, promptChars: text.length, inputMode: "freeform" });
77
+ const attachment = attachLocalImagesFromText(text, event.images, ctx.cwd, () => dependencies.imageAccess(ctx));
78
+ if (attachment?.attached.length) return { action: "transform", text: attachment.text,
79
+ images: [...(event.images ?? []), ...attachment.images] };
80
+ if (attachment?.skipped.length) ctx.ui.notify(`Skipped ${attachment.skipped.length} inaccessible image(s)`, "warning");
81
+ return { action: "continue" };
243
82
  });
244
83
  }
@@ -5,6 +5,7 @@ import { contextIndexV2Status } from "../../extensions/context-engine.js";
5
5
  import { resolveExecutionBackend } from "../../extensions/execution-backend.js";
6
6
  import type { ProjectProfile, TaskContract } from "../../extensions/guard-types.js";
7
7
  import { pruneCaptureFiles } from "../../extensions/state-retention.js";
8
+ import { ensureStateRootIgnored } from "../../extensions/local-state-path.js";
8
9
  import { migrateTaskState, readSessionTaskBinding } from "../../extensions/task-state.js";
9
10
  import {
10
11
  pruneTaskJournal
@@ -71,6 +72,7 @@ type SessionStartHookDependencies = {
71
72
 
72
73
  export function registerSessionStartHook(pi: ExtensionAPI, dependencies: SessionStartHookDependencies): void {
73
74
  pi.on("session_start", async (_event, ctx) => {
75
+ ensureStateRootIgnored(ctx.cwd);
74
76
  dependencies.activateToolGroups(ctx, []);
75
77
  const projectTrusted = ctx.isProjectTrusted();
76
78
  const explicitProfile = Boolean(process.env.PIAGENT_PROFILE?.trim());
@@ -210,7 +210,15 @@ export class PiApprovalBroker {
210
210
  #action(binding: Binding, draft: ApprovalActionDraft, cwd: string): Record<string, unknown> {
211
211
  const command = draft.commandPreview === null || draft.commandPreview === undefined ? null : safe(draft.commandPreview, 4_000);
212
212
  const parameter = safe(draft.parameterPreview ?? "No additional parameters", 4_000);
213
- const targetPaths = (draft.targetPaths ?? []).slice(0, 32).map((item) => safe(item, 500)).filter((item) => item.text).map((item) => item.text);
213
+ // Shown to the member: a path inside the project is written relative to it
214
+ // ("." is the project folder itself), so the card does not repeat the
215
+ // home directory; a path outside the project stays absolute.
216
+ const shown = (item: string) => {
217
+ if (!path.isAbsolute(item)) return item;
218
+ const relative = path.relative(cwd, item);
219
+ return relative === "" ? "." : !relative.startsWith("..") && !path.isAbsolute(relative) ? relative : item;
220
+ };
221
+ const targetPaths = (draft.targetPaths ?? []).slice(0, 32).map((item) => safe(shown(item), 500)).filter((item) => item.text).map((item) => item.text);
214
222
  const targetSummaries = (draft.targetSummaries ?? []).slice(0, 32).map((item) => safe(item, 500)).filter((item) => item.text).map((item) => item.text);
215
223
  const removed = [command, parameter].filter((item) => item?.redacted).length;
216
224
  const truncated = Boolean(command?.truncated || parameter.truncated || (draft.targetPaths?.length ?? 0) > 32 || (draft.targetSummaries?.length ?? 0) > 32);
@@ -0,0 +1,10 @@
1
+ export type ManagedFailure = { role: "main" | "scout" | "research" | "verify" | "review"; code: string; kind: string; requestId: string | null; local: boolean };
2
+ export function failureKind(code: string): string;
3
+ export function failureIsLocal(code: string): boolean;
4
+ export function failureCode(raw: unknown): string;
5
+ export function describeFailure(role: string, raw: unknown): string;
6
+ export function parseFailure(raw: unknown): ManagedFailure | null;
7
+ export function failureIsAdmissionRefusal(code: string): boolean;
8
+ export function failureReason(code: string): string;
9
+ export function unnamedFailureText(raw: unknown): string | null;
10
+ export function failureIsBriefRefusal(code: string): boolean;
@@ -0,0 +1,166 @@
1
+ // Why a company request failed, as one stable code and one sentence a member
2
+ // can act on. Studio answers with a machine code; the broker and this runtime
3
+ // have their own. The Terminal shows the sentence, the WebUI maps the code to
4
+ // its own copy, and nothing here names the harness model.
5
+ //
6
+ // Kinds: refused (the model's provider declined the request under its usage
7
+ // policy), quota (this key's token budget), provider-limit (the company's model
8
+ // account), account (that account must sign in again), busy (retry shortly),
9
+ // trial-limit (Studio's live-test ceiling), key (import the key again),
10
+ // policy (model or thinking not allowed),
11
+ // new-session (this conversation cannot continue), rejected (the request
12
+ // itself), unreachable (no connection to Studio), service (Studio answered
13
+ // but its model gateway failed), config-changed (key or harness changed while
14
+ // running), update (this Piagent is too old for the harness), tool (a tool's
15
+ // own failure), failed (anything else).
16
+ import { redactSensitiveText } from '../security/sensitive-data.js';
17
+
18
+ const CODES = {
19
+ token_quota_exhausted: ['quota', "this key's token quota is used up"],
20
+ upstream_rate_limited: ['provider-limit', "the company's model account is rate limited or out of usage at the provider"],
21
+ account_capacity_unavailable: ['provider-limit', 'no company model account has capacity for this model right now'],
22
+ concurrency_limit: ['busy', 'this key already runs its maximum number of requests'],
23
+ request_rate_limit: ['busy', 'this key sends requests too fast'],
24
+ helper_capacity_exhausted: ['busy', 'this key already runs its maximum number of subagents'],
25
+ connector_busy: ['busy', "Studio's model gateway is at capacity"],
26
+ operation_unavailable: ['busy', 'Studio could not check this request in time'],
27
+ account_recovering: ['busy', 'the company model account is being checked after an earlier failure'],
28
+ session_account_busy_retry_later: ['busy', "this conversation's model account is busy"],
29
+ session_account_not_ready_retry_later: ['busy', "this conversation's model account is not ready yet"],
30
+ live_trial_limit_reached: ['trial-limit', "Studio's live-test request ceiling is used up; an administrator has to raise it"],
31
+ authentication_required: ['key', 'the company key is no longer valid'],
32
+ invalid_run_grant: ['key', 'the company key or this run is no longer valid'],
33
+ run_grant_required: ['key', 'this key only works through Agent Watch'],
34
+ policy_denied: ['policy', 'this key may not use this model, thinking level or output size'],
35
+ thinking_level_unavailable: ['policy', 'this thinking level is not available for the harness'],
36
+ harness_model_unsupported: ['policy', 'the harness uses a model Studio cannot run'],
37
+ harness_profile_unavailable: ['policy', 'the team has no usable harness'],
38
+ execution_capability_unavailable: ['policy', 'no company model account can run this model'],
39
+ session_account_unavailable_start_new_session: ['new-session', "this conversation's model account is gone"],
40
+ run_state_conflict: ['config-changed', 'this run was closed or replaced'],
41
+ run_recovery_pending_usage: ['busy', "an earlier request of this run is still being accounted"],
42
+ invalid_request: ['rejected', 'Studio rejected the request as invalid'],
43
+ upstream_request_rejected: ['rejected', 'the model rejected the request (too long or not valid for it)'],
44
+ upstream_policy_refusal: ['refused', "the model's provider declined this request under its usage policy"],
45
+ request_too_large: ['rejected', 'the conversation is larger than Studio accepts in one request (4 MiB)'],
46
+ execution_precondition_failed: ['service', 'the chosen company model account changed before the request started'],
47
+ connector_unavailable: ['service', "Studio's model gateway is not available"],
48
+ connector_execution_failed: ['service', "Studio's model gateway failed"],
49
+ inference_unavailable: ['service', 'Studio could not run the request'],
50
+ managed_service_unavailable: ['service', 'Studio could not start the run'],
51
+ upstream_interrupted: ['service', 'the model response was interrupted'],
52
+ upstream_incomplete: ['service', 'the model returned no response'],
53
+ upstream_timeout: ['service', 'the model did not answer in time'],
54
+ upstream_unavailable: ['service', 'the model provider is not answering'],
55
+ upstream_auth_required: ['account', "the company's model account has to sign in again in Studio"],
56
+ 'studio-unreachable': ['unreachable', 'Studio could not be reached'],
57
+ 'managed-broker:offline': ['unreachable', 'Agent Watch could not reach Studio'],
58
+ 'managed-broker-timeout': ['unreachable', 'Agent Watch did not answer in time'],
59
+ 'managed-broker-disconnected': ['unreachable', 'the Agent Watch helper stopped'],
60
+ 'managed-broker:serverUnavailable': ['service', 'Studio could not start the run'],
61
+ 'managed-broker:upstreamUnavailable': ['service', 'Studio could not start the run'],
62
+ 'managed-broker:invalidKey': ['key', 'the company key is no longer valid'],
63
+ 'managed-broker:permissionDenied': ['key', 'Agent Watch no longer holds a usable company key'],
64
+ 'managed-broker:keychainApprovalRequired': ['key', 'macOS has not allowed Agent Watch to read the company key'],
65
+ 'managed-broker:quotaExceeded': ['quota', "this key's token quota is used up"],
66
+ 'managed-broker:rateLimited': ['busy', 'Studio is limiting this key right now'],
67
+ 'managed-broker:identityChanged': ['config-changed', 'the key, harness or run changed in Studio'],
68
+ 'managed-broker:invalidResponse': ['config-changed', 'Studio refused to start the run with the current harness'],
69
+ 'managed-route-changed': ['config-changed', 'the harness model changed while this run was active'],
70
+ 'managed-grant-invalid': ['config-changed', 'the harness changed and this conversation still has the old one'],
71
+ 'managed-configuration-unavailable': ['config-changed', 'the key has no usable harness'],
72
+ 'managed-main-model-unavailable': ['policy', 'the harness main model is not available to this key'],
73
+ 'managed-native-catalog-update-required': ['update', 'this Piagent does not know the harness model; update Piagent'],
74
+ 'managed-session-scope-changed': ['new-session', 'this conversation belongs to another member or another Studio'],
75
+ 'managed-run-required': ['config-changed', 'no company run is active'],
76
+ 'managed-helper-unavailable': ['busy', 'this subagent is already running'],
77
+ 'managed-helper-used': ['tool', 'this subagent already ran for this message; each runs once per message'],
78
+ 'managed-helper-not-configured': ['tool', 'the company Harness does not enable this subagent'],
79
+ 'managed-helper-limit': ['tool', 'this subagent already ran 8 times for this message'],
80
+ helper_run_limit_reached: ['tool', 'this subagent already ran 8 times for this message'],
81
+ 'managed-helper-cancelled': ['tool', 'the subagent was stopped'],
82
+ 'managed-helper-failed': ['failed', 'the subagent ended without an answer'],
83
+ 'web-search-empty': ['tool', 'the search returned nothing'],
84
+ 'managed-web-search-unavailable': ['tool', 'web search runs only on a Claude or Codex model; use web_fetch with a known address'],
85
+ search_unavailable: ['tool', 'no web search provider answered right now; try again shortly or use web_fetch with a known address'],
86
+ 'managed-session-open-elsewhere': ['new-session', 'this conversation is running in the Piagent WebUI; continue it there or start a new one'],
87
+ 'managed-session-lease-unavailable': ['failed', 'Piagent could not record which process runs this conversation'],
88
+ 'managed-plan-required': ['tool', 'for a complex task the Harness asks for a checklist (update_plan) before the first file edit'],
89
+ 'managed-plan-invalid': ['tool', 'the checklist was not in the expected form'],
90
+ 'managed-request-failed': ['failed', 'the request could not be sent'],
91
+ 'studio-request-failed': ['failed', 'Studio answered with an error'],
92
+ };
93
+ const ROLES = { main: 'main agent', scout: 'scout subagent', research: 'research subagent', verify: 'verify subagent', review: 'review subagent' };
94
+ const REQUEST = /\b([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\b/;
95
+ const NETWORK = /fetch failed|econnrefused|econnreset|enotfound|ehostunreach|etimedout|socket hang up|network|connection error|terminated|timed? ?out|aborted due to timeout/i;
96
+
97
+ export function failureKind(code) { return CODES[code]?.[0] ?? 'failed'; }
98
+ // Studio said no at admission, before any model account was asked: the same
99
+ // request can be asked again later, and nothing reaches the model twice. A
100
+ // failure after admission (the model's own error, an interrupted answer) is
101
+ // never in this list: replaying it could repeat billed work.
102
+ const ADMISSION_REFUSALS = new Set(['session_account_busy_retry_later', 'session_account_not_ready_retry_later',
103
+ 'concurrency_limit', 'account_capacity_unavailable', 'request_rate_limit']);
104
+ export function failureIsAdmissionRefusal(code) { return ADMISSION_REFUSALS.has(code); }
105
+ // Refused before the model did any work, for a reason that passes in seconds:
106
+ // the account's sign-in was being renewed (the provider refused the old one)
107
+ // or changed just before the request started, or the run's two-minute grant
108
+ // lapsed under the request (after the Mac slept, Studio's clock catches up
109
+ // at once), or Studio could not check the request's grant in its 5 s (many
110
+ // requests of one member at once); each ask renews the grant first. Asked
111
+ // again a few times only: an account that really has to sign in again, or a
112
+ // run that was closed, still says so.
113
+ const BRIEF_REFUSALS = new Set(['execution_precondition_failed', 'upstream_auth_required', 'invalid_run_grant', 'operation_unavailable']);
114
+ export function failureIsBriefRefusal(code) { return BRIEF_REFUSALS.has(code); }
115
+ // The words of a failure no code names, redacted, for a record nobody is
116
+ // shown; null for a named failure.
117
+ export function unnamedFailureText(raw) {
118
+ return ['managed-request-failed', 'studio-request-failed'].includes(failureCode(raw)) ? redactSensitiveText(String(raw ?? '')).text.slice(0, 600) : null;
119
+ }
120
+ // The sentence alone, for a status line that already says who is waiting.
121
+ export function failureReason(code) { return (CODES[code] ?? CODES['managed-request-failed'])[1]; }
122
+ // A failure Studio never saw: nothing was sent, so it is not a model request.
123
+ export function failureIsLocal(code) { return code.startsWith('managed-') || code === 'studio-unreachable'; }
124
+
125
+ function known(code) { return typeof code === 'string' && Object.hasOwn(CODES, code) ? code : null; }
126
+
127
+ // The stable code inside any error text this runtime can produce or receive.
128
+ export function failureCode(raw) {
129
+ const text = String(raw ?? '');
130
+ const tagged = /\[([A-Za-z0-9_:.-]{1,80})\](?: \(request [0-9a-f-]{36}\))?\s*$/.exec(text)?.[1];
131
+ if (known(tagged)) return tagged;
132
+ const studio = /"code"\s*:\s*"([a-z0-9_]{1,80})"/.exec(text)?.[1] ?? /web-search-failed: ([a-z0-9_-]{1,80})/.exec(text)?.[1];
133
+ if (studio === 'initial_request_limit_reached') return 'live_trial_limit_reached';
134
+ if (known(studio)) return studio;
135
+ const local = /(managed-broker:[A-Za-z_]{1,80}|managed-[a-z-]{1,80}|web-search-empty)/.exec(text)?.[1];
136
+ if (known(local)) return local;
137
+ if (studio || /\bAPI error \(\d{3}\)|^\d{3} \{/.test(text)) return 'studio-request-failed';
138
+ // The provider declined the request itself (its usage policy), not Studio:
139
+ // the same request is declined again, so it is said in words.
140
+ if (/usage policy|violative|refusals-and-fallback/i.test(text)) return 'upstream_policy_refusal';
141
+ // The provider library's words for an answer stream that ended empty: Studio
142
+ // admitted the request and nothing came back. It is not asked again.
143
+ if (/stream ended without a (terminal event|stop reason)|response has no body/i.test(text)) return 'upstream_incomplete';
144
+ if (NETWORK.test(text)) return 'studio-unreachable';
145
+ return 'managed-request-failed';
146
+ }
147
+
148
+ // "Agent Watch main agent: <what failed> [code] (request <id>)". The role says
149
+ // who failed; the model behind it is the harness's business, not the reader's.
150
+ export function describeFailure(role, raw) {
151
+ const code = failureCode(raw), request = REQUEST.exec(String(raw ?? ''))?.[1];
152
+ return `Agent Watch ${ROLES[role] ?? ROLES.main}: ${CODES[code][1]} [${code}]${request && !failureIsLocal(code) ? ` (request ${request})` : ''}`;
153
+ }
154
+
155
+ // What a reader of a stored error message needs: who, what kind, which code
156
+ // and Studio's request id when there is one. Null when it is not a company
157
+ // failure this module recognises the shape of.
158
+ export function parseFailure(raw) {
159
+ const text = String(raw ?? '');
160
+ if (!text.trim()) return null;
161
+ const code = failureCode(text);
162
+ const role = /^Agent Watch (main agent|(?:scout|research|verify|review) subagent): /.exec(text)?.[1]?.split(' ')[0]
163
+ ?? /(scout|research|verify|review) subagent/.exec(text)?.[1] ?? 'main';
164
+ const request = failureIsLocal(code) ? null : REQUEST.exec(text)?.[1] ?? null;
165
+ return { role, code, kind: failureKind(code), requestId: request, local: failureIsLocal(code) };
166
+ }
@@ -1,12 +1,11 @@
1
- export const MINIMAL_DELEGATION_POLICY_VERSION = "minimal-delegation-v1" as const;
1
+ export const MINIMAL_DELEGATION_POLICY_VERSION = "bounded-delegation-v2" as const;
2
2
 
3
3
  export const MINIMAL_DELEGATION_LIMITS = Object.freeze({
4
- maxConcurrentHelpers: 1,
5
- maxTotalHelpers: 1,
4
+ maxConcurrentHelpers: 2,
5
+ maxTotalHelpers: 2,
6
6
  maxRetries: 0,
7
7
  maxHelperCalls: 8,
8
- maxTransferTokens: 2_048,
9
- minimumProjectedSavingsRatio: 0.3
8
+ maxTransferTokens: 2_048
10
9
  });
11
10
 
12
11
  export type MinimalDelegationEvidence = {
@@ -30,29 +29,11 @@ function boundedInteger(value: unknown, minimum: number, maximum: number): value
30
29
  return Number.isInteger(value) && Number(value) >= minimum && Number(value) <= maximum;
31
30
  }
32
31
 
33
- function validWorkstreams(value: unknown): value is string[] {
34
- return Array.isArray(value)
35
- && value.length >= 2
36
- && value.length <= 8
37
- && new Set(value).size === value.length
38
- && value.every((item) => typeof item === "string" && /^[a-z0-9][a-z0-9:._/-]{0,127}$/i.test(item));
39
- }
40
-
41
- /**
42
- * Helpers are an optimization, not a default reasoning layer. A helper is only
43
- * eligible when the runtime can show two genuinely independent lanes, a fresh
44
- * minimal handoff, and at least 30% projected token savings after merge cost.
45
- * The estimate is advisory, while the one-helper ledger below is enforcing.
46
- */
32
+ /** Estimates are telemetry only. Actual role, context and concurrency budgets enforce delegation. */
47
33
  export function assessMinimalDelegation(evidence?: MinimalDelegationEvidence): MinimalDelegationAssessment {
48
- if (!evidence) return { eligible: false, projectedSavingsRatio: null, reasonCodes: ["delegation-evidence-missing", "parent-direct-default"] };
34
+ if (!evidence) return { eligible: true, projectedSavingsRatio: null, reasonCodes: ["delegation-estimate-unavailable"] };
49
35
 
50
36
  const reasons: string[] = [];
51
- if (evidence.source !== "runtime-estimate") reasons.push("delegation-estimate-not-runtime-owned");
52
- if (!validWorkstreams(evidence.independentWorkstreams)) reasons.push("independent-workstreams-not-proven");
53
- if (evidence.canRunWithoutParentOutput !== true) reasons.push("helper-depends-on-parent-output");
54
- if (!boundedInteger(evidence.estimatedSoloTokens, 1, 100_000_000)
55
- || !boundedInteger(evidence.estimatedTotalTokensWithHelper, 1, 100_000_000)) reasons.push("delegation-token-estimate-invalid");
56
37
  if (!boundedInteger(evidence.transferTokens, 0, MINIMAL_DELEGATION_LIMITS.maxTransferTokens)) reasons.push("helper-context-transfer-too-large");
57
38
  if (evidence.inheritedParentTokens !== 0) reasons.push("parent-history-inheritance-forbidden");
58
39
  if (evidence.retryOfDeterministicFailure !== false) reasons.push("deterministic-helper-retry-forbidden");
@@ -61,11 +42,9 @@ export function assessMinimalDelegation(evidence?: MinimalDelegationEvidence): M
61
42
  && boundedInteger(evidence.estimatedTotalTokensWithHelper, 1, 100_000_000)
62
43
  ? (evidence.estimatedSoloTokens - evidence.estimatedTotalTokensWithHelper) / evidence.estimatedSoloTokens
63
44
  : null;
64
- if (projectedSavingsRatio === null || projectedSavingsRatio < MINIMAL_DELEGATION_LIMITS.minimumProjectedSavingsRatio) {
65
- reasons.push("projected-token-savings-below-30pct");
66
- }
45
+
67
46
 
68
47
  return reasons.length
69
48
  ? { eligible: false, projectedSavingsRatio, reasonCodes: [...new Set(reasons), "parent-direct-default"] }
70
- : { eligible: true, projectedSavingsRatio, reasonCodes: ["independent-workstreams-proven", "projected-token-savings-at-least-30pct", "isolated-minimal-context"] };
49
+ : { eligible: true, projectedSavingsRatio, reasonCodes: ["bounded-helper-eligible", "isolated-minimal-context"] };
71
50
  }
@@ -10,7 +10,7 @@ export const OWNED_WORK_BUDGET_VERSION = "owned-work-budget-v1" as const;
10
10
  // Keep the complete ceiling shape for persisted/operator projections, but clamp
11
11
  // every caller to one read-only helper and zero retries/writers below.
12
12
  export type OwnedWorkCeilings = Readonly<{ maxConcurrentHelpers: number; maxTotalHelpers: number; maxScoutPasses: number; maxPlannerPasses: number; maxReviewPasses: number; maxOracleCalls: number; maxRepairPasses: number; maxWriters: number }>;
13
- export const ABSOLUTE_OWNED_WORK_CEILINGS: OwnedWorkCeilings = Object.freeze({ maxConcurrentHelpers: 1, maxTotalHelpers: 1, maxScoutPasses: 1, maxPlannerPasses: 1, maxReviewPasses: 1, maxOracleCalls: 1, maxRepairPasses: 0, maxWriters: 0 });
13
+ export const ABSOLUTE_OWNED_WORK_CEILINGS: OwnedWorkCeilings = Object.freeze({ maxConcurrentHelpers: 2, maxTotalHelpers: 2, maxScoutPasses: 1, maxPlannerPasses: 1, maxReviewPasses: 1, maxOracleCalls: 1, maxRepairPasses: 0, maxWriters: 0 });
14
14
  export const DEFAULT_OWNED_WORK_CEILINGS: OwnedWorkCeilings = ABSOLUTE_OWNED_WORK_CEILINGS;
15
15
  export const AUTOMATIC_OWNED_WORK_CEILINGS: OwnedWorkCeilings = ABSOLUTE_OWNED_WORK_CEILINGS;
16
16
  const LOCK_WAIT_MS = 5;
@@ -30,9 +30,11 @@ const REMEDIES: Array<{ match: RegExp; remedy: string }> = [
30
30
  },
31
31
  {
32
32
  match: /(touches|resolves to) protected path|access to protected path|write to protected path/i,
33
- remedy: "Use an approved `piagent-context` projection if one is available. Direct access requires the operator"
34
- + " to change the protected-path policy; task scope alone does not override it."
35
- + " `piagent explain '<command>'` shows which pattern matched."
33
+ // Models repeated "an approved piagent-context projection" to members, who
34
+ // could not act on it. Say what the member can do instead.
35
+ remedy: "This file is protected because it can hold credentials; do not look for another way to read it."
36
+ + " Tell the user it is protected: they can open it themselves or paste the non-secret part they need."
37
+ + " Only the operator can change the protected-path policy. `piagent explain '<command>'` shows which pattern matched."
36
38
  },
37
39
  {
38
40
  match: /write to read-only path/i,
@@ -446,7 +446,7 @@ export function registerContextCommands(pi: ExtensionAPI, deps: Record<string, a
446
446
  content: [
447
447
  `mode: ${orchestration.defaultMode}`,
448
448
  `helpersMode: ${helpersMode()}`,
449
- `helpers: max ${orchestration.maxConcurrentSubagents} fresh read-only total; requires >=30% projected net token saving`,
449
+ `helpers: max ${orchestration.maxConcurrentSubagents} fresh read-only total; no token-saving threshold`,
450
450
  `lenses: ${orchestration.defaultReviewLenses.join(", ")}`,
451
451
  `fieldGuide: ${orchestration.fieldGuide.enabled ? `${fieldGuidePath} (${fieldGuideExists ? "exists" : "missing"})` : "off"}`,
452
452
  "writer: parent only; helper workers and retries are disabled"
@@ -12,10 +12,10 @@ export function registerKnowledgeTools(pi: ExtensionAPI, deps: Record<string, an
12
12
  name: "piagent_orchestration_policy",
13
13
  label: "Piagent Orchestration Policy",
14
14
  description: "Return parent-direct helper limits, review lenses, model-role, and Field Guide policy for the current project.",
15
- promptSnippet: "Use this before planning medium/large tasks so the parent stays direct and helper use remains a measured token optimization.",
15
+ promptSnippet: "Read the enabled helper roles, limits and scope when delegation would help.",
16
16
  promptGuidelines: [
17
- "Default to the parent model working directly; review quality alone does not justify another model run.",
18
- "Use at most one fresh read-only helper only with two independent lanes and at least 30% projected total token savings; never delegate implementation or retry a deterministic failure.",
17
+ "Work directly by default; delegate focused research or patch review when useful.",
18
+ "Use at most two fresh read-only helpers; never delegate writes, nest helpers, or retry a deterministic failure.",
19
19
  "Treat Field Guide memory as advisory and verify it against current repository files."
20
20
  ],
21
21
  parameters: Type.Object({
@@ -45,9 +45,9 @@ export function registerKnowledgeTools(pi: ExtensionAPI, deps: Record<string, an
45
45
  path: fieldGuidePath,
46
46
  exists: fieldGuideExists
47
47
  },
48
- helperBudget: "1-readonly-total/0-retries/0-writers",
49
- minimumProjectedNetSaving: 0.3,
50
- stance: "parent-direct; one fresh read-only helper only when runtime-measured net token savings justify it"
48
+ helperBudget: "2-readonly-total/0-retries/0-writers",
49
+ minimumProjectedNetSaving: null,
50
+ stance: "main owns implementation; research and review are optional, bounded and read-only"
51
51
  };
52
52
  const text = params.detail === "full"
53
53
  ? JSON.stringify(payload, null, 2)
@@ -56,7 +56,7 @@ export function registerKnowledgeTools(pi: ExtensionAPI, deps: Record<string, an
56
56
  `maxConcurrentSubagents: ${payload.maxConcurrentSubagents}`,
57
57
  `helpersMode: ${payload.helpersMode}`,
58
58
  `helperBudget: ${payload.helperBudget}`,
59
- `minimumProjectedNetSaving: ${Math.round(payload.minimumProjectedNetSaving * 100)}%`,
59
+ "tokenSavingsRequirement: none",
60
60
  `reviewLenses: ${payload.defaultReviewLenses.join(", ")}`,
61
61
  `fieldGuide: ${payload.fieldGuide.enabled ? `${payload.fieldGuide.path} (${payload.fieldGuide.exists ? "exists" : "missing"})` : "off"}`,
62
62
  `fieldGuidePolicy: ${payload.fieldGuide.writePolicy}, maxLines=${payload.fieldGuide.maxLines}`,