@piagent/platform 1.6.0 → 1.6.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 (213) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.md +17 -14
  3. package/README.vi.md +26 -1
  4. package/benchmarks/production-v1/spend-control.v1.json +27 -3
  5. package/benchmarks/production-v1/suite.json +5 -1
  6. package/catalog/capabilities.json +16 -16
  7. package/docs/authority-manifest.md +1 -1
  8. package/docs/auto-delegation-policy.md +38 -73
  9. package/docs/command-reference-vietnamese.md +23 -31
  10. package/docs/context-engine.md +5 -3
  11. package/docs/context-window-policy.md +37 -1
  12. package/docs/distribution-standard.md +7 -7
  13. package/docs/en/architecture.md +26 -13
  14. package/docs/languages.json +4 -4
  15. package/docs/operator-manual-vietnamese.md +21 -41
  16. package/docs/publishing-for-teams.md +5 -0
  17. package/docs/quality-benchmark.md +179 -23
  18. package/docs/release-install-policy.md +6 -6
  19. package/docs/runtime-harness-standard.md +3 -3
  20. package/docs/security-threat-model.md +1 -1
  21. package/docs/subagent-orchestration-capabilities.md +19 -51
  22. package/docs/subagents-and-multiagent.md +21 -47
  23. package/docs/task-implementation-contract.md +3 -3
  24. package/docs/task-lifecycle-vietnamese.md +1 -1
  25. package/docs/team-onboarding.md +2 -2
  26. package/docs/vi/architecture.md +26 -13
  27. package/docs-site/architecture.html +1 -1
  28. package/docs-site/benchmark.html +89 -95
  29. package/docs-site/commands.html +1 -1
  30. package/docs-site/content/benchmark.html +88 -94
  31. package/docs-site/content/en/benchmark.html +134 -90
  32. package/docs-site/content/en/index.html +19 -19
  33. package/docs-site/content/en/runtime.html +4 -4
  34. package/docs-site/content/en/whats-new.html +12 -1
  35. package/docs-site/content/en/workflows.html +2 -2
  36. package/docs-site/content/index.html +19 -19
  37. package/docs-site/content/runtime.html +4 -4
  38. package/docs-site/content/whats-new.html +12 -1
  39. package/docs-site/content/workflows.html +2 -2
  40. package/docs-site/context-engine.html +1 -1
  41. package/docs-site/en/architecture.html +1 -1
  42. package/docs-site/en/benchmark.html +135 -91
  43. package/docs-site/en/commands.html +1 -1
  44. package/docs-site/en/context-engine.html +1 -1
  45. package/docs-site/en/index.html +20 -20
  46. package/docs-site/en/mcp-approval.html +1 -1
  47. package/docs-site/en/mcp-auth.html +1 -1
  48. package/docs-site/en/mcp-servers.html +1 -1
  49. package/docs-site/en/mcp.html +1 -1
  50. package/docs-site/en/permissions.html +1 -1
  51. package/docs-site/en/profiles.html +1 -1
  52. package/docs-site/en/quickstart.html +1 -1
  53. package/docs-site/en/runtime.html +5 -5
  54. package/docs-site/en/scope.html +1 -1
  55. package/docs-site/en/security.html +1 -1
  56. package/docs-site/en/team.html +1 -1
  57. package/docs-site/en/whats-new.html +13 -2
  58. package/docs-site/en/workflows.html +3 -3
  59. package/docs-site/index.html +20 -20
  60. package/docs-site/mcp-approval.html +1 -1
  61. package/docs-site/mcp-auth.html +1 -1
  62. package/docs-site/mcp-servers.html +1 -1
  63. package/docs-site/mcp.html +1 -1
  64. package/docs-site/permissions.html +1 -1
  65. package/docs-site/profiles.html +1 -1
  66. package/docs-site/quickstart.html +1 -1
  67. package/docs-site/runtime.html +5 -5
  68. package/docs-site/scope.html +1 -1
  69. package/docs-site/security.html +1 -1
  70. package/docs-site/team.html +1 -1
  71. package/docs-site/whats-new.html +13 -2
  72. package/docs-site/workflows.html +3 -3
  73. package/evals/fixtures/benchmark-assurance-evidence.invalid.json +1 -1
  74. package/evals/fixtures/benchmark-assurance-evidence.valid.json +1 -1
  75. package/evals/fixtures/codex-relative-efficiency-v1.invalid.json +4 -0
  76. package/evals/fixtures/codex-relative-efficiency-v1.valid.json +94 -0
  77. package/evals/fixtures/helper-request.invalid.json +1 -1
  78. package/evals/fixtures/helper-request.valid.json +1 -1
  79. package/evals/fixtures/piagent-webui/subagent-tree-v1.valid.json +7 -0
  80. package/evals/fixtures/task-authority-snapshot.valid.json +4 -4
  81. package/evals/fs4-readiness-matrix.v1.json +2 -2
  82. package/evals/fs5-causal-arm.v1.json +2 -2
  83. package/evals/fs5-pilot-protocol.v1.json +3 -3
  84. package/evals/fs5-pilot-protocol.v2.json +8 -8
  85. package/evals/fs5-pilot-protocol.v3.json +8 -8
  86. package/evals/fs5-pilot-protocol.v4.json +6 -6
  87. package/evals/fs5-pilot-protocol.v5.json +7 -6
  88. package/evals/private-holdout-v1/public-exposure.v1.json +1 -1
  89. package/evals/runtime-conformance-v1/README.md +43 -0
  90. package/evals/runtime-conformance-v1/evaluate-paid-canary.mjs +198 -0
  91. package/evals/runtime-conformance-v1/lane.json +57 -0
  92. package/evals/runtime-conformance-v1/paid-canary.json +31 -0
  93. package/evals/runtime-conformance-v1/runner.mjs +456 -0
  94. package/package.json +1 -1
  95. package/packages/piagent-core/README.md +13 -4
  96. package/packages/piagent-core/benchmark/benchmark-codex-relative-efficiency.js +698 -0
  97. package/packages/piagent-core/benchmark/benchmark-core.js +4 -3
  98. package/packages/piagent-core/benchmark/benchmark-forensics.js +4 -2
  99. package/packages/piagent-core/benchmark/benchmark-preflight.js +3 -1
  100. package/packages/piagent-core/benchmark/benchmark-provider-free-evidence.js +345 -0
  101. package/packages/piagent-core/benchmark/benchmark-record-validation.js +74 -5
  102. package/packages/piagent-core/benchmark/benchmark-report.js +15 -1
  103. package/packages/piagent-core/benchmark/benchmark-runtime-causal.js +244 -0
  104. package/packages/piagent-core/benchmark/benchmark-stage-diagnostic.js +201 -4
  105. package/packages/piagent-core/benchmark/benchmark-suite.js +22 -1
  106. package/packages/piagent-core/benchmark/benchmark-summary-support.js +4 -0
  107. package/packages/piagent-core/benchmark/benchmark-usage.js +15 -4
  108. package/packages/piagent-core/capabilities/runtime-integrity.js +3 -0
  109. package/packages/piagent-core/extensions/acceptance-behavior-proof.js +806 -0
  110. package/packages/piagent-core/extensions/acceptance-receipt.js +89 -48
  111. package/packages/piagent-core/extensions/context-engine.js +1 -11
  112. package/packages/piagent-core/extensions/context-index-policy.js +4 -0
  113. package/packages/piagent-core/extensions/context-query-paths.js +57 -0
  114. package/packages/piagent-core/extensions/criterion-context-pack.js +16 -3
  115. package/packages/piagent-core/extensions/criterion-graph.js +4 -1
  116. package/packages/piagent-core/extensions/fallback-base-policy.ts +101 -0
  117. package/packages/piagent-core/extensions/guard-types.ts +4 -0
  118. package/packages/piagent-core/extensions/piagent-guard.ts +194 -226
  119. package/packages/piagent-core/extensions/task-lifecycle.js +32 -0
  120. package/packages/piagent-core/extensions/task-state.js +69 -8
  121. package/packages/piagent-core/extensions/verification-intelligence.js +49 -12
  122. package/packages/piagent-core/extensions/workspace-evidence-roots.js +52 -16
  123. package/packages/piagent-core/package.json +1 -1
  124. package/packages/piagent-core/policies/base-policy.json +42 -2
  125. package/packages/piagent-core/policy/authority-manifest.v1.json +3 -3
  126. package/packages/piagent-core/prompts/be-to-fe.md +2 -8
  127. package/packages/piagent-core/prompts/plan.md +1 -1
  128. package/packages/piagent-core/prompts/platform-improve.md +2 -6
  129. package/packages/piagent-core/prompts/review.md +2 -7
  130. package/packages/piagent-core/prompts/scout.md +1 -1
  131. package/packages/piagent-core/prompts/task.md +4 -3
  132. package/packages/piagent-core/runtime/hooks/completion-hook.ts +7 -6
  133. package/packages/piagent-core/runtime/hooks/input-hook.ts +6 -0
  134. package/packages/piagent-core/runtime/hooks/session-start-hook.ts +100 -1
  135. package/packages/piagent-core/runtime/hooks/tool-result-hook.ts +58 -72
  136. package/packages/piagent-core/runtime/hooks/tool-result-value-helpers.ts +66 -0
  137. package/packages/piagent-core/runtime/inspection/bounded-emission-guard.ts +286 -0
  138. package/packages/piagent-core/runtime/inspection/runtime-event-store.ts +37 -7
  139. package/packages/piagent-core/runtime/inspection/task-subagent-tree.ts +27 -4
  140. package/packages/piagent-core/runtime/inspection/tool-failure-classification.ts +20 -3
  141. package/packages/piagent-core/runtime/inspection/webui-snapshot.ts +47 -11
  142. package/packages/piagent-core/runtime/orchestration/helper-lifecycle.ts +16 -12
  143. package/packages/piagent-core/runtime/orchestration/minimal-delegation-policy.ts +71 -0
  144. package/packages/piagent-core/runtime/orchestration/owned-work-budget.ts +28 -20
  145. package/packages/piagent-core/runtime/orchestration/role-policy.ts +27 -9
  146. package/packages/piagent-core/runtime/orchestration/subagent-tool-policy.ts +329 -0
  147. package/packages/piagent-core/runtime/policy/block-remedy.ts +0 -4
  148. package/packages/piagent-core/runtime/product/efficiency-metrics.ts +3 -0
  149. package/packages/piagent-core/runtime/quality/edit-freshness-guard.ts +102 -0
  150. package/packages/piagent-core/runtime/quality/exact-output-contract.ts +20 -10
  151. package/packages/piagent-core/runtime/quality/performance-assurance.ts +3 -3
  152. package/packages/piagent-core/runtime/recovery/continuation-budget.ts +1 -1
  153. package/packages/piagent-core/runtime/recovery/handoff-projection.ts +4 -1
  154. package/packages/piagent-core/runtime/recovery/pristine-task-policy-refresh.ts +91 -0
  155. package/packages/piagent-core/runtime/recovery/recovery-policy.ts +7 -4
  156. package/packages/piagent-core/runtime/recovery/resume-state.ts +112 -14
  157. package/packages/piagent-core/runtime/recovery/semantic-repair-handshake.ts +7 -13
  158. package/packages/piagent-core/runtime/recovery/task-evidence-coverage-refresh.ts +217 -0
  159. package/packages/piagent-core/runtime/registration/context-commands.ts +16 -2
  160. package/packages/piagent-core/runtime/registration/knowledge-tools.ts +9 -5
  161. package/packages/piagent-core/runtime/registration/operator-request-intake.ts +49 -0
  162. package/packages/piagent-core/runtime/registration/policy-tools.ts +4 -4
  163. package/packages/piagent-core/runtime/registration/session-commands.ts +1 -1
  164. package/packages/piagent-core/runtime/registration/task-completion-tools.ts +12 -9
  165. package/packages/piagent-core/runtime/registration/task-evidence-tools.ts +21 -12
  166. package/packages/piagent-core/runtime/registration/task-start-guidance.ts +19 -13
  167. package/packages/piagent-core/runtime/registration/task-start-manifest.ts +54 -0
  168. package/packages/piagent-core/runtime/registration/task-start-tool.ts +57 -57
  169. package/packages/piagent-core/runtime/runtime-limits.ts +16 -2
  170. package/packages/piagent-core/runtime/session/adaptive-context-analysis.ts +461 -0
  171. package/packages/piagent-core/runtime/session/adaptive-context-governor.ts +464 -0
  172. package/packages/piagent-core/runtime/session/adaptive-context-ledger.ts +265 -0
  173. package/packages/piagent-core/runtime/session/message-signals.ts +7 -3
  174. package/packages/piagent-core/runtime/session/operator-request-carry.ts +44 -0
  175. package/packages/piagent-core/runtime/session/runtime-state.ts +11 -7
  176. package/packages/piagent-core/runtime/session/source-checkout-read-grants.ts +35 -0
  177. package/packages/piagent-core/runtime/session/system-prompt.ts +76 -14
  178. package/packages/piagent-core/runtime/session/task-contract-presentation.ts +39 -0
  179. package/packages/piagent-core/runtime/tools/phase-tools.ts +8 -8
  180. package/packages/piagent-core/runtime/tools/tool-groups.ts +12 -2
  181. package/packages/piagent-core/runtime/verification/exact-verifier-reuse.ts +101 -0
  182. package/packages/piagent-core/runtime/workflows/runtime-intake-compaction.ts +164 -0
  183. package/packages/piagent-core/runtime/workflows/task-intake.ts +167 -39
  184. package/packages/piagent-core/subagents/piagent-oracle.md +5 -3
  185. package/packages/piagent-core/subagents/piagent-planner.md +6 -4
  186. package/packages/piagent-core/subagents/piagent-reviewer.md +5 -3
  187. package/packages/piagent-core/subagents/piagent-scout.md +4 -2
  188. package/packages/piagent-core/subagents/piagent-worker.md +6 -4
  189. package/packages/piagent-webui/dist/client/assets/{MarkdownMessage-CxeZqTte.js → MarkdownMessage-B4XehTCU.js} +1 -1
  190. package/packages/piagent-webui/dist/client/assets/{index-DynIMe8I.js → index-CgOEyRPG.js} +4 -4
  191. package/packages/piagent-webui/dist/client/index.html +1 -1
  192. package/packages/piagent-webui/extension/piagent-webui.ts +3 -1
  193. package/packages/piagent-webui/extension/same-session-bridge.ts +1 -1
  194. package/packages/piagent-webui/extension/session-stream-adapter.ts +33 -10
  195. package/packages/piagent-webui/gateway/gateway-session-stream.ts +37 -21
  196. package/packages/piagent-webui/gateway/session-operation-lifecycle.ts +237 -0
  197. package/packages/piagent-webui/gateway/session-operation-retry-control.ts +38 -0
  198. package/packages/piagent-webui/gateway/session-operation-watchdog.ts +9 -2
  199. package/packages/piagent-webui/gateway/session-runtime-supervisor.ts +8 -7
  200. package/packages/piagent-webui/ownership/session-lease-store.ts +5 -2
  201. package/packages/piagent-webui/server/core-inspection-provider.ts +2 -1
  202. package/packages/piagent-webui/server/transcript-projection.ts +91 -5
  203. package/schemas/codex-relative-efficiency-v1.schema.json +155 -0
  204. package/schemas/piagent-webui/runtime-event-v2.schema.json +15 -5
  205. package/schemas/piagent-webui/subagent-tree-v1.schema.json +11 -1
  206. package/schemas/project-profile.schema.json +1 -1
  207. package/schemas/task-contract.schema.json +15 -0
  208. package/scripts/benchmark-runner-core.mjs +14 -6
  209. package/scripts/benchmark-runner-finalization.mjs +4 -0
  210. package/scripts/configure-subagents.sh +42 -34
  211. package/scripts/helper-evaluation.mjs +1 -1
  212. package/scripts/team-doctor.sh +1 -1
  213. package/scripts/verify-local.sh +5 -0
