@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.
- package/CHANGELOG.md +49 -0
- package/README.md +17 -14
- package/README.vi.md +26 -1
- package/benchmarks/production-v1/spend-control.v1.json +27 -3
- package/benchmarks/production-v1/suite.json +5 -1
- package/catalog/capabilities.json +16 -16
- package/docs/authority-manifest.md +1 -1
- package/docs/auto-delegation-policy.md +38 -73
- package/docs/command-reference-vietnamese.md +23 -31
- package/docs/context-engine.md +5 -3
- package/docs/context-window-policy.md +37 -1
- package/docs/distribution-standard.md +7 -7
- package/docs/en/architecture.md +26 -13
- package/docs/languages.json +4 -4
- package/docs/operator-manual-vietnamese.md +21 -41
- package/docs/publishing-for-teams.md +5 -0
- package/docs/quality-benchmark.md +179 -23
- package/docs/release-install-policy.md +6 -6
- package/docs/runtime-harness-standard.md +3 -3
- package/docs/security-threat-model.md +1 -1
- package/docs/subagent-orchestration-capabilities.md +19 -51
- package/docs/subagents-and-multiagent.md +21 -47
- package/docs/task-implementation-contract.md +3 -3
- package/docs/task-lifecycle-vietnamese.md +1 -1
- package/docs/team-onboarding.md +2 -2
- package/docs/vi/architecture.md +26 -13
- package/docs-site/architecture.html +1 -1
- package/docs-site/benchmark.html +89 -95
- package/docs-site/commands.html +1 -1
- package/docs-site/content/benchmark.html +88 -94
- package/docs-site/content/en/benchmark.html +134 -90
- package/docs-site/content/en/index.html +19 -19
- package/docs-site/content/en/runtime.html +4 -4
- package/docs-site/content/en/whats-new.html +12 -1
- package/docs-site/content/en/workflows.html +2 -2
- package/docs-site/content/index.html +19 -19
- package/docs-site/content/runtime.html +4 -4
- package/docs-site/content/whats-new.html +12 -1
- package/docs-site/content/workflows.html +2 -2
- package/docs-site/context-engine.html +1 -1
- package/docs-site/en/architecture.html +1 -1
- package/docs-site/en/benchmark.html +135 -91
- package/docs-site/en/commands.html +1 -1
- package/docs-site/en/context-engine.html +1 -1
- package/docs-site/en/index.html +20 -20
- package/docs-site/en/mcp-approval.html +1 -1
- package/docs-site/en/mcp-auth.html +1 -1
- package/docs-site/en/mcp-servers.html +1 -1
- package/docs-site/en/mcp.html +1 -1
- package/docs-site/en/permissions.html +1 -1
- package/docs-site/en/profiles.html +1 -1
- package/docs-site/en/quickstart.html +1 -1
- package/docs-site/en/runtime.html +5 -5
- package/docs-site/en/scope.html +1 -1
- package/docs-site/en/security.html +1 -1
- package/docs-site/en/team.html +1 -1
- package/docs-site/en/whats-new.html +13 -2
- package/docs-site/en/workflows.html +3 -3
- package/docs-site/index.html +20 -20
- package/docs-site/mcp-approval.html +1 -1
- package/docs-site/mcp-auth.html +1 -1
- package/docs-site/mcp-servers.html +1 -1
- package/docs-site/mcp.html +1 -1
- package/docs-site/permissions.html +1 -1
- package/docs-site/profiles.html +1 -1
- package/docs-site/quickstart.html +1 -1
- package/docs-site/runtime.html +5 -5
- package/docs-site/scope.html +1 -1
- package/docs-site/security.html +1 -1
- package/docs-site/team.html +1 -1
- package/docs-site/whats-new.html +13 -2
- package/docs-site/workflows.html +3 -3
- package/evals/fixtures/benchmark-assurance-evidence.invalid.json +1 -1
- package/evals/fixtures/benchmark-assurance-evidence.valid.json +1 -1
- package/evals/fixtures/codex-relative-efficiency-v1.invalid.json +4 -0
- package/evals/fixtures/codex-relative-efficiency-v1.valid.json +94 -0
- package/evals/fixtures/helper-request.invalid.json +1 -1
- package/evals/fixtures/helper-request.valid.json +1 -1
- package/evals/fixtures/piagent-webui/subagent-tree-v1.valid.json +7 -0
- package/evals/fixtures/task-authority-snapshot.valid.json +4 -4
- package/evals/fs4-readiness-matrix.v1.json +2 -2
- package/evals/fs5-causal-arm.v1.json +2 -2
- package/evals/fs5-pilot-protocol.v1.json +3 -3
- package/evals/fs5-pilot-protocol.v2.json +8 -8
- package/evals/fs5-pilot-protocol.v3.json +8 -8
- package/evals/fs5-pilot-protocol.v4.json +6 -6
- package/evals/fs5-pilot-protocol.v5.json +7 -6
- package/evals/private-holdout-v1/public-exposure.v1.json +1 -1
- package/evals/runtime-conformance-v1/README.md +43 -0
- package/evals/runtime-conformance-v1/evaluate-paid-canary.mjs +198 -0
- package/evals/runtime-conformance-v1/lane.json +57 -0
- package/evals/runtime-conformance-v1/paid-canary.json +31 -0
- package/evals/runtime-conformance-v1/runner.mjs +456 -0
- package/package.json +1 -1
- package/packages/piagent-core/README.md +13 -4
- package/packages/piagent-core/benchmark/benchmark-codex-relative-efficiency.js +698 -0
- package/packages/piagent-core/benchmark/benchmark-core.js +4 -3
- package/packages/piagent-core/benchmark/benchmark-forensics.js +4 -2
- package/packages/piagent-core/benchmark/benchmark-preflight.js +3 -1
- package/packages/piagent-core/benchmark/benchmark-provider-free-evidence.js +345 -0
- package/packages/piagent-core/benchmark/benchmark-record-validation.js +74 -5
- package/packages/piagent-core/benchmark/benchmark-report.js +15 -1
- package/packages/piagent-core/benchmark/benchmark-runtime-causal.js +244 -0
- package/packages/piagent-core/benchmark/benchmark-stage-diagnostic.js +201 -4
- package/packages/piagent-core/benchmark/benchmark-suite.js +22 -1
- package/packages/piagent-core/benchmark/benchmark-summary-support.js +4 -0
- package/packages/piagent-core/benchmark/benchmark-usage.js +15 -4
- package/packages/piagent-core/capabilities/runtime-integrity.js +3 -0
- package/packages/piagent-core/extensions/acceptance-behavior-proof.js +806 -0
- package/packages/piagent-core/extensions/acceptance-receipt.js +89 -48
- package/packages/piagent-core/extensions/context-engine.js +1 -11
- package/packages/piagent-core/extensions/context-index-policy.js +4 -0
- package/packages/piagent-core/extensions/context-query-paths.js +57 -0
- package/packages/piagent-core/extensions/criterion-context-pack.js +16 -3
- package/packages/piagent-core/extensions/criterion-graph.js +4 -1
- package/packages/piagent-core/extensions/fallback-base-policy.ts +101 -0
- package/packages/piagent-core/extensions/guard-types.ts +4 -0
- package/packages/piagent-core/extensions/piagent-guard.ts +194 -226
- package/packages/piagent-core/extensions/task-lifecycle.js +32 -0
- package/packages/piagent-core/extensions/task-state.js +69 -8
- package/packages/piagent-core/extensions/verification-intelligence.js +49 -12
- package/packages/piagent-core/extensions/workspace-evidence-roots.js +52 -16
- package/packages/piagent-core/package.json +1 -1
- package/packages/piagent-core/policies/base-policy.json +42 -2
- package/packages/piagent-core/policy/authority-manifest.v1.json +3 -3
- package/packages/piagent-core/prompts/be-to-fe.md +2 -8
- package/packages/piagent-core/prompts/plan.md +1 -1
- package/packages/piagent-core/prompts/platform-improve.md +2 -6
- package/packages/piagent-core/prompts/review.md +2 -7
- package/packages/piagent-core/prompts/scout.md +1 -1
- package/packages/piagent-core/prompts/task.md +4 -3
- package/packages/piagent-core/runtime/hooks/completion-hook.ts +7 -6
- package/packages/piagent-core/runtime/hooks/input-hook.ts +6 -0
- package/packages/piagent-core/runtime/hooks/session-start-hook.ts +100 -1
- package/packages/piagent-core/runtime/hooks/tool-result-hook.ts +58 -72
- package/packages/piagent-core/runtime/hooks/tool-result-value-helpers.ts +66 -0
- package/packages/piagent-core/runtime/inspection/bounded-emission-guard.ts +286 -0
- package/packages/piagent-core/runtime/inspection/runtime-event-store.ts +37 -7
- package/packages/piagent-core/runtime/inspection/task-subagent-tree.ts +27 -4
- package/packages/piagent-core/runtime/inspection/tool-failure-classification.ts +20 -3
- package/packages/piagent-core/runtime/inspection/webui-snapshot.ts +47 -11
- package/packages/piagent-core/runtime/orchestration/helper-lifecycle.ts +16 -12
- package/packages/piagent-core/runtime/orchestration/minimal-delegation-policy.ts +71 -0
- package/packages/piagent-core/runtime/orchestration/owned-work-budget.ts +28 -20
- package/packages/piagent-core/runtime/orchestration/role-policy.ts +27 -9
- package/packages/piagent-core/runtime/orchestration/subagent-tool-policy.ts +329 -0
- package/packages/piagent-core/runtime/policy/block-remedy.ts +0 -4
- package/packages/piagent-core/runtime/product/efficiency-metrics.ts +3 -0
- package/packages/piagent-core/runtime/quality/edit-freshness-guard.ts +102 -0
- package/packages/piagent-core/runtime/quality/exact-output-contract.ts +20 -10
- package/packages/piagent-core/runtime/quality/performance-assurance.ts +3 -3
- package/packages/piagent-core/runtime/recovery/continuation-budget.ts +1 -1
- package/packages/piagent-core/runtime/recovery/handoff-projection.ts +4 -1
- package/packages/piagent-core/runtime/recovery/pristine-task-policy-refresh.ts +91 -0
- package/packages/piagent-core/runtime/recovery/recovery-policy.ts +7 -4
- package/packages/piagent-core/runtime/recovery/resume-state.ts +112 -14
- package/packages/piagent-core/runtime/recovery/semantic-repair-handshake.ts +7 -13
- package/packages/piagent-core/runtime/recovery/task-evidence-coverage-refresh.ts +217 -0
- package/packages/piagent-core/runtime/registration/context-commands.ts +16 -2
- package/packages/piagent-core/runtime/registration/knowledge-tools.ts +9 -5
- package/packages/piagent-core/runtime/registration/operator-request-intake.ts +49 -0
- package/packages/piagent-core/runtime/registration/policy-tools.ts +4 -4
- package/packages/piagent-core/runtime/registration/session-commands.ts +1 -1
- package/packages/piagent-core/runtime/registration/task-completion-tools.ts +12 -9
- package/packages/piagent-core/runtime/registration/task-evidence-tools.ts +21 -12
- package/packages/piagent-core/runtime/registration/task-start-guidance.ts +19 -13
- package/packages/piagent-core/runtime/registration/task-start-manifest.ts +54 -0
- package/packages/piagent-core/runtime/registration/task-start-tool.ts +57 -57
- package/packages/piagent-core/runtime/runtime-limits.ts +16 -2
- package/packages/piagent-core/runtime/session/adaptive-context-analysis.ts +461 -0
- package/packages/piagent-core/runtime/session/adaptive-context-governor.ts +464 -0
- package/packages/piagent-core/runtime/session/adaptive-context-ledger.ts +265 -0
- package/packages/piagent-core/runtime/session/message-signals.ts +7 -3
- package/packages/piagent-core/runtime/session/operator-request-carry.ts +44 -0
- package/packages/piagent-core/runtime/session/runtime-state.ts +11 -7
- package/packages/piagent-core/runtime/session/source-checkout-read-grants.ts +35 -0
- package/packages/piagent-core/runtime/session/system-prompt.ts +76 -14
- package/packages/piagent-core/runtime/session/task-contract-presentation.ts +39 -0
- package/packages/piagent-core/runtime/tools/phase-tools.ts +8 -8
- package/packages/piagent-core/runtime/tools/tool-groups.ts +12 -2
- package/packages/piagent-core/runtime/verification/exact-verifier-reuse.ts +101 -0
- package/packages/piagent-core/runtime/workflows/runtime-intake-compaction.ts +164 -0
- package/packages/piagent-core/runtime/workflows/task-intake.ts +167 -39
- package/packages/piagent-core/subagents/piagent-oracle.md +5 -3
- package/packages/piagent-core/subagents/piagent-planner.md +6 -4
- package/packages/piagent-core/subagents/piagent-reviewer.md +5 -3
- package/packages/piagent-core/subagents/piagent-scout.md +4 -2
- package/packages/piagent-core/subagents/piagent-worker.md +6 -4
- package/packages/piagent-webui/dist/client/assets/{MarkdownMessage-CxeZqTte.js → MarkdownMessage-B4XehTCU.js} +1 -1
- package/packages/piagent-webui/dist/client/assets/{index-DynIMe8I.js → index-CgOEyRPG.js} +4 -4
- package/packages/piagent-webui/dist/client/index.html +1 -1
- package/packages/piagent-webui/extension/piagent-webui.ts +3 -1
- package/packages/piagent-webui/extension/same-session-bridge.ts +1 -1
- package/packages/piagent-webui/extension/session-stream-adapter.ts +33 -10
- package/packages/piagent-webui/gateway/gateway-session-stream.ts +37 -21
- package/packages/piagent-webui/gateway/session-operation-lifecycle.ts +237 -0
- package/packages/piagent-webui/gateway/session-operation-retry-control.ts +38 -0
- package/packages/piagent-webui/gateway/session-operation-watchdog.ts +9 -2
- package/packages/piagent-webui/gateway/session-runtime-supervisor.ts +8 -7
- package/packages/piagent-webui/ownership/session-lease-store.ts +5 -2
- package/packages/piagent-webui/server/core-inspection-provider.ts +2 -1
- package/packages/piagent-webui/server/transcript-projection.ts +91 -5
- package/schemas/codex-relative-efficiency-v1.schema.json +155 -0
- package/schemas/piagent-webui/runtime-event-v2.schema.json +15 -5
- package/schemas/piagent-webui/subagent-tree-v1.schema.json +11 -1
- package/schemas/project-profile.schema.json +1 -1
- package/schemas/task-contract.schema.json +15 -0
- package/scripts/benchmark-runner-core.mjs +14 -6
- package/scripts/benchmark-runner-finalization.mjs +4 -0
- package/scripts/configure-subagents.sh +42 -34
- package/scripts/helper-evaluation.mjs +1 -1
- package/scripts/team-doctor.sh +1 -1
- 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
|
-
//
|
|
189
|
-
//
|
|
190
|
-
//
|
|
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", ["
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
168
|
-
const executionMap =
|
|
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
|
-
|
|
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
|
-
"
|
|
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:", ...
|
|
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
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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, ...
|
|
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) && !
|
|
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) ||
|
|
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
|
|
185
|
-
*
|
|
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
|
-
`
|
|
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:
|
|
448
|
+
"writer: parent only; helper workers and retries are disabled"
|
|
435
449
|
].join("\n"),
|
|
436
450
|
display: true,
|
|
437
451
|
details: {
|