@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
@@ -0,0 +1,91 @@
1
+ import type { ProjectProfile, TaskContract } from "../../extensions/guard-types.ts";
2
+ import { compileCriterionGraph } from "../../extensions/criterion-graph.js";
3
+ import { meaningfulVerificationCommands, selectVerificationPlan } from "../../extensions/verification-intelligence.js";
4
+ import { workingTreeSnapshot, workingTreeSnapshotHasUnavailableEvidence } from "../../extensions/task-state.js";
5
+
6
+ export type PristineTaskVerificationRefresh = {
7
+ task: TaskContract;
8
+ refreshed: boolean;
9
+ reason: string;
10
+ verifyGroup?: string;
11
+ previousCommands: string[];
12
+ nextCommands: string[];
13
+ };
14
+
15
+ function sameStringRecord(left: Record<string, string>, right: Record<string, string>): boolean {
16
+ const leftEntries = Object.entries(left).sort(([leftKey], [rightKey]) => leftKey.localeCompare(rightKey));
17
+ const rightEntries = Object.entries(right).sort(([leftKey], [rightKey]) => leftKey.localeCompare(rightKey));
18
+ return leftEntries.length === rightEntries.length
19
+ && leftEntries.every(([key, value], index) => rightEntries[index]?.[0] === key && rightEntries[index]?.[1] === value);
20
+ }
21
+
22
+ function sameCommands(left: string[], right: string[]): boolean {
23
+ return left.length === right.length && left.every((command, index) => command === right[index]);
24
+ }
25
+
26
+ /**
27
+ * Refresh profile-derived verifier commands only while an automatic task is
28
+ * still byte-for-byte pristine. This lets a corrected workspace profile reach
29
+ * a resumed task without rewriting any source-change evidence or weakening an
30
+ * already-started implementation contract.
31
+ */
32
+ export function refreshPristineRuntimeTaskVerification(
33
+ cwd: string,
34
+ task: TaskContract,
35
+ profile: ProjectProfile,
36
+ updatedAt = new Date().toISOString()
37
+ ): PristineTaskVerificationRefresh {
38
+ const previousCommands = meaningfulVerificationCommands(task.verifyCommands);
39
+ const unchanged = (reason: string): PristineTaskVerificationRefresh => ({
40
+ task, refreshed: false, reason, previousCommands, nextCommands: previousCommands
41
+ });
42
+ if (task.trace.outcome !== "pending" || task.intakeMode !== "runtime" || task.changeMode !== "source-change") {
43
+ return unchanged("task-not-refreshable");
44
+ }
45
+ if ((task.changedFiles?.length ?? 0) > 0 || (task.observedChangedFiles?.length ?? 0) > 0 || (task.verifyEvidence?.length ?? 0) > 0) {
46
+ return unchanged("task-has-runtime-evidence");
47
+ }
48
+ const configuredGroups = profile.verifyCommands && typeof profile.verifyCommands === "object"
49
+ ? profile.verifyCommands
50
+ : {};
51
+ const retainedGroup = task.verifyGroup && Object.hasOwn(configuredGroups, task.verifyGroup)
52
+ ? task.verifyGroup
53
+ : undefined;
54
+ const plan = selectVerificationPlan(profile, retainedGroup, task.changeMode, cwd, task.scope);
55
+ if (plan.error) return unchanged("profile-verifier-invalid");
56
+ const nextCommands = meaningfulVerificationCommands(plan.commands);
57
+ if (nextCommands.length === 0) return unchanged("profile-verifier-unavailable");
58
+ if (sameCommands(previousCommands, nextCommands) && task.verifyGroup === plan.group) {
59
+ return unchanged("profile-verifier-current");
60
+ }
61
+ if (!task.baselineFileDigests || typeof task.baselineFileDigests !== "object" || Array.isArray(task.baselineFileDigests)) {
62
+ return unchanged("baseline-unavailable");
63
+ }
64
+ const baseline = task.baselineFileDigests;
65
+ const current = workingTreeSnapshot(cwd) as Record<string, string>;
66
+ if (workingTreeSnapshotHasUnavailableEvidence(current) || !sameStringRecord(baseline, current)) {
67
+ return unchanged("working-tree-not-pristine");
68
+ }
69
+ const refreshed: TaskContract = {
70
+ ...task,
71
+ verifyGroup: plan.group,
72
+ verifyCommands: nextCommands,
73
+ criterionGraph: compileCriterionGraph({
74
+ acceptanceCriteria: task.acceptanceCriteria,
75
+ scope: task.scope,
76
+ verifyCommands: nextCommands,
77
+ changeMode: task.changeMode,
78
+ mode: task.criterionGraph?.mode ?? "mechanical",
79
+ createdAt: task.criterionGraph?.createdAt ?? task.createdAt
80
+ }) as NonNullable<TaskContract["criterionGraph"]>,
81
+ updatedAt
82
+ };
83
+ return {
84
+ task: refreshed,
85
+ refreshed: true,
86
+ reason: "pristine-profile-verifier-refresh",
87
+ verifyGroup: plan.group,
88
+ previousCommands,
89
+ nextCommands
90
+ };
91
+ }
@@ -29,6 +29,9 @@ export type RecoveryReasonCode =
29
29
  | "provider-transient-retry"
30
30
  | "provider-retry-exhausted"