@@ -5,6 +5,7 @@ import { durableContextEvidenceEntries, hasDurableContextEvidence } from "./cont
5
5
  import { matchesAnyPath, normalizePathCandidate } from "./policy-core.js";
6
6
  import { baselineReturnRepresentationConflicts, returnRepresentationGuidance } from "./return-contract.js";
7
7
  import { hasLengthPrefixedIdentityKey, tenantAssertionSignals } from "./tenant-contract.js";
8
+ import { criterionBehaviorProofDisposition, criterionRequiresBehavioralProof, durableBehaviorProofRequired, genericCriterionEvidence, genericFallbackEvidence, isVerificationOnlyCriterion, verifierCommandsCoverTests } from "./acceptance-behavior-proof.js";
8
9
  import { latestObservedVerificationEvidence, meaningfulVerificationCommands, verificationEvidenceProvesStableTree } from "./verification-intelligence.js";
9
10
  import {
10
11
  acceptanceContractProofGuidance,
@@ -34,7 +35,7 @@ const EVIDENCE_FIELDS = new Set(["kind", "summary", "paths", "command", "exitCod
34
35
  const PROVENANCE_FIELDS = new Set(["assurance", "disposition", "repairCount", "retryCount", "finalRecoveryDisposition", "failureRef", "recoveryRef", "handoffRef", "recordedAt"]);
35
36
  const FAILURE_REF_FIELDS = new Set(["evidenceDigest", "category", "captureRef"]);
36
37
  const RECOVERY_REF_FIELDS = new Set(["policyVersion", "action", "reasonCodes"]);
37
- const HELPER_USAGE_FIELDS = new Set(["mode", "used", "reasonCodes", "helpers", "recordedAt"]);
38
+ const HELPER_USAGE_FIELDS = new Set(["mode", "used", "decision", "projectedSavingsRatio", "reasonCodes", "helpers", "recordedAt"]);
38
39
  const HELPER_ENTRY_FIELDS = new Set(["role", "disposition", "requestRef", "outputDigest", "calls", "tokens"]);
39
40
  const HELPER_ROLES = new Set(["retriever", "scout", "planner", "worker", "reviewer", "oracle", "researcher"]);
40
41
  const RECEIPT_DISPOSITIONS = new Set(["first-pass-success", "repaired-success", "blocked", "partial", "failed", "pending"]);
@@ -43,7 +44,7 @@ const RECOVERY_ACTIONS = new Set(["repair", "retry", "fresh-session", "ask-opera
43
44
  const FAILURE_CATEGORIES = new Set(["passed", "compile-typecheck", "test-assertion", "lint-format", "dependency-config", "environment", "provider-network", "permission-policy", "scope-protected-path", "flaky-infrastructure", "unknown"]);
44
45
  const HASH = /^[a-f0-9]{64}$/;
45
46
  const SAFE_REF = /^[a-z0-9.][a-z0-9:._/-]{0,511}$/i;
46
- const READ_ONLY_BOUNDARY = /\b(?:read-only|no edits?|do not edit(?: files?| source| project| repo)?|do not change (?:files?|source|project|repo)|do not mutate (?:files?|source|project|repo|workspace)|khong sua(?: file| source| project)?|khong edit(?: file| source| project)?|khong doi(?: file| source| project)?)\b/;
47
+ const READ_ONLY_BOUNDARY = /\b(?:read-only|no\s+(?:code|source|project|file|workspace|repo(?:sitory)?)?\s*(?:edits?|changes?|mutations?)|do not\s+(?:edit|change|modify|mutate|touch)(?: files?| source| project| repo| workspace)?|leave\s+(?:the\s+)?(?:source|project|repo|workspace)\s+(?:unchanged|unmodified)|khong\s+(?:sua|edit|thay\s+doi)(?: file| source| project)?)\b/;
47
48
 
48
49
  function sha256(value) {
49
50
  return crypto.createHash("sha256").update(String(value ?? "")).digest("hex");
@@ -80,11 +81,9 @@ function normalizedText(value) {
80
81
  .replace(/[\u0300-\u036f]/g, "")
81
82
  .toLowerCase();
82
83
  }
83
-
84
84
  function includesAny(text, patterns) {
85
85
  return patterns.some((pattern) => pattern.test(text));
86
86
  }
87
-
88
87
  function pathBoundaryMentioned(text, patterns) {
89
88
  const value = normalizedText(text);
90
89
  return uniqueStrings(patterns).some((pattern) => {
@@ -95,20 +94,35 @@ function pathBoundaryMentioned(text, patterns) {
95
94
  return value.includes(lower) || (Boolean(basename) && value.includes(basename));
96
95
  });
97
96
  }
98
-
97
+ function readOnlyBoundaryTiedToPattern(text, patterns) {
98
+ return normalizedText(text)
99
+ .split(/(?:\r?\n+|[.!?;]+|\s+[—–]\s+)/)
100
+ .some((clause) => READ_ONLY_BOUNDARY.test(clause) && pathBoundaryMentioned(clause, patterns));
101
+ }
99
102
  function changedFilesRespectBoundaries(changedFiles, boundaryPatterns) {
100
103
  const patterns = uniqueStrings(boundaryPatterns);
101
104
  if (patterns.length === 0) return true;
102
105
  return uniqueStrings(changedFiles).every((file) => !matchesAnyPath(file, patterns));
103
106
  }
107
+ function acceptanceReadOnlyInferenceOptions(input, text) {
108
+ const boundaryPatterns = uniqueStrings([
109
+ ...(Array.isArray(input?.outOfScope) ? input.outOfScope : []),
110
+ ...(Array.isArray(input?.protectedPaths) ? input.protectedPaths : [])
111
+ ]);
112
+ return {
113
+ mutationPolicy: input?.mutationPolicy,
114
+ readOnlyBoundary: boundaryPatterns.length > 0
115
+ && readOnlyBoundaryTiedToPattern(text, boundaryPatterns)
116
+ };
117
+ }
104
118
 
105
- export function inferAcceptanceObligations(text, changeMode = "source-change") {
119
+ export function inferAcceptanceObligations(text, changeMode = "source-change", options = {}) {
106
120
  const value = normalizedText(text);
107
121
  const accessControlText = value
108
122
  .replace(/`[^`\n]*`/g, " ")
109
123
  .replace(/\b(?:[a-z0-9_.@-]+\/)+[a-z0-9_.@/-]+\b/g, " ");
110
124
  const obligations = [];
111
- if (changeMode === "read-only" || READ_ONLY_BOUNDARY.test(value)) {
125
+ if (changeMode === "read-only" || options.mutationPolicy === "forbidden" || options.readOnlyBoundary === true) {
112
126
  obligations.push("read-only-evidence");
113
127
  }
114
128
  const actorAccessControl = includesAny(accessControlText, [
@@ -191,7 +205,6 @@ const GENERATED_ACCEPTANCE_TEXTS = new Set([
191
205
  "authorization-deny-case", "tenant-boundary", "tenant-storage-isolation", "invalid-input-rejection", "boundary-case",
192
206
  "backward-compatibility", "read-only-evidence", "verification-evidence"
193
207
  ].map((obligation) => generatedCriterionForObligation(obligation)));
194
-
195
208
  function acceptanceTaskText(task) {
196
209
  return [
197
210
  task?.summary,
@@ -227,8 +240,9 @@ function semanticConflictReasons(obligation, task, corpus, criterion) {
227
240
 
228
241
  /**
229
242
  * Surface a concrete existing return contract before implementation starts.
230
- * This is intentionally source-derived and bounded to exact in-scope files;
231
- * it does not guess from benchmark scenario names or hidden expectations.
243
+ * This is intentionally source-derived and bounded to exact known files; it
244
+ * does not turn the task's initial focus into write authority or guess from
245
+ * benchmark scenario names and hidden expectations.
232
246
  */
233
247
  export function acceptanceBaselineGuidance(task, options = {}) {
234
248
  if (!options.cwd) return [];
@@ -247,7 +261,11 @@ export function acceptanceSemanticConflicts(task, options = {}) {
247
261
  : { ...emptyAcceptanceCorpus(changedFiles.length > 0 ? changedFiles : ["inline.js"]), sourceText: normalizedText(options.sourceText), allText: normalizedText(options.sourceText) };
248
262
  if (!corpus.adapter.proofCapable) return [];
249
263
  const obligations = task?.acceptanceReceipt?.criteria?.map((criterion) => criterion.obligation)
250
- ?? inferAcceptanceObligations(acceptanceTaskText(task), task?.changeMode);
264
+ ?? inferAcceptanceObligations(
265
+ acceptanceTaskText(task),
266
+ task?.changeMode,
267
+ acceptanceReadOnlyInferenceOptions(task, acceptanceTaskText(task))
268
+ );
251
269
  return uniqueStrings([
252
270
  ...obligations.flatMap((obligation) => semanticConflictReasons(obligation, task, corpus)),
253
271
  ...(options.cwd ? baselineReturnRepresentationConflicts(acceptanceTaskText(task), options.cwd, corpus.sourceFiles) : [])
@@ -258,7 +276,6 @@ function criterionId(text, obligation, index) {
258
276
  const prefix = compactId(obligation ?? "criterion").slice(0, 24);
259
277
  return `ac-${String(index + 1).padStart(2, "0")}-${prefix}-${sha256(text).slice(0, 8)}`.slice(0, 80);
260
278
  }
261
-
262
279
  export function buildAcceptanceReceipt(input = {}) {
263
280
  const changeMode = input.changeMode === "read-only" ? "read-only" : "source-change";
264
281
  const baseText = [
@@ -266,12 +283,20 @@ export function buildAcceptanceReceipt(input = {}) {
266
283
  input.expectedOutput,
267
284
  ...(Array.isArray(input.acceptanceCriteria) ? input.acceptanceCriteria : [])
268
285
  ].filter(Boolean).join("\n");
269
- const obligations = inferAcceptanceObligations(baseText, changeMode);
286
+ const obligations = inferAcceptanceObligations(
287
+ baseText,
288
+ changeMode,
289
+ acceptanceReadOnlyInferenceOptions(input, baseText)
290
+ );
270
291
  const texts = uniqueStrings(Array.isArray(input.acceptanceCriteria) ? input.acceptanceCriteria : []);
271
292
  const generatedObligations = new Map();
272
293
  for (const obligation of obligations) {
273
294
  const generated = generatedCriterionForObligation(obligation);
274
- if (!texts.some((text) => inferAcceptanceObligations(text, changeMode).includes(obligation))) {
295
+ if (!texts.some((text) => inferAcceptanceObligations(
296
+ text,
297
+ changeMode,
298
+ acceptanceReadOnlyInferenceOptions(input, text)
299
+ ).includes(obligation))) {
275
300
  texts.push(generated);
276
301
  generatedObligations.set(generated, obligation);
277
302
  }
@@ -282,17 +307,23 @@ export function buildAcceptanceReceipt(input = {}) {
282
307
  : generatedCriterionForObligation("verification-evidence"));
283
308
  }
284
309
  const acceptanceCriteria = texts.slice(0, MAX_CRITERIA);
310
+ const durableBehaviorProof = durableBehaviorProofRequired(baseText, changeMode);
285
311
  const criteria = acceptanceCriteria.map((text, index) => {
286
- const inferred = inferAcceptanceObligations(text, changeMode);
312
+ const inferred = inferAcceptanceObligations(
313
+ text,
314
+ changeMode,
315
+ acceptanceReadOnlyInferenceOptions(input, text)
316
+ );
287
317
  const obligation = generatedObligations.get(text)
288
318
  ?? inferred.find((item) => item !== "verification-evidence" && item !== "backward-compatibility")
289
- ?? inferred[0]
319
+ ?? (isVerificationOnlyCriterion(text) ? "verification-evidence" : undefined)
320
+ ?? (inferred.includes("backward-compatibility") ? "backward-compatibility" : undefined)
290
321
  ?? "requested-behavior";
291
322
  return {
292
323
  id: criterionId(text, obligation, index),
293
324
  hash: sha256(text),
294
325
  obligation,
295
- priority: CRITICAL_OBLIGATIONS.has(obligation) ? "critical" : "normal",
326
+ priority: CRITICAL_OBLIGATIONS.has(obligation) || (durableBehaviorProof && obligation !== "verification-evidence") ? "critical" : "normal",
296
327
  status: "pending",
297
328
  evidence: []
298
329
  };
@@ -351,20 +382,30 @@ export function normalizeAcceptanceReceipt(value) {
351
382
 
352
383
  function normalizeHelperUsage(value) {
353
384
  if (!isRecord(value) || !["off", "recommend", "on"].includes(value.mode)) return undefined;
354
- const helpers = Array.isArray(value.helpers) ? value.helpers.slice(0, 3).filter(isRecord).map((item) => ({
385
+ const rawHelpers = Array.isArray(value.helpers) ? value.helpers.slice(0, 3).filter(isRecord) : [];
386
+ const helpers = rawHelpers.map((item) => ({
355
387
  role: HELPER_ROLES.has(item.role) ? item.role : "scout",
356
388
  disposition: compactId(item.disposition ?? "unknown"),
357
389
  requestRef: HASH.test(String(item.requestRef)) ? item.requestRef : sha256(item.requestRef),
358
390
  outputDigest: item.outputDigest === null || HASH.test(String(item.outputDigest)) ? item.outputDigest : null,
359
391
  calls: Number.isInteger(item.calls) ? Math.max(0, Math.min(100, item.calls)) : 0,
360
392
  tokens: Number.isInteger(item.tokens) ? Math.max(0, Math.min(100_000_000, item.tokens)) : 0
361
- })) : [];
362
- return { mode: value.mode, used: helpers.some((item) => !["solo", "recommend", "unavailable", "blocked"].includes(item.disposition)), reasonCodes: uniqueStrings(value.reasonCodes).map(compactId).slice(0, 16), helpers, recordedAt: validTimestamp(value.recordedAt) ? value.recordedAt : new Date().toISOString() };
393
+ }));
394
+ const used = helpers.some((item) => !["solo", "recommend", "unavailable", "blocked"].includes(item.disposition));
395
+ const helperDecision = rawHelpers.find((item) => ["dispatch", "skip"].includes(item.decision))?.decision;
396
+ const decision = ["dispatch", "skip"].includes(value.decision) ? value.decision : helperDecision ?? (used ? "dispatch" : "skip");
397
+ const rawProjectedSavings = typeof value.projectedSavingsRatio === "number"
398
+ ? value.projectedSavingsRatio
399
+ : rawHelpers.find((item) => typeof item.projectedSavingsRatio === "number")?.projectedSavingsRatio;
400
+ const projectedSavingsRatio = typeof rawProjectedSavings === "number" && Number.isFinite(rawProjectedSavings)
401
+ ? Math.max(-10, Math.min(1, rawProjectedSavings))
402
+ : null;
403
+ return { mode: value.mode, used, decision, projectedSavingsRatio, reasonCodes: uniqueStrings(value.reasonCodes).map(compactId).slice(0, 16), helpers, recordedAt: validTimestamp(value.recordedAt) ? value.recordedAt : new Date().toISOString() };
363
404
  }
364
405
 
365
406
  export function applyAcceptanceHelperUsage(task, input = {}) {
366
407
  const receipt = normalizeAcceptanceReceipt(task?.acceptanceReceipt); if (!receipt) return task;
367
- const helperUsage = normalizeHelperUsage({ mode: input.mode ?? "off", reasonCodes: input.reasonCodes ?? [], helpers: input.helpers ?? [], recordedAt: input.recordedAt ?? new Date().toISOString() });
408
+ const helperUsage = normalizeHelperUsage({ mode: input.mode ?? "off", decision: input.decision, projectedSavingsRatio: input.projectedSavingsRatio, reasonCodes: input.reasonCodes ?? [], helpers: input.helpers ?? [], recordedAt: input.recordedAt ?? new Date().toISOString() });
368
409
  return { ...task, acceptanceReceipt: { ...receipt, criteria: structuredClone(task.acceptanceReceipt.criteria), helperUsage } };
369
410
  }
370
411
 
@@ -633,8 +674,8 @@ function hasStructuredCompositeIdentity(sourceText) {
633
674
  return jsonTuple || nestedLookup || hasLengthPrefixedIdentityKey(text);
634
675
  }
635
676
 
636
- function focusedContractEvidence(obligation, task, corpus, verifierEvidence, criterion) {
637
- if (!verifierEvidence || corpus.sourceFiles.length === 0 || corpus.testFiles.length === 0) return undefined;
677
+ function focusedContractEvidence(obligation, task, corpus, verifierEvidence, criterion, cwd) {
678
+ if (!verifierEvidence || corpus.sourceFiles.length === 0 || !verifierCommandsCoverTests(task, corpus.testFiles, cwd)) return undefined;
638
679
  const targets = namedCodeTargets(task, criterion, corpus.sourceText);
639
680
  const targetCoverage = targets.length === 0 || targets.every((target) => (
640
681
  new RegExp(`\\b${escapeRegex(target.toLowerCase())}\\b`).test(corpus.testText)
@@ -681,7 +722,7 @@ function hasCurrentPassingVerifier(task, currentWorkingTreeDigest) {
681
722
  });
682
723
  }
683
724
 
684
- function evidenceForObligation(obligation, task, corpus, currentWorkingTreeDigest, criterion) {
725
+ function evidenceForObligation(obligation, task, corpus, currentWorkingTreeDigest, criterion, cwd) {
685
726
  const passingVerifier = hasCurrentPassingVerifier(task, currentWorkingTreeDigest);
686
727
  const latestVerifyEvidence = latestObservedVerificationEvidence(task?.verifyEvidence);
687
728
  const verifyEvidence = meaningfulVerificationCommands(task?.verifyCommands ?? [])
@@ -699,7 +740,7 @@ function evidenceForObligation(obligation, task, corpus, currentWorkingTreeDiges
699
740
 
700
741
  if (obligation === "read-only-evidence") {
701
742
  const readOnlyEvidence = durableContextEvidenceEntries(task);
702
- const readOnlyOk = task.changeMode === "read-only"
743
+ const readOnlyOk = (task.changeMode === "read-only" || task.mutationPolicy === "forbidden")
703
744
  && corpus.files.length === 0
704
745
  && hasDurableContextEvidence(task);
705
746
  if (readOnlyOk) return {
@@ -716,9 +757,9 @@ function evidenceForObligation(obligation, task, corpus, currentWorkingTreeDiges
716
757
  ].filter(Boolean).join("\n");
717
758
  const combinedText = `${taskText}\n${corpus.allText}`;
718
759
  const sourceChangeReadOnlyOk = corpus.files.length > 0
719
- && READ_ONLY_BOUNDARY.test(normalizedText(combinedText))
760
+ && boundaryPatterns.length > 0
761
+ && readOnlyBoundaryTiedToPattern(combinedText, boundaryPatterns)
720
762
  && changedFilesRespectBoundaries(corpus.files, boundaryPatterns)
721
- && (boundaryPatterns.length === 0 || pathBoundaryMentioned(combinedText, boundaryPatterns))
722
763
  && hasDurableContextEvidence(task);
723
764
  if (sourceChangeReadOnlyOk) {
724
765
  return {
@@ -732,23 +773,21 @@ function evidenceForObligation(obligation, task, corpus, currentWorkingTreeDiges
732
773
  return undefined;
733
774
  }
734
775
 
735
- if (obligation === "verification-evidence" || obligation === "requested-behavior" || obligation === "backward-compatibility") {
736
- if (!passingVerifier) return undefined;
737
- return verifierEvidence;
738
- }
776
+ const generic = genericCriterionEvidence({ obligation, task, criterion, taskText: acceptanceTaskText(task), corpus, verifierEvidence, passingVerifier, cwd });
777
+ if (generic.handled) return generic.evidence;
739
778
 
740
779
  if (!corpus.adapter.proofCapable) return undefined;
741
780
 
742
781
  if (obligation === "tenant-boundary") {
743
- return focusedContractEvidence(obligation, task, corpus, verifierEvidence, criterion);
782
+ return focusedContractEvidence(obligation, task, corpus, verifierEvidence, criterion, cwd);
744
783
  }
745
784
 
746
785
  if (obligation === "tenant-storage-isolation") {
747
- return focusedContractEvidence(obligation, task, corpus, verifierEvidence, criterion);
786
+ return focusedContractEvidence(obligation, task, corpus, verifierEvidence, criterion, cwd);
748
787
  }
749
788
 
750
789
  if (obligation === "authorization-deny-case") {
751
- return focusedContractEvidence(obligation, task, corpus, verifierEvidence, criterion);
790
+ return focusedContractEvidence(obligation, task, corpus, verifierEvidence, criterion, cwd);
752
791
  }
753
792
 
754
793
  if (obligation === "invalid-input-rejection") {
@@ -761,7 +800,7 @@ function evidenceForObligation(obligation, task, corpus, currentWorkingTreeDiges
761
800
  namedTargets: namedCodeTargets(task, criterion, corpus.sourceText),
762
801
  provenanceTargets: explicitlyCallableTargets(task, criterion, corpus.sourceText)
763
802
  });
764
- if (passingVerifier && corpus.sourceFiles.length > 0 && corpus.testFiles.length > 0 && sourceOk && testOk) {
803
+ if (passingVerifier && corpus.sourceFiles.length > 0 && verifierCommandsCoverTests(task, corpus.testFiles, cwd) && sourceOk && testOk) {
765
804
  return {
766
805
  ...verifierEvidence,
767
806
  kind: "verifier-backed-focused-test",
@@ -779,7 +818,7 @@ function evidenceForObligation(obligation, task, corpus, currentWorkingTreeDiges
779
818
  const sourceOk = /math\.ceil|ceil|clamp|min|max|round|zero|return\s+0|\?\?|nullish|falsey|falsy|default|retrylimit|enabled|label/.test(corpus.sourceText)
780
819
  || expirySourceOk
781
820
  || (passingVerifier && corpus.sourceFiles.length > 0 && testOk);
782
- if (passingVerifier && corpus.sourceFiles.length > 0 && corpus.testFiles.length > 0 && sourceOk && testOk) {
821
+ if (passingVerifier && corpus.sourceFiles.length > 0 && verifierCommandsCoverTests(task, corpus.testFiles, cwd) && sourceOk && testOk) {
783
822
  return {
784
823
  ...verifierEvidence,
785
824
  kind: "verifier-backed-focused-test",
@@ -790,12 +829,12 @@ function evidenceForObligation(obligation, task, corpus, currentWorkingTreeDiges
790
829
  return undefined;
791
830
  }
792
831
 
793
- return passingVerifier ? verifierEvidence : undefined;
832
+ return genericFallbackEvidence({ behavioral: generic.behavioral, corpus, verifierEvidence, passingVerifier });
794
833
  }
795
834
 
796
835
  export function refreshAcceptanceReceipt(task, options = {}) {
797
836
  const receipt = normalizeAcceptanceReceipt(task?.acceptanceReceipt);
798
- if (!receipt) return { task, missing: [], criticalMissing: [], changed: false };
837
+ if (!receipt) return { task, missing: [], criticalMissing: [], adapterAbstained: [], changed: false };
799
838
  const cwd = options.cwd;
800
839
  const changedFiles = uniqueStrings(options.changedFiles ?? task.changedFiles ?? task.observedChangedFiles ?? []);
801
840
  const currentWorkingTreeDigest = options.currentWorkingTreeDigest;
@@ -810,7 +849,7 @@ export function refreshAcceptanceReceipt(task, options = {}) {
810
849
  criterion.evidence = [];
811
850
  const evidence = task?.workingTreeDigestAlgorithm === WORKING_TREE_DIGEST_ALGORITHM
812
851
  && isCurrentWorkingTreeDigest(currentWorkingTreeDigest)
813
- ? evidenceForObligation(criterion.obligation, task, corpus, currentWorkingTreeDigest, criterion)
852
+ ? evidenceForObligation(criterion.obligation, task, corpus, currentWorkingTreeDigest, criterion, cwd)
814
853
  : undefined;
815
854
  if (evidence) addEvidence(criterion, evidence, recordedAt);
816
855
  const nextEvidence = (criterion.evidence ?? []).map(evidenceKey);
@@ -818,17 +857,16 @@ export function refreshAcceptanceReceipt(task, options = {}) {
818
857
  continue;
819
858
  }
820
859
  if (criterion.status === "satisfied") continue;
821
- const evidence = evidenceForObligation(criterion.obligation, task, corpus, currentWorkingTreeDigest, criterion);
860
+ const evidence = evidenceForObligation(criterion.obligation, task, corpus, currentWorkingTreeDigest, criterion, cwd);
822
861
  if (evidence) changed = addEvidence(criterion, evidence, recordedAt) || changed;
823
862
  }
824
863
  const missing = receipt.criteria.filter((criterion) => criterion.status !== "satisfied");
825
- const criticalMissing = missing.filter((criterion) => criterion.priority === "critical");
864
+ const passingVerifier = hasCurrentPassingVerifier(task, currentWorkingTreeDigest);
865
+ const adapterAbstained = missing.filter((criterion) => criterionBehaviorProofDisposition({ obligation: criterion.obligation, task, criterion, taskText: acceptanceTaskText(task), corpus, passingVerifier }) === "unknown");
866
+ const criticalMissing = missing.filter((criterion) => (criterion.priority === "critical" || criterionRequiresBehavioralProof(task, criterion, acceptanceTaskText(task))
867
+ ));
826
868
  return {
827
- task: { ...task, acceptanceReceipt: receipt },
828
- receipt,
829
- missing,
830
- criticalMissing,
831
- changed
869
+ task: { ...task, acceptanceReceipt: receipt }, receipt, missing, criticalMissing, adapterAbstained, changed
832
870
  };
833
871
  }
834
872
 
@@ -850,9 +888,12 @@ export function acceptanceCriticalRecoveryProjection(task, options = {}) {
850
888
  const verifierCurrent = hasCurrentPassingVerifier(task, currentWorkingTreeDigest);
851
889
  const projections = [];
852
890
  for (const criterion of receipt.criteria.filter((item) => item.priority === "critical")) {
891
+ const disposition = criterionBehaviorProofDisposition({ obligation: criterion.obligation, task, criterion, taskText: acceptanceTaskText(task), corpus, passingVerifier: verifierCurrent });
853
892
  const criterionText = (Array.isArray(task?.acceptanceCriteria) ? task.acceptanceCriteria : [])
854
893
  .find((text) => typeof text === "string" && sha256(text) === criterion.hash && !GENERATED_ACCEPTANCE_TEXTS.has(text));
855
- if (!criterionText || evidenceForObligation(criterion.obligation, task, corpus, currentWorkingTreeDigest, criterion)) continue;
894
+ if (!criterionText) continue;
895
+ if (disposition === "unknown") { projections.push({ criterionId: criterion.id, criterionHash: criterion.hash, criterionText: criterionText.slice(0, 700), targets: [], missingDimensions: ["adapter-linkage"], proofHints: ["Add a focused executable test with a direct relative import to the changed source, or configure a deterministic language adapter that resolves this helper/barrel path; then rerun the exact verifier."] }); continue; }
896
+ if (evidenceForObligation(criterion.obligation, task, corpus, currentWorkingTreeDigest, criterion, cwd)) continue;
856
897
  const targets = namedCodeTargets(task, criterion, corpus.sourceText);
857
898
  const missingDimensions = [];
858
899
  if (!verifierCurrent) missingDimensions.push("current-verifier");
@@ -902,7 +943,7 @@ export function acceptanceReceiptValidationErrors(value) {
902
943
  }
903
944
  if (value.helperUsage !== undefined) {
904
945
  const usage = value.helperUsage;
905
- if (!isRecord(usage) || hasUnsupportedField(usage, HELPER_USAGE_FIELDS) || !["off", "recommend", "on"].includes(usage.mode) || typeof usage.used !== "boolean" || !Array.isArray(usage.reasonCodes) || usage.reasonCodes.length > 16 || usage.reasonCodes.some((item) => typeof item !== "string" || compactId(item) !== item) || !Array.isArray(usage.helpers) || usage.helpers.length > 3 || !validTimestamp(usage.recordedAt)) errors.push("acceptanceReceipt helperUsage is invalid");
946
+ if (!isRecord(usage) || hasUnsupportedField(usage, HELPER_USAGE_FIELDS) || !["off", "recommend", "on"].includes(usage.mode) || typeof usage.used !== "boolean" || (usage.decision !== undefined && !["dispatch", "skip"].includes(usage.decision)) || (usage.projectedSavingsRatio !== undefined && usage.projectedSavingsRatio !== null && (typeof usage.projectedSavingsRatio !== "number" || !Number.isFinite(usage.projectedSavingsRatio) || usage.projectedSavingsRatio < -10 || usage.projectedSavingsRatio > 1)) || !Array.isArray(usage.reasonCodes) || usage.reasonCodes.length > 16 || usage.reasonCodes.some((item) => typeof item !== "string" || compactId(item) !== item) || !Array.isArray(usage.helpers) || usage.helpers.length > 3 || !validTimestamp(usage.recordedAt)) errors.push("acceptanceReceipt helperUsage is invalid");
906
947
  else for (const helper of usage.helpers) if (!isRecord(helper) || hasUnsupportedField(helper, HELPER_ENTRY_FIELDS) || !HELPER_ROLES.has(helper.role) || typeof helper.disposition !== "string" || compactId(helper.disposition) !== helper.disposition || !HASH.test(String(helper.requestRef)) || (helper.outputDigest !== null && !HASH.test(String(helper.outputDigest))) || !Number.isInteger(helper.calls) || helper.calls < 0 || helper.calls > 100 || !Number.isInteger(helper.tokens) || helper.tokens < 0 || helper.tokens > 100_000_000) errors.push("acceptanceReceipt helper entry is invalid");
907
948
  }
908
949
  if (!Array.isArray(value.criteria) || value.criteria.length === 0 || value.criteria.length > MAX_CRITERIA) {
@@ -14,6 +14,7 @@ import { contextMetricPartition } from "./context-efficiency-metrics.js";
14
14
  import { writeContextEfficiencyReport } from "./context-efficiency-report.js";
15
15
  import { inspectContextTelemetryIntegrity } from "./context-telemetry-integrity.js";
16
16
  import { directExplicitImportLinks, extractJavaScriptModuleImports } from "./context-import-links.js";
17
+ import { queryPathCandidates } from "./context-query-paths.js";
17
18
  import { redactSensitiveProjectFileText } from "../security/sensitive-data.js";
18
19
  const INDEX_SCHEMA_VERSION = 2;
19
20
  const TELEMETRY_SCHEMA_VERSION = 2;
@@ -492,17 +493,6 @@ function tokenizeQuery(query) {
492
493
  return [...new Set(terms.map((term) => term.toLowerCase()).filter((term) => !STOP_TERMS.has(foldSearchSignal(term))))].slice(0, 16);
493
494
  }
494
495
 
495
- function queryPathCandidates(query) {
496
- const pattern = /(?:^|[\s"'`(])((?:\.{0,2}\/)?[\p{L}\p{N}_.@-]+(?:\/[\p{L}\p{N}_.@-]+)+|[\p{L}\p{N}_.@-]+\.[\p{L}\p{N}]{1,8}|\.[\p{L}\p{N}_@-]{1,32})(?=$|[\s"'`),:])/gu;
497
- const explicitRoots = /^(?:\.github|app|apps|backend|bin|config|docs|examples|frontend|lib|logs|packages|pages|public|scripts|services|spec|src|test|tests|vendor|__tests__)\//i;
498
- return [...String(query ?? "").matchAll(pattern)].flatMap((match) => {
499
- const raw = match[1], candidate = normalizeRelative(raw);
500
- const explicit = candidate && (raw.startsWith("./") || raw.startsWith("../") || explicitRoots.test(candidate)
501
- || candidate.split("/").length >= 3 || /(?:^|\/)(?:\.[^/]+|[^/]+\.[\p{L}\p{N}]{1,8})$/u.test(candidate));
502
- return explicit ? [candidate] : [];
503
- });
504
- }
505
-
506
496
  function ftsQuery(terms) {
507
497
  return terms.map((term) => `"${term.replaceAll('"', '""')}"`).join(" OR ");
508
498
  }
@@ -3,6 +3,7 @@ import fs from "node:fs";
3
3
  import path from "node:path";
4
4
 
5
5
  import { resolveProjectProfileDocument } from "../capabilities/project-profile.js";
6
+ import { WORKSPACE_PRIVATE_STATE_PATTERNS } from "./workspace-evidence-roots.js";
6
7
 
7
8
  const DEFAULT_CONTEXT_INDEX_PATH = ".pi/context-index.json";
8
9
  const CONTEXT_ENGINE_STATE_PATTERN = ".pi/piagent-state/**";
@@ -67,17 +68,20 @@ export function effectiveProtectedPaths(policy = {}, profile = {}) {
67
68
  const contextIndexPath = contextIndexStatePath(profile);
68
69
  return {
69
70
  readProtectedPaths: uniqueStrings([
71
+ ...WORKSPACE_PRIVATE_STATE_PATTERNS,
70
72
  ...baseReadProtectedPaths,
71
73
  ...profileProtectedPaths,
72
74
  contextIndexPath
73
75
  ]),
74
76
  writeProtectedPaths: uniqueStrings([
77
+ ...WORKSPACE_PRIVATE_STATE_PATTERNS,
75
78
  ...policyProtectedPaths,
76
79
  ...profileProtectedPaths,
77
80
  ...readOnlyPaths,
78
81
  contextIndexPath
79
82
  ]),
80
83
  shellProtectedPaths: uniqueStrings([
84
+ ...WORKSPACE_PRIVATE_STATE_PATTERNS,
81
85
  ...baseReadProtectedPaths,
82
86
  ...profileShellProtectedPaths,
83
87
  ...readOnlyPaths,
@@ -0,0 +1,57 @@
1
+ import path from "node:path";
2
+
3
+ function normalizeRelative(value) {
4
+ return String(value ?? "").replaceAll("\\", "/").replace(/^\.\/+/, "").replace(/\/+/g, "/");
5
+ }
6
+
7
+ function safeProjectRelativeQueryPath(raw) {
8
+ const candidate = normalizeRelative(raw);
9
+ if (!candidate || candidate.length > 512 || path.posix.isAbsolute(candidate)) return undefined;
10
+ if (candidate.split("/").some((segment) => !segment || segment === "." || segment === "..")) return undefined;
11
+ return candidate;
12
+ }
13
+
14
+ function safeGlobSegment(segment) {
15
+ let bracketDepth = 0;
16
+ let braceDepth = 0;
17
+ for (const character of segment) {
18
+ if (character === "[") {
19
+ if (++bracketDepth > 1) return false;
20
+ } else if (character === "]") {
21
+ if (--bracketDepth < 0) return false;
22
+ } else if (character === "{") {
23
+ if (++braceDepth > 1) return false;
24
+ } else if (character === "}") {
25
+ if (--braceDepth < 0) return false;
26
+ } else if ((character === "," && braceDepth === 0) || (character === "!" && bracketDepth === 0)) {
27
+ return false;
28
+ }
29
+ }
30
+ return bracketDepth === 0 && braceDepth === 0;
31
+ }
32
+
33
+ function safeProjectRelativeGlob(raw) {
34
+ const candidate = safeProjectRelativeQueryPath(String(raw ?? "").replace(/[.,]+$/u, ""));
35
+ if (!candidate || !candidate.includes("/") || !/[*?\[\]{}]/u.test(candidate)) return undefined;
36
+ if (!/^[\p{L}\p{N}_.@*?{}\[\]!,-]+(?:\/[\p{L}\p{N}_.@*?{}\[\]!,-]+)+$/u.test(candidate)) return undefined;
37
+ return candidate.split("/").every(safeGlobSegment) ? candidate : undefined;
38
+ }
39
+
40
+ export function queryPathCandidates(query) {
41
+ const pattern = /(?:^|[\s"'`(])((?:\.{0,2}\/)?[\p{L}\p{N}_.@-]+(?:\/[\p{L}\p{N}_.@-]+)+|[\p{L}\p{N}_.@-]+\.[\p{L}\p{N}]{1,8}|\.[\p{L}\p{N}_@-]{1,32})(?=$|[\s"'`),:])/gu;
42
+ const globPattern = /(?:^|[\s"'(\x60=,:])((?:\.\/)?[\p{L}\p{N}_.@*?{}\[\]!,-]+(?:\/[\p{L}\p{N}_.@*?{}\[\]!,-]+)+)(?=$|[\s"'\x60),:;])/gu;
43
+ const explicitRoots = /^(?:\.github|app|apps|backend|bin|config|docs|examples|frontend|lib|logs|packages|pages|public|scripts|services|spec|src|test|tests|vendor|__tests__)\//i;
44
+ const text = String(query ?? "");
45
+ const candidates = [...text.matchAll(pattern)].flatMap((match) => {
46
+ const raw = match[1], candidate = safeProjectRelativeQueryPath(raw);
47
+ const explicit = candidate && (raw.startsWith("./") || explicitRoots.test(candidate)
48
+ || candidate.split("/").length >= 3 || /(?:^|\/)(?:\.[^/]+|[^/]+\.[\p{L}\p{N}]{1,8})$/u.test(candidate));
49
+ return explicit ? [{ candidate, index: match.index ?? 0 }] : [];
50
+ });
51
+ for (const match of text.matchAll(globPattern)) {
52
+ const candidate = safeProjectRelativeGlob(match[1]);
53
+ if (candidate) candidates.push({ candidate, index: match.index ?? 0 });
54
+ }
55
+ candidates.sort((left, right) => left.index - right.index);
56
+ return [...new Set(candidates.map((entry) => entry.candidate))];
57
+ }
@@ -36,7 +36,20 @@ function executablePlannedTest(candidate, plannedPaths, plannedSelectionComplete
36
36
  && plannedSelectionComplete
37
37
  && acceptanceLanguageAdapterForPath(candidate.path).disposition === "supported"
38
38
  && !/\.d\.[cm]?ts$/i.test(candidate.path)
39
- && /(?:^|[._-])(?:test|tests|spec)(?:[._-]|$)/i.test(basename);
39
+ && /(?:^|[._-])(?:assertions?|test|tests|spec)(?:[._-]|$)/i.test(basename);
40
+ }
41
+
42
+ function explicitFocusedTestNavigation(text) {
43
+ const clauses = String(text ?? "").split(/(?:[;\n]+|(?<=[.!?])\s+)/).map((clause) => clause.trim()).filter(Boolean);
44
+ return clauses.some((clause) => {
45
+ const positive = /\b(?:add|author|create|include|prove|update|write)\b.{0,80}\b(?:assertions?|coverage|specs?|tests?)\b/i.test(clause)
46
+ || /\b(?:behavioral|deterministic|directly[- ]linked|durable|executable|focused|integration|live|nearest|scoped)\s+(?:assertions?|coverage|specs?|tests?)\b/i.test(clause)
47
+ || /\bensure\b.{0,100}\b(?:cover(?:ed|age)?|test(?:ed|s)?|specs?|assert(?:ed|ions?))\b/i.test(clause)
48
+ || /\bensure\b.{0,80}\b(?:assertions?|coverage|specs?|tests?)\b.{0,60}\bcover(?:ed|s|age)?\b/i.test(clause);
49
+ const negative = /\b(?:do\s+not|don't|dont|never|without)\b.{0,100}\b(?:assertions?|coverage|specs?|tests?)\b/i.test(clause)
50
+ || /\b(?:assertions?|coverage|specs?|tests?)\b.{0,30}\b(?:not|never|unchanged|untouched)\b/i.test(clause);
51
+ return positive && !negative;
52
+ });
40
53
  }
41
54
 
42
55
  const DIRECT_EXPLICIT_LINK_KINDS = new Set(["explicit-imports-candidate", "candidate-imports-explicit"]);
@@ -281,9 +294,9 @@ export function composeCriterionContextEntries(input = {}, options = {}) {
281
294
  // establish singleton cardinality, and only a test-shaped JS/TS filename is
282
295
  // eligible. This remains navigation context; acceptance still depends on a
283
296
  // changed live assertion and final verification.
284
- if (!selected.some((entry) => testPath(entry.path))) {
297
+ if (!selected.some((entry) => testPath(entry.path)) && explicitFocusedTestNavigation(criteriaText)) {
285
298
  const scopedTests = values.filter((candidate) => executablePlannedTest(candidate, plannedPaths, input.plannedSelectionComplete === true));
286
- if (scopedTests.length === 1) add(scopedTests[0], "Sole proven scoped test target (navigation context; not acceptance proof)");
299
+ if (scopedTests.length === 1) add(scopedTests[0], "Operator-requested sole scoped test target (navigation context; not acceptance proof)");
287
300
  }
288
301
 
289
302
  relatedTests
@@ -114,7 +114,10 @@ function targetHintsFor(obligation, scope, mode) {
114
114
  const stem = basename.replace(/\.[a-z0-9]+$/i, "");
115
115
  return text.includes(normalized) || (basename.length >= 3 && text.includes(basename)) || (stem.length >= 4 && text.includes(stem));
116
116
  });
117
- return (matched.length > 0 ? matched : scope).slice(0, 4);
117
+ const requestedTestScopes = /\btests?\b/i.test(text)
118
+ ? scope.filter((candidate) => /(?:^|\/)(?:test|tests|spec|__tests__)(?:\/|$)|[._-](?:test|spec)\.[cm]?[jt]sx?$/i.test(candidate))
119
+ : [];
120
+ return (matched.length > 0 ? [...new Set([...matched, ...requestedTestScopes])] : scope).slice(0, 4);
118
121
  }
119
122
 
120
123
  function criterionLabel(obligation) {
@@ -0,0 +1,101 @@
1
+ import type { BasePolicy, ResolvedOrchestrationPolicy } from "./guard-types.ts";
2
+
3
+ /**
4
+ * Fail-closed policy used only when the packaged JSON policy cannot be read.
5
+ * Tests lock this copy to the file-backed policy so recovery never becomes the
6
+ * less restrictive path.
7
+ */
8
+ export function fallbackBasePolicy(
9
+ contextIndexFile: string,
10
+ orchestrationPolicy: ResolvedOrchestrationPolicy
11
+ ): BasePolicy {
12
+ return {
13
+ protectedPaths: [
14
+ ".git/**", "**/.git/**", "**/.hg/**", "**/.svn/**", "**/.pi/**",
15
+ "**/.aws/**", "**/.azure/**", "**/.docker/**", "**/.gnupg/**", "**/.kube/**", "**/.ssh/**",
16
+ "**/.git-credentials", "**/.netrc", "**/.npmrc", "**/.pypirc",
17
+ "**/auth.json", "**/credential.json", "**/credentials.json", "**/secret.json", "**/secrets.json", "**/token.json", "**/tokens.json",
18
+ "**/.env", "**/.env.*", "**/node_modules/**", "**/dist/**", ".pi/piagent-state/**", ".pi/settings.json",
19
+ ".pi/piagent-profile.json", ".pi/piagent-profile.lock.json", contextIndexFile
20
+ ],
21
+ shellProtectedPaths: [
22
+ ".git/**", "**/.git/**", "**/.hg/**", "**/.svn/**", "**/.pi/**",
23
+ "**/.aws/**", "**/.azure/**", "**/.docker/**", "**/.gnupg/**", "**/.kube/**", "**/.ssh/**",
24
+ "**/.git-credentials", "**/.netrc", "**/.npmrc", "**/.pypirc",
25
+ "**/auth.json", "**/credential.json", "**/credentials.json", "**/secret.json", "**/secrets.json", "**/token.json", "**/tokens.json",
26
+ "**/.env", "**/.env.*", ".pi/piagent-state/**", ".pi/settings.json", ".pi/piagent-profile.json",
27
+ ".pi/piagent-profile.lock.json", contextIndexFile
28
+ ],
29
+ blockedCommandPatterns: ["rm -rf /", "rm -rf ~", "rm -rf $HOME", "git reset --hard", "git clean -fd", "sudo ", "chmod -R 777"],
30
+ requireConfirmationPatterns: ["deploy", "release", "publish", "migration", "gh pr merge", "git push"],
31
+ defaultRequiredContext: ["AGENTS.md", "README.md"],
32
+ permissionProfiles: {
33
+ defaultMode: "workspace-write",
34
+ allowedModes: ["read-only", "workspace-write", "trusted-full-access"]
35
+ },
36
+ execPolicy: {
37
+ defaultMode: "enforce",
38
+ bannedPrefixSuggestions: [
39
+ ["python"], ["python3"], ["node"], ["node", "-e"], ["bash"], ["bash", "-lc"],
40
+ ["sh"], ["sh", "-c"], ["zsh"], ["zsh", "-lc"], ["git"], ["sudo"], ["env"]
41
+ ],
42
+ rules: [{
43
+ id: "prompt-git-add-broad",
44
+ action: "prompt",
45
+ match: "regex",
46
+ value: "(?:^|\\s)git\\s+(?:-C\\s+\\S+\\s+)?add\\s+(?:(?:--all|-A)(?:\\s+(?:\\.|:/))?|--\\s+(?:\\.|:/)|(?:\\.|:/))(?:\\s|$)",
47
+ reason: "Broad git staging can include unrelated or sensitive changes; inspect git status/diff and confirm the exact scope first."
48
+ }]
49
+ },
50
+ contextBudget: {
51
+ defaultMode: "enforce",
52
+ contextDeltaShadow: "sample",
53
+ maxContextFileChars: 50000,
54
+ maxMemoryFileChars: 20000,
55
+ maxManifestFiles: 80,
56
+ warnFragmentChars: 4000
57
+ },
58
+ toolRegistry: {
59
+ defaultMode: "advisory",
60
+ alwaysAllowedTools: [
61
+ "piagent_tools", "piagent_context_engine", "piagent_context", "piagent_permission_status",
62
+ "piagent_exec_policy_check", "piagent_context_budget", "piagent_tool_policy_check",
63
+ "piagent_task_gate_check", "piagent_usage_snapshot", "piagent_orchestration_policy",
64
+ "piagent_memory_status", "piagent_memory_note", "piagent_memory_search",
65
+ "piagent_memory_citation_record", "piagent_context_index_status", "piagent_context_index_record",
66
+ "piagent_context_index_search", "piagent_profile_options", "piagent_profile_apply",
67
+ "piagent_profile_tech_options", "piagent_profile_tech_apply", "piagent_profile_tech_context_record",
68
+ "piagent_project_onboarding_record", "piagent_task_start", "piagent_task_progress",
69
+ "piagent_source_checkout", "piagent_context_record", "piagent_verify_record", "piagent_trace_record"
70
+ ],
71
+ toolCapabilities: {
72
+ bash: ["shell"], shell: ["shell"], exec: ["shell"],
73
+ read: ["filesystem-readonly"], grep: ["filesystem-readonly"], find: ["filesystem-readonly"], ls: ["filesystem-readonly"],
74
+ write: ["filesystem-write"], edit: ["filesystem-write"], apply_patch: ["filesystem-write"],
75
+ browser: ["browser"], github: ["github"]
76
+ }
77
+ },
78
+ externalActionPolicy: {
79
+ defaultMode: "enforce",
80
+ providerKeywords: [
81
+ "github", "gitlab", "bitbucket", "vercel", "netlify", "cloudflare", "aws", "gcp", "azure",
82
+ "slack", "teams", "jira", "linear", "notion", "figma", "stripe", "supabase", "firebase"
83
+ ],
84
+ writeVerbs: [
85
+ "add", "approve", "archive", "assign", "close", "comment", "create", "delete", "deploy",
86
+ "dispatch", "merge", "open", "post", "publish", "push", "release", "remove", "reopen", "run",
87
+ "send", "submit", "trigger", "update", "upload", "write"
88
+ ],
89
+ safeVerbs: ["fetch", "find", "get", "inspect", "list", "read", "search", "show", "view"]
90
+ },
91
+ finalGate: {
92
+ defaultMode: "enforce",
93
+ requireTaskContract: true,
94
+ requireContextManifest: true,
95
+ requireVerifyEvidence: true,
96
+ requireTrace: true,
97
+ requirePassingVerify: true
98
+ },
99
+ orchestrationPolicy
100
+ };
101
+ }