@piagent/platform 1.5.4 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (230) hide show
  1. package/CHANGELOG.md +136 -0
  2. package/README.md +9 -8
  3. package/README.vi.md +9 -5
  4. package/adapters/web-frontend/profile.json +2 -4
  5. package/architecture/layers.json +18 -3
  6. package/benchmarks/deep-logic-v1/SPECIALIST_PROTOCOL.vi.md +122 -0
  7. package/benchmarks/deep-logic-v1/grade.mjs +272 -10
  8. package/benchmarks/deep-logic-v1/project/apps/admin/src/billing-summary.js +3 -0
  9. package/benchmarks/deep-logic-v1/project/benchmark-contract/public-smoke.test.js +166 -0
  10. package/benchmarks/deep-logic-v1/project/package.json +1 -1
  11. package/benchmarks/deep-logic-v1/project/packages/billing/src/close-period.js +3 -0
  12. package/benchmarks/deep-logic-v1/project/packages/billing/src/plan-timeline.js +3 -0
  13. package/benchmarks/deep-logic-v1/project/test/smoke.test.js +7 -1
  14. package/benchmarks/deep-logic-v1/prompts/budgeted-context-graph.md +2 -0
  15. package/benchmarks/deep-logic-v1/prompts/fair-dependency-scheduler.md +2 -0
  16. package/benchmarks/deep-logic-v1/prompts/layered-policy-resolution.md +2 -0
  17. package/benchmarks/deep-logic-v1/prompts/resumable-stream-assembly.md +3 -1
  18. package/benchmarks/deep-logic-v1/prompts/revision-event-reconciliation.md +2 -0
  19. package/benchmarks/deep-logic-v1/prompts/temporal-usage-billing-close.md +123 -0
  20. package/benchmarks/deep-logic-v1/prompts/transactional-config-merge.md +2 -0
  21. package/benchmarks/deep-logic-v1/suite.json +51 -3
  22. package/benchmarks/deep-logic-v1/variant.mjs +22 -1
  23. package/benchmarks/production-v1/spend-control.v1.json +68 -0
  24. package/benchmarks/production-v1/suite.json +42 -3
  25. package/catalog/capabilities.json +6 -6
  26. package/docs/command-reference-vietnamese.md +6 -6
  27. package/docs/context-engine.md +7 -1
  28. package/docs/distribution-standard.md +7 -7
  29. package/docs/en/architecture.md +1 -0
  30. package/docs/languages.json +4 -4
  31. package/docs/model-options.md +8 -5
  32. package/docs/operator-manual-vietnamese.md +13 -11
  33. package/docs/project-adapters.md +2 -2
  34. package/docs/publishing-for-teams.md +9 -0
  35. package/docs/quality-benchmark.md +271 -62
  36. package/docs/release-install-policy.md +10 -9
  37. package/docs/runtime-policy-design.md +4 -4
  38. package/docs/runtime-quality-baseline.md +13 -6
  39. package/docs/security-threat-model.md +2 -2
  40. package/docs/task-lifecycle-vietnamese.md +2 -2
  41. package/docs/team-onboarding.md +2 -2
  42. package/docs/usage-observability.md +7 -2
  43. package/docs/vi/architecture.md +1 -0
  44. package/docs-site/architecture.html +1 -1
  45. package/docs-site/benchmark.html +1 -1
  46. package/docs-site/commands.html +1 -1
  47. package/docs-site/context-engine.html +1 -1
  48. package/docs-site/en/architecture.html +1 -1
  49. package/docs-site/en/benchmark.html +1 -1
  50. package/docs-site/en/commands.html +1 -1
  51. package/docs-site/en/context-engine.html +1 -1
  52. package/docs-site/en/index.html +1 -1
  53. package/docs-site/en/mcp-approval.html +1 -1
  54. package/docs-site/en/mcp-auth.html +1 -1
  55. package/docs-site/en/mcp-servers.html +1 -1
  56. package/docs-site/en/mcp.html +1 -1
  57. package/docs-site/en/permissions.html +1 -1
  58. package/docs-site/en/profiles.html +1 -1
  59. package/docs-site/en/quickstart.html +1 -1
  60. package/docs-site/en/runtime.html +1 -1
  61. package/docs-site/en/scope.html +1 -1
  62. package/docs-site/en/security.html +1 -1
  63. package/docs-site/en/team.html +1 -1
  64. package/docs-site/en/whats-new.html +1 -1
  65. package/docs-site/en/workflows.html +1 -1
  66. package/docs-site/index.html +1 -1
  67. package/docs-site/mcp-approval.html +1 -1
  68. package/docs-site/mcp-auth.html +1 -1
  69. package/docs-site/mcp-servers.html +1 -1
  70. package/docs-site/mcp.html +1 -1
  71. package/docs-site/permissions.html +1 -1
  72. package/docs-site/profiles.html +1 -1
  73. package/docs-site/quickstart.html +1 -1
  74. package/docs-site/runtime.html +1 -1
  75. package/docs-site/scope.html +1 -1
  76. package/docs-site/security.html +1 -1
  77. package/docs-site/team.html +1 -1
  78. package/docs-site/whats-new.html +1 -1
  79. package/docs-site/workflows.html +1 -1
  80. package/evals/fixtures/benchmark-assurance-evidence.invalid.json +1 -1
  81. package/evals/fixtures/benchmark-assurance-evidence.valid.json +1 -1
  82. package/evals/fixtures/piagent-webui/session-live-state-v1.invalid.json +18 -0
  83. package/evals/fixtures/piagent-webui/session-live-state-v1.valid.json +27 -0
  84. package/evals/fs4-readiness-matrix.v1.json +2 -2
  85. package/evals/fs5-causal-arm.v1.json +2 -2
  86. package/evals/fs5-pilot-protocol.v1.json +2 -2
  87. package/evals/fs5-pilot-protocol.v4.json +6 -5
  88. package/evals/fs5-pilot-protocol.v5.json +7 -6
  89. package/evals/private-holdout-v1/public-exposure.v1.json +1 -1
  90. package/package.json +3 -1
  91. package/packages/piagent-core/benchmark/benchmark-assurance.js +4 -1
  92. package/packages/piagent-core/benchmark/benchmark-bootstrap.js +91 -9
  93. package/packages/piagent-core/benchmark/benchmark-claim-restrictions.js +38 -7
  94. package/packages/piagent-core/benchmark/benchmark-cli.js +7 -7
  95. package/packages/piagent-core/benchmark/benchmark-codex.js +3 -1
  96. package/packages/piagent-core/benchmark/benchmark-comparison.js +271 -2
  97. package/packages/piagent-core/benchmark/benchmark-core.js +732 -661
  98. package/packages/piagent-core/benchmark/benchmark-execution-guard.js +7 -5
  99. package/packages/piagent-core/benchmark/benchmark-forensics.js +439 -4
  100. package/packages/piagent-core/benchmark/benchmark-host-readiness.js +639 -0
  101. package/packages/piagent-core/benchmark/benchmark-matrix.js +22 -4
  102. package/packages/piagent-core/benchmark/benchmark-normalized-cost.js +317 -0
  103. package/packages/piagent-core/benchmark/benchmark-preflight.js +3 -1
  104. package/packages/piagent-core/benchmark/benchmark-process.js +6 -5
  105. package/packages/piagent-core/benchmark/benchmark-provider-wire.js +126 -0
  106. package/packages/piagent-core/benchmark/benchmark-record-validation.js +183 -0
  107. package/packages/piagent-core/benchmark/benchmark-report.js +116 -12
  108. package/packages/piagent-core/benchmark/benchmark-resume-recovery.js +14 -0
  109. package/packages/piagent-core/benchmark/benchmark-runner-policy.js +26 -0
  110. package/packages/piagent-core/benchmark/benchmark-stage-diagnostic.js +782 -0
  111. package/packages/piagent-core/benchmark/benchmark-statistics.js +64 -0
  112. package/packages/piagent-core/benchmark/benchmark-stop-policy.js +8 -5
  113. package/packages/piagent-core/benchmark/benchmark-suite-identity.js +31 -0
  114. package/packages/piagent-core/benchmark/benchmark-suite-runtime.js +13 -9
  115. package/packages/piagent-core/benchmark/benchmark-suite.js +128 -2
  116. package/packages/piagent-core/benchmark/benchmark-summary-support.js +376 -0
  117. package/packages/piagent-core/benchmark/benchmark-timing-diagnostics.js +971 -0
  118. package/packages/piagent-core/benchmark/benchmark-usage.js +311 -0
  119. package/packages/piagent-core/benchmark/benchmark-workflow.js +166 -0
  120. package/packages/piagent-core/docs/context-efficiency-improvement-plan.md +1080 -0
  121. package/packages/piagent-core/extensions/acceptance-contract-semantics.js +83 -69
  122. package/packages/piagent-core/extensions/acceptance-error-classes.js +160 -0
  123. package/packages/piagent-core/extensions/acceptance-executable-evidence.js +12 -9
  124. package/packages/piagent-core/extensions/acceptance-receipt.js +34 -11
  125. package/packages/piagent-core/extensions/acceptance-state-machine-evidence.js +518 -0
  126. package/packages/piagent-core/extensions/acceptance-test-binding-evidence.js +60 -0
  127. package/packages/piagent-core/extensions/context-efficiency-metrics.js +302 -0
  128. package/packages/piagent-core/extensions/context-efficiency-report.js +167 -0
  129. package/packages/piagent-core/extensions/context-engine.js +148 -145
  130. package/packages/piagent-core/extensions/context-evidence.js +48 -0
  131. package/packages/piagent-core/extensions/context-import-links.js +296 -0
  132. package/packages/piagent-core/extensions/context-telemetry-integrity.js +65 -0
  133. package/packages/piagent-core/extensions/criterion-context-pack.js +422 -7
  134. package/packages/piagent-core/extensions/criterion-graph.js +78 -8
  135. package/packages/piagent-core/extensions/guard-types.ts +3 -0
  136. package/packages/piagent-core/extensions/javascript-regex-evidence.js +32 -0
  137. package/packages/piagent-core/extensions/piagent-guard.ts +250 -43
  138. package/packages/piagent-core/extensions/policy-core.js +1 -1
  139. package/packages/piagent-core/extensions/task-contract-view.js +17 -3
  140. package/packages/piagent-core/extensions/task-lifecycle.js +7 -2
  141. package/packages/piagent-core/extensions/task-state.js +40 -134
  142. package/packages/piagent-core/extensions/verification-intelligence.js +3 -1
  143. package/packages/piagent-core/extensions/workspace-evidence-roots.js +157 -0
  144. package/packages/piagent-core/package.json +1 -1
  145. package/packages/piagent-core/policies/base-policy.json +3 -0
  146. package/packages/piagent-core/runtime/context/context-delivery.ts +103 -0
  147. package/packages/piagent-core/runtime/context/context-delta-shadow.ts +53 -0
  148. package/packages/piagent-core/runtime/context/context-efficiency-format.ts +47 -0
  149. package/packages/piagent-core/runtime/context/context-evidence-qualification.ts +32 -0
  150. package/packages/piagent-core/runtime/context/prefix-telemetry.ts +47 -0
  151. package/packages/piagent-core/runtime/context/repository-memory-hints.ts +27 -0
  152. package/packages/piagent-core/runtime/hooks/agent-prompt-telemetry.ts +56 -0
  153. package/packages/piagent-core/runtime/hooks/agent-start-hook.ts +162 -102
  154. package/packages/piagent-core/runtime/hooks/completion-hook.ts +5 -6
  155. package/packages/piagent-core/runtime/hooks/input-hook.ts +11 -1
  156. package/packages/piagent-core/runtime/hooks/session-hooks.ts +22 -2
  157. package/packages/piagent-core/runtime/hooks/session-start-hook.ts +7 -2
  158. package/packages/piagent-core/runtime/hooks/tool-result-content-guards.ts +95 -0
  159. package/packages/piagent-core/runtime/hooks/tool-result-hook.ts +84 -102
  160. package/packages/piagent-core/runtime/inspection/activity-event-adapter.ts +9 -3
  161. package/packages/piagent-core/runtime/inspection/activity-recovery.ts +104 -0
  162. package/packages/piagent-core/runtime/inspection/approval-broker.ts +29 -5
  163. package/packages/piagent-core/runtime/inspection/task-compaction-history.ts +1 -1
  164. package/packages/piagent-core/runtime/inspection/task-handoff-history.ts +1 -1
  165. package/packages/piagent-core/runtime/inspection/tool-failure-classification.ts +53 -0
  166. package/packages/piagent-core/runtime/inspection/webui-snapshot.ts +34 -6
  167. package/packages/piagent-core/runtime/model/authenticated-catalog.ts +17 -9
  168. package/packages/piagent-core/runtime/model/openai-codex-reasoning.ts +83 -0
  169. package/packages/piagent-core/runtime/model/provider-wire-fingerprint.ts +238 -0
  170. package/packages/piagent-core/runtime/orchestration/role-policy.ts +1 -1
  171. package/packages/piagent-core/runtime/product/activity-inspector.ts +7 -2
  172. package/packages/piagent-core/runtime/product/efficiency-metrics.ts +4 -2
  173. package/packages/piagent-core/runtime/quality/exact-output-contract.ts +2 -1
  174. package/packages/piagent-core/runtime/quality/performance-assurance-signals.ts +23 -0
  175. package/packages/piagent-core/runtime/quality/performance-assurance.ts +6 -2
  176. package/packages/piagent-core/runtime/recovery/edit-recovery-context.ts +113 -0
  177. package/packages/piagent-core/runtime/recovery/edit-recovery-delivery.ts +42 -0
  178. package/packages/piagent-core/runtime/recovery/semantic-repair-handshake.ts +55 -2
  179. package/packages/piagent-core/runtime/recovery/semantic-repair-runtime.ts +55 -2
  180. package/packages/piagent-core/runtime/registration/context-commands.ts +66 -5
  181. package/packages/piagent-core/runtime/registration/policy-tools.ts +46 -11
  182. package/packages/piagent-core/runtime/registration/task-completion-tools.ts +49 -16
  183. package/packages/piagent-core/runtime/registration/task-start-guidance.ts +163 -0
  184. package/packages/piagent-core/runtime/registration/task-start-retry-helpers.ts +37 -0
  185. package/packages/piagent-core/runtime/registration/task-start-tool.ts +67 -62
  186. package/packages/piagent-core/runtime/session/runtime-state.ts +154 -2
  187. package/packages/piagent-core/runtime/session/system-prompt.ts +4 -4
  188. package/packages/piagent-core/runtime/tools/apply-patch-tool.ts +500 -0
  189. package/packages/piagent-core/runtime/trajectory/trajectory-runtime.ts +2 -1
  190. package/packages/piagent-core/runtime/workflows/task-intake.ts +43 -2
  191. package/packages/piagent-core/security/sensitive-data.js +13 -218
  192. package/packages/piagent-core/security/sensitive-project-file.js +42 -0
  193. package/packages/piagent-core/security/sensitive-source-expression.js +330 -0
  194. package/packages/piagent-core/security/sensitive-source-formats.js +394 -0
  195. package/packages/piagent-core/security/sensitive-text.js +234 -0
  196. package/packages/piagent-core/subagents/piagent-worker.md +1 -1
  197. package/packages/piagent-webui/dist/client/assets/{MarkdownMessage-rD9WJKxs.js → MarkdownMessage-CxeZqTte.js} +1 -1
  198. package/packages/piagent-webui/dist/client/assets/index-DynIMe8I.js +176 -0
  199. package/packages/piagent-webui/dist/client/index.html +1 -1
  200. package/packages/piagent-webui/gateway/gateway-events.ts +109 -5
  201. package/packages/piagent-webui/gateway/gateway-service.ts +3 -0
  202. package/packages/piagent-webui/gateway/gateway-session-stream.ts +93 -7
  203. package/packages/piagent-webui/gateway/session-command-controller.ts +18 -3
  204. package/packages/piagent-webui/gateway/session-effective-options.ts +47 -0
  205. package/packages/piagent-webui/gateway/session-live-state.ts +21 -0
  206. package/packages/piagent-webui/gateway/session-operation-start.ts +5 -1
  207. package/packages/piagent-webui/gateway/session-operation-watchdog.ts +158 -0
  208. package/packages/piagent-webui/gateway/session-runtime-factory.ts +45 -0
  209. package/packages/piagent-webui/gateway/session-runtime-supervisor.ts +139 -120
  210. package/packages/piagent-webui/server/loopback-server.ts +8 -0
  211. package/packages/piagent-webui/server/transcript-projection.ts +22 -4
  212. package/packages/piagent-webui/shared/text-visibility.ts +10 -0
  213. package/schemas/piagent-webui/catalog-v1.json +9 -0
  214. package/schemas/piagent-webui/gateway-protocol-v1.schema.json +53 -3
  215. package/schemas/piagent-webui/session-live-state-v1.schema.json +76 -0
  216. package/schemas/task-contract.schema.json +12 -1
  217. package/scripts/benchmark-runner-core.mjs +355 -359
  218. package/scripts/benchmark-runner-finalization.mjs +309 -0
  219. package/scripts/benchmark-runner-host-readiness.mjs +94 -0
  220. package/scripts/benchmark-runner-support.mjs +200 -0
  221. package/scripts/benchmark-session.mjs +95 -41
  222. package/scripts/configure-model-scope.sh +4 -3
  223. package/scripts/helper-evaluation.mjs +1 -1
  224. package/scripts/install-global.sh +2 -2
  225. package/scripts/pi-usage-history.mjs +37 -10
  226. package/scripts/piagent-dashboard.mjs +13 -1
  227. package/scripts/setup.sh +2 -2
  228. package/scripts/verify-local.sh +5 -1
  229. package/templates/global/settings.json +2 -1
  230. package/packages/piagent-webui/dist/client/assets/index-BZRFw1u6.js +0 -174