31
31
  | "permission-expansion-forbidden"
32
+ | "protected-path-forbidden"
33
+ // Legacy persisted recovery records may still carry this value. New runtime
34
+ // decisions never emit it because task scope is advisory.
32
35
  | "scope-replan-required"
33
36
  | "transient-verifier-retry"
34
37
  | "transient-retry-unavailable"
@@ -185,11 +188,11 @@ export function selectRecoveryDecision(input: RecoveryPolicyInput): RecoveryDeci
185
188
  if (input.currentPhase === "terminal") return decision(input, "blocked", ["terminal-phase"]);
186
189
  if (input.currentPhase === "handoff") return decision(input, "handoff", ["handoff-already-observed"]);
187
190
  if (input.classification.category === "passed") return decision(input, "handoff", ["no-failure"]);
188
- // A completion boundary violation cannot be repaired within the immutable
189
- // task contract. Stop before stale-verifier or unknown-diagnostic retries so
190
- // recovery never spends another turn attempting source work outside scope.
191
+ // Protected-path evidence cannot be repaired by another model turn. The
192
+ // failure category also accepts historical scope diagnostics for replay
193
+ // compatibility, but current task scope itself is advisory.
191
194
  if (input.classification.category === "scope-protected-path") {
192
- return decision(input, "handoff", ["scope-replan-required"]);
195
+ return decision(input, "handoff", ["protected-path-forbidden"]);
193
196
  }
194
197
 
195
198
  const counts = countsFor(input);
@@ -2,7 +2,6 @@ import path from "node:path";
2
2
 
3
3
  import type { TaskContract } from "../../extensions/guard-types.ts";
4
4
  import { allVerifyCommandsPassCurrentTree } from "../../extensions/task-contract-view.js";
5
- import { criterionGraphGuidance } from "../../extensions/criterion-graph.js";
6
5
  import { replayTaskCheckpoints, taskRecoveryDecision } from "../../extensions/task-journal.js";
7
6
  import { taskDigestMigrationArchiveStatus, workingTreeSnapshot, workingTreeSnapshotHasUnavailableEvidence } from "../../extensions/task-state.js";
8
7
  import { workingTreeEvidenceDigest } from "../../extensions/task-lifecycle.js";
@@ -10,12 +9,21 @@ import { hashEvidenceCommand } from "../../extensions/runtime-evidence.js";
10
9
  import { WORKING_TREE_DIGEST_ALGORITHM, isCurrentWorkingTreeDigest, workingTreeSnapshotUsesCurrentAlgorithm } from "../../extensions/working-tree-digest.js";
11
10
  import { handoffProjectionPath, readHandoffProjection } from "./handoff-projection.ts";
12
11
  import { inspectTaskAuthorityResumePolicy, type AuthorityResumeDecision } from "../policy/authority-resume-policy.ts";
12
+ import { presentedAcceptanceCriteria, presentedCriterionGraphGuidance } from "../session/task-contract-presentation.ts";
13
+ import { operatorRequestCarryLines } from "../session/operator-request-carry.ts";
13
14
  import { readTrajectoryStore } from "../trajectory/trajectory-store.ts";
14
15
  import { findVerifierFileSnapshot, inspectVerifierStaleness, readVerifierFileSnapshots } from "../inspection/verifier-snapshot-store.ts";
15
16
 
16
17
  export const RESUME_STATE_VERSION = "resume-v1" as const;
17
18
  export const RESUME_CONTEXT_VERSION = "resume-context-v1" as const;