@@ -1,4 +1,5 @@
1
1
  import { acceptanceReceiptProvenanceSummary, acceptanceReceiptSummary } from "./acceptance-receipt.js";
2
+ import { isDurableContextEvidenceEntry } from "./context-evidence.js";
2
3
  import { runtimeLifecycleMode } from "./task-lifecycle.js";
3
4
  import { latestObservedVerificationEvidence, meaningfulVerificationCommands, verificationEvidenceProvesStableTree } from "./verification-intelligence.js";
4
5
  import { isCurrentWorkingTreeDigest, WORKING_TREE_DIGEST_ALGORITHM } from "./working-tree-digest.js";
@@ -25,6 +26,7 @@ export function compactTaskDetails(task) {
25
26
  sessionId: task.sessionId,
26
27
  sessionName: task.sessionName,
27
28
  changeMode: task.changeMode,
29
+ mutationPolicy: task.mutationPolicy ?? (task.changeMode === "read-only" ? "forbidden" : "required"),
28
30
  attempt: task.attempt,
29
31
  maxAttempts: task.maxAttempts,
30
32
  previousAttempts: task.previousAttempts,
@@ -51,12 +53,24 @@ export function compactTaskDetails(task) {
51
53
  }
52
54
 
53
55
  export function mergeObservedTaskContext(task, entries, maxManifestFiles, redact = (value) => String(value ?? "")) {
54
- const known = new Set((task.contextManifest ?? []).map((item) => item.path));
56
+ const known = new Map((task.contextManifest ?? []).map((item, index) => [item.path, index]));
57
+ let durableCount = (task.contextManifest ?? []).filter(isDurableContextEvidenceEntry).length;
55
58
  const added = [];
56
59
  for (const entry of entries ?? []) {
57
- if (task.contextManifest.length >= maxManifestFiles || known.has(entry.path)) continue;
60
+ const existingIndex = known.get(entry.path);
61
+ if (existingIndex !== undefined) {
62
+ const existing = task.contextManifest[existingIndex];
63
+ if (isDurableContextEvidenceEntry(existing) || !isDurableContextEvidenceEntry(entry)) continue;
64
+ if (durableCount >= maxManifestFiles) continue;
65
+ task.contextManifest[existingIndex] = { path: entry.path, reason: redact(entry.reason) };
66
+ durableCount += 1;
67
+ added.push(entry.path);
68
+ continue;
69
+ }
70
+ if (durableCount >= maxManifestFiles || !isDurableContextEvidenceEntry(entry)) continue;
58
71
  task.contextManifest.push({ path: entry.path, reason: redact(entry.reason) });
59
- known.add(entry.path);
72
+ durableCount += 1;
73
+ known.set(entry.path, task.contextManifest.length - 1);
60
74
  added.push(entry.path);
61
75
  }
62
76
  return added;
@@ -31,16 +31,20 @@ const ASSISTED_READ_ONLY_PLAN = [
31
31
  { id: "review", role: "piagent-reviewer", mode: "review", dependsOn: ["scout"] }
32
32
  ];
33
33
 
34
+ function mutationForbidden(task) {
35
+ return task?.changeMode === "read-only" || task?.mutationPolicy === "forbidden";
36
+ }
37
+
34
38
  export function runtimeLifecycleMode(task) {
35
39
  if (!Array.isArray(task?.workPlan)) return "manual";
36
40
  if (
37
- task.changeMode === "read-only"
41
+ mutationForbidden(task)
38
42
  && (task.riskLane === "tiny" || (task.intakeMode === "runtime" && task.riskLane === "normal"))
39
43
  && task.workPlan.length === AUTOMATIC_READ_ONLY_PLAN.length
40
44
  && task.workPlan.every((step, index) => matchesStep(step, AUTOMATIC_READ_ONLY_PLAN[index]))
41
45
  ) return "automatic-readonly";
42
46
  if (
43
- task.changeMode === "read-only"
47
+ mutationForbidden(task)
44
48
  && task.riskLane === "normal"
45
49
  && task.workPlan.length === ASSISTED_READ_ONLY_PLAN.length
46
50
  && task.workPlan.every((step, index) => matchesStep(step, ASSISTED_READ_ONLY_PLAN[index]))
@@ -68,6 +72,7 @@ function nonEmptyStrings(value) {
68
72
  function sourceContractReady(task) {
69
73
  return task?.schemaVersion === 2
70
74
  && task.changeMode === "source-change"
75
+ && task.mutationPolicy !== "forbidden"
71
76
  && task.trace?.outcome === "pending"
72
77
  && nonEmptyStrings(task.acceptanceCriteria)
73
78
  && nonEmptyStrings(task.scope)
@@ -1,7 +1,6 @@
1
1
  import crypto from "node:crypto";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
- import { execFileSync } from "node:child_process";
5
4
  import { ensurePrivateStateDirectory, resolveLocalStatePath } from "./local-state-path.js";
6
5
  import { appendTaskJournalEvent, readTaskJournal } from "./task-journal.js";
7
6
  import {
@@ -28,12 +27,25 @@ import {
28
27
  unavailableWorkingTreeHash,
29
28
  versionWorkingTreeHash
30
29
  } from "./working-tree-digest.js";
30
+ import {
31
+ directChildGitEvidenceRoots,
32
+ gitEvidenceRootDetails,
33
+ gitEvidenceRoots,
34
+ gitOutput,
35
+ isGitWorkingTree,
36
+ nonGitWorkspaceFileDetails,
37
+ nonGitWorkspaceFiles,
38
+ normalizedGitPath,
39
+ pathWithinNonGitWorkspaceEvidenceRoot,
40
+ prefixedGitPath
41
+ } from "./workspace-evidence-roots.js";
42
+ export { isGitWorkingTree } from "./workspace-evidence-roots.js";
31
43
 
32
44
  export const TASK_CONTRACT_SCHEMA_VERSION = 2; export const DEFAULT_MAX_TASK_ATTEMPTS = 3;
33
45
  const TASK_OUTCOMES = ["pending", "completed", "blocked", "partial", "failed"], TERMINAL_TASK_OUTCOMES = new Set(["completed", "blocked", "partial", "failed"]);
34
46
  const REVIEW_LENSES = ["correctness", "tests", "scope", "security", "docs", "release", "package"];
35
47
  const TASK_CONTRACT_FIELDS = new Set([
36
- "schemaVersion", "taskRunId", "taskId", "sessionId", "sessionName", "changeMode", "attempt", "maxAttempts",
48
+ "schemaVersion", "taskRunId", "taskId", "sessionId", "sessionName", "changeMode", "mutationPolicy", "attempt", "maxAttempts",
37
49
  "previousAttempts", "summary", "riskLane", "intakeMode", "expectedOutput", "acceptanceCriteria", "criterionGraph", "scope", "outOfScope",
38
50
  "protectedPaths", "requiredContext", "contextManifest", "memoryCitations", "mcpCapabilities", "verifyGroup",
39
51
  "verifyCommands", "workPlan", "reviewLenses", "orchestration", "acceptanceReceipt", "authoritySnapshot", "workingTreeDigestAlgorithm", "workingTreeDigestMigration", "baselineChangedFiles", "baselineFileDigests",
@@ -47,14 +59,6 @@ const ORCHESTRATION_FIELDS = new Set(["mode", "subagents", "reason", "fieldGuide
47
59
  const MODEL_ROLE_FIELDS = new Set(["planner", "worker", "reviewer", "watchdog"]);
48
60
  const VERIFY_EVIDENCE_FIELDS = new Set(["command", "exitCode", "summary", "recordedAt", "observed", "observedAt", "isError", "matchedProfileCommand", "preWorkingTreeDigest", "workingTreeDigest"]);
49
61
  const TRACE_FIELDS = new Set(["outcome", "friction", "notes", "recordedAt"]);
50
- const WORKSPACE_EVIDENCE_MAX_FILES = 2000;
51
- const WORKSPACE_EVIDENCE_SKIP_NAMES = new Set([
52
- ".git", ".hg", ".svn", ".pi", "node_modules", "dist", "build", ".next", "coverage"
53
- ]);
54
- const WORKSPACE_EVIDENCE_PRIORITY_NAMES = new Map([
55
- ["plans", 0]
56
- ]);
57
-
58
62
  export function safeTaskId(value) {
59
63
  const normalized = String(value ?? "")
60
64
  .trim()
@@ -198,6 +202,8 @@ export function taskContractValidationErrors(input) {
198
202
  }
199
203
  if (input.failedAt !== undefined && !["research", "plan", "execute", "verify", "review"].includes(input.failedAt)) errors.push("failedAt is invalid");
200
204
  if (!["source-change", "read-only"].includes(input.changeMode)) errors.push("changeMode is invalid");
205
+ if (input.mutationPolicy !== undefined && !["required", "forbidden"].includes(input.mutationPolicy)) errors.push("mutationPolicy is invalid");
206
+ if (input.changeMode === "read-only" && input.mutationPolicy === "required") errors.push("read-only tasks cannot require mutation");
201
207
  if (!["tiny", "normal", "high-risk"].includes(input.riskLane)) errors.push("riskLane is invalid");
202
208
  if (input.intakeMode !== undefined && !["model", "runtime"].includes(input.intakeMode)) errors.push("intakeMode is invalid");
203
209
  errors.push(...taskDigestContractValidationErrors(input));
@@ -227,6 +233,7 @@ export function taskContractValidationErrors(input) {
227
233
  }
228
234
  if (Array.isArray(input.acceptanceCriteria) && input.acceptanceCriteria.length === 0) errors.push("acceptanceCriteria must not be empty");
229
235
  if (Array.isArray(input.scope) && input.scope.length === 0) errors.push("scope must not be empty");
236
+ if (Array.isArray(input.scope) && input.scope.length > 2000) errors.push("scope must contain at most 2000 entries");
230
237
  if (Array.isArray(input.previousAttempts) && input.previousAttempts.length > 10) errors.push("previousAttempts must contain at most 10 entries");
231
238
  if (Array.isArray(input.workPlan) && input.workPlan.length > 12) errors.push("workPlan must contain at most 12 entries");
232
239
  if (Array.isArray(input.reviewLenses)) {
@@ -390,6 +397,7 @@ export function normalizeTaskContract(input, options = {}) {
390
397
  sessionId,
391
398
  sessionName: String(input.sessionName ?? options.sessionName ?? "").trim() || undefined,
392
399
  changeMode: input.changeMode === "read-only" ? "read-only" : "source-change",
400
+ mutationPolicy: input.changeMode === "read-only" || input.mutationPolicy === "forbidden" ? "forbidden" : "required",
393
401
  attempt: positiveInteger(input.attempt, 1, 100),
394
402
  maxAttempts: positiveInteger(input.maxAttempts, DEFAULT_MAX_TASK_ATTEMPTS, 10),
395
403
  previousAttempts: Array.isArray(input.previousAttempts) ? input.previousAttempts.slice(-10).map((item) => pickFields(item, PREVIOUS_ATTEMPT_FIELDS)) : [],
@@ -576,13 +584,6 @@ export function summarizeAttempt(task) {
576
584
  };
577
585
  }
578
586
 
579
- function gitOutput(cwd, args, options = {}) {
580
- return execFileSync("git", ["-C", cwd, ...args], {
581
- encoding: options.encoding ?? "utf8",
582
- maxBuffer: options.maxBuffer ?? 16 * 1024 * 1024,
583
- stdio: ["ignore", "pipe", "ignore"]
584
- });
585
- }
586
587
  function gitHasHead(cwd) {
587
588
  try {
588
589
  gitOutput(cwd, ["rev-parse", "--verify", "HEAD"]);
@@ -591,20 +592,6 @@ function gitHasHead(cwd) {
591
592
  return false;
592
593
  }
593
594
  }
594
- export function isGitWorkingTree(cwd) {
595
- try {
596
- return gitOutput(cwd, ["rev-parse", "--is-inside-work-tree"]).trim() === "true";
597
- } catch {
598
- return false;
599
- }
600
- }
601
- function normalizedGitPath(value) {
602
- return String(value ?? "").replaceAll("\\", "/").replace(/^\/+/, "");
603
- }
604
- function prefixedGitPath(prefix, file) {
605
- const normalized = normalizedGitPath(file);
606
- return prefix ? `${prefix}/${normalized}` : normalized;
607
- }
608
595
 
609
596
  function workingTreeFilesForGitRoot(cwd, observeRename) {
610
597
  if (!isGitWorkingTree(cwd)) return [];
@@ -615,90 +602,8 @@ function workingTreeFilesForGitRoot(cwd, observeRename) {
615
602
  return [...new Set([...changed, ...untracked].map(normalizedGitPath).filter((item) => item && !item.startsWith(".pi/piagent-state/")))].sort();
616
603
  }
617
604
 
618
- function directChildGitEvidenceRoots(cwd) {
619
- const roots = [];
620
- let entries;
621
- try {
622
- entries = fs.readdirSync(cwd, { withFileTypes: true });
623
- } catch {
624
- return roots;
625
- }
626
- for (const entry of entries) {
627
- if (!entry.isDirectory() || entry.name.startsWith(".") || entry.name === "node_modules") continue;
628
- const child = path.join(cwd, entry.name);
629
- try {
630
- if (fs.lstatSync(child).isSymbolicLink()) continue;
631
- if (!isGitWorkingTree(child)) continue;
632
- const topLevel = gitOutput(child, ["rev-parse", "--show-toplevel"]).trim();
633
- if (fs.realpathSync(topLevel) !== fs.realpathSync(child)) continue;
634
- roots.push({ cwd: child, prefix: normalizedGitPath(entry.name) });
635
- } catch {
636
- // Ignore unreadable or non-standard child folders; source-change start
637
- // only needs at least one reliable evidence root.
638
- }
639
- }
640
- return roots.sort((left, right) => left.prefix.localeCompare(right.prefix));
641
- }
642
-
643
- function gitEvidenceRoots(cwd) {
644
- if (isGitWorkingTree(cwd)) return [{ cwd, prefix: "" }];
645
- return directChildGitEvidenceRoots(cwd);
646
- }
647
-
648
- function nonGitWorkspaceFiles(cwd, gitRootPrefixes) {
649
- const files = [];
650
- const gitRoots = new Set(gitRootPrefixes.filter(Boolean));
651
- function orderedEntries(entries) {
652
- return [...entries].sort((left, right) => {
653
- const leftPriority = WORKSPACE_EVIDENCE_PRIORITY_NAMES.get(left.name.toLowerCase()) ?? 10;
654
- const rightPriority = WORKSPACE_EVIDENCE_PRIORITY_NAMES.get(right.name.toLowerCase()) ?? 10;
655
- if (leftPriority !== rightPriority) return leftPriority - rightPriority;
656
- return left.name.localeCompare(right.name, "en-US");
657
- });
658
- }
659
- function visit(directory, relativeDirectory) {
660
- if (files.length >= WORKSPACE_EVIDENCE_MAX_FILES) return;
661
- let entries;
662
- try {
663
- entries = fs.readdirSync(directory, { withFileTypes: true });
664
- } catch {
665
- return;
666
- }
667
- for (const entry of orderedEntries(entries)) {
668
- if (files.length >= WORKSPACE_EVIDENCE_MAX_FILES) return;
669
- if (WORKSPACE_EVIDENCE_SKIP_NAMES.has(entry.name)) continue;
670
- const relative = relativeDirectory ? `${relativeDirectory}/${entry.name}` : entry.name;
671
- const topSegment = relative.split("/")[0];
672
- if (gitRoots.has(topSegment)) continue;
673
- const absolute = path.join(directory, entry.name);
674
- try {
675
- if (fs.lstatSync(absolute).isSymbolicLink()) continue;
676
- } catch {
677
- continue;
678
- }
679
- if (entry.isDirectory()) {
680
- visit(absolute, relative);
681
- } else if (entry.isFile()) {
682
- files.push(normalizedGitPath(relative));
683
- }
684
- }
685
- }
686
- visit(cwd, "");
687
- return files.sort();
688
- }
689
-
690
605
  function nonGitWorkspaceFileDigest(cwd, file, protectedProjectPath = null) { return streamedWorkingTreeFileDigest(cwd, file, false, protectedProjectPath); }
691
606
 
692
- function pathWithinNonGitWorkspaceEvidenceRoot(candidate, gitRootPrefixes) {
693
- const topSegment = candidate.split("/")[0];
694
- return Boolean(
695
- topSegment
696
- && !WORKSPACE_EVIDENCE_SKIP_NAMES.has(topSegment)
697
- && !topSegment.startsWith(".")
698
- && !gitRootPrefixes.includes(topSegment)
699
- );
700
- }
701
-
702
607
  export function hasGitEvidenceRoot(cwd) {
703
608
  return gitEvidenceRoots(cwd).length > 0;
704
609
  }
@@ -739,35 +644,36 @@ export function workingTreeFiles(cwd) {
739
644
  return [];
740
645
  }
741
646
  }
742
-
743
- export function repositoryFileManifest(cwd, maxFiles = 10_000) {
744
- const limit = Number.isInteger(maxFiles) ? Math.max(1, Math.min(50_000, maxFiles)) : 10_000;
745
- const files = [];
647
+ export function repositoryFileManifestDetails(cwd, maxFiles = 10_000) {
648
+ const limit = Number.isInteger(maxFiles) ? Math.max(1, Math.min(50_000, maxFiles)) : 10_000, files = new Set();
746
649
  try {
747
- for (const root of gitEvidenceRoots(cwd)) {
748
- const listed = gitOutput(root.cwd, ["ls-files", "--cached", "--others", "--exclude-standard", "-z"])
749
- .split("\0")
750
- .filter(Boolean);
650
+ const rootDetails = gitEvidenceRootDetails(cwd), roots = rootDetails.roots;
651
+ const add = (candidate) => {
652
+ if (!candidate || candidate.startsWith(".pi/") || candidate.startsWith(".git/") || candidate.split("/").includes("node_modules")
653
+ || /(?:^|\/)\.env(?:\.|$)/i.test(candidate) || /(?:^|\/)(?:auth|credentials?|secrets?|tokens?)\.json$/i.test(candidate)) return true;
654
+ files.add(candidate);
655
+ return files.size <= limit;
656
+ };
657
+ for (const root of roots) {
658
+ const listed = gitOutput(root.cwd, ["ls-files", "--cached", "--others", "--exclude-standard", "-z"]).split("\0").filter(Boolean);
751
659
  for (const file of listed) {
752
660
  const candidate = prefixedGitPath(root.prefix, file);
753
- if (
754
- !candidate
755
- || candidate.startsWith(".pi/")
756
- || candidate.startsWith(".git/")
757
- || candidate.split("/").includes("node_modules")
758
- || /(?:^|\/)\.env(?:\.|$)/i.test(candidate)
759
- || /(?:^|\/)(?:auth|credentials?|secrets?|tokens?)\.json$/i.test(candidate)
760
- ) continue;
761
- files.push(candidate);
762
- if (files.length >= limit) return [...new Set(files)].sort();
661
+ if (!add(candidate)) return { files: [...files].slice(0, limit).sort(), complete: false, candidateCount: files.size };
763
662
  }
764
663
  }
664
+ if (!isGitWorkingTree(cwd) && roots.length > 0) {
665
+ const looseDetails = nonGitWorkspaceFileDetails(cwd, roots.map((root) => root.prefix));
666
+ const looseFiles = looseDetails.files;
667
+ for (const file of looseFiles) if (!add(file)) return { files: [...files].slice(0, limit).sort(), complete: false, candidateCount: files.size };
668
+ if (!looseDetails.complete) return { files: [...files].sort(), complete: false, candidateCount: files.size };
669
+ }
670
+ if (!rootDetails.complete) return { files: [...files].sort(), complete: false, candidateCount: files.size };
765
671
  } catch {
766
- return [];
672
+ return { files: [], complete: false, candidateCount: 0 };
767
673
  }
768
- return [...new Set(files)].sort();
674
+ return { files: [...files].sort(), complete: true, candidateCount: files.size };
769
675
  }
770
-
676
+ export function repositoryFileManifest(cwd, maxFiles = 10_000) { return repositoryFileManifestDetails(cwd, maxFiles).files; }
771
677
  function changedPathsFromNameStatus(output, observeRename) {
772
678
  const fields = String(output ?? "").split("\0").filter(Boolean);
773
679
  const files = [];
@@ -295,7 +295,9 @@ export function selectVerificationPlan(profile, requestedGroup, changeMode, cwd,
295
295
  const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
296
296
  const scripts = manifest.scripts && typeof manifest.scripts === "object" ? manifest.scripts : {};
297
297
  const selected = ["type-check", "typecheck", "lint", "test"].filter((name) => typeof scripts[name] === "string");
298
- if (selected.length > 0) commands = selected.map((name) => name === "test" ? "npm test" : `npm run ${name}`);
298
+ if (selected.length > 0) {
299
+ commands = [selected.map((name) => name === "test" ? "npm test" : `npm run ${name}`).join(" && ")];
300
+ }
299
301
  }
300
302
  } catch {
301
303
  // Preserve the profile's fail-closed verifier when package metadata is absent or invalid.
@@ -0,0 +1,157 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+
5
+ const WORKSPACE_EVIDENCE_MAX_FILES = 2000;
6
+ const WORKSPACE_EVIDENCE_SKIP_NAMES = new Set([
7
+ ".git", ".hg", ".svn", ".pi", "node_modules", "dist", "build", ".next", "coverage"
8
+ ]);
9
+ const WORKSPACE_EVIDENCE_PRIORITY_NAMES = new Map([["plans", 0]]);
10
+
11
+ function hiddenWorkspaceEvidenceSegment(value) {
12
+ return String(value ?? "").startsWith(".");
13
+ }
14
+
15
+ export function gitOutput(cwd, args, options = {}) {
16
+ return execFileSync("git", ["-C", cwd, ...args], {
17
+ encoding: options.encoding ?? "utf8",
18
+ maxBuffer: options.maxBuffer ?? 16 * 1024 * 1024,
19
+ stdio: ["ignore", "pipe", "ignore"]
20
+ });
21
+ }
22
+
23
+ export function isGitWorkingTree(cwd) {
24
+ try {
25
+ return gitOutput(cwd, ["rev-parse", "--is-inside-work-tree"]).trim() === "true";
26
+ } catch {
27
+ return false;
28
+ }
29
+ }
30
+
31
+ export function normalizedGitPath(value) {
32
+ return String(value ?? "").replaceAll("\\", "/").replace(/^\/+/, "");
33
+ }
34
+
35
+ export function prefixedGitPath(prefix, file) {
36
+ const normalized = normalizedGitPath(file);
37
+ return prefix ? `${prefix}/${normalized}` : normalized;
38
+ }
39
+
40
+ function directChildGitEvidenceRootDetails(cwd) {
41
+ const roots = [];
42
+ let complete = true;
43
+ let entries;
44
+ try {
45
+ entries = fs.readdirSync(cwd, { withFileTypes: true });
46
+ } catch {
47
+ return { roots, complete: false };
48
+ }
49
+ for (const entry of entries) {
50
+ if (!entry.isDirectory() || entry.name.startsWith(".") || entry.name === "node_modules") continue;
51
+ const child = path.join(cwd, entry.name);
52
+ try {
53
+ if (fs.lstatSync(child).isSymbolicLink()) continue;
54
+ let gitMarker;
55
+ try {
56
+ gitMarker = fs.lstatSync(path.join(child, ".git"));
57
+ } catch (error) {
58
+ if (error?.code === "ENOENT") continue;
59
+ complete = false;
60
+ continue;
61
+ }
62
+ if (!gitMarker.isDirectory() && !gitMarker.isFile()) {
63
+ complete = false;
64
+ continue;
65
+ }
66
+ if (!isGitWorkingTree(child)) {
67
+ complete = false;
68
+ continue;
69
+ }
70
+ const topLevel = gitOutput(child, ["rev-parse", "--show-toplevel"]).trim();
71
+ if (fs.realpathSync(topLevel) !== fs.realpathSync(child)) {
72
+ complete = false;
73
+ continue;
74
+ }
75
+ roots.push({ cwd: child, prefix: normalizedGitPath(entry.name) });
76
+ } catch {
77
+ complete = false;
78
+ }
79
+ }
80
+ return { roots: roots.sort((left, right) => left.prefix.localeCompare(right.prefix)), complete };
81
+ }
82
+
83
+ export function directChildGitEvidenceRoots(cwd) {
84
+ return directChildGitEvidenceRootDetails(cwd).roots;
85
+ }
86
+
87
+ export function gitEvidenceRootDetails(cwd) {
88
+ if (isGitWorkingTree(cwd)) return { roots: [{ cwd, prefix: "" }], complete: true };
89
+ return directChildGitEvidenceRootDetails(cwd);
90
+ }
91
+
92
+ export function gitEvidenceRoots(cwd) {
93
+ return gitEvidenceRootDetails(cwd).roots;
94
+ }
95
+
96
+ export function nonGitWorkspaceFileDetails(cwd, gitRootPrefixes) {
97
+ const files = [];
98
+ let complete = true;
99
+ const gitRoots = new Set(gitRootPrefixes.filter(Boolean));
100
+ const orderedEntries = (entries) => [...entries].sort((left, right) => {
101
+ const leftPriority = WORKSPACE_EVIDENCE_PRIORITY_NAMES.get(left.name.toLowerCase()) ?? 10;
102
+ const rightPriority = WORKSPACE_EVIDENCE_PRIORITY_NAMES.get(right.name.toLowerCase()) ?? 10;
103
+ return leftPriority - rightPriority || left.name.localeCompare(right.name, "en-US");
104
+ });
105
+ function visit(directory, relativeDirectory) {
106
+ if (files.length >= WORKSPACE_EVIDENCE_MAX_FILES) {
107
+ complete = false;
108
+ return;
109
+ }
110
+ let entries;
111
+ try {
112
+ entries = fs.readdirSync(directory, { withFileTypes: true });
113
+ } catch {
114
+ complete = false;
115
+ return;
116
+ }
117
+ for (const entry of orderedEntries(entries)) {
118
+ if (files.length >= WORKSPACE_EVIDENCE_MAX_FILES) {
119
+ complete = false;
120
+ return;
121
+ }
122
+ // Loose files beside nested repositories are not governed by either
123
+ // repository's ignore/trust policy. Keep hidden editor, agent, cloud,
124
+ // credential, and cache state out of both manifests and change evidence
125
+ // instead of attempting to maintain an incomplete name blacklist.
126
+ if (WORKSPACE_EVIDENCE_SKIP_NAMES.has(entry.name) || hiddenWorkspaceEvidenceSegment(entry.name)) continue;
127
+ const relative = relativeDirectory ? `${relativeDirectory}/${entry.name}` : entry.name;
128
+ if (gitRoots.has(relative.split("/")[0])) continue;
129
+ const absolute = path.join(directory, entry.name);
130
+ try {
131
+ if (fs.lstatSync(absolute).isSymbolicLink()) continue;
132
+ } catch {
133
+ complete = false;
134
+ continue;
135
+ }
136
+ if (entry.isDirectory()) visit(absolute, relative);
137
+ else if (entry.isFile()) files.push(normalizedGitPath(relative));
138
+ }
139
+ }
140
+ visit(cwd, "");
141
+ return { files: files.sort(), complete };
142
+ }
143
+
144
+ export function nonGitWorkspaceFiles(cwd, gitRootPrefixes) {
145
+ return nonGitWorkspaceFileDetails(cwd, gitRootPrefixes).files;
146
+ }
147
+
148
+ export function pathWithinNonGitWorkspaceEvidenceRoot(candidate, gitRootPrefixes) {
149
+ const segments = candidate.split("/").filter(Boolean);
150
+ const topSegment = segments[0];
151
+ return Boolean(
152
+ topSegment
153
+ && !WORKSPACE_EVIDENCE_SKIP_NAMES.has(topSegment)
154
+ && !segments.some(hiddenWorkspaceEvidenceSegment)
155
+ && !gitRootPrefixes.includes(topSegment)
156
+ );
157
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@piagent/core",
3
- "version": "1.5.4",
3
+ "version": "1.6.0",
4
4
  "type": "module",
5
5
  "private": true,
6
6
  "license": "MIT",
@@ -99,6 +99,7 @@
99
99
  "id": "prompt-database-migration",
100
100
  "action": "prompt",
101
101
  "match": "contains",
102
+ "ignoreSearchArguments": true,
102
103
  "value": "migrate",
103
104
  "reason": "Database migrations require explicit human confirmation."
104
105
  }
@@ -106,6 +107,7 @@
106
107
  },
107
108
  "contextBudget": {
108
109
  "defaultMode": "enforce",
110
+ "contextDeltaShadow": "sample",
109
111
  "maxContextFileChars": 50000,
110
112
  "maxMemoryFileChars": 20000,
111
113
  "maxManifestFiles": 80,
@@ -154,6 +156,7 @@
154
156
  "ls": ["filesystem-readonly"],
155
157
  "write": ["filesystem-write"],
156
158
  "edit": ["filesystem-write"],
159
+ "apply_patch": ["filesystem-write"],
157
160
  "browser": ["browser"],
158
161
  "github": ["github"]
159
162
  }
@@ -0,0 +1,103 @@
1
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+
3
+ import type { TaskContract } from "../../extensions/guard-types.js";
4
+ import type { PendingContextDelivery, RuntimeSessionState, ObservedTaskContext } from "../session/runtime-state.ts";
5
+
6
+ export type ContextDeliveryConfirmationDependencies = {
7
+ state: RuntimeSessionState;
8
+ maxManifestFiles: number;
9
+ activeTask: (ctx: ExtensionContext) => TaskContract | undefined;
10
+ flushObservedTaskContext: (
11
+ pi: ExtensionAPI,
12
+ ctx: ExtensionContext,
13
+ pendingContext: ObservedTaskContext[],
14
+ maxManifestFiles: number,
15
+ event: string
16
+ ) => TaskContract | undefined;
17
+ telemetry: (ctx: ExtensionContext, payload: Record<string, unknown>) => void;
18
+ };
19
+
20
+ export function contextDeliveryIdFromDetails(details: unknown): string | undefined {
21
+ if (!details || typeof details !== "object" || Array.isArray(details)) return undefined;
22
+ const rawDelivery = (details as { contextDelivery?: unknown }).contextDelivery;
23
+ if (!rawDelivery || typeof rawDelivery !== "object" || Array.isArray(rawDelivery)) return undefined;
24
+ const deliveryId = (rawDelivery as { deliveryId?: unknown }).deliveryId;
25
+ return typeof deliveryId === "string" && deliveryId.length > 0 && deliveryId.length <= 200
26
+ ? deliveryId
27
+ : undefined;
28
+ }
29
+
30
+ export function confirmContextDelivery(
31
+ pi: ExtensionAPI,
32
+ ctx: ExtensionContext,
33
+ deliveryId: string,
34
+ dependencies: ContextDeliveryConfirmationDependencies
35
+ ): TaskContract | undefined {
36
+ const delivery = dependencies.state.takeContextDelivery(ctx, deliveryId);
37
+ if (!delivery) return undefined;
38
+ const task = dependencies.activeTask(ctx);
39
+ if (!task || task.trace.outcome !== "pending" || task.taskRunId !== delivery.taskRunId) {
40
+ dependencies.telemetry(ctx, { event: "context_delivery_rejected", deliveryId, reason: "task-identity-mismatch" });
41
+ return undefined;
42
+ }
43
+ for (const entry of delivery.entries) {
44
+ dependencies.state.rememberObservedContext(ctx, entry);
45
+ dependencies.state.rememberQualifiedContextEvidence(ctx, delivery.taskRunId, entry);
46
+ }
47
+ const written = dependencies.flushObservedTaskContext(
48
+ pi,
49
+ ctx,
50
+ dependencies.state.qualifiedContextEvidence(ctx, delivery.taskRunId),
51
+ dependencies.maxManifestFiles,
52
+ "context_delivery_confirmed"
53
+ );
54
+ if (!written || written.taskRunId !== delivery.taskRunId) return undefined;
55
+ if (delivery.pack) {
56
+ const { retrievalKey, ...pack } = delivery.pack;
57
+ dependencies.state.rememberInjectedContextPack(ctx, retrievalKey, pack);
58
+ }
59
+ dependencies.telemetry(ctx, {
60
+ event: "context_delivery_confirmed",
61
+ deliveryId,
62
+ turnId: delivery.turnId,
63
+ taskId: written.taskId,
64
+ taskRunId: written.taskRunId,
65
+ selectedPaths: delivery.entries.map((entry) => entry.path),
66
+ selected: delivery.entries.length
67
+ });
68
+ if (delivery.injection) dependencies.telemetry(ctx, {
69
+ event: "context_pack_injected",
70
+ injectionId: deliveryId,
71
+ turnId: delivery.turnId,
72
+ ...delivery.injection,
73
+ selectedPaths: delivery.injection.selectedItems.map((item) => item.path)
74
+ });
75
+ return written;
76
+ }
77
+
78
+ export function stageContextDelivery(
79
+ ctx: ExtensionContext,
80
+ delivery: PendingContextDelivery,
81
+ dependencies: Pick<ContextDeliveryConfirmationDependencies, "state" | "telemetry">
82
+ ): void {
83
+ dependencies.state.stageContextDelivery(ctx, delivery);
84
+ if (!delivery.injection) return;
85
+ dependencies.telemetry(ctx, {
86
+ event: "context_pack_offered",
87
+ deliveryId: delivery.deliveryId,
88
+ turnId: delivery.turnId,
89
+ ...delivery.injection,
90
+ selectedPaths: delivery.injection.selectedItems.map((item) => item.path)
91
+ });
92
+ }
93
+
94
+ export function confirmContextDeliveryFromToolResult(
95
+ pi: ExtensionAPI,
96
+ ctx: ExtensionContext,
97
+ event: { details?: unknown; isError?: boolean },
98
+ dependencies: ContextDeliveryConfirmationDependencies
99
+ ): TaskContract | undefined {
100
+ if (event.isError === true) return undefined;
101
+ const deliveryId = contextDeliveryIdFromDetails(event.details);
102
+ return deliveryId ? confirmContextDelivery(pi, ctx, deliveryId, dependencies) : undefined;
103
+ }