18
- export const RESUME_CONTEXT_MAX_CHARS = 6_000;
19
+ export const RESUME_CONTEXT_MAX_CHARS = 12_000;
20
+ // Validated task text is bounded in Unicode code points by JSON Schema. Astral
21
+ // characters occupy two UTF-16 code units, so the exceptional resume ceiling
22
+ // must cover the worst valid lossless task even though the normal target stays
23
+ // at 12k. This overflow is explicit and is never used for routine compaction.
24
+ export const RESUME_CONTEXT_LOSSLESS_MAX_CHARS = 40_000;
25
+ export const RESUME_CONTEXT_LOSSLESS_OVERFLOW_MARKER = `[Piagent lossless resume overflow: the ${RESUME_CONTEXT_MAX_CHARS}-character target was exceeded; complete authoritative task truth is retained.]`;
26
+ export const LEGACY_TASK_TRUTH_OVERFLOW_MARKER = `[Piagent legacy-task-truth-overflow: automatic continuation is paused because lossless task truth exceeds the ${RESUME_CONTEXT_LOSSLESS_MAX_CHARS}-character exceptional ceiling.]`;
19
27
  type ResumePlanStep = {
20
28
  id: string;
21
29
  title: string;
@@ -153,44 +161,129 @@ function limitedLines(values: unknown[], maximumItems: number, maximumChars: num
153
161
  return selected;
154
162
  }
155
163
 
164
+ function losslessManualResumeContent(task: TaskContract): string {
165
+ return [
166
+ "[Piagent durable task resume]",
167
+ RESUME_CONTEXT_LOSSLESS_OVERFLOW_MARKER,
168
+ `Task: ${compact(task.taskId, 120)} (${compact(task.taskRunId, 120)})`,
169
+ `Goal: ${task.summary}`,
170
+ `Expected output: ${task.expectedOutput}`,
171
+ "Acceptance focus:", ...presentedAcceptanceCriteria(task).map((value, index) => `- C${index + 1}: ${value}`),
172
+ "Exact verifier commands:", ...task.verifyCommands.map((value, index) => `${index + 1}. ${value}`),
173
+ "Continue only under the current runtime-enforced task state and every durable task clause above; do not infer progress from transcript memory."
174
+ ].join("\n");
175
+ }
176
+
177
+ /** Exact UTF-16 traffic size for the smallest lossless automatic legacy resume. */
178
+ export function legacyTaskResumeTruthChars(task: TaskContract): number {
179
+ return operatorRequestCarryLines(task).length > 0 ? 0 : losslessManualResumeContent(task).length;
180
+ }
181
+
182
+ function legacyTaskTruthOverflowsAutomaticResume(task: TaskContract): boolean {
183
+ const chars = legacyTaskResumeTruthChars(task);
184
+ return chars > RESUME_CONTEXT_LOSSLESS_MAX_CHARS;
185
+ }
186
+
187
+ function legacyOverflowResumeContent(task: TaskContract): string {
188
+ return [
189
+ "[Piagent durable task resume]",
190
+ LEGACY_TASK_TRUTH_OVERFLOW_MARKER,
191
+ `Task: ${compact(task.taskId, 120)} (${compact(task.taskRunId, 120)})`,
192
+ "The durable task remains byte-for-byte intact, but its complete acceptance truth cannot be injected safely in one automatic resume.",
193
+ "Do not mutate or auto-continue this task. Ask the operator to re-intake the original request as a new bounded task or provide an explicit compact handoff."
194
+ ].join("\n");
195
+ }
196
+
156
197
  export function buildTaskResumeContext(task: TaskContract, resume: ResumeState): {
157
198
  customType: "piagent-runtime-task-resume";
158
199
  content: string;
159
200
  details: Record<string, unknown>;
160
201
  } {
161
- const criteria = limitedLines(task.acceptanceCriteria, 8, 180, (value, index) => `- C${index + 1}: ${value}`);
202
+ if (legacyTaskTruthOverflowsAutomaticResume(task)) {
203
+ return {
204
+ customType: "piagent-runtime-task-resume",
205
+ content: legacyOverflowResumeContent(task),
206
+ details: {
207
+ resumeContextVersion: RESUME_CONTEXT_VERSION, taskId: task.taskId, taskRunId: task.taskRunId,
208
+ phase: resume.phase, checkpointId: resume.latestCheckpoint?.checkpointId ?? null,
209
+ decision: "blocked", enforcementSafe: false, verifierEvidenceCurrent: resume.verifierEvidenceCurrent,
210
+ staleVerifierEvidence: resume.staleVerifierEvidence, losslessOverflow: true,
211
+ legacyTaskTruthOverflow: true, nextAction: resume.reconstruction.nextAction
212
+ }
213
+ };
214
+ }
215
+ const operatorTruth = operatorRequestCarryLines(task);
216
+ const presentedCriteria = presentedAcceptanceCriteria(task);
217
+ const criteria = operatorTruth.length > 0
218
+ ? limitedLines(presentedCriteria, 8, 180, (value, index) => `- C${index + 1}: ${value}`)
219
+ : presentedCriteria.map((value, index) => `- C${index + 1}: ${value}`);
162
220
  const scope = limitedLines(task.scope, 8, 100, (value) => `- ${value}`);
163
221
  const plan = resume.reconstruction.plan.slice(0, 12).map((step) => (
164
222
  `- ${step.id}: ${step.status}; ${compact(step.title, 140)}${step.dependsOn.length > 0 ? `; after=${step.dependsOn.join(",")}` : ""}`
165
223
  ));
166
224
  if (resume.reconstruction.plan.length > 12) plan.push(`- [${resume.reconstruction.plan.length - 12} more steps retained in the Task Contract]`);
167
- const verifiers = limitedLines(task.verifyCommands, 8, 180, (value, index) => `${index + 1}. ${value}`);
168
- const executionMap = criterionGraphGuidance(task.criterionGraph, 8).map((line: string) => `- ${line}`);
225
+ const verifiers = task.verifyCommands.map((value, index) => `${index + 1}. ${value}`);
226
+ const executionMap = presentedCriterionGraphGuidance(task, 8).map((line: string) => `- ${line}`);
169
227
  const next = resume.reconstruction.nextAction;
170
228
  const lines = [
171
229
  "[Piagent durable task resume]",
172
230
  "This brief is reconstructed from the current Task Contract, journal, trajectory, handoff and working tree. Durable files remain authoritative.",
173
231
  `Task: ${compact(task.taskId, 160)} (${compact(task.taskRunId, 160)})`,
174
- `Goal: ${compact(task.summary, 500)}`,
175
- `Expected output: ${compact(task.expectedOutput, 400)}`,
232
+ `Goal: ${operatorTruth.length > 0 ? compact(task.summary, 500) : task.summary}`,
233
+ ...operatorTruth,
234
+ `Expected output: ${operatorTruth.length > 0 ? compact(task.expectedOutput, 400) : task.expectedOutput}`,
176
235
  "Acceptance focus:", ...criteria,
177
- "Scope:", ...scope,
236
+ "Initial focus (advisory):", ...scope,
237
+ "Initial focus guides retrieval/review and neither authorizes nor forbids mutation.",
178
238
  "Work plan/progress:", ...plan,
179
239
  ...(executionMap.length > 0 ? ["Execution map (planning only):", ...executionMap] : []),
180
240
  `Current phase/checkpoint: ${resume.phase ?? "unknown"} / ${resume.latestCheckpoint?.checkpointId ?? "none"}`,
181
241
  `Verifier state: current=${resume.verifierEvidenceCurrent}; stale=${resume.staleVerifierEvidence}; tree=${resume.currentTreeDigest}`,
182
242
  "Exact verifier commands:", ...verifiers,
183
243
  `Next safe action: ${next.action}${next.stepId ? ` (${next.stepId})` : ""}. ${compact(next.reason, 500)}`,
184
- ...(next.exactCommands.length > 0 ? ["Required commands for that action:", ...limitedLines(next.exactCommands, 8, 180, (value, index) => `${index + 1}. ${value}`)] : []),
244
+ ...(next.exactCommands.length > 0 ? ["Required commands for that action:", ...next.exactCommands.map((value, index) => `${index + 1}. ${value}`)] : []),
185
245
  `Resume safety: decision=${resume.decision}; enforcementSafe=${resume.enforcementSafe}; handoff=${resume.handoff.exists ? resume.handoff.path : "none"}.`,
186
246
  "Do not infer progress from transcript memory, reopen completed steps, or mutate when the next action is inspect-handoff, wait-paused, or terminal."
187
247
  ];
188
248
  let content = lines.join("\n");
189
249
  if (content.length > RESUME_CONTEXT_MAX_CHARS) {
190
- const marker = "\n[Middle details shortened; complete task truth remains in the Task Contract.]\n";
191
- const available = RESUME_CONTEXT_MAX_CHARS - marker.length;
192
- const head = Math.floor(available * 0.7);
193
- content = `${content.slice(0, head).trimEnd()}${marker}${content.slice(-(available - head)).trimStart()}`;
250
+ const boundedOperatorTruth = operatorRequestCarryLines(task);
251
+ content = boundedOperatorTruth.length > 0 ? [
252
+ "[Piagent durable task resume]",
253
+ `Task: ${compact(task.taskId, 160)} (${compact(task.taskRunId, 160)})`,
254
+ ...boundedOperatorTruth,
255
+ "Acceptance focus:", ...limitedLines(presentedAcceptanceCriteria(task), 8, 160, (value, index) => `- C${index + 1}: ${value}`),
256
+ `Current phase/checkpoint: ${resume.phase ?? "unknown"} / ${resume.latestCheckpoint?.checkpointId ?? "none"}`,
257
+ "Exact verifier commands:", ...verifiers,
258
+ `Next safe action: ${next.action}${next.stepId ? ` (${next.stepId})` : ""}. ${compact(next.reason, 400)}`,
259
+ `Resume safety: decision=${resume.decision}; enforcementSafe=${resume.enforcementSafe}.`,
260
+ "Continue from current source. Preserve every operator clause exactly; do not infer progress from transcript memory."
261
+ ].join("\n") : losslessManualResumeContent(task);
262
+ if (content.length > RESUME_CONTEXT_MAX_CHARS && boundedOperatorTruth.length > 0) {
263
+ content = [
264
+ "[Piagent durable task resume]",
265
+ RESUME_CONTEXT_LOSSLESS_OVERFLOW_MARKER,
266
+ `Task: ${compact(task.taskId, 120)} (${compact(task.taskRunId, 120)})`,
267
+ ...boundedOperatorTruth,
268
+ "Acceptance focus:", ...limitedLines(presentedAcceptanceCriteria(task), 12, 220, (value, index) => `- C${index + 1}: ${value}`),
269
+ "Exact verifier commands:", ...verifiers,
270
+ `Next safe action: ${next.action}${next.stepId ? ` (${next.stepId})` : ""}. ${compact(next.reason, 300)}`,
271
+ `Resume safety: decision=${resume.decision}; enforcementSafe=${resume.enforcementSafe}.`,
272
+ "Continue only from current source, the complete operator request, exact verifiers, and durable atomic acceptance facts."
273
+ ].join("\n");
274
+ if (content.length > RESUME_CONTEXT_LOSSLESS_MAX_CHARS) {
275
+ content = [
276
+ "[Piagent durable task resume]",
277
+ RESUME_CONTEXT_LOSSLESS_OVERFLOW_MARKER,
278
+ `Task: ${compact(task.taskId, 120)} (${compact(task.taskRunId, 120)})`,
279
+ ...boundedOperatorTruth,
280
+ "Exact verifier commands:", ...verifiers,
281
+ `Next safe action: ${next.action}${next.stepId ? ` (${next.stepId})` : ""}. ${compact(next.reason, 240)}`,
282
+ `Resume safety: decision=${resume.decision}; enforcementSafe=${resume.enforcementSafe}.`,
283
+ "The complete operator request above is the acceptance source of truth. Continue only from current source and these exact verifiers."
284
+ ].join("\n");
285
+ }
286
+ }
194
287
  }
195
288
  return {
196
289
  customType: "piagent-runtime-task-resume",
@@ -205,6 +298,7 @@ export function buildTaskResumeContext(task: TaskContract, resume: ResumeState):
205
298
  enforcementSafe: resume.enforcementSafe,
206
299
  verifierEvidenceCurrent: resume.verifierEvidenceCurrent,
207
300
  staleVerifierEvidence: resume.staleVerifierEvidence,
301
+ losslessOverflow: content.length > RESUME_CONTEXT_MAX_CHARS,
208
302
  nextAction: resume.reconstruction.nextAction
209
303
  }
210
304
  };
@@ -220,6 +314,7 @@ export function inspectTaskResumeState(
220
314
  const currentTreeDigest = workingTreeEvidenceDigest(currentDigests);
221
315
  const authorityPolicy = inspectTaskAuthorityResumePolicy(cwd, task);
222
316
  const archive = taskDigestMigrationArchiveStatus(cwd, task);
317
+ const legacyTruthOverflow = task.trace.outcome === "pending" && legacyTaskTruthOverflowsAutomaticResume(task);
223
318
  const algorithmReady = task.workingTreeDigestAlgorithm === WORKING_TREE_DIGEST_ALGORITHM
224
319
  && isCurrentWorkingTreeDigest(currentTreeDigest)
225
320
  && workingTreeSnapshotUsesCurrentAlgorithm(currentDigests)
@@ -265,8 +360,9 @@ export function inspectTaskResumeState(
265
360
  if (!trajectory.enforcementSafe) warnings.push(`trajectory state is unsafe: ${trajectory.warnings[0] ?? "unknown error"}`);
266
361
  if (!algorithmReady) warnings.push("working-tree digest algorithm/evidence is not current and proof-capable");
267
362
  if (!archive.valid) warnings.push(archive.reason);
363
+ if (legacyTruthOverflow) warnings.push("legacy-task-truth-overflow requires explicit operator re-intake or compact handoff");
268
364
  if (refreshRequired) warnings.push("legacy evidence was archived; run each exact configured verifier once against the current wt-content-v2 tree");
269
- const enforcementSafe = authorityReady && !identityConflict && algorithmReady && archive.valid && journal.corruptions.length === 0 && trajectory.enforcementSafe && handoffValid;
365
+ const enforcementSafe = authorityReady && !identityConflict && !legacyTruthOverflow && algorithmReady && archive.valid && journal.corruptions.length === 0 && trajectory.enforcementSafe && handoffValid;
270
366
  const recovery = refreshRequired ? { decision: "resume", reason: "Digest migration requires current exact verifier evidence." } : taskRecoveryDecision(task, journal);
271
367
  const decision: ResumeState["decision"] = terminal
272
368
  ? "terminal"
@@ -281,6 +377,8 @@ export function inspectTaskResumeState(
281
377
  : "resume";
282
378
  const reason = terminal
283
379
  ? `Task contract is immutable after ${task.trace.outcome}.`
380
+ : legacyTruthOverflow
381
+ ? "legacy-task-truth-overflow: automatic continuation is paused; ask the operator to re-intake the original request or provide a compact handoff."
284
382
  : journal.corruptions.length > 0
285
383
  ? `task journal is corrupt: ${journal.corruptions[0]}`
286
384
  : !authorityReady
@@ -11,7 +11,6 @@ import { safeTaskId } from "../../extensions/task-state.js";
11
11
  import { isCurrentWorkingTreeDigest } from "../../extensions/working-tree-digest.js";
12
12
 
13
13
  const HASH = /^[a-f0-9]{64}$/;
14
- const GLOB = /[?*[\]{}]/;
15
14
  const MAX_FILE_BYTES = 256 * 1024;
16
15
  export const MAX_SEMANTIC_REPAIR_PATHS = 12;
17
16
  export const MAX_SEMANTIC_REPAIR_REVISIONS = 2;
@@ -106,13 +105,6 @@ function tokenHash(token: string): string {
106
105
  return crypto.createHash("sha256").update(token).digest("hex");
107
106
  }
108
107
 
109
- function exactDeclaredPath(task: TaskContract, target: string): boolean {
110
- return (task.scope ?? []).some((candidate) => {
111
- const normalized = normalizePathCandidate(candidate);
112
- return Boolean(normalized && !GLOB.test(normalized) && normalized === target);
113
- });
114
- }
115
-
116
108
  function isTestPath(file: string): boolean {
117
109
  return /(^|\/)(?:test|tests|spec|__tests__)(\/|$)|[._-](?:test|spec)\.[cm]?[jt]sx?$/i.test(file);
118
110
  }
@@ -146,12 +138,13 @@ export function decideSemanticRepairHandshake(input: {
146
138
  verifierCurrent: boolean;
147
139
  }): SemanticRepairHandshakeDecision {
148
140
  const targets = uniquePaths(input.mutationTargets), delta = new Set(uniquePaths(input.currentDeltaPaths));
141
+ const targetSet = new Set(targets);
149
142
  const empty = { authorized: false, conflictCodes: [], eligibleTargets: [], eligiblePaths: [], pathConflictCodes: {} };
150
143
  if (!input.verifierCurrent || targets.length === 0 || delta.size === 0) return empty;
151
- const candidates = uniquePaths([...delta, ...(input.task.scope ?? []).filter((item) => !GLOB.test(item))]);
144
+ const candidates = uniquePaths([...delta, ...targets]);
152
145
  const pathConflictCodes: Record<string, string[]> = {}, sourceTexts = new Map<string, string>();
153
146
  for (const file of candidates) {
154
- if (isTestPath(file) || (!delta.has(file) && !exactDeclaredPath(input.task, file))) continue;
147
+ if (isTestPath(file) || (!delta.has(file) && !targetSet.has(file))) continue;
155
148
  const source = readSmallFile(input.cwd, file), conflicts = contentLinkedConflictCodes(input.task, source);
156
149
  if (conflicts.length === 0) continue;
157
150
  pathConflictCodes[file] = conflicts;
@@ -161,7 +154,7 @@ export function decideSemanticRepairHandshake(input: {
161
154
  if (sourcePaths.length === 0) return empty;
162
155
  const relatedTests = candidates.filter((file) => (
163
156
  isTestPath(file)
164
- && (delta.has(file) || exactDeclaredPath(input.task, file))
157
+ && (delta.has(file) || targetSet.has(file))
165
158
  && sourcePaths.some((sourcePath) => acceptanceExecutableTestBinding({
166
159
  sourceEntry: { path: sourcePath, text: sourceTexts.get(sourcePath) ?? "" },
167
160
  testEntry: { path: file, text: readSmallFile(input.cwd, file) }
@@ -181,8 +174,9 @@ export function decideSemanticRepairHandshake(input: {
181
174
  * This is intentionally narrower than semantic repair: every target must be
182
175
  * an actual current task delta, must have been observed in this task, and must
183
176
  * still carry successful structured-mutation authorship for the exact task,
184
- * run, and session. The caller separately enforces task scope and CAP-09/12/13
185
- * authority before persisting the reservation.
177
+ * run, and session. The caller separately enforces CAP-09/12/13 and every
178
+ * normal protected-path, permission, approval, and exact-target guard before
179
+ * persisting the reservation; task scope is advisory.
186
180
  */
187
181
  export function decideBoundedRecoveryHandshake(input: {
188
182
  task: TaskContract;
@@ -0,0 +1,217 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ import type { TaskContract } from "../../extensions/guard-types.ts";
5
+ import { readTaskJournal } from "../../extensions/task-journal.js";
6
+ import {
7
+ directChildGitEvidenceRoots,
8
+ normalizedGitPath
9
+ } from "../../extensions/workspace-evidence-roots.js";
10
+ import {
11
+ workingTreeSnapshot,
12
+ workingTreeSnapshotHasUnavailableEvidence
13
+ } from "../../extensions/task-state.js";
14
+ import { workingTreeEvidenceDigest } from "../../extensions/working-tree-digest.js";
15
+ import { readMutationProvenance } from "../inspection/mutation-provenance-store.ts";
16
+ import { readTaskBaselineManifest } from "../inspection/source-evidence-store.ts";
17
+
18
+ // Some filesystems expose timestamps at coarse granularity. Requiring a clear
19
+ // pre-task gap prevents a file created in the same timestamp bucket as task
20
+ // startup from being mistaken for legacy baseline state.
21
+ const LEGACY_EVIDENCE_TIMESTAMP_SAFETY_MS = 2_000;
22
+
23
+ export type TaskEvidenceCoverageRefresh = {
24
+ task: TaskContract;
25
+ refreshed: boolean;
26
+ reason: string;
27
+ addedPaths: string[];
28
+ retainedMutationPaths: string[];
29
+ ambiguousPaths: string[];
30
+ previousBaselineDigest: string;
31
+ nextBaselineDigest: string;
32
+ };
33
+
34
+ function strings(value: unknown): string[] {
35
+ return Array.isArray(value)
36
+ ? [...new Set(value.filter((item): item is string => typeof item === "string" && item.trim().length > 0)
37
+ .map((item) => normalizedGitPath(item).replace(/\/$/, "")))]
38
+ : [];
39
+ }
40
+
41
+ function decodedMutationPath(value: unknown): string | undefined {
42
+ if (typeof value !== "string" || value.length > 8192 || !/^[A-Za-z0-9_-]*$/.test(value)) return undefined;
43
+ try {
44
+ const decoded = Buffer.from(value, "base64url").toString("utf8");
45
+ const normalized = normalizedGitPath(decoded);
46
+ return normalized && normalized !== "." && normalized !== ".." && !normalized.startsWith("../")
47
+ ? normalized
48
+ : undefined;
49
+ } catch {
50
+ return undefined;
51
+ }
52
+ }
53
+
54
+ function journalMutationPaths(cwd: string, taskRunId: string): { paths: string[]; corruptions: string[] } {
55
+ const journal = readTaskJournal(cwd, { taskRunId });
56
+ const paths: string[] = [];
57
+ for (const event of journal.events) {
58
+ if (event.eventType !== "checkpoint" || event.taskRunId !== taskRunId) continue;
59
+ const evidence = event.data?.evidence;
60
+ if (!evidence || typeof evidence !== "object" || Array.isArray(evidence)) continue;
61
+ paths.push(...strings((evidence as Record<string, unknown>).files));
62
+ paths.push(...strings((evidence as Record<string, unknown>).targetPaths));
63
+ }
64
+ return { paths: [...new Set(paths)].sort(), corruptions: journal.corruptions };
65
+ }
66
+
67
+ function provenanceMutationPaths(cwd: string, taskRunId: string): { paths: string[]; corruptions: string[] } {
68
+ const provenance = readMutationProvenance(cwd, taskRunId);
69
+ const paths = provenance.records.flatMap((record) => record.changes
70
+ .map((change) => decodedMutationPath(change.repoPathBase64))
71
+ .filter((item): item is string => Boolean(item)));
72
+ return { paths: [...new Set(paths)].sort(), corruptions: provenance.corruptions };
73
+ }
74
+
75
+ function isLegacyLooseHiddenPath(file: string, childGitPrefixes: string[]): boolean {
76
+ const normalized = normalizedGitPath(file);
77
+ if (!normalized || normalized === "." || normalized === ".." || normalized.startsWith("../")) return false;
78
+ if (childGitPrefixes.some((prefix) => normalized === prefix || normalized.startsWith(`${prefix}/`))) return false;
79
+ return normalized.split("/").some((segment) => segment.startsWith(".") && segment.length > 1);
80
+ }
81
+
82
+ function fileStatePredatesTask(cwd: string, file: string, createdAt: string): boolean {
83
+ const taskStartedAt = Date.parse(createdAt);
84
+ if (!Number.isFinite(taskStartedAt)) return false;
85
+ const absolute = path.resolve(cwd, file);
86
+ const relative = path.relative(cwd, absolute);
87
+ if (!relative || relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) return false;
88
+
89
+ // The legacy inventory has no content digest for this path. Metadata that is
90
+ // older than the task is the only local positive evidence that the exact file
91
+ // state existed before task execution. A symlink, unreadable component, zero
92
+ // timestamp, or any task-time ctime/mtime remains an ordinary task delta.
93
+ let current = cwd;
94
+ try {
95
+ for (const segment of relative.split(path.sep).filter(Boolean)) {
96
+ current = path.join(current, segment);
97
+ const component = fs.lstatSync(current);
98
+ if (component.isSymbolicLink()) return false;
99
+ }
100
+ const stat = fs.lstatSync(absolute);
101
+ if (!stat.isFile() || stat.isSymbolicLink()) return false;
102
+ const timestamps = [stat.birthtimeMs, stat.ctimeMs, stat.mtimeMs];
103
+ const latestSafeTimestamp = taskStartedAt - LEGACY_EVIDENCE_TIMESTAMP_SAFETY_MS;
104
+ return timestamps.every((value) => Number.isFinite(value) && value > 0 && value <= latestSafeTimestamp);
105
+ } catch {
106
+ return false;
107
+ }
108
+ }
109
+
110
+ function manifestProvenBaselinePaths(
111
+ cwd: string,
112
+ task: TaskContract,
113
+ previousBaseline: Record<string, string>,
114
+ current: Record<string, string>,
115
+ candidates: string[]
116
+ ): string[] {
117
+ if (candidates.length === 0) return [];
118
+ try {
119
+ const manifest = readTaskBaselineManifest(cwd, task.taskRunId);
120
+ if (!manifest || manifest.taskId !== task.taskId || manifest.taskRunId !== task.taskRunId) return [];
121
+ const expanded = { ...previousBaseline };
122
+ for (const file of candidates) expanded[file] = current[file];
123
+ return workingTreeEvidenceDigest(expanded) === manifest.baselineTreeDigest ? candidates : [];
124
+ } catch {
125
+ return [];
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Reconcile the one coverage gap introduced when parent-workspace evidence
131
+ * began including hidden project source. Only paths the legacy collector could
132
+ * not see are adopted, and any path with durable mutation evidence remains a
133
+ * real task delta. Existing baseline entries are never rewritten.
134
+ */
135
+ export function refreshLegacyHiddenWorkspaceEvidenceCoverage(
136
+ cwd: string,
137
+ task: TaskContract
138
+ ): TaskEvidenceCoverageRefresh {
139
+ const previousBaseline = task.baselineFileDigests ?? {};
140
+ const previousBaselineDigest = workingTreeEvidenceDigest(previousBaseline);
141
+ const unchanged = (
142
+ reason: string,
143
+ retainedMutationPaths: string[] = [],
144
+ ambiguousPaths: string[] = []
145
+ ): TaskEvidenceCoverageRefresh => ({
146
+ task,
147
+ refreshed: false,
148
+ reason,
149
+ addedPaths: [],
150
+ retainedMutationPaths,
151
+ ambiguousPaths,
152
+ previousBaselineDigest,
153
+ nextBaselineDigest: previousBaselineDigest
154
+ });
155
+
156
+ if (task.trace.outcome !== "pending" || task.changeMode !== "source-change") return unchanged("task-not-refreshable");
157
+ if (!previousBaseline || typeof previousBaseline !== "object" || Array.isArray(previousBaseline)) return unchanged("baseline-unavailable");
158
+ const childGitPrefixes = directChildGitEvidenceRoots(cwd).map((root) => normalizedGitPath(root.prefix)).filter(Boolean);
159
+ if (childGitPrefixes.length === 0) return unchanged("not-parent-multi-repo-workspace");
160
+
161
+ const current = workingTreeSnapshot(cwd) as Record<string, string>;
162
+ if (workingTreeSnapshotHasUnavailableEvidence(current)) return unchanged("current-evidence-unavailable");
163
+ const candidates = Object.keys(current)
164
+ .filter((file) => previousBaseline[file] === undefined && isLegacyLooseHiddenPath(file, childGitPrefixes))
165
+ .sort();
166
+ if (candidates.length === 0) return unchanged("coverage-current");
167
+
168
+ const journal = journalMutationPaths(cwd, task.taskRunId);
169
+ const provenance = provenanceMutationPaths(cwd, task.taskRunId);
170
+ if (journal.corruptions.length > 0 || provenance.corruptions.length > 0) {
171
+ return unchanged("mutation-evidence-unavailable");
172
+ }
173
+ const mutated = new Set([
174
+ ...strings(task.observedChangedFiles),
175
+ ...strings(task.changedFiles),
176
+ ...journal.paths,
177
+ ...provenance.paths
178
+ ]);
179
+ const retainedMutationPaths = candidates.filter((file) => mutated.has(file));
180
+ const unclaimedCandidates = candidates.filter((file) => !mutated.has(file));
181
+ // A current, integrity-validated baseline manifest is stronger than file
182
+ // timestamps: if adding the currently observed missing paths reconstructs
183
+ // its exact task-start tree digest, none of those paths changed after start.
184
+ // Legacy manifests that did not cover the paths simply fail this equality
185
+ // and retain the conservative timestamp fallback.
186
+ const manifestProven = new Set(manifestProvenBaselinePaths(cwd, task, previousBaseline, current, unclaimedCandidates));
187
+ const addedPaths = unclaimedCandidates.filter((file) => manifestProven.has(file) || fileStatePredatesTask(cwd, file, task.createdAt));
188
+ const added = new Set(addedPaths);
189
+ const ambiguousPaths = unclaimedCandidates.filter((file) => !added.has(file));
190
+ if (addedPaths.length === 0) {
191
+ return unchanged(
192
+ ambiguousPaths.length > 0 ? "coverage-candidates-ambiguous" : "coverage-candidates-have-mutation-evidence",
193
+ retainedMutationPaths,
194
+ ambiguousPaths
195
+ );
196
+ }
197
+
198
+ const baselineFileDigests = { ...previousBaseline };
199
+ for (const file of addedPaths) baselineFileDigests[file] = current[file];
200
+ const refreshed: TaskContract = {
201
+ ...task,
202
+ baselineChangedFiles: Object.keys(baselineFileDigests).sort(),
203
+ baselineFileDigests
204
+ };
205
+ return {
206
+ task: refreshed,
207
+ refreshed: true,
208
+ reason: ambiguousPaths.length > 0
209
+ ? "legacy-hidden-workspace-coverage-partially-expanded"
210
+ : "legacy-hidden-workspace-coverage-expanded",
211
+ addedPaths,
212
+ retainedMutationPaths,
213
+ ambiguousPaths,
214
+ previousBaselineDigest,
215
+ nextBaselineDigest: workingTreeEvidenceDigest(baselineFileDigests)
216
+ };
217
+ }
@@ -1,5 +1,6 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import { stageContextDelivery } from "../context/context-delivery.ts";
3
+ import { semanticCompactionCancelled } from "../session/system-prompt.ts";
3
4
 
4
5
  type ExtensionContext = any;
5
6
 
@@ -236,6 +237,19 @@ export function registerContextCommands(pi: ExtensionAPI, deps: Record<string, a
236
237
  function compactCurrentSession(ctx: ExtensionContext): void {
237
238
  const sessionId = ctx.sessionManager.getSessionId();
238
239
  const instructions = semanticCompactionInstructions(ctx.cwd, sessionId);
240
+ if (semanticCompactionCancelled(instructions)) {
241
+ telemetry(ctx, {
242
+ event: "compaction_cancelled",
243
+ mode: "semantic",
244
+ reason: "lossless-task-truth-exceeds-carry-target",
245
+ hasTaskContract: Boolean(compactSessionTask(ctx.cwd, sessionId))
246
+ });
247
+ ctx.ui.notify("Piagent kept the current context because compacting it would lose required task details.", "warning");
248
+ emitRuntimeMessage(ctx, "piagent-context-compaction-cancelled", instructions, {
249
+ reason: "lossless-task-truth-exceeds-carry-target"
250
+ });
251
+ return;
252
+ }
239
253
  telemetry(ctx, {
240
254
  event: "compaction_requested",
241
255
  mode: "semantic",
@@ -428,10 +442,10 @@ export function registerContextCommands(pi: ExtensionAPI, deps: Record<string, a
428
442
  content: [
429
443
  `mode: ${orchestration.defaultMode}`,
430
444
  `helpersMode: ${helpersMode()}`,
431
- `subagents: bounded read-only scout/planning/review; max ${orchestration.maxConcurrentSubagents}`,
445
+ `helpers: max ${orchestration.maxConcurrentSubagents} fresh read-only total; requires >=30% projected net token saving`,
432
446
  `lenses: ${orchestration.defaultReviewLenses.join(", ")}`,
433
447
  `fieldGuide: ${orchestration.fieldGuide.enabled ? `${fieldGuidePath} (${fieldGuideExists ? "exists" : "missing"})` : "off"}`,
434
- "writer: single writer by default; parallel writers need explicit approval + isolation"
448
+ "writer: parent only; helper workers and retries are disabled"
435
449
  ].join("\n"),
436
450
  display: true,
437
451
  details: